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 8b42a17e241dc82c86a4c79ad437cc6bca74c528
Author: danhaywood <[email protected]>
AuthorDate: Thu Feb 15 07:12:03 2024 +0000

    CAUSEWAY-3676: use addChildFieldFor where possible; removes 'grid' from 
test; adds clob controller
---
 .../graphql/model/domain/GqlvAbstractCustom.java   | 14 ++--
 .../viewer/graphql/model/domain/GqlvAction.java    | 33 ++++----
 .../graphql/model/domain/GqlvActionParam.java      | 41 ++--------
 .../graphql/model/domain/GqlvActionParams.java     |  4 +-
 .../graphql/model/domain/GqlvCollection.java       | 15 +---
 .../graphql/model/domain/GqlvDomainObject.java     | 25 +++---
 .../graphql/model/domain/GqlvDomainService.java    |  2 +-
 .../viewer/graphql/model/domain/GqlvMeta.java      | 45 ++++-------
 .../viewer/graphql/model/domain/GqlvProperty.java  | 71 ++++++++---------
 .../graphql/model/domain/GqlvPropertyGetClob.java  | 90 ++++++++++++++++++++++
 .../model/domain/GqlvPropertyGetClobAbstract.java  | 78 +++++++++++++++++++
 .../model/domain/GqlvPropertyGetClobChars.java     | 49 ++++++++++++
 .../model/domain/GqlvPropertyGetClobMimeType.java  | 39 ++++++++++
 .../model/domain/GqlvPropertyGetClobName.java      | 39 ++++++++++
 .../viewer/graphql/model/domain/GqlvScenario.java  |  3 +-
 .../graphql/model/domain/GqlvScenarioStep.java     | 10 ++-
 .../model/toplevel/GqlvTopLevelMutation.java       |  4 +-
 .../graphql/model/toplevel/GqlvTopLevelQuery.java  | 13 ++--
 ...gTest.create_staff_member_with_department._.gql |  1 -
 ...eate_staff_member_with_department.approved.json |  3 +-
 .../viewer/CausewayModuleViewerGraphqlViewer.java  |  4 +-
 ...ytesController.java => ResourceController.java} | 41 +++++++---
 22 files changed, 447 insertions(+), 177 deletions(-)

diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
index 479b7cc62e..5d4e78a012 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAbstractCustom.java
@@ -27,6 +27,8 @@ import static graphql.schema.GraphQLObjectType.newObject;
 
 import org.apache.causeway.viewer.graphql.model.context.Context;
 
+import org.springframework.lang.Nullable;
+
 import lombok.AccessLevel;
 import lombok.Getter;
 
@@ -59,15 +61,17 @@ public abstract class GqlvAbstractCustom extends 
GqlvAbstract implements Parent
         return gqlObjectType != null;
     }
 
-    protected final void addChildFieldFor(GqlvAbstract hasField) {
-        addChildField(hasField.getField());
-    }
-
-    protected final void addChildField(GraphQLFieldDefinition childField) {
+    protected final void addChildFieldFor(@Nullable GqlvAbstract hasField) {
         if (isBuilt()) {
+            throw new IllegalStateException("Object type has already been 
built");
+        }
+        if (hasField == null) {
             return;
         }
+        addChildField(hasField.getField());
+    }
 
+    void addChildField(GraphQLFieldDefinition childField) {
         if (childField != null) {
             gqlObjectTypeBuilder.field(childField);
         }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
index fa8ef68e2c..7c46953a23 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvAction.java
@@ -26,6 +26,7 @@ import java.util.stream.Collectors;
 import org.apache.causeway.applib.services.bookmark.Bookmark;
 import org.apache.causeway.applib.services.bookmark.BookmarkService;
 import org.apache.causeway.commons.collections.Can;
+import org.apache.causeway.core.config.CausewayConfiguration;
 import org.apache.causeway.core.metamodel.object.ManagedObject;
 import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
 import org.apache.causeway.core.metamodel.spec.feature.ObjectAction;
@@ -76,23 +77,29 @@ public class GqlvAction
         addChildFieldFor(this.disabled = new GqlvMemberDisabled<>(this, 
context));
         addChildFieldFor(this.validate = new GqlvActionValidity(this, 
context));
 
-        val variant = 
context.causewayConfiguration.getViewer().getGraphql().getApiVariant();
-        if (objectAction.getSemantics().isSafeInNature() || variant == 
QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT) {
-            addChildFieldFor(this.invoke = new GqlvActionInvoke(this, 
context));
-        } else {
-            this.invoke = null;
-        }
-        val params = new GqlvActionParams(this, context);
-        if (params.hasParams()) {
-            this.params = params;
-            addChildField(params.getField());
-        } else {
-            this.params = null;
-        }
+        addChildFieldFor(
+                this.invoke = isInvokeAllowed(objectAction)
+                    ? new GqlvActionInvoke(this, context)
+                    : null);
+        addChildFieldFor(this.params = new GqlvActionParams(this, context));
 
         buildObjectTypeAndField(objectAction.getId());
     }
 
+    private boolean isInvokeAllowed(ObjectAction objectAction) {
+        val apiVariant = 
context.causewayConfiguration.getViewer().getGraphql().getApiVariant();
+        switch (apiVariant) {
+            case QUERY_ONLY:
+            case QUERY_AND_MUTATIONS:
+                return objectAction.getSemantics().isSafeInNature();
+            case QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT:
+                return true;
+            default:
+                // shouldn't happen
+                throw new IllegalArgumentException("Unknown API variant: " + 
apiVariant);
+        }
+    }
+
     public Can<ManagedObject> argumentManagedObjectsFor(
             final DataFetchingEnvironment dataFetchingEnvironment,
             final ObjectAction objectAction,
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParam.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParam.java
index d9f6d096dc..6cf6e6df7a 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParam.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParam.java
@@ -79,40 +79,13 @@ public class GqlvActionParam
         this.objectActionParameter = objectActionParameter;
         this.paramNum = paramNum;
 
-        this.hidden = new GqlvActionParamHidden(this, context);
-        addChildField(hidden.getField());
-        this.disabled = new GqlvActionParamDisabled(this, context);
-        addChildField(disabled.getField());
-
-        val choices = new GqlvActionParamChoices(this, context);
-        addChildField(choices.getField());
-        if (choices.isFieldDefined()) {
-            this.choices = choices;
-        } else {
-            this.choices = null;
-        }
-
-        val autoComplete = new GqlvActionParamAutoComplete(this, context);
-        addChildField(autoComplete.getField());
-        if (autoComplete.isFieldDefined()) {
-            this.autoComplete = autoComplete;
-        } else {
-            this.autoComplete = null;
-        }
-
-        val default_ = new GqlvActionParamDefault(this, context);
-        addChildField(default_.getField());
-        if (default_.isFieldDefined()) {
-            this.default_ = default_;
-        } else {
-            this.default_ = null;
-        }
-
-        this.validate = new GqlvActionParamValidate(this, context);
-        addChildField(validate.getField());
-
-        this.datatype = new GqlvActionParamDatatype(this, context);
-        addChildField(datatype.getField());
+        addChildFieldFor(this.hidden = new GqlvActionParamHidden(this, 
context));
+        addChildFieldFor(this.disabled = new GqlvActionParamDisabled(this, 
context));
+        addChildFieldFor(this.choices = new GqlvActionParamChoices(this, 
context));
+        addChildFieldFor(this.autoComplete = new 
GqlvActionParamAutoComplete(this, context));
+        addChildFieldFor(this.default_ = new GqlvActionParamDefault(this, 
context));
+        addChildFieldFor(this.validate = new GqlvActionParamValidate(this, 
context));
+        addChildFieldFor(this.datatype = new GqlvActionParamDatatype(this, 
context));
 
         buildObjectTypeAndField(objectActionParameter.getId());
     }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParams.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParams.java
index e043d61d1d..0eccf8d435 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParams.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvActionParams.java
@@ -85,8 +85,8 @@ public class GqlvActionParams
     }
 
     void addParam(ObjectActionParameter objectActionParameter, int paramNum) {
-        GqlvActionParam gqlvActionParam = new GqlvActionParam(this, 
objectActionParameter, context, paramNum);
-        addChildField(gqlvActionParam.getField());
+        val gqlvActionParam = new GqlvActionParam(this, objectActionParameter, 
context, paramNum);
+        addChildFieldFor(gqlvActionParam);
         params.put(objectActionParameter.getId(), gqlvActionParam);
     }
 
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvCollection.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvCollection.java
index 4d3ab7a15c..c5b4ff6343 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvCollection.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvCollection.java
@@ -41,17 +41,10 @@ public class GqlvCollection
     ) {
         super(holder, oneToManyAssociation, 
TypeNames.collectionTypeNameFor(holder.getObjectSpecification(), 
oneToManyAssociation), context);
 
-        this.hidden = new GqlvMemberHidden<>(this, context);
-        addChildField(hidden.getField());
-
-        this.disabled = new GqlvMemberDisabled<>(this, context);
-        addChildField(disabled.getField());
-
-        this.get = new GqlvCollectionGet(this, context);
-        addChildField(get.getField());
-
-        this.datatype = new GqlvCollectionDatatype(this, context);
-        addChildField(datatype.getField());
+        addChildFieldFor(this.hidden = new GqlvMemberHidden<>(this, context));
+        addChildFieldFor(this.disabled = new GqlvMemberDisabled<>(this, 
context));
+        addChildFieldFor(this.get = new GqlvCollectionGet(this, context));
+        addChildFieldFor(this.datatype = new GqlvCollectionDatatype(this, 
context));
 
         buildObjectTypeAndField(oneToManyAssociation.getId());
     }
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainObject.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainObject.java
index e4f895fd26..71a88f83e6 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainObject.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainObject.java
@@ -74,11 +74,10 @@ public class GqlvDomainObject
         this.objectSpecification = objectSpecification;
         gqlObjectTypeBuilder.description(objectSpecification.getDescription());
 
-        this.meta = new GqlvMeta(this, context);
-        addChildField(meta.getField());
+        addChildFieldFor(this.meta = new GqlvMeta(this, context));
 
-        GraphQLInputObjectType.Builder inputTypeBuilder = 
newInputObject().name(TypeNames.inputTypeNameFor(objectSpecification));
-        inputTypeBuilder
+        val inputObjectTypeBuilder = 
newInputObject().name(TypeNames.inputTypeNameFor(objectSpecification));
+        inputObjectTypeBuilder
                 .field(newInputObjectField()
                         .name("id")
                         .type(Scalars.GraphQLID)
@@ -90,7 +89,7 @@ public class GqlvDomainObject
                         .build()
                 )
         ;
-        gqlInputObjectType = inputTypeBuilder.build();
+        gqlInputObjectType = inputObjectTypeBuilder.build();
 
         setField(buildFieldDefinition(gqlInputObjectType));
 
@@ -129,8 +128,8 @@ public class GqlvDomainObject
 
         objectSpecification.streamActions(context.getActionScope(), 
MixedIn.INCLUDED)
                 .forEach(objectAction -> {
-                    GqlvAction gqlvAction = new GqlvAction(this, objectAction, 
context);
-                    addChildField(gqlvAction.getField());
+                    val gqlvAction = new GqlvAction(this, objectAction, 
context);
+                    addChildFieldFor(gqlvAction);
                     actions.put(objectAction.getId(), gqlvAction);
                 });
     }
@@ -143,16 +142,16 @@ public class GqlvDomainObject
     }
 
     private void addProperty(final OneToOneAssociation otoa) {
-        GqlvProperty gqlvProperty = new GqlvProperty(this, otoa, context);
-        addChildField(gqlvProperty.getField());
+        val gqlvProperty = new GqlvProperty(this, otoa, context);
+        addChildFieldFor(gqlvProperty);
         properties.put(otoa.getId(), gqlvProperty);
     }
 
     private void addCollection(OneToManyAssociation otom) {
-        GqlvCollection collection = new GqlvCollection(this, otom, context);
-        addChildField(collection.getField());
-        if (collection.isFieldDefined()) {
-            collections.put(otom.getId(), collection);
+        val gqlvCollection = new GqlvCollection(this, otom, context);
+        addChildFieldFor(gqlvCollection);
+        if (gqlvCollection.isFieldDefined()) {
+            collections.put(otom.getId(), gqlvCollection);
         }
     }
 
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainService.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainService.java
index 8ffe5a6c85..7a51948524 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainService.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvDomainService.java
@@ -82,7 +82,7 @@ public class GqlvDomainService
 
     private void addAction(final ObjectAction objectAction) {
         val gqlvAction = new GqlvAction(this, objectAction, context);
-        addChildField(gqlvAction.getField());
+        addChildFieldFor(gqlvAction);
         actions.put(objectAction.getId(), gqlvAction);
     }
 
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java
index 15181e1c19..bd12ca4380 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvMeta.java
@@ -57,46 +57,29 @@ public class GqlvMeta extends GqlvAbstractCustom {
         super(TypeNames.metaTypeNameFor(holder.getObjectSpecification()), 
context);
         this.holder = holder;
 
-        metaId = new GqlvMetaId(context);
-        addChildField(metaId.getField());
-
-        metaLogicalTypeName = new GqlvMetaLogicalTypeName(context);
-        addChildField(metaLogicalTypeName.getField());
-
-        if (holder.getObjectSpecification().getBeanSort() == BeanSort.ENTITY) {
-            metaVersion = new GqlvMetaVersion(context);
-            addChildField(metaVersion.getField());
-        } else {
-            metaVersion = null;
-        }
-
-        metaTitle = new GqlvMetaTitle(context);
-        addChildField(metaTitle.getField());
-
-        metaIconName = new GqlvMetaIconName(context);
-        addChildField(metaIconName.getField());
-
-        metaCssClass = new GqlvMetaCssClass(context);
-        addChildField(metaCssClass.getField());
-
-        metaLayout = new GqlvMetaLayout(context);
-        addChildField(metaLayout.getField());
-
-        metaGrid = new GqlvMetaGrid(context);
-        addChildField(metaGrid.getField());
-
-        metaSaveAs = new GqlvMetaSaveAs(context);
-        addChildField(metaSaveAs.getField());
+        addChildFieldFor(this.metaId = new GqlvMetaId(context));
+        addChildFieldFor(this.metaLogicalTypeName = new 
GqlvMetaLogicalTypeName(context));
+        addChildFieldFor(this.metaVersion = isEntity() ? new 
GqlvMetaVersion(context) : null);
+        addChildFieldFor(this.metaTitle = new GqlvMetaTitle(context));
+        addChildFieldFor(this.metaIconName = new GqlvMetaIconName(context));
+        addChildFieldFor(this.metaCssClass = new GqlvMetaCssClass(context));
+        addChildFieldFor(this.metaLayout = new GqlvMetaLayout(context));
+        addChildFieldFor(this.metaGrid = new GqlvMetaGrid(context));
+        addChildFieldFor(this.metaSaveAs = new GqlvMetaSaveAs(context));
 
         val fieldName = 
context.causewayConfiguration.getViewer().getGraphql().getMetaData().getFieldName();
         buildObjectTypeAndField(fieldName);
     }
 
+    private boolean isEntity() {
+        return holder.getObjectSpecification().getBeanSort() == 
BeanSort.ENTITY;
+    }
+
     @Override
     protected void addDataFetchersForChildren() {
         metaId.addDataFetcher(this);
         metaLogicalTypeName.addDataFetcher(this);
-        if (holder.getObjectSpecification().getBeanSort() == BeanSort.ENTITY) {
+        if (isEntity()) {
             metaVersion.addDataFetcher(this);
         }
         metaTitle.addDataFetcher(this);
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
index 7e82bd35fc..f6ffed52d3 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvProperty.java
@@ -25,6 +25,7 @@ import org.apache.causeway.applib.value.Blob;
 import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.core.config.CausewayConfiguration;
 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.types.TypeMapper;
@@ -41,7 +42,8 @@ public class GqlvProperty
         GqlvPropertyValidate.Holder,
         GqlvPropertySet.Holder,
         GqlvAssociationDatatype.Holder<OneToOneAssociation>,
-        GqlvPropertyGetBlob.Holder {
+        GqlvPropertyGetBlob.Holder,
+        GqlvPropertyGetClob.Holder {
 
     private final GqlvMemberHidden<OneToOneAssociation> hidden;
     private final GqlvMemberDisabled<OneToOneAssociation> disabled;
@@ -68,51 +70,42 @@ public class GqlvProperty
             final Context context) {
         super(holder, oneToOneAssociation, 
TypeNames.propertyTypeNameFor(holder.getObjectSpecification(), 
oneToOneAssociation), context);
 
-        this.hidden = new GqlvMemberHidden<>(this, context);
-        addChildField(hidden.getField());
+        addChildFieldFor(this.hidden = new GqlvMemberHidden<>(this, context));
+        addChildFieldFor(this.disabled = new GqlvMemberDisabled<>(this, 
context));
 
-        this.disabled = new GqlvMemberDisabled<>(this, context);
-        addChildField(disabled.getField());
+        this.get = isBlob() ? new GqlvPropertyGetBlob(this, context) : 
isClob() ? new GqlvPropertyGetClob(this, context) : new GqlvPropertyGet(this, 
context);
+        addChildFieldFor(
+                isBlob()
+                    ? new GqlvPropertyGetBlob(this, context)
+                    : isClob()
+                        ? new GqlvPropertyGetClob(this, context)
+                        : new GqlvPropertyGet(this, context)
+        );
 
-        if (isBlob()) {
-            this.get = new GqlvPropertyGetBlob(this, context);
-        } else {
-            this.get = new GqlvPropertyGet(this, context);
-        }
-        addChildField(get.getField());
-
-        this.validate = new GqlvPropertyValidate(this, context);
-        addChildField(this.validate.getField());
+        addChildFieldFor(this.validate = new GqlvPropertyValidate(this, 
context));
+        addChildFieldFor(this.choices = new GqlvPropertyChoices(this, 
context));
+        addChildFieldFor(this.autoComplete = new 
GqlvPropertyAutoComplete(this, context));
+        addChildFieldFor(this.set = isSetterAllowed() ? new 
GqlvPropertySet(this, context) : null);
+        addChildFieldFor(this.datatype = new GqlvPropertyDatatype(this, 
context));
 
-        val choices = new GqlvPropertyChoices(this, context);
-        if (choices.isFieldDefined()) {
-            addChildField(choices.getField());
-            this.choices = choices;
-        } else {
-            this.choices = null;
-        }
-
-        val autoComplete = new GqlvPropertyAutoComplete(this, context);
-        if (autoComplete.isFieldDefined()) {
-            addChildField(autoComplete.getField());
-            this.autoComplete = autoComplete;
-        } else {
-            this.autoComplete = null;
-        }
+        buildObjectTypeAndField(oneToOneAssociation.getId());
+    }
 
-        val variant = 
context.causewayConfiguration.getViewer().getGraphql().getApiVariant();
-        if (variant == 
CausewayConfiguration.Viewer.Graphql.ApiVariant.QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT)
 {
-            this.set = new GqlvPropertySet(this, context);
-            addChildField(set.getField());
-        } else {
-            this.set = null;
+    private boolean isSetterAllowed() {
+        val apiVariant = 
context.causewayConfiguration.getViewer().getGraphql().getApiVariant();
+        switch (apiVariant) {
+            case QUERY_ONLY:
+            case QUERY_AND_MUTATIONS:
+                return false;
+            case QUERY_WITH_MUTATIONS_NON_SPEC_COMPLIANT:
+                return true;
+            default:
+                // shouldn't happen
+                throw new IllegalArgumentException("Unknown API variant: " + 
apiVariant);
         }
+    }
 
-        this.datatype = new GqlvPropertyDatatype(this, context);
-        addChildField(datatype.getField());
 
-        buildObjectTypeAndField(oneToOneAssociation.getId());
-    }
 
     private boolean isBlob() {
         return 
getOneToOneAssociation().getElementType().getCorrespondingClass() == Blob.class;
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClob.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClob.java
new file mode 100644
index 0000000000..1944815515
--- /dev/null
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClob.java
@@ -0,0 +1,90 @@
+/*
+ *  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 graphql.schema.DataFetchingEnvironment;
+
+import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition;
+
+import org.apache.causeway.core.metamodel.spec.ObjectSpecification;
+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.fetcher.BookmarkedPojo;
+import 
org.apache.causeway.viewer.graphql.model.mmproviders.ObjectAssociationProvider;
+import 
org.apache.causeway.viewer.graphql.model.mmproviders.ObjectSpecificationProvider;
+
+public class GqlvPropertyGetClob
+        extends GqlvAbstractCustom
+        implements GqlvPropertyGetClobChars.Holder
+{
+
+    final Holder holder;
+    final GqlvPropertyGetClobChars clobChars;
+    final GqlvPropertyGetClobMimeType clobMimeType;
+    final GqlvPropertyGetClobName clobName;
+
+    public GqlvPropertyGetClob(
+            final Holder holder,
+            final Context context) {
+        
super(TypeNames.propertyBlobTypeNameFor(holder.getObjectSpecification(), 
holder.getObjectMember()), context);
+        this.holder = holder;
+
+        addChildFieldFor(clobChars = new GqlvPropertyGetClobChars(this, 
context));
+        addChildFieldFor(clobMimeType = new GqlvPropertyGetClobMimeType(this, 
context));
+        addChildFieldFor(clobName = new GqlvPropertyGetClobName(this, 
context));
+
+        setField(newFieldDefinition()
+                    .name("get")
+                    .type(buildObjectType())
+                    .build());
+    }
+
+    @Override
+    protected Object fetchData(final DataFetchingEnvironment 
dataFetchingEnvironment) {
+        return BookmarkedPojo.sourceFrom(dataFetchingEnvironment, context);
+    }
+
+    @Override
+    protected void addDataFetchersForChildren() {
+        clobChars.addDataFetcher(this);
+        clobMimeType.addDataFetcher(this);
+        clobName.addDataFetcher(this);
+    }
+
+    @Override
+    public OneToOneAssociation getObjectAssociation() {
+        return holder.getObjectAssociation();
+    }
+
+    @Override
+    public OneToOneAssociation getObjectMember() {
+        return holder.getObjectMember();
+    }
+
+    @Override
+    public ObjectSpecification getObjectSpecification() {
+        return holder.getObjectSpecification();
+    }
+
+    public interface Holder
+            extends ObjectSpecificationProvider,
+                    ObjectAssociationProvider<OneToOneAssociation> {
+
+    }
+}
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobAbstract.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobAbstract.java
new file mode 100644
index 0000000000..87107f919c
--- /dev/null
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobAbstract.java
@@ -0,0 +1,78 @@
+/*
+ *  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.Optional;
+import java.util.function.Function;
+
+import graphql.Scalars;
+import graphql.schema.DataFetchingEnvironment;
+import graphql.schema.GraphQLFieldDefinition;
+
+import org.apache.causeway.applib.value.Blob;
+import org.apache.causeway.core.metamodel.object.ManagedObject;
+import org.apache.causeway.viewer.graphql.model.context.Context;
+import org.apache.causeway.viewer.graphql.model.domain.GqlvAbstract;
+import org.apache.causeway.viewer.graphql.model.domain.GqlvPropertyGet;
+import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
+
+import lombok.val;
+
+public abstract class GqlvPropertyGetClobAbstract extends GqlvAbstract {
+
+    final Holder holder;
+
+    public GqlvPropertyGetClobAbstract(
+            final Holder holder,
+            final Context context, String name) {
+        super(context);
+        this.holder = holder;
+
+        setField(GraphQLFieldDefinition.newFieldDefinition()
+                    .name(name)
+                    .type(Scalars.GraphQLString)
+                    .build());
+    }
+
+    protected Object fetchDataFromBlob(DataFetchingEnvironment environment, 
Function<Blob, ?> mapper) {
+        val sourcePojo = BookmarkedPojo.sourceFrom(environment);
+
+        val sourcePojoClass = sourcePojo.getClass();
+        val objectSpecification = 
context.specificationLoader.loadSpecification(sourcePojoClass);
+        if (objectSpecification == null) {
+            // not expected
+            return null;
+        }
+
+        val association = holder.getObjectAssociation();
+        val managedObject = ManagedObject.adaptSingular(objectSpecification, 
sourcePojo);
+        val resultManagedObject = association.get(managedObject);
+
+        return Optional.ofNullable(resultManagedObject)
+                .map(ManagedObject::getPojo)
+                .filter(Blob.class::isInstance)
+                .map(Blob.class::cast)
+                .map(mapper)
+                .orElse(null);
+    }
+
+    public interface Holder extends GqlvPropertyGet.Holder {
+    }
+
+}
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobChars.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobChars.java
new file mode 100644
index 0000000000..fd59b02d32
--- /dev/null
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobChars.java
@@ -0,0 +1,49 @@
+/*
+ *  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.Optional;
+
+import graphql.schema.DataFetchingEnvironment;
+
+import org.apache.causeway.applib.services.bookmark.Bookmark;
+import org.apache.causeway.viewer.graphql.model.context.Context;
+import org.apache.causeway.viewer.graphql.model.fetcher.BookmarkedPojo;
+
+import lombok.val;
+
+public class GqlvPropertyGetClobChars extends GqlvPropertyGetClobAbstract {
+
+    public GqlvPropertyGetClobChars(
+            final Holder holder,
+            final Context context) {
+        super(holder, context, "bytes");
+    }
+
+    @Override
+    protected Object fetchData(DataFetchingEnvironment environment) {
+        val sourcePojo = BookmarkedPojo.sourceFrom(environment);
+
+        Optional<Bookmark> bookmarkIfAny = 
context.bookmarkService.bookmarkFor(sourcePojo);
+        return bookmarkIfAny.map(x -> String.format(
+                "///%s/object/%s:%s/%s/clobChars", "graphql", 
x.getLogicalTypeName(), x.getIdentifier(), 
holder.getObjectAssociation().getId())).orElse(null);
+
+    }
+
+}
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobMimeType.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobMimeType.java
new file mode 100644
index 0000000000..9f314203f3
--- /dev/null
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobMimeType.java
@@ -0,0 +1,39 @@
+/*
+ *  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 graphql.schema.DataFetchingEnvironment;
+
+import org.apache.causeway.viewer.graphql.model.context.Context;
+
+public class GqlvPropertyGetClobMimeType extends GqlvPropertyGetClobAbstract {
+
+    public GqlvPropertyGetClobMimeType(
+            final Holder holder,
+            final Context context) {
+        super(holder, context, "mimeType");
+
+    }
+
+    @Override
+    protected Object fetchData(DataFetchingEnvironment environment) {
+        return fetchDataFromBlob(environment, blob -> 
blob.getMimeType().toString());
+    }
+
+}
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobName.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobName.java
new file mode 100644
index 0000000000..5ea781c578
--- /dev/null
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvPropertyGetClobName.java
@@ -0,0 +1,39 @@
+/*
+ *  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 graphql.schema.DataFetchingEnvironment;
+
+import org.apache.causeway.applib.value.Blob;
+import org.apache.causeway.viewer.graphql.model.context.Context;
+
+public class GqlvPropertyGetClobName extends GqlvPropertyGetClobAbstract {
+
+    public GqlvPropertyGetClobName(
+            final Holder holder,
+            final Context context) {
+        super(holder, context, "name");
+    }
+
+    @Override
+    protected Object fetchData(DataFetchingEnvironment environment) {
+        return fetchDataFromBlob(environment, Blob::getName);
+    }
+
+}
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenario.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenario.java
index ede625c72e..07e4f851bf 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenario.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenario.java
@@ -41,8 +41,7 @@ public class GqlvScenario
             final Context context) {
         super("Scenario", context);
 
-        this.scenarioName = new GqlvScenarioName(context);
-        addChildField(scenarioName.getField());
+        addChildFieldFor(this.scenarioName = new GqlvScenarioName(context));
 
         this.scenarioStep = new GqlvScenarioStep(context);
         addChildField(scenarioStep.newField("Given"));
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenarioStep.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenarioStep.java
index a852daaa71..4411e63aa5 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenarioStep.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/domain/GqlvScenarioStep.java
@@ -6,6 +6,8 @@ import java.util.Objects;
 
 import graphql.schema.DataFetchingEnvironment;
 
+import lombok.val;
+
 import org.apache.causeway.applib.services.metamodel.BeanSort;
 import org.apache.causeway.viewer.graphql.model.context.Context;
 
@@ -36,16 +38,16 @@ public class GqlvScenarioStep
             if (Objects.requireNonNull(objectSpec.getBeanSort()) == 
BeanSort.MANAGED_BEAN_CONTRIBUTING) { // @DomainService
                 
context.serviceRegistry.lookupBeanById(objectSpec.getLogicalTypeName())
                         .ifPresent(servicePojo -> {
-                            GqlvDomainService gqlvDomainService = 
GqlvDomainService.of(objectSpec, servicePojo, context);
-                            addChildField(gqlvDomainService.getField());
+                            val gqlvDomainService = 
GqlvDomainService.of(objectSpec, servicePojo, context);
+                            addChildFieldFor(gqlvDomainService);
                             domainServices.add(gqlvDomainService);
                         });
             }
         });
 
         // add domain object lookup to top-level query
-        for (GqlvDomainObject domainObject : this.domainObjects) {
-            addChildField(domainObject.getField());
+        for (val gqlvDomainObject : this.domainObjects) {
+            addChildFieldFor(gqlvDomainObject);
         }
 
         buildObjectType();
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelMutation.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelMutation.java
index 3fa541054f..20a692f50f 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelMutation.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelMutation.java
@@ -57,13 +57,13 @@ public class GqlvTopLevelMutation
 
     public void addAction(ObjectSpecification objectSpec, final ObjectAction 
objectAction) {
         val gqlvMutationForAction = new GqlvMutationForAction(objectSpec, 
objectAction, context);
-        addChildField(gqlvMutationForAction.getField());
+        addChildFieldFor(gqlvMutationForAction);
         actions.add(gqlvMutationForAction);
     }
 
     public void addProperty(ObjectSpecification objectSpec, final 
OneToOneAssociation property) {
         val gqlvMutationForProperty = new GqlvMutationForProperty(objectSpec, 
property, context);
-        addChildField(gqlvMutationForProperty.getField());
+        addChildFieldFor(gqlvMutationForProperty);
         properties.add(gqlvMutationForProperty);
     }
 
diff --git 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelQuery.java
 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelQuery.java
index 1668709db0..b0baf4d332 100644
--- 
a/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelQuery.java
+++ 
b/viewers/graphql/model/src/main/java/org/apache/causeway/viewer/graphql/model/toplevel/GqlvTopLevelQuery.java
@@ -6,6 +6,8 @@ import java.util.List;
 import graphql.schema.DataFetchingEnvironment;
 import graphql.schema.GraphQLObjectType;
 
+import lombok.val;
+
 import org.apache.causeway.viewer.graphql.model.context.Context;
 import org.apache.causeway.viewer.graphql.model.domain.GqlvAbstractCustom;
 import org.apache.causeway.viewer.graphql.model.domain.GqlvDomainObject;
@@ -44,8 +46,8 @@ public class GqlvTopLevelQuery
                 case MANAGED_BEAN_CONTRIBUTING: // @DomainService
                     
context.serviceRegistry.lookupBeanById(objectSpec.getLogicalTypeName())
                             .ifPresent(servicePojo -> {
-                                GqlvDomainService gqlvDomainService = 
GqlvDomainService.of(objectSpec, servicePojo, context);
-                                addChildField(gqlvDomainService.getField());
+                                val gqlvDomainService = 
GqlvDomainService.of(objectSpec, servicePojo, context);
+                                addChildFieldFor(gqlvDomainService);
                                 domainServices.add(gqlvDomainService);
                             });
                     break;
@@ -53,12 +55,11 @@ public class GqlvTopLevelQuery
         });
 
         // add domain object lookup to top-level query
-        for (GqlvDomainObject domainObject : this.domainObjects) {
-            addChildField(domainObject.getField());
+        for (val gqlvDomainObject : this.domainObjects) {
+            addChildFieldFor(gqlvDomainObject);
         }
 
-        scenario = new GqlvScenario(context);
-        addChildField(scenario.getField());
+        addChildFieldFor(scenario = new GqlvScenario(context));
 
         buildObjectType();
     }
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department._.gql
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department._.gql
index 5876019ffb..b18a3e5a64 100644
--- 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department._.gql
+++ 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department._.gql
@@ -32,7 +32,6 @@
               version
               cssClass
               iconName
-              grid
             }
           }
         }
diff --git 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department.approved.json
 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department.approved.json
index 1929ae96ef..4548f25fb4 100644
--- 
a/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department.approved.json
+++ 
b/viewers/graphql/test/src/test/java/org/apache/causeway/viewer/graphql/viewer/test/e2e/queryandmutations/Staff_IntegTest.create_staff_member_with_department.approved.json
@@ -32,8 +32,7 @@
                 "logicalTypeName" : "university.dept.StaffMember",
                 "version" : null,
                 "cssClass" : null,
-                "iconName" : null,
-                "grid" : "<?xml version=\"1.0\" encoding=\"UTF-8\"?><bs:grid 
xmlns:bs=\"https://causeway.apache.org/applib/layout/grid/bootstrap3\"; 
xmlns:lnk=\"https://causeway.apache.org/applib/layout/links\"; 
xmlns:cpt=\"https://causeway.apache.org/applib/layout/component\";>\n    
<bs:row>\n        <bs:col span=\"12\" unreferencedActions=\"true\">\n           
 <cpt:domainObject bookmarking=\"AS_ROOT\"/>\n        </bs:col>\n    
</bs:row>\n    <bs:row>\n        <bs:col span=\"4\">\n        [...]
+                "iconName" : null
               }
             }
           }
diff --git 
a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/CausewayModuleViewerGraphqlViewer.java
 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/CausewayModuleViewerGraphqlViewer.java
index 1c28790e91..1f52b24e61 100644
--- 
a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/CausewayModuleViewerGraphqlViewer.java
+++ 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/CausewayModuleViewerGraphqlViewer.java
@@ -18,7 +18,7 @@
  */
 package org.apache.causeway.viewer.graphql.viewer;
 
-import 
org.apache.causeway.viewer.graphql.viewer.controller.BlobBytesController;
+import org.apache.causeway.viewer.graphql.viewer.controller.ResourceController;
 
 import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
 import org.springframework.boot.autoconfigure.graphql.GraphQlAutoConfiguration;
@@ -47,7 +47,7 @@ import 
org.apache.causeway.viewer.graphql.model.CausewayModuleViewerGraphqlModel
         GraphQlWebMvcAutoConfiguration.class,
 
         // controllers
-        BlobBytesController.class
+        ResourceController.class
 })
 @EnableConfigurationProperties({
         GraphQlProperties.class, GraphQlCorsProperties.class
diff --git 
a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/BlobBytesController.java
 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
similarity index 71%
rename from 
viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/BlobBytesController.java
rename to 
viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
index a72670ce79..2b388ce3cf 100644
--- 
a/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/BlobBytesController.java
+++ 
b/viewers/graphql/viewer/src/main/java/org/apache/causeway/viewer/graphql/viewer/controller/ResourceController.java
@@ -2,12 +2,14 @@ package org.apache.causeway.viewer.graphql.viewer.controller;
 
 import javax.inject.Inject;
 
+import org.apache.causeway.applib.value.Clob;
 import org.apache.causeway.core.metamodel.spec.feature.OneToOneAssociation;
 
 import org.springframework.http.ContentDisposition;
 import org.springframework.http.HttpHeaders;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.util.MimeType;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -27,7 +29,7 @@ import java.util.Optional;
 @RestController()
 @RequestMapping("/graphql/object")
 @RequiredArgsConstructor(onConstructor_ = {@Inject})
-public class BlobBytesController {
+public class ResourceController {
 
     private final BookmarkService bookmarkService;
     private final ObjectManager objectManager;
@@ -38,23 +40,44 @@ public class BlobBytesController {
             @PathVariable final String id,
             @PathVariable final String propertyId) {
 
-        return 
bookmarkService.lookup(Bookmark.forLogicalTypeNameAndIdentifier(logicalTypeName,
 id))
-                .map(objectManager::adapt)
-                .map(managedObject -> 
ManagedObjectAndPropertyIfAny.of(managedObject, 
managedObject.getSpecification().getProperty(propertyId)))
-                .filter(ManagedObjectAndPropertyIfAny::isPropertyPresent)
-                .map(ManagedObjectAndProperty::of)
-                .map(ManagedObjectAndProperty::value)
-                .map(ManagedObject::getPojo)
+        return valueOf(logicalTypeName, id, propertyId)
                 .filter(Blob.class::isInstance)
                 .map(Blob.class::cast)
                 .map(blob -> ResponseEntity.ok()
-                        .contentType(MediaType.APPLICATION_PDF)
+                        
.contentType(MediaType.asMediaType(MimeType.valueOf(blob.getMimeType().toString())))
                         .header(HttpHeaders.CONTENT_DISPOSITION, 
ContentDisposition.attachment().filename(blob.getName()).build().toString())
                         .contentLength(blob.getBytes().length)
                         .body(blob.getBytes()))
                 .orElse(ResponseEntity.notFound().build());
     }
 
+    @GetMapping(value = "/{logicalTypeName}:{id}/{propertyId}/clobChars")
+    public ResponseEntity<CharSequence> propertyClobChars(
+            @PathVariable final String logicalTypeName,
+            @PathVariable final String id,
+            @PathVariable final String propertyId) {
+
+        return valueOf(logicalTypeName, id, propertyId)
+                .filter(Clob.class::isInstance)
+                .map(Clob.class::cast)
+                .map(clob -> ResponseEntity.ok()
+                        
.contentType(MediaType.asMediaType(MimeType.valueOf(clob.getMimeType().toString())))
+                        .header(HttpHeaders.CONTENT_DISPOSITION, 
ContentDisposition.attachment().filename(clob.getName()).build().toString())
+                        .contentLength(clob.getChars().length())
+                        .body(clob.getChars()))
+                .orElse(ResponseEntity.notFound().build());
+    }
+
+    private Optional<Object> valueOf(String logicalTypeName, String id, String 
propertyId) {
+        return 
bookmarkService.lookup(Bookmark.forLogicalTypeNameAndIdentifier(logicalTypeName,
 id))
+                .map(objectManager::adapt)
+                .map(managedObject -> 
ManagedObjectAndPropertyIfAny.of(managedObject, 
managedObject.getSpecification().getProperty(propertyId)))
+                .filter(ManagedObjectAndPropertyIfAny::isPropertyPresent)
+                .map(ManagedObjectAndProperty::of)
+                .map(ManagedObjectAndProperty::value)
+                .map(ManagedObject::getPojo);
+    }
+
     @Value(staticConstructor = "of")
     private static class ManagedObjectAndPropertyIfAny {
         ManagedObject owningObject;

Reply via email to