Merge branch 'file-uploads' into 1.2.0
This commit is contained in:
commit
359919e8fd
2 changed files with 112 additions and 0 deletions
|
@ -1,4 +1,46 @@
|
||||||
|
import 'package:angel_framework/angel_framework.dart';
|
||||||
|
import 'package:graphql_schema/graphql_schema.dart';
|
||||||
export 'src/graphiql.dart';
|
export 'src/graphiql.dart';
|
||||||
export 'src/graphql_http.dart';
|
export 'src/graphql_http.dart';
|
||||||
export 'src/graphql_ws.dart';
|
export 'src/graphql_ws.dart';
|
||||||
export 'src/resolvers.dart';
|
export 'src/resolvers.dart';
|
||||||
|
|
||||||
|
/// The canonical [GraphQLUploadType] instance.
|
||||||
|
final GraphQLUploadType graphQLUpload = GraphQLUploadType();
|
||||||
|
|
||||||
|
/// A [GraphQLScalarType] that is used to read uploaded files from
|
||||||
|
/// `multipart/form-data` requests.
|
||||||
|
class GraphQLUploadType extends GraphQLScalarType<UploadedFile, UploadedFile> {
|
||||||
|
@override
|
||||||
|
String get name => 'Upload';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get description =>
|
||||||
|
'Represents a file that has been uploaded to the server.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
GraphQLType<UploadedFile, UploadedFile> coerceToInputObject() => this;
|
||||||
|
|
||||||
|
@override
|
||||||
|
UploadedFile deserialize(UploadedFile serialized) => serialized;
|
||||||
|
|
||||||
|
@override
|
||||||
|
UploadedFile serialize(UploadedFile value) => value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ValidationResult<UploadedFile> validate(String key, UploadedFile input) {
|
||||||
|
if (input != null && input is! UploadedFile) {
|
||||||
|
return _Vr(false, errors: ['Expected "$key" to be a boolean.']);
|
||||||
|
}
|
||||||
|
return _Vr(true, value: input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Really need to make the validation result constructors *public*
|
||||||
|
class _Vr<T> implements ValidationResult<T> {
|
||||||
|
final bool successful;
|
||||||
|
final List<String> errors;
|
||||||
|
final T value;
|
||||||
|
|
||||||
|
_Vr(this.successful, {this.errors, this.value});
|
||||||
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ final Validator graphQlPostBody = new Validator({
|
||||||
'variables': predicate((v) => v == null || v is String || v is Map),
|
'variables': predicate((v) => v == null || v is String || v is Map),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final RegExp _num = RegExp(r'^[0-9]+$');
|
||||||
|
|
||||||
/// A [RequestHandler] that serves a spec-compliant GraphQL backend.
|
/// A [RequestHandler] that serves a spec-compliant GraphQL backend.
|
||||||
///
|
///
|
||||||
/// Follows the guidelines listed here:
|
/// Follows the guidelines listed here:
|
||||||
|
@ -80,6 +82,74 @@ RequestHandler graphQLHttp(GraphQL graphQL,
|
||||||
if (await validate(graphQlPostBody)(req, res) as bool) {
|
if (await validate(graphQlPostBody)(req, res) as bool) {
|
||||||
return await executeMap(req.bodyAsMap);
|
return await executeMap(req.bodyAsMap);
|
||||||
}
|
}
|
||||||
|
} else if (req.headers.contentType?.mimeType == 'multipart/form-data') {
|
||||||
|
// TODO: Support file uploads in batch requests.
|
||||||
|
var fields = await req.parseBody().then((_) => req.bodyAsMap);
|
||||||
|
var operations = fields['operations'] as String;
|
||||||
|
if (operations == null) {
|
||||||
|
throw AngelHttpException.badRequest(
|
||||||
|
message: 'Missing "operations" field.');
|
||||||
|
}
|
||||||
|
var map = fields.containsKey('map')
|
||||||
|
? json.decode(fields['map'] as String)
|
||||||
|
: null;
|
||||||
|
if (map is! Map) {
|
||||||
|
throw AngelHttpException.badRequest(
|
||||||
|
message: '"map" field must decode to a JSON object.');
|
||||||
|
}
|
||||||
|
var variables = Map<String, dynamic>.from(globalVariables);
|
||||||
|
for (var entry in (map as Map).entries) {
|
||||||
|
var file = req.uploadedFiles
|
||||||
|
.firstWhere((f) => f.name == entry.key, orElse: () => null);
|
||||||
|
if (file == null) {
|
||||||
|
throw AngelHttpException.badRequest(
|
||||||
|
message:
|
||||||
|
'"map" contained key "${entry.key}", but no uploaded file '
|
||||||
|
'has that name.');
|
||||||
|
}
|
||||||
|
if (entry.value is! List) {
|
||||||
|
throw AngelHttpException.badRequest(
|
||||||
|
message:
|
||||||
|
'The value for "${entry.key}" in the "map" field was not a JSON array.');
|
||||||
|
}
|
||||||
|
var objectPaths = entry.value as List;
|
||||||
|
for (var objectPath in objectPaths) {
|
||||||
|
var subPaths = (objectPath as String).split('.');
|
||||||
|
if (subPaths[0] == 'variables') {
|
||||||
|
Object current = variables;
|
||||||
|
for (int i = 0; i < subPaths.length; i++) {
|
||||||
|
var name = subPaths[0];
|
||||||
|
var parent = subPaths.take(i).join('.');
|
||||||
|
if (_num.hasMatch(name)) {
|
||||||
|
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.');
|
||||||
|
}
|
||||||
|
(current as List)[int.parse(name)] = file;
|
||||||
|
} 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.');
|
||||||
|
}
|
||||||
|
(current as Map)[name] = file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw AngelHttpException.badRequest(
|
||||||
|
message:
|
||||||
|
'All array values in the "map" field must begin with "variables.".');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return await sendGraphQLResponse(await graphQL.parseAndExecute(
|
||||||
|
operations,
|
||||||
|
sourceUrl: 'input',
|
||||||
|
globalVariables: variables,
|
||||||
|
));
|
||||||
} else {
|
} else {
|
||||||
throw new AngelHttpException.badRequest();
|
throw new AngelHttpException.badRequest();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue