Skip to content

Commit

Permalink
Merge pull request #37 from graphcool/subscriptions
Browse files Browse the repository at this point in the history
feat: added subscriptions support
  • Loading branch information
schickling authored Jan 3, 2018
2 parents c2d6871 + ab0f497 commit ea7460c
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 31 deletions.
11 changes: 6 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
54 changes: 32 additions & 22 deletions src/Graphcool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()`')
}
Expand All @@ -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.`,
)
}
}
Expand All @@ -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.`,
)
}
}
Expand All @@ -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())
}
Expand All @@ -74,9 +88,11 @@ export class Graphcool extends Binding {
context: {
[key: string]: any
},
info?: GraphQLResolveInfo | string
info?: GraphQLResolveInfo | string,
): Promise<boolean> {
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)
}
}

Expand All @@ -86,19 +102,13 @@ class ExistsHandler implements ProxyHandler<Graphcool> {
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]
Expand Down
26 changes: 26 additions & 0 deletions src/SharedLink.ts
Original file line number Diff line number Diff line change
@@ -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<FetchResult> | null {
if (!this.innerLink) {
throw new Error('No inner link set')
}

return this.innerLink.request(operation, forward)
}
}
45 changes: 41 additions & 4 deletions src/link.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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({
Expand Down Expand Up @@ -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
}

0 comments on commit ea7460c

Please sign in to comment.