diff --git a/examples/microprofile-graphql/README.md b/examples/microprofile-graphql/README.md new file mode 100644 index 0000000000..c925d922f6 --- /dev/null +++ b/examples/microprofile-graphql/README.md @@ -0,0 +1,12 @@ +Blaze-Persistence Examples Spring Data GraphQL +========== +This is an GraphQL sample application showcasing how the Blaze-Persistence GraphQL +integration can be used to develop GraphQL interfaces with ease. + +## How to use it? + +Just run `mvn spring-boot:run` and navigate to http://localhost:8080/graphiql where you can run your GraphQL queries. +You will see that the queries exposed there use the GraphQL Relay spec to implement pagination through cursors. +You can take a look at the generated queries to further understand what happens or you take a look into the [documentation](https://persistence.blazebit.com/documentation/core/manual/en_US/index.html#anchor-keyset-pagination) for more information on the topic. + +Another thing that you will see from the generated queries is that the GraphQL selection list actually alters what is selected in the SQL query! \ No newline at end of file diff --git a/examples/microprofile-graphql/pom.xml b/examples/microprofile-graphql/pom.xml new file mode 100644 index 0000000000..7527ba99c6 --- /dev/null +++ b/examples/microprofile-graphql/pom.xml @@ -0,0 +1,182 @@ + + + + + 4.0.0 + + com.blazebit + blaze-persistence-examples + 1.6.0-SNAPSHOT + ../pom.xml + + + blaze-persistence-examples-microprofile-graphql + jar + + Blazebit Persistence Examples MicroProfile GraphQL + + + com.blazebit.persistence.examples.microprofile.graphql + UTF-8 + 1.8 + 1.8 + + 1.10.3.Final + 5.4.22.Final + + + + + + io.quarkus + quarkus-bom + ${version.quarkus} + pom + import + + + + + + + io.quarkus + quarkus-smallrye-graphql + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-hibernate-orm + + + com.blazebit + blaze-persistence-integration-quarkus + + + com.graphql-java + graphql-java + 16.1 + + + ${project.groupId} + blaze-persistence-integration-hibernate-base + + + ${project.groupId} + blaze-persistence-integration-hibernate-5.3 + + + ${project.groupId} + blaze-persistence-integration-graphql + + + ${project.groupId} + blaze-persistence-integration-jaxrs + + + + + junit + junit + test + + + io.quarkus + quarkus-jdbc-h2 + + + io.quarkus + quarkus-test-h2 + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + + + javax.xml.bind + jaxb-api + ${version.jaxb} + + + com.sun.xml.bind + jaxb-core + ${version.jaxb} + + + com.sun.xml.bind + jaxb-impl + ${version.jaxb} + + + javax.annotation + javax.annotation-api + ${version.annotation} + + + + + + + io.quarkus + quarkus-maven-plugin + ${version.quarkus} + true + + + + build + generate-code + generate-code-tests + + + + + + maven-surefire-plugin + + + org.jboss.logmanager.LogManager + + true + + + h2 + + + + + + + \ No newline at end of file diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/Application.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/Application.java new file mode 100644 index 0000000000..3579bf65d6 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/Application.java @@ -0,0 +1,56 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Cat; +import com.blazebit.persistence.examples.microprofile.graphql.model.Person; +import io.quarkus.runtime.StartupEvent; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@ApplicationScoped +public class Application { + + @Inject + EntityManager em; + + @Transactional + void onStart(@Observes StartupEvent ev) { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List people = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + Person p = new Person("Person " + i); + people.add(p); + em.persist(p); + } + for (int i = 0; i < 100; i++) { + Cat c = new Cat("Cat " + i, random.nextInt(20), people.get(random.nextInt(4))); + em.persist(c); + } + } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLEntityViewSupport.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLEntityViewSupport.java new file mode 100644 index 0000000000..3fbb60c84e --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLEntityViewSupport.java @@ -0,0 +1,477 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql; + +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.DefaultKeyset; +import com.blazebit.persistence.DefaultKeysetPage; +import com.blazebit.persistence.KeysetPage; +import com.blazebit.persistence.PaginatedCriteriaBuilder; +import com.blazebit.persistence.integration.graphql.GraphQLCursor; +import com.blazebit.persistence.integration.graphql.GraphQLCursorObjectInputStream; +import com.blazebit.persistence.view.ConfigurationProperties; +import com.blazebit.persistence.view.EntityViewSetting; +import graphql.schema.DataFetchingEnvironment; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLType; +import graphql.schema.SelectedField; + +import java.io.ByteArrayInputStream; +import java.io.ObjectInputStream; +import java.io.Serializable; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * A support class to interact with entity views in a GraphQL environment. + * + * @author Christian Beikov + * @since 1.4.0 + */ +public class GraphQLEntityViewSupport { + + /** + * Default name for the page size field. + */ + public static final String PAGE_SIZE_NAME = "first"; + /** + * Default name for the page size field. + */ + public static final String RELAY_LAST_NAME = "last"; + /** + * Default name for the offset field. + */ + public static final String OFFSET_NAME = "offset"; + /** + * Default name for the before cursor field. + */ + public static final String BEFORE_CURSOR_NAME = "before"; + /** + * Default name for the after cursor field. + */ + public static final String AFTER_CURSOR_NAME = "after"; + /** + * Default name for the edges field. + */ + public static final String EDGES_NAME = "edges"; + /** + * Default name for the node field. + */ + public static final String EDGE_NODE_NAME = "node"; + /** + * Default name for the cursor field. + */ + public static final String EDGE_CURSOR_NAME = "cursor"; + /** + * Default name for the total count field. + */ + public static final String TOTAL_COUNT_NAME = "totalCount"; + + private final Map> typeNameToClass; + private final Set serializableBasicTypes; + + private final String pageSizeName; + private final String offsetName; + private final String beforeCursorName; + private final String afterCursorName; + private final String totalCountName; + private final String pageElementsName; + private final String pageElementObjectName; + private final String elementCursorName; + + /** + * Creates a new {@link GraphQLEntityViewSupport} instance with the given type name to class mapping and serializable basic type whitelist. + * It uses the GraphQL Relay specification names for accessing page info fields for paginated settings. + * + * @param typeNameToClass The mapping from GraphQL type names to entity view class names + * @param serializableBasicTypes The whitelist of allowed serializable basic types to use for cursor deserialization + */ + public GraphQLEntityViewSupport(Map> typeNameToClass, Set serializableBasicTypes) { + this(typeNameToClass, serializableBasicTypes, PAGE_SIZE_NAME, OFFSET_NAME, BEFORE_CURSOR_NAME, AFTER_CURSOR_NAME, TOTAL_COUNT_NAME, EDGES_NAME, EDGE_NODE_NAME, EDGE_CURSOR_NAME); + } + + /** + * Creates a new {@link GraphQLEntityViewSupport} instance with the given type name to class mapping and serializable basic type whitelist. + * @param typeNameToClass The mapping from GraphQL type names to entity view class names + * @param serializableBasicTypes The whitelist of allowed serializable basic types to use for cursor deserialization + * @param pageSizeName The name of the page size field + * @param offsetName The name of the offset field + * @param beforeCursorName The name of the beforeCursor field + * @param afterCursorName The name of the afterCursor field + * @param totalCountName The name of the totalCount field + * @param pageElementsName The name of the elements field + * @param pageElementObjectName The name of the element object field within elements + * @param elementCursorName The name of the cursor field within elements + */ + public GraphQLEntityViewSupport(Map> typeNameToClass, Set serializableBasicTypes, String pageSizeName, String offsetName, String beforeCursorName, String afterCursorName, String totalCountName, String pageElementsName, String pageElementObjectName, String elementCursorName) { + this.pageSizeName = pageSizeName; + this.offsetName = offsetName; + this.beforeCursorName = beforeCursorName; + this.afterCursorName = afterCursorName; + this.totalCountName = totalCountName; + this.pageElementsName = pageElementsName; + this.typeNameToClass = typeNameToClass; + this.serializableBasicTypes = serializableBasicTypes; + this.pageElementObjectName = pageElementObjectName; + this.elementCursorName = elementCursorName; + } + + /** + * Returns a new entity view setting for the given data fetching environment. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createPaginatedSetting(DataFetchingEnvironment dataFetchingEnvironment) { + return createPaginatedSetting(dataFetchingEnvironment, pageElementsName); + } + + /** + * Returns a new entity view setting for the given data fetching environment. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param elementRoot The field at which to find the elements for fetch extraction + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createPaginatedSetting(DataFetchingEnvironment dataFetchingEnvironment, String elementRoot) { + String typeName = getElementTypeName(dataFetchingEnvironment, elementRoot); + Class entityViewClass = typeNameToClass.get(typeName); + if (entityViewClass == null) { + throw new IllegalArgumentException("No entity view type is registered for the name: " + typeName); + } + return createPaginatedSetting((Class) entityViewClass, dataFetchingEnvironment, elementRoot); + } + + /** + * Like calling {{@link #createSetting(Class, DataFetchingEnvironment, String)}} with the configured page elements name. + * + * @param entityViewClass The entity view class + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createPaginatedSetting(Class entityViewClass, DataFetchingEnvironment dataFetchingEnvironment) { + return createPaginatedSetting(entityViewClass, dataFetchingEnvironment, pageElementsName); + } + + /** + * Like calling {{@link #createSetting(Class, DataFetchingEnvironment, String)}} with the configured page elements name. + * + * @param entityViewClass The entity view class + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param elementRoot The field at which to find the elements for fetch extraction + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createPaginatedSetting(Class entityViewClass, DataFetchingEnvironment dataFetchingEnvironment, String elementRoot) { + String objectRoot; + if (pageElementObjectName == null || pageElementObjectName.isEmpty()) { + objectRoot = elementRoot; + } else if (elementRoot == null || elementRoot.isEmpty()) { + objectRoot = pageElementObjectName; + } else { + objectRoot = elementRoot + "/" + pageElementObjectName; + } + EntityViewSetting> setting = (EntityViewSetting>) (EntityViewSetting) createSetting(entityViewClass, dataFetchingEnvironment, objectRoot); + + Map> map = dataFetchingEnvironment.getSelectionSet().getFieldsGroupedByResultKey(); + + if (!map.containsKey(totalCountName)) { + setting.setProperty(ConfigurationProperties.PAGINATION_DISABLE_COUNT_QUERY, Boolean.TRUE); + } + + if (elementCursorName != null && !elementCursorName.isEmpty()) { + String elementCursorPath; + if (elementRoot == null || elementRoot.isEmpty()) { + elementCursorPath = elementCursorName; + } else { + elementCursorPath = elementRoot + "/" + elementCursorName; + } + + if (map.containsKey(elementCursorPath)) { + setting.setProperty(ConfigurationProperties.PAGINATION_EXTRACT_ALL_KEYSETS, Boolean.TRUE); + } + } + return setting; + } + + /** + * Like calling {{@link #createSetting(DataFetchingEnvironment, String)}} with an empty element root. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createSetting(DataFetchingEnvironment dataFetchingEnvironment) { + return createSetting(dataFetchingEnvironment, ""); + } + + /** + * Returns a new entity view setting for the given data fetching environment and element root. + * Determines the entity view class by using the type of the element root as resolved with the {@link DataFetchingEnvironment}. + * Like calling {{@link #createSetting(Class, DataFetchingEnvironment, String)}} with the explicit entity view class. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param elementRoot The field at which to find the elements for fetch extraction + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createSetting(DataFetchingEnvironment dataFetchingEnvironment, String elementRoot) { + String typeName = getElementTypeName(dataFetchingEnvironment, elementRoot); + Class entityViewClass = typeNameToClass.get(typeName); + if (entityViewClass == null) { + throw new IllegalArgumentException("No entity view type is registered for the name: " + typeName); + } + return createSetting((Class) entityViewClass, dataFetchingEnvironment, elementRoot); + } + + public String getElementTypeName(DataFetchingEnvironment dataFetchingEnvironment, String elementRoot) { + GraphQLType type = dataFetchingEnvironment.getFieldType(); + if (type instanceof GraphQLNonNull) { + type = ((GraphQLNonNull) type).getWrappedType(); + } + if (type instanceof GraphQLList) { + type = ((GraphQLList) type).getWrappedType(); + } + if (type instanceof GraphQLNonNull) { + type = ((GraphQLNonNull) type).getWrappedType(); + } + + String[] parts = elementRoot.split("/"); + for (int i = 0; i < parts.length; i++) { + if (type instanceof GraphQLObjectType) { + if (parts[i].length() > 0) { + type = ((GraphQLObjectType) type).getFieldDefinition(parts[i]).getType(); + } + if (type instanceof GraphQLNonNull) { + type = ((GraphQLNonNull) type).getWrappedType(); + } + if (type instanceof GraphQLList) { + type = ((GraphQLList) type).getWrappedType(); + } + if (type instanceof GraphQLNonNull) { + type = ((GraphQLNonNull) type).getWrappedType(); + } + } else { + throw new IllegalArgumentException("The element root part '" + parts[i] + "' wasn't found on type: " + type); + } + } + + return ((GraphQLNamedType) type).getName(); + } + + /** + * Like calling {{@link #createSetting(Class, DataFetchingEnvironment, String)}} with an empty element root. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createSetting(Class entityViewClass, DataFetchingEnvironment dataFetchingEnvironment) { + return createSetting(entityViewClass, dataFetchingEnvironment, ""); + } + + /** + * Returns a new entity view setting for the given data fetching environment. + * + * @param entityViewClass The entity view class + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param elementRoot The field at which to find the elements for fetch extraction + * @param The entity view type + * @return the entity view setting + */ + public EntityViewSetting> createSetting(Class entityViewClass, DataFetchingEnvironment dataFetchingEnvironment, String elementRoot) { + KeysetPage keysetPage = extractKeysetPage(dataFetchingEnvironment); + EntityViewSetting> setting; + boolean forceUseKeyset = false; + if (keysetPage == null) { + setting = EntityViewSetting.create(entityViewClass); + } else { + Integer pageSize = dataFetchingEnvironment.getArgument(pageSizeName); + Integer offset = dataFetchingEnvironment.getArgument(offsetName); + Integer last = dataFetchingEnvironment.getArgument(RELAY_LAST_NAME); + + if (pageSize == null) { + pageSize = Integer.MAX_VALUE; + } else if (pageSize < 0) { + throw new RuntimeException("Illegal negative " + pageSizeName + " parameter: " + pageSize); + } + if (last != null) { + if (last < 0) { + throw new RuntimeException("Illegal negative " + RELAY_LAST_NAME + " parameter: " + last); + } + if (Integer.MAX_VALUE == pageSize) { + pageSize = last; + if (offset == null) { + forceUseKeyset = true; + } + } else { + if (offset == null) { + offset = pageSize - last; + pageSize = last; + } else { + offset += pageSize - last; + pageSize = last; + } + if (offset < 0) { + offset = 0; + } + if (keysetPage.getLowest() != null || keysetPage.getHighest() != null) { + forceUseKeyset = true; + } + } + } else if (offset == null) { + forceUseKeyset = true; + } else if (offset < 0) { + throw new RuntimeException("Illegal negative " + offsetName + " parameter: " + offset); + } + setting = (EntityViewSetting>) (EntityViewSetting) EntityViewSetting.create(entityViewClass, offset == null ? 0 : (int) offset, (int) pageSize); + setting.withKeysetPage(keysetPage); + } + + if (forceUseKeyset) { + setting.setProperty(ConfigurationProperties.PAGINATION_FORCE_USE_KEYSET, true); + } + applyFetches(dataFetchingEnvironment, setting, elementRoot); + return setting; + } + + /** + * Extracts the {@link KeysetPage} from the {@link DataFetchingEnvironment} by extracting page size and offset, + * as well as deserializing before or afterCursors. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @return the {@link KeysetPage} or null + */ + public KeysetPage extractKeysetPage(DataFetchingEnvironment dataFetchingEnvironment) { + Integer pageSize = dataFetchingEnvironment.getArgument(pageSizeName); + Integer last = dataFetchingEnvironment.getArgument(RELAY_LAST_NAME); + String beforeCursor = dataFetchingEnvironment.getArgument(beforeCursorName); + String afterCursor = dataFetchingEnvironment.getArgument(afterCursorName); + if (pageSize == null && last == null && beforeCursor == null && afterCursor == null) { + return null; + } else { + KeysetPage keysetPage; + + if (beforeCursor != null) { + if (afterCursor != null) { + throw new RuntimeException("Can't provide both beforeCursor and afterCursor!"); + } + GraphQLCursor cursor = deserialize(beforeCursor); + keysetPage = new DefaultKeysetPage(cursor.getOffset(), cursor.getPageSize(), new DefaultKeyset(cursor.getTuple()), null); + } else if (afterCursor != null) { + if (last != null) { + // Using an after cursor with last does not make sense, so skip using the cursor + // The only problem with that is, that the cursor could refer to the last element + // If that is the case, we would still get a result, which is IMO an edge case and can be ignored + keysetPage = new DefaultKeysetPage(0, last, new DefaultKeyset(null), null); + } else { + GraphQLCursor cursor = deserialize(afterCursor); + keysetPage = new DefaultKeysetPage(cursor.getOffset(), cursor.getPageSize(), null, new DefaultKeyset(cursor.getTuple())); + } + } else if (pageSize != null) { + keysetPage = new DefaultKeysetPage(0, pageSize, null, null); + } else { + // Keyset with empty tuple is a special case for traversing the result list in reverse order + keysetPage = new DefaultKeysetPage(0, last, new DefaultKeyset(null), null); + } + + return keysetPage; + } + } + + /** + * Like {{@link #applyFetches(DataFetchingEnvironment, EntityViewSetting, String)}} but with an empty element root. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param setting The entity view setting + */ + public void applyFetches(DataFetchingEnvironment dataFetchingEnvironment, EntityViewSetting setting) { + applyFetches(dataFetchingEnvironment, setting, ""); + } + + /** + * Applies the fetches to the {@link EntityViewSetting} as requested by the selection set of {@link DataFetchingEnvironment} + * and interpreting the only paths below the given element root. + * + * @param dataFetchingEnvironment The GraphQL data fetching environment + * @param setting The entity view setting + * @param elementRoot The element root + */ + public void applyFetches(DataFetchingEnvironment dataFetchingEnvironment, EntityViewSetting setting, String elementRoot) { + String prefix = elementRoot == null || elementRoot.isEmpty() ? "" : elementRoot + "/"; + Collection keys = dataFetchingEnvironment.getSelectionSet().getFieldsGroupedByResultKey().keySet(); + StringBuilder sb = new StringBuilder(); + OUTER: for (String key : keys) { + sb.setLength(0); + for (int i = 0; i < key.length(); i++) { + final char c = key.charAt(i); + if (i < prefix.length()) { + if (c != prefix.charAt(i)) { + continue OUTER; + } else { + continue; + } + } + if (c == '/') { + sb.append('.'); + } else { + sb.append(c); + } + } + if (sb.length() > 0) { + setting.fetch(sb.toString()); + } + } + } + + /** + * Deserializes the given Base64 encoded cursor to a {@link GraphQLCursor} object. + * + * @param beforeCursor The Base64 encoded cursor + * @return a new cursor + */ + protected GraphQLCursor deserialize(String beforeCursor) { + try (ObjectInputStream ois = new GraphQLCursorObjectInputStream(Base64.getDecoder().wrap(new ByteArrayInputStream(beforeCursor.getBytes())), serializableBasicTypes)) { + int offset = ois.read(); + int pageSize = ois.read(); + Serializable[] tuple = (Serializable[]) ois.readObject(); + return new GraphQLCursor(offset, pageSize, tuple); + } catch (Exception e) { + throw new RuntimeException("Couldn't read cursor", e); + } + } + + /** + * Returns the entity view class for the given GraphQL type name. + * + * @param typeName The GraphQL type name + * @return the entity view class or null + */ + public Class getEntityViewClass(String typeName) { + return typeNameToClass.get(typeName); + } + +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLEntityViewSupportFactory.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLEntityViewSupportFactory.java new file mode 100644 index 0000000000..ad1aacdd6b --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLEntityViewSupportFactory.java @@ -0,0 +1,408 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql; + +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.metamodel.ManagedViewType; +import com.blazebit.persistence.view.metamodel.SingularAttribute; +import graphql.language.NonNullType; +import graphql.language.ObjectTypeDefinition; +import graphql.language.Type; +import graphql.language.TypeName; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.TypeResolver; +import graphql.schema.idl.TypeDefinitionRegistry; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * A factory for creating a support class for using entity views in a GraphQL environment. + * + * @author Christian Beikov + * @since 1.4.0 + */ +public class GraphQLEntityViewSupportFactory { + + private static final Map, String> TYPES; + + static { + Map, String> types = new HashMap<>(); + types.put(boolean.class, "Boolean"); + types.put(Boolean.class, "Boolean"); + types.put(byte.class, "Byte"); + types.put(Byte.class, "Byte"); + types.put(short.class, "Short"); + types.put(Short.class, "Short"); + types.put(int.class, "Int"); + types.put(Integer.class, "Int"); + types.put(long.class, "Long"); + types.put(Long.class, "Long"); + types.put(float.class, "Float"); + types.put(Float.class, "Float"); + types.put(double.class, "Float"); + types.put(Double.class, "Float"); + types.put(BigInteger.class, "BigInteger"); + types.put(BigDecimal.class, "BigDecimal"); + types.put(char.class, "Char"); + types.put(Character.class, "Char"); + types.put(String.class, "String"); + TYPES = types; + } + + private boolean defineNormalTypes; + private boolean defineRelayTypes; + private Boolean implementRelayNode; + private boolean defineRelayNodeIfNotExist = false; + + /** + * Creates a new entity view support factory with the given configuration. + * + * @param defineNormalTypes If true, generates normal types for managed view types + * @param defineRelayTypes If true, generates relay types for managed views + */ + public GraphQLEntityViewSupportFactory(boolean defineNormalTypes, boolean defineRelayTypes) { + this.defineNormalTypes = defineNormalTypes; + this.defineRelayTypes = defineRelayTypes; + } + + /** + * Returns true if normal types should be defined. + * + * @return true if normal types should be defined + */ + public boolean isDefineNormalTypes() { + return defineNormalTypes; + } + + /** + * Sets whether normal types should be defined. + * + * @param defineNormalTypes Whether normal types should be defined + */ + public void setDefineNormalTypes(boolean defineNormalTypes) { + this.defineNormalTypes = defineNormalTypes; + } + + /** + * Returns true if Relay types should be defined. + * + * @return true if Relay types should be defined + */ + public boolean isDefineRelayTypes() { + return defineRelayTypes; + } + + /** + * Sets whether Relay types should be defined. + * + * @param defineRelayTypes Whether Relay types should be defined + */ + public void setDefineRelayTypes(boolean defineRelayTypes) { + this.defineRelayTypes = defineRelayTypes; + } + + /** + * Returns true if node types should implement the Relay Node interface. + * + * @return true if node types should implement the Relay Node interface + */ + public boolean isImplementRelayNode() { + return implementRelayNode == null ? defineRelayNodeIfNotExist : implementRelayNode; + } + + /** + * Sets whether Relay Node type should be implemented by node types. + * + * @param implementRelayNode Whether Relay Node type should be implemented by node types + */ + public void setImplementRelayNode(boolean implementRelayNode) { + this.implementRelayNode = implementRelayNode; + } + + /** + * Returns true if the Relay Node interface should be created if not found in the type registry. + * + * @return true if the Relay Node interface should be created if not found in the type registry + */ + public boolean isDefineRelayNodeIfNotExist() { + return defineRelayNodeIfNotExist; + } + + /** + * Sets whether the Relay Node interface should be defined if not found in the type registry. + * + * @param defineRelayNodeIfNotExist Whether the Relay Node interface should be defined if not found in the type registry + */ + public void setDefineRelayNodeIfNotExist(boolean defineRelayNodeIfNotExist) { + this.defineRelayNodeIfNotExist = defineRelayNodeIfNotExist; + } + + /** + * Returns a new {@link GraphQLEntityViewSupport} after registering the entity view types from {@link EntityViewManager} + * on the given {@link TypeDefinitionRegistry}. + * + * @param schemaBuilder The registry to register types + * @param entityViewManager The entity view manager + * @return a new {@link GraphQLEntityViewSupport} + */ + public GraphQLEntityViewSupport create(GraphQLSchema.Builder schemaBuilder, EntityViewManager entityViewManager) { + GraphQLSchema schema = schemaBuilder.build(); + Map> typeNameToClass = new HashMap<>(); + GraphQLCodeRegistry codeRegistry = schema.getCodeRegistry(); + schema.getType("CatSimpleView"); + for (GraphQLType type : schema.getAdditionalTypes()) { + if (type instanceof GraphQLInterfaceType) { + TypeResolver typeResolver = codeRegistry.getTypeResolver((GraphQLInterfaceType) type); + } + } + +// for (ManagedViewType managedView : entityViewManager.getMetamodel().getManagedViews()) { +// String typeName = getObjectTypeName(managedView); +// List fieldDefinitions = new ArrayList<>(); +// for (MethodAttribute attribute : managedView.getAttributes()) { +// FieldDefinition fieldDefinition = new FieldDefinition(attribute.getName()); +// Type type; +// if (attribute instanceof SingularAttribute) { +// SingularAttribute singularAttribute = (SingularAttribute) attribute; +// if (singularAttribute.isId()) { +// type = getIdType(typeRegistry, singularAttribute); +// } else { +// type = getElementType(typeRegistry, singularAttribute); +// } +// } else if (attribute instanceof MapAttribute) { +// MapAttribute mapAttribute = (MapAttribute) attribute; +// type = getEntryType(typeRegistry, attribute, getKeyType(typeRegistry, mapAttribute), getElementType(typeRegistry, mapAttribute)); +// } else { +// type = new ListType(getElementType(typeRegistry, (PluralAttribute) attribute)); +// } +// fieldDefinition.setType(type); +// fieldDefinitions.add(fieldDefinition); +// } +// List implementsTypes = new ArrayList<>(0); +// List directives = new ArrayList<>(0); +// addObjectTypeDefinition(typeRegistry, typeNameToClass, managedView, new ObjectTypeDefinition(typeName, implementsTypes, directives, fieldDefinitions)); +// } + + Set serializableBasicTypes = new HashSet<>(); +// for (javax.persistence.metamodel.Type basicType : entityViewManager.getService(EntityMetamodel.class).getBasicTypes()) { +// for (Class superType : ReflectionUtils.getSuperTypes(basicType.getJavaType())) { +// serializableBasicTypes.add(superType.getName()); +// } +// +// serializableBasicTypes.add(basicType.getJavaType().getName()); +// } +// +// serializableBasicTypes.add(Serializable[].class.getName()); +// serializableBasicTypes.add(GraphQLCursor.class.getName()); + return new GraphQLEntityViewSupport( typeNameToClass, serializableBasicTypes); + } + +// protected void addObjectTypeDefinition(TypeDefinitionRegistry typeRegistry, Map> typeNameToClass, ManagedViewType managedView, ObjectTypeDefinition objectTypeDefinition) { +// if (isDefineNormalTypes()) { +// registerManagedViewType(typeRegistry, typeNameToClass, managedView, objectTypeDefinition); +// } +// if (isDefineRelayTypes()) { +// List implementTypes = new ArrayList<>(objectTypeDefinition.getImplements()); +// if (isImplementRelayNode()) { +// implementTypes.add(new TypeName("Node")); +// } +// ObjectTypeDefinition nodeType = new ObjectTypeDefinition(objectTypeDefinition.getName() + "Node", implementTypes, objectTypeDefinition.getDirectives(), objectTypeDefinition.getFieldDefinitions()); +// +// if (!typeRegistry.getType("Node").isPresent() && isImplementRelayNode() && isDefineRelayNodeIfNotExist()) { +// List nodeFields = new ArrayList<>(4); +// nodeFields.add(new FieldDefinition("id", new NonNullType(new TypeName("ID")))); +// typeRegistry.add(new InterfaceTypeDefinition("Node", nodeFields, new ArrayList<>())); +// } +// +// List edgeFields = new ArrayList<>(2); +// edgeFields.add(new FieldDefinition("node", new NonNullType(new TypeName(nodeType.getName())))); +// edgeFields.add(new FieldDefinition("cursor", new NonNullType(new TypeName("String")))); +// ObjectTypeDefinition edgeType = new ObjectTypeDefinition(objectTypeDefinition.getName() + "Edge", new ArrayList<>(), new ArrayList<>(), edgeFields); +// +// List connectionFields = new ArrayList<>(2); +// connectionFields.add(new FieldDefinition("edges", new ListType(new TypeName(edgeType.getName())))); +// connectionFields.add(new FieldDefinition("pageInfo", new NonNullType(new TypeName("PageInfo")))); +// connectionFields.add(new FieldDefinition("totalCount", new NonNullType(new TypeName("Int")))); +// ObjectTypeDefinition connectionType = new ObjectTypeDefinition(objectTypeDefinition.getName() + "Connection", new ArrayList<>(), new ArrayList<>(), connectionFields); +// +// if (!typeRegistry.getType("PageInfo").isPresent() && isDefineRelayNodeIfNotExist()) { +// List pageInfoFields = new ArrayList<>(4); +// pageInfoFields.add(new FieldDefinition("hasNextPage", new NonNullType(new TypeName("Boolean")))); +// pageInfoFields.add(new FieldDefinition("hasPreviousPage", new NonNullType(new TypeName("Boolean")))); +// pageInfoFields.add(new FieldDefinition("startCursor", new TypeName("String"))); +// pageInfoFields.add(new FieldDefinition("endCursor", new TypeName("String"))); +// typeRegistry.add(new ObjectTypeDefinition("PageInfo", new ArrayList<>(), new ArrayList<>(), pageInfoFields)); +// } +// +// registerManagedViewType(typeRegistry, typeNameToClass, managedView, nodeType); +// registerManagedViewType(typeRegistry, typeNameToClass, managedView, edgeType); +// registerManagedViewType(typeRegistry, typeNameToClass, managedView, connectionType); +// } +// } + + protected void registerManagedViewType(TypeDefinitionRegistry typeRegistry, Map> typeNameToClass, ManagedViewType managedView, ObjectTypeDefinition objectTypeDefinition) { + typeRegistry.add(objectTypeDefinition); + Class old; + if ((old = typeNameToClass.put(objectTypeDefinition.getName(), managedView.getJavaType())) != null) { + throw new IllegalArgumentException("Type with name '" + objectTypeDefinition.getName() + "' is registered multiple times: [" + old.getName() + ", " + managedView.getJavaType().getName() + "]!"); + } + } + + /** + * Return the GraphQL id type for the given singular attribute. + * + * @param typeRegistry The type registry + * @param singularAttribute The singular attribute + * @return The type + */ + protected Type getIdType(TypeDefinitionRegistry typeRegistry, SingularAttribute singularAttribute) { + return new NonNullType(new TypeName("ID")); + } + +// /** +// * Return the GraphQL entry type for the given map attribute with the given key and value types. +// * +// * @param typeRegistry The type registry +// * @param attribute The map attribute +// * @param key The key type +// * @param value The value type +// * @return The type +// */ +// protected Type getEntryType(TypeDefinitionRegistry typeRegistry, MethodAttribute attribute, Type key, Type value) { +// String entryName = getObjectTypeName(attribute.getDeclaringType()) + StringUtils.firstToLower(attribute.getName()) + "Entry"; +// List fields = new ArrayList<>(); +// fields.add(new FieldDefinition("key", key)); +// fields.add(new FieldDefinition("value", value)); +// typeRegistry.add(new ObjectTypeDefinition(entryName, new ArrayList<>(), new ArrayList<>(), fields)); +// return new ListType(new TypeName(entryName)); +// } + + /** + * Returns the GraphQL type name for the given managed view type. + * + * @param type The managed view type + * @return The GraphQL type name + */ + protected String getObjectTypeName(ManagedViewType type) { + return type.getJavaType().getSimpleName(); + } + + /** + * Returns the GraphQL type name for the given java type. + * + * @param type The java type + * @return The GraphQL type name + */ + protected String getTypeName(Class type) { + return type.getSimpleName(); + } + + /** + * Return the GraphQL type for the given managed view type. + * + * @param type The managed view type + * @return The type + */ + protected Type getObjectType(ManagedViewType type) { + return new TypeName(getObjectTypeName(type)); + } + +// /** +// * Return the GraphQL type for the given singular attribute. +// * +// * @param typeRegistry The type registry +// * @param singularAttribute The singular attribute +// * @return The type +// */ +// protected Type getElementType(TypeDefinitionRegistry typeRegistry, SingularAttribute singularAttribute) { +// com.blazebit.persistence.view.metamodel.Type elementType = singularAttribute.getType(); +// if (elementType.getMappingType() == com.blazebit.persistence.view.metamodel.Type.MappingType.BASIC) { +// return getScalarType(typeRegistry, elementType.getJavaType()); +// } else { +// return getObjectType((ManagedViewType) elementType); +// } +// } +// +// /** +// * Return the GraphQL type for the given plural attribute. +// * +// * @param typeRegistry The type registry +// * @param pluralAttribute The plural attribute +// * @return The type +// */ +// protected Type getElementType(TypeDefinitionRegistry typeRegistry, PluralAttribute pluralAttribute) { +// com.blazebit.persistence.view.metamodel.Type elementType = pluralAttribute.getElementType(); +// if (elementType.getMappingType() == com.blazebit.persistence.view.metamodel.Type.MappingType.BASIC) { +// return getScalarType(typeRegistry, elementType.getJavaType()); +// } else { +// return getObjectType((ManagedViewType) elementType); +// } +// } +// +// /** +// * Return the GraphQL type for the key of the given map attribute. +// * +// * @param typeRegistry The type registry +// * @param mapAttribute The map attribute +// * @return The type +// */ +// protected Type getKeyType(TypeDefinitionRegistry typeRegistry, MapAttribute mapAttribute) { +// com.blazebit.persistence.view.metamodel.Type elementType = mapAttribute.getKeyType(); +// if (elementType.getMappingType() == com.blazebit.persistence.view.metamodel.Type.MappingType.BASIC) { +// return getScalarType(typeRegistry, elementType.getJavaType()); +// } else { +// return getObjectType((ManagedViewType) elementType); +// } +// } +// +// /** +// * Return the GraphQL type for the given scalar java type. +// * +// * @param typeRegistry The type registry +// * @param javaType The java type +// * @return The type +// */ +// protected Type getScalarType(TypeDefinitionRegistry typeRegistry, Class javaType) { +// String typeName = TYPES.get(javaType); +// if (typeName == null) { +// if (javaType.isEnum()) { +// typeName = getTypeName(javaType); +// if (typeRegistry.getType(typeName).isPresent()) { +// throw new IllegalArgumentException("Enum type with name '" + typeName + "' is registered multiple times: " + javaType.getName()); +// } else { +// List enumValueDefinitions = new ArrayList<>(); +// for (Enum enumConstant : (Enum[]) javaType.getEnumConstants()) { +// enumValueDefinitions.add(new EnumValueDefinition(enumConstant.name(), new ArrayList<>(0))); +// } +// +// typeRegistry.add(new EnumTypeDefinition(typeName, enumValueDefinitions, new ArrayList<>(0))); +// } +// } else { +// typeName = "String"; +// } +// } +// return new TypeName(typeName); +// } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLProducer.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLProducer.java new file mode 100644 index 0000000000..32fe2a1a20 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/GraphQLProducer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql; + +import graphql.schema.GraphQLSchema; +import io.smallrye.graphql.schema.model.Operation; +import io.smallrye.graphql.schema.model.Reference; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.event.Observes; +import javax.enterprise.inject.Produces; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@ApplicationScoped +public class GraphQLProducer { + +// @Inject +// EntityViewManager evm; + + Set interfaceReferences = new HashSet<>(); + GraphQLEntityViewSupport graphQLEntityViewSupport; + + void alterOperation(@Observes Operation operation) { +// List references = new ArrayList<>(); +// references.add(operation.getReference()); +// while (!references.isEmpty()) { +// Reference reference = references.remove(references.size() - 1); +// if (reference.getType() == ReferenceType.INTERFACE) { +// interfaceReferences.add(reference); +// } +// Map parametrizedTypeArguments = reference.getParametrizedTypeArguments(); +// if (parametrizedTypeArguments != null) { +// references.addAll(parametrizedTypeArguments.values()); +// } +// } + } + + void configure(@Observes GraphQLSchema.Builder schemaBuilder) { +// for (Reference interfaceReference : interfaceReferences) { +// schemaBuilder.additionalType( +// GraphQLObjectType.newObject() +// .name(interfaceReference.getName() + "Impl") +// .withInterface(GraphQLTypeReference.typeRef(interfaceReference.getName())) +// .build() +// ); +// } + + GraphQLEntityViewSupportFactory graphQLEntityViewSupportFactory = new GraphQLEntityViewSupportFactory(true, true); + graphQLEntityViewSupportFactory.setImplementRelayNode(false); + graphQLEntityViewSupportFactory.setDefineRelayNodeIfNotExist(true); + this.graphQLEntityViewSupport = graphQLEntityViewSupportFactory.create(schemaBuilder, null); + } + + @Produces + @ApplicationScoped + GraphQLEntityViewSupport graphQLEntityViewSupport() { + return graphQLEntityViewSupport; + } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/model/Cat.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/model/Cat.java new file mode 100644 index 0000000000..278dfc5516 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/model/Cat.java @@ -0,0 +1,118 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import javax.persistence.Entity; +import javax.persistence.FetchType; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.ManyToMany; +import javax.persistence.ManyToOne; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@Entity +public class Cat { + + @Id + @GeneratedValue + private Long id; + private String name; + private Integer age; + @JsonIgnoreProperties("kittens") + @ManyToOne(fetch = FetchType.LAZY, optional = true) + private Person owner; + @ManyToOne(fetch = FetchType.LAZY, optional = true) + private Cat mother; + @ManyToOne(fetch = FetchType.LAZY, optional = true) + private Cat father; + @JsonIgnore + @ManyToMany + private Set kittens = new HashSet<>(); + + public Cat() { + } + + public Cat(String name, Integer age, Person owner) { + this.name = name; + this.age = age; + this.owner = owner; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public Person getOwner() { + return owner; + } + + public void setOwner(Person owner) { + this.owner = owner; + } + + public Cat getMother() { + return mother; + } + + public void setMother(Cat mother) { + this.mother = mother; + } + + public Cat getFather() { + return father; + } + + public void setFather(Cat father) { + this.father = father; + } + + public Set getKittens() { + return kittens; + } + + public void setKittens(Set kittens) { + this.kittens = kittens; + } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/model/Person.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/model/Person.java new file mode 100644 index 0000000000..621a37e24e --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/model/Person.java @@ -0,0 +1,70 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.model; + +import javax.persistence.Entity; +import javax.persistence.GeneratedValue; +import javax.persistence.Id; +import javax.persistence.OneToMany; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@Entity +public class Person { + + @Id + @GeneratedValue + private Long id; + private String name; + @OneToMany(mappedBy = "owner") + private Set kittens = new HashSet<>(); + + public Person() { + } + + public Person(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getKittens() { + return kittens; + } + + public void setKittens(Set kittens) { + this.kittens = kittens; + } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/repository/CatViewRepository.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/repository/CatViewRepository.java new file mode 100644 index 0000000000..6115706fb3 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/repository/CatViewRepository.java @@ -0,0 +1,50 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.repository; + +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.EntityViewSetting; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import java.util.List; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@ApplicationScoped +public class CatViewRepository { + + @Inject + EntityManager em; + @Inject + CriteriaBuilderFactory cbf; + @Inject + EntityViewManager evm; + + public T findById(EntityViewSetting> setting, Long id) { + return evm.find(em, setting, id); + } + + public List findAll(EntityViewSetting setting) { + return evm.applySetting(setting, cbf.create(em, evm.getMetamodel().managedView(setting.getEntityViewClass()).getEntityClass())).getResultList(); + } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/resource/CatResource.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/resource/CatResource.java new file mode 100644 index 0000000000..8b980c7a7c --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/resource/CatResource.java @@ -0,0 +1,116 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.resource; + +import com.blazebit.persistence.CriteriaBuilder; +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.examples.microprofile.graphql.GraphQLEntityViewSupport; +import com.blazebit.persistence.examples.microprofile.graphql.model.Cat; +import com.blazebit.persistence.examples.microprofile.graphql.view.CatSimpleView; +import com.blazebit.persistence.examples.microprofile.graphql.view.CatUpdateView; +import com.blazebit.persistence.examples.microprofile.graphql.view.CatWithOwnerView; +import com.blazebit.persistence.integration.graphql.GraphQLRelayConnection; +import com.blazebit.persistence.integration.jaxrs.EntityViewId; +import com.blazebit.persistence.view.EntityViewManager; +import com.blazebit.persistence.view.EntityViewSetting; +import com.blazebit.persistence.view.Sorters; +import graphql.schema.DataFetchingEnvironment; +import io.smallrye.graphql.api.Context; +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.transaction.Transactional; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.List; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +@GraphQLApi +@Path("cats") +public class CatResource { + + @Inject + EntityManager em; + @Inject + EntityViewManager evm; + @Inject + CriteriaBuilderFactory cbf; + @Inject + Context context; + @Inject + GraphQLEntityViewSupport graphQLEntityViewSupport; + + @Transactional + @PUT + @Path("{id}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public CatSimpleView updateCat(@EntityViewId("id") CatUpdateView catUpdateView) { + evm.save(em, catUpdateView); + return evm.find(em, CatSimpleView.class, catUpdateView.getId()); + } + + @POST + @Transactional + @Produces(MediaType.APPLICATION_JSON) + public Response addCat(CatUpdateView view) { + evm.save(em, view); + return Response.created(URI.create("/cats/" + view.getId())).build(); + } + + @GET + @Query("cats") + @Transactional + @Produces(MediaType.APPLICATION_JSON) + public List getCats() { + CriteriaBuilder cb = cbf.create(em, Cat.class); + return evm.applySetting(graphQLEntityViewSupport.createSetting(context.unwrap(DataFetchingEnvironment.class)), cb).getResultList(); + } + + @Query("catViews") + public GraphQLRelayConnection findAll() { + CriteriaBuilder cb = cbf.create(em, Cat.class); + EntityViewSetting> setting = graphQLEntityViewSupport.createSetting( + context.unwrap(DataFetchingEnvironment.class) + ); + setting.addAttributeSorter("id", Sorters.ascending()); + if (setting.getMaxResults() == 0) { + return new GraphQLRelayConnection<>(); + } + return new GraphQLRelayConnection<>(evm.applySetting(setting, cb).getResultList()); + } + + @DELETE + @Transactional + public Response clearCats() { + cbf.delete(em, Cat.class).executeUpdate(); + return Response.ok().build(); + } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/resource/PersonResource.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/resource/PersonResource.java new file mode 100644 index 0000000000..b6850a6290 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/resource/PersonResource.java @@ -0,0 +1,78 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.resource; + +import java.net.URI; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.transaction.Transactional; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import com.blazebit.persistence.CriteriaBuilderFactory; +import com.blazebit.persistence.examples.microprofile.graphql.model.Person; +import com.blazebit.persistence.examples.microprofile.graphql.view.PersonCreateView; +import com.blazebit.persistence.examples.microprofile.graphql.view.PersonSimpleView; +import com.blazebit.persistence.examples.microprofile.graphql.view.PersonUpdateView; +import com.blazebit.persistence.integration.jaxrs.EntityViewId; +import com.blazebit.persistence.view.EntityViewManager; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +@Path("persons") +public class PersonResource { + + @Inject + private EntityManager em; + @Inject + private EntityViewManager evm; + @Inject + private CriteriaBuilderFactory cbf; + + @Transactional + @PUT + @Path("{id}") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public PersonSimpleView updatePerson(@EntityViewId("id") PersonUpdateView personUpdateView) { + evm.save(em, personUpdateView); + return evm.find(em, PersonSimpleView.class, personUpdateView.getId()); + } + + @POST + @Transactional + @Produces(MediaType.APPLICATION_JSON) + public Response addPerson(PersonCreateView view) { + evm.save(em, view); + return Response.created(URI.create("/persons/" + view.getId())).build(); + } + + @DELETE + @Transactional + public Response clearPersons() { + cbf.delete(em, Person.class).executeUpdate(); + return Response.ok().build(); + } +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatCreateView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatCreateView.java new file mode 100644 index 0000000000..3185ffe06d --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatCreateView.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Cat; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@CreatableEntityView +@EntityView(Cat.class) +public interface CatCreateView extends CatUpdateView { + + PersonIdView getOwner(); + + void setOwner(PersonIdView owner); +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatSimpleView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatSimpleView.java new file mode 100644 index 0000000000..3780c2a1c2 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatSimpleView.java @@ -0,0 +1,34 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Cat; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@EntityView(Cat.class) +public interface CatSimpleView { + + @IdMapping + Long getId(); + + String getName(); +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatUpdateView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatUpdateView.java new file mode 100644 index 0000000000..15c6956154 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatUpdateView.java @@ -0,0 +1,36 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Cat; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.UpdatableEntityView; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@UpdatableEntityView +@EntityView(Cat.class) +public interface CatUpdateView extends CatSimpleView { + + void setName(String name); + + Integer getAge(); + + void setAge(Integer age); +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatWithOwnerView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatWithOwnerView.java new file mode 100644 index 0000000000..29784ad79d --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/CatWithOwnerView.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Cat; +import com.blazebit.persistence.view.EntityView; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@EntityView(Cat.class) +public interface CatWithOwnerView extends CatSimpleView { + + PersonSimpleView getOwner(); + +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonCreateView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonCreateView.java new file mode 100644 index 0000000000..bb6f24d1dc --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonCreateView.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Person; +import com.blazebit.persistence.view.CreatableEntityView; +import com.blazebit.persistence.view.EntityView; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +@CreatableEntityView +@EntityView(Person.class) +public interface PersonCreateView extends PersonSimpleView { + + void setId(Long id); + + void setName(String name); +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonIdView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonIdView.java new file mode 100644 index 0000000000..499d16cb66 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonIdView.java @@ -0,0 +1,33 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Person; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.IdMapping; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@EntityView(Person.class) +public interface PersonIdView { + + @IdMapping + Long getId(); + +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonSimpleView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonSimpleView.java new file mode 100644 index 0000000000..dd09b75622 --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonSimpleView.java @@ -0,0 +1,31 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Person; +import com.blazebit.persistence.view.EntityView; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +@EntityView(Person.class) +public interface PersonSimpleView extends PersonIdView { + + String getName(); + +} diff --git a/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonUpdateView.java b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonUpdateView.java new file mode 100644 index 0000000000..f9db90a90a --- /dev/null +++ b/examples/microprofile-graphql/src/main/java/com/blazebit/persistence/examples/microprofile/graphql/view/PersonUpdateView.java @@ -0,0 +1,32 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.view; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Person; +import com.blazebit.persistence.view.EntityView; +import com.blazebit.persistence.view.UpdatableEntityView; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +@UpdatableEntityView +@EntityView(Person.class) +public interface PersonUpdateView extends PersonSimpleView { + + void setName(String name); +} diff --git a/examples/microprofile-graphql/src/main/resources/META-INF/beans.xml b/examples/microprofile-graphql/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..09247f2450 --- /dev/null +++ b/examples/microprofile-graphql/src/main/resources/META-INF/beans.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/examples/microprofile-graphql/src/main/resources/application.properties b/examples/microprofile-graphql/src/main/resources/application.properties new file mode 100644 index 0000000000..52be77dcfc --- /dev/null +++ b/examples/microprofile-graphql/src/main/resources/application.properties @@ -0,0 +1,6 @@ +quarkus.datasource.db-kind=h2 +quarkus.datasource.username=username-default +quarkus.datasource.jdbc.url=jdbc:h2:mem:test +quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.log.sql=false +quarkus.smallrye-graphql.events.enabled=true \ No newline at end of file diff --git a/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/AbstractSampleTest.java b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/AbstractSampleTest.java new file mode 100644 index 0000000000..dac703c901 --- /dev/null +++ b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/AbstractSampleTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.repository; + +import com.blazebit.persistence.examples.microprofile.graphql.model.Cat; +import com.blazebit.persistence.examples.microprofile.graphql.model.Person; +import org.junit.Before; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +public abstract class AbstractSampleTest { + + @Inject + DataInitializer dataInitializer; + + @Before + public void init() { + dataInitializer.run(em -> { + ThreadLocalRandom random = ThreadLocalRandom.current(); + List people = new ArrayList<>(); + for (int i = 0; i < 4; i++) { + Person p = new Person("Person " + i); + people.add(p); + em.persist(p); + } + for (int i = 0; i < 100; i++) { + Cat c = new Cat("Cat " + i, random.nextInt(20), people.get(random.nextInt(4))); + em.persist(c); + } + }); + } + + @Transactional + @ApplicationScoped + public static class DataInitializer { + + @Inject + EntityManager em; + + public void run(Consumer c) { + c.accept(em); + } + } +} diff --git a/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/H2DatabaseTestResource.java b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/H2DatabaseTestResource.java new file mode 100644 index 0000000000..bb6b18d0cb --- /dev/null +++ b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/H2DatabaseTestResource.java @@ -0,0 +1,54 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.repository; + +import java.sql.SQLException; +import java.util.Collections; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.h2.tools.Server; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +public class H2DatabaseTestResource implements QuarkusTestResourceLifecycleManager { + + private Server tcpServer; + + @Override + public Map start() { + + try { + tcpServer = Server.createTcpServer(); + tcpServer.start(); + System.out.println("[INFO] H2 database started in TCP server mode; server status: " + tcpServer.getStatus()); + } catch (SQLException e) { + throw new RuntimeException(e); + } + return Collections.emptyMap(); + } + + @Override + public synchronized void stop() { + if (tcpServer != null) { + tcpServer.stop(); + System.out.println("[INFO] H2 database was shut down; server status: " + tcpServer.getStatus()); + tcpServer = null; + } + } +} diff --git a/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/SampleTest.java b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/SampleTest.java new file mode 100644 index 0000000000..ac0ca2015f --- /dev/null +++ b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/SampleTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.repository; + +import org.junit.Test; +import org.junit.jupiter.api.AfterEach; + +import io.quarkus.test.common.http.TestHTTPResource; + +import java.net.URI; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.CoreMatchers.is; + +/** + * @author Christian Beikov + * @since 1.4.0 + */ +public class SampleTest extends AbstractSampleTest { + + @TestHTTPResource + protected URI apiBaseUri; + + @AfterEach + public void clearData() { +// given().when().delete("/documents"); +// given().when().delete("/document-types"); +// given().when().delete("/persons"); + } + + @Test + public void testSanity() { + given() + .when().get("/graphql/schema.json") + .then() + .statusCode(200); + } + + @Test + public void testRequestScope() { + given() + .contentType("application/graphql") + .body(request(5, null)) + .when().post("/graphql") + .then() + .statusCode(200) + .body("size(data.findAll.edges)", is(5)); +// ResponseEntity response = this.restTemplate.postForEntity("/graphql", new HttpEntity<>(requestGraphQL, headers), JsonNode.class); +// +// JsonNode connection = response.getBody().get("data").get("findAll"); +// ArrayNode arrayNode = (ArrayNode) connection.get("edges"); +// List nodes = arrayNode.findValues("node"); +// +// assertEquals(5, nodes.size()); +// assertEquals("Cat 0", nodes.get(0).get("name").asText()); +// +// requestGraphQL = request(5, connection.get("pageInfo").get("endCursor").asText()); +// response = this.restTemplate.postForEntity("/graphql", new HttpEntity<>(requestGraphQL, headers), JsonNode.class); +// connection = response.getBody().get("data").get("findAll"); +// arrayNode = (ArrayNode) connection.get("edges"); +// nodes = arrayNode.findValues("node"); +// +// assertEquals(5, nodes.size()); +// assertEquals("Cat 5", nodes.get(0).get("name").asText()); + } + + static String request(int first, String after) { + String other = ""; + if (after != null) { + other = ", after: \"" + after + "\""; + } + String requestGraphQL = "query {\n" + + " findAll(first: " + first + other + "){\n" + + " edges {\n" + + " node {\n" + + " id\n" + + " name\n" + + " }\n" + + " }\n" + + " pageInfo {\n" + + " startCursor\n" + + " endCursor\n" + + " }\n" + + " }\n" + + "}"; + return requestGraphQL; + } +} diff --git a/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/TestResources.java b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/TestResources.java new file mode 100644 index 0000000000..7d7b26f3dc --- /dev/null +++ b/examples/microprofile-graphql/src/test/java/com/blazebit/persistence/examples/microprofile/graphql/repository/TestResources.java @@ -0,0 +1,26 @@ +/* + * Copyright 2014 - 2020 Blazebit. + * + * Licensed 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 com.blazebit.persistence.examples.microprofile.graphql.repository; + +import io.quarkus.test.common.QuarkusTestResource; + +/** + * @author Moritz Becker + * @since 1.5.0 + */ +@QuarkusTestResource(H2DatabaseTestResource.class) +public class TestResources { +} diff --git a/examples/pom.xml b/examples/pom.xml index f7924ba131..4c7b5ba74a 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -36,6 +36,7 @@ spring-hateoas it-service-management quarkus + microprofile-graphql \ No newline at end of file diff --git a/integration/graphql/pom.xml b/integration/graphql/pom.xml index 95e0f44afc..7322e415cd 100644 --- a/integration/graphql/pom.xml +++ b/integration/graphql/pom.xml @@ -52,6 +52,19 @@ + + org.jboss.jandex + jandex-maven-plugin + 1.0.8 + + + make-index + + jandex + + + + org.moditect moditect-maven-plugin diff --git a/integration/graphql/src/main/java/com/blazebit/persistence/integration/graphql/GraphQLRelayConnection.java b/integration/graphql/src/main/java/com/blazebit/persistence/integration/graphql/GraphQLRelayConnection.java index 60c7abfd59..55e9668ac4 100644 --- a/integration/graphql/src/main/java/com/blazebit/persistence/integration/graphql/GraphQLRelayConnection.java +++ b/integration/graphql/src/main/java/com/blazebit/persistence/integration/graphql/GraphQLRelayConnection.java @@ -22,6 +22,7 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Base64; +import java.util.Collections; import java.util.List; /** @@ -38,6 +39,15 @@ public class GraphQLRelayConnection implements Serializable { private final GraphQLRelayPageInfo pageInfo; private final long totalCount; + /** + * Creates an empty GraphQL page. + */ + public GraphQLRelayConnection() { + this.edges = Collections.emptyList(); + this.pageInfo = GraphQLRelayPageInfo.EMPTY; + this.totalCount = 0; + } + /** * Creates a new GraphQL page from the given list. * @@ -93,7 +103,7 @@ private static List> createEdges(List list, GraphQLRe * * @return the elements */ - public List> getElements() { + public List> getEdges() { return edges; } diff --git a/integration/graphql/src/main/resources/META-INF/beans.xml b/integration/graphql/src/main/resources/META-INF/beans.xml new file mode 100644 index 0000000000..09247f2450 --- /dev/null +++ b/integration/graphql/src/main/resources/META-INF/beans.xml @@ -0,0 +1,17 @@ + + + \ No newline at end of file