Skip to content

Commit

Permalink
feature: Configure Redwood Realtime in GraphQL Yoga (redwoodjs#8397)
Browse files Browse the repository at this point in the history
* Implement useRedwoodLiveQuery

* Test useRedwoodLiveQuery

* Rename to Realtime, add pub sub

* Handle RedwoodRealtimeOptions and auto allow subs if ok

* Refactor for realtime yoga config

* Document realtime types

* Apply suggestions from code review

* Fix yarn.lock

* Changes PubSub type

* include the live directive for realtime support

* Adds realtime graphql schema test case for live query directive

* Ensure live query actually added to schema (just once)

* Remove stray console.debug

* yarn dedupe

* Update test snapshot

* Update packages/graphql-server/src/createGraphQLYoga.ts

Co-authored-by: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com>

* Improved the should update schema with live directive test

---------

Co-authored-by: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com>
  • Loading branch information
dthyresson and Josh-Walker-GM authored Jun 1, 2023
1 parent 46df16b commit 29edb41
Show file tree
Hide file tree
Showing 25 changed files with 556 additions and 47 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,17 +57,12 @@ async function serve() {
logger: logger,
options: { query: true, data: true, level: 'trace' },
},
graphiQLEndpoint: '/yoga',
graphiQLEndpoint: '/.redwood/functions/graphql',
sdls,
services,
directives,
allowIntrospection: true,
allowGraphiQL: true,
allowedOperations: [
OperationTypeNode.SUBSCRIPTION,
OperationTypeNode.QUERY,
OperationTypeNode.MUTATION,
],
})

// Start
Expand Down
4 changes: 4 additions & 0 deletions packages/graphql-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,15 @@
"@envelop/depth-limit": "3.0.0",
"@envelop/disable-introspection": "4.0.6",
"@envelop/filter-operation-type": "4.0.6",
"@envelop/live-query": "6.0.0",
"@envelop/on-resolve": "2.0.6",
"@escape.tech/graphql-armor": "1.8.2",
"@graphql-tools/merge": "8.4.2",
"@graphql-tools/schema": "9.0.19",
"@graphql-tools/utils": "9.2.1",
"@graphql-yoga/subscription": "3.1.0",
"@n1ru4l/graphql-live-query": "0.10.0",
"@n1ru4l/in-memory-live-query-store": "0.10.0",
"@opentelemetry/api": "1.4.1",
"@redwoodjs/api": "5.0.0",
"@redwoodjs/project-config": "5.0.0",
Expand Down
67 changes: 43 additions & 24 deletions packages/graphql-server/src/createGraphQLYoga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import {
useRedwoodOpenTelemetry,
useRedwoodLogger,
useRedwoodPopulateContext,
useRedwoodRealtime,
} from './plugins'
import type {
useRedwoodDirectiveReturn,
DirectivePluginOptions,
} from './plugins/useRedwoodDirective'
import { makeSubscriptions } from './subscriptions/makeSubscriptions'
import type { RedwoodSubscription } from './subscriptions/makeSubscriptions'
import type { GraphQLYogaOptions } from './types'

export const createGraphQLYoga = ({
Expand All @@ -37,14 +39,14 @@ export const createGraphQLYoga = ({
services,
sdls,
directives = [],
subscriptions = [],
armorConfig,
allowedOperations,
allowIntrospection,
allowGraphiQL,
defaultError = 'Something went wrong.',
graphiQLEndpoint = '/graphql',
schemaOptions,
realtime,
}: GraphQLYogaOptions) => {
let schema: GraphQLSchema
let redwoodDirectivePlugins = [] as Plugin[]
Expand All @@ -61,8 +63,14 @@ export const createGraphQLYoga = ({
)
}

// @NOTE: Subscriptions are optional and only work in the context of a server
const projectSubscriptions = makeSubscriptions(subscriptions)
// @NOTE: Subscriptions are optional and only work in the context of a server
let projectSubscriptions = [] as RedwoodSubscription[]

if (realtime?.subscriptions?.subscriptions) {
projectSubscriptions = makeSubscriptions(
realtime.subscriptions.subscriptions
)
}

schema = makeMergedSchema({
sdls,
Expand Down Expand Up @@ -112,21 +120,6 @@ export const createGraphQLYoga = ({
}
: false

logger.debug(
{
healthCheckId,
allowedOperations,
allowIntrospection,
defaultError,
disableIntrospection,
disableGraphQL,
allowGraphiQL,
graphiql,
graphiQLEndpoint,
},
'GraphiQL and Introspection Config'
)

if (disableIntrospection) {
plugins.push(useDisableIntrospection())
}
Expand All @@ -149,15 +142,26 @@ export const createGraphQLYoga = ({
plugins.push(useArmor(logger, armorConfig))

// Only allow execution of specific operation types
const defaultAllowedOperations = [
OperationTypeNode.QUERY,
OperationTypeNode.MUTATION,
]

// now allow subscriptions if using them (unless you override)
if (realtime?.subscriptions?.subscriptions) {
defaultAllowedOperations.push(OperationTypeNode.SUBSCRIPTION)
} else {
logger.info('Subscriptions are disabled.')
}

plugins.push(
useFilterAllowedOperations(
allowedOperations || [
OperationTypeNode.QUERY,
OperationTypeNode.MUTATION,
]
)
useFilterAllowedOperations(allowedOperations || defaultAllowedOperations)
)

if (realtime) {
plugins.push(useRedwoodRealtime(realtime))
}

// App-defined plugins
if (extraPlugins && extraPlugins.length > 0) {
plugins.push(...extraPlugins)
Expand Down Expand Up @@ -196,6 +200,21 @@ export const createGraphQLYoga = ({
// so can process any data added to results and extensions
plugins.push(useRedwoodLogger(loggerConfig))

logger.debug(
{
healthCheckId,
allowedOperations,
defaultAllowedOperations,
allowIntrospection,
defaultError,
disableIntrospection,
disableGraphQL,
allowGraphiQL,
graphiql,
graphiQLEndpoint,
},
'GraphiQL and Introspection Config'
)
const yoga = createYoga({
id: healthCheckId,
landingPage: isDevEnv,
Expand Down
8 changes: 8 additions & 0 deletions packages/graphql-server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,12 @@ export {
useRedwoodDirective,
} from './plugins/useRedwoodDirective'

export {
useRedwoodRealtime,
createPubSub,
InMemoryLiveQueryStore,
liveDirectiveTypeDefs,
} from './plugins/useRedwoodRealtime'

export type { PubSub } from './plugins/useRedwoodRealtime'
export * as rootSchema from './rootSchema'
58 changes: 58 additions & 0 deletions packages/graphql-server/src/plugins/__fixtures__/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,55 @@ export const testSchema = makeExecutableSchema({
},
})

export const testLiveSchema = makeExecutableSchema({
typeDefs: /* GraphQL */ `
"""
Instruction for establishing a live connection that is updated once the underlying data changes.
"""
directive @live(
"""
Whether the query should be live or not.
"""
if: Boolean = true
"""
Propose a desired throttle interval ot the server in order to receive updates to at most once per "throttle" milliseconds. The server must not accept this value.
"""
throttle: Int
) on QUERY
type Query {
me: User!
}
type Query {
forbiddenUser: User!
getUser(id: Int!): User!
}
type User {
id: ID!
name: String!
}
`,
resolvers: {
Query: {
me: () => {
return { _id: 1, firstName: 'Ba', lastName: 'Zinga' }
},
forbiddenUser: () => {
throw Error('You are forbidden')
},
getUser: (id) => {
return { id, firstName: 'Ba', lastName: 'Zinga' }
},
},
User: {
id: (u) => u._id,
name: (u) => `${u.firstName} ${u.lastName}`,
},
},
})
export const testQuery = /* GraphQL */ `
query meQuery {
me {
Expand Down Expand Up @@ -62,6 +111,15 @@ export const testErrorQuery = /* GraphQL */ `
}
`

export const testLiveQuery = /* GraphQL */ `
query meQuery @live {
me {
id
name
}
}
`

export const testParseErrorQuery = /* GraphQL */ `
query ParseErrorQuery {
me {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`useRedwoodRealtime should update schema with live directive 1`] = `"@live"`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {
createTestkit,
createSpiedPlugin,
assertStreamExecutionValue,
} from '@envelop/testing'

import { testLiveQuery, testSchema } from '../__fixtures__/common'
import {
useRedwoodRealtime,
InMemoryLiveQueryStore,
} from '../useRedwoodRealtime'

describe('useRedwoodRealtime', () => {
const liveQueryStore = new InMemoryLiveQueryStore()

it('should support a @live query directive', async () => {
const testkit = createTestkit(
[useRedwoodRealtime({ liveQueries: { liveQueryStore } })],
testSchema
)

const result = await testkit.execute(testLiveQuery, {}, {})

assertStreamExecutionValue(result)
const current = await result.next()
expect(current.value).toMatchInlineSnapshot(`
{
"data": {
"me": {
"id": "1",
"name": "Ba Zinga",
},
},
"isLive": true,
}
`)
})

it('should update schema with live directive', async () => {
const spiedPlugin = createSpiedPlugin()

// the original schema should not have the live directive before the useRedwoodRealtime plugin is applied
expect(testSchema.getDirective('live')).toBeUndefined()

createTestkit(
[
useRedwoodRealtime({ liveQueries: { liveQueryStore } }),
spiedPlugin.plugin,
],
testSchema
)

// the replaced schema should have the live directive afterwards
const replacedSchema =
spiedPlugin.spies.onSchemaChange.mock.calls[0][0].schema

const liveDirectiveOnSchema = replacedSchema.getDirective('live')

expect(liveDirectiveOnSchema.name).toEqual('live')
expect(replacedSchema.getDirective('live')).toMatchSnapshot()
})
})
1 change: 1 addition & 0 deletions packages/graphql-server/src/plugins/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export { useRedwoodAuthContext } from './useRedwoodAuthContext'
export { useRedwoodDirective } from './useRedwoodDirective'
export { useRedwoodError } from './useRedwoodError'
export { useRedwoodGlobalContextSetter } from './useRedwoodGlobalContextSetter'
export { useRedwoodRealtime } from './useRedwoodRealtime'
export { useRedwoodLogger } from './useRedwoodLogger'
export { useRedwoodPopulateContext } from './useRedwoodPopulateContext'
export { useRedwoodOpenTelemetry } from './useRedwoodOpenTelemetry'
9 changes: 5 additions & 4 deletions packages/graphql-server/src/plugins/useRedwoodDirective.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,22 +233,23 @@ export const useRedwoodDirective = (
/**
* This symbol is added to the schema extensions for checking whether the transform got already applied.
*/
const didMapSchemaSymbol = Symbol('useRedwoodDirective.didMapSchemaSymbol')
const wasDirectiveApplied = Symbol.for(`useRedwoodDirective.${options.name}}`)

return {
onSchemaChange({ schema, replaceSchema }) {
/**
* Currently graphql-js extensions typings are limited to string keys.
* We are using symbols as each useRedwoodDirective plugin instance should use its own unique symbol.
*/
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
if (schema.extensions?.[didMapSchemaSymbol] === true) {
if (schema.extensions?.[wasDirectiveApplied] === true) {
return
}
const transformedSchema = wrapAffectedResolvers(schema, options)
transformedSchema.extensions = {
...schema.extensions,
[didMapSchemaSymbol]: true,
[wasDirectiveApplied]: true,
}

replaceSchema(transformedSchema)
},
}
Expand Down
Loading

0 comments on commit 29edb41

Please sign in to comment.