io.stargate.sgv2.graphql.schema.graphqlfirst.processor.CqlDirectives Maven / Gradle / Ivy
/*
* Copyright The Stargate Authors
*
* 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 io.stargate.sgv2.graphql.schema.graphqlfirst.processor;
import static graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION;
import static graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION;
import static graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT;
import static graphql.introspection.Introspection.DirectiveLocation.OBJECT;
import static graphql.schema.GraphQLArgument.newArgument;
import static graphql.schema.GraphQLDirective.newDirective;
import static graphql.schema.GraphQLEnumType.newEnum;
import static graphql.schema.GraphQLEnumValueDefinition.newEnumValueDefinition;
import static graphql.schema.GraphQLSchema.newSchema;
import com.google.common.collect.ImmutableSet;
import graphql.Scalars;
import graphql.schema.GraphQLArgument;
import graphql.schema.GraphQLDirective;
import graphql.schema.GraphQLEnumType;
import graphql.schema.GraphQLFieldDefinition;
import graphql.schema.GraphQLObjectType;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.SchemaPrinter;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.stargate.bridge.proto.QueryOuterClass.Consistency;
import io.stargate.sgv2.common.cql.builder.CollectionIndexingType;
import io.stargate.sgv2.common.cql.builder.Predicate;
import java.util.Set;
/**
* Builds all the GraphQL directives that can be used in a deployed schema to customize the CQL
* mapping.
*
* {@link #ALL_AS_STRING} and {@link #ALL_AS_REGISTRY} can be used to get all the directives at
* once. The other public constants represent the names of the directives and their arguments.
*/
public class CqlDirectives {
private static final GraphQLEnumType ENTITY_TARGET_ENUM =
newEnum()
.name("EntityTarget")
.description("The type of schema element a GraphQL object maps to")
.value(EntityModel.Target.TABLE.name())
.value(EntityModel.Target.UDT.name())
.build();
public static final String ENTITY = "cql_entity";
public static final String ENTITY_NAME = "name";
public static final String ENTITY_TARGET = "target";
private static final GraphQLDirective ENTITY_DIRECTIVE =
newDirective()
.name(ENTITY)
.description("Customizes the mapping of a GraphQL object to a CQL table or UDT")
.argument(
newArgument()
.name(ENTITY_NAME)
.type(Scalars.GraphQLString)
.description(
"A custom table or UDT name (otherwise it uses the same name as the object)")
.build())
.argument(
newArgument()
.name(ENTITY_TARGET)
.type(ENTITY_TARGET_ENUM)
.description("Whether the object maps to a CQL table (the default) or UDT")
.build())
.validLocation(OBJECT)
.build();
public static final String INPUT = "cql_input";
public static final String INPUT_NAME = "name";
private static final GraphQLDirective INPUT_DIRECTIVE =
newDirective()
.name(INPUT)
.description(
"Annotates a GraphQL object to trigger the generation of a matching input type.\n"
+ "The generated type will have the same fields (names and types), and can be "
+ "referenced in mutations that target the corresponding CQL table or UDT.")
.argument(
newArgument()
.name(INPUT_NAME)
.type(Scalars.GraphQLString)
.description(
"The name of the generated type.\n"
+ "If not specified, it will be generated by appending 'Input' to the "
+ "name of the original object")
.build())
.validLocation(OBJECT)
.build();
private static final GraphQLEnumType CLUSTERING_ORDER_ENUM =
newEnum()
.name("ClusteringOrder")
.description("The sorting order for clustering columns")
.value("ASC")
.value("DESC")
.build();
public static final String COLUMN = "cql_column";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_PARTITION_KEY = "partitionKey";
public static final String COLUMN_CLUSTERING_ORDER = "clusteringOrder";
public static final String COLUMN_TYPE_HINT = "typeHint";
private static final GraphQLDirective COLUMN_DIRECTIVE =
newDirective()
.name(COLUMN)
.description("Customizes the mapping of a GraphQL field to a CQL column (or UDT field)")
.argument(
newArgument()
.name(COLUMN_NAME)
.type(Scalars.GraphQLString)
.description(
"A custom column name (otherwise it uses the same name as the field)")
.build())
.argument(
newArgument()
.name(COLUMN_PARTITION_KEY)
.type(Scalars.GraphQLBoolean)
.description("Whether the column forms part of the partition key")
.build())
.argument(
newArgument()
.name(COLUMN_CLUSTERING_ORDER)
.type(CLUSTERING_ORDER_ENUM)
.description(
"Whether the column is a clustering column, and if so in which order")
.build())
.argument(
newArgument()
.name(COLUMN_TYPE_HINT)
.type(Scalars.GraphQLString)
.description(
"The CQL type to map to (e.g. `frozen>`).\n"
+ "Most of the time you don't need this, the CQL type will be inferred"
+ " from the GraphQL type. It is only needed for fine control over the "
+ "\"frozen-ness\" of columns, or if you want to map a GraphQL list to "
+ "a CQL set (instead of a list).")
.build())
.validLocation(FIELD_DEFINITION)
.build();
private static final GraphQLEnumType INDEX_TARGET_ENUM =
newEnum()
.name("IndexTarget")
.description("Which part of a collection field will be indexed.")
.value(
newEnumValueDefinition()
.name(CollectionIndexingType.VALUES.name())
.description(
"Indexes the values of a list field.\n"
+ "This is only allowed if the CQL type is not frozen.")
.build())
.value(
newEnumValueDefinition()
.name(CollectionIndexingType.FULL.name())
.description(
"Indexes the full collection for a list, set or map column.\n"
+ "This is only allowed if the CQL type is frozen.")
.build())
.build();
public static final String INDEX = "cql_index";
public static final String INDEX_NAME = "name";
public static final String INDEX_CLASS = "class";
public static final String INDEX_TARGET = "target";
public static final String INDEX_OPTIONS = "options";
private static final GraphQLDirective INDEX_DIRECTIVE =
newDirective()
.name(INDEX)
.description(
String.format(
"Requests the creation of a CQL index for a GraphQL object field.\n"
+ "This is only allowed for objects that map to CQL tables, and only for "
+ "non-partition-key fields (in other words, fields that have neither"
+ "`@%1$s.%2$s` nor `@%1$s.%3$s set).",
COLUMN, COLUMN_PARTITION_KEY, COLUMN_CLUSTERING_ORDER))
.argument(
newArgument()
.name(INDEX_NAME)
.type(Scalars.GraphQLString)
.description(
"A custom name for the index. If not specified, one will be generated.")
.build())
.argument(
newArgument()
.name(INDEX_CLASS)
.type(Scalars.GraphQLString)
.description(
"If the index is custom, the name of the index class to use. If not "
+ "specified, this will be a regular secondary index.")
.build())
.argument(
newArgument()
.name(INDEX_TARGET)
.type(INDEX_TARGET_ENUM)
.description(
"(Only used with list fields) Which part of the field to index. If not "
+ "specified, this will default to `VALUES`.")
.build())
.argument(
newArgument()
.name(INDEX_OPTIONS)
.type(Scalars.GraphQLString)
.description(
"Any custom options to pass to the index, in the format: "
+ "`'option1': 'value1', 'option2': 'value2'...`")
.build())
.validLocation(FIELD_DEFINITION)
.build();
public static final String PAGING_STATE = "cql_pagingState";
private static final GraphQLDirective PAGING_STATE_DIRECTIVE =
newDirective()
.name(PAGING_STATE)
.description(
"Annotates a query parameter to indicate that it will receive the paging state. "
+ "That parameter must have type `String`.")
.validLocation(ARGUMENT_DEFINITION)
.build();
public static final String PAYLOAD = "cql_payload";
private static final GraphQLDirective PAYLOAD_DIRECTIVE =
newDirective()
.name(PAYLOAD)
.description(
"Indicates that a type represents a \"payload\" object that will be used as the "
+ "argument or return type of a GraphQL operation. Such objects are NOT mapped "
+ "to a CQL table.")
.validLocations(OBJECT, INPUT_OBJECT)
.build();
private static final GraphQLEnumType QUERY_CONSISTENCY_ENUM =
newEnum()
.name("QueryConsistency")
.description("The consistency level of the CQL SELECT generated for a query.")
.value(Consistency.LOCAL_ONE.name())
.value(Consistency.LOCAL_QUORUM.name())
.value(Consistency.ALL.name())
.value(Consistency.SERIAL.name())
.value(Consistency.LOCAL_SERIAL.name())
.build();
public static final String SELECT = "cql_select";
public static final String SELECT_LIMIT = "limit";
public static final String SELECT_PAGE_SIZE = "pageSize";
public static final String SELECT_CONSISTENCY_LEVEL = "consistencyLevel";
private static final GraphQLDirective SELECT_DIRECTIVE =
newDirective()
.name(SELECT)
.description(
"Provides additional options to the CQL SELECT generated for a query.\n"
+ "This is only required if you pass arguments to the directive. Otherwise, "
+ "GraphQL queries are always mapped to SELECT implicitly.")
.argument(
newArgument()
.name(SELECT_LIMIT)
.type(Scalars.GraphQLInt)
.description("How many results to return overall.")
.build())
.argument(
newArgument()
.name(SELECT_PAGE_SIZE)
.type(Scalars.GraphQLInt)
.defaultValue(100)
.description(
"How many results to return at a time.\n"
+ "If there are more, paging can be implemented by:\n"
+ "\n"
+ String.format(
"* wrapping the return type into an object annotated with @%s, "
+ "that defines a field `pagingState: String`;\n",
PAYLOAD)
+ String.format(
"* adding a `String` parameter annotated with `@%s` to the query.\n",
PAGING_STATE)
+ "\n"
+ "Then the page state returned by each query can be reinjected into the "
+ "next query to get the next page.")
.build())
.argument(
newArgument()
.name(SELECT_CONSISTENCY_LEVEL)
.type(QUERY_CONSISTENCY_ENUM)
.description("The consistency level to use.")
.defaultValue(Consistency.LOCAL_QUORUM.name())
.build())
.validLocation(FIELD_DEFINITION)
.build();
private static final GraphQLEnumType MUTATION_CONSISTENCY_ENUM =
newEnum()
.name("MutationConsistency")
.description("The consistency level of the CQL query generated for a mutation.")
.value(Consistency.LOCAL_ONE.name())
.value(Consistency.LOCAL_QUORUM.name())
.value(Consistency.ALL.name())
.build();
private static final GraphQLEnumType SERIAL_CONSISTENCY_ENUM =
newEnum()
.name("SerialConsistency")
.description("The serial consistency level of the CQL query generated for a mutation.")
.value(Consistency.SERIAL.name())
.value(Consistency.LOCAL_SERIAL.name())
.build();
public static final String INSERT = "cql_insert";
public static final String INSERT_IF_NOT_EXISTS = "ifNotExists";
public static final String UPDATE = "cql_update";
public static final String UPDATE_OR_DELETE_TARGET_ENTITY = "targetEntity";
public static final String UPDATE_OR_DELETE_IF_EXISTS = "ifExists";
public static final String DELETE = "cql_delete";
public static final String MUTATION_CONSISTENCY_LEVEL = "consistencyLevel";
public static final String MUTATION_SERIAL_CONSISTENCY_LEVEL = "serialConsistency";
public static final String UPDATE_OR_INSERT_TTL = "ttl";
private static final GraphQLArgument MUTATION_CONSISTENCY_LEVEL_ARGUMENT =
newArgument()
.name(MUTATION_CONSISTENCY_LEVEL)
.type(MUTATION_CONSISTENCY_ENUM)
.description("The consistency level to use.")
.defaultValue(Consistency.LOCAL_QUORUM.name())
.build();
private static final GraphQLArgument MUTATION_SERIAL_CONSISTENCY_LEVEL_ARGUMENT =
newArgument()
.name(MUTATION_SERIAL_CONSISTENCY_LEVEL)
.type(SERIAL_CONSISTENCY_ENUM)
.description("The serial consistency level to use.")
.defaultValue(Consistency.SERIAL.name())
.build();
private static final GraphQLArgument UPDATE_OR_INSERT_TTL_ARGUMENT =
newArgument()
.name(UPDATE_OR_INSERT_TTL)
.type(Scalars.GraphQLString)
.description(
"The TTL to use.\n"
+ "If this is a raw integer, it will be interpreted as a number of seconds. "
+ "Otherwise, it must be a valid ISO-8601 duration string (note that the "
+ "minimum granularity is seconds, so if the duration has a nanosecond part it "
+ "will be truncated). The value must be between 0 and 2^31- 1 (both included).")
.build();
private static final GraphQLDirective INSERT_DIRECTIVE =
newDirective()
.name(INSERT)
.description(
"Indicates that a mutation should be mapped to a CQL INSERT query.\n"
+ "Note that this is not required if the mutation name starts with `insert` or "
+ "`create`.")
.argument(
newArgument()
.name(INSERT_IF_NOT_EXISTS)
.type(Scalars.GraphQLBoolean)
.defaultValue(false)
.description(
"What to do if the entity already exists.\n\n"
+ "* If `false`, the insert will overwrite any data that was already "
+ "present.\n"
+ "* If `true`, it won't, and the mutation will return the existing "
+ "entity. In this case, it is strongly recommended to wrap the response "
+ "in a payload object that also defines an `applied` field, for "
+ "example: `type InsertUserResponse @cql_payload { applied: Boolean!, "
+ "user: User! }`\n\n"
+ "Note that setting this flag to `true` might increase the latency of "
+ "the operation. It should not be used casually.\n"
+ "By convention, this flag will be set automatically if the mutation "
+ "name ends with `IfNotExists`.")
.build())
.argument(MUTATION_CONSISTENCY_LEVEL_ARGUMENT)
.argument(MUTATION_SERIAL_CONSISTENCY_LEVEL_ARGUMENT)
.argument(UPDATE_OR_INSERT_TTL_ARGUMENT)
.validLocation(FIELD_DEFINITION)
.build();
private static final GraphQLDirective UPDATE_DIRECTIVE =
newDirective()
.name(UPDATE)
.description(
"Indicates that a mutation should be mapped to a CQL UPDATE query.\n"
+ "Note that this is not required if the mutation name starts with `update`.")
.argument(
newArgument()
.name(UPDATE_OR_DELETE_TARGET_ENTITY)
.type(Scalars.GraphQLString)
.description(
"The name of the type to update.\n"
+ "This is only needed if the mutation takes individual key fields as "
+ "arguments (as opposed to an instance of the type).\n"
+ "This must be a type that maps to a table.")
.build())
.argument(
newArgument()
.name(UPDATE_OR_DELETE_IF_EXISTS)
.type(Scalars.GraphQLBoolean)
.defaultValue(false)
.description(
"Whether to check if the entity exists before updating.\n\n"
+ "Update mutations return the outcome of the update, either directly if "
+ "the mutation returns `Boolean`, or via the `applied` field if the "
+ "mutation returns a response payload object.\n\n"
+ "* If `ifExists` is `false`, the mutation will always return `true`, "
+ "whether it actually updated something or not.\n"
+ "* If `ifExists` is `true`, the mutation will return `true` if it "
+ "updated something, and `false`otherwise.\n\n"
+ "Note that setting this flag to `true` might increase the latency of "
+ "the operation. It should not be used casually.\n"
+ "By convention, this flag will be set automatically if the mutation "
+ "name ends with `IfExists`.")
.build())
.argument(MUTATION_CONSISTENCY_LEVEL_ARGUMENT)
.argument(MUTATION_SERIAL_CONSISTENCY_LEVEL_ARGUMENT)
.argument(UPDATE_OR_INSERT_TTL_ARGUMENT)
.validLocation(FIELD_DEFINITION)
.build();
private static final GraphQLDirective DELETE_DIRECTIVE =
newDirective()
.name(DELETE)
.description(
"Indicates that a mutation should be mapped to a CQL DELETE query.\n"
+ "Note that this is not required if the mutation name starts with `delete` or "
+ "`remove`.")
.argument(
newArgument()
.name(UPDATE_OR_DELETE_TARGET_ENTITY)
.type(Scalars.GraphQLString)
.description(
"The name of the type to delete.\n"
+ "This is only needed if the mutation takes individual key fields as "
+ "arguments (as opposed to an instance of the type).\n"
+ "This must be a type that maps to a table.")
.build())
.argument(
newArgument()
.name(UPDATE_OR_DELETE_IF_EXISTS)
.type(Scalars.GraphQLBoolean)
.defaultValue(false)
.description(
"Whether to check if the entity exists before deleting.\n\n"
+ "Delete mutations return the outcome of the deletion, either directly "
+ "if the mutation returns `Boolean`, or via the `applied` field if the "
+ "mutation returns a response payload object.\n\n"
+ "* If `ifExists` is `false`, the mutation will always return `true`, "
+ "whether it actually deleted something or not.\n"
+ "* If `ifExists` is `true`, the mutation will return `true` if it "
+ "deleted something, and `false` otherwise.\n\n"
+ "Note that setting this flag to `true` might increase the latency of "
+ "the operation. It should not be used casually.\n"
+ "By convention, this flag will be set automatically if the mutation "
+ "name ends with `IfExists`.")
.build())
.argument(MUTATION_CONSISTENCY_LEVEL_ARGUMENT)
.argument(MUTATION_SERIAL_CONSISTENCY_LEVEL_ARGUMENT)
.validLocation(FIELD_DEFINITION)
.build();
public static final String WHERE = "cql_where";
public static final String IF = "cql_if";
public static final String INCREMENT = "cql_increment";
public static final String WHERE_OR_IF_OR_INCREMENT_FIELD = "field";
public static final String WHERE_OR_IF_PREDICATE = "predicate";
public static final String INCREMENT_PREPEND = "prepend";
public static final String TIMESTAMP = "cql_timestamp";
private static final GraphQLEnumType PREDICATE_ENUM =
newEnum()
.name("Predicate")
.description(String.format("A predicate used in `@%s` to define a condition.", WHERE))
.value(Predicate.EQ.name())
.value(Predicate.IN.name())
.value(Predicate.LT.name())
.value(Predicate.GT.name())
.value(Predicate.LTE.name())
.value(Predicate.GTE.name())
.value(Predicate.CONTAINS.name())
.build();
private static final GraphQLDirective WHERE_DIRECTIVE =
newDirective()
.name(WHERE)
.description(
"Annotates a parameter to customize the WHERE condition that is generated from it.")
.argument(
newArgument()
.name(WHERE_OR_IF_OR_INCREMENT_FIELD)
.type(Scalars.GraphQLString)
.description(
"The name of the field that the condition applies to (if absent, it will be "
+ "the name of the argument).")
.build())
.argument(
newArgument()
.name(WHERE_OR_IF_PREDICATE)
.type(PREDICATE_ENUM)
.defaultValue(Predicate.EQ.name())
.description("The predicate to use for the condition.")
.build())
.validLocation(ARGUMENT_DEFINITION)
.build();
private static final GraphQLDirective INCREMENT_DIRECTIVE =
newDirective()
.name(INCREMENT)
.description(
"Annotates a parameter to indicate that it will be incremented.\n"
+ "It is supported on the counter, set and list types.\n"
+ "This is only allowed for update mutations.")
.argument(
newArgument()
.name(WHERE_OR_IF_OR_INCREMENT_FIELD)
.type(Scalars.GraphQLString)
.description(
"The name of the field that the increment applies to (if absent, it will be "
+ "the name of the argument).")
.build())
.argument(
newArgument()
.name(INCREMENT_PREPEND)
.type(Scalars.GraphQLBoolean)
.defaultValue(false)
.description(
"Specifies whether the value should be appended or prepended.\n"
+ "It applies only to list. The default is false, meaning that the value will be appended.")
.build())
.validLocation(ARGUMENT_DEFINITION)
.build();
private static final GraphQLEnumType IF_PREDICATE_ENUM =
newEnum()
.name("IfPredicate")
.description(String.format("A predicate used in `@%s` to define a condition.", IF))
.value(Predicate.EQ.name())
.value(Predicate.NEQ.name())
.value(Predicate.IN.name())
.value(Predicate.LT.name())
.value(Predicate.GT.name())
.value(Predicate.LTE.name())
.value(Predicate.GTE.name())
.build();
private static final GraphQLDirective IF_DIRECTIVE =
newDirective()
.name(IF)
.description(
"Annotates a parameter to indicate that it will be used as a condition that must "
+ "test true on the selected entity in order for the mutation to be applied.\n\n"
+ "This is only allowed for delete and update mutations.")
.argument(
newArgument()
.name(WHERE_OR_IF_OR_INCREMENT_FIELD)
.type(Scalars.GraphQLString)
.description(
"The name of the field that the condition applies to (if absent, it will be "
+ "the name of the argument).")
.build())
.argument(
newArgument()
.name(WHERE_OR_IF_PREDICATE)
.type(IF_PREDICATE_ENUM)
.defaultValue(Predicate.EQ.name())
.description("The predicate to use for the condition.")
.build())
.validLocation(ARGUMENT_DEFINITION)
.build();
private static final GraphQLDirective TIMESTAMP_DIRECTIVE =
newDirective()
.name(TIMESTAMP)
.description(
"Annotates a parameter to indicate that it will be used as a write timestamp for this row."
+ "This is only allowed for insert and update mutations. The parameter can be "
+ "either a `BigInt` (that represents a number of microseconds since the epoch), "
+ "or a `String` (that represents an ISO-8601 zoned date time, e.g. "
+ "`2007-12-03T10:15:30+01:00`).")
.validLocation(ARGUMENT_DEFINITION)
.build();
public static final String ALL_AS_STRING;
public static final TypeDefinitionRegistry ALL_AS_REGISTRY;
static {
// We need a query type in order to build a valid schema:
GraphQLObjectType dummyQueryType =
GraphQLObjectType.newObject()
.name("Query")
.field(
GraphQLFieldDefinition.newFieldDefinition()
.name("dummy")
.type(Scalars.GraphQLBoolean)
.build())
.build();
GraphQLSchema schema =
newSchema()
.additionalDirective(ENTITY_DIRECTIVE)
.additionalDirective(INPUT_DIRECTIVE)
.additionalDirective(COLUMN_DIRECTIVE)
.additionalDirective(INDEX_DIRECTIVE)
.additionalDirective(PAGING_STATE_DIRECTIVE)
.additionalDirective(PAYLOAD_DIRECTIVE)
.additionalDirective(SELECT_DIRECTIVE)
.additionalDirective(INSERT_DIRECTIVE)
.additionalDirective(UPDATE_DIRECTIVE)
.additionalDirective(DELETE_DIRECTIVE)
.additionalDirective(WHERE_DIRECTIVE)
.additionalDirective(IF_DIRECTIVE)
.additionalDirective(INCREMENT_DIRECTIVE)
.additionalDirective(TIMESTAMP_DIRECTIVE)
.query(dummyQueryType)
.build();
// The printer adds these default directives, but we only want ours:
Set defaultDirectives = ImmutableSet.of("include", "skip", "deprecated", "specifiedBy");
ALL_AS_STRING =
new SchemaPrinter(
SchemaPrinter.Options.defaultOptions()
.includeDirectives(d -> !defaultDirectives.contains(d.getName()))
.includeSchemaElement(e -> e != dummyQueryType))
.print(schema);
ALL_AS_REGISTRY = new SchemaParser().parse(ALL_AS_STRING);
}
}