Added mirrors support

This commit is contained in:
Tobe O 2018-08-02 13:02:00 -04:00
parent 16645c1104
commit b3530e5a2a
12 changed files with 385 additions and 22 deletions

View 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>

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,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');
}

View file

@ -0,0 +1,3 @@
export 'src/graphiql.dart';
export 'src/graphql_http.dart';
export 'src/resolvers.dart';

View 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();
}

View 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();
}
};
}

View 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;
};
}

View 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

View file

@ -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,

View 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;
}

View file

@ -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

View file

@ -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!'}
]
});
});
}