Skip to content

Commit

Permalink
feat: added subscriptions support
Browse files Browse the repository at this point in the history
  • Loading branch information
schickling committed Jan 3, 2018
1 parent 383a09f commit c2fe6cc
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 33 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
},
"dependencies": {
"@types/graphql": "0.11.7",
"graphql": "^0.12.3",
"graphql-tools": "^2.14.1"
"graphql": "0.12.3",
"graphql-tools": "2.15.0",
"iterall": "^1.1.3"
}
}
36 changes: 26 additions & 10 deletions src/Binding.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,67 @@
import { buildInfo } from './info'
import { GraphQLResolveInfo, graphql, GraphQLSchema } from 'graphql'
import { delegateToSchema } from 'graphql-tools'
import { makeProxy } from './proxy'
import { QueryMap, BindingOptions, FragmentReplacements } from './types'
import { makeProxy, makeSubscriptionProxy } from './proxy'
import {
QueryMap,
BindingOptions,
FragmentReplacements,
SubscriptionMap,
Operation,
} from './types'

export class Binding {
query: QueryMap
mutation: QueryMap
subscription: SubscriptionMap
schema: GraphQLSchema
before: () => void

private fragmentReplacements: FragmentReplacements

constructor({ schema, fragmentReplacements }: BindingOptions) {
constructor({ schema, fragmentReplacements, before }: BindingOptions) {
this.fragmentReplacements = fragmentReplacements || {}
this.schema = schema
this.before = before || (() => undefined)

this.query = makeProxy<QueryMap>({
schema: this.schema,
fragmentReplacements: this.fragmentReplacements,
operation: 'query'
operation: 'query',
before: this.before,
})
this.mutation = makeProxy<QueryMap>({
schema: this.schema,
fragmentReplacements: this.fragmentReplacements,
operation: 'mutation'
operation: 'mutation',
before: this.before,
})
this.subscription = makeSubscriptionProxy<SubscriptionMap>({
schema: this.schema,
fragmentReplacements: this.fragmentReplacements,
before: this.before,
})
}

async request<T = any>(
query: string,
variables?: { [key: string]: any }
variables?: { [key: string]: any },
): Promise<T> {
return graphql(this.schema, query, null, null, variables).then(
r => r.data![Object.keys(r.data!)[0]]
r => r.data![Object.keys(r.data!)[0]],
)
}

async delegate(
operation: 'query' | 'mutation',
operation: Operation,
fieldName: string,
args: {
[key: string]: any
},
context: {
[key: string]: any
},
info?: GraphQLResolveInfo | string
info?: GraphQLResolveInfo | string,
) {
info = buildInfo(fieldName, operation, this.schema, info)

Expand All @@ -56,7 +72,7 @@ export class Binding {
fieldName,
args,
context,
info
info,
)
}
}
61 changes: 58 additions & 3 deletions src/handler.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { GraphQLResolveInfo, ExecutionResult, GraphQLSchema } from 'graphql'
import { delegateToSchema } from 'graphql-tools'
import { $$asyncIterator } from 'iterall'
import { FragmentReplacements } from './types'
import { buildInfo } from './info'

export class Handler<T extends object> implements ProxyHandler<T> {
constructor(
private schema: GraphQLSchema,
private fragmentReplacements: FragmentReplacements,
private operation: 'query' | 'mutation'
private operation: 'query' | 'mutation',
private before: () => void,
) {}

get(target: T, rootFieldName: string) {
return (
args?: { [key: string]: any },
info?: GraphQLResolveInfo | string
info?: GraphQLResolveInfo | string,
): Promise<ExecutionResult> => {
this.before()

const operation = this.operation
info = buildInfo(rootFieldName, operation, this.schema, info)

Expand All @@ -25,8 +29,59 @@ export class Handler<T extends object> implements ProxyHandler<T> {
rootFieldName,
args || {},
{},
info
info,
)
}
}
}

export class SubscriptionHandler<T extends object> implements ProxyHandler<T> {
constructor(
private schema: GraphQLSchema,
private fragmentReplacements: FragmentReplacements,
private before: () => void,
) {}

get(target: T, rootFieldName: string) {
return async (
args?: { [key: string]: any },
infoOrQuery?: GraphQLResolveInfo | string,
): Promise<AsyncIterator<any>> => {
this.before()

const info = buildInfo(
rootFieldName,
'subscription',
this.schema,
infoOrQuery,
)

const iterator = await delegateToSchema(
this.schema,
this.fragmentReplacements,
'subscription',
rootFieldName,
args || {},
{},
info,
)

return {
async next() {
const { value } = await iterator.next()
const data = { [info.fieldName]: value.data[rootFieldName] }
return { value: data, done: false }
},
return() {
return Promise.resolve({ value: undefined, done: true })
},
throw(error) {
return Promise.reject(error)
},
[$$asyncIterator]() {
return this
},
}
}
}
}
20 changes: 12 additions & 8 deletions src/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ import {
parse,
validate,
} from 'graphql'
import { Operation } from './types'
import { isScalar, getTypeForRootFieldName } from './utils'

export function buildInfo(
rootFieldName: string,
operation: 'query' | 'mutation',
operation: Operation,
schema: GraphQLSchema,
info?: GraphQLResolveInfo | string
info?: GraphQLResolveInfo | string,
): GraphQLResolveInfo {
if (!info) {
info = buildInfoForAllScalars(rootFieldName, schema, operation)
Expand All @@ -26,7 +27,7 @@ export function buildInfo(
export function buildInfoForAllScalars(
rootFieldName: string,
schema: GraphQLSchema,
operation: 'query' | 'mutation',
operation: Operation,
): GraphQLResolveInfo {
const fieldNodes: FieldNode[] = []
const type = getTypeForRootFieldName(rootFieldName, operation, schema)
Expand All @@ -51,8 +52,11 @@ export function buildInfoForAllScalars(
fieldNodes.push(fieldNode)
}

const parentType =
operation === 'query' ? schema.getQueryType() : schema.getMutationType()!
const parentType = {
query: () => schema.getQueryType(),
mutation: () => schema.getMutationType()!,
subscription: () => schema.getSubscriptionType()!,
}[operation]()

return {
fieldNodes,
Expand All @@ -62,7 +66,7 @@ export function buildInfoForAllScalars(
returnType: type,
parentType,
path: undefined,
rootValue: null,
rootValue: undefined,
operation: {
kind: 'OperationDefinition',
operation,
Expand All @@ -75,7 +79,7 @@ export function buildInfoForAllScalars(
export function buildInfoFromFragment(
rootFieldName: string,
schema: GraphQLSchema,
operation: 'query' | 'mutation',
operation: Operation,
query: string,
): GraphQLResolveInfo {
const type = getTypeForRootFieldName(
Expand All @@ -97,7 +101,7 @@ export function buildInfoFromFragment(
returnType: type,
parentType: schema.getQueryType(),
path: undefined,
rootValue: null,
rootValue: undefined,
operation: {
kind: 'OperationDefinition',
operation,
Expand Down
23 changes: 20 additions & 3 deletions src/proxy.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,35 @@
import { GraphQLSchema } from 'graphql'
import { FragmentReplacements } from './types'
import { Handler } from './handler'
import { Handler, SubscriptionHandler } from './handler'

export function makeProxy<T extends object = any>({
schema,
fragmentReplacements,
operation
operation,
before,
}: {
schema: GraphQLSchema
fragmentReplacements: FragmentReplacements
operation: 'query' | 'mutation'
before: () => void
}): T {
return new Proxy(
{} as T,
new Handler<T>(schema, fragmentReplacements, operation)
new Handler<T>(schema, fragmentReplacements, operation, before),
)
}

export function makeSubscriptionProxy<T extends object = any>({
schema,
fragmentReplacements,
before,
}: {
schema: GraphQLSchema
fragmentReplacements: FragmentReplacements
before: () => void
}): T {
return new Proxy(
{} as T,
new SubscriptionHandler<T>(schema, fragmentReplacements, before),
)
}
12 changes: 11 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { GraphQLResolveInfo, GraphQLSchema, InlineFragmentNode } from 'graphql'

export type Operation = 'query' | 'mutation' | 'subscription'

export interface FragmentReplacements {
[typeName: string]: {
[fieldName: string]: InlineFragmentNode
Expand All @@ -9,11 +11,19 @@ export interface FragmentReplacements {
export interface QueryMap {
[rootField: string]: (
args?: any,
info?: GraphQLResolveInfo | string
info?: GraphQLResolveInfo | string,
) => Promise<any>
}

export interface SubscriptionMap {
[rootField: string]: (
args?: any,
info?: GraphQLResolveInfo | string,
) => AsyncIterator<any> | Promise<AsyncIterator<any>>
}

export interface BindingOptions {
fragmentReplacements?: FragmentReplacements
schema: GraphQLSchema
before?: () => void
}
18 changes: 12 additions & 6 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
GraphQLSchema,
getNamedType,
} from 'graphql'
import { Operation } from './types'

export function isScalar(t: GraphQLOutputType): boolean {
if (t instanceof GraphQLScalarType || t instanceof GraphQLEnumType) {
Expand Down Expand Up @@ -40,19 +41,24 @@ export function isScalar(t: GraphQLOutputType): boolean {

export function getTypeForRootFieldName(
rootFieldName: string,
operation: 'query' | 'mutation',
operation: Operation,
schema: GraphQLSchema,
): GraphQLOutputType {
if (operation === 'mutation' && !schema.getMutationType()) {
throw new Error(`Schema doesn't have mutation type`)
}

const rootFields =
operation === 'query'
? schema.getQueryType().getFields()
: schema.getMutationType()!.getFields()
if (operation === 'subscription' && !schema.getSubscriptionType()) {
throw new Error(`Schema doesn't have subscription type`)
}

const rootType = {
query: () => schema.getQueryType(),
mutation: () => schema.getMutationType()!,
subscription: () => schema.getSubscriptionType()!,
}[operation]()

const rootField = rootFields[rootFieldName]
const rootField = rootType.getFields()[rootFieldName]

if (!rootField) {
throw new Error(`No such root field found: ${rootFieldName}`)
Expand Down

0 comments on commit c2fe6cc

Please sign in to comment.