Pull in graphql_schema

This commit is contained in:
Tobe O 2018-08-02 09:31:54 -04:00
parent 6e8189fc6e
commit cd4a2233de
18 changed files with 814 additions and 145 deletions

67
graphql_schema/README.md Normal file
View file

@ -0,0 +1,67 @@
# graphql_schema
[![Pub](https://img.shields.io/pub/v/graphql_schema.svg)](https://pub.dartlang.org/packages/graphql_schema)
An implementation of GraphQL's type system in Dart. Supports any platform where Dart runs.
# Usage
It's easy to define a schema with the
[helper functions](#helpers):
```dart
final GraphQLSchema todoSchema = new GraphQLSchema(
query: objectType('Todo', [
field('text', type: graphQLString.nonNullable()),
field('created_at', type: graphQLDate)
]));
```
All GraphQL types are generic, in order to leverage Dart's strong typing support.
# Serialization
GraphQL types can `serialize` and `deserialize` input data.
The exact implementation of this depends on the type.
```dart
var iso8601String = graphQLDate.serialize(new DateTime.now());
var date = graphQLDate.deserialize(iso8601String);
print(date.millisecondsSinceEpoch);
```
# Validation
GraphQL types can `validate` input data.
```dart
var validation = myType.validate('@root', {...});
if (validation.successful) {
doSomething(validation.value);
} else {
print(validation.errors);
}
```
# Helpers
* `graphQLSchema` - Create a `GraphQLSchema`
* `objectType` - Create a `GraphQLObjectType` with fields
* `field` - Create a `GraphQLField` with a type/argument/resolver
* `listType` - Create a `GraphQLListType` with the provided `innerType`
# Types
All of the GraphQL scalar types are built in, as well as a `Date` type:
* `graphQLString`
* `graphQLId`
* `graphQLBoolean`
* `graphQLInt`
* `graphQLFloat`
* `graphQLDate`
## Non-Nullable Types
You can easily make a type non-nullable by calling its `nonNullable` method.
## List Types
Support for list types is also included. Use the `listType` helper for convenience.
```dart
/// A non-nullable list of non-nullable integers
listType(graphQLInt.nonNullable()).nonNullable();
```

View file

@ -0,0 +1,26 @@
import 'package:graphql_schema/graphql_schema.dart';
final GraphQLSchema todoSchema = new GraphQLSchema(
query: objectType('Todo', [
field('text', type: graphQLString.nonNullable()),
field('created_at', type: graphQLDate)
]));
main() {
// Validation
var validation = todoSchema.query
.validate('@root', {'foo': 'bar', 'text': null, 'created_at': 24});
if (validation.successful) {
print('This is valid data!!!');
} else {
print('Invalid data.');
validation.errors.forEach((s) => print(' * $s'));
}
// Serialization
print(todoSchema.query.serialize({
'text': 'Clean your room!',
'created_at': new DateTime.now().subtract(new Duration(days: 10))
}));
}

View file

@ -2,7 +2,11 @@
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
<excludeFolder url="file://$MODULE_DIR$/.pub" />
<excludeFolder url="file://$MODULE_DIR$/build" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="Dart SDK" level="project" />

View file

@ -0,0 +1 @@
export 'src/schema.dart';

View file

@ -0,0 +1,8 @@
part of graphql_schema.src.schema;
class GraphQLFieldArgument<Value, Serialized> {
final String name;
final GraphQLType<Value, Serialized> type;
final Value defaultValue;
GraphQLFieldArgument(this.name, this.type, {this.defaultValue});
}

View file

@ -0,0 +1,22 @@
part of graphql_schema.src.schema;
typedef FutureOr<Value> GraphQLFieldResolver<Value, Serialized>(
Serialized serialized);
class GraphQLField<Value, Serialized> {
final String name;
final GraphQLFieldArgument argument;
final GraphQLFieldResolver<Value, Serialized> resolve;
final GraphQLType<Value, Serialized> type;
GraphQLField(this.name, {this.argument, this.resolve, this.type});
FutureOr<Serialized> serialize(Value value) {
return type.serialize(value);
}
FutureOr<Value> deserialize(Serialized serialized) {
if (resolve != null) return resolve(serialized);
return type.deserialize(serialized);
}
}

View file

@ -0,0 +1,13 @@
part of graphql_schema.src.schema;
GraphQLObjectType objectType(String name,
[Iterable<GraphQLField> fields = const []]) =>
new GraphQLObjectType(name)..fields.addAll(fields ?? []);
GraphQLField<T, Serialized> field<T, Serialized>(String name,
{GraphQLFieldArgument<T, Serialized> argument,
GraphQLFieldResolver<T, Serialized> resolve,
GraphQLType<T, Serialized> type}) {
return new GraphQLField(name,
argument: argument, resolve: resolve, type: type);
}

View file

@ -0,0 +1,61 @@
part of graphql_schema.src.schema;
class GraphQLObjectType
extends GraphQLType<Map<String, dynamic>, Map<String, dynamic>>
with _NonNullableMixin<Map<String, dynamic>, Map<String, dynamic>> {
final String name;
final List<GraphQLField> fields = [];
GraphQLObjectType(this.name);
@override
ValidationResult<Map<String, dynamic>> validate(String key, Map input) {
if (input is! Map)
return new ValidationResult._failure(['Expected "$key" to be a Map.']);
var out = {};
List<String> errors = [];
input.keys.forEach((k) {
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
if (field == null) {
errors.add('Unexpected field "$k" encountered in $key.');
} else {
var v = input[k];
var result = field.type.validate(k, v);
if (!result.successful) {
errors.addAll(result.errors.map((s) => '$key: $s'));
} else {
out[k] = v;
}
}
});
if (errors.isNotEmpty) {
return new ValidationResult._failure(errors);
} else
return new ValidationResult._ok(out);
}
@override
Map<String, dynamic> serialize(Map value) {
return value.keys.fold<Map<String, dynamic>>({}, (out, k) {
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
if (field == null)
throw new UnsupportedError('Cannot serialize field "$k", which was not defined in the schema.');
return out..[k] = field.serialize(value[k]);
});
}
@override
Map<String, dynamic> deserialize(Map value) {
return value.keys.fold<Map<String, dynamic>>({}, (out, k) {
var field = fields.firstWhere((f) => f.name == k, orElse: () => null);
if (field == null)
throw new UnsupportedError('Unexpected field "$k" encountered in map.');
return out..[k] = field.deserialize(value[k]);
});
}
}

View file

@ -0,0 +1,119 @@
part of graphql_schema.src.schema;
/// `true` or `false`.
final GraphQLScalarType<bool, bool> graphQLBoolean = new _GraphQLBoolType();
/// A UTF8 character sequence.
final GraphQLScalarType<String, String> graphQLString =
new _GraphQLStringType._();
/// The ID scalar type represents a unique identifier, often used to refetch an object or as the key for a cache.
///
/// The ID type is serialized in the same way as a String; however, defining it as an ID signifies that it is not intended to be humanreadable.
final GraphQLScalarType<String, String> graphQLId = new _GraphQLStringType._();
/// A [DateTime].
final GraphQLScalarType<DateTime, String> graphQLDate =
new _GraphQLDateType._();
/// A signed 32bit integer.
final GraphQLScalarType<int, int> graphQLInt =
new _GraphQLNumType<int>((x) => x is int, 'an integer');
/// A signed double-precision floating-point value.
final GraphQLScalarType<double, double> graphQLFloat =
new _GraphQLNumType<double>((x) => x is double, 'a float');
abstract class GraphQLScalarType<Value, Serialized>
extends GraphQLType<Value, Serialized> with _NonNullableMixin<Value, Serialized> {}
typedef bool _NumVerifier(x);
class _GraphQLBoolType extends GraphQLScalarType<bool, bool> {
@override
bool serialize(bool value) {
return value;
}
@override
ValidationResult<bool> validate(String key, input) {
if (input != null && input is! bool)
return new ValidationResult._failure(
['Expected "$key" to be a boolean.']);
return new ValidationResult._ok(input);
}
@override
bool deserialize(bool serialized) {
return serialized;
}
}
class _GraphQLNumType<T extends num> extends GraphQLScalarType<T, T> {
final _NumVerifier verifier;
final String expected;
_GraphQLNumType(this.verifier, this.expected);
@override
ValidationResult<T> validate(String key, input) {
if (input != null && !verifier(input))
return new ValidationResult._failure(
['Expected "$key" to be $expected.']);
return new ValidationResult._ok(input);
}
@override
T deserialize(T serialized) {
return serialized;
}
@override
T serialize(T value) {
return value;
}
}
class _GraphQLStringType extends GraphQLScalarType<String, String> {
_GraphQLStringType._();
@override
String serialize(String value) => value;
@override
String deserialize(String serialized) => serialized;
@override
ValidationResult<String> validate(String key, input) =>
input == null || input is String
? new ValidationResult<String>._ok(input)
: new ValidationResult._failure(['Expected "$key" to be a string.']);
}
class _GraphQLDateType extends GraphQLScalarType<DateTime, String>
with _NonNullableMixin<DateTime, String> {
_GraphQLDateType._();
@override
String serialize(DateTime value) => value.toIso8601String();
@override
DateTime deserialize(String serialized) => DateTime.parse(serialized);
@override
ValidationResult<String> validate(String key, input) {
if (input != null && input is! String)
return new ValidationResult<String>._failure(
['$key must be an ISO 8601-formatted date string.']);
else if (input == null) return new ValidationResult<String>._ok(input);
try {
DateTime.parse(input);
return new ValidationResult<String>._ok(input);
} on FormatException {
return new ValidationResult<String>._failure(
['$key must be an ISO 8601-formatted date string.']);
}
}
}

View file

@ -0,0 +1,22 @@
library graphql_schema.src.schema;
import 'dart:async';
import 'package:meta/meta.dart';
part 'argument.dart';
part 'field.dart';
part 'gen.dart';
part 'object_type.dart';
part 'scalar.dart';
part 'type.dart';
part 'validation_result.dart';
class GraphQLSchema {
final GraphQLObjectType query;
final GraphQLObjectType mutation;
GraphQLSchema({this.query, this.mutation});
}
GraphQLSchema graphQLSchema(
{@required GraphQLObjectType query, GraphQLObjectType mutation}) =>
new GraphQLSchema(query: query, mutation: mutation);

View file

@ -0,0 +1,90 @@
part of graphql_schema.src.schema;
abstract class GraphQLType<Value, Serialized> {
Serialized serialize(Value value);
Value deserialize(Serialized serialized);
ValidationResult<Serialized> validate(String key, Serialized input);
GraphQLType<Value, Serialized> nonNullable();
}
/// Shorthand to create a [GraphQLListType].
GraphQLListType<Value, Serialized> listType<Value, Serialized>(
GraphQLType<Value, Serialized> innerType) =>
new GraphQLListType<Value, Serialized>(innerType);
class GraphQLListType<Value, Serialized>
extends GraphQLType<List<Value>, List<Serialized>>
with _NonNullableMixin<List<Value>, List<Serialized>> {
final GraphQLType<Value, Serialized> type;
GraphQLListType(this.type);
@override
ValidationResult<List<Serialized>> validate(
String key, List<Serialized> input) {
if (input is! List)
return new ValidationResult._failure(['Expected "$key" to be a list.']);
List<Serialized> out = [];
List<String> errors = [];
for (int i = 0; i < input.length; i++) {
var k = '"$key" at index $i';
var v = input[i];
var result = type.validate(k, v);
if (!result.successful)
errors.addAll(result.errors);
else
out.add(v);
}
if (errors.isNotEmpty) return new ValidationResult._failure(errors);
return new ValidationResult._ok(out);
}
@override
List<Value> deserialize(List<Serialized> serialized) {
return serialized.map<Value>(type.deserialize).toList();
}
@override
List<Serialized> serialize(List<Value> value) {
return value.map<Serialized>(type.serialize).toList();
}
}
abstract class _NonNullableMixin<Value, Serialized>
implements GraphQLType<Value, Serialized> {
GraphQLType<Value, Serialized> _nonNullableCache;
GraphQLType<Value, Serialized> nonNullable() => _nonNullableCache ??=
new _GraphQLNonNullableType<Value, Serialized>._(this);
}
class _GraphQLNonNullableType<Value, Serialized>
extends GraphQLType<Value, Serialized> {
final GraphQLType<Value, Serialized> type;
_GraphQLNonNullableType._(this.type);
@override
GraphQLType<Value, Serialized> nonNullable() {
throw new UnsupportedError(
'Cannot call nonNullable() on a non-nullable type.');
}
@override
ValidationResult<Serialized> validate(String key, Serialized input) {
if (input == null)
return new ValidationResult._failure(
['Expected "$key" to be a non-null value.']);
return type.validate(key, input);
}
@override
Value deserialize(Serialized serialized) {
return type.deserialize(serialized);
}
@override
Serialized serialize(Value value) {
return type.serialize(value);
}
}

View file

@ -0,0 +1,15 @@
part of graphql_schema.src.schema;
class ValidationResult<T> {
final bool successful;
final T value;
final List<String> errors;
ValidationResult._ok(this.value)
: errors = [],
successful = true;
ValidationResult._failure(this.errors)
: value = null,
successful = false;
}

View file

@ -0,0 +1,9 @@
name: graphql_schema
version: 1.0.0
description: An implementation of GraphQL's type system in Dart.
author: Tobe O <thosakwe@gmail.com>
homepage: https://github.com/thosakwe/graphql_schema
environment:
sdk: ">=1.8.0 <3.0.0"
dev_dependencies:
test: ^0.12.0

View file

@ -0,0 +1,14 @@
import 'package:graphql_schema/graphql_schema.dart';
final GraphQLObjectType pokemonType = objectType('Pokemon', [
field('species', type: graphQLString),
field('catch_date', type: graphQLDate)
]);
final GraphQLObjectType trainerType =
objectType('Trainer', [field('name', type: graphQLString)]);
final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', [
field('trainer', type: trainerType),
field('pokemon_species', type: listType(pokemonType))
]);

View file

@ -0,0 +1,59 @@
import 'package:graphql_schema/graphql_schema.dart';
import 'package:test/test.dart';
import 'common.dart';
main() {
test('scalar', () {
expect(graphQLString.serialize('a'), 'a');
var now = new DateTime.now();
expect(graphQLDate.serialize(now), now.toIso8601String());
});
test('list', () {
expect(listType(graphQLString).serialize(['foo', 'bar']), ['foo', 'bar']);
var today = new DateTime.now();
var tomorrow = today.add(new Duration(days: 1));
expect(listType(graphQLDate).serialize([today, tomorrow]),
[today.toIso8601String(), tomorrow.toIso8601String()]);
});
test('object', () {
var catchDate = new DateTime.now();
var pikachu = {'species': 'Pikachu', 'catch_date': catchDate};
expect(pokemonType.serialize(pikachu),
{'species': 'Pikachu', 'catch_date': catchDate.toIso8601String()});
});
test('nested object', () {
var pikachuDate = new DateTime.now(),
charizardDate = pikachuDate.subtract(new Duration(days: 10));
var pikachu = {'species': 'Pikachu', 'catch_date': pikachuDate};
var charizard = {'species': 'Charizard', 'catch_date': charizardDate};
var trainer = {'name': 'Tobe O'};
var region = pokemonRegionType.serialize({
'trainer': trainer,
'pokemon_species': [pikachu, charizard]
});
print(region);
expect(region, {
'trainer': trainer,
'pokemon_species': [
{'species': 'Pikachu', 'catch_date': pikachuDate.toIso8601String()},
{'species': 'Charizard', 'catch_date': charizardDate.toIso8601String()}
]
});
expect(() => pokemonRegionType.serialize({
'trainer': trainer,
'DIGIMON_species': [pikachu, charizard]
}), throwsUnsupportedError);
});
}

View file

@ -0,0 +1,281 @@
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(
'Unknown GraphQL type: "${ctx.typeName.name}"\n${ctx.span
.highlight()}');
break;
}
} else {
throw new ArgumentError(
'Invalid GraphQL type: "${ctx.span.text}"\n${ctx.span.highlight()}');
}
}
executeRequest(
GraphQLSchema schema, DocumentContext document, String operationName,
{Map<String, dynamic> variableValues: const {}, initialValue}) {
var operation = getOperation(document, operationName);
var coercedVariableValues =
coerceVariableValues(schema, operation, variableValues ?? {});
if (operation.isQuery)
return executeQuery(
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(
'Missing required operation "$operationName".');
} 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;
}
GraphQLResult executeQuery(
DocumentContext document,
OperationDefinitionContext query,
GraphQLSchema schema,
Map<String, dynamic> variableValues,
initialValue) {
var queryType = schema.query;
var selectionSet = query.selectionSet;
return executeSelectionSet(
document, selectionSet, queryType, initialValue, variableValues);
}
Map<String, dynamic> executeSelectionSet(
DocumentContext document,
SelectionSetContext selectionSet,
GraphQLObjectType objectType,
objectValue,
Map<String, dynamic> variableValues) {
var groupedFieldSet =
collectFields(document, objectType, selectionSet, variableValues);
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;
var responseValue = executeField(
objectType, objectValue, fields, fieldType, variableValues);
resultMap[responseKey] = responseValue;
}
}
return resultMap;
}
executeField(
GraphQLObjectType objectType,
objectValue,
List<SelectionContext> fields,
GraphQLType fieldType,
Map<String, dynamic> variableValues) {
var field = fields[0];
var argumentValues =
coerceArgumentValues(objectType, field, variableValues);
var resolvedValue = resolveFieldValue(
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);
// TODO: Multiple arguments?
var argumentDefinitions = desiredField.argument;
return coercedValues;
}
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 =
groupedFields.putIfAbsent(responseKey, () => []);
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,
orElse: () => null);
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 =
groupedFields.putIfAbsent(responseKey, () => []);
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 =
groupedFields.putIfAbsent(responseKey, () => []);
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);
}

View file

@ -1,144 +0,0 @@
import 'package:graphql_parser/graphql_parser.dart';
import 'package:symbol_table/symbol_table.dart';
class GraphQLQueryExecutor {
const GraphQLQueryExecutor();
Map<String, dynamic> visitDocument(DocumentContext ctx, Map<String, dynamic> inputData) {
var scope = new SymbolTable();
return ctx.definitions.fold(inputData, (o, def) {
var result = visitDefinition(def, o, scope);
return result ?? o;
});
}
Map<String, dynamic> visitDefinition(
DefinitionContext ctx, inputData, SymbolTable scope) {
if (ctx is OperationDefinitionContext)
return visitOperationDefinition(ctx, inputData, scope);
else if (ctx is FragmentDefinitionContext)
return visitFragmentDefinition(ctx, inputData, scope);
else
throw new UnsupportedError('Unsupported definition: $ctx');
}
Map<String, dynamic> visitOperationDefinition(
OperationDefinitionContext ctx, inputData, SymbolTable scope) {
// Add variable definitions
ctx.variableDefinitions?.variableDefinitions?.forEach((def) {
scope.assign(def.variable.name, def.defaultValue?.value?.value);
});
callback(o, SelectionContext sel) {
var result = visitSelection(sel, o, scope);
return result ?? o;
}
if (inputData is List) {
return {
'data': inputData.map((x) {
return ctx.selectionSet.selections.fold(x, callback);
}).toList()
};
} else if (inputData is Map) {
return {'data': ctx.selectionSet.selections.fold(inputData, callback)};
} else
throw new UnsupportedError(
'Cannot execute GraphQL queries against $inputData.');
}
Map<String, dynamic> visitFragmentDefinition(
FragmentDefinitionContext ctx, inputData, SymbolTable scope) {}
visitSelection(SelectionContext ctx, inputData, SymbolTable scope) {
if (inputData is! Map && inputData is! List)
return inputData;
else if (inputData is List)
return inputData.map((x) => visitSelection(ctx, x, scope)).toList();
if (ctx.field != null)
return visitField(ctx.field, inputData, scope);
// TODO: Spread, inline fragment
else
throw new UnsupportedError('Unsupported selection: $ctx');
}
visitField(FieldContext ctx, inputData, SymbolTable scope, [value]) {
bool hasValue = value != null;
var s = scope.createChild();
Map out = {};
value ??= inputData[ctx.fieldName.name];
// Apply arguments to query lists...
if (ctx.arguments.isNotEmpty) {
var listSearch = value is List ? value : inputData;
if (listSearch is! List)
throw new UnsupportedError('Arguments are only supported on Lists.');
value = listSearch.firstWhere((x) {
if (x is! Map)
return null;
else {
return ctx.arguments.every((a) {
var value;
if (a.valueOrVariable.value != null)
value = a.valueOrVariable.value.value;
else {
// TODO: Unknown key
value = scope.resolve(a.valueOrVariable.variable.name).value;
}
// print('Looking for ${a.name} == $value in $x');
return x[a.name] == value;
});
}
}, orElse: () => null);
}
if (value == null) {
//print('Why is ${ctx.fieldName.name} null in $inputData??? hasValue: $hasValue');
return value;
}
if (ctx.selectionSet == null) return value;
var target = {};
for (var selection in ctx.selectionSet.selections) {
if (selection.field != null) {
// Get the corresponding data
var key = selection.field.fieldName.name;
var childValue = value[key];
if (childValue is! List && childValue is! Map)
target[key] = childValue;
else {
applyFieldSelection(x, [root]) {
//print('Select ${selection.field.fieldName.name} from $x');
return visitField(selection.field, root ?? x, s, x);
}
var output = childValue is List
? childValue
.map((x) => applyFieldSelection(x, childValue))
.toList()
: applyFieldSelection(childValue);
//print('$key => $output');
target[key] = output;
}
}
// TODO: Spread, inline fragment
}
// Set this as the value within the current scope...
if (hasValue) {
return target;
} else
out[ctx.fieldName.name] = target;
s.create(ctx.fieldName.name, value: target);
return out;
}
}

View file

@ -1,5 +1,7 @@
name: graphql_server
dependencies:
graphql_schema:
path: ../graphql_schema
graphql_parser:
path: ../graphql_parser
symbol_table: ^1.0.0