From 1c232662eacd7a7915c1d15831f84e7e92653013 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Mon, 20 Jan 2025 21:44:26 -0500 Subject: [PATCH] chore: finish up the prior fix on required inputs --- documentation/dsls/DSL-AshGraphql.Resource.md | 36 +- lib/graphql/resolver.ex | 3 +- lib/resource/resource.ex | 175 ++++--- priv/relay_ids.graphql | 164 +++++++ priv/root_level_errors.graphql | 238 +++++++++- priv/schema.graphql | 445 +++++++++++++++++- test/create_test.exs | 48 ++ test/support/resources/comment.ex | 6 + test/support/resources/post.ex | 9 + 9 files changed, 988 insertions(+), 136 deletions(-) diff --git a/documentation/dsls/DSL-AshGraphql.Resource.md b/documentation/dsls/DSL-AshGraphql.Resource.md index e9ad1374..355a61fc 100644 --- a/documentation/dsls/DSL-AshGraphql.Resource.md +++ b/documentation/dsls/DSL-AshGraphql.Resource.md @@ -558,7 +558,7 @@ Generates input objects for `manage_relationship` arguments on resource actions. ### Examples ``` managed_relationships do - manage_relationship :create_post, :comments + managed_relationship :create_post, :comments end ``` @@ -580,36 +580,10 @@ managed_relationship action, argument ``` -Instructs ash_graphql that a given argument with a `manage_relationship` change should have its input objects derived automatically from the potential actions to be called. +Configures the behavior of a given managed_relationship for a given action. -For example, given an action like: - -```elixir -actions do -create :create do -argument :comments, {:array, :map} - -change manage_relationship(:comments, type: :direct_control) # <- we look for this change with a matching argument name -end -end -``` - -You could add the following managed_relationship - -```elixir -graphql do -... - -managed_relationships do -managed_relationship :create, :comments -end -end -``` - -By default, the `{:array, :map}` would simply be a `json[]` type. If the argument name -is placed in this list, all of the potential actions that could be called will be combined -into a single input object. If there are type conflicts (for example, if the input could create -or update a record, and the create and update actions have an argument of the same name but with a different type), +If there are type conflicts (for example, if the input could create or update a record, and the +create and update actions have an argument of the same name but with a different type), a warning is emitted at compile time and the first one is used. If that is insufficient, you will need to do one of the following: 1.) provide the `:types` option to the `managed_relationship` constructor (see that option for more) @@ -625,6 +599,8 @@ For `non_null` use `{:non_null, type}`, and for a list, use `{:array, type}`, fo To *remove* a key from the input object, simply pass `nil` as the type. +Use `ignore?: true` to skip this type generation. + diff --git a/lib/graphql/resolver.ex b/lib/graphql/resolver.ex index 71bb6467..0ef3f351 100644 --- a/lib/graphql/resolver.ex +++ b/lib/graphql/resolver.ex @@ -961,8 +961,9 @@ defmodule AshGraphql.Graphql.Resolver do manage_opts = Spark.Options.validate!(opts[:opts], manage_opts_schema) fields = - manage_opts + resource |> AshGraphql.Resource.manage_fields( + manage_opts, managed_relationship, relationship, __MODULE__ diff --git a/lib/resource/resource.ex b/lib/resource/resource.ex index 5883611f..f1f65b52 100644 --- a/lib/resource/resource.ex +++ b/lib/resource/resource.ex @@ -2157,7 +2157,8 @@ defmodule AshGraphql.Resource do manage_opts = Spark.Options.validate!(opts[:opts], manage_opts_schema) - fields = manage_fields(manage_opts, managed_relationship, relationship, schema) + fields = + manage_fields(resource, manage_opts, managed_relationship, relationship, schema) type = managed_relationship.type_name || default_managed_type_name(resource, action, argument) @@ -2184,7 +2185,7 @@ defmodule AshGraphql.Resource do end @doc false - def manage_fields(manage_opts, managed_relationship, relationship, schema) do + def manage_fields(_resource, manage_opts, managed_relationship, relationship, schema) do [ on_match_fields(manage_opts, relationship, schema), on_no_match_fields(manage_opts, relationship, schema), @@ -2204,15 +2205,7 @@ defmodule AshGraphql.Resource do new_fields = Enum.reduce(field_set, [], fn {resource, action, field}, fields -> if required?(field) do - if Enum.all?(field_sets, fn field_set -> - Enum.all?(field_set, fn {_, _, other_field} -> - other_field.identifier != field.identifier || required?(other_field) - end) - end) do - [{resource, action, field} | fields] - else - [{resource, action, %{field | type: field.type.of_type}} | fields] - end + maybe_unrequire(field_sets, resource, action, field, fields) else [{resource, action, field} | fields] end @@ -2223,6 +2216,18 @@ defmodule AshGraphql.Resource do end end + defp maybe_unrequire(field_sets, resource, action, field, fields) do + if Enum.all?(field_sets, fn field_set -> + Enum.any?(field_set, fn {_, _, other_field} -> + other_field.identifier == field.identifier && required?(other_field) + end) + end) do + [{resource, action, field} | fields] + else + [{resource, action, %{field | type: field.type.of_type}} | fields] + end + end + defp required?(%{type: %Absinthe.Blueprint.TypeReference.NonNull{}}), do: true defp required?(_), do: false @@ -2457,90 +2462,110 @@ defmodule AshGraphql.Resource do opts |> ManagedRelationshipHelpers.on_match_destination_actions(relationship) |> List.wrap() - |> Enum.flat_map(fn + |> Enum.reject(fn + {:join, nil, _} -> + true + {:destination, nil} -> + true + + _ -> + false + end) + |> case do + [] -> :none - {:destination, action_name} -> - action = Ash.Resource.Info.action(relationship.destination, action_name) + actions -> + Enum.flat_map(actions, fn + {:destination, action_name} -> + action = Ash.Resource.Info.action(relationship.destination, action_name) - relationship.destination - |> mutation_fields(schema, action, action.type) - |> Enum.map(fn field -> - {relationship.destination, action.name, field} - end) - |> Enum.reject(fn {_, _, field} -> - field.identifier == relationship.destination_attribute - end) - - {:join, nil, _} -> - :none + relationship.destination + |> mutation_fields(schema, action, action.type) + |> Enum.map(fn field -> + {relationship.destination, action.name, field} + end) + |> Enum.reject(fn {_, _, field} -> + field.identifier == relationship.destination_attribute + end) - {:join, action_name, fields} -> - action = Ash.Resource.Info.action(relationship.through, action_name) + {:join, action_name, fields} -> + action = Ash.Resource.Info.action(relationship.through, action_name) - if fields == :all do - mutation_fields(relationship.through, schema, action, action.type) - else - relationship.through - |> mutation_fields(schema, action, action.type) - |> Enum.filter(&(&1.identifier in fields)) - end - |> Enum.map(fn field -> - {relationship.through, action.name, field} - end) - |> Enum.reject(fn {_, _, field} -> - field.identifier in [ - relationship.destination_attribute_on_join_resource, - relationship.source_attribute_on_join_resource - ] + if fields == :all do + mutation_fields(relationship.through, schema, action, action.type) + else + relationship.through + |> mutation_fields(schema, action, action.type) + |> Enum.filter(&(&1.identifier in fields)) + end + |> Enum.map(fn field -> + {relationship.through, action.name, field} + end) + |> Enum.reject(fn {_, _, field} -> + field.identifier in [ + relationship.destination_attribute_on_join_resource, + relationship.source_attribute_on_join_resource + ] + end) end) - end) + end end defp on_no_match_fields(opts, relationship, schema) do opts |> ManagedRelationshipHelpers.on_no_match_destination_actions(relationship) |> List.wrap() - |> Enum.flat_map(fn + |> Enum.reject(fn + {:join, nil, _} -> + true + {:destination, nil} -> - :none + true - {:destination, action_name} -> - action = Ash.Resource.Info.action(relationship.destination, action_name) + _ -> + false + end) + |> case do + [] -> + :none - relationship.destination - |> mutation_fields(schema, action, action.type) - |> Enum.map(fn field -> - {relationship.destination, action.name, field} - end) - |> Enum.reject(fn {_, _, field} -> - field.identifier == relationship.destination_attribute - end) + actions -> + Enum.flat_map(actions, fn + {:destination, action_name} -> + action = Ash.Resource.Info.action(relationship.destination, action_name) - {:join, nil, _} -> - :none + relationship.destination + |> mutation_fields(schema, action, action.type) + |> Enum.map(fn field -> + {relationship.destination, action.name, field} + end) + |> Enum.reject(fn {_, _, field} -> + field.identifier == relationship.destination_attribute + end) - {:join, action_name, fields} -> - action = Ash.Resource.Info.action(relationship.through, action_name) + {:join, action_name, fields} -> + action = Ash.Resource.Info.action(relationship.through, action_name) - if fields == :all do - mutation_fields(relationship.through, schema, action, action.type) - else - relationship.through - |> mutation_fields(schema, action, action.type) - |> Enum.filter(&(&1.identifier in fields)) - end - |> Enum.map(fn field -> - {relationship.through, action.name, field} - end) - |> Enum.reject(fn {_, _, field} -> - field.identifier in [ - relationship.destination_attribute_on_join_resource, - relationship.source_attribute_on_join_resource - ] + if fields == :all do + mutation_fields(relationship.through, schema, action, action.type) + else + relationship.through + |> mutation_fields(schema, action, action.type) + |> Enum.filter(&(&1.identifier in fields)) + end + |> Enum.map(fn field -> + {relationship.through, action.name, field} + end) + |> Enum.reject(fn {_, _, field} -> + field.identifier in [ + relationship.destination_attribute_on_join_resource, + relationship.source_attribute_on_join_resource + ] + end) end) - end) + end end defp manage_pkey_fields(opts, managed_relationship, relationship, schema) do diff --git a/priv/relay_ids.graphql b/priv/relay_ids.graphql index e1793aa4..b44f71ea 100644 --- a/priv/relay_ids.graphql +++ b/priv/relay_ids.graphql @@ -30,6 +30,17 @@ input CreateUserInput { name: String } +input UserFilterNameTwice { + isNil: Boolean + eq: String + notEq: String + in: [String!] + lessThan: String + greaterThan: String + lessThanOrEqual: String + greaterThanOrEqual: String +} + input UserFilterName { isNil: Boolean eq: String @@ -59,6 +70,7 @@ input UserFilterInput { id: UserFilterId name: UserFilterName posts: PostFilterInput + nameTwice: UserFilterNameTwice } type User implements Node { @@ -77,6 +89,7 @@ type User implements Node { "The number of records to skip." offset: Int ): [Post!]! + nameTwice: String } "The result of the :create_resource mutation" @@ -223,6 +236,7 @@ input PostFilterInput { text: PostFilterText authorId: PostFilterAuthorId author: UserFilterInput + comments: CommentFilterInput } input PostSortInput { @@ -235,6 +249,139 @@ type Post implements Node { text: String authorId: ID author: User + comments( + "How to sort the records in the response" + sort: [CommentSortInput] + + "A filter to limit the results" + filter: CommentFilterInput + + "The number of records to return." + limit: Int + + "The number of records to skip." + offset: Int + ): [Comment!]! +} + +"The result of the :create_comment mutation" +type CreateCommentResult { + "The successful result of the mutation" + result: Comment + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreateCommentInput { + text: String + postId: ID + authorId: ID +} + +enum CommentSortField { + ID + TEXT + TYPE + POST_ID + AUTHOR_ID + TIMESTAMP +} + +input CommentFilterTimestamp { + isNil: Boolean + eq: DateTime + notEq: DateTime + in: [DateTime!] + lessThan: DateTime + greaterThan: DateTime + lessThanOrEqual: DateTime + greaterThanOrEqual: DateTime +} + +input CommentFilterAuthorId { + isNil: Boolean + eq: ID + notEq: ID + in: [ID] + lessThan: ID + greaterThan: ID + lessThanOrEqual: ID + greaterThanOrEqual: ID +} + +input CommentFilterPostId { + isNil: Boolean + eq: ID + notEq: ID + in: [ID] + lessThan: ID + greaterThan: ID + lessThanOrEqual: ID + greaterThanOrEqual: ID +} + +input CommentFilterType { + isNil: Boolean + eq: String + notEq: String + in: [String] + lessThan: String + greaterThan: String + lessThanOrEqual: String + greaterThanOrEqual: String +} + +input CommentFilterText { + isNil: Boolean + eq: String + notEq: String + in: [String] + lessThan: String + greaterThan: String + lessThanOrEqual: String + greaterThanOrEqual: String +} + +input CommentFilterId { + isNil: Boolean + eq: ID + notEq: ID + in: [ID!] + lessThan: ID + greaterThan: ID + lessThanOrEqual: ID + greaterThanOrEqual: ID +} + +input CommentFilterInput { + and: [CommentFilterInput!] + or: [CommentFilterInput!] + not: [CommentFilterInput!] + id: CommentFilterId + text: CommentFilterText + type: CommentFilterType + postId: CommentFilterPostId + authorId: CommentFilterAuthorId + post: PostFilterInput + author: UserFilterInput + timestamp: CommentFilterTimestamp +} + +input CommentSortInput { + order: SortOrder + field: CommentSortField! +} + +type Comment { + id: ID! + text: String + type: String + postId: ID + authorId: ID + post: Post + author: User + timestamp: DateTime } "A relay node" @@ -277,6 +424,14 @@ type RootQueryType { id: ID! ): Node! + listComments( + "How to sort the records in the response" + sort: [CommentSortInput] + + "A filter to limit the results" + filter: CommentFilterInput + ): [Comment!]! + getPost( "The id of the record" id: ID! @@ -316,6 +471,7 @@ type RootQueryType { } type RootMutationType { + createComment(input: CreateCommentInput): CreateCommentResult! simpleCreatePost(input: SimpleCreatePostInput): SimpleCreatePostResult! updatePost(id: ID!, input: UpdatePostInput): UpdatePostResult! assignAuthor(id: ID!, input: AssignAuthorInput): AssignAuthorResult! @@ -331,3 +487,11 @@ character sequences. The Json type is most often used to represent a free-form human-readable json string. """ scalar Json + +""" +The `DateTime` scalar type represents a date and time in the UTC +timezone. The DateTime appears in a JSON response as an ISO8601 formatted +string, including UTC timezone ("Z"). The parsed date and time string will +be converted to UTC if there is an offset. +""" +scalar DateTime diff --git a/priv/root_level_errors.graphql b/priv/root_level_errors.graphql index c7902e91..5f21b3c2 100644 --- a/priv/root_level_errors.graphql +++ b/priv/root_level_errors.graphql @@ -14,6 +14,13 @@ input PostWithCommentsSponsoredCommentsInput { text: String } +input PostWithCommentsLookupCommentsInput { + id: ID + text: String + required: String! + authorId: ID +} + input CreatePostCommentWithTag { id: ID text: String @@ -162,6 +169,12 @@ type IndirectChannelMessages { count: Int! } +type FilterByActorChannelMessages { + results: [Message]! + hasNextPage: Boolean! + count: Int! +} + input CommonMapStructInput { stuff: String! some: String! @@ -231,8 +244,10 @@ type Channel { name: String! createdAt: DateTime! channelMessageCount: Int! + filterByUserChannelMessageCount: Int! directChannelMessages: [Message!] indirectChannelMessages(offset: Int, limit: Int): IndirectChannelMessages + filterByActorChannelMessages(offset: Int, limit: Int): FilterByActorChannelMessages } "The result of the :delete_current_user mutation" @@ -622,10 +637,33 @@ type RelayTag implements Node { name: String } +"The result of the :post_count_with_errors query" +type PostCountWithErrorsResult { + "The successful result of the query" + result: Int + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + input RandomPostInput { published: Boolean } +"The result of the :delete_post_with_invalid_arguments_names mutation" +type DeletePostWithInvalidArgumentsNamesResult { + "The record that was successfully deleted" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input DeletePostWithInvalidArgumentsNamesInput { + invalid1: String + removeInvalid: Boolean +} + "The result of the :delete_post_with_error mutation" type DeletePostWithErrorResult { "The record that was successfully deleted" @@ -690,6 +728,45 @@ input ArchivePostInput { authorId: ID } +"The result of the :update_post_with_invalid_arguments_names mutation" +type UpdatePostWithInvalidArgumentsNamesResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input UpdatePostWithInvalidArgumentsNamesInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID + invalid1: String + removeInvalid: Boolean +} + "The result of the :update_post_with_hidden_input mutation" type UpdatePostWithHiddenInputResult { "The successful result of the mutation" @@ -914,6 +991,45 @@ input UpdatePostInput { authorId: ID } +"The result of the :create_post_with_invalid_arguments_names mutation" +type CreatePostWithInvalidArgumentsNamesResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreatePostWithInvalidArgumentsNamesInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID + invalid1: String + removeInvalid: Boolean +} + type CreatePostWithCustomDescriptionMetadata { foo: String } @@ -997,6 +1113,44 @@ input CreatePostWithCommentsAndTagsInput { tags: [PostWithCommentsAndTagsTagsInput!]! } +"The result of the :create_post_with_comments_lookup mutation" +type CreatePostWithCommentsLookupResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreatePostWithCommentsLookupInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID + comments: [PostWithCommentsLookupCommentsInput!] +} + "The result of the :create_post_with_comments mutation" type CreatePostWithCommentsResult { "The successful result of the mutation" @@ -1226,6 +1380,43 @@ input UpsertPostInput { id: ID } +"The result of the :create_post_with_length_constraint mutation" +type CreatePostWithLengthConstraintResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreatePostWithLengthConstraintInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID +} + "The result of the :create_post mutation" type CreatePostResult { "The successful result of the mutation" @@ -1743,6 +1934,7 @@ input PostFilterInput { createdAt: PostFilterCreatedAt authorId: PostFilterAuthorId author: UserFilterInput + authorThatIsActor: UserFilterInput comments: CommentFilterInput sponsoredComments: SponsoredCommentFilterInput paginatedComments: CommentFilterInput @@ -1819,6 +2011,8 @@ type Post { author: User + authorThatIsActor: User + comments( "How to sort the records in the response" sort: [CommentSortInput] @@ -1854,12 +2048,12 @@ type Post { "A filter to limit the results" filter: CommentFilterInput - "The number of records to return. Maximum 250" - limit: Int! + "The number of records to return." + limit: Int "The number of records to skip." offset: Int - ): PageOfComment! + ): [Comment!]! tags( "How to sort the records in the response" @@ -2167,18 +2361,6 @@ enum CommentSortField { TIMESTAMP } -"A page of :comment" -type PageOfComment { - "Total count on all pages" - count: Int - - "The records contained in the page" - results: [Comment!] - - "Whether or not there is a next page" - hasNextPage: Boolean! -} - input CommentFilterTimestamp { isNil: Boolean eq: DateTime @@ -2498,8 +2680,24 @@ type RootQueryType { offset: Int ): PageOfPost + readPostWithInvalidArgumentsNames( + "How to sort the records in the response" + sort: [PostSortInput] + + "A filter to limit the results" + filter: PostFilterInput + + invalid1: String + + removeInvalid: Boolean + ): [Post!]! + postCount(published: Boolean): Int! + postCountWithErrors(published: Boolean): PostCountWithErrorsResult + + postCustomActionWithInvalidArgumentsNames(invalid1: String, removeInvalid: Boolean): String! + getRelayTag( "The id of the record" id: ID! @@ -2606,6 +2804,8 @@ type RootMutationType { createPost(input: CreatePostInput): CreatePostResult + createPostWithLengthConstraint(input: CreatePostWithLengthConstraintInput): CreatePostWithLengthConstraintResult + upsertPost(input: UpsertPostInput): UpsertPostResult createPostWithCommonMap(input: CreatePostWithCommonMapInput): CreatePostWithCommonMapResult @@ -2618,11 +2818,15 @@ type RootMutationType { createPostWithComments(input: CreatePostWithCommentsInput): CreatePostWithCommentsResult + createPostWithCommentsLookup(input: CreatePostWithCommentsLookupInput): CreatePostWithCommentsLookupResult + createPostWithCommentsAndTags(input: CreatePostWithCommentsAndTagsInput!): CreatePostWithCommentsAndTagsResult "Another custom description" createPostWithCustomDescription(input: CreatePostWithCustomDescriptionInput): CreatePostWithCustomDescriptionResult + createPostWithInvalidArgumentsNames(input: CreatePostWithInvalidArgumentsNamesInput): CreatePostWithInvalidArgumentsNamesResult + updatePost(id: ID!, input: UpdatePostInput): UpdatePostResult updatePostWithComments(id: ID!, input: UpdatePostWithCommentsInput): UpdatePostWithCommentsResult @@ -2635,6 +2839,8 @@ type RootMutationType { updatePostWithHiddenInput(id: ID!, input: UpdatePostWithHiddenInputInput): UpdatePostWithHiddenInputResult + updatePostWithInvalidArgumentsNames(id: ID!, input: UpdatePostWithInvalidArgumentsNamesInput): UpdatePostWithInvalidArgumentsNamesResult + archivePost(id: ID!, input: ArchivePostInput): ArchivePostResult deletePost(id: ID!): DeletePostResult @@ -2643,6 +2849,8 @@ type RootMutationType { deletePostWithError(id: ID!): DeletePostWithErrorResult + deletePostWithInvalidArgumentsNames(id: ID!, input: DeletePostWithInvalidArgumentsNamesInput): DeletePostWithInvalidArgumentsNamesResult + randomPost(input: RandomPostInput): Post createRelayTag(input: CreateRelayTagInput): CreateRelayTagResult diff --git a/priv/schema.graphql b/priv/schema.graphql index 296c4bb3..3d530569 100644 --- a/priv/schema.graphql +++ b/priv/schema.graphql @@ -1,4 +1,5 @@ schema { + subscription: RootSubscriptionType mutation: RootMutationType query: RootQueryType } @@ -130,6 +131,12 @@ type IndirectChannelMessages { count: Int! } +type FilterByActorChannelMessages { + results: [Message]! + hasNextPage: Boolean! + count: Int! +} + input CommonMapStructInput { stuff: String! some: String! @@ -178,6 +185,172 @@ type ConstrainedMap { fooBar: String! } +"The result of the :update_error_handling mutation" +type UpdateErrorHandlingResult { + "The successful result of the mutation" + result: ErrorHandling + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input UpdateErrorHandlingInput { + name: String +} + +"The result of the :create_error_handling mutation" +type CreateErrorHandlingResult { + "The successful result of the mutation" + result: ErrorHandling + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreateErrorHandlingInput { + name: String! +} + +type ErrorHandling { + id: ID! + name: String! +} + +type subscribable_events_with_arguments_result { + created: Subscribable +} + +type deduped_subscribable_events_result { + created: Subscribable + updated: Subscribable + destroyed: ID +} + +type subscribable_events_result { + created: Subscribable + updated: Subscribable + destroyed: ID +} + +type subscribed_on_domain_result { + created: Subscribable +} + +"The result of the :destroy_subscribable mutation" +type DestroySubscribableResult { + "The record that was successfully deleted" + result: Subscribable + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +"The result of the :update_subscribable mutation" +type UpdateSubscribableResult { + "The successful result of the mutation" + result: Subscribable + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input UpdateSubscribableInput { + hiddenField: String + text: String + topic: String + actorId: Int +} + +"The result of the :create_subscribable mutation" +type CreateSubscribableResult { + "The successful result of the mutation" + result: Subscribable + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreateSubscribableInput { + hiddenField: String + text: String + topic: String + actorId: Int +} + +input SubscribableFilterActorId { + isNil: Boolean + eq: Int + notEq: Int + in: [Int] + lessThan: Int + greaterThan: Int + lessThanOrEqual: Int + greaterThanOrEqual: Int +} + +input SubscribableFilterTopic { + isNil: Boolean + eq: String + notEq: String + in: [String] + lessThan: String + greaterThan: String + lessThanOrEqual: String + greaterThanOrEqual: String +} + +input SubscribableFilterText { + isNil: Boolean + eq: String + notEq: String + in: [String] + lessThan: String + greaterThan: String + lessThanOrEqual: String + greaterThanOrEqual: String +} + +input SubscribableFilterHiddenField { + isNil: Boolean + eq: String + notEq: String + in: [String!] + lessThan: String + greaterThan: String + lessThanOrEqual: String + greaterThanOrEqual: String +} + +input SubscribableFilterId { + isNil: Boolean + eq: ID + notEq: ID + in: [ID!] + lessThan: ID + greaterThan: ID + lessThanOrEqual: ID + greaterThanOrEqual: ID +} + +input SubscribableFilterInput { + and: [SubscribableFilterInput!] + or: [SubscribableFilterInput!] + not: [SubscribableFilterInput!] + id: SubscribableFilterId + hiddenField: SubscribableFilterHiddenField + text: SubscribableFilterText + topic: SubscribableFilterTopic + actorId: SubscribableFilterActorId +} + +type Subscribable { + id: ID! + hiddenField: String! + text: String + topic: String + actorId: Int +} + type ImageMessage { id: ID! text: String @@ -219,8 +392,10 @@ type Channel { name: String! createdAt: DateTime! channelMessageCount: Int! + filterByUserChannelMessageCount: Int! directChannelMessages: [Message!] indirectChannelMessages(offset: Int, limit: Int): IndirectChannelMessages + filterByActorChannelMessages(offset: Int, limit: Int): FilterByActorChannelMessages } "The result of the :delete_current_user mutation" @@ -683,10 +858,33 @@ type RelayTag implements Node { name: String } +"The result of the :post_count_with_errors query" +type PostCountWithErrorsResult { + "The successful result of the query" + result: Int + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + input RandomPostInput { published: Boolean } +"The result of the :delete_post_with_invalid_arguments_names mutation" +type DeletePostWithInvalidArgumentsNamesResult { + "The record that was successfully deleted" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input DeletePostWithInvalidArgumentsNamesInput { + invalid1: String + removeInvalid: Boolean +} + "The result of the :delete_post_with_error mutation" type DeletePostWithErrorResult { "The record that was successfully deleted" @@ -751,6 +949,45 @@ input ArchivePostInput { authorId: ID } +"The result of the :update_post_with_invalid_arguments_names mutation" +type UpdatePostWithInvalidArgumentsNamesResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input UpdatePostWithInvalidArgumentsNamesInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID + invalid1: String + removeInvalid: Boolean +} + "The result of the :update_post_with_hidden_input mutation" type UpdatePostWithHiddenInputResult { "The successful result of the mutation" @@ -975,6 +1212,45 @@ input UpdatePostInput { authorId: ID } +"The result of the :create_post_with_invalid_arguments_names mutation" +type CreatePostWithInvalidArgumentsNamesResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreatePostWithInvalidArgumentsNamesInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID + invalid1: String + removeInvalid: Boolean +} + type CreatePostWithCustomDescriptionMetadata { foo: String } @@ -1058,6 +1334,44 @@ input CreatePostWithCommentsAndTagsInput { tags: [PostWithCommentsAndTagsTagsInput!]! } +"The result of the :create_post_with_comments_lookup mutation" +type CreatePostWithCommentsLookupResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreatePostWithCommentsLookupInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID + comments: [PostWithCommentsLookupCommentsInput!] +} + "The result of the :create_post_with_comments mutation" type CreatePostWithCommentsResult { "The successful result of the mutation" @@ -1287,6 +1601,43 @@ input UpsertPostInput { id: ID } +"The result of the :create_post_with_length_constraint mutation" +type CreatePostWithLengthConstraintResult { + "The successful result of the mutation" + result: Post + + "Any errors generated, if the mutation failed" + errors: [MutationError!]! +} + +input CreatePostWithLengthConstraintInput { + text: String + published: Boolean + foo: FooInput + status: Status + statusEnum: StatusEnum + enumWithAshGraphqlDescription: EnumWithAshGraphqlDescription + enumWithAshDescription: EnumWithAshDescription + best: Boolean + score: Float + integerAsStringInDomain: String + embed: PostEmbedInput + text1: String + text2: String + visibility: String + simpleUnion: SimpleUnionInput + embedFoo: PostEmbedFooInput + embedUnionNewTypeList: [FooBarUnnestedInput!] + embedUnionNewType: EmbedUnionNewTypeInput + embedUnionUnnested: FooBarUnnestedInput + stringNewType: String + privateAttribute: Boolean + requiredString: String + commonMapAttribute: CommonMapInput + commonMapStructAttribute: CommonMapStructInput + authorId: ID +} + "The result of the :create_post mutation" type CreatePostResult { "The successful result of the mutation" @@ -1804,6 +2155,7 @@ input PostFilterInput { createdAt: PostFilterCreatedAt authorId: PostFilterAuthorId author: UserFilterInput + authorThatIsActor: UserFilterInput comments: CommentFilterInput sponsoredComments: SponsoredCommentFilterInput paginatedComments: CommentFilterInput @@ -1880,6 +2232,8 @@ type Post { author: User + authorThatIsActor: User + comments( "How to sort the records in the response" sort: [CommentSortInput] @@ -1915,12 +2269,12 @@ type Post { "A filter to limit the results" filter: CommentFilterInput - "The number of records to return. Maximum 250" - limit: Int! + "The number of records to return." + limit: Int "The number of records to skip." offset: Int - ): PageOfComment! + ): [Comment!]! tags( "How to sort the records in the response" @@ -2363,18 +2717,6 @@ enum CommentSortField { TIMESTAMP } -"A page of :comment" -type PageOfComment { - "Total count on all pages" - count: Int - - "The records contained in the page" - results: [Comment!] - - "Whether or not there is a next page" - hasNextPage: Boolean! -} - input CommentFilterTimestamp { isNil: Boolean eq: DateTime @@ -2792,6 +3134,13 @@ input PostWithCommentsSponsoredCommentsInput { text: String } +input PostWithCommentsLookupCommentsInput { + id: ID + text: String + required: String! + authorId: ID +} + input CreatePostCommentWithTag { id: ID text: String @@ -2887,6 +3236,10 @@ type OtherResource { } type RootQueryType { + customGetPost(id: ID!): Post + + customGetPostQuery(id: ID!): Post + getOtherResource( "The id of the record" id: ID! @@ -3102,8 +3455,24 @@ type RootQueryType { offset: Int ): PageOfPost + readPostWithInvalidArgumentsNames( + "How to sort the records in the response" + sort: [PostSortInput] + + "A filter to limit the results" + filter: PostFilterInput + + invalid1: String + + removeInvalid: Boolean + ): [Post!]! + postCount(published: Boolean): Int! + postCountWithErrors(published: Boolean): PostCountWithErrorsResult! + + postCustomActionWithInvalidArgumentsNames(invalid1: String, removeInvalid: Boolean): String! + getRelayTag( "The id of the record" id: ID! @@ -3173,6 +3542,11 @@ type RootQueryType { "The id of the record" id: ID! ): Channel + + getSubscribable( + "The id of the record" + id: ID! + ): Subscribable } type Foo { @@ -3218,6 +3592,8 @@ type RootMutationType { createPost(input: CreatePostInput): CreatePostResult! + createPostWithLengthConstraint(input: CreatePostWithLengthConstraintInput): CreatePostWithLengthConstraintResult! + upsertPost(input: UpsertPostInput): UpsertPostResult! createPostWithCommonMap(input: CreatePostWithCommonMapInput): CreatePostWithCommonMapResult! @@ -3230,11 +3606,15 @@ type RootMutationType { createPostWithComments(input: CreatePostWithCommentsInput): CreatePostWithCommentsResult! + createPostWithCommentsLookup(input: CreatePostWithCommentsLookupInput): CreatePostWithCommentsLookupResult! + createPostWithCommentsAndTags(input: CreatePostWithCommentsAndTagsInput!): CreatePostWithCommentsAndTagsResult! "Another custom description" createPostWithCustomDescription(input: CreatePostWithCustomDescriptionInput): CreatePostWithCustomDescriptionResult! + createPostWithInvalidArgumentsNames(input: CreatePostWithInvalidArgumentsNamesInput): CreatePostWithInvalidArgumentsNamesResult! + updatePost(id: ID!, input: UpdatePostInput): UpdatePostResult! updatePostWithComments(id: ID!, input: UpdatePostWithCommentsInput): UpdatePostWithCommentsResult! @@ -3247,6 +3627,8 @@ type RootMutationType { updatePostWithHiddenInput(id: ID!, input: UpdatePostWithHiddenInputInput): UpdatePostWithHiddenInputResult! + updatePostWithInvalidArgumentsNames(id: ID!, input: UpdatePostWithInvalidArgumentsNamesInput): UpdatePostWithInvalidArgumentsNamesResult! + archivePost(id: ID!, input: ArchivePostInput): ArchivePostResult! deletePost(id: ID!): DeletePostResult! @@ -3255,6 +3637,8 @@ type RootMutationType { deletePostWithError(id: ID!): DeletePostWithErrorResult! + deletePostWithInvalidArgumentsNames(id: ID!, input: DeletePostWithInvalidArgumentsNamesInput): DeletePostWithInvalidArgumentsNamesResult! + randomPost(input: RandomPostInput): Post createRelayTag(input: CreateRelayTagInput): CreateRelayTagResult! @@ -3274,6 +3658,37 @@ type RootMutationType { deleteCurrentUser: DeleteCurrentUserResult! updateChannel(channelId: ID!, input: UpdateChannelInput!): UpdateChannelResult! + + createSubscribable(input: CreateSubscribableInput): CreateSubscribableResult! + + updateSubscribable(id: ID!, input: UpdateSubscribableInput): UpdateSubscribableResult! + + destroySubscribable(id: ID!): DestroySubscribableResult! + + createErrorHandling(input: CreateErrorHandlingInput!): CreateErrorHandlingResult! + + updateErrorHandling(id: ID!, input: UpdateErrorHandlingInput): UpdateErrorHandlingResult! +} + +type RootSubscriptionType { + subscribedOnDomain( + "A filter to limit the results" + filter: SubscribableFilterInput + ): subscribed_on_domain_result + subscribableEvents( + "A filter to limit the results" + filter: SubscribableFilterInput + ): subscribable_events_result + dedupedSubscribableEvents( + "A filter to limit the results" + filter: SubscribableFilterInput + ): deduped_subscribable_events_result + subscribableEventsWithArguments( + "A filter to limit the results" + filter: SubscribableFilterInput + + topic: String! + ): subscribable_events_with_arguments_result } """ diff --git a/test/create_test.exs b/test/create_test.exs index bb0e410f..d719e84c 100644 --- a/test/create_test.exs +++ b/test/create_test.exs @@ -96,6 +96,54 @@ defmodule AshGraphql.CreateTest do } = result end + test "a create with a managed relationship that does a lookup works" do + comment1 = Ash.Seed.seed!(%AshGraphql.Test.Comment{text: "a"}) + + resp = + """ + mutation CreatePostWithCommentsLookup($input: CreatePostWithCommentsLookupInput) { + createPostWithCommentsLookup(input: $input) { + result{ + text + comments(sort:{field:TEXT}){ + text + } + } + errors{ + message + fields + } + } + } + """ + |> Absinthe.run(AshGraphql.Test.Schema, + variables: %{ + "input" => %{ + "text" => "foobar", + "comments" => [ + %{"id" => comment1.id}, + %{"text" => "b", "required" => "foo"} + ] + } + } + ) + + assert {:ok, result} = resp + + refute Map.has_key?(result, :errors) + + assert %{ + data: %{ + "createPostWithCommentsLookup" => %{ + "result" => %{ + "text" => "foobar", + "comments" => [%{"text" => "a"}, %{"text" => "b"}] + } + } + } + } = result + end + test "a union type can be written to" do resp = """ diff --git a/test/support/resources/comment.ex b/test/support/resources/comment.ex index 86f27999..a0ea4d57 100644 --- a/test/support/resources/comment.ex +++ b/test/support/resources/comment.ex @@ -26,6 +26,12 @@ defmodule AshGraphql.Test.Comment do primary?(true) end + create :with_required do + argument(:text, :string, allow_nil?: false) + argument(:required, :string, allow_nil?: false) + change(set_attribute(:text, arg(:text))) + end + read :paginated do pagination(required?: true, offset?: true, countable: true) end diff --git a/test/support/resources/post.ex b/test/support/resources/post.ex index 4c59bd63..795f729e 100644 --- a/test/support/resources/post.ex +++ b/test/support/resources/post.ex @@ -237,6 +237,7 @@ defmodule AshGraphql.Test.Post do create :create_post_bar_with_baz, :create_bar_with_baz create :create_post_with_comments, :with_comments + create :create_post_with_comments_lookup, :with_comments_lookup create :create_post_with_comments_and_tags, :with_comments_and_tags create :create_post_with_custom_description, :create, @@ -358,6 +359,14 @@ defmodule AshGraphql.Test.Post do validate(confirm(:text, :confirmation)) end + create :with_comments_lookup do + argument(:comments, {:array, :map}) + + change( + manage_relationship(:comments, on_lookup: :relate, on_no_match: {:create, :with_required}) + ) + end + create :with_comments do argument(:comments, {:array, :map}) argument(:sponsored_comments, {:array, :map})