Skip to content

Commit

Permalink
Support a factory function and promise as a schema input (#1497)
Browse files Browse the repository at this point in the history
* Support a factory function and promise as a schema input

* Less diff
  • Loading branch information
ardatan committed Aug 1, 2022
1 parent e021c6f commit 1d7f810
Show file tree
Hide file tree
Showing 5 changed files with 259 additions and 62 deletions.
25 changes: 25 additions & 0 deletions .changeset/healthy-waves-sing.md
@@ -0,0 +1,25 @@
---
'graphql-yoga': minor
---

Support a schema factory function that runs per request or a promise to be resolved before the first request.

```ts
createYoga({
schema(request: Request) {
return getSchemaForToken(request.headers.get('x-my-token'))
},
})
```

```ts
async function buildSchemaAsync() {
const typeDefs = await fs.promises.readFile('./schema.graphql', 'utf8')
const resolvers = await import('./resolvers.js')
return makeExecutableSchema({ typeDefs, resolvers })
}

createYoga({
schema: buildSchemaAsync(),
})
```
91 changes: 91 additions & 0 deletions packages/graphql-yoga/__tests__/schema-def.test.ts
@@ -0,0 +1,91 @@
import { makeExecutableSchema } from '@graphql-tools/schema'
import { GraphQLSchema } from 'graphql'
import { createYoga, YogaInitialContext } from 'graphql-yoga'

describe('useSchema', () => {
it('should accept a factory function', async () => {
let count = 0
const schemaFactory = async (request: Request) => {
const countFromContext = request.headers.get('count')
return makeExecutableSchema<YogaInitialContext>({
typeDefs: /* GraphQL */ `
type Query {
foo${countFromContext}: Boolean
}
`,
resolvers: {
Query: {
[`foo${countFromContext}`]: (_, __, { request }) =>
countFromContext === request.headers.get('count'),
},
},
})
}
const yoga = createYoga({
schema: schemaFactory,
})
while (true) {
if (count === 3) {
break
}
count++
const query = /* GraphQL */ `
query {
foo${count}
}
`
const result = await yoga.fetch('http://localhost:3000/graphql', {
method: 'POST',
body: JSON.stringify({ query }),
headers: {
count: count.toString(),
'Content-Type': 'application/json',
},
})
const { data } = await result.json()
expect(data).toEqual({
[`foo${count}`]: true,
})
}
expect.assertions(3)
})
it('should accept a promise', async () => {
const schemaPromise = new Promise<GraphQLSchema>((resolve) => {
setTimeout(() => {
resolve(
makeExecutableSchema({
typeDefs: /* GraphQL */ `
type Query {
foo: Boolean
}
`,
resolvers: {
Query: {
foo: () => true,
},
},
}),
)
}, 300)
})
const yoga = createYoga({
schema: schemaPromise,
})
const query = /* GraphQL */ `
query {
foo
}
`
const result = await yoga.fetch('http://localhost:3000/graphql', {
method: 'POST',
body: JSON.stringify({ query }),
headers: {
'Content-Type': 'application/json',
},
})
const { data } = await result.json()
expect(data).toEqual({
foo: true,
})
})
})
1 change: 1 addition & 0 deletions packages/graphql-yoga/src/index.ts
Expand Up @@ -11,3 +11,4 @@ export {
renderGraphiQL,
} from './plugins/useGraphiQL.js'
export { Plugin } from './plugins/types.js'
export { useSchema } from './plugins/useSchema.js'
135 changes: 135 additions & 0 deletions packages/graphql-yoga/src/plugins/useSchema.ts
@@ -0,0 +1,135 @@
import { makeExecutableSchema } from '@graphql-tools/schema'
import { IResolvers, TypeSource } from '@graphql-tools/utils'
import { GraphQLError, GraphQLSchema, isSchema } from 'graphql'
import { Plugin, PromiseOrValue, YogaInitialContext } from 'graphql-yoga'

// TODO: Will be removed later
type TypeDefsAndResolvers<TContext, TRootValue = {}> = {
typeDefs: TypeSource
resolvers?:
| IResolvers<TRootValue, TContext>
| Array<IResolvers<TRootValue, TContext>>
}

export type YogaSchemaDefinition<TContext, TRootValue> =
| TypeDefsAndResolvers<TContext, TRootValue>
| PromiseOrValue<GraphQLSchema>
| ((request: Request) => PromiseOrValue<GraphQLSchema>)

// Will be moved to a seperate export later
export function getDefaultSchema() {
return makeExecutableSchema({
typeDefs: /* GraphQL */ `
"""
Greetings from GraphQL Yoga!
"""
type Query {
greetings: String
}
type Subscription {
"""
Current Time
"""
time: String
}
`,
resolvers: {
Query: {
greetings: () =>
'This is the `greetings` field of the root `Query` type',
},
Subscription: {
time: {
async *subscribe() {
while (true) {
yield { time: new Date().toISOString() }
await new Promise((resolve) => setTimeout(resolve, 1000))
}
},
},
},
},
})
}

export const useSchema = <
TContext extends YogaInitialContext = YogaInitialContext,
TRootValue = {},
>(
schemaDef?: YogaSchemaDefinition<TContext, TRootValue>,
): Plugin<TContext> => {
if (schemaDef == null) {
const schema = getDefaultSchema()
return {
onPluginInit({ setSchema }) {
setSchema(schema)
},
}
}
if ('typeDefs' in schemaDef) {
const schema = makeExecutableSchema(schemaDef)
return {
onPluginInit({ setSchema }) {
setSchema(schema)
},
}
}
if (isSchema(schemaDef)) {
return {
onPluginInit({ setSchema }) {
setSchema(schemaDef)
},
}
}
if ('then' in schemaDef) {
let schema: GraphQLSchema | undefined
return {
async onRequest() {
if (!schema) {
schema = await schemaDef
}
},
onEnveloped({ setSchema }) {
if (!schema) {
throw new GraphQLError(
`You provide a promise of a schema but it hasn't been resolved yet. Make sure you use this plugin with GraphQL Yoga.`,
{
extensions: {
http: {
status: 500,
},
},
},
)
}
setSchema(schema)
},
}
}
const schemaByRequest = new WeakMap<Request, GraphQLSchema>()
return {
async onRequest({ request }) {
const schema = await schemaDef(request)
schemaByRequest.set(request, schema)
},
onEnveloped({ setSchema, context }) {
if (context?.request) {
const schema = schemaByRequest.get(context.request)
if (schema) {
setSchema(schema)
}
} else {
throw new GraphQLError(
'Request object is not available in the context. Make sure you use this plugin with GraphQL Yoga.',
{
extensions: {
http: {
status: 500,
},
},
},
)
}
},
}
}
69 changes: 7 additions & 62 deletions packages/graphql-yoga/src/server.ts
@@ -1,12 +1,11 @@
import { GraphQLSchema, isSchema, print } from 'graphql'
import { print } from 'graphql'
import {
GetEnvelopedFn,
envelop,
useMaskedErrors,
UseMaskedErrorsOpts,
useExtendContext,
useLogger,
useSchema,
PromiseOrValue,
} from '@envelop/core'
import { useValidationCache, ValidationCache } from '@envelop/validation-cache'
Expand Down Expand Up @@ -83,6 +82,7 @@ import { useHTTPValidationError } from './plugins/requestValidation/useHTTPValid
import { usePreventMutationViaGET } from './plugins/requestValidation/usePreventMutationViaGET.js'
import { useUnhandledRoute } from './plugins/useUnhandledRoute.js'
import { yogaDefaultFormatError } from './utils/yogaDefaultFormatError.js'
import { useSchema, YogaSchemaDefinition } from './plugins/useSchema.js'

interface OptionsWithPlugins<TContext> {
/**
Expand Down Expand Up @@ -157,22 +157,10 @@ export type YogaServerOptions<

renderGraphiQL?: (options?: GraphiQLOptions) => PromiseOrValue<BodyInit>

schema?:
| GraphQLSchema
| {
typeDefs: TypeSource
resolvers?:
| IResolvers<
TRootValue,
TUserContext & TServerContext & YogaInitialContext
>
| Array<
IResolvers<
TRootValue,
TUserContext & TServerContext & YogaInitialContext
>
>
}
schema?: YogaSchemaDefinition<
TUserContext & TServerContext & YogaInitialContext,
TRootValue
>

parserCache?: boolean | ParserCacheOptions
validationCache?: boolean | ValidationCache
Expand All @@ -183,41 +171,6 @@ export type YogaServerOptions<
OptionsWithPlugins<TUserContext & TServerContext & YogaInitialContext>
>

export function getDefaultSchema() {
return makeExecutableSchema({
typeDefs: /* GraphQL */ `
"""
Greetings from GraphQL Yoga!
"""
type Query {
greetings: String
}
type Subscription {
"""
Current Time
"""
time: String
}
`,
resolvers: {
Query: {
greetings: () =>
'This is the `greetings` field of the root `Query` type',
},
Subscription: {
time: {
async *subscribe() {
while (true) {
yield { time: new Date().toISOString() }
await new Promise((resolve) => setTimeout(resolve, 1000))
}
},
},
},
},
})
}

/**
* Base class that can be extended to create a GraphQL server with any HTTP server framework.
* @internal
Expand Down Expand Up @@ -255,14 +208,6 @@ export class YogaServer<
createFetch({
useNodeFetch: true,
})
const schema = options?.schema
? isSchema(options.schema)
? options.schema
: makeExecutableSchema({
typeDefs: options.schema.typeDefs,
resolvers: options.schema.resolvers,
})
: getDefaultSchema()

const logger = options?.logging != null ? options.logging : true
this.logger =
Expand Down Expand Up @@ -293,7 +238,7 @@ export class YogaServer<

this.plugins = [
// Use the schema provided by the user
schema != null && useSchema(schema),
useSchema(options?.schema),
// Performance things
options?.parserCache !== false &&
useParserCache(
Expand Down

0 comments on commit 1d7f810

Please sign in to comment.