Finalize subscriptions

This commit is contained in:
Tobe O 2019-04-17 20:52:26 -04:00
parent 4d30090322
commit b07cb10af7
9 changed files with 179 additions and 15 deletions

View file

@ -0,0 +1,99 @@
// Inspired by:
// https://www.apollographql.com/docs/apollo-server/features/subscriptions/#subscriptions-example
import 'package:angel_file_service/angel_file_service.dart';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_graphql/angel_graphql.dart';
import 'package:angel_serialize/angel_serialize.dart';
import 'package:file/local.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
import 'package:graphql_server/mirrors.dart';
import 'package:http/io_client.dart';
import 'package:logging/logging.dart';
main() async {
var logger = Logger('angel_graphql');
var app = Angel(logger: logger);
var http = AngelHttp(app);
app.logger.onRecord.listen((rec) {
print(rec);
if (rec.error != null) print(rec.error);
if (rec.stackTrace != null) print(rec.stackTrace);
});
// Create an in-memory service.
var fs = LocalFileSystem();
var postService =
app.use('/api/posts', JsonFileService(fs.file('posts.json')));
// Also get a [Stream] of item creation events.
var postAdded = postService.afterCreated
.asStream()
.map((e) => {'postAdded': e.result})
.asBroadcastStream();
// GraphQL setup.
var postType = objectType('Post', fields: [
field('author', graphQLString),
field('comment', graphQLString),
]);
var schema = graphQLSchema(
// Hooked up to the postService:
// type Query { posts: [Post] }
queryType: objectType(
'Query',
fields: [
field(
'posts',
listOf(postType),
resolve: resolveViaServiceIndex(postService),
),
],
),
// Hooked up to the postService:
// type Mutation {
// addPost(author: String!, comment: String!): Post
// }
mutationType: objectType(
'Mutation',
fields: [
field(
'addPost',
postType,
inputs: [
GraphQLFieldInput(
'data', postType.toInputObject('PostInput').nonNullable()),
],
resolve: resolveViaServiceCreate(postService),
),
],
),
// Hooked up to `postAdded`:
// type Subscription { postAdded: Post }
subscriptionType: objectType(
'Subscription',
fields: [
field('postAdded', postType, resolve: (_, __) => postAdded),
],
),
);
// Mount GraphQL routes; we'll support HTTP and WebSockets transports.
app.get('/graphql', graphQLHttp(GraphQL(schema)));
app.get('/graphql/subscriptions', graphQLWS(GraphQL(schema)));
app.get('/graphiql', graphiQL());
var server = await http.startServer('127.0.0.1', 3000);
var uri =
Uri(scheme: 'http', host: server.address.address, port: server.port);
var graphiqlUri = uri.replace(path: 'graphiql');
var postsUri = uri.replace(pathSegments: ['api', 'posts']);
print('Listening at $uri');
print('Access graphiql at $graphiqlUri');
print('Access posts service at $postsUri');
}

View file

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

View file

@ -0,0 +1,36 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:angel_framework/angel_framework.dart';
import 'package:angel_framework/http.dart';
import 'package:angel_validate/server.dart';
import 'package:graphql_parser/graphql_parser.dart';
import 'package:graphql_schema/graphql_schema.dart';
import 'package:graphql_server/graphql_server.dart';
/// A [RequestHandler] that serves a spec-compliant GraphQL backend, over WebSockets.
/// This endpoint only supports WebSockets, and can be used to deliver subscription events.
///
/// `graphQLWS` uses the Apollo WebSocket protocol, for the sake of compatibility with
/// existing tooling.
///
/// See:
/// * https://github.com/apollographql/subscriptions-transport-ws
RequestHandler graphQLWS(GraphQL graphQL) {
return (req, res) async {
if (req is HttpRequestContext) {
if (WebSocketTransformer.isUpgradeRequest(req.rawRequest)) {
await res.detach();
var socket = await WebSocketTransformer.upgrade(req.rawRequest);
// TODO: Apollo protocol
throw UnimplementedError('Apollo protocol not yet implemented.');
} else {
throw AngelHttpException.badRequest(
message: 'The `graphQLWS` endpoint only accepts WebSockets.');
}
} else {
throw AngelHttpException.badRequest(
message: 'The `graphQLWS` endpoint only accepts HTTP/1.1 requests.');
}
};
}

View file

@ -62,6 +62,24 @@ GraphQLFieldResolver<Value, Serialized>
};
}
/// A GraphQL resolver that `creates` a single value in an Angel service.
///
/// This resolver should be used on a field with at least the following input:
/// * `data`: a [GraphQLObjectType] corresponding to the format of `data` to be passed to `create`
///
/// The arguments passed to the resolver will be forwarded to the service, and the
/// service will receive [Providers.graphql].
GraphQLFieldResolver<Value, Serialized>
resolveViaServiceCreate<Value, Serialized>(
Service<dynamic, Value> service) {
return (_, arguments) async {
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
return await service.create(arguments['data'] as Value, params);
};
}
/// A GraphQL resolver that `modifies` a single value from an Angel service.
///
/// This resolver should be used on a field with at least the following inputs:
@ -77,7 +95,6 @@ GraphQLFieldResolver<Value, Serialized>
var _requestInfo = _fetchRequestInfo(arguments);
var params = {'query': _getQuery(arguments), 'provider': Providers.graphQL}
..addAll(_requestInfo);
print(params);
var id = arguments.remove(idField);
return await service.modify(id, arguments['data'] as Value, params);
};

1
angel_graphql/posts.json Normal file
View file

@ -0,0 +1 @@
[]

View file

@ -6,6 +6,7 @@ author: Tobe O <thosakwe@gmail.com>
environment:
sdk: ">=2.0.0-dev <3.0.0"
dependencies:
angel_file_service: ^2.0.0
angel_framework: ^2.0.0-alpha
angel_websocket: ^2.0.0
angel_validate: ^2.0.0-alpha
@ -16,6 +17,6 @@ dependencies:
dev_dependencies:
angel_serialize: ^2.0.0
logging: ^0.11.0
# dependency_overrides:
# graphql_server:
# path: ../graphql_server
dependency_overrides:
graphql_server:
path: ../graphql_server

View file

@ -1,3 +1,6 @@
# 1.0.0-beta.3
* Introspection on subscription types (if any).
# 1.0.0-beta.2
* Fix bug where field aliases would not be resolved.

View file

@ -47,6 +47,8 @@ class GraphQL {
if (_schema.queryType != null) this.customTypes.add(_schema.queryType);
if (_schema.mutationType != null)
this.customTypes.add(_schema.mutationType);
if (_schema.subscriptionType != null)
this.customTypes.add(_schema.subscriptionType);
}
GraphQLType convertType(TypeContext ctx) {
@ -269,7 +271,7 @@ class GraphQL {
) async* {
await for (var event in sourceStream) {
yield await executeSubscriptionEvent(document, subscription, schema,
initialValue, variableValues, globalVariables, event);
event, variableValues, globalVariables);
}
}
@ -279,8 +281,7 @@ class GraphQL {
GraphQLSchema schema,
initialValue,
Map<String, dynamic> variableValues,
Map<String, dynamic> globalVariables,
event) async {
Map<String, dynamic> globalVariables) async {
var selectionSet = subscription.selectionSet;
var subscriptionType = schema.subscriptionType;
if (subscriptionType == null)
@ -290,7 +291,7 @@ class GraphQL {
try {
var data = await executeSelectionSet(document, selectionSet,
subscriptionType, initialValue, variableValues, globalVariables);
return {'data': data, 'errors': []};
return {'data': data};
} on GraphQLException catch (e) {
return {
'data': null,
@ -384,7 +385,9 @@ class GraphQL {
var argumentValues = field.field.arguments;
var fieldName =
field.field.fieldName.alias?.name ?? field.field.fieldName.name;
var desiredField = objectType.fields.firstWhere((f) => f.name == fieldName);
var desiredField = objectType.fields.firstWhere((f) => f.name == fieldName,
orElse: () => throw FormatException(
'${objectType.name} has no field named "$fieldName".'));
var argumentDefinitions = desiredField.inputs;
for (var argumentDefinition in argumentDefinitions) {
@ -480,15 +483,13 @@ class GraphQL {
String fieldName, Map<String, dynamic> argumentValues) async {
var field = objectType.fields.firstWhere((f) => f.name == fieldName);
if (field.resolve == null) {
if (objectValue is Map) {
return objectValue[fieldName] as T;
} else if (field.resolve == null) {
if (defaultFieldResolver != null)
return await defaultFieldResolver(
objectValue, fieldName, argumentValues);
if (objectValue is Map) {
return objectValue[fieldName] as T;
}
return null;
} else {
return await field.resolve(objectValue, argumentValues) as T;
@ -505,7 +506,7 @@ class GraphQL {
Map<String, dynamic> globalVariables) async {
if (fieldType is GraphQLNonNullableType) {
var innerType = fieldType.ofType;
var completedResult = completeValue(document, fieldName, innerType,
var completedResult = await completeValue(document, fieldName, innerType,
fields, result, variableValues, globalVariables);
if (completedResult == null) {

View file

@ -89,6 +89,7 @@ GraphQLSchema reflectSchema(GraphQLSchema schema, List<GraphQLType> allTypes) {
return new GraphQLSchema(
queryType: objectType(schema.queryType.name, fields: fields),
mutationType: schema.mutationType,
subscriptionType: schema.subscriptionType,
);
}
@ -436,6 +437,10 @@ List<GraphQLType> fetchAllTypes(
types.addAll(_fetchAllTypesFromObject(schema.mutationType));
}
if (schema.subscriptionType != null) {
types.addAll(_fetchAllTypesFromObject(schema.subscriptionType));
}
return types;
}