Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support a factory function and promise as a schema input #1497

Merged
merged 2 commits into from
Aug 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/healthy-waves-sing.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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