Finalize subscriptions
This commit is contained in:
parent
4d30090322
commit
b07cb10af7
9 changed files with 179 additions and 15 deletions
99
angel_graphql/example/subscription.dart
Normal file
99
angel_graphql/example/subscription.dart
Normal 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');
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export 'src/graphiql.dart';
|
||||
export 'src/graphql_http.dart';
|
||||
export 'src/graphql_ws.dart';
|
||||
export 'src/resolvers.dart';
|
||||
|
|
36
angel_graphql/lib/src/graphql_ws.dart
Normal file
36
angel_graphql/lib/src/graphql_ws.dart
Normal 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.');
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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
1
angel_graphql/posts.json
Normal file
|
@ -0,0 +1 @@
|
|||
[]
|
|
@ -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
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue