platform/graphql_server/lib/graphql_server.dart

459 lines
16 KiB
Dart
Raw Normal View History

2018-08-02 13:53:47 +00:00
import 'dart:async';
2018-08-02 13:31:54 +00:00
import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_schema/graphql_schema.dart';
2018-08-03 17:45:40 +00:00
2018-08-03 17:26:11 +00:00
import 'introspection.dart';
2018-08-02 13:31:54 +00:00
class GraphQL {
final Map<String, GraphQLType> customTypes = {};
2018-08-02 18:33:50 +00:00
GraphQLSchema _schema;
2018-08-02 13:31:54 +00:00
2018-08-02 18:33:50 +00:00
GraphQL(GraphQLSchema schema, {bool introspect: true}) : _schema = schema {
if (introspect) {
2018-08-02 19:22:16 +00:00
var allTypes = <GraphQLType>[];
_schema = reflectSchema(_schema, allTypes);
for (var type in allTypes) {
customTypes[type.name] = type;
}
2018-08-02 18:33:50 +00:00
}
if (_schema.query != null) customTypes[_schema.query.name] = _schema.query;
if (_schema.mutation != null)
customTypes[_schema.mutation.name] = _schema.mutation;
2018-08-02 13:31:54 +00:00
}
GraphQLType convertType(TypeContext ctx) {
if (ctx.listType != null) {
return new GraphQLListType(convertType(ctx.listType.type));
} else if (ctx.typeName != null) {
switch (ctx.typeName.name) {
case 'Int':
return graphQLString;
case 'Float':
return graphQLFloat;
case 'String':
return graphQLString;
case 'Boolean':
return graphQLBoolean;
case 'ID':
return graphQLId;
case 'Date':
case 'DateTime':
return graphQLDate;
default:
if (customTypes.containsKey(ctx.typeName.name))
return customTypes[ctx.typeName.name];
throw new ArgumentError(
2018-08-02 18:33:50 +00:00
'Unknown GraphQL type: "${ctx.typeName.name}"');
2018-08-02 13:31:54 +00:00
break;
}
} else {
2018-08-02 18:33:50 +00:00
throw new ArgumentError('Invalid GraphQL type: "${ctx.span.text}"');
2018-08-02 13:31:54 +00:00
}
}
2018-08-02 15:17:14 +00:00
Future<Map<String, dynamic>> parseAndExecute(String text,
{String operationName,
sourceUrl,
Map<String, dynamic> variableValues: const {},
initialValue}) {
var tokens = scan(text, sourceUrl: sourceUrl);
var parser = new Parser(tokens);
var document = parser.parseDocument();
2018-08-02 18:33:50 +00:00
return executeRequest(_schema, document,
2018-08-02 15:17:14 +00:00
operationName: operationName,
initialValue: initialValue,
variableValues: variableValues);
}
Future<Map<String, dynamic>> executeRequest(
2018-08-02 15:17:14 +00:00
GraphQLSchema schema, DocumentContext document,
{String operationName,
Map<String, dynamic> variableValues: const {},
initialValue}) async {
2018-08-02 13:31:54 +00:00
var operation = getOperation(document, operationName);
var coercedVariableValues =
2018-08-02 13:50:31 +00:00
coerceVariableValues(schema, operation, variableValues ?? {});
2018-08-02 13:31:54 +00:00
if (operation.isQuery)
2018-08-02 13:53:47 +00:00
return await executeQuery(
2018-08-02 13:31:54 +00:00
document, operation, schema, coercedVariableValues, initialValue);
else {
throw new UnimplementedError('mutations');
// return executeMutation(
// document, operation, schema, coercedVariableValues, initialValue);
}
2018-08-02 13:31:54 +00:00
}
OperationDefinitionContext getOperation(
DocumentContext document, String operationName) {
2018-08-02 15:17:14 +00:00
var ops =
document.definitions.where((d) => d is OperationDefinitionContext);
2018-08-02 13:31:54 +00:00
if (operationName == null) {
return ops.length == 1
2018-08-02 15:17:14 +00:00
? ops.first as OperationDefinitionContext
2018-08-03 21:07:08 +00:00
: throw new GraphQLException.fromMessage(
2018-08-02 15:17:14 +00:00
'This document does not define any operations.');
2018-08-02 13:31:54 +00:00
} else {
2018-08-02 15:17:14 +00:00
return ops.firstWhere(
(d) => (d as OperationDefinitionContext).name == operationName,
2018-08-03 21:07:08 +00:00
orElse: () => throw new GraphQLException.fromMessage(
2018-08-02 15:17:14 +00:00
'Missing required operation "$operationName".'))
as OperationDefinitionContext;
2018-08-02 13:31:54 +00:00
}
}
Map<String, dynamic> coerceVariableValues(
GraphQLSchema schema,
OperationDefinitionContext operation,
Map<String, dynamic> variableValues) {
var coercedValues = <String, dynamic>{};
var variableDefinitions =
operation.variableDefinitions?.variableDefinitions ?? [];
for (var variableDefinition in variableDefinitions) {
var variableName = variableDefinition.variable.name;
var variableType = variableDefinition.type;
var defaultValue = variableDefinition.defaultValue;
var value = variableValues[variableName];
if (value == null) {
if (defaultValue != null) {
coercedValues[variableName] = defaultValue.value.value;
} else if (!variableType.isNullable) {
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromSourceSpan(
'Missing required variable "$variableName".',
variableDefinition.span);
2018-08-02 13:31:54 +00:00
}
} else {
var type = convertType(variableType);
var validation = type.validate(variableName, value);
if (!validation.successful) {
2018-08-03 21:07:08 +00:00
throw new GraphQLException(validation.errors
.map((e) => new GraphQLExceptionError(e, locations: [
new GraphExceptionErrorLocation.fromSourceLocation(
variableDefinition.span.start)
]))
.toList());
2018-08-02 13:31:54 +00:00
} else {
coercedValues[variableName] = type.deserialize(value);
}
}
}
return coercedValues;
}
Future<Map<String, dynamic>> executeQuery(
2018-08-02 13:31:54 +00:00
DocumentContext document,
OperationDefinitionContext query,
GraphQLSchema schema,
Map<String, dynamic> variableValues,
2018-08-02 13:53:47 +00:00
initialValue) async {
2018-08-02 13:31:54 +00:00
var queryType = schema.query;
var selectionSet = query.selectionSet;
2018-08-02 13:53:47 +00:00
return await executeSelectionSet(
2018-08-02 13:31:54 +00:00
document, selectionSet, queryType, initialValue, variableValues);
}
2018-08-02 13:53:47 +00:00
Future<Map<String, dynamic>> executeSelectionSet(
2018-08-02 13:31:54 +00:00
DocumentContext document,
SelectionSetContext selectionSet,
GraphQLObjectType objectType,
objectValue,
2018-08-02 13:53:47 +00:00
Map<String, dynamic> variableValues) async {
2018-08-02 13:31:54 +00:00
var groupedFieldSet =
2018-08-02 13:50:31 +00:00
collectFields(document, objectType, selectionSet, variableValues);
2018-08-02 13:31:54 +00:00
var resultMap = <String, dynamic>{};
for (var responseKey in groupedFieldSet.keys) {
var fields = groupedFieldSet[responseKey];
for (var field in fields) {
var fieldName = field.field.fieldName.name;
2018-08-02 15:17:14 +00:00
var fieldType = objectType.fields
.firstWhere((f) => f.name == fieldName, orElse: () => null)
?.type;
2018-08-02 13:31:54 +00:00
if (fieldType == null) continue;
var responseValue = await executeField(document, fieldName, objectType,
objectValue, fields, fieldType, variableValues);
2018-08-02 13:31:54 +00:00
resultMap[responseKey] = responseValue;
}
}
return resultMap;
}
2018-08-02 17:02:00 +00:00
Future executeField(
DocumentContext document,
String fieldName,
2018-08-02 13:31:54 +00:00
GraphQLObjectType objectType,
objectValue,
List<SelectionContext> fields,
GraphQLType fieldType,
2018-08-02 13:53:47 +00:00
Map<String, dynamic> variableValues) async {
2018-08-02 13:31:54 +00:00
var field = fields[0];
var argumentValues =
2018-08-02 13:50:31 +00:00
coerceArgumentValues(objectType, field, variableValues);
2018-08-02 13:53:47 +00:00
var resolvedValue = await resolveFieldValue(
2018-08-02 13:31:54 +00:00
objectType, objectValue, field.field.fieldName.name, argumentValues);
return completeValue(
document, fieldName, fieldType, fields, resolvedValue, variableValues);
2018-08-02 13:31:54 +00:00
}
Map<String, dynamic> coerceArgumentValues(GraphQLObjectType objectType,
SelectionContext field, Map<String, dynamic> variableValues) {
var coercedValues = <String, dynamic>{};
var argumentValues = field.field.arguments;
var fieldName = field.field.fieldName.name;
var desiredField = objectType.fields.firstWhere((f) => f.name == fieldName);
2018-08-02 13:50:31 +00:00
var argumentDefinitions = desiredField.arguments;
for (var argumentDefinition in argumentDefinitions) {
var argumentName = argumentDefinition.name;
var argumentType = argumentDefinition.type;
var defaultValue = argumentDefinition.defaultValue;
var value = argumentValues.firstWhere((a) => a.name == argumentName,
orElse: () => null);
2018-08-02 18:33:50 +00:00
if (value?.valueOrVariable?.variable != null) {
var variableName = value.valueOrVariable.variable.name;
2018-08-02 13:50:31 +00:00
var variableValue = variableValues[variableName];
if (variableValues.containsKey(variableName)) {
coercedValues[argumentName] = variableValue;
} else if (defaultValue != null || argumentDefinition.defaultsToNull) {
coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) {
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromSourceSpan(
'Missing value for argument "$argumentName".',
value.valueOrVariable.span);
2018-08-02 18:33:50 +00:00
} else {
continue;
2018-08-02 13:50:31 +00:00
}
2018-08-02 18:33:50 +00:00
} else if (value == null) {
2018-08-02 13:50:31 +00:00
if (defaultValue != null || argumentDefinition.defaultsToNull) {
coercedValues[argumentName] = defaultValue;
} else if (argumentType is GraphQLNonNullableType) {
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromSourceSpan(
'Missing value for argument "$argumentName".',
value.valueOrVariable.span);
2018-08-02 18:33:50 +00:00
} else {
continue;
}
} else {
var validation =
argumentType.validate(fieldName, value.valueOrVariable.value.value);
if (!validation.successful) {
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromSourceSpan(
'Coercion error for value of argument "$argumentName".',
value.valueOrVariable.span);
2018-08-02 18:33:50 +00:00
} else {
var coercedValue = validation.value;
coercedValues[argumentName] = coercedValue;
2018-08-02 13:50:31 +00:00
}
}
}
2018-08-02 13:31:54 +00:00
return coercedValues;
}
2018-08-02 13:53:47 +00:00
Future<T> resolveFieldValue<T>(GraphQLObjectType objectType, T objectValue,
2018-08-02 13:56:00 +00:00
String fieldName, Map<String, dynamic> argumentValues) async {
var field = objectType.fields.firstWhere((f) => f.name == fieldName);
2018-08-02 15:17:14 +00:00
if (field.resolve == null) {
return null;
} else {
return await field.resolve(objectValue, argumentValues) as T;
}
2018-08-02 13:56:00 +00:00
}
2018-08-02 13:53:47 +00:00
Future completeValue(
DocumentContext document,
String fieldName,
GraphQLType fieldType,
List<SelectionContext> fields,
result,
Map<String, dynamic> variableValues) async {
if (fieldType is GraphQLNonNullableType) {
var innerType = fieldType.innerType;
var completedResult = completeValue(
document, fieldName, innerType, fields, result, variableValues);
if (completedResult == null) {
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromMessage(
'Null value provided for non-nullable field "$fieldName".');
} else {
return completedResult;
}
}
if (result == null) {
return null;
}
if (fieldType is GraphQLListType) {
if (result is! Iterable) {
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromMessage(
2018-08-03 17:26:11 +00:00
'Value of field "$fieldName" must be a list or iterable, got $result instead.');
}
var innerType = fieldType.innerType;
2018-08-02 17:02:00 +00:00
var out = [];
for (var resultItem in (result as Iterable)) {
out.add(await completeValue(document, '(item in "$fieldName")',
innerType, fields, resultItem, variableValues));
}
return out;
}
if (fieldType is GraphQLScalarType) {
2018-08-03 17:45:40 +00:00
try {
var validation = fieldType.validate(fieldName, result);
2018-08-03 17:45:40 +00:00
if (!validation.successful) {
return null;
} else {
return validation.value;
}
} on TypeError {
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromMessage(
2018-08-03 17:45:40 +00:00
'Value of field "$fieldName" must be ${fieldType.valueType}, got $result instead.');
}
}
if (fieldType is GraphQLObjectType) {
var objectType = fieldType;
2018-08-02 17:02:00 +00:00
var subSelectionSet = mergeSelectionSets(fields);
return await executeSelectionSet(
document, subSelectionSet, objectType, result, variableValues);
}
// TODO: Interface/union type
throw new UnsupportedError('Unsupported type: $fieldType');
}
2018-08-02 17:02:00 +00:00
SelectionSetContext mergeSelectionSets(List<SelectionContext> fields) {
var selections = <SelectionContext>[];
for (var field in fields) {
if (field.field?.selectionSet != null) {
selections.addAll(field.field.selectionSet.selections);
} else if (field.inlineFragment?.selectionSet != null) {
selections.addAll(field.inlineFragment.selectionSet.selections);
}
}
return new SelectionSetContext.merged(selections);
}
2018-08-02 13:31:54 +00:00
Map<String, List<SelectionContext>> collectFields(
DocumentContext document,
GraphQLObjectType objectType,
SelectionSetContext selectionSet,
Map<String, dynamic> variableValues,
2018-08-02 18:33:50 +00:00
{List visitedFragments}) {
2018-08-02 13:31:54 +00:00
var groupedFields = <String, List<SelectionContext>>{};
2018-08-02 18:33:50 +00:00
visitedFragments ??= [];
2018-08-02 13:31:54 +00:00
for (var selection in selectionSet.selections) {
if (getDirectiveValue('skip', 'if', selection, variableValues) == true)
continue;
if (getDirectiveValue('include', 'if', selection, variableValues) ==
false) continue;
if (selection.field != null) {
var responseKey = selection.field.fieldName.name;
var groupForResponseKey =
2018-08-02 13:50:31 +00:00
groupedFields.putIfAbsent(responseKey, () => []);
2018-08-02 13:31:54 +00:00
groupForResponseKey.add(selection);
} else if (selection.fragmentSpread != null) {
var fragmentSpreadName = selection.fragmentSpread.name;
if (visitedFragments.contains(fragmentSpreadName)) continue;
visitedFragments.add(fragmentSpreadName);
var fragment = document.definitions
2018-08-02 18:33:50 +00:00
.where((d) => d is FragmentDefinitionContext)
.firstWhere(
(f) =>
(f as FragmentDefinitionContext).name == fragmentSpreadName,
orElse: () => null) as FragmentDefinitionContext;
2018-08-02 13:31:54 +00:00
if (fragment == null) continue;
var fragmentType = fragment.typeCondition;
if (!doesFragmentTypeApply(objectType, fragmentType)) continue;
var fragmentSelectionSet = fragment.selectionSet;
var fragmentGroupFieldSet = collectFields(
document, objectType, fragmentSelectionSet, variableValues);
for (var responseKey in fragmentGroupFieldSet.keys) {
var fragmentGroup = fragmentGroupFieldSet[responseKey];
var groupForResponseKey =
2018-08-02 13:50:31 +00:00
groupedFields.putIfAbsent(responseKey, () => []);
2018-08-02 13:31:54 +00:00
groupForResponseKey.addAll(fragmentGroup);
}
} else if (selection.inlineFragment != null) {
var fragmentType = selection.inlineFragment.typeCondition;
if (fragmentType != null &&
!doesFragmentTypeApply(objectType, fragmentType)) continue;
var fragmentSelectionSet = selection.inlineFragment.selectionSet;
var fragmentGroupFieldSet = collectFields(
document, objectType, fragmentSelectionSet, variableValues);
for (var responseKey in fragmentGroupFieldSet.keys) {
var fragmentGroup = fragmentGroupFieldSet[responseKey];
var groupForResponseKey =
2018-08-02 13:50:31 +00:00
groupedFields.putIfAbsent(responseKey, () => []);
2018-08-02 13:31:54 +00:00
groupForResponseKey.addAll(fragmentGroup);
}
}
}
return groupedFields;
}
getDirectiveValue(String name, String argumentName,
SelectionContext selection, Map<String, dynamic> variableValues) {
if (selection.field == null) return null;
var directive = selection.field.directives.firstWhere((d) {
var vv = d.valueOrVariable;
if (vv.value != null) return vv.value.value == name;
return vv.variable.name == name;
}, orElse: () => null);
if (directive == null) return null;
if (directive.argument?.name != argumentName) return null;
var vv = directive.argument.valueOrVariable;
if (vv.value != null) return vv.value.value;
var vname = vv.variable.name;
if (!variableValues.containsKey(vname))
2018-08-03 21:07:08 +00:00
throw new GraphQLException.fromSourceSpan('Unknown variable: "$vname"', vv.span);
2018-08-02 13:31:54 +00:00
return variableValues[vname];
}
bool doesFragmentTypeApply(
GraphQLObjectType objectType, TypeConditionContext fragmentType) {
var type = convertType(new TypeContext(fragmentType.typeName, null));
// TODO: Handle interface type, union?
if (type is GraphQLObjectType) {
for (var field in type.fields)
if (!objectType.fields.any((f) => f.name == field.name)) return false;
return true;
}
return false;
}
}