buildSchema function
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
);
}