From 6694d9afa0bead63bafa3b42709da4aa5c5cb221 Mon Sep 17 00:00:00 2001 From: Serhii Plyhun Date: Wed, 18 Oct 2023 15:24:33 +0200 Subject: [PATCH 1/3] SUP-15971: Projects atteched to the (micro)schema --- .../data/dao/impl/SchemaDaoWrapperImpl.java | 2 +- .../mesh/graphql/filter/ProjectFilter.java | 58 +++++++++++++++++++ .../graphql/type/AbstractTypeProvider.java | 40 +++++++++++-- .../graphql/type/MicroschemaTypeProvider.java | 22 ++++--- .../mesh/graphql/type/SchemaTypeProvider.java | 30 +++++++++- 5 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ProjectFilter.java diff --git a/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/SchemaDaoWrapperImpl.java b/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/SchemaDaoWrapperImpl.java index 92212bcadd..6ed9b2b5e3 100644 --- a/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/SchemaDaoWrapperImpl.java +++ b/mdm/orientdb-wrapper/src/main/java/com/gentics/mesh/core/data/dao/impl/SchemaDaoWrapperImpl.java @@ -146,7 +146,7 @@ public Result findDraftFieldContainers(HibSchem @Override public Result findLinkedProjects(HibSchema schema) { - return new TraversalResult<>(getRoots(schema).stream().map(root -> root.getProject())); + return new TraversalResult<>(getRoots(schema).stream().map(root -> root.getProject()).filter(Objects::nonNull)); } @Override diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ProjectFilter.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ProjectFilter.java new file mode 100644 index 0000000000..7950ab0a8d --- /dev/null +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/filter/ProjectFilter.java @@ -0,0 +1,58 @@ +package com.gentics.mesh.graphql.filter; + +import java.util.ArrayList; +import java.util.List; + +import com.gentics.graphqlfilter.filter.FilterField; +import com.gentics.graphqlfilter.filter.MappedFilter; +import com.gentics.graphqlfilter.filter.StartMainFilter; +import com.gentics.graphqlfilter.filter.StringFilter; +import com.gentics.mesh.core.data.project.HibProject; + +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLTypeReference; + +/** + * Start project GraphQL filter + */ +public class ProjectFilter extends StartMainFilter { + + private static final String NAME = "ProjectFilter"; + + private static ProjectFilter instance; + public static synchronized ProjectFilter filter() { + if (instance == null) { + instance = new ProjectFilter(); + } + return instance; + } + + private final boolean byRef; + + private ProjectFilter() { + this(false); + } + + private ProjectFilter(boolean byRef) { + super(NAME, "Filters projects"); + this.byRef = byRef; + } + + @Override + public GraphQLInputType getType() { + if (byRef) { + return GraphQLTypeReference.typeRef(NAME); + } else { + return super.getType(); + } + } + + @Override + protected List> getFilters() { + List> filters = new ArrayList<>(); + filters.add(CommonFields.hibUuidFilter()); + filters.addAll(CommonFields.hibUserTrackingFilter()); + filters.add(new MappedFilter<>("name", "Filters by name", StringFilter.filter(), HibProject::getName)); + return filters; + } +} diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/AbstractTypeProvider.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/AbstractTypeProvider.java index 1f9ec8e6e2..52a265f9c2 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/AbstractTypeProvider.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/AbstractTypeProvider.java @@ -12,6 +12,7 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Function; @@ -21,6 +22,7 @@ import org.apache.commons.lang3.StringUtils; import com.gentics.graphqlfilter.filter.StartFilter; +import com.gentics.graphqlfilter.filter.StartMainFilter; import com.gentics.mesh.core.action.DAOActions; import com.gentics.mesh.core.data.HibBaseElement; import com.gentics.mesh.core.data.HibCoreElement; @@ -510,8 +512,30 @@ protected GraphQLFieldDefinition newElementField(String name, String description * @return */ protected GraphQLFieldDefinition newElementField(String name, String description, DAOActions actions, - String elementType, boolean hidePermissionsErrors) { - return newFieldDefinition() + String elementType, boolean hidePermissionsErrors) { + return newElementField(name, description, actions, elementType, hidePermissionsErrors, new GraphQLArgument[] {}); + } + + /** + * Create a new elements field which automatically allows to resolve the element using it's name or uuid, with the optional extra arguments (filters etc). + * + * @param name + * Name of the field + * @param description + * Description of the field + * @param actions + * DAO Actions for the type of elements + * @param elementType + * Type name of the element which can be loaded + * @param hidePermissionsErrors + * Does not show errors if the permission is missing. Useful for sensitive data (ex. fetching users by name) + * @param extraArgs + * Extra filters + * @return + */ + protected GraphQLFieldDefinition newElementField(String name, String description, DAOActions actions, + String elementType, boolean hidePermissionsErrors, GraphQLArgument... extraArgs) { + Builder fieldBuilder = newFieldDefinition() .name(name) .description(description) .argument(createUuidArg("Uuid of the " + name + ".")) @@ -526,7 +550,11 @@ protected GraphQLFieldDefinition newElementField(String name, String description } else { return handleUuidNameArgs(env, null, actions); } - }).build(); + }); + if (extraArgs != null) { + Arrays.stream(extraArgs).filter(Objects::nonNull).forEach(fieldBuilder::argument); + } + return fieldBuilder.build(); } /** @@ -571,12 +599,16 @@ protected DynamicStreamPageImpl fetchFilteredNodes(DataFetchingEnvi } protected DynamicStreamPageImpl applyNodeFilter(DataFetchingEnvironment env, Stream stream) { + return applyFilter(env, stream, NodeFilter.filter(env.getContext())); + } + + protected > DynamicStreamPageImpl applyFilter(DataFetchingEnvironment env, Stream stream, F filter) { Map filterArgument = env.getArgument("filter"); PagingParameters pagingInfo = getPagingInfo(env); GraphQLContext gc = env.getContext(); if (filterArgument != null) { - return new DynamicStreamPageImpl<>(stream, pagingInfo, NodeFilter.filter(gc).createPredicate(filterArgument)); + return new DynamicStreamPageImpl<>(stream, pagingInfo, filter.createPredicate(filterArgument)); } else { return new DynamicStreamPageImpl<>(stream, pagingInfo); } diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/MicroschemaTypeProvider.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/MicroschemaTypeProvider.java index 9ac769b7f5..40868c29f0 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/MicroschemaTypeProvider.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/MicroschemaTypeProvider.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Singleton; @@ -18,8 +19,10 @@ import org.apache.commons.lang3.StringUtils; import com.gentics.mesh.core.data.branch.HibBranch; +import com.gentics.mesh.core.data.dao.SchemaDao; import com.gentics.mesh.core.data.dao.UserDao; import com.gentics.mesh.core.data.perm.InternalPermission; +import com.gentics.mesh.core.data.project.HibProject; import com.gentics.mesh.core.data.schema.HibMicroschema; import com.gentics.mesh.core.data.schema.HibMicroschemaVersion; import com.gentics.mesh.core.db.Tx; @@ -27,8 +30,10 @@ import com.gentics.mesh.core.rest.microschema.impl.MicroschemaModelImpl; import com.gentics.mesh.core.rest.schema.FieldSchema; import com.gentics.mesh.core.rest.schema.change.impl.SchemaChangeModel; +import com.gentics.mesh.core.result.Result; import com.gentics.mesh.etc.config.MeshOptions; import com.gentics.mesh.graphql.context.GraphQLContext; +import com.gentics.mesh.graphql.filter.ProjectFilter; import com.gentics.mesh.json.JsonUtil; import graphql.schema.DataFetchingEnvironment; @@ -80,16 +85,19 @@ public GraphQLObjectType createType() { // .description schemaType.field(newFieldDefinition().name("description").description("Description of the microschema.").type(GraphQLString)); - schemaType.field(newPagingFieldWithFetcher("projects", "Projects that this schema is assigned to", (env) -> { + // .projects + schemaType.field(newPagingFieldWithFetcherBuilder("projects", "Load projects that this schema is attached to", env -> { GraphQLContext gc = env.getContext(); HibMicroschema microschema = env.getSource(); UserDao userDao = Tx.get().userDao(); - return microschema.findReferencedBranches().keySet().stream() - .map(HibBranch::getProject) - .distinct() - .filter(it -> userDao.hasPermission(gc.getUser(), it, InternalPermission.READ_PERM)) - .collect(Collectors.toList()); - }, PROJECT_REFERENCE_PAGE_TYPE_NAME)); + Stream projects = microschema.findReferencedBranches().keySet().stream() + .map(HibBranch::getProject) + .distinct() + .filter(it -> userDao.hasPermission(gc.getUser(), it, InternalPermission.READ_PERM)); + + return applyFilter(env, projects, ProjectFilter.filter()); + }, PROJECT_REFERENCE_PAGE_TYPE_NAME) + .argument(ProjectFilter.filter().createFilterArgument())); // .isEmpty schemaType.field(newFieldDefinition().name("isEmpty").type(GraphQLBoolean).dataFetcher((env) -> { diff --git a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/SchemaTypeProvider.java b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/SchemaTypeProvider.java index a2ad8fb6cb..0123e0192f 100644 --- a/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/SchemaTypeProvider.java +++ b/verticles/graphql/src/main/java/com/gentics/mesh/graphql/type/SchemaTypeProvider.java @@ -1,6 +1,7 @@ package com.gentics.mesh.graphql.type; import static com.gentics.mesh.graphql.type.NodeTypeProvider.NODE_PAGE_TYPE_NAME; +import static com.gentics.mesh.graphql.type.ProjectReferenceTypeProvider.PROJECT_REFERENCE_PAGE_TYPE_NAME; import static graphql.Scalars.GraphQLBoolean; import static graphql.Scalars.GraphQLString; import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition; @@ -16,7 +17,10 @@ import com.gentics.mesh.core.data.HibNamedElement; import com.gentics.mesh.core.data.dao.NodeDao; import com.gentics.mesh.core.data.dao.SchemaDao; +import com.gentics.mesh.core.data.dao.UserDao; import com.gentics.mesh.core.data.node.NodeContent; +import com.gentics.mesh.core.data.perm.InternalPermission; +import com.gentics.mesh.core.data.project.HibProject; import com.gentics.mesh.core.data.schema.HibSchema; import com.gentics.mesh.core.data.schema.HibSchemaVersion; import com.gentics.mesh.core.db.Tx; @@ -24,9 +28,11 @@ import com.gentics.mesh.core.rest.schema.FieldSchema; import com.gentics.mesh.core.rest.schema.SchemaVersionModel; import com.gentics.mesh.core.rest.schema.impl.SchemaModelImpl; +import com.gentics.mesh.core.result.Result; import com.gentics.mesh.etc.config.MeshOptions; import com.gentics.mesh.graphql.context.GraphQLContext; import com.gentics.mesh.graphql.filter.NodeFilter; +import com.gentics.mesh.graphql.filter.ProjectFilter; import com.gentics.mesh.json.JsonUtil; import graphql.schema.DataFetchingEnvironment; @@ -113,7 +119,6 @@ public GraphQLObjectType createType(GraphQLContext context) { GraphQLContext gc = env.getContext(); List languageTags = getLanguageArgument(env); ContainerType type = getNodeVersion(env); - SchemaDao schemaDao = tx.schemaDao(); Stream nodes = nodeDao.findAllContent(getSchemaContainerVersion(env), gc, languageTags, type); return applyNodeFilter(env, nodes); @@ -121,6 +126,18 @@ public GraphQLObjectType createType(GraphQLContext context) { .argument(NodeFilter.filter(context).createFilterArgument()) .argument(createLanguageTagArg(true))); + // .projects + schemaType.field(newPagingFieldWithFetcherBuilder("projects", "Load projects that this schema is attached to", env -> { + Tx tx = Tx.get(); + GraphQLContext gc = env.getContext(); + SchemaDao schemaDao = tx.schemaDao(); + UserDao userDao = tx.userDao(); + Result projects = schemaDao.findLinkedProjects(getSchemaContainer(env)); + + return applyFilter(env, projects.stream().filter(it -> userDao.hasPermission(gc.getUser(), it, InternalPermission.READ_PERM)), ProjectFilter.filter()); + }, PROJECT_REFERENCE_PAGE_TYPE_NAME) + .argument(ProjectFilter.filter().createFilterArgument())); + Builder fieldListBuilder = newObject().name(SCHEMA_FIELD_TYPE).description("List of schema fields"); // .name @@ -161,6 +178,17 @@ private HibSchemaVersion getSchemaContainerVersion(DataFetchingEnvironment env) } } + private HibSchema getSchemaContainer(DataFetchingEnvironment env) { + Object source = env.getSource(); + if (source instanceof HibSchemaVersion) { + return ((HibSchemaVersion) source).getSchemaContainer(); + } else if (source instanceof HibSchema) { + return ((HibSchema) source); + } else { + throw new RuntimeException("Invalid type {" + source + "}."); + } + } + private SchemaVersionModel loadModelWithFallback(DataFetchingEnvironment env) { Object source = env.getSource(); if (source instanceof HibSchema) { From 8ee7f7feee7b59cb2c86c15b2bcf95085df9d058 Mon Sep 17 00:00:00 2001 From: Serhii Plyhun Date: Wed, 18 Oct 2023 15:30:40 +0200 Subject: [PATCH 2/3] Changelog --- LTS-CHANGELOG.adoc | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LTS-CHANGELOG.adoc b/LTS-CHANGELOG.adoc index 6088a6a4a7..97ce60e5d5 100644 --- a/LTS-CHANGELOG.adoc +++ b/LTS-CHANGELOG.adoc @@ -17,6 +17,11 @@ include::content/docs/variables.adoc-include[] The LTS changelog lists releases which are only accessible via a commercial subscription. All fixes and changes in LTS releases will be released the next minor release. Changes from LTS 1.4.x will be included in release 1.5.0. +[[v1.10.18]] +== 1.10.18 (TBD) + +icon:check[] GraphQL: New filterable field 'projects' has been added to the output of the result of `schemas` and `microschemas` requests, depicting the projects that the (micro)schema is attached to. + [[v1.10.17]] == 1.10.17 (TBD) From d267ed0569a8008ee5011a8bffc165cdd38aaa5c Mon Sep 17 00:00:00 2001 From: Serhii Plyhun Date: Fri, 20 Oct 2023 15:02:40 +0200 Subject: [PATCH 3/3] Projects GQL tests --- .../core/graphql/GraphQLEndpointTest.java | 2 +- .../graphql/microschema-projects-query | 87 +++++++++++++++++-- .../resources/graphql/schema-projects-query | 87 +++++++++++++++++-- 3 files changed, 161 insertions(+), 15 deletions(-) diff --git a/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java b/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java index 8be8326dfb..a0d96deb66 100644 --- a/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java +++ b/tests/tests-core/src/main/java/com/gentics/mesh/core/graphql/GraphQLEndpointTest.java @@ -152,7 +152,7 @@ public static Collection paramData() { Arrays.asList("role-user-group-query", true, false, "draft"), Arrays.asList("group-query", true, false, "draft"), Arrays.asList("schema-query", true, false, "draft"), - // Arrays.asList("schema-projects-query", true, false, "draft"), + Arrays.asList("schema-projects-query", true, false, "draft"), Arrays.asList("microschema-query", true, false, "draft"), Arrays.asList("paging-query", true, false, "draft"), Arrays.asList("tagFamily-query", true, false, "draft"), diff --git a/tests/tests-core/src/main/resources/graphql/microschema-projects-query b/tests/tests-core/src/main/resources/graphql/microschema-projects-query index 377a954acc..bb1e10e28e 100644 --- a/tests/tests-core/src/main/resources/graphql/microschema-projects-query +++ b/tests/tests-core/src/main/resources/graphql/microschema-projects-query @@ -1,20 +1,93 @@ { - microschema(name:"vcard") { + vcard: microschema(name:"vcard") { projects { - # [$.data.microschema.projects.elements.size()=1] + # [$.data.vcard.projects.elements.size()=1] elements { - # [$.data.microschema.projects.elements[0].name=dummy] + # [$.data.vcard.projects.elements[0].name=dummy] name - # [$.data.microschema.projects.elements[0].uuid=] + # [$.data.vcard.projects.elements[0].uuid=] uuid - # [$.data.microschema.projects.elements[0].etag=] + # [$.data.vcard.projects.elements[0].etag=] etag - # [$.data.microschema.projects.elements[0].created=] + # [$.data.vcard.projects.elements[0].created=] created - # [$.data.microschema.projects.elements[0].edited=] + # [$.data.vcard.projects.elements[0].edited=] edited } } } + all: microschemas { + elements { + projects { + # [$.data.all.elements[0].projects.elements.size()=1] + elements { + # [$.data.all.elements[0].projects.elements[0].name=dummy] + name + # [$.data.all.elements[0].projects.elements[0].uuid=] + uuid + # [$.data.all.elements[0].projects.elements[0].etag=] + etag + # [$.data.all.elements[0].projects.elements[0].created=] + created + # [$.data.all.elements[0].projects.elements[0].edited=] + edited + } + } + } + } + vcard_bogus: microschema(name:"vcard") { + projects(filter: {name: {equals: "bogus"}}) { + # [$.data.vcard_bogus.projects.elements.size()=0] + elements { + uuid + } + } + } + all_bogus: microschemas { + elements { + projects(filter: {name: {equals: "bogus"}}) { + # [$.data.all_bogus.elements[0].projects.elements.size()=0] + elements { + uuid + } + } + } + } + vcard_dummy: microschema(name:"vcard") { + projects(filter: {name: {equals: "dummy"}}) { + # [$.data.vcard_dummy.projects.elements.size()=1] + elements { + # [$.data.vcard_dummy.projects.elements[0].name=dummy] + name + # [$.data.vcard_dummy.projects.elements[0].uuid=] + uuid + # [$.data.vcard_dummy.projects.elements[0].etag=] + etag + # [$.data.vcard_dummy.projects.elements[0].created=] + created + # [$.data.vcard_dummy.projects.elements[0].edited=] + edited + } + } + } + all_dummy: microschemas { + elements { + projects(filter: {name: {equals: "dummy"}}) { + # [$.data.all_dummy.elements[0].projects.elements.size()=1] + elements { + # [$.data.all_dummy.elements[0].projects.elements[0].name=dummy] + name + # [$.data.all_dummy.elements[0].projects.elements[0].uuid=] + uuid + # [$.data.all_dummy.elements[0].projects.elements[0].etag=] + etag + # [$.data.all_dummy.elements[0].projects.elements[0].created=] + created + # [$.data.all_dummy.elements[0].projects.elements[0].edited=] + edited + } + } + } + } } # [$.errors=] diff --git a/tests/tests-core/src/main/resources/graphql/schema-projects-query b/tests/tests-core/src/main/resources/graphql/schema-projects-query index e23fcafec5..9c58fa1d56 100644 --- a/tests/tests-core/src/main/resources/graphql/schema-projects-query +++ b/tests/tests-core/src/main/resources/graphql/schema-projects-query @@ -1,20 +1,93 @@ { - schema(name:"content") { + content: schema(name:"content") { projects { - # [$.data.schema.projects.elements.size()=1] + # [$.data.content.projects.elements.size()=1] elements { - # [$.data.schema.projects.elements[0].name=dummy] + # [$.data.content.projects.elements[0].name=dummy] name - # [$.data.schema.projects.elements[0].uuid=] + # [$.data.content.projects.elements[0].uuid=] uuid - # [$.data.schema.projects.elements[0].uuid=] + # [$.data.content.projects.elements[0].uuid=] etag - # [$.data.schema.projects.elements[0].uuid=] + # [$.data.content.projects.elements[0].uuid=] created - # [$.data.schema.projects.elements[0].uuid=] + # [$.data.content.projects.elements[0].uuid=] edited } } } + all: schemas { + elements { + projects { + # [$.data.all.elements[0].projects.elements.size()=1] + elements { + # [$.data.all.elements[0].projects.elements[0].name=dummy] + name + # [$.data.all.elements[0].projects.elements[0].uuid=] + uuid + # [$.data.all.elements[0].projects.elements[0].uuid=] + etag + # [$.data.all.elements[0].projects.elements[0].uuid=] + created + # [$.data.all.elements[0].projects.elements[0].uuid=] + edited + } + } + } + } + content_bogus: schema(name:"content") { + projects (filter: {name: {equals: "bogus"}}) { + # [$.data.content_bogus.projects.elements.size()=0] + elements { + name + } + } + } + all_bogus: schemas { + elements { + projects (filter: {name: {equals: "bogus"}}) { + # [$.data.all_bogus.elements[0].projects.elements.size()=0] + elements { + name + } + } + } + } + content_dummy: schema(name:"content") { + projects (filter: {name: {equals: "dummy"}}) { + # [$.data.content_dummy.projects.elements.size()=1] + elements { + # [$.data.content_dummy.projects.elements[0].name=dummy] + name + # [$.data.content_dummy.projects.elements[0].uuid=] + uuid + # [$.data.content_dummy.projects.elements[0].uuid=] + etag + # [$.data.content_dummy.projects.elements[0].uuid=] + created + # [$.data.content_dummy.projects.elements[0].uuid=] + edited + } + } + } + all_dummy: schemas { + elements { + projects (filter: {name: {equals: "dummy"}}) { + # [$.data.all_dummy.elements[0].projects.elements.size()=1] + elements { + # [$.data.all_dummy.elements[0].projects.elements[0].name=dummy] + name + # [$.data.all_dummy.elements[0].projects.elements[0].uuid=] + uuid + # [$.data.all_dummy.elements[0].projects.elements[0].uuid=] + etag + # [$.data.all_dummy.elements[0].projects.elements[0].uuid=] + created + # [$.data.all_dummy.elements[0].projects.elements[0].uuid=] + edited + } + } + } + } } # [$.errors=]