This is an automated email from the ASF dual-hosted git repository. danhaywood pushed a commit to branch CAUSEWAY-3676 in repository https://gitbox.apache.org/repos/asf/causeway.git
commit 1657422d0f7ff1722aa1096fd646fb3c70cc6da6 Author: danhaywood <[email protected]> AuthorDate: Tue Jan 30 16:44:28 2024 +0000 CAUSEWAY-3676: updates docs --- .../core/config/CausewayConfiguration.java | 29 ++-- viewers/graphql/adoc/modules/ROOT/pages/about.adoc | 162 ++++++++++++++++++-- .../model/domain/GqlvMutationForProperty.java | 165 +++++++++++++++++++++ .../graphql/model/domain/GqlvPropertySet.java | 1 - ...ayViewerGraphqlTestModuleIntegTestAbstract.java | 5 +- ...Mutating_IntegTest.change_department_name._.gql | 5 +- ...tHeadMutating_IntegTest.create_department._.gql | 5 +- .../StaffMutating_IntegTest.java | 79 ++++++++++ ...Mutating_IntegTest.staff_member_edit_name._.gql | 10 ++ ..._IntegTest.staff_member_edit_name.approved.json | 9 ++ viewers/graphql/test/src/test/resources/schema.gql | 28 ++++ .../integration/GraphQlSourceForCauseway.java | 5 + .../viewer/toplevel/GqlvTopLevelMutation.java | 11 +- 13 files changed, 481 insertions(+), 33 deletions(-) diff --git a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java index f10e28c091..6613c49d1b 100644 --- a/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java +++ b/core/config/src/main/java/org/apache/causeway/core/config/CausewayConfiguration.java @@ -2353,9 +2353,15 @@ public class CausewayConfiguration { * are excluded from the API, as is the ability to set properties. */ QUERY_ONLY, + /** + * Exposes an API with Query for query/safe separate queries and field access, with mutating (idempotent + * and non-idempotent) actions and property setters instead surfaced as Mutations, as per the + * <a href="https://spec.graphql.org/June2018/#sec-Language.Operations">GraphQL spec</a>. + */ + QUERY_AND_MUTATIONS, /** * Exposes only a Query API, but relaxes the rule that system state may not be changed by also including - * idempotent and non-idempotent actions as part of the "query" API. Modifiable properties + * idempotent and non-idempotent actions as part of the schema. Modifiable properties * can also be set. * * <p> @@ -2365,29 +2371,16 @@ public class CausewayConfiguration { * </p> */ QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT, - /** - * Exposes an API with Query for query/safe separate queries and field access, with mutating (idempotent - * and non-idempotent) actions instead surfaced as Mutations, as per the - * <a href="https://spec.graphql.org/June2018/#sec-Language.Operations">GraphQL spec</a>. - * - * <p> - * <b>NOTE</b>: this is not currently implemented. - * </p> - */ - QUERY_AND_MUTATIONS, ; } /** * Which variant of API to expose: {@link ApiVariant#QUERY_ONLY} (which suppresses any actions that mutate the state of the - * system), or alternatively as {@link ApiVariant#QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT} (which does expose actions that mutate the system but within a query, and so is not spec-compliant), or - * as {@link ApiVariant#QUERY_AND_MUTATIONS} (which also exposes actions that mutate the system but as mutations, and so <i>is</i> spec-compliant. - * - * <p> - * <b>NOTE:</b> {@link ApiVariant#QUERY_AND_MUTATIONS} is not currently implemented. - * </p> + * system), or as {@link ApiVariant#QUERY_AND_MUTATIONS} (which additionally exposes actions that mutate the system as mutations) + * or alternatively as {@link ApiVariant#QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT}, a query-only schema that relaxes the read-only rule + * by exposing actions that mutate the system; it is therefore not compliant with the GraphQL spec), */ - private ApiVariant apiVariant = ApiVariant.QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT; + private ApiVariant apiVariant = ApiVariant.QUERY_AND_MUTATIONS; private final MetaData metaData = new MetaData(); @Data diff --git a/viewers/graphql/adoc/modules/ROOT/pages/about.adoc b/viewers/graphql/adoc/modules/ROOT/pages/about.adoc index 9679c0a8ae..dc35227752 100644 --- a/viewers/graphql/adoc/modules/ROOT/pages/about.adoc +++ b/viewers/graphql/adoc/modules/ROOT/pages/about.adoc @@ -13,7 +13,13 @@ The diagram below shows a simple domain footnote:[in fact, this is the domain us image::test-domain.drawio.png[width=600] -To list all Departments: +GraphQL distinguishes queries and mutations, so let's look at each. + +NOTE: GraphQL also defines the notion of subscriptions; the GraphQL viewer currently has no support for these. + +=== Queries + +To list all Departments, we can submit this query: [source,graphql] ---- @@ -68,7 +74,7 @@ For example: <.> whether this action is disabled <.> whether the property of the resultant object is hidden -Similarly, there are fields for: +Similarly, there are fields for action parameters: * `validate` - is the proposed action parameter valid? * `disable` - is the action or action parameter disabled? @@ -76,8 +82,82 @@ Similarly, there are fields for: * `autoComplete` - for an action parameter, is there an auto-complete? * `default` - for an action parameter, is there a default value? +There are also similar fields for properties: + +* `validate` - is the proposed value of the property valid? +* `disable` - is the property disabled? +* `choices` - for a property, are their choices? +* `autoComplete` - for a property , is there an auto-complete? + + +The next section explains how use mutations to change the state of the system. + +=== Mutations + +Actions that mutate the state of the system (with idempotent or non-idempotent xref:refguide:applib:index/annotation/Action.adoc#semantics[@Action#semantics]) are exposed as mutations. +Editable properties are also exposed as mutations. -=== Support for "mutating" queries +IF the action is on a domain service, then the target is implicit; but if the action is on a domain object -- and also for properties -- then the target domain object must be specified. + +For example, to invoke a mutating action on a domain service: + +[source,graphql] +---- +mutation { + university_dept_Departments__createDepartment( #<.> + name: "Geophysics", + deptHead: null + ) { + name { + get + } + } +} +---- +<.> derived from the logical type name of the domain service, and the action Id. + +For example, to invoke a mutating action on a domain object: + +[source,graphql] +---- +mutation { + university_dept_Department__changeName( # <.> + _gqlv_target: {id : "1"}, # <.> + newName: "Classics and Ancient History" + ) { + name { + get + } + } +} +---- +<.> derived from the logical type name of the domain object, and the action Id. +<.> the `_gqlv_target` specifies the target object + + +Or, to set a property on a domain object: + +[source,graphql] +---- +mutation { + university_dept_StaffMember__name( #<.> + _gqlv_target: {id: "1"}, #<.> + name: "Jonathon Gartner" + ) { + name { #<.> + get + } + } +} +---- +<.> derived from the logical type name of the domain object, and the property Id. +<.> the `_gqlv_target` specifies the target object +<.> property setters are `void`, so as a convenience the mutator instead returns the target object. + + + + +=== Mutations using Queries According to the link:https://spec.graphql.org/June2018/#sec-Language.Operations[GraphQL specification], queries should be read-only; they must not change the state of the system. @@ -91,17 +171,79 @@ As specified by xref:refguide:applib:index/annotation/Action.adoc#semantics[@Act * `set` - to modify a property. -It also means that the above fields for supporting methods also apply to properties: +For example, to invoke an action on a domain service -* `validate` - is the proposed value of the property valid? -* `disable` - is the property disabled? -* `choices` - for a property, are their choices? -* `autoComplete` - for a property , is there an auto-complete? +[source,graphql] +---- +{ + university_dept_Staff { + createStaffMember { + invokeNonIdempotent( + name: "Dr. Georgina McGovern", + department: { id: "1"} + ) { + name { + get + } + department { + get { + name { + get + } + } + } + } + } + } +} + +---- + +Or, to find a domain object and then invoke a mutating action on it: + +[source,graphql] +---- +{ + university_dept_DeptHeads { + findHeadByName { + invoke(name: "Prof. Dicky Horwich") { + changeName { + invokeIdempotent(newName: "Prof. Richard Horwich") { + name { + get + } + } + } + } + } + } +} +---- + +Or, similarly to find a domain object and then set a property afterwards: + +[source,graphql] +---- +{ + university_dept_Staff { + findStaffMemberByName { + invoke(name: "Gerry Jones") { + name { + set(name: "Gerald Johns") { + name { + get + } + } + } + } + } + } +} +---- -This relaxed mode is enabled by default, but if you want read-only queries then there is a configuration property, see xref:setup-and-configuration.adoc[]. +This relaxed mode is specified using a configuration property, see xref:setup-and-configuration.adoc[]. -IMPORTANT: GraphQL mutations are currently not yet implemented, and so there is not yet any spec-compliant way to mutate the state of the system. == See also diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMutationForProperty.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMutationForProperty.java new file mode 100644 index 0000000000..1ebcd55b80 --- /dev/null +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMutationForProperty.java @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.viewer.graphql.model.domain; + +import java.util.ArrayList; +import java.util.Map; + +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLOutputType; + +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; + +import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; + +import org.apache.causeway.applib.annotation.Where; +import org.apache.causeway.core.metamodel.consent.InteractionInitiatedBy; +import org.apache.causeway.core.metamodel.object.ManagedObject; +import org.apache.causeway.core.metamodel.spec.ObjectSpecification; +import org.apache.causeway.viewer.graphql.applib.types.TypeMapper; +import org.apache.causeway.viewer.graphql.model.context.Context; +import org.apache.causeway.viewer.graphql.model.exceptions.DisabledException; +import org.apache.causeway.viewer.graphql.model.exceptions.HiddenException; +import org.apache.causeway.viewer.graphql.model.exceptions.InvalidException; + +import lombok.val; +import lombok.extern.log4j.Log4j2; + +@Log4j2 +public class GqlvMutationForProperty { + + private final Holder holder; + private final ObjectSpecification objectSpec; + private final OneToOneAssociation oneToOneAssociation; + private final Context context; + private final GraphQLFieldDefinition field; + private String argumentName; + + public GqlvMutationForProperty( + final Holder holder, + final ObjectSpecification objectSpec, + final OneToOneAssociation oneToOneAssociation, + final Context context) { + this.holder = holder; + this.objectSpec = objectSpec; + this.oneToOneAssociation = oneToOneAssociation; + this.context = context; + + this.argumentName = context.causewayConfiguration.getViewer().getGraphql().getMutation().getTargetArgName(); + + GraphQLOutputType type = context.typeMapper.outputTypeFor(objectSpec); // setter returns void, so will return target instead. + if (type != null) { + val fieldBuilder = newFieldDefinition() + .name(fieldName(objectSpec, oneToOneAssociation)) + .type(type); + addGqlArguments(fieldBuilder); + this.field = holder.addField(fieldBuilder.build()); + } else { + this.field = null; + } + } + + private static String fieldName( + final ObjectSpecification objectSpecification, + final OneToOneAssociation oneToOneAssociation) { + return TypeNames.objectTypeNameFor(objectSpecification) + "__" + oneToOneAssociation.getId(); + } + + + public void addDataFetcher() { + + val beanSort = oneToOneAssociation.getElementType().getBeanSort(); + + switch (beanSort) { + case VALUE: + case VIEW_MODEL: + case ENTITY: + context.codeRegistryBuilder.dataFetcher( + holder.coordinatesFor(field), + this::set); + + break; + } + + context.codeRegistryBuilder.dataFetcher( + holder.coordinatesFor(field), + this::set + ); + } + + private Object set(final DataFetchingEnvironment dataFetchingEnvironment) { + + + Object target = dataFetchingEnvironment.getArgument(argumentName); + Object sourcePojo = GqlvAction.asPojo(objectSpec, target, context.bookmarkService) + .orElseThrow(); // TODO: better error handling if no such object found. + + val managedObject = ManagedObject.adaptSingular(objectSpec, sourcePojo); + + Map<String, Object> arguments = dataFetchingEnvironment.getArguments(); + Object argumentValue = arguments.get(oneToOneAssociation.getId()); + ManagedObject argumentManagedObject = ManagedObject.adaptProperty(oneToOneAssociation, argumentValue); + + val visibleConsent = oneToOneAssociation.isVisible(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); + if (visibleConsent.isVetoed()) { + throw new HiddenException(oneToOneAssociation.getFeatureIdentifier()); + } + + val usableConsent = oneToOneAssociation.isUsable(managedObject, InteractionInitiatedBy.USER, Where.ANYWHERE); + if (usableConsent.isVetoed()) { + throw new DisabledException(oneToOneAssociation.getFeatureIdentifier()); + } + + val validityConsent = oneToOneAssociation.isAssociationValid(managedObject, argumentManagedObject, InteractionInitiatedBy.USER); + if (validityConsent.isVetoed()) { + throw new InvalidException(validityConsent); + } + + oneToOneAssociation.set(managedObject, argumentManagedObject, InteractionInitiatedBy.USER); + + return managedObject; // return the original object because setters return void + } + + + private void addGqlArguments(final GraphQLFieldDefinition.Builder fieldBuilder) { + + // add target + val targetArgName = context.causewayConfiguration.getViewer().getGraphql().getMutation().getTargetArgName(); + fieldBuilder.argument( + GraphQLArgument.newArgument() + .name(targetArgName) + .type(context.typeMapper.inputTypeFor(objectSpec)) + .build() + ); + + fieldBuilder.argument( + GraphQLArgument.newArgument() + .name(oneToOneAssociation.getId()) + .type(context.typeMapper.inputTypeFor(oneToOneAssociation, TypeMapper.InputContext.INVOKE)) + .build()); + } + + + public interface Holder + extends GqlvHolder { + } + +} diff --git a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertySet.java b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertySet.java index d2659adcd0..762ba7a081 100644 --- a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertySet.java +++ b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertySet.java @@ -96,7 +96,6 @@ public class GqlvPropertySet { Object set(final DataFetchingEnvironment dataFetchingEnvironment) { - val sourcePojo = BookmarkedPojo.sourceFrom(dataFetchingEnvironment); val sourcePojoClass = sourcePojo.getClass(); diff --git a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/CausewayViewerGraphqlTestModuleIntegTestAbstract.java b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/CausewayViewerGraphqlTestModuleIntegTestAbstract.java index 9f9fb857b3..227a2eebea 100644 --- a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/CausewayViewerGraphqlTestModuleIntegTestAbstract.java +++ b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/CausewayViewerGraphqlTestModuleIntegTestAbstract.java @@ -76,7 +76,10 @@ import lombok.val; classes = { CausewayViewerGraphqlTestModuleIntegTestAbstract.TestApp.class }, - webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "causeway.viewer.graphql.api-variant=QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT" + } ) @AutoConfigureHttpGraphQlTester @TestInstance(TestInstance.Lifecycle.PER_CLASS) diff --git a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.change_department_name._.gql b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.change_department_name._.gql index 45e4992450..2d140978b5 100644 --- a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.change_department_name._.gql +++ b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.change_department_name._.gql @@ -1,5 +1,8 @@ mutation { - university_dept_Department__changeName(_gqlv_target: {id : "$departmentId"}, newName: "Classics and Ancient History") { + university_dept_Department__changeName( + _gqlv_target: {id : "$departmentId"}, + newName: "Classics and Ancient History" + ) { name { get } diff --git a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.create_department._.gql b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.create_department._.gql index a2dc6052c2..8521588968 100644 --- a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.create_department._.gql +++ b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/DeptHeadMutating_IntegTest.create_department._.gql @@ -1,5 +1,8 @@ mutation { - university_dept_Departments__createDepartment(name: "Geophysics", deptHead: null) { + university_dept_Departments__createDepartment( + name: "Geophysics", + deptHead: null + ) { name { get } diff --git a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.java b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.java new file mode 100644 index 0000000000..a59c89fd4d --- /dev/null +++ b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.causeway.viewer.graphql.viewer.test.e2e.query_and_mutations; + +import java.util.Optional; + +import org.apache.causeway.viewer.graphql.viewer.test.CausewayViewerGraphqlTestModuleIntegTestAbstract; + +import org.approvaltests.Approvals; +import org.approvaltests.reporters.DiffReporter; +import org.approvaltests.reporters.UseReporter; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Propagation; + +import org.apache.causeway.applib.services.bookmark.Bookmark; +import org.apache.causeway.commons.internal.collections._Maps; +import org.apache.causeway.viewer.graphql.viewer.test.domain.dept.Department; +import org.apache.causeway.viewer.graphql.viewer.test.domain.dept.StaffMember; +import org.apache.causeway.viewer.graphql.viewer.test.e2e.Abstract_IntegTest; + +import lombok.val; + + +// NOT USING @Transactional since we are running server within same transaction otherwise +@SpringBootTest( + classes = { + CausewayViewerGraphqlTestModuleIntegTestAbstract.TestApp.class + }, + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = { + "causeway.viewer.graphql.api-variant=QUERY_AND_MUTATIONS" + } +) +@Order(60) +@ActiveProfiles("test") +public class StaffMutating_IntegTest extends Abstract_IntegTest { + + @Test + @UseReporter(DiffReporter.class) + void staff_member_edit_name() throws Exception { + + final Bookmark bookmark = + transactionService.callTransactional( + Propagation.REQUIRED, + () -> { + StaffMember staffMember = staffMemberRepository.findByName("John Gartner"); + return bookmarkService.bookmarkFor(staffMember).orElseThrow(); + } + ).valueAsNonNullElseFail(); + + val response = submit(_Maps.unmodifiable("$staffMemberId", bookmark.getIdentifier())); + + // then payload + Approvals.verify(response, jsonOptions()); + } +} diff --git a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.staff_member_edit_name._.gql b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.staff_member_edit_name._.gql new file mode 100644 index 0000000000..c251ee695e --- /dev/null +++ b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.staff_member_edit_name._.gql @@ -0,0 +1,10 @@ +mutation { + university_dept_StaffMember__name( + _gqlv_target: {id: "$staffMemberId"}, + name: "Jonathon Gartner" + ) { + name { + get + } + } +} diff --git a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.staff_member_edit_name.approved.json b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.staff_member_edit_name.approved.json new file mode 100644 index 0000000000..9e142d65fd --- /dev/null +++ b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/query_and_mutations/StaffMutating_IntegTest.staff_member_edit_name.approved.json @@ -0,0 +1,9 @@ +{ + "data" : { + "university_dept_StaffMember__name" : { + "name" : { + "get" : "Jonathon Gartner" + } + } + } +} \ No newline at end of file diff --git a/viewers/graphql/test/src/test/resources/schema.gql b/viewers/graphql/test/src/test/resources/schema.gql index 5f6c0fe828..94892a724d 100644 --- a/viewers/graphql/test/src/test/resources/schema.gql +++ b/viewers/graphql/test/src/test/resources/schema.gql @@ -23,10 +23,30 @@ directive @specifiedBy( ) on SCALAR type Mutation { + causeway_applib_PropertyNode__mixedIn(_gqlv_target: causeway_applib_PropertyNode__gqlv_input, mixedIn: Boolean!): causeway_applib_PropertyNode causeway_applib_PropertyNode__streamChildNodes(_gqlv_target: causeway_applib_PropertyNode__gqlv_input): java_util_stream_Stream + causeway_applib_node_ActionNode__mixedIn(_gqlv_target: causeway_applib_node_ActionNode__gqlv_input, mixedIn: Boolean!): causeway_applib_node_ActionNode causeway_applib_node_ActionNode__streamChildNodes(_gqlv_target: causeway_applib_node_ActionNode__gqlv_input): java_util_stream_Stream + causeway_applib_node_CollectionNode__mixedIn(_gqlv_target: causeway_applib_node_CollectionNode__gqlv_input, mixedIn: Boolean!): causeway_applib_node_CollectionNode causeway_applib_node_CollectionNode__streamChildNodes(_gqlv_target: causeway_applib_node_CollectionNode__gqlv_input): java_util_stream_Stream + causeway_applib_node_FacetNode__shadowed(_gqlv_target: causeway_applib_node_FacetNode__gqlv_input, shadowed: Boolean!): causeway_applib_node_FacetNode + causeway_conf_ConfigurationProperty__key(_gqlv_target: causeway_conf_ConfigurationProperty__gqlv_input, key: String!): causeway_conf_ConfigurationProperty + causeway_conf_ConfigurationProperty__value(_gqlv_target: causeway_conf_ConfigurationProperty__gqlv_input, value: String!): causeway_conf_ConfigurationProperty + causeway_schema_metamodel_v2_DomainClassDto__actions(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, actions: String!): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__annotations(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, annotations: String!): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__collections(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, collections: String!): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__facets(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, facets: String!): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__id(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, id: String!): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__majorVersion(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, majorVersion: String): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__minorVersion(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, minorVersion: String): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__properties(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, properties: String!): causeway_schema_metamodel_v2_DomainClassDto + causeway_schema_metamodel_v2_DomainClassDto__service(_gqlv_target: causeway_schema_metamodel_v2_DomainClassDto__gqlv_input, service: Boolean!): causeway_schema_metamodel_v2_DomainClassDto + causeway_testing_fixtures_FixtureResult__fixtureScriptClassName(_gqlv_target: causeway_testing_fixtures_FixtureResult__gqlv_input, fixtureScriptClassName: String): causeway_testing_fixtures_FixtureResult + causeway_testing_fixtures_FixtureResult__key(_gqlv_target: causeway_testing_fixtures_FixtureResult__gqlv_input, key: String!): causeway_testing_fixtures_FixtureResult + causeway_testing_fixtures_FixtureResult__object(_gqlv_target: causeway_testing_fixtures_FixtureResult__gqlv_input, object: String!): causeway_testing_fixtures_FixtureResult + org_apache_causeway_core_metamodel_inspect_model_MemberNode__mixedIn(_gqlv_target: org_apache_causeway_core_metamodel_inspect_model_MemberNode__gqlv_input, mixedIn: Boolean!): org_apache_causeway_core_metamodel_inspect_model_MemberNode org_apache_causeway_core_metamodel_inspect_model_MemberNode__streamChildNodes(_gqlv_target: org_apache_causeway_core_metamodel_inspect_model_MemberNode__gqlv_input): java_util_stream_Stream + org_apache_causeway_testing_fixtures_applib_fixturescripts_FixtureScript__friendlyName(_gqlv_target: org_apache_causeway_testing_fixtures_applib_fixturescripts_FixtureScript__gqlv_input, friendlyName: String!): org_apache_causeway_testing_fixtures_applib_fixturescripts_FixtureScript university_admin_AdminMenu__actionWithDisabledParam(firstParam: String!, secondParam: String!, thirdParameter: String!): String university_admin_AdminMenu__actionWithHiddenParam(firstParam: String!, secondParam: String!): String university_admin_AdminMenu__adminAction: String @@ -35,10 +55,18 @@ type Mutation { university_dept_Department__addStaffMembers(_gqlv_target: university_dept_Department__gqlv_input, staffMembers: [university_dept_StaffMember__gqlv_input]): university_dept_Department university_dept_Department__changeDeptHead(_gqlv_target: university_dept_Department__gqlv_input, newDeptHead: university_dept_DeptHead__gqlv_input!): university_dept_Department university_dept_Department__changeName(_gqlv_target: university_dept_Department__gqlv_input, newName: String!): university_dept_Department + university_dept_Department__deptHead(_gqlv_target: university_dept_Department__gqlv_input, deptHead: university_dept_DeptHead__gqlv_input): university_dept_Department + university_dept_Department__name(_gqlv_target: university_dept_Department__gqlv_input, name: String!): university_dept_Department university_dept_Department__removeStaffMember(_gqlv_target: university_dept_Department__gqlv_input, staffMember: university_dept_StaffMember__gqlv_input!): university_dept_Department university_dept_Departments__createDepartment(deptHead: university_dept_DeptHead__gqlv_input, name: String!): university_dept_Department university_dept_DeptHead__changeDepartment(_gqlv_target: university_dept_DeptHead__gqlv_input, department: university_dept_Department__gqlv_input!): university_dept_DeptHead university_dept_DeptHead__changeName(_gqlv_target: university_dept_DeptHead__gqlv_input, newName: String!): university_dept_DeptHead + university_dept_DeptHead__department(_gqlv_target: university_dept_DeptHead__gqlv_input, department: university_dept_Department__gqlv_input): university_dept_DeptHead + university_dept_DeptHead__name(_gqlv_target: university_dept_DeptHead__gqlv_input, name: String): university_dept_DeptHead + university_dept_StaffMember__department(_gqlv_target: university_dept_StaffMember__gqlv_input, department: university_dept_Department__gqlv_input): university_dept_StaffMember + university_dept_StaffMember__grade(_gqlv_target: university_dept_StaffMember__gqlv_input, grade: String!): university_dept_StaffMember + university_dept_StaffMember__name(_gqlv_target: university_dept_StaffMember__gqlv_input, name: String!): university_dept_StaffMember + university_dept_StaffMember__photo(_gqlv_target: university_dept_StaffMember__gqlv_input, photo: String): university_dept_StaffMember university_dept_Staff__createStaffMember(department: university_dept_Department__gqlv_input!, name: String!): university_dept_StaffMember } diff --git a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/integration/GraphQlSourceForCauseway.java b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/integration/GraphQlSourceForCauseway.java index 1d8930e97c..852ec0c70a 100644 --- a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/integration/GraphQlSourceForCauseway.java +++ b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/integration/GraphQlSourceForCauseway.java @@ -24,6 +24,7 @@ import javax.annotation.PostConstruct; import javax.inject.Inject; import org.apache.causeway.commons.functional.Either; +import org.apache.causeway.core.metamodel.facets.properties.update.modify.PropertySetterFacet; import org.apache.causeway.core.metamodel.spec.feature.MixedIn; import org.apache.causeway.viewer.graphql.viewer.toplevel.GqlvTopLevelMutation; @@ -138,6 +139,10 @@ public class GraphQlSourceForCauseway implements GraphQlSource { objectSpec.streamActions(context.getActionScope(), MixedIn.INCLUDED) .filter(x -> ! x.getSemantics().isSafeInNature()) .forEach(objectAction -> topLevelMutation.addAction(objectSpec, objectAction)); + objectSpec.streamProperties(MixedIn.INCLUDED) + .filter(property -> ! property.isAlwaysHidden()) + .filter(property -> property.containsFacet(PropertySetterFacet.class)) + .forEach(property -> topLevelMutation.addProperty(objectSpec, property)); }); topLevelMutation.buildMutationType(); diff --git a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/toplevel/GqlvTopLevelMutation.java b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/toplevel/GqlvTopLevelMutation.java index feb16fd775..822f2948ca 100644 --- a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/toplevel/GqlvTopLevelMutation.java +++ b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/toplevel/GqlvTopLevelMutation.java @@ -11,12 +11,15 @@ import static graphql.schema.GraphQLObjectType.newObject; import org.apache.causeway.core.metamodel.spec.ObjectSpecification; import org.apache.causeway.core.metamodel.spec.feature.ObjectAction; +import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation; import org.apache.causeway.viewer.graphql.model.context.Context; import org.apache.causeway.viewer.graphql.model.domain.GqlvMutationForAction; +import org.apache.causeway.viewer.graphql.model.domain.GqlvMutationForProperty; import lombok.Getter; -public class GqlvTopLevelMutation implements GqlvMutationForAction.Holder { +public class GqlvTopLevelMutation + implements GqlvMutationForAction.Holder, GqlvMutationForProperty.Holder { private final Context context; @@ -29,6 +32,7 @@ public class GqlvTopLevelMutation implements GqlvMutationForAction.Holder { private GraphQLObjectType gqlObjectType; private final List<GqlvMutationForAction> actions = new ArrayList<>(); + private final List<GqlvMutationForProperty> properties = new ArrayList<>(); public GqlvTopLevelMutation(final Context context) { this.context = context; @@ -61,6 +65,10 @@ public class GqlvTopLevelMutation implements GqlvMutationForAction.Holder { actions.add(new GqlvMutationForAction(this, objectSpec, objectAction, context)); } + public void addProperty(ObjectSpecification objectSpec, final OneToOneAssociation property) { + properties.add(new GqlvMutationForProperty(this, objectSpec, property, context)); + } + @Override public GraphQLFieldDefinition addField(GraphQLFieldDefinition field) { gqlObjectTypeBuilder.field(field); @@ -74,6 +82,7 @@ public class GqlvTopLevelMutation implements GqlvMutationForAction.Holder { public void addDataFetchers() { actions.forEach(GqlvMutationForAction::addDataFetcher); + properties.forEach(GqlvMutationForProperty::addDataFetcher); }
