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';
|
|
|
|
|
|
|
|
class GraphQL {
|
|
|
|
final Map<String, GraphQLType> customTypes = {};
|
|
|
|
final GraphQLSchema schema;
|
|
|
|
|
|
|
|
GraphQL(this.schema) {
|
|
|
|
if (schema.query != null) customTypes[schema.query.name] = schema.query;
|
|
|
|
if (schema.mutation != null)
|
|
|
|
customTypes[schema.mutation.name] = schema.mutation;
|
|
|
|
}
|
|
|
|
|
|
|
|
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 13:50:31 +00:00
|
|
|
'Unknown GraphQL type: "${ctx.typeName.name}"\n${ctx.span.highlight()}');
|
2018-08-02 13:31:54 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new ArgumentError(
|
|
|
|
'Invalid GraphQL type: "${ctx.span.text}"\n${ctx.span.highlight()}');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-02 13:53:47 +00:00
|
|
|
Future<GraphQLResult> executeRequest(
|
2018-08-02 13:31:54 +00:00
|
|
|
GraphQLSchema schema, DocumentContext document, String operationName,
|
2018-08-02 13:53:47 +00:00
|
|
|
{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
|
|
|
|
return executeMutation(
|
|
|
|
document, operation, schema, coercedVariableValues, initialValue);
|
|
|
|
}
|
|
|
|
|
|
|
|
OperationDefinitionContext getOperation(
|
|
|
|
DocumentContext document, String operationName) {
|
|
|
|
var ops = document.definitions.whereType<OperationDefinitionContext>();
|
|
|
|
|
|
|
|
if (operationName == null) {
|
|
|
|
return ops.length == 1
|
|
|
|
? ops.first
|
|
|
|
: throw new GraphQLException(
|
2018-08-02 13:50:31 +00:00
|
|
|
'Missing required operation "$operationName".');
|
2018-08-02 13:31:54 +00:00
|
|
|
} else {
|
|
|
|
return ops.firstWhere((d) => d.name == operationName,
|
|
|
|
orElse: () => throw new GraphQLException(
|
|
|
|
'Missing required operation "$operationName".'));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
|
|
|
throw new GraphQLException(
|
|
|
|
'Missing required variable "$variableName".');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
var type = convertType(variableType);
|
|
|
|
var validation = type.validate(variableName, value);
|
|
|
|
|
|
|
|
if (!validation.successful) {
|
|
|
|
throw new GraphQLException(validation.errors[0]);
|
|
|
|
} else {
|
|
|
|
coercedValues[variableName] = type.deserialize(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return coercedValues;
|
|
|
|
}
|
|
|
|
|
2018-08-02 13:53:47 +00:00
|
|
|
Future<GraphQLResult> 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;
|
|
|
|
var fieldType =
|
|
|
|
objectType.fields.firstWhere((f) => f.name == fieldName)?.type;
|
|
|
|
if (fieldType == null) continue;
|
2018-08-02 13:53:47 +00:00
|
|
|
var responseValue = await executeField(
|
2018-08-02 13:31:54 +00:00
|
|
|
objectType, objectValue, fields, fieldType, variableValues);
|
|
|
|
resultMap[responseKey] = responseValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resultMap;
|
|
|
|
}
|
|
|
|
|
|
|
|
executeField(
|
|
|
|
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(fieldType, fields, resolvedValue, variableValues);
|
|
|
|
}
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
if (value != null) {
|
|
|
|
var variableName = value.name;
|
|
|
|
var variableValue = variableValues[variableName];
|
|
|
|
|
|
|
|
if (variableValues.containsKey(variableName)) {
|
|
|
|
coercedValues[argumentName] = variableValue;
|
|
|
|
} else if (defaultValue != null || argumentDefinition.defaultsToNull) {
|
|
|
|
coercedValues[argumentName] = defaultValue;
|
|
|
|
} else if (!argumentType.isNullable) {
|
|
|
|
throw new GraphQLException(
|
|
|
|
'Missing value for argument "$argumentName".');
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (defaultValue != null || argumentDefinition.defaultsToNull) {
|
|
|
|
coercedValues[argumentName] = defaultValue;
|
|
|
|
} else if (!argumentType.isNullable) {
|
|
|
|
throw new GraphQLException(
|
|
|
|
'Missing value for argument "$argumentName".');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
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);
|
|
|
|
return await field.resolve(objectValue, argumentValues) as T;
|
|
|
|
}
|
2018-08-02 13:53:47 +00:00
|
|
|
|
2018-08-02 13:31:54 +00:00
|
|
|
Map<String, List<SelectionContext>> collectFields(
|
|
|
|
DocumentContext document,
|
|
|
|
GraphQLObjectType objectType,
|
|
|
|
SelectionSetContext selectionSet,
|
|
|
|
Map<String, dynamic> variableValues,
|
|
|
|
{List visitedFragments: const []}) {
|
|
|
|
var groupedFields = <String, List<SelectionContext>>{};
|
|
|
|
|
|
|
|
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
|
|
|
|
.whereType<FragmentDefinitionContext>()
|
|
|
|
.firstWhere((f) => f.name == fragmentSpreadName,
|
2018-08-02 13:50:31 +00:00
|
|
|
orElse: () => null);
|
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))
|
|
|
|
throw new GraphQLException(
|
|
|
|
'Unknown variable: "$vname"\n${vv.variable.span.highlight()}');
|
|
|
|
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class GraphQLException extends FormatException {
|
|
|
|
GraphQLException(String message) : super(message);
|
2018-08-02 13:50:31 +00:00
|
|
|
}
|