2019-03-29 19:39:52 +00:00
|
|
|
import 'dart:async';
|
2018-11-02 00:31:46 +00:00
|
|
|
import 'dart:convert';
|
2018-08-02 17:02:00 +00:00
|
|
|
import 'dart:io';
|
|
|
|
import 'package:angel_framework/angel_framework.dart';
|
|
|
|
import 'package:angel_validate/server.dart';
|
2018-08-03 21:07:08 +00:00
|
|
|
import 'package:graphql_parser/graphql_parser.dart';
|
|
|
|
import 'package:graphql_schema/graphql_schema.dart';
|
2018-08-02 17:02:00 +00:00
|
|
|
import 'package:graphql_server/graphql_server.dart';
|
|
|
|
|
2019-08-14 16:02:51 +00:00
|
|
|
final ContentType graphQlContentType = ContentType('application', 'graphql');
|
2018-08-02 17:02:00 +00:00
|
|
|
|
2019-08-14 16:02:51 +00:00
|
|
|
final Validator graphQlPostBody = Validator({
|
2018-08-02 17:02:00 +00:00
|
|
|
'query*': isNonEmptyString,
|
|
|
|
'operation_name': isNonEmptyString,
|
2018-08-04 01:41:16 +00:00
|
|
|
'variables': predicate((v) => v == null || v is String || v is Map),
|
2018-08-02 17:02:00 +00:00
|
|
|
});
|
|
|
|
|
2019-08-14 02:44:04 +00:00
|
|
|
final RegExp _num = RegExp(r'^[0-9]+$');
|
|
|
|
|
2018-08-04 03:05:51 +00:00
|
|
|
/// A [RequestHandler] that serves a spec-compliant GraphQL backend.
|
|
|
|
///
|
|
|
|
/// Follows the guidelines listed here:
|
|
|
|
/// https://graphql.org/learn/serving-over-http/
|
2019-03-29 19:39:52 +00:00
|
|
|
RequestHandler graphQLHttp(GraphQL graphQL,
|
|
|
|
{Function(RequestContext, ResponseContext, Stream<Map<String, dynamic>>)
|
|
|
|
onSubscription}) {
|
2018-08-02 17:02:00 +00:00
|
|
|
return (req, res) async {
|
2018-08-04 19:54:18 +00:00
|
|
|
var globalVariables = <String, dynamic>{
|
|
|
|
'__requestctx': req,
|
|
|
|
'__responsectx': res,
|
|
|
|
};
|
|
|
|
|
2019-03-29 19:39:52 +00:00
|
|
|
sendGraphQLResponse(result) async {
|
|
|
|
if (result is Stream<Map<String, dynamic>>) {
|
|
|
|
if (onSubscription == null) {
|
|
|
|
throw StateError(
|
|
|
|
'The GraphQL backend returned a Stream, but no `onSubscription` callback was provided.');
|
|
|
|
} else {
|
|
|
|
return await onSubscription(req, res, result);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
'data': result,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-08-04 01:41:16 +00:00
|
|
|
executeMap(Map map) async {
|
2019-01-23 19:52:02 +00:00
|
|
|
var body = await req.parseBody().then((_) => req.bodyAsMap);
|
2018-11-01 21:27:36 +00:00
|
|
|
var text = body['query'] as String;
|
|
|
|
var operationName = body['operation_name'] as String;
|
|
|
|
var variables = body['variables'];
|
2018-08-04 01:41:16 +00:00
|
|
|
|
|
|
|
if (variables is String) {
|
|
|
|
variables = json.decode(variables as String);
|
|
|
|
}
|
|
|
|
|
2019-03-29 19:39:52 +00:00
|
|
|
return await sendGraphQLResponse(await graphQL.parseAndExecute(
|
|
|
|
text,
|
|
|
|
sourceUrl: 'input',
|
|
|
|
operationName: operationName,
|
|
|
|
variableValues: foldToStringDynamic(variables as Map),
|
|
|
|
globalVariables: globalVariables,
|
|
|
|
));
|
2018-08-04 01:41:16 +00:00
|
|
|
}
|
|
|
|
|
2018-08-03 21:07:08 +00:00
|
|
|
try {
|
2018-08-04 01:41:16 +00:00
|
|
|
if (req.method == 'GET') {
|
2018-11-01 21:27:36 +00:00
|
|
|
if (await validateQuery(graphQlPostBody)(req, res) as bool) {
|
2019-01-23 19:52:02 +00:00
|
|
|
return await executeMap(req.queryParameters);
|
2018-08-04 01:41:16 +00:00
|
|
|
}
|
|
|
|
} else if (req.method == 'POST') {
|
|
|
|
if (req.headers.contentType?.mimeType == graphQlContentType.mimeType) {
|
2019-01-23 19:52:02 +00:00
|
|
|
var text = await req.body.transform(utf8.decoder).join();
|
2019-03-29 19:39:52 +00:00
|
|
|
return sendGraphQLResponse(await graphQL.parseAndExecute(
|
|
|
|
text,
|
|
|
|
sourceUrl: 'input',
|
|
|
|
globalVariables: globalVariables,
|
|
|
|
));
|
2018-08-04 01:41:16 +00:00
|
|
|
} else if (req.headers.contentType?.mimeType == 'application/json') {
|
2018-11-01 21:27:36 +00:00
|
|
|
if (await validate(graphQlPostBody)(req, res) as bool) {
|
2019-01-23 19:52:02 +00:00
|
|
|
return await executeMap(req.bodyAsMap);
|
2018-08-04 01:41:16 +00:00
|
|
|
}
|
2019-08-14 02:44:04 +00:00
|
|
|
} else if (req.headers.contentType?.mimeType == 'multipart/form-data') {
|
|
|
|
var fields = await req.parseBody().then((_) => req.bodyAsMap);
|
|
|
|
var operations = fields['operations'] as String;
|
2019-08-14 02:55:46 +00:00
|
|
|
if (operations == null) {
|
|
|
|
throw AngelHttpException.badRequest(
|
|
|
|
message: 'Missing "operations" field.');
|
|
|
|
}
|
2019-08-14 02:44:04 +00:00
|
|
|
var map = fields.containsKey('map')
|
|
|
|
? json.decode(fields['map'] as String)
|
|
|
|
: null;
|
2019-08-14 02:55:46 +00:00
|
|
|
if (map is! Map) {
|
|
|
|
throw AngelHttpException.badRequest(
|
|
|
|
message: '"map" field must decode to a JSON object.');
|
|
|
|
}
|
2019-08-14 02:44:04 +00:00
|
|
|
var variables = Map<String, dynamic>.from(globalVariables);
|
|
|
|
for (var entry in (map as Map).entries) {
|
2019-08-14 02:58:15 +00:00
|
|
|
var file = req.uploadedFiles
|
|
|
|
.firstWhere((f) => f.name == entry.key, orElse: () => null);
|
|
|
|
if (file == null) {
|
2019-08-14 02:55:46 +00:00
|
|
|
throw AngelHttpException.badRequest(
|
|
|
|
message:
|
2019-08-14 02:58:15 +00:00
|
|
|
'"map" contained key "${entry.key}", but no uploaded file '
|
|
|
|
'has that name.');
|
2019-08-14 02:55:46 +00:00
|
|
|
}
|
|
|
|
if (entry.value is! List) {
|
|
|
|
throw AngelHttpException.badRequest(
|
|
|
|
message:
|
2019-08-14 02:58:15 +00:00
|
|
|
'The value for "${entry.key}" in the "map" field was not a JSON array.');
|
2019-08-14 02:55:46 +00:00
|
|
|
}
|
2019-08-14 02:44:04 +00:00
|
|
|
var objectPaths = entry.value as List;
|
|
|
|
for (var objectPath in objectPaths) {
|
|
|
|
var subPaths = (objectPath as String).split('.');
|
|
|
|
if (subPaths[0] == 'variables') {
|
2019-08-14 02:55:46 +00:00
|
|
|
Object current = variables;
|
2019-08-14 16:14:25 +00:00
|
|
|
for (int i = 1; i < subPaths.length; i++) {
|
|
|
|
var name = subPaths[i];
|
2019-08-14 02:55:46 +00:00
|
|
|
var parent = subPaths.take(i).join('.');
|
2019-08-14 02:44:04 +00:00
|
|
|
if (_num.hasMatch(name)) {
|
2019-08-14 02:55:46 +00:00
|
|
|
if (current is! List) {
|
|
|
|
throw AngelHttpException.badRequest(
|
|
|
|
message:
|
|
|
|
'Object "$parent" is not a JSON array, but the '
|
|
|
|
'"map" field contained a mapping to $parent.$name.');
|
|
|
|
}
|
2019-08-14 02:58:15 +00:00
|
|
|
(current as List)[int.parse(name)] = file;
|
2019-08-14 02:55:46 +00:00
|
|
|
} else {
|
|
|
|
if (current is! Map) {
|
|
|
|
throw AngelHttpException.badRequest(
|
|
|
|
message:
|
|
|
|
'Object "$parent" is not a JSON object, but the '
|
|
|
|
'"map" field contained a mapping to $parent.$name.');
|
|
|
|
}
|
2019-08-14 02:58:15 +00:00
|
|
|
(current as Map)[name] = file;
|
2019-08-14 02:44:04 +00:00
|
|
|
}
|
|
|
|
}
|
2019-08-14 02:55:46 +00:00
|
|
|
} else {
|
|
|
|
throw AngelHttpException.badRequest(
|
|
|
|
message:
|
|
|
|
'All array values in the "map" field must begin with "variables.".');
|
2019-08-14 02:44:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return await sendGraphQLResponse(await graphQL.parseAndExecute(
|
|
|
|
operations,
|
|
|
|
sourceUrl: 'input',
|
|
|
|
globalVariables: variables,
|
|
|
|
));
|
2018-08-04 01:41:16 +00:00
|
|
|
} else {
|
2019-08-14 16:02:51 +00:00
|
|
|
throw AngelHttpException.badRequest();
|
2018-08-03 21:07:08 +00:00
|
|
|
}
|
|
|
|
} else {
|
2019-08-14 16:02:51 +00:00
|
|
|
throw AngelHttpException.badRequest();
|
2018-08-02 17:02:00 +00:00
|
|
|
}
|
2018-08-04 00:16:08 +00:00
|
|
|
} on ValidationException catch (e) {
|
2019-08-14 16:02:51 +00:00
|
|
|
var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
|
2018-08-04 00:16:08 +00:00
|
|
|
|
2019-08-14 16:02:51 +00:00
|
|
|
errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
|
|
|
|
return GraphQLException(errors).toJson();
|
2018-08-03 21:07:08 +00:00
|
|
|
} on AngelHttpException catch (e) {
|
2019-08-14 16:02:51 +00:00
|
|
|
var errors = <GraphQLExceptionError>[GraphQLExceptionError(e.message)];
|
2018-08-03 21:07:08 +00:00
|
|
|
|
2019-08-14 16:02:51 +00:00
|
|
|
errors.addAll(e.errors.map((ee) => GraphQLExceptionError(ee)).toList());
|
|
|
|
return GraphQLException(errors).toJson();
|
2018-08-03 21:07:08 +00:00
|
|
|
} on SyntaxError catch (e) {
|
2019-08-14 16:02:51 +00:00
|
|
|
return GraphQLException.fromSourceSpan(e.message, e.span);
|
2018-08-03 21:07:08 +00:00
|
|
|
} on GraphQLException catch (e) {
|
|
|
|
return e.toJson();
|
2018-08-04 19:18:53 +00:00
|
|
|
} catch (e, st) {
|
|
|
|
if (req.app?.logger != null) {
|
|
|
|
req.app.logger.severe(
|
|
|
|
'An error occurred while processing GraphQL query at ${req.uri}.',
|
|
|
|
e,
|
|
|
|
st);
|
|
|
|
}
|
|
|
|
|
2019-08-14 16:02:51 +00:00
|
|
|
return GraphQLException.fromMessage(e.toString()).toJson();
|
2018-08-02 17:02:00 +00:00
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|