Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions dotCMS/src/main/java/com/dotcms/graphql/CustomFieldType.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.dotcms.graphql;

import com.dotcms.contenttype.model.type.BaseContentType;
import com.dotcms.graphql.datafetcher.AssetBinaryPropertyDataFetcher;
import com.dotcms.graphql.datafetcher.BinaryFieldDataFetcher;
import com.dotcms.graphql.datafetcher.FieldDataFetcher;
import com.dotcms.graphql.datafetcher.KeyValueFieldDataFetcher;
Expand All @@ -19,6 +20,7 @@
import graphql.schema.PropertyDataFetcher;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
Expand Down Expand Up @@ -46,7 +48,7 @@ public enum CustomFieldType {
KEY_VALUE("DotKeyValue"),
LANGUAGE("DotLanguage"),
USER("DotUser"),
FILEASSET("DotFileasset"),
FILEASSET("DotFileassetFlat"),
STORY_BLOCK("DotStoryBlock");

CustomFieldType(String typeName) {
Expand All @@ -61,6 +63,25 @@ public String getTypeName() {

private static Map<String, GraphQLObjectType> customFieldTypes = new HashMap<>();

private static Map<String, TypeFetcher> assetFlatFields;

/**
* @return the long-standing flat properties of an asset-pointing field, minus
* {@code description}, for reuse by the asset-content interface and by the object types that
* implement it.
*/
public static Map<String, TypeFetcher> getAssetFlatFields() {
if (null == assetFlatFields) {
// Only reachable if something reads this while this class is still initializing --
// i.e. a new static-init cycle. Returning null would surface much later as an
// unexplained NPE inside schema construction; say so here instead.
throw new IllegalStateException("CustomFieldType is still initializing: asset flat "
+ "fields were read from within its own static initialization, which means a "
+ "type-initialization cycle has been introduced.");
}
return Collections.unmodifiableMap(assetFlatFields);
}

static {
final Map<String, GraphQLOutputType> binaryTypeFields = new HashMap<>();
binaryTypeFields.put("versionPath", GraphQLString);
Expand Down Expand Up @@ -158,7 +179,42 @@ public String getTypeName() {
new TypeFetcher(list(CustomFieldType.KEY_VALUE.getType()), new KeyValueFieldDataFetcher()));
fileAssetTypeFields.put(FILEASSET_SHOW_ON_MENU_FIELD_VAR, new TypeFetcher(list(GraphQLString), new MultiValueFieldDataFetcher()));
fileAssetTypeFields.put(FILEASSET_SORT_ORDER_FIELD_VAR, new TypeFetcher(GraphQLInt, new FieldDataFetcher()));
customFieldTypes.put("FILEASSET", TypeUtil.createObjectType(FILEASSET.getTypeName(), fileAssetTypeFields));

// Deliberately NOT registered as a schema type. Asset-pointing fields are described by
// the asset interface now, so nothing references this object type; registering it would
// leave an orphan in every customer's schema, visible in introspection and reachable by
// nobody. The field map below is still built because the interface and the object types
// that implement it reuse these exact fetchers.

// The same properties, minus `description`, reused as the flat half of the asset-content
// interface and synthesized onto DOTASSET-derived object types. Reusing these exact
// TypeFetchers is what guarantees the synthesized fields answer identically to the flat
// view -- notably `fileName`, which is not a stored value for DOTASSET content, and the
// binary, which BinaryFieldDataFetcher already maps to `asset` for that base type.
//
// `description` is excluded on purpose: DOTASSET-derived types either have their own with
// a different meaning, or none at all. See InterfaceType#ASSET_INTERFACE_NAME.
assetFlatFields = new HashMap<>(fileAssetTypeFields);
assetFlatFields.remove(FILEASSET_DESCRIPTION_FIELD_VAR);

// The binary's own properties, flattened onto the asset so a client need not descend into
// the binary field to reach them. Ten of the twelve DotBinary carries: `title` and
// `modDate` are deliberately absent, because on a contentlet those names already mean the
// contentlet's title and modification date. Declaring them here would make the same name
// answer with the FILE's title on an asset and the CONTENT's title everywhere else — the
// class of silent divergence this work exists to avoid. Both remain reachable through the
// binary field itself. See issue #34540.
final AssetBinaryPropertyDataFetcher binaryProperty = new AssetBinaryPropertyDataFetcher();
assetFlatFields.put("name", new TypeFetcher(GraphQLString, binaryProperty));
assetFlatFields.put("size", new TypeFetcher(GraphQLLong, binaryProperty));
assetFlatFields.put("mime", new TypeFetcher(GraphQLString, binaryProperty));
assetFlatFields.put("versionPath", new TypeFetcher(GraphQLString, binaryProperty));
assetFlatFields.put("idPath", new TypeFetcher(GraphQLString, binaryProperty));
assetFlatFields.put("path", new TypeFetcher(GraphQLString, binaryProperty));
assetFlatFields.put("sha256", new TypeFetcher(GraphQLString, binaryProperty));
assetFlatFields.put("isImage", new TypeFetcher(GraphQLBoolean, binaryProperty));
assetFlatFields.put("width", new TypeFetcher(GraphQLLong, binaryProperty));
assetFlatFields.put("height", new TypeFetcher(GraphQLLong, binaryProperty));

final Map<String, TypeFetcher> siteTypeFields = new HashMap<>(ContentFields.getContentFields());
siteTypeFields.remove(HOST_KEY); // remove myself
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.dotmarketing.util.Logger;
import com.dotmarketing.util.UtilMethods;
import graphql.kickstart.servlet.AbstractGraphQLHttpServlet;
import graphql.kickstart.execution.GraphQLQueryInvoker;
import graphql.kickstart.servlet.GraphQLConfiguration;
import io.vavr.Lazy;
import io.vavr.control.Try;
Expand Down Expand Up @@ -43,6 +44,11 @@ protected GraphQLConfiguration getConfiguration() {
.with(new DotGraphQLSchemaProvider())
.with(List.of(new DotGraphQLServletListener()))
.with(new DotGraphQLContextBuilder())
// Reports narrowing clauses that matched nothing, through the response's
// `extensions`. A client that ignores extensions is unaffected. See #34540.
.with(GraphQLQueryInvoker.newBuilder()
.withInstrumentation(new UnmatchedTypeConditionInstrumentation())
.build())
.build();
}

Expand Down
61 changes: 61 additions & 0 deletions dotCMS/src/main/java/com/dotcms/graphql/InterfaceType.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,22 @@ public enum InterfaceType {
public static final String FORM_INTERFACE_NAME = "FormBaseType";
public static final String DOTASSET_INTERFACE_NAME = "DotAssetBaseType";

/**
* Describes the content an Image or File field points at, by whatever content type it actually
* is. Unlike the interfaces above it is not tied to a single base type: an asset-pointing field
* can hold either DOTASSET- or FILEASSET-based content — an Image field resolves a FileAsset
* perfectly well today — so neither {@link #DOTASSET_INTERFACE_NAME} nor
* {@link #FILE_INTERFACE_NAME} alone can describe what such a field may return.
*
* <p><b>It deliberately keeps the name the flat object type used to carry.</b> That name is
* what clients already write in {@code ... on DotFileasset} clauses, and a fragment on the
* position's own interface always matches — so those clauses keep working and keep returning
* data. Introducing a new name instead would have left every such clause invalid. The kind
* does change, from object to interface, which query text does not notice but client code
* generators do: anyone with generated types must regenerate them. See #34540.
*/
public static final String ASSET_INTERFACE_NAME = "DotFileasset";

public static final String DOT_CONTENTLET = "DotContentlet";

static {
Expand All @@ -87,6 +103,11 @@ public enum InterfaceType {
final Map<String, TypeFetcher> fileAssetFields = new HashMap<>(contentFields);
addBaseTypeFields(fileAssetFields, ImmutableFileAssetContentType.builder().name("dummy")
.build().requiredFields());
// Same flat properties the asset interface carries. Every possible type of this interface
// already has them, and leaving them off would make `fileName` selectable on the concrete
// types and on the asset interface but not here — the same query changing shape depending
// on which clause a client happens to narrow through. See #34540.
fileAssetFields.putAll(CustomFieldType.getAssetFlatFields());
interfaceTypes.put("FILEASSET", createInterfaceType(FILE_INTERFACE_NAME, fileAssetFields, new ContentResolver()));

final Map<String, TypeFetcher> pageAssetFields = new HashMap<>(contentFields);
Expand Down Expand Up @@ -124,7 +145,47 @@ public enum InterfaceType {
final Map<String, TypeFetcher> dotAssetFields = new HashMap<>(contentFields);
addBaseTypeFields(dotAssetFields, ImmutableDotAssetContentType.builder().name("dummy")
.build().requiredFields());
// See the note on the FILEASSET interface above: DOTASSET content has no stored file name,
// but every DOTASSET-derived object type carries the synthesized one, so the interface must
// too or `fileName` is reachable everywhere except through this clause.
dotAssetFields.putAll(CustomFieldType.getAssetFlatFields());
interfaceTypes.put("DOTASSET", createInterfaceType(DOTASSET_INTERFACE_NAME, dotAssetFields, new ContentResolver()));

// Carries the common content fields plus the flat properties an asset-pointing field has
// always exposed, so that retyping such a field to this interface leaves those selections
// working. Every implementing object type must therefore carry them too -- synthesized for
// DOTASSET-derived types, already present on FILEASSET-derived ones.
//
// `description` is deliberately absent. FILEASSET content stores one; DOTASSET content does
// not, and the flat view answered it with the contentlet title instead. Declaring it here
// would make DOTASSET-derived types answer with their own stored description -- the same
// name quietly returning a different value, which is the one outcome this work refuses.
// It is reachable, correctly, through a narrowing clause on the concrete type.
final Map<String, TypeFetcher> assetContentFields = new HashMap<>(contentFields);
assetContentFields.putAll(CustomFieldType.getAssetFlatFields());

assetContentInterface = createInterfaceType(ASSET_INTERFACE_NAME,
assetContentFields, new ContentResolver());
}

private static GraphQLInterfaceType assetContentInterface;


/**
* @return the interface describing what an asset-pointing field returns, implemented by every
* content type derived from either asset base type.
*/
public static GraphQLInterfaceType getAssetContentInterface() {
return assetContentInterface;
}

/**
* @return whether content of this base type can sit behind an Image or File field, and must
* therefore implement {@link #getAssetContentInterface()}.
*/
public static boolean isAssetBaseType(final BaseContentType baseContentType) {
return BaseContentType.DOTASSET == baseContentType
|| BaseContentType.FILEASSET == baseContentType;
}

/**
Expand Down
Loading
Loading