diff --git a/package.json b/package.json index 000ba3c9..c9d40884 100644 --- a/package.json +++ b/package.json @@ -28,17 +28,18 @@ "dependencies": { "apollo-link": "1.0.7", "apollo-link-http": "1.3.2", + "apollo-link-ws": "^1.0.4", "cross-fetch": "^1.1.1", "graphql": "0.12.3", - "graphql-binding": "0.2.1", + "graphql-binding": "0.3.0", "graphql-import": "0.1.9", - "graphql-schema-cache": "0.3.4", - "graphql-tools": "2.15.0", - "jsonwebtoken": "^8.1.0" + "graphql-tools": "2.16.0", + "jsonwebtoken": "^8.1.0", + "subscriptions-transport-ws": "^0.9.4" }, "devDependencies": { - "@types/jsonwebtoken": "7.2.5", "@types/graphql": "0.11.7", + "@types/jsonwebtoken": "7.2.5", "@types/node": "8.5.2", "tslint": "5.8.0", "tslint-config-standard": "7.0.0", diff --git a/src/Graphcool.ts b/src/Graphcool.ts index 23ac2358..f1477053 100644 --- a/src/Graphcool.ts +++ b/src/Graphcool.ts @@ -2,19 +2,28 @@ import { Binding } from 'graphql-binding' import { Exists, GraphcoolOptions } from './types' import { sign } from 'jsonwebtoken' import { makeGraphcoolLink } from './link' -import { SchemaCache } from 'graphql-schema-cache' import { GraphQLResolveInfo, isListType, isWrappingType } from 'graphql' import { buildExistsInfo } from './info' import { importSchema } from 'graphql-import' -import { GraphQLNamedType } from 'graphql'; +import { GraphQLNamedType, GraphQLSchema } from 'graphql' +import { SharedLink } from './SharedLink' +import { makeRemoteExecutableSchema } from 'graphql-tools' -const schemaCache = new SchemaCache() const typeDefsCache: { [schemaPath: string]: string } = {} +const sharedLink = new SharedLink() +let remoteSchema: GraphQLSchema | undefined + export class Graphcool extends Binding { exists: Exists - constructor({ typeDefs, endpoint, secret, fragmentReplacements, debug }: GraphcoolOptions) { + constructor({ + typeDefs, + endpoint, + secret, + fragmentReplacements, + debug, + }: GraphcoolOptions) { if (!typeDefs) { throw new Error('No `typeDefs` provided when calling `new Graphcool()`') } @@ -28,7 +37,7 @@ export class Graphcool extends Binding { endpoint = process.env.GRAPHCOOL_ENDPOINT } else { throw new Error( - `No Graphcool endpoint found. Either provide \`endpoint\` constructor option or set \`GRAPHCOOL_ENDPOINT\` env var.` + `No Graphcool endpoint found. Either provide \`endpoint\` constructor option or set \`GRAPHCOOL_ENDPOINT\` env var.`, ) } } @@ -42,7 +51,7 @@ export class Graphcool extends Binding { secret = process.env.GRAPHCOOL_SECRET } else { throw new Error( - `No Graphcool secret found. Either provide \`secret\` constructor option or set \`GRAPHCOOL_SECRET\` env var.` + `No Graphcool secret found. Either provide \`secret\` constructor option or set \`GRAPHCOOL_SECRET\` env var.`, ) } } @@ -54,13 +63,18 @@ export class Graphcool extends Binding { const token = sign({}, secret!) const link = makeGraphcoolLink({ endpoint: endpoint!, token, debug }) - const remoteSchema = schemaCache.makeExecutableSchema({ - link, - typeDefs, - key: endpoint! - }) + if (!remoteSchema) { + remoteSchema = makeRemoteExecutableSchema({ + link: sharedLink, + schema: typeDefs, + }) + } - super({ schema: remoteSchema, fragmentReplacements }) + const before = () => { + sharedLink.setInnerLink(link) + } + + super({ schema: remoteSchema, fragmentReplacements, before }) this.exists = new Proxy({}, new ExistsHandler()) } @@ -74,9 +88,11 @@ export class Graphcool extends Binding { context: { [key: string]: any }, - info?: GraphQLResolveInfo | string + info?: GraphQLResolveInfo | string, ): Promise { - return super.delegate(operation, fieldName, args, context, info).then(res => res.length > 0) + return super + .delegate(operation, fieldName, args, context, info) + .then(res => res.length > 0) } } @@ -86,19 +102,13 @@ class ExistsHandler implements ProxyHandler { const rootFieldName: string = this.findRootFieldName(target, typeName) const args = { where } const info = buildExistsInfo(rootFieldName, target.schema) - return target.existsDelegate( - 'query', - rootFieldName, - args, - {}, - info - ) + return target.existsDelegate('query', rootFieldName, args, {}, info) } } findRootFieldName(target: Graphcool, typeName: string): string { const fields = target.schema.getQueryType().getFields() - + // Loop over all query root fields for (const field in fields) { const fieldDef = fields[field] diff --git a/src/SharedLink.ts b/src/SharedLink.ts new file mode 100644 index 00000000..4cb42ca5 --- /dev/null +++ b/src/SharedLink.ts @@ -0,0 +1,26 @@ +import { + ApolloLink, + Operation, + NextLink, + Observable, + FetchResult, +} from 'apollo-link' + +export class SharedLink extends ApolloLink { + private innerLink?: ApolloLink + + setInnerLink(innerLink: ApolloLink) { + this.innerLink = innerLink + } + + request( + operation: Operation, + forward?: NextLink, + ): Observable | null { + if (!this.innerLink) { + throw new Error('No inner link set') + } + + return this.innerLink.request(operation, forward) + } +} diff --git a/src/link.ts b/src/link.ts index 200eca90..f4782ed1 100644 --- a/src/link.ts +++ b/src/link.ts @@ -1,7 +1,10 @@ import { createHttpLink } from 'apollo-link-http' import * as fetch from 'cross-fetch' -import { print } from 'graphql' -import { ApolloLink } from 'apollo-link' +import { print, OperationDefinitionNode } from 'graphql' +import { ApolloLink, Operation, split } from 'apollo-link' +import { WebSocketLink } from 'apollo-link-ws' +import { SubscriptionClient } from 'subscriptions-transport-ws' +import * as ws from 'ws' export function makeGraphcoolLink({ endpoint, @@ -18,6 +21,17 @@ export function makeGraphcoolLink({ fetch, }) + // also works for https/wss + const wsEndpoint = endpoint.replace(/^http/, 'ws') + const subscriptionClient = new SubscriptionClient( + wsEndpoint, + { reconnect: true }, + ws, + ) + const wsLink = new WebSocketLink(subscriptionClient) + + const backendLink = split(op => isSubscription(op), wsLink, httpLink) + const reportErrors = new ApolloLink((operation, forward) => { const observer = forward!(operation) observer.subscribe({ @@ -49,8 +63,31 @@ export function makeGraphcoolLink({ }) }) - return ApolloLink.from([debugLink, reportErrors, httpLink]) + return ApolloLink.from([debugLink, reportErrors, backendLink]) } else { - return ApolloLink.from([reportErrors, httpLink]) + return ApolloLink.from([reportErrors, backendLink]) + } +} + +function isSubscription(operation: Operation): boolean { + const selectedOperation = getSelectedOperation(operation) + if (selectedOperation) { + return selectedOperation.operation === 'subscription' } + return false +} + +function getSelectedOperation( + operation: Operation, +): OperationDefinitionNode | undefined { + if (operation.query.definitions.length === 1) { + return operation.query.definitions[0] as OperationDefinitionNode + } + + return operation.query.definitions.find( + d => + d.kind === 'OperationDefinition' && + !!d.name && + d.name.value === operation.operationName, + ) as OperationDefinitionNode }