apollo patch
This commit is contained in:
parent
37d58529c2
commit
7385aacc59
6 changed files with 174 additions and 87 deletions
|
@ -85,8 +85,9 @@ main() async {
|
|||
|
||||
// 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());
|
||||
app.get('/subscriptions', graphQLWS(GraphQL(schema)));
|
||||
app.get('/graphiql',
|
||||
graphiQL(subscriptionsEndpoint: 'ws://localhost:3000/subscriptions'));
|
||||
|
||||
var server = await http.startServer('127.0.0.1', 3000);
|
||||
var uri =
|
||||
|
|
|
@ -5,16 +5,38 @@ import 'package:http_parser/http_parser.dart';
|
|||
///
|
||||
/// By default, the interface expects your backend to be mounted at `/graphql`; this is configurable
|
||||
/// via [graphQLEndpoint].
|
||||
RequestHandler graphiQL({String graphQLEndpoint: '/graphql'}) {
|
||||
RequestHandler graphiQL(
|
||||
{String graphQLEndpoint: '/graphql', String subscriptionsEndpoint}) {
|
||||
return (req, res) {
|
||||
res
|
||||
..contentType = new MediaType('text', 'html')
|
||||
..write(renderGraphiql(graphqlEndpoint: graphQLEndpoint))
|
||||
..write(renderGraphiql(
|
||||
graphqlEndpoint: graphQLEndpoint,
|
||||
subscriptionsEndpoint: subscriptionsEndpoint))
|
||||
..close();
|
||||
};
|
||||
}
|
||||
|
||||
String renderGraphiql({String graphqlEndpoint: '/graphql'}) {
|
||||
String renderGraphiql(
|
||||
{String graphqlEndpoint: '/graphql', String subscriptionsEndpoint}) {
|
||||
var subscriptionsScripts = '',
|
||||
subscriptionsFetcher = '',
|
||||
fetcherName = 'graphQLFetcher';
|
||||
|
||||
if (subscriptionsEndpoint != null) {
|
||||
fetcherName = 'subscriptionsFetcher';
|
||||
subscriptionsScripts = '''
|
||||
<script src="//unpkg.com/subscriptions-transport-ws@0.5.4/browser/client.js"></script>
|
||||
<script src="//unpkg.com/graphiql-subscriptions-fetcher@0.0.2/browser/client.js"></script>
|
||||
''';
|
||||
subscriptionsFetcher = '''
|
||||
let subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient('$subscriptionsEndpoint', {
|
||||
reconnect: true
|
||||
});
|
||||
let $fetcherName = window.GraphiQLSubscriptionsFetcher.graphQLFetcher(subscriptionsClient, graphQLFetcher);
|
||||
''';
|
||||
}
|
||||
|
||||
return '''
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
@ -38,6 +60,7 @@ String renderGraphiql({String graphqlEndpoint: '/graphql'}) {
|
|||
<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.js"></script>
|
||||
$subscriptionsScripts
|
||||
<script>
|
||||
window.onload = function() {
|
||||
function graphQLFetcher(graphQLParams) {
|
||||
|
@ -49,10 +72,11 @@ String renderGraphiql({String graphqlEndpoint: '/graphql'}) {
|
|||
return response.json();
|
||||
});
|
||||
}
|
||||
$subscriptionsFetcher
|
||||
ReactDOM.render(
|
||||
React.createElement(
|
||||
GraphiQL,
|
||||
{fetcher: graphQLFetcher}
|
||||
{fetcher: $fetcherName}
|
||||
),
|
||||
document.getElementById('app')
|
||||
);
|
||||
|
|
|
@ -18,15 +18,24 @@ import 'package:web_socket_channel/io.dart';
|
|||
///
|
||||
/// See:
|
||||
/// * https://github.com/apollographql/subscriptions-transport-ws
|
||||
RequestHandler graphQLWS(GraphQL graphQL) {
|
||||
RequestHandler graphQLWS(GraphQL graphQL, {Duration keepAliveInterval}) {
|
||||
return (req, res) async {
|
||||
if (req is HttpRequestContext) {
|
||||
if (WebSocketTransformer.isUpgradeRequest(req.rawRequest)) {
|
||||
await res.detach();
|
||||
var socket = await WebSocketTransformer.upgrade(req.rawRequest);
|
||||
var socket = await WebSocketTransformer.upgrade(req.rawRequest,
|
||||
protocolSelector: (protocols) {
|
||||
if (protocols.contains('graphql-subscriptions'))
|
||||
return 'graphql-subscriptions';
|
||||
else
|
||||
throw AngelHttpException.badRequest(
|
||||
message:
|
||||
'Only the "graphql-subscriptions" protocol is allowed.');
|
||||
});
|
||||
var channel = IOWebSocketChannel(socket);
|
||||
var client = stw.RemoteClient(channel.cast<String>());
|
||||
var server = _GraphQLWSServer(client, graphQL, req, res);
|
||||
var server =
|
||||
_GraphQLWSServer(client, graphQL, req, res, keepAliveInterval);
|
||||
await server.done;
|
||||
} else {
|
||||
throw AngelHttpException.badRequest(
|
||||
|
@ -44,8 +53,9 @@ class _GraphQLWSServer extends stw.Server {
|
|||
final RequestContext req;
|
||||
final ResponseContext res;
|
||||
|
||||
_GraphQLWSServer(stw.RemoteClient client, this.graphQL, this.req, this.res)
|
||||
: super(client);
|
||||
_GraphQLWSServer(stw.RemoteClient client, this.graphQL, this.req, this.res,
|
||||
Duration keepAliveInterval)
|
||||
: super(client, keepAliveInterval: keepAliveInterval);
|
||||
|
||||
@override
|
||||
bool onConnect(stw.RemoteClient client, [Map connectionParams]) => true;
|
||||
|
|
|
@ -8,7 +8,10 @@ class RemoteClient extends StreamChannelMixin<OperationMessage> {
|
|||
StreamChannelController();
|
||||
|
||||
RemoteClient.withoutJson(this.channel) {
|
||||
_ctrl.local.stream.map((m) => m.toJson()).cast<Map>().pipe(channel.sink);
|
||||
_ctrl.local.stream
|
||||
.map((m) => m.toJson())
|
||||
.cast<Map>()
|
||||
.forEach(channel.sink.add);
|
||||
channel.stream.listen((m) {
|
||||
_ctrl.local.sink.add(OperationMessage.fromJson(m));
|
||||
});
|
||||
|
|
|
@ -4,80 +4,109 @@ import 'transport.dart';
|
|||
|
||||
abstract class Server {
|
||||
final RemoteClient client;
|
||||
final Duration keepAliveInterval;
|
||||
final Completer _done = Completer();
|
||||
StreamSubscription<OperationMessage> _sub;
|
||||
bool _init = false;
|
||||
|
||||
Future get done => _done.future;
|
||||
|
||||
Server(this.client) {
|
||||
_sub = client.stream.listen((msg) async {
|
||||
if (msg.type == OperationMessage.gqlConnectionInit && !_init) {
|
||||
try {
|
||||
Map connectionParams = null;
|
||||
if (msg.payload is Map)
|
||||
connectionParams = msg.payload as Map;
|
||||
else if (msg.payload != null)
|
||||
throw FormatException(
|
||||
'${msg.type} payload must be a map (object).');
|
||||
Server(this.client, {this.keepAliveInterval}) {
|
||||
_sub = client.stream.listen(
|
||||
(msg) async {
|
||||
if ((msg.type == OperationMessage.gqlConnectionInit) && !_init) {
|
||||
try {
|
||||
Map connectionParams = null;
|
||||
if (msg.payload is Map)
|
||||
connectionParams = msg.payload as Map;
|
||||
else if (msg.payload != null)
|
||||
throw FormatException(
|
||||
'${msg.type} payload must be a map (object).');
|
||||
|
||||
var connect = await onConnect(client, connectionParams);
|
||||
if (!connect) throw false;
|
||||
_init = true;
|
||||
client.sink.add(OperationMessage(OperationMessage.gqlConnectionAck));
|
||||
} catch (e) {
|
||||
if (e == false)
|
||||
_reportError('The connection was rejected.');
|
||||
else
|
||||
_reportError(e.toString());
|
||||
}
|
||||
} else if (_init) {
|
||||
if (msg.type == OperationMessage.gqlStart) {
|
||||
if (msg.id == null)
|
||||
throw FormatException('${msg.type} id is required.');
|
||||
if (msg.payload == null)
|
||||
throw FormatException('${msg.type} payload is required.');
|
||||
else if (msg.payload is! Map)
|
||||
throw FormatException(
|
||||
'${msg.type} payload must be a map (object).');
|
||||
var payload = msg.payload as Map;
|
||||
var query = payload['query'];
|
||||
var variables = payload['variables'];
|
||||
var operationName = payload['operationName'];
|
||||
if (query == null || query is! String)
|
||||
throw FormatException(
|
||||
'${msg.type} payload must contain a string named "query".');
|
||||
if (variables != null && variables is! Map)
|
||||
throw FormatException(
|
||||
'${msg.type} payload\'s "variables" field must be a map (object).');
|
||||
if (operationName != null && operationName is! String)
|
||||
throw FormatException(
|
||||
'${msg.type} payload\'s "operationName" field must be a string.');
|
||||
var result = await onOperation(
|
||||
msg.id,
|
||||
query as String,
|
||||
(variables as Map).cast<String, dynamic>(),
|
||||
operationName as String);
|
||||
var data = result.data;
|
||||
var connect = await onConnect(client, connectionParams);
|
||||
if (!connect) throw false;
|
||||
_init = true;
|
||||
client.sink
|
||||
.add(OperationMessage(OperationMessage.gqlConnectionAck));
|
||||
|
||||
if (data is Stream) {
|
||||
await for (var event in data) {
|
||||
client.sink.add(OperationMessage(OperationMessage.gqlData,
|
||||
id: msg.id,
|
||||
payload: {'data': event, 'errors': result.errors}));
|
||||
// if (keepAliveInterval != null) {
|
||||
// client.sink.add(
|
||||
// OperationMessage(OperationMessage.gqlConnectionKeepAlive));
|
||||
// }
|
||||
} catch (e) {
|
||||
if (e == false)
|
||||
_reportError('The connection was rejected.');
|
||||
else
|
||||
_reportError(e.toString());
|
||||
}
|
||||
} else {
|
||||
client.sink.add(OperationMessage(OperationMessage.gqlData,
|
||||
id: msg.id, payload: {'data': data, 'errors': result.errors}));
|
||||
}
|
||||
} else if (_init) {
|
||||
if (msg.type == OperationMessage.gqlStart) {
|
||||
if (msg.id == null)
|
||||
throw FormatException('${msg.type} id is required.');
|
||||
if (msg.payload == null)
|
||||
throw FormatException('${msg.type} payload is required.');
|
||||
else if (msg.payload is! Map)
|
||||
throw FormatException(
|
||||
'${msg.type} payload must be a map (object).');
|
||||
var payload = msg.payload as Map;
|
||||
var query = payload['query'];
|
||||
var variables = payload['variables'];
|
||||
var operationName = payload['operationName'];
|
||||
if (query == null || query is! String)
|
||||
throw FormatException(
|
||||
'${msg.type} payload must contain a string named "query".');
|
||||
if (variables != null && variables is! Map)
|
||||
throw FormatException(
|
||||
'${msg.type} payload\'s "variables" field must be a map (object).');
|
||||
if (operationName != null && operationName is! String)
|
||||
throw FormatException(
|
||||
'${msg.type} payload\'s "operationName" field must be a string.');
|
||||
var result = await onOperation(
|
||||
msg.id,
|
||||
query as String,
|
||||
(variables as Map)?.cast<String, dynamic>(),
|
||||
operationName as String);
|
||||
// var c = Completer();
|
||||
// if (keepAliveInterval != null) {
|
||||
// Timer.periodic(keepAliveInterval, (timer) {
|
||||
// if (c.isCompleted) {
|
||||
// timer.cancel();
|
||||
// } else {
|
||||
// client.sink.add(OperationMessage(
|
||||
// OperationMessage.gqlConnectionKeepAlive,
|
||||
// id: msg.id));
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
|
||||
client.sink
|
||||
.add(OperationMessage(OperationMessage.gqlComplete, id: msg.id));
|
||||
} else if (msg.type == OperationMessage.gqlConnectionTerminate) {
|
||||
await _sub?.cancel();
|
||||
}
|
||||
}
|
||||
}, onError: _done.completeError, onDone: _done.complete);
|
||||
var data = result.data;
|
||||
|
||||
if (data is Stream) {
|
||||
await for (var event in data) {
|
||||
client.sink.add(OperationMessage(OperationMessage.gqlData,
|
||||
id: msg.id,
|
||||
payload: {'data': event, 'errors': result.errors}));
|
||||
}
|
||||
} else {
|
||||
client.sink.add(OperationMessage(OperationMessage.gqlData,
|
||||
id: msg.id,
|
||||
payload: {'data': data, 'errors': result.errors}));
|
||||
}
|
||||
|
||||
// c.complete();
|
||||
client.sink.add(
|
||||
OperationMessage(OperationMessage.gqlComplete, id: msg.id));
|
||||
}
|
||||
// TODO: https://github.com/apollographql/subscriptions-transport-ws/issues/551
|
||||
// else if (msg.type == OperationMessage.gqlConnectionTerminate) {
|
||||
// await _sub?.cancel();
|
||||
// }
|
||||
}
|
||||
},
|
||||
onError: _done.completeError,
|
||||
onDone: () {
|
||||
_done.complete();
|
||||
});
|
||||
}
|
||||
|
||||
void _reportError(String message) {
|
||||
|
|
|
@ -4,16 +4,28 @@ import 'package:stream_channel/stream_channel.dart';
|
|||
|
||||
/// A basic message in the Apollo WebSocket protocol.
|
||||
class OperationMessage {
|
||||
static const String gqlConnectionInit = 'GQL_CONNECTION_INIT',
|
||||
gqlConnectionAck = 'GQL_CONNECTION_ACK',
|
||||
gqlConnectionKeepAlive = 'GQL_CONNECTION_KEEP_ALIVE',
|
||||
gqlConnectionError = 'GQL_CONNECTION_ERROR',
|
||||
gqlStart = 'GQL_START',
|
||||
gqlStop = 'GQL_STOP',
|
||||
gqlConnectionTerminate = 'GQL_CONNECTION_TERMINATE',
|
||||
gqlData = 'GQL_DATA',
|
||||
gqlError = 'GQL_ERROR',
|
||||
gqlComplete = 'GQL_COMPLETE';
|
||||
static const String gqlConnectionInit = 'init',
|
||||
gqlConnectionAck = 'init_success',
|
||||
gqlConnectionKeepAlive = 'keepalive',
|
||||
gqlConnectionError = 'init_fail',
|
||||
gqlStart = 'subscription_start',
|
||||
gqlStop = 'subscription_end',
|
||||
// TODO: Does this have a replacement?
|
||||
// https://github.com/apollographql/subscriptions-transport-ws/issues/551
|
||||
// gqlConnectionTerminate = 'subscription_end',
|
||||
gqlData = 'subscription_data',
|
||||
gqlError = 'subscription_fail',
|
||||
gqlComplete = 'subscription_success';
|
||||
// static const String gqlConnectionInit = 'GQL_CONNECTION_INIT',
|
||||
// gqlConnectionAck = 'GQL_CONNECTION_ACK',
|
||||
// gqlConnectionKeepAlive = 'GQL_CONNECTION_KEEP_ALIVE',
|
||||
// gqlConnectionError = 'GQL_CONNECTION_ERROR',
|
||||
// gqlStart = 'GQL_START',
|
||||
// gqlStop = 'GQL_STOP',
|
||||
// gqlConnectionTerminate = 'GQL_CONNECTION_TERMINATE',
|
||||
// gqlData = 'GQL_DATA',
|
||||
// gqlError = 'GQL_ERROR',
|
||||
// gqlComplete = 'GQL_COMPLETE';
|
||||
final dynamic payload;
|
||||
final String id;
|
||||
final String type;
|
||||
|
@ -29,8 +41,16 @@ class OperationMessage {
|
|||
throw ArgumentError.notNull('type');
|
||||
else if (type is! String)
|
||||
throw ArgumentError.value(type, 'type', 'must be a string');
|
||||
else if (id is num)
|
||||
id = id.toString();
|
||||
else if (id != null && id is! String)
|
||||
throw ArgumentError.value(type, 'id', 'must be a string');
|
||||
throw ArgumentError.value(id, 'id', 'must be a string or number');
|
||||
|
||||
// TODO: This is technically a violation of the spec.
|
||||
// https://github.com/apollographql/subscriptions-transport-ws/issues/551
|
||||
if (map.containsKey('query') ||
|
||||
map.containsKey('operationName') ||
|
||||
map.containsKey('variables')) payload = Map.from(map);
|
||||
return OperationMessage(type as String, id: id as String, payload: payload);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue