Using Ash Resources as input types

I've occasionally encountered issues using Ash resources as input types to actions of other resources. The most common patterns I've seen to resolve this are to simply turn the inputs into a map and let manage_relationship figure it out, or to take as a parameter only the :uuid and similarly construct a map with just %{id: uuid} for manage_relationship. The culprit in some cases seems to be AshGraphql, which errors following this pattern:
== Compilation error in file lib/app/schema.ex ==
** (RuntimeError) Cannot construct an input type for App.Post
(ash_graphql 0.23.2) lib/resource/resource.ex:3300: AshGraphql.Resource.do_field_type/4
(ash_graphql 0.23.2) lib/resource/resource.ex:866: anonymous fn/6 in AshGraphql.Resource.mutation_fields/4
(elixir 1.14.4) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash_graphql 0.23.2) lib/resource/resource.ex:859: AshGraphql.Resource.mutation_fields/4
(ash_graphql 0.23.2) lib/resource/resource.ex:461: anonymous fn/5 in AshGraphql.Resource.mutations/4
(elixir 1.14.4) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(elixir 1.14.4) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash_graphql 0.23.2) lib/resource/resource.ex:431: AshGraphql.Resource.mutations/4
== Compilation error in file lib/app/schema.ex ==
** (RuntimeError) Cannot construct an input type for App.Post
(ash_graphql 0.23.2) lib/resource/resource.ex:3300: AshGraphql.Resource.do_field_type/4
(ash_graphql 0.23.2) lib/resource/resource.ex:866: anonymous fn/6 in AshGraphql.Resource.mutation_fields/4
(elixir 1.14.4) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash_graphql 0.23.2) lib/resource/resource.ex:859: AshGraphql.Resource.mutation_fields/4
(ash_graphql 0.23.2) lib/resource/resource.ex:461: anonymous fn/5 in AshGraphql.Resource.mutations/4
(elixir 1.14.4) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(elixir 1.14.4) lib/enum.ex:1658: Enum."-map/2-lists^map/1-0-"/2
(ash_graphql 0.23.2) lib/resource/resource.ex:431: AshGraphql.Resource.mutations/4
Internally, there is not a lot of danger to using the :map approach, since manage_relationship still knows what kind of resource it's supposed to be creating and will error accordingly, but it would be quite nice to be able to declare the input type as the resource itself and get it reported in the GQL schema, generated docs, etc.
61 Replies
ZachDaniel
ZachDaniel3y ago
So when you are setting up your managed relationships in the way you described are you getting json inputs for relationships?
\ ឵឵឵
\ ឵឵឵OP3y ago
Sort of, if it's coming through the GraphQL layer they are usually parameterized queries with the arguments provided as JSON. Is that what you mean?
ZachDaniel
ZachDaniel3y ago
Well, I guess a better question is wether or not you are using the managed_relationships config in graphql
\ ឵឵឵
\ ឵឵឵OP3y ago
Nope
ZachDaniel
ZachDaniel3y ago
Ash HQ
Ash.Resource
View the documentation for Ash.Resource on Ash HQ.
ZachDaniel
ZachDaniel3y ago
That will get you better/more specified types for those arguments.
\ ឵឵឵
\ ឵឵឵OP3y ago
Looks like the link should be taking me to a section heading, but it's not. The references to keyword "managed" are only for writable? Does that mean I can just set the relationship to be writable? and I don't need to specify args or do manage_relationship at all and it will be handled by the default write actions?
ZachDaniel
ZachDaniel3y ago
No you still have to have the managed relationship, but you’re configuring it’s representation per write action
\ ឵឵឵
\ ឵឵឵OP3y ago
Gotcha. It kept clearing my selection of AshGraphql each time I refreshed the page, so the section wasn't coming up. So is it preferred then to use {:array, :map} and :map for argument types rather than resources directly? I guess what struck me as odd was that sometimes it seems to be happy with the Ash resource as an argument type specifier.
ZachDaniel
ZachDaniel3y ago
It probably works in cases where the resource in question uses the ash_graphql extension and this defines a graphql type for it And breaks in cases where it doesn’t But even then, it doesn’t really make sense to put an action with arguments like argument :foo, Resource Sorry, doesn’t make sense to use that with ash_graphql Because the graphql client can’t possibly provide that type
\ ឵឵឵
\ ឵឵឵OP3y ago
That would make sense to me, but in this case both resources are in the registry/api/schema.
ZachDaniel
ZachDaniel3y ago
Someone can’t send %Resource{} over graphql So using that type would always fail to cast input
\ ឵឵឵
\ ឵឵឵OP3y ago
Yeah, maybe this is simply a case of what exactly argument means. If it means (perhaps not only this) that the argument is a tagged struct, then for sure.
ZachDaniel
ZachDaniel3y ago
argument :foo, Resource means I accept an instance of the Resource struct
\ ឵឵឵
\ ឵឵឵OP3y ago
Hm. I guess, is there a way to derive an Ash.Type (which is what it wants) from an Ash.Resource?
ZachDaniel
ZachDaniel3y ago
Not all types can be provided by all api layers That is exactly what
managed_relationships do
managed_relationship :action_name, :argument_name
end
managed_relationships do
managed_relationship :action_name, :argument_name
end
is meant to do
\ ឵឵឵
\ ឵឵឵OP3y ago
Right on, makes sense. Is there an equivalent for JSON:API?
ZachDaniel
ZachDaniel3y ago
Its not really necessary for JSON:API, or is less necessary because we can just say "object" in the json schema but in graphql you have to be explicit (otherwise it expects a string as JSON which is ugly)
\ ឵឵឵
\ ឵឵឵OP3y ago
Definitely. Yeah I was thinking JSON:API allows you to specify a nested schema for object.
ZachDaniel
ZachDaniel3y ago
Yeah, I forget wether or not we've set that up but if not its just a matter of making it derive that TBH ash_graphql should just do that by default
\ ឵឵឵
\ ឵឵឵OP3y ago
I was sort of thinking the same, for code_interface it's fine to require the tagged struct, and Ash{Graphql,JsonApi} could use the existing machinery to derive the type/schema.
ZachDaniel
ZachDaniel3y ago
Honestly, you shouldn't use attribute :type, Resource It causes compile time issues and is not a portable type
\ ឵឵឵
\ ឵឵឵OP3y ago
Ah, I'm not, though. It's a belongs_to relationship in this case.
ZachDaniel
ZachDaniel3y ago
oh Sorry I meant argument :type, Resource Like
create :create do
argument :foo, :map
change manage_relationship(:foo, ...)
end
create :create do
argument :foo, :map
change manage_relationship(:foo, ...)
end
You should always use :map or {:array, :map} or :uuid (or w/e the primary key is) The reason is that manage_relationship can call multiple actions, and those actions might take arguments. If you say its a struct, for example, then you'd never be able to include those arguments.
\ ឵឵឵
\ ឵឵឵OP3y ago
Right on, that works for me. Through manage_relationship the same checks will be performed when it tries to turn that map into an instance of the relationship, presumably by passing it to the primary create action.
ZachDaniel
ZachDaniel3y ago
Yeah, exactly. But it can include more than just the create action input as well i.e on_match: :update, on_no_match: :create that would update all matches (i.e if the thing you specified was already related) and create any non-matches
\ ឵឵឵
\ ឵឵឵OP3y ago
Sure, yep. Does it handle nested relationships by default or is it dependant on the create action being called?
ZachDaniel
ZachDaniel3y ago
If the nested actions also manage relationships, then it will manage those too (by virtue of calling the relevant actions)
\ ឵឵឵
\ ឵឵឵OP3y ago
Great, that's what I was hoping. So you still retain control over the chain.
ZachDaniel
ZachDaniel3y ago
Yep! Although keep in mind if you're doing like...large lists of related data doing large lists of related data that it can call many resource actions. Once we introduce bulk actions, the idea is that we will have manage_relationship use those bulk actions to do its work, to remove the N+1 issue there Which will basically make massive data imports with lots of related data actually a reasonable thing to do.
\ ឵឵឵
\ ឵឵឵OP3y ago
Sounds great! I have some stuff that will probably be quite pleased about that :thinkies: I actually was wondering, is there an equivalent to read vs. read_one when defining the actions themselves?
ZachDaniel
ZachDaniel3y ago
read :read do
get? true # This action will only ever return one thing
end
read :read do
get? true # This action will only ever return one thing
end
\ ឵឵឵
\ ឵឵឵OP3y ago
For updates maybe less relevant since it can tell from the input type. Perfect
ZachDaniel
ZachDaniel3y ago
If you use Api.read(...) on that it will still return a list but the code interface will switch to automatically return a single thing
\ ឵឵឵
\ ឵឵឵OP3y ago
But then code_interface will do the right thing Cool
ZachDaniel
ZachDaniel3y ago
yep
\ ឵឵឵
\ ឵឵឵OP3y ago
Thanks mate! One thing that would definitely be nice is if I could omit the action name, like managed_relationship :rel, and have it apply to all actions with the argument and attribute of :rel. Seems like this is already the most common pattern. Lmk if you see any downsides.
ZachDaniel
ZachDaniel3y ago
🤔 Potentially...I'd rather just add a configuration that makes it automatically assume you want to derive those types, and then you only override per action/argument that you want to change the config for
\ ឵឵឵
\ ឵឵឵OP3y ago
Would it then infer the type from the use of the argument in manage_relationship? Otherwise the action body doesn't have the info, since there it's just map.
ZachDaniel
ZachDaniel3y ago
Yeah, thats what it already does
\ ឵឵឵
\ ឵឵឵OP3y ago
And the above seems shaky, what with manual actions etc.
ZachDaniel
ZachDaniel3y ago
managed_relationship :action, :argument figures it out from the call to manage_relationship
\ ឵឵឵
\ ឵឵឵OP3y ago
Ah, I would've thought it looks at the relationship through Ash.Resource.Info.
ZachDaniel
ZachDaniel3y ago
Nope, it can't because the shape of the input is determined by the options given to manage_relationship
\ ឵឵឵
\ ឵឵឵OP3y ago
Sure, right on. I was thinking it just needs to differentiate between singular and plural, which it can get from the argument type. Given that, this sounds great.
ZachDaniel
ZachDaniel3y ago
Well imagine if you had on one resource actions like this:
create :create do
accept []
argument :foo, :string
end

update :update do
accept []
argument :bar, :string
end
create :create do
accept []
argument :foo, :string
end

update :update do
accept []
argument :bar, :string
end
And then you managed it with manage_relationship options like on_match: :update, on_no_match: :create You need to get a type like this:
{
id: ID,
foo: String,
bar: String
}
{
id: ID,
foo: String,
bar: String
}
And you could customize the actions i.e on_match: {:update, :something_else} so we have to synthesize the various actions that could be called
\ ឵឵឵
\ ឵឵឵OP3y ago
Yep, that makes sense, tbh I figured that it was matching the argument and relationship name there, but when we start talking about actions with arguments and accepts down the manage_relationship chain, the derived type needs to be a union of those, and wouldn't necessarily match a type derived from the resource itself at all 😂 Since that machinery is already there, seems like most of the work has been done towards making it automatically derive all argument types unless told otherwise, though, no?
ZachDaniel
ZachDaniel3y ago
Alright, try main and do this:
managed_relationships do
auto? true
end
managed_relationships do
auto? true
end
All arguments with an attached change manage_relationship should have their types automatically figured out
\ ឵឵឵
\ ឵឵឵OP3y ago
Sec Alright, had to weed out all the places where I was using resources as types and it was working (;
2) Once you have done the above (or skipped it because you don't care about the type names),
you can set the following configuration:


config :ash_graphql, :default_managed_relationship_type_name_template, :action_name
2) Once you have done the above (or skipped it because you don't care about the type names),
you can set the following configuration:


config :ash_graphql, :default_managed_relationship_type_name_template, :action_name
I'm getting a lot of this, and then it errors out on a non-unique name deal.
ZachDaniel
ZachDaniel3y ago
Yeah, just put that config in
\ ឵឵឵
\ ឵឵឵OP3y ago
Is there a flag to set that for all actions? Aha, ok
ZachDaniel
ZachDaniel3y ago
oh, yeah its saying literally :action_name
\ ឵឵឵
\ ឵឵឵OP3y ago
😄
ZachDaniel
ZachDaniel3y ago
not like a generic some_action_name 😆
\ ឵឵឵
\ ឵឵឵OP3y ago
Ok, I'm getting only a non-unique now, but a different one. Hmm, that is one of the resources that still has the default create action.
ZachDaniel
ZachDaniel3y ago
can you show me the full error?
\ ឵឵឵
\ ឵឵឵OP3y ago
== Compilation error in file lib/app/schema.ex ==
** (Absinthe.Schema.Error) Compilation failed:
---------------------------------------
## Locations
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:709

Type name "CreateEntityInput" is not unique.

References to types must be unique.

> All types within a GraphQL schema must have unique names. No two provided
> types may have the same name. No provided type may have a name which
> conflicts with any built in types (including Scalar and Introspection
> types).

Reference: https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#type-system
---------------------------------------
## Locations
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:709

Absinthe type identifier :create_entity_input is not unique.

References to types must be unique.

> All types within a GraphQL schema must have unique names. No two provided
> types may have the same name. No provided type may have a name which
> conflicts with any built in types (including Scalar and Introspection
> types).

Reference: https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#type-system


(absinthe 1.7.1) lib/absinthe/schema.ex:392: Absinthe.Schema.__after_compile__/2
(stdlib 4.3) lists.erl:1350: :lists.foldl/3
make: *** [build] Error 1
== Compilation error in file lib/app/schema.ex ==
** (Absinthe.Schema.Error) Compilation failed:
---------------------------------------
## Locations
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:709

Type name "CreateEntityInput" is not unique.

References to types must be unique.

> All types within a GraphQL schema must have unique names. No two provided
> types may have the same name. No provided type may have a name which
> conflicts with any built in types (including Scalar and Introspection
> types).

Reference: https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#type-system
---------------------------------------
## Locations
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:1451
/home/bcksl/app/deps/ash_graphql/lib/resource/resource.ex:709

Absinthe type identifier :create_entity_input is not unique.

References to types must be unique.

> All types within a GraphQL schema must have unique names. No two provided
> types may have the same name. No provided type may have a name which
> conflicts with any built in types (including Scalar and Introspection
> types).

Reference: https://github.com/facebook/graphql/blob/master/spec/Section%203%20--%20Type%20System.md#type-system


(absinthe 1.7.1) lib/absinthe/schema.ex:392: Absinthe.Schema.__after_compile__/2
(stdlib 4.3) lists.erl:1350: :lists.foldl/3
make: *** [build] Error 1
ZachDaniel
ZachDaniel3y ago
🤔 can you delete your _build directory and recompile?
\ ឵឵឵
\ ឵឵឵OP3y ago
Yep, just a minute Same
ZachDaniel
ZachDaniel3y ago
well yeah damn one sec can you try main again?
\ ឵឵឵
\ ឵឵឵OP3y ago
Sure Yep, looks good. Awesome mate, thanks!

Did you find this page helpful?