Pull in graphql_schema
This commit is contained in:
parent
6e8189fc6e
commit
cd4a2233de
18 changed files with 814 additions and 145 deletions
67
graphql_schema/README.md
Normal file
67
graphql_schema/README.md
Normal file
|
@ -0,0 +1,67 @@
|
|||
# graphql_schema
|
||||
[](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();
|
||||
```
|
26
graphql_schema/example/todo.dart
Normal file
26
graphql_schema/example/todo.dart
Normal 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))
|
||||
}));
|
||||
}
|
|
@ -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" />
|
||||
|
|
1
graphql_schema/lib/graphql_schema.dart
Normal file
1
graphql_schema/lib/graphql_schema.dart
Normal file
|
@ -0,0 +1 @@
|
|||
export 'src/schema.dart';
|
8
graphql_schema/lib/src/argument.dart
Normal file
8
graphql_schema/lib/src/argument.dart
Normal 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});
|
||||
}
|
22
graphql_schema/lib/src/field.dart
Normal file
22
graphql_schema/lib/src/field.dart
Normal 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);
|
||||
}
|
||||
}
|
13
graphql_schema/lib/src/gen.dart
Normal file
13
graphql_schema/lib/src/gen.dart
Normal 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);
|
||||
}
|
61
graphql_schema/lib/src/object_type.dart
Normal file
61
graphql_schema/lib/src/object_type.dart
Normal 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]);
|
||||
});
|
||||
}
|
||||
}
|
119
graphql_schema/lib/src/scalar.dart
Normal file
119
graphql_schema/lib/src/scalar.dart
Normal file
|
@ -0,0 +1,119 @@
|
|||
part of graphql_schema.src.schema;
|
||||
|
||||
/// `true` or `false`.
|
||||
final GraphQLScalarType<bool, bool> graphQLBoolean = new _GraphQLBoolType();
|
||||
|
||||
/// A UTF‐8 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 human‐readable.
|
||||
final GraphQLScalarType<String, String> graphQLId = new _GraphQLStringType._();
|
||||
|
||||
/// A [DateTime].
|
||||
final GraphQLScalarType<DateTime, String> graphQLDate =
|
||||
new _GraphQLDateType._();
|
||||
|
||||
/// A signed 32‐bit 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.']);
|
||||
}
|
||||
}
|
||||
}
|
22
graphql_schema/lib/src/schema.dart
Normal file
22
graphql_schema/lib/src/schema.dart
Normal 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);
|
90
graphql_schema/lib/src/type.dart
Normal file
90
graphql_schema/lib/src/type.dart
Normal 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);
|
||||
}
|
||||
}
|
15
graphql_schema/lib/src/validation_result.dart
Normal file
15
graphql_schema/lib/src/validation_result.dart
Normal 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;
|
||||
}
|
9
graphql_schema/pubspec.yaml
Normal file
9
graphql_schema/pubspec.yaml
Normal 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
|
14
graphql_schema/test/common.dart
Normal file
14
graphql_schema/test/common.dart
Normal 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))
|
||||
]);
|
59
graphql_schema/test/serialize_test.dart
Normal file
59
graphql_schema/test/serialize_test.dart
Normal 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);
|
||||
});
|
||||
}
|
281
graphql_server/lib/graphql.dart
Normal file
281
graphql_server/lib/graphql.dart
Normal 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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
name: graphql_server
|
||||
dependencies:
|
||||
graphql_schema:
|
||||
path: ../graphql_schema
|
||||
graphql_parser:
|
||||
path: ../graphql_parser
|
||||
symbol_table: ^1.0.0
|
Loading…
Reference in a new issue