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 
&quot;query&quot; 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);
     }
 
 


Reply via email to