All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.stargate.graphql.schema.graphqlfirst.processor.FieldModelBuilder Maven / Gradle / Ivy

There is a newer version: 2.0.31
Show newest version
/*
 * 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.graphql.schema.graphqlfirst.processor;

import com.datastax.oss.driver.shaded.guava.common.collect.ImmutableMap;
import graphql.Scalars;
import graphql.language.Directive;
import graphql.language.EnumTypeDefinition;
import graphql.language.FieldDefinition;
import graphql.language.ListType;
import graphql.language.NonNullType;
import graphql.language.ObjectTypeDefinition;
import graphql.language.Type;
import graphql.language.TypeDefinition;
import graphql.language.TypeName;
import graphql.schema.idl.TypeDefinitionRegistry;
import io.stargate.db.schema.Column;
import io.stargate.db.schema.ImmutableUserDefinedType;
import io.stargate.db.schema.ParameterizedType;
import io.stargate.db.schema.UserDefinedType;
import io.stargate.graphql.schema.scalars.CqlScalar;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

class FieldModelBuilder extends ModelBuilderBase {

  private static final Map GRAPHQL_SCALAR_MAPPINGS =
      ImmutableMap.builder()
          .put(Scalars.GraphQLInt.getName(), Column.Type.Int)
          .put(Scalars.GraphQLFloat.getName(), Column.Type.Double)
          .put(Scalars.GraphQLString.getName(), Column.Type.Text)
          .put(Scalars.GraphQLBoolean.getName(), Column.Type.Boolean)
          .put(Scalars.GraphQLID.getName(), Column.Type.Uuid)
          .build();

  private final FieldDefinition field;
  private final String parentCqlName;
  private final EntityModel.Target targetContainer;
  private final boolean checkForInputType;
  private final String graphqlName;
  private final Type graphqlType;
  private final String messagePrefix;
  private final Optional cqlColumnDirective;

  FieldModelBuilder(
      FieldDefinition field,
      ProcessingContext context,
      String parentCqlName,
      String parentGraphqlName,
      EntityModel.Target targetContainer,
      boolean checkForInputType) {

    super(context, field.getSourceLocation());

    this.field = field;
    this.parentCqlName = parentCqlName;
    this.targetContainer = targetContainer;
    this.checkForInputType = checkForInputType;

    this.graphqlName = field.getName();
    this.graphqlType = field.getType();
    this.messagePrefix = parentGraphqlName + "." + graphqlName;
    this.cqlColumnDirective = DirectiveHelper.getDirective(CqlDirectives.COLUMN, field);
  }

  @Override
  FieldModel build() throws SkipException {
    String cqlName =
        cqlColumnDirective
            .flatMap(d -> DirectiveHelper.getStringArgument(d, CqlDirectives.COLUMN_NAME, context))
            .orElse(graphqlName);
    boolean isUdtField = targetContainer == EntityModel.Target.UDT;
    boolean partitionKey = isPartitionKey(isUdtField);
    Optional clusteringOrder = getClusteringOrder(isUdtField);

    if (partitionKey && clusteringOrder.isPresent()) {
      invalidMapping("%s: can't be both a partition key and a clustering key.", messagePrefix);
      throw SkipException.INSTANCE;
    }

    boolean isPk = partitionKey || clusteringOrder.isPresent();

    Column.ColumnType cqlType = inferCqlType(graphqlType, context);
    if (isPk || (cqlType.isUserDefined() && isUdtField)) {
      cqlType = cqlType.frozen();
    }

    cqlType = maybeUseTypeHint(cqlType, isPk, isUdtField);

    return new FieldModel(
        graphqlName,
        graphqlType,
        cqlName,
        cqlType,
        partitionKey,
        clusteringOrder,
        getIndex(cqlName, isUdtField, isPk, cqlType));
  }

  private Boolean isPartitionKey(boolean isUdtField) {
    return cqlColumnDirective
        .flatMap(
            d -> DirectiveHelper.getBooleanArgument(d, CqlDirectives.COLUMN_PARTITION_KEY, context))
        .filter(
            __ -> {
              if (isUdtField) {
                warn(
                    "%s: UDT fields should not be marked as partition keys (this will be ignored)",
                    messagePrefix);
                return false;
              }
              return true;
            })
        .orElse(false);
  }

  private Optional getClusteringOrder(boolean isUdtField) {
    return cqlColumnDirective
        .flatMap(
            d ->
                DirectiveHelper.getEnumArgument(
                    d, CqlDirectives.COLUMN_CLUSTERING_ORDER, Column.Order.class, context))
        .filter(
            __ -> {
              if (isUdtField) {
                warn(
                    "%s: UDT fields should not be marked as clustering keys (this will be ignored)",
                    messagePrefix);
                return false;
              }
              return true;
            });
  }

  /** Computes the CQL type that corresponds to the GraphQL type of a field. */
  private Column.ColumnType inferCqlType(Type graphqlType, ProcessingContext context)
      throws SkipException {
    if (graphqlType instanceof NonNullType) {
      return inferCqlType(((NonNullType) graphqlType).getType(), context);
    } else if (graphqlType instanceof ListType) {
      return Column.Type.List.of(inferCqlType(((ListType) graphqlType).getType(), context));
    } else {
      String typeName = ((TypeName) graphqlType).getName();
      TypeDefinitionRegistry typeRegistry = context.getTypeRegistry();

      // Check built-in GraphQL scalars
      if (GRAPHQL_SCALAR_MAPPINGS.containsKey(typeName)) {
        return GRAPHQL_SCALAR_MAPPINGS.get(typeName);
      }

      // Otherwise, check if the type references another definition in the user's schema
      if (typeRegistry.types().containsKey(typeName)) {
        TypeDefinition definition =
            typeRegistry.getType(typeName).orElseThrow(AssertionError::new);
        if (definition instanceof EnumTypeDefinition) {
          return Column.Type.Text;
        }
        if (definition instanceof ObjectTypeDefinition) {
          return expectUdt((ObjectTypeDefinition) definition);
        }
        invalidMapping("%s: can't map type '%s' to CQL", messagePrefix, graphqlType);
        throw SkipException.INSTANCE;
      }

      // Otherwise, check our own CQL scalars
      // Note that we do this last in case the user defined an object or enum type that happens to
      // have the same name as a CQL scalar.
      Optional maybeCqlScalar = CqlScalar.fromGraphqlName(typeName);
      if (maybeCqlScalar.isPresent()) {
        CqlScalar cqlScalar = maybeCqlScalar.get();
        // Remember that we'll need to add the scalar to the RuntimeWiring
        context.getUsedCqlScalars().add(cqlScalar);
        return cqlScalar.getCqlType();
      }

      invalidMapping("%s: can't map type '%s' to CQL", messagePrefix, graphqlType);
      throw SkipException.INSTANCE;
    }
  }

  /** Checks that if a field is an object type, then that object maps to a UDT. */
  private Column.ColumnType expectUdt(ObjectTypeDefinition definition) throws SkipException {
    boolean isUdt =
        DirectiveHelper.getDirective(CqlDirectives.ENTITY, definition)
            .flatMap(
                d ->
                    DirectiveHelper.getEnumArgument(
                        d, CqlDirectives.ENTITY_TARGET, EntityModel.Target.class, context))
            .filter(target -> target == EntityModel.Target.UDT)
            .isPresent();
    if (isUdt) {
      if (checkForInputType
          && !DirectiveHelper.getDirective(CqlDirectives.INPUT, definition).isPresent()) {
        invalidMapping(
            "%s: type '%s' must also be annotated with @%s",
            messagePrefix, definition.getName(), CqlDirectives.INPUT);
        throw SkipException.INSTANCE;
      }
      return ImmutableUserDefinedType.builder()
          .keyspace(context.getKeyspace().name())
          .name(definition.getName())
          .build();
    }

    invalidMapping(
        "%s: can't map type '%s' to CQL -- "
            + "if a field references an object type, then that object should map to a UDT",
        messagePrefix, definition.getName());
    throw SkipException.INSTANCE;
  }

  /**
   * If the directive provides a CQL type hint, use it, provided that it is valid and compatible
   * with the inferred type.
   */
  private Column.ColumnType maybeUseTypeHint(
      Column.ColumnType cqlType, boolean isPk, boolean isUdtField) throws SkipException {
    Optional maybeCqlTypeHint =
        cqlColumnDirective.flatMap(
            d -> DirectiveHelper.getStringArgument(d, CqlDirectives.COLUMN_TYPE_HINT, context));
    if (!maybeCqlTypeHint.isPresent()) {
      return cqlType;
    }

    Column.ColumnType cqlTypeHint;
    String spec = maybeCqlTypeHint.get();
    try {
      cqlTypeHint = Column.Type.fromCqlDefinitionOf(context.getKeyspace(), spec, false);
    } catch (IllegalArgumentException e) {
      invalidSyntax("%s: could not parse CQL type '%s': %s", messagePrefix, spec, e.getMessage());
      throw SkipException.INSTANCE;
    }
    if (isPk
        && (cqlTypeHint.isParameterized() || cqlTypeHint.isUserDefined())
        && !cqlTypeHint.isFrozen()) {
      invalidMapping(
          "%s: invalid type hint '%s' -- partition or clustering columns must be frozen",
          messagePrefix, spec);
      throw SkipException.INSTANCE;
    }
    if (cqlTypeHint.isUserDefined() && isUdtField && !cqlTypeHint.isFrozen()) {
      invalidMapping(
          "%s: invalid type hint '%s' -- nested UDTs must be frozen", messagePrefix, spec);
      throw SkipException.INSTANCE;
    }
    if (!isCompatible(cqlTypeHint, cqlType)) {
      invalidMapping(
          "%s: invalid type hint '%s'-- the inferred type was '%s', you can only change "
              + "frozenness or use sets instead of lists",
          messagePrefix, cqlTypeHint.cqlDefinition(), cqlType.cqlDefinition());
      throw SkipException.INSTANCE;
    }
    return cqlTypeHint;
  }

  /** Checks if the only differences are frozen vs. not frozen, or list vs. set. */
  private static boolean isCompatible(Column.ColumnType type1, Column.ColumnType type2) {
    Column.Type raw1 = turnSetIntoList(type1.rawType());
    Column.Type raw2 = turnSetIntoList(type2.rawType());
    if (!Objects.equals(raw1, raw2)) {
      return false;
    }
    if (raw2 == Column.Type.UDT) {
      // Only compare the coordinates, the types might be shallow
      UserDefinedType udt1 = (UserDefinedType) type1;
      UserDefinedType udt2 = (UserDefinedType) type2;
      return Objects.equals(udt1.keyspace(), udt2.keyspace())
          && Objects.equals(udt1.name(), udt2.name());
    }
    if (raw2.isParameterized()) {
      ParameterizedType parameterized1 = (ParameterizedType) type1;
      ParameterizedType parameterized2 = (ParameterizedType) type2;
      if (parameterized1.parameters().size() != parameterized2.parameters().size()) {
        return false;
      }
      for (int i = 0; i < parameterized1.parameters().size(); i++) {
        Column.ColumnType sub1 = parameterized1.parameters().get(i);
        Column.ColumnType sub2 = parameterized2.parameters().get(i);
        if (!isCompatible(sub1, sub2)) {
          return false;
        }
      }
    }
    return true;
  }

  private static Column.Type turnSetIntoList(Column.Type type) {
    return (type == Column.Type.Set) ? Column.Type.List : type;
  }

  private Optional getIndex(
      String cqlName, boolean isUdtField, boolean isPk, final Column.ColumnType cqlType)
      throws SkipException {
    Optional cqlIndexDirective =
        DirectiveHelper.getDirective(CqlDirectives.INDEX, field);
    if (cqlIndexDirective.isPresent()) {
      if (isPk) {
        invalidMapping("%s: partition or clustering columns can't have an index", messagePrefix);
        throw SkipException.INSTANCE;
      }
      if (isUdtField) {
        invalidMapping("%s: UDT fields can't have an index", messagePrefix);
        throw SkipException.INSTANCE;
      }
      return Optional.of(
          new IndexModelBuilder(
                  cqlIndexDirective.get(), parentCqlName, cqlName, cqlType, messagePrefix, context)
              .build());
    }
    return Optional.empty();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy