Added mirrors support
This commit is contained in:
parent
16645c1104
commit
b3530e5a2a
12 changed files with 385 additions and 22 deletions
7
.idea/runConfigurations/main_dart.xml
Normal file
7
.idea/runConfigurations/main_dart.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="main.dart" type="DartCommandLineRunConfigurationType" factoryName="Dart Command Line Application" singleton="true" nameIsGenerated="true">
|
||||
<option name="filePath" value="$PROJECT_DIR$/angel_graphql/example/main.dart" />
|
||||
<option name="workingDirectory" value="$PROJECT_DIR$/angel_graphql" />
|
||||
<method />
|
||||
</configuration>
|
||||
</component>
|
|
@ -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" />
|
||||
|
|
40
angel_graphql/example/main.dart
Normal file
40
angel_graphql/example/main.dart
Normal file
|
@ -0,0 +1,40 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_graphql/angel_graphql.dart';
|
||||
import 'package:graphql_schema/graphql_schema.dart';
|
||||
import 'package:graphql_server/graphql_server.dart';
|
||||
|
||||
main() async {
|
||||
var app = new Angel();
|
||||
var http = new AngelHttp(app);
|
||||
|
||||
var todoService = app.use('api/todos', new MapService()) as Service;
|
||||
|
||||
var todo = objectType('todo', [
|
||||
field(
|
||||
'text',
|
||||
type: graphQLString,
|
||||
),
|
||||
]);
|
||||
|
||||
var api = objectType('api', [
|
||||
field(
|
||||
'todos',
|
||||
type: listType(todo),
|
||||
resolve: resolveFromService(todoService),
|
||||
),
|
||||
]);
|
||||
|
||||
var schema = graphQLSchema(query: api);
|
||||
|
||||
app.all('/graphql', graphQLHttp(new GraphQL(schema)));
|
||||
app.get('/graphiql', graphiql());
|
||||
|
||||
await todoService.create({'text': 'Clean your room!', 'completed': true});
|
||||
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
var uri =
|
||||
new Uri(scheme: 'http', host: server.address.address, port: server.port);
|
||||
var graphiqlUri = uri.replace(path: 'graphiql');
|
||||
print('Listening at $uri');
|
||||
print('Access graphiql at $graphiqlUri');
|
||||
}
|
3
angel_graphql/lib/angel_graphql.dart
Normal file
3
angel_graphql/lib/angel_graphql.dart
Normal file
|
@ -0,0 +1,3 @@
|
|||
export 'src/graphiql.dart';
|
||||
export 'src/graphql_http.dart';
|
||||
export 'src/resolvers.dart';
|
62
angel_graphql/lib/src/graphiql.dart
Normal file
62
angel_graphql/lib/src/graphiql.dart
Normal file
|
@ -0,0 +1,62 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
|
||||
RequestHandler graphiql({String graphqlEndpoint: '/graphql'}) {
|
||||
return (req, res) {
|
||||
res
|
||||
..contentType = new ContentType('text', 'html')
|
||||
..write(renderGraphiql(graphqlEndpoint: graphqlEndpoint))
|
||||
..end();
|
||||
};
|
||||
}
|
||||
|
||||
String renderGraphiql({String graphqlEndpoint: '/graphql'}) {
|
||||
return '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Angel GraphQL</title>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.css">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html, body, #app {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.2.0/umd/react.production.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.2.0/umd/react-dom.production.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/2.0.3/fetch.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/graphiql/0.11.11/graphiql.min.js"></script>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
return fetch('$graphqlEndpoint', {
|
||||
method: 'post',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(graphQLParams)
|
||||
}).then(function(response) {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
GraphiQL,
|
||||
{fetcher: graphQLFetcher}
|
||||
),
|
||||
document.getElementById('app')
|
||||
);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
.trim();
|
||||
}
|
47
angel_graphql/lib/src/graphql_http.dart
Normal file
47
angel_graphql/lib/src/graphql_http.dart
Normal file
|
@ -0,0 +1,47 @@
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:angel_validate/server.dart';
|
||||
import 'package:dart2_constant/convert.dart';
|
||||
import 'package:graphql_server/graphql_server.dart';
|
||||
|
||||
final ContentType graphQlContentType =
|
||||
new ContentType('application', 'graphql');
|
||||
|
||||
final Validator graphQlPostBody = new Validator({
|
||||
'query*': isNonEmptyString,
|
||||
'operation_name': isNonEmptyString,
|
||||
'variables': predicate((v) => v == null || v is Map),
|
||||
});
|
||||
|
||||
Map<String, dynamic> _foldToStringDynamic(Map map) {
|
||||
return map == null
|
||||
? null
|
||||
: map.keys.fold<Map<String, dynamic>>(
|
||||
<String, dynamic>{}, (out, k) => out..[k.toString()] = map[k]);
|
||||
}
|
||||
|
||||
RequestHandler graphQLHttp(GraphQL graphQl) {
|
||||
return (req, res) async {
|
||||
if (req.headers.contentType?.mimeType == graphQlContentType.mimeType) {
|
||||
var text = utf8.decode(await req.lazyOriginalBuffer());
|
||||
return {'data': await graphQl.parseAndExecute(text, sourceUrl: 'input')};
|
||||
} else if (req.headers.contentType?.mimeType == 'application/json') {
|
||||
if (await validate(graphQlPostBody)(req, res)) {
|
||||
var text = req.body['query'] as String;
|
||||
var operationName = req.body['operation_name'] as String;
|
||||
var variables = req.body['variables'] as Map;
|
||||
return {
|
||||
'data': await graphQl.parseAndExecute(
|
||||
text,
|
||||
sourceUrl: 'input',
|
||||
operationName: operationName,
|
||||
variableValues: _foldToStringDynamic(variables),
|
||||
),
|
||||
};
|
||||
}
|
||||
} else {
|
||||
throw new AngelHttpException.badRequest();
|
||||
}
|
||||
};
|
||||
}
|
25
angel_graphql/lib/src/resolvers.dart
Normal file
25
angel_graphql/lib/src/resolvers.dart
Normal file
|
@ -0,0 +1,25 @@
|
|||
import 'package:angel_framework/angel_framework.dart';
|
||||
import 'package:graphql_schema/graphql_schema.dart';
|
||||
|
||||
/// A GraphQL resolver that indexes an Angel service.
|
||||
///
|
||||
/// If [enableRead] is `true`, and the [idField] is present in the input,
|
||||
/// a `read` will be performed, rather than an `index`.
|
||||
///
|
||||
/// The arguments passed to the resolver will be forwarded to service, and the
|
||||
/// service will receive [Providers.graphql].
|
||||
GraphQLFieldResolver<Value, Serialized> resolveFromService<Value, Serialized>(
|
||||
Service service,
|
||||
{String idField: 'id',
|
||||
bool enableRead: true}) {
|
||||
return (_, arguments) async {
|
||||
var params = {'query': arguments, 'provider': Providers.graphql};
|
||||
|
||||
if (enableRead && arguments.containsKey(idField)) {
|
||||
var id = arguments.remove(idField);
|
||||
return await service.read(id, params) as Value;
|
||||
}
|
||||
|
||||
return await service.index(params) as Value;
|
||||
};
|
||||
}
|
10
angel_graphql/pubspec.yaml
Normal file
10
angel_graphql/pubspec.yaml
Normal file
|
@ -0,0 +1,10 @@
|
|||
name: angel_graphql
|
||||
dependencies:
|
||||
angel_framework: ^1.0.0
|
||||
angel_validate: ^1.0.0
|
||||
graphql_parser:
|
||||
path: ../graphql_parser
|
||||
graphql_schema:
|
||||
path: ../graphql_schema
|
||||
graphql_server:
|
||||
path: ../graphql_server
|
|
@ -171,7 +171,7 @@ class GraphQL {
|
|||
return resultMap;
|
||||
}
|
||||
|
||||
executeField(
|
||||
Future executeField(
|
||||
DocumentContext document,
|
||||
String fieldName,
|
||||
GraphQLObjectType objectType,
|
||||
|
@ -246,6 +246,7 @@ class GraphQL {
|
|||
List<SelectionContext> fields,
|
||||
result,
|
||||
Map<String, dynamic> variableValues) async {
|
||||
|
||||
if (fieldType is GraphQLNonNullableType) {
|
||||
var innerType = fieldType.innerType;
|
||||
var completedResult = completeValue(
|
||||
|
@ -270,10 +271,14 @@ class GraphQL {
|
|||
}
|
||||
|
||||
var innerType = fieldType.innerType;
|
||||
return (result as Iterable)
|
||||
.map((resultItem) => completeValue(document, '(item in "$fieldName")',
|
||||
innerType, fields, resultItem, variableValues))
|
||||
.toList();
|
||||
var out = [];
|
||||
|
||||
for (var resultItem in (result as Iterable)) {
|
||||
out.add(await completeValue(document, '(item in "$fieldName")',
|
||||
innerType, fields, resultItem, variableValues));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
if (fieldType is GraphQLScalarType) {
|
||||
|
@ -288,7 +293,7 @@ class GraphQL {
|
|||
|
||||
if (fieldType is GraphQLObjectType) {
|
||||
var objectType = fieldType;
|
||||
var subSelectionSet = new SelectionSetContext.merged(fields);
|
||||
var subSelectionSet = mergeSelectionSets(fields);
|
||||
return await executeSelectionSet(
|
||||
document, subSelectionSet, objectType, result, variableValues);
|
||||
}
|
||||
|
@ -297,6 +302,20 @@ class GraphQL {
|
|||
throw new UnsupportedError('Unsupported type: $fieldType');
|
||||
}
|
||||
|
||||
SelectionSetContext mergeSelectionSets(List<SelectionContext> fields) {
|
||||
var selections = <SelectionContext>[];
|
||||
|
||||
for (var field in fields) {
|
||||
if (field.field?.selectionSet != null) {
|
||||
selections.addAll(field.field.selectionSet.selections);
|
||||
} else if (field.inlineFragment?.selectionSet != null) {
|
||||
selections.addAll(field.inlineFragment.selectionSet.selections);
|
||||
}
|
||||
}
|
||||
|
||||
return new SelectionSetContext.merged(selections);
|
||||
}
|
||||
|
||||
Map<String, List<SelectionContext>> collectFields(
|
||||
DocumentContext document,
|
||||
GraphQLObjectType objectType,
|
||||
|
|
130
graphql_server/lib/mirrors.dart
Normal file
130
graphql_server/lib/mirrors.dart
Normal file
|
@ -0,0 +1,130 @@
|
|||
import 'dart:mirrors';
|
||||
|
||||
import 'package:angel_serialize/angel_serialize.dart';
|
||||
import 'package:graphql_schema/graphql_schema.dart';
|
||||
import 'package:recase/recase.dart';
|
||||
|
||||
/// Reflects upon a given [type] and dynamically generates a [GraphQLType] that corresponds to it.
|
||||
///
|
||||
/// This function is aware of the annotations from `package:angel_serialize`, and works seamlessly
|
||||
/// with them.
|
||||
GraphQLType objectTypeFromDartType(Type type, [List<Type> typeArguments]) {
|
||||
if (type == bool) {
|
||||
return graphQLBoolean;
|
||||
} else if (type == int) {
|
||||
return graphQLInt;
|
||||
} else if (type == double || type == num) {
|
||||
return graphQLFloat;
|
||||
} else if (type == String) {
|
||||
return graphQLString;
|
||||
}
|
||||
|
||||
var mirror = reflectType(type, typeArguments);
|
||||
|
||||
if (mirror is! ClassMirror) {
|
||||
throw new StateError(
|
||||
'$type is not a class, and therefore cannot be converted into a GraphQL object type.');
|
||||
}
|
||||
|
||||
return objectTypeFromClassMirror(mirror as ClassMirror);
|
||||
}
|
||||
|
||||
GraphQLObjectType objectTypeFromClassMirror(ClassMirror mirror) {
|
||||
var fields = <GraphQLField>[];
|
||||
|
||||
for (var name in mirror.instanceMembers.keys) {
|
||||
var methodMirror = mirror.instanceMembers[name];
|
||||
var exclude = _getExclude(name, methodMirror);
|
||||
var canAdd = exclude?.canSerialize != true;
|
||||
if (methodMirror.isGetter && canAdd) {
|
||||
fields.add(fieldFromGetter(name, methodMirror, exclude, mirror));
|
||||
}
|
||||
}
|
||||
|
||||
return objectType(MirrorSystem.getName(mirror.simpleName), fields);
|
||||
}
|
||||
|
||||
GraphQLField fieldFromGetter(
|
||||
Symbol name, MethodMirror mirror, Exclude exclude, ClassMirror clazz) {
|
||||
var type = objectTypeFromDartType(mirror.returnType.reflectedType,
|
||||
mirror.returnType.typeArguments.map((t) => t.reflectedType).toList());
|
||||
|
||||
var nameString = _getSerializedName(name, mirror, clazz);
|
||||
var defaultValue = _getDefaultValue(mirror);
|
||||
|
||||
if (nameString == 'id' && _autoNames(clazz)) {
|
||||
type = graphQLId;
|
||||
}
|
||||
|
||||
return field(
|
||||
nameString,
|
||||
type: type,
|
||||
resolve: (obj, _) {
|
||||
if (obj is Map && exclude?.canSerialize != true) {
|
||||
return obj[nameString];
|
||||
} else if (obj != null && exclude?.canSerialize != true) {
|
||||
return reflect(obj).getField(name);
|
||||
} else {
|
||||
return defaultValue;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Exclude _getExclude(Symbol name, MethodMirror mirror) {
|
||||
for (var obj in mirror.metadata) {
|
||||
if (obj.reflectee is Exclude) {
|
||||
var exclude = obj.reflectee as Exclude;
|
||||
return exclude;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
String _getSerializedName(Symbol name, MethodMirror mirror, ClassMirror clazz) {
|
||||
// First search for an @Alias()
|
||||
for (var obj in mirror.metadata) {
|
||||
if (obj.reflectee is Alias) {
|
||||
var alias = obj.reflectee as Alias;
|
||||
return alias.name;
|
||||
}
|
||||
}
|
||||
|
||||
// Next, search for a @Serializable()
|
||||
for (var obj in clazz.metadata) {
|
||||
if (obj.reflectee is Serializable) {
|
||||
var ann = obj.reflectee as Serializable;
|
||||
|
||||
if (ann.autoSnakeCaseNames != false) {
|
||||
return new ReCase(MirrorSystem.getName(name)).snakeCase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return MirrorSystem.getName(name);
|
||||
}
|
||||
|
||||
dynamic _getDefaultValue(MethodMirror mirror) {
|
||||
// Search for a @DefaultValue
|
||||
for (var obj in mirror.metadata) {
|
||||
if (obj.reflectee is DefaultValue) {
|
||||
var ann = obj.reflectee as DefaultValue;
|
||||
return ann.value;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
bool _autoNames(ClassMirror clazz) {
|
||||
// Search for a @Serializable()
|
||||
for (var obj in clazz.metadata) {
|
||||
if (obj.reflectee is Serializable) {
|
||||
var ann = obj.reflectee as Serializable;
|
||||
return ann.autoIdAndDateFields != false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -1,8 +1,10 @@
|
|||
name: graphql_server
|
||||
dependencies:
|
||||
angel_serialize: ^2.0.0
|
||||
graphql_schema:
|
||||
path: ../graphql_schema
|
||||
graphql_parser:
|
||||
path: ../graphql_parser
|
||||
recase: ^1.0.0
|
||||
dev_dependencies:
|
||||
test: ^0.12.0
|
|
@ -3,30 +3,44 @@ import 'package:graphql_server/graphql_server.dart';
|
|||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
test('todo', () async {
|
||||
test('single element', () async {
|
||||
var todoType = objectType('todo', [
|
||||
field(
|
||||
'text',
|
||||
type: graphQLString,
|
||||
resolve: (obj, args) => obj.text,
|
||||
),
|
||||
field(
|
||||
'completed',
|
||||
type: graphQLBoolean,
|
||||
resolve: (obj, args) => obj.completed,
|
||||
),
|
||||
]);
|
||||
|
||||
var schema = graphQLSchema(
|
||||
query: objectType('todo', [
|
||||
query: objectType('api', [
|
||||
field(
|
||||
'text',
|
||||
type: graphQLString,
|
||||
resolve: (obj, args) => obj['text'],
|
||||
),
|
||||
field(
|
||||
'completed',
|
||||
type: graphQLBoolean,
|
||||
resolve: (obj, args) => obj['completed'],
|
||||
'todos',
|
||||
type: listType(todoType),
|
||||
resolve: (_, __) => [
|
||||
new Todo(
|
||||
text: 'Clean your room!',
|
||||
completed: false,
|
||||
)
|
||||
],
|
||||
),
|
||||
]),
|
||||
);
|
||||
|
||||
var graphql = new GraphQL(schema);
|
||||
var result = await graphql.parseAndExecute('{ text }', initialValue: {
|
||||
'text': 'Clean your room!',
|
||||
'completed': false,
|
||||
});
|
||||
var result = await graphql.parseAndExecute('{ todos { text } }');
|
||||
|
||||
print(result);
|
||||
expect(result, {'text': 'Clean your room!'});
|
||||
expect(result, {
|
||||
'todos': [
|
||||
{'text': 'Clean your room!'}
|
||||
]
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue