buildSchema function

GraphQLSchema buildSchema(
  1. String schemaStr, {
  2. Map<String, Object?>? payload,
  3. SerdeCtx? serdeCtx,
})

Create a GraphQLSchema from a GraphQL Schema Definition Language (SDL) String schemaStr.

throws SourceSpanException if there is an error on parsing throws GraphQLException if there is an error on SDL or schema validation

Implementation

GraphQLSchema buildSchema(
  String schemaStr, {
  Map<String, Object?>? payload,
  SerdeCtx? serdeCtx,
}) {
  final schemaDoc = gql.parseString(schemaStr);

  final errors = validateSDL(schemaDoc);
  if (errors.isNotEmpty) {
    throw GraphQLException(errors);
  }

  final schemaDef = schemaDoc.definitions.whereType<SchemaDefinitionNode>();
  final typeDefs = schemaDoc.definitions.whereType<TypeDefinitionNode>();
  final directiveDefs =
      schemaDoc.definitions.whereType<DirectiveDefinitionNode>();
  final typeDefsExtensions = schemaDoc.definitions
      .whereType<TypeExtensionNode>()
      .groupListsBy((element) => element.name.value);

  final typesMap = <String, GraphQLNamedType<Object?, Object?>>{};

  final directives = List.of(
    directiveDefs.map(
      (e) => GraphQLDirective(
        name: e.name.value,
        description: e.description?.value,
        isRepeatable: e.repeatable,
        locations: List.of(e.locations.map(mapDirectiveLocation)),
        astNode: e,
      ),
    ),
  );
  final _directiveNames = directives.map((e) => e.name).toSet();
  directives.addAll(
    GraphQLDirective.specifiedDirectives.where(
      (d) => !_directiveNames.contains(d.name),
    ),
  );

  final Map<String, GraphQLDirective> directivesMap = directives
      .fold({}, (previous, element) => previous..[element.name] = element);

  for (final def in typeDefs) {
    final name = def.name.value;
    final extensions = typeDefsExtensions[name] ?? [];

    final GraphQLNamedType type;
    if (def is ScalarTypeDefinitionNode) {
      final _extensions =
          extensions.whereType<ScalarTypeExtensionNode>().toList();
      type = GraphQLScalarTypeValue<Object?, Object?>(
        name: name,
        description: def.description?.value,
        specifiedByURL: getDirectiveValue(
          'specifiedBy',
          'url',
          def.directives,
          payload,
          directivesMap: directivesMap,
        ) as String?,
        serialize: (s) => s,
        deserialize: (_, s) => s,
        validate: (k, inp) => ValidationResult.ok(inp),
        extra: GraphQLTypeDefinitionExtra.ast(def, _extensions),
      );
    } else if (def is ObjectTypeDefinitionNode) {
      final _extensions =
          extensions.whereType<ObjectTypeExtensionNode>().toList();
      type = GraphQLObjectType<Object?>(
        name,
        description: def.description?.value,
        isInterface: false,
        extra: GraphQLTypeDefinitionExtra.ast(def, _extensions),
      );
    } else if (def is InterfaceTypeDefinitionNode) {
      final _extensions =
          extensions.whereType<InterfaceTypeExtensionNode>().toList();
      type = GraphQLObjectType<Object?>(
        name,
        description: def.description?.value,
        isInterface: true,
        extra: GraphQLTypeDefinitionExtra.ast(def, _extensions),
      );
    } else if (def is UnionTypeDefinitionNode) {
      final _extensions =
          extensions.whereType<UnionTypeExtensionNode>().toList();
      type = GraphQLUnionType<Object?>(
        name,
        [],
        description: def.description?.value,
        extra: GraphQLTypeDefinitionExtra.ast(def, _extensions),
      );
    } else if (def is EnumTypeDefinitionNode) {
      final _extensions =
          extensions.whereType<EnumTypeExtensionNode>().toList();
      type = GraphQLEnumType<Object?>(
        name,
        [
          ...def.values.map(
            (e) => GraphQLEnumValue(
              e.name.value,
              EnumValue(e.name.value),
              description: e.description?.value,
              deprecationReason: getDirectiveValue(
                'deprecated',
                'reason',
                e.directives,
                payload,
                directivesMap: directivesMap,
              ) as String?,
              astNode: e,
            ),
          )
        ],
        description: def.description?.value,
        extra: GraphQLTypeDefinitionExtra.ast(def, _extensions),
      );
    } else if (def is InputObjectTypeDefinitionNode) {
      final _extensions =
          extensions.whereType<InputObjectTypeExtensionNode>().toList();
      type = GraphQLInputObjectType<Object?>(
        name,
        description: def.description?.value,
        extra: GraphQLTypeDefinitionExtra.ast(def, _extensions),
        isOneOf: def.directives.any((d) => d.name.value == 'oneOf'),
      );
    } else {
      throw Error();
    }
    typesMap[name] = type;
    if (type.extra.extensionAstNodes.length != extensions.length) {
      final wrongExtensions = extensions
          .where((element) => !type.extra.extensionAstNodes.contains(element))
          .toList();
      errors.add(GraphQLError(
        'Cannot extend $type with ${wrongExtensions.map((e) => e.runtimeType).join(', ')}.',
        locations: [
          ...wrongExtensions.map((e) =>
              GraphQLErrorLocation.fromSourceLocation(e.name.span!.start)),
        ],
      ));
    }
  }

  Iterable<GraphQLFieldInput<Object?, Object?>> arguments(
    NameNode nameNode,
    List<InputValueDefinitionNode> args, {
    String? objectName,
  }) {
    return args.map(
      (e) {
        final type = convertTypeOrNull(e.type, typesMap);
        if (!isInputType(type)) {
          final fieldName = objectName == null
              ? '${nameNode.value}.${e.name.value}'
              : '$objectName.${nameNode.value}(${e.name.value}:)';
          errors.add(
            GraphQLError(
              'The type of $fieldName must be Input Type but got: $type.',
              locations: GraphQLErrorLocation.firstFromNodes([
                e,
                e.type,
                nameNode,
              ]),
            ),
          );
          return null;
        }
        return GraphQLFieldInput(
          e.name.value,
          type!,
          description: e.description?.value,
          deprecationReason: getDirectiveValue(
            'deprecated',
            'reason',
            e.directives,
            payload,
            directivesMap: directivesMap,
          ) as String?,
          defaultValue: e.defaultValue == null
              ? null
              : computeValue(type, e.defaultValue!, null),
          defaultsToNull: e.defaultValue is NullValueNode,
          astNode: e,
        );
      },
    ).whereType();
  }

  directiveDefs.forEachIndexed((index, e) {
    directives[index].inputs.addAll(arguments(e.name, e.args));
  });

  typesMap.forEach((key, value) {
    value.whenNamed(
      enum_: (enum_) {},
      scalar: (scalar) {},
      object: (object) {
        final astNode = object.extra.astNode!;
        final extensionAstNodes = object.extra.extensionAstNodes;
        final List<FieldDefinitionNode> fields = [
          ...astNode is ObjectTypeDefinitionNode
              ? astNode.fields
              : (astNode as InterfaceTypeDefinitionNode).fields,
          ...extensionAstNodes is List<ObjectTypeExtensionNode>
              ? extensionAstNodes.expand((e) => e.fields)
              : (extensionAstNodes as List<InterfaceTypeExtensionNode>)
                  .expand((e) => e.fields),
        ];

        object.fields.addAll([
          ...fields.map(
            (e) {
              final type = convertTypeOrNull(e.type, typesMap);
              if (!isOutputType(type)) {
                errors.add(GraphQLError(
                  'The type of ${object.name}.${e.name.value} must be Output Type but got: $type.',
                ));
                return null;
              }
              return type!.field<Object?>(
                e.name.value,
                description: e.description?.value,
                deprecationReason: getDirectiveValue(
                  'deprecated',
                  'reason',
                  e.directives,
                  payload,
                  directivesMap: directivesMap,
                ) as String?,
                inputs: arguments(e.name, e.args, objectName: object.name),
                astNode: e,
              );
            },
          ).whereType()
        ]);

        final List<NamedTypeNode> interfaces =
            extensionAstNodes is List<ObjectTypeExtensionNode>
                ? [
                    ...(astNode as ObjectTypeDefinitionNode).interfaces,
                    ...extensionAstNodes.expand((element) => element.interfaces)
                  ]
                : [
                    ...(astNode as InterfaceTypeDefinitionNode).interfaces,
                    ...(extensionAstNodes as List<InterfaceTypeExtensionNode>)
                        .expand((element) => element.interfaces)
                  ];

        for (final i in interfaces) {
          // TODO: 3I could be null?
          final type = typesMap[i.name.value];
          if (type is! GraphQLObjectType || !type.isInterface) {
            errors.add(
              GraphQLError(
                'Type $object must only implement Interface types,'
                ' it cannot implement ${i.name.value}.',
              ),
            );
            continue;
          }
          object.inheritFrom(type, inheritInterfaces: false);
        }
      },
      input: (input) {
        final astNode = input.extra.astNode!;
        final fields = [
          ...astNode.fields,
          ...input.extra.extensionAstNodes.expand((e) => e.fields)
        ];
        input.fields.addAll(arguments(astNode.name, fields));
      },
      union: (union) {
        final typeNodes = [
          ...union.extra.astNode!.types,
          ...union.extra.extensionAstNodes.expand((e) => e.types)
        ];
        union.possibleTypes.addAll(
          [
            ...typeNodes.map((e) {
              final type = typesMap[e.name.value];
              if (type is! GraphQLObjectType || type.isInterface) {
                errors.add(
                  GraphQLError(
                    'Union type $union can only include Object types,'
                    ' it cannot include ${e.name.value}.',
                    locations: [
                      GraphQLErrorLocation.fromSourceLocation(
                          e.name.span!.start),
                    ],
                  ),
                );
                return null;
              }
              return type;
            }).whereType()
          ],
        );
      },
    );
  });
  GraphQLObjectType? queryType;
  GraphQLObjectType? mutationType;
  GraphQLObjectType? subscriptionType;
  SchemaDefinitionNode? schemaNode;
  if (schemaDef.isEmpty) {
    final _queryType = typesMap['Query'];
    if (_queryType is GraphQLObjectType) {
      queryType = _queryType;
    }
    final _mutationType = typesMap['Mutation'];
    if (_mutationType is GraphQLObjectType) {
      mutationType = _mutationType;
    }
    final _subscriptionType = typesMap['Subscription'];
    if (_subscriptionType is GraphQLObjectType) {
      subscriptionType = _subscriptionType;
    }
    [
      _queryType,
      _mutationType,
      _subscriptionType,
    ].forEachIndexed((index, element) {
      if (element != null &&
          (element is! GraphQLObjectType || element.isInterface)) {
        final opType = const ['Query', 'Mutation', 'Subscription'][index];
        errors.add(GraphQLError(
          '$opType root type must be Object type${index != 0 ? ' if provided' : ''}, it cannot be $element.',
        ));
      }
    });
  } else {
    schemaNode = schemaDef.first;
    for (final op in schemaDef.first.operationTypes) {
      final typeName = op.type.name.value;
      final type = typesMap[typeName];
      if (type != null && (type is! GraphQLObjectType || type.isInterface)) {
        final opType = op.operation.toString().split('.').last;
        errors.add(GraphQLError(
          '${opType.substring(0, 1).toUpperCase()}${opType.substring(1)}'
          ' root type must be Object type${op.operation != OperationType.query ? ' if provided' : ''}, it cannot be $type.',
        ));
        continue;
      }
      switch (op.operation) {
        case OperationType.query:
          queryType = queryType ?? type as GraphQLObjectType?;
          break;
        case OperationType.mutation:
          mutationType = mutationType ?? type as GraphQLObjectType?;
          break;
        case OperationType.subscription:
          subscriptionType = subscriptionType ?? type as GraphQLObjectType?;
          break;
      }
    }
  }

  if (errors.isNotEmpty) {
    throw GraphQLException(errors);
  }

  return GraphQLSchema(
    queryType: queryType,
    mutationType: mutationType,
    subscriptionType: subscriptionType,
    serdeCtx: serdeCtx,
    directives: directives,
    otherTypes: typesMap.values.toList(),
    astNode: schemaNode,
    description: schemaNode?.description?.value,
    // TODO: 3I extensionAstNodes
  );
}