Basic query actually works
This commit is contained in:
parent
10e740b2b1
commit
39fd284c67
15 changed files with 177 additions and 95 deletions
7
.idea/runConfigurations/tests_in_query_test_dart.xml
Normal file
7
.idea/runConfigurations/tests_in_query_test_dart.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="tests in query_test.dart" type="DartTestRunConfigurationType" factoryName="Dart Test" singleton="true" nameIsGenerated="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/graphql_server/test/query_test.dart" />
|
||||
<option name="testRunnerOptions" value="-j4" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
|
@ -1,7 +1,8 @@
|
|||
import 'package:source_span/source_span.dart';
|
||||
|
||||
import '../token.dart';
|
||||
import 'definition.dart';
|
||||
import 'directive.dart';
|
||||
import 'package:source_span/source_span.dart';
|
||||
import 'selection_set.dart';
|
||||
import 'variable_definitions.dart';
|
||||
|
||||
|
@ -12,7 +13,8 @@ class OperationDefinitionContext extends DefinitionContext {
|
|||
final SelectionSetContext selectionSet;
|
||||
|
||||
bool get isMutation => TYPE?.text == 'mutation';
|
||||
bool get isQuery => TYPE?.text == 'query';
|
||||
|
||||
bool get isQuery => TYPE?.text == 'query' || TYPE == null;
|
||||
|
||||
String get name => NAME?.text;
|
||||
|
||||
|
|
|
@ -55,8 +55,8 @@ class StringValueContext extends ValueContext {
|
|||
buf.writeCharCode(next);
|
||||
}
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Unexpected "\\" in string literal.', span.start);
|
||||
throw new SyntaxError(
|
||||
'Unexpected "\\" in string literal.', span);
|
||||
} else {
|
||||
buf.writeCharCode(ch);
|
||||
}
|
||||
|
|
|
@ -35,9 +35,9 @@ final Map<Pattern, TokenType> _patterns = {
|
|||
_name: TokenType.NAME
|
||||
};
|
||||
|
||||
List<Token> scan(String text) {
|
||||
List<Token> scan(String text, {sourceUrl}) {
|
||||
List<Token> out = [];
|
||||
var scanner = new SpanScanner(text);
|
||||
var scanner = new SpanScanner(text, sourceUrl: sourceUrl);
|
||||
|
||||
while (!scanner.isDone) {
|
||||
List<Token> potential = [];
|
||||
|
@ -54,7 +54,7 @@ List<Token> scan(String text) {
|
|||
if (potential.isEmpty) {
|
||||
var ch = new String.fromCharCode(scanner.readChar());
|
||||
throw new SyntaxError(
|
||||
"Unexpected token '$ch'.", scanner.state.line, scanner.state.column);
|
||||
"Unexpected token '$ch'.", scanner.emptySpan);
|
||||
} else {
|
||||
// Choose longest token
|
||||
potential.sort((a, b) => b.text.length.compareTo(a.text.length));
|
||||
|
|
|
@ -66,13 +66,13 @@ class Parser {
|
|||
TYPE, NAME, variables, selectionSet)
|
||||
..directives.addAll(dirs);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected selection set in fragment definition.',
|
||||
NAME.span.end);
|
||||
NAME.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected name after operation type "${TYPE.text}" in operation definition.',
|
||||
TYPE.span.end);
|
||||
TYPE.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -94,21 +94,21 @@ class Parser {
|
|||
FRAGMENT, NAME, ON, typeCondition, selectionSet)
|
||||
..directives.addAll(dirs);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected selection set in fragment definition.',
|
||||
typeCondition.span.end);
|
||||
typeCondition.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected type condition after "on" in fragment definition.',
|
||||
ON.span.end);
|
||||
ON.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected "on" after name "${NAME.text}" in fragment definition.',
|
||||
NAME.span.end);
|
||||
NAME.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected name after "fragment" in fragment definition.',
|
||||
FRAGMENT.span.end);
|
||||
FRAGMENT.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -142,18 +142,18 @@ class Parser {
|
|||
ELLIPSIS, ON, typeCondition, selectionSet)
|
||||
..directives.addAll(directives);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected selection set in inline fragment.',
|
||||
directives.isEmpty
|
||||
? typeCondition.span.end
|
||||
: directives.last.span.end);
|
||||
? typeCondition.span
|
||||
: directives.last.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected type condition after "on" in inline fragment.',
|
||||
ON.span.end);
|
||||
ON.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected "on" after "..." in inline fragment.', ELLIPSIS.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected "on" after "..." in inline fragment.', ELLIPSIS.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -174,9 +174,9 @@ class Parser {
|
|||
return new SelectionSetContext(LBRACE, current)
|
||||
..selections.addAll(selections);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected "}" after selection set.',
|
||||
selections.isEmpty ? LBRACE.span.end : selections.last.span.end);
|
||||
selections.isEmpty ? LBRACE.span : selections.last.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -216,8 +216,8 @@ class Parser {
|
|||
return new FieldNameContext(
|
||||
null, new AliasContext(NAME1, COLON, current));
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected name after colon in alias.', COLON.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected name after colon in alias.', COLON.span);
|
||||
} else
|
||||
return new FieldNameContext(NAME1);
|
||||
} else
|
||||
|
@ -240,8 +240,8 @@ class Parser {
|
|||
return new VariableDefinitionsContext(LPAREN, current)
|
||||
..variableDefinitions.addAll(defs);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected ")" after variable definitions.', LPAREN.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected ")" after variable definitions.', LPAREN.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -257,11 +257,11 @@ class Parser {
|
|||
return new VariableDefinitionContext(
|
||||
variable, COLON, type, defaultValue);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected type in variable definition.', COLON.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected type in variable definition.', COLON.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected ":" in variable definition.', variable.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected ":" in variable definition.', variable.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -287,11 +287,11 @@ class Parser {
|
|||
if (next(TokenType.RBRACKET)) {
|
||||
return new ListTypeContext(LBRACKET, type, current);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected "]" in list type.', type.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected "]" in list type.', type.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected type after "[".', LBRACKET.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected type after "[".', LBRACKET.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -320,9 +320,9 @@ class Parser {
|
|||
return new DirectiveContext(
|
||||
ARROBA, NAME, COLON, null, null, null, val);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected value or variable in directive after colon.',
|
||||
COLON.span.end);
|
||||
COLON.span);
|
||||
} else if (next(TokenType.LPAREN)) {
|
||||
var LPAREN = current;
|
||||
var arg = parseArgument();
|
||||
|
@ -331,17 +331,17 @@ class Parser {
|
|||
return new DirectiveContext(
|
||||
ARROBA, NAME, null, LPAREN, current, arg, null);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected \')\'', arg.valueOrVariable.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected \')\'', arg.valueOrVariable.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected argument in directive.', LPAREN.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected argument in directive.', LPAREN.span);
|
||||
} else
|
||||
return new DirectiveContext(
|
||||
ARROBA, NAME, null, null, null, null, null);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected name for directive.', ARROBA.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected name for directive.', ARROBA.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -363,8 +363,8 @@ class Parser {
|
|||
if (next(TokenType.RPAREN))
|
||||
return out;
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected ")" in argument list.', LPAREN.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected ")" in argument list.', LPAREN.span);
|
||||
} else
|
||||
return [];
|
||||
}
|
||||
|
@ -378,11 +378,11 @@ class Parser {
|
|||
if (val != null)
|
||||
return new ArgumentContext(NAME, COLON, val);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected value or variable in argument.', COLON.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected value or variable in argument.', COLON.span);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected colon after name in argument.', NAME.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected colon after name in argument.', NAME.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -406,9 +406,9 @@ class Parser {
|
|||
if (next(TokenType.NAME))
|
||||
return new VariableContext(DOLLAR, current);
|
||||
else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
throw new SyntaxError(
|
||||
'Expected name for variable; found a lone "\$" instead.',
|
||||
DOLLAR.span.end);
|
||||
DOLLAR.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -420,8 +420,8 @@ class Parser {
|
|||
if (value != null) {
|
||||
return new DefaultValueContext(EQUALS, value);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Expected value after "=" sign.', EQUALS.span.end);
|
||||
throw new SyntaxError(
|
||||
'Expected value after "=" sign.', EQUALS.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
@ -474,8 +474,8 @@ class Parser {
|
|||
if (next(TokenType.RBRACKET)) {
|
||||
return new ArrayValueContext(LBRACKET, current)..values.addAll(values);
|
||||
} else
|
||||
throw new SyntaxError.fromSourceLocation(
|
||||
'Unterminated array literal.', LBRACKET.span.end);
|
||||
throw new SyntaxError(
|
||||
'Unterminated array literal.', LBRACKET.span);
|
||||
} else
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -1,18 +1,11 @@
|
|||
import 'package:source_span/source_span.dart';
|
||||
import 'token.dart';
|
||||
|
||||
class SyntaxError implements Exception {
|
||||
final String message;
|
||||
final int line, column;
|
||||
final Token offendingToken;
|
||||
final FileSpan span;
|
||||
|
||||
SyntaxError(this.message, this.line, this.column, [this.offendingToken]);
|
||||
|
||||
factory SyntaxError.fromSourceLocation(
|
||||
String message, SourceLocation location,
|
||||
[Token offendingToken]) =>
|
||||
new SyntaxError(message, location.line, location.column, offendingToken);
|
||||
SyntaxError(this.message, this.span);
|
||||
|
||||
@override
|
||||
String toString() => 'Syntax error at line $line, column $column: $message';
|
||||
String toString() => 'Syntax error at ${span.start.toolString}: $message\n${span.highlight()}';
|
||||
}
|
||||
|
|
|
@ -4,9 +4,14 @@ final GraphQLSchema todoSchema = new GraphQLSchema(
|
|||
query: objectType('Todo', [
|
||||
field(
|
||||
'text',
|
||||
innerType: graphQLString.nonNullable(),
|
||||
type: graphQLString.nonNullable(),
|
||||
resolve: resolveToNull,
|
||||
),
|
||||
field(
|
||||
'created_at',
|
||||
type: graphQLDate,
|
||||
resolve: resolveToNull,
|
||||
),
|
||||
field('created_at', innerType: graphQLDate),
|
||||
]),
|
||||
);
|
||||
|
||||
|
@ -14,7 +19,11 @@ main() {
|
|||
// Validation
|
||||
var validation = todoSchema.query.validate(
|
||||
'@root',
|
||||
{'foo': 'bar', 'text': null, 'created_at': 24,},
|
||||
{
|
||||
'foo': 'bar',
|
||||
'text': null,
|
||||
'created_at': 24,
|
||||
},
|
||||
);
|
||||
|
||||
if (validation.successful) {
|
||||
|
|
|
@ -11,7 +11,7 @@ class GraphQLField<Value, Serialized> {
|
|||
|
||||
GraphQLField(this.name,
|
||||
{Iterable<GraphQLFieldArgument> arguments: const <GraphQLFieldArgument>[],
|
||||
this.resolve,
|
||||
@required this.resolve,
|
||||
this.type}) {
|
||||
this.arguments.addAll(arguments ?? <GraphQLFieldArgument>[]);
|
||||
}
|
||||
|
|
|
@ -5,10 +5,9 @@ GraphQLObjectType objectType(String name,
|
|||
new GraphQLObjectType(name)..fields.addAll(fields ?? []);
|
||||
|
||||
GraphQLField<T, Serialized> field<T, Serialized>(String name,
|
||||
{Iterable<GraphQLFieldArgument<T, Serialized>> arguments:
|
||||
const <GraphQLFieldArgument<T, Serialized>>[],
|
||||
{Iterable<GraphQLFieldArgument<T, Serialized>> arguments: const [],
|
||||
GraphQLFieldResolver<T, Serialized> resolve,
|
||||
GraphQLType<T, Serialized> innerType}) {
|
||||
GraphQLType<T, Serialized> type}) {
|
||||
return new GraphQLField(name,
|
||||
arguments: arguments, resolve: resolve, type: innerType);
|
||||
arguments: arguments, resolve: resolve, type: type);
|
||||
}
|
||||
|
|
|
@ -20,3 +20,6 @@ class GraphQLSchema {
|
|||
GraphQLSchema graphQLSchema(
|
||||
{@required GraphQLObjectType query, GraphQLObjectType mutation}) =>
|
||||
new GraphQLSchema(query: query, mutation: mutation);
|
||||
|
||||
/// A default resolver that always returns `null`.
|
||||
resolveToNull(_, __) => null;
|
||||
|
|
|
@ -5,5 +5,7 @@ author: Tobe O <thosakwe@gmail.com>
|
|||
homepage: https://github.com/thosakwe/graphql_schema
|
||||
environment:
|
||||
sdk: ">=1.8.0 <3.0.0"
|
||||
dependencies:
|
||||
meta: ^1.0.0
|
||||
dev_dependencies:
|
||||
test: ^0.12.0
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:graphql_schema/graphql_schema.dart';
|
||||
|
||||
final GraphQLObjectType pokemonType = objectType('Pokemon', [
|
||||
field('species', innerType: graphQLString),
|
||||
field('catch_date', innerType: graphQLDate)
|
||||
field('species', type: graphQLString),
|
||||
field('catch_date', type: graphQLDate)
|
||||
]);
|
||||
|
||||
final GraphQLObjectType trainerType =
|
||||
objectType('Trainer', [field('name', innerType: graphQLString)]);
|
||||
objectType('Trainer', [field('name', type: graphQLString)]);
|
||||
|
||||
final GraphQLObjectType pokemonRegionType = objectType('PokemonRegion', [
|
||||
field('trainer', innerType: trainerType),
|
||||
field('pokemon_species', innerType: listType(pokemonType))
|
||||
field('trainer', type: trainerType),
|
||||
field('pokemon_species', type: listType(pokemonType))
|
||||
]);
|
||||
|
|
|
@ -44,9 +44,25 @@ class GraphQL {
|
|||
}
|
||||
}
|
||||
|
||||
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();
|
||||
return executeRequest(schema, document,
|
||||
operationName: operationName,
|
||||
initialValue: initialValue,
|
||||
variableValues: variableValues);
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> executeRequest(
|
||||
GraphQLSchema schema, DocumentContext document, String operationName,
|
||||
{Map<String, dynamic> variableValues: const {}, initialValue}) async {
|
||||
GraphQLSchema schema, DocumentContext document,
|
||||
{String operationName,
|
||||
Map<String, dynamic> variableValues: const {},
|
||||
initialValue}) async {
|
||||
var operation = getOperation(document, operationName);
|
||||
var coercedVariableValues =
|
||||
coerceVariableValues(schema, operation, variableValues ?? {});
|
||||
|
@ -62,17 +78,20 @@ class GraphQL {
|
|||
|
||||
OperationDefinitionContext getOperation(
|
||||
DocumentContext document, String operationName) {
|
||||
var ops = document.definitions.whereType<OperationDefinitionContext>();
|
||||
var ops =
|
||||
document.definitions.where((d) => d is OperationDefinitionContext);
|
||||
|
||||
if (operationName == null) {
|
||||
return ops.length == 1
|
||||
? ops.first
|
||||
? ops.first as OperationDefinitionContext
|
||||
: throw new GraphQLException(
|
||||
'Missing required operation "$operationName".');
|
||||
'This document does not define any operations.');
|
||||
} else {
|
||||
return ops.firstWhere((d) => d.name == operationName,
|
||||
return ops.firstWhere(
|
||||
(d) => (d as OperationDefinitionContext).name == operationName,
|
||||
orElse: () => throw new GraphQLException(
|
||||
'Missing required operation "$operationName".'));
|
||||
'Missing required operation "$operationName".'))
|
||||
as OperationDefinitionContext;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -139,8 +158,9 @@ class GraphQL {
|
|||
|
||||
for (var field in fields) {
|
||||
var fieldName = field.field.fieldName.name;
|
||||
var fieldType =
|
||||
objectType.fields.firstWhere((f) => f.name == fieldName)?.type;
|
||||
var fieldType = objectType.fields
|
||||
.firstWhere((f) => f.name == fieldName, orElse: () => null)
|
||||
?.type;
|
||||
if (fieldType == null) continue;
|
||||
var responseValue = await executeField(document, fieldName, objectType,
|
||||
objectValue, fields, fieldType, variableValues);
|
||||
|
@ -211,8 +231,13 @@ class GraphQL {
|
|||
Future<T> resolveFieldValue<T>(GraphQLObjectType objectType, T objectValue,
|
||||
String fieldName, Map<String, dynamic> argumentValues) async {
|
||||
var field = objectType.fields.firstWhere((f) => f.name == fieldName);
|
||||
|
||||
if (field.resolve == null) {
|
||||
return null;
|
||||
} else {
|
||||
return await field.resolve(objectValue, argumentValues) as T;
|
||||
}
|
||||
}
|
||||
|
||||
Future completeValue(
|
||||
DocumentContext document,
|
||||
|
@ -375,4 +400,7 @@ class GraphQL {
|
|||
|
||||
class GraphQLException extends FormatException {
|
||||
GraphQLException(String message) : super(message);
|
||||
|
||||
@override
|
||||
String toString() => 'GraphQL exception: $message';
|
||||
}
|
|
@ -4,4 +4,5 @@ dependencies:
|
|||
path: ../graphql_schema
|
||||
graphql_parser:
|
||||
path: ../graphql_parser
|
||||
symbol_table: ^1.0.0
|
||||
dev_dependencies:
|
||||
test: ^0.12.0
|
38
graphql_server/test/query_test.dart
Normal file
38
graphql_server/test/query_test.dart
Normal file
|
@ -0,0 +1,38 @@
|
|||
import 'package:graphql_schema/graphql_schema.dart';
|
||||
import 'package:graphql_server/graphql_server.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('todo', () async {
|
||||
var schema = graphQLSchema(
|
||||
query: objectType('todo', [
|
||||
field(
|
||||
'text',
|
||||
type: graphQLString,
|
||||
resolve: (obj, args) => obj['text'],
|
||||
),
|
||||
field(
|
||||
'completed',
|
||||
type: graphQLBoolean,
|
||||
resolve: (obj, args) => obj['completed'],
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
var graphql = new GraphQL(schema);
|
||||
var result = await graphql.parseAndExecute('{ text }', initialValue: {
|
||||
'text': 'Clean your room!',
|
||||
'completed': false,
|
||||
});
|
||||
|
||||
print(result);
|
||||
expect(result, {'text': 'Clean your room!'});
|
||||
});
|
||||
}
|
||||
|
||||
class Todo {
|
||||
final String text;
|
||||
final bool completed;
|
||||
|
||||
Todo({this.text, this.completed});
|
||||
}
|
Loading…
Reference in a new issue