From 29edb414fca110d4738b2edd26d1ee9da6b14705 Mon Sep 17 00:00:00 2001 From: David Thyresson Date: Thu, 1 Jun 2023 13:05:01 -0400 Subject: [PATCH] feature: Configure Redwood Realtime in GraphQL Yoga (#8397) * 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> --- .../experimental/templates/server.ts.template | 7 +- packages/graphql-server/package.json | 4 + .../graphql-server/src/createGraphQLYoga.ts | 67 ++++++++++------ packages/graphql-server/src/index.ts | 8 ++ .../src/plugins/__fixtures__/common.ts | 58 ++++++++++++++ .../useRedwoodRealtime.test.ts.snap | 3 + .../__tests__/useRedwoodRealtime.test.ts | 62 +++++++++++++++ packages/graphql-server/src/plugins/index.ts | 1 + .../src/plugins/useRedwoodDirective.ts | 9 ++- .../src/plugins/useRedwoodRealtime.ts | 79 +++++++++++++++++++ packages/graphql-server/src/types.ts | 24 +++--- .../__snapshots__/graphqlSchema.test.ts.snap | 69 ++++++++++++++++ .../realtime/api/db/schema.prisma | 21 +++++ .../src/directives/requireAuth/requireAuth.js | 18 +++++ .../api/src/directives/skipAuth/skipAuth.js | 13 +++ .../api/src/graphql/currentUser.sdl.ts | 7 ++ .../realtime/api/src/graphql/todos.sdl.js | 18 +++++ .../graphqlCodeGen/realtime/api/src/lib/db.js | 6 ++ .../graphqlCodeGen/realtime/api/src/server.ts | 0 .../realtime/api/src/services/todos/todos.js | 21 +++++ .../api/src/services/todos/todos.test.js | 11 +++ .../graphqlCodeGen/realtime/redwood.toml | 10 +++ .../src/__tests__/graphqlSchema.test.ts | 21 +++++ .../internal/src/generate/graphqlSchema.ts | 9 ++- yarn.lock | 57 ++++++++++++- 25 files changed, 556 insertions(+), 47 deletions(-) create mode 100644 packages/graphql-server/src/plugins/__tests__/__snapshots__/useRedwoodRealtime.test.ts.snap create mode 100644 packages/graphql-server/src/plugins/__tests__/useRedwoodRealtime.test.ts create mode 100644 packages/graphql-server/src/plugins/useRedwoodRealtime.ts create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/db/schema.prisma create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/requireAuth/requireAuth.js create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/skipAuth/skipAuth.js create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/currentUser.sdl.ts create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/todos.sdl.js create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/lib/db.js create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/server.ts create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.js create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.test.js create mode 100644 packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/redwood.toml diff --git a/packages/cli/src/commands/experimental/templates/server.ts.template b/packages/cli/src/commands/experimental/templates/server.ts.template index f359867cc49b..c6b65df0ca28 100644 --- a/packages/cli/src/commands/experimental/templates/server.ts.template +++ b/packages/cli/src/commands/experimental/templates/server.ts.template @@ -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 diff --git a/packages/graphql-server/package.json b/packages/graphql-server/package.json index 71b7dc9b0c7c..b98b6b7aec5f 100644 --- a/packages/graphql-server/package.json +++ b/packages/graphql-server/package.json @@ -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", diff --git a/packages/graphql-server/src/createGraphQLYoga.ts b/packages/graphql-server/src/createGraphQLYoga.ts index a22588420389..b6e5af3edde7 100644 --- a/packages/graphql-server/src/createGraphQLYoga.ts +++ b/packages/graphql-server/src/createGraphQLYoga.ts @@ -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 = ({ @@ -37,7 +39,6 @@ export const createGraphQLYoga = ({ services, sdls, directives = [], - subscriptions = [], armorConfig, allowedOperations, allowIntrospection, @@ -45,6 +46,7 @@ export const createGraphQLYoga = ({ defaultError = 'Something went wrong.', graphiQLEndpoint = '/graphql', schemaOptions, + realtime, }: GraphQLYogaOptions) => { let schema: GraphQLSchema let redwoodDirectivePlugins = [] as Plugin[] @@ -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, @@ -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()) } @@ -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) @@ -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, diff --git a/packages/graphql-server/src/index.ts b/packages/graphql-server/src/index.ts index d4de56574803..b58fd4806acb 100644 --- a/packages/graphql-server/src/index.ts +++ b/packages/graphql-server/src/index.ts @@ -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' diff --git a/packages/graphql-server/src/plugins/__fixtures__/common.ts b/packages/graphql-server/src/plugins/__fixtures__/common.ts index ebf093e72b61..4b190d15ab82 100644 --- a/packages/graphql-server/src/plugins/__fixtures__/common.ts +++ b/packages/graphql-server/src/plugins/__fixtures__/common.ts @@ -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 { @@ -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 { diff --git a/packages/graphql-server/src/plugins/__tests__/__snapshots__/useRedwoodRealtime.test.ts.snap b/packages/graphql-server/src/plugins/__tests__/__snapshots__/useRedwoodRealtime.test.ts.snap new file mode 100644 index 000000000000..90cfb0eb6009 --- /dev/null +++ b/packages/graphql-server/src/plugins/__tests__/__snapshots__/useRedwoodRealtime.test.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useRedwoodRealtime should update schema with live directive 1`] = `"@live"`; diff --git a/packages/graphql-server/src/plugins/__tests__/useRedwoodRealtime.test.ts b/packages/graphql-server/src/plugins/__tests__/useRedwoodRealtime.test.ts new file mode 100644 index 000000000000..27e679d97079 --- /dev/null +++ b/packages/graphql-server/src/plugins/__tests__/useRedwoodRealtime.test.ts @@ -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() + }) +}) diff --git a/packages/graphql-server/src/plugins/index.ts b/packages/graphql-server/src/plugins/index.ts index ade4972cb892..00cdf25d9c9b 100644 --- a/packages/graphql-server/src/plugins/index.ts +++ b/packages/graphql-server/src/plugins/index.ts @@ -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' diff --git a/packages/graphql-server/src/plugins/useRedwoodDirective.ts b/packages/graphql-server/src/plugins/useRedwoodDirective.ts index fafebf85dabb..dbf150b60fc2 100644 --- a/packages/graphql-server/src/plugins/useRedwoodDirective.ts +++ b/packages/graphql-server/src/plugins/useRedwoodDirective.ts @@ -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) }, } diff --git a/packages/graphql-server/src/plugins/useRedwoodRealtime.ts b/packages/graphql-server/src/plugins/useRedwoodRealtime.ts new file mode 100644 index 000000000000..bcc38756bf00 --- /dev/null +++ b/packages/graphql-server/src/plugins/useRedwoodRealtime.ts @@ -0,0 +1,79 @@ +import type { Plugin } from '@envelop/core' +import type { UseLiveQueryOptions } from '@envelop/live-query' +import { useLiveQuery } from '@envelop/live-query' +import { mergeSchemas } from '@graphql-tools/schema' +import { astFromDirective } from '@graphql-tools/utils' +import type { PubSub } from '@graphql-yoga/subscription' +import { createPubSub } from '@graphql-yoga/subscription' +import { GraphQLLiveDirective } from '@n1ru4l/graphql-live-query' +import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store' +import { print } from 'graphql' + +import type { SubscriptionGlobImports } from 'src/subscriptions/makeSubscriptions' + +export type { PubSub } + +export { createPubSub, InMemoryLiveQueryStore } + +export const liveDirectiveTypeDefs = print( + astFromDirective(GraphQLLiveDirective) +) + +export type RedwoodRealtimeOptions = { + liveQueries?: UseLiveQueryOptions + /** + * @description Subscriptions passed from the glob import: + * import subscriptions from 'src/subscriptions/**\/*.{js,ts}' + */ + subscriptions?: { + subscriptions: SubscriptionGlobImports + pubSub: ReturnType + } +} + +export const useRedwoodRealtime = (options: RedwoodRealtimeOptions): Plugin => { + if (options.liveQueries?.liveQueryStore) { + const liveQueryPlugin = useLiveQuery({ + liveQueryStore: options.liveQueries.liveQueryStore, + }) + + /** + * This symbol is added to the schema extensions for checking whether the live query was added to the schema only once. + */ + const wasLiveQueryAdded = Symbol.for('useRedwoodRealtime.wasLiveQueryAdded') + + return { + onSchemaChange({ replaceSchema, schema }) { + if (schema.extensions?.[wasLiveQueryAdded] === true) { + return + } + + const liveSchema = mergeSchemas({ + schemas: [schema], + typeDefs: [liveDirectiveTypeDefs], + }) + + liveSchema.extensions = { + ...schema.extensions, + [wasLiveQueryAdded]: true, + } + + replaceSchema(liveSchema) + }, + onPluginInit({ addPlugin }) { + addPlugin(liveQueryPlugin) + }, + onContextBuilding() { + return ({ extendContext }) => { + extendContext({ + liveQueryStore: options.liveQueries?.liveQueryStore, + pubSub: options.subscriptions?.pubSub as ReturnType< + typeof createPubSub + >, + }) + } + }, + } + } + return {} +} diff --git a/packages/graphql-server/src/types.ts b/packages/graphql-server/src/types.ts index 4629f97094d0..31b571bc132f 100644 --- a/packages/graphql-server/src/types.ts +++ b/packages/graphql-server/src/types.ts @@ -9,13 +9,13 @@ import type { AuthContextPayload, Decoder } from '@redwoodjs/api' import { CorsConfig } from '@redwoodjs/api' import { DirectiveGlobImports } from 'src/directives/makeDirectives' -import type { SubscriptionGlobImports } from 'src/subscriptions/makeSubscriptions' import type { useRedwoodDirectiveReturn, DirectivePluginOptions, } from './plugins/useRedwoodDirective' import { LoggerConfig } from './plugins/useRedwoodLogger' +import type { RedwoodRealtimeOptions } from './plugins/useRedwoodRealtime' export type Resolver = (...args: unknown[]) => unknown export type Services = { @@ -78,7 +78,7 @@ export interface RedwoodGraphQLContext { /** * GraphQLYogaOptions */ -export interface GraphQLYogaOptions { +export type GraphQLYogaOptions = { /** * @description The identifier used in the GraphQL health check response. * It verifies readiness when sent as a header in the readiness check request. @@ -131,12 +131,6 @@ export interface GraphQLYogaOptions { */ directives?: DirectiveGlobImports - /** - * @description Subscriptions passed from the glob import: - * import subscriptions from 'src/subscriptions/**\/*.{js,ts}' - */ - subscriptions?: SubscriptionGlobImports - /** * @description A list of options passed to [makeExecutableSchema] * (https://www.graphql-tools.com/docs/generate-schema/#makeexecutableschemaoptions). @@ -213,6 +207,18 @@ export interface GraphQLYogaOptions { * Headers must set auth-provider, Authorization and (if using dbAuth) the encrypted cookie. */ generateGraphiQLHeader?: GenerateGraphiQLHeader + + /** + * @description Configure RedwoodRealtime plugin with subscriptions and live queries + * + * Only supported in a swerver deploy and not allowed with GraphQLHandler config + */ + realtime?: RedwoodRealtimeOptions } -export interface GraphQLHandlerOptions extends GraphQLYogaOptions {} +/** + * @description Configure GraphQLHandler with options + * + * Note: RedwoodRealtime is not supported + */ +export type GraphQLHandlerOptions = Omit diff --git a/packages/internal/src/__tests__/__snapshots__/graphqlSchema.test.ts.snap b/packages/internal/src/__tests__/__snapshots__/graphqlSchema.test.ts.snap index 525641636998..316f6ec48c7d 100644 --- a/packages/internal/src/__tests__/__snapshots__/graphqlSchema.test.ts.snap +++ b/packages/internal/src/__tests__/__snapshots__/graphqlSchema.test.ts.snap @@ -55,3 +55,72 @@ type Todo { status: String! }" `; + +exports[`Includes live query directive if serverful and realtime 1`] = ` +"""" +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 + +directive @requireAuth(roles: [String]) on FIELD_DEFINITION + +directive @skipAuth on FIELD_DEFINITION + +scalar BigInt + +scalar Date + +scalar DateTime + +scalar JSON + +scalar JSONObject + +type Mutation { + createTodo(body: String!): Todo + renameTodo(body: String!, id: Int!): Todo + updateTodoStatus(id: Int!, status: String!): Todo +} + +"""About the Redwood queries.""" +type Query { + currentUser: JSON + + """Fetches the Redwood root schema.""" + redwood: Redwood + todos: [Todo] + todosCount: Int! +} + +""" +The RedwoodJS Root Schema + +Defines details about RedwoodJS such as the current user and version information. +""" +type Redwood { + """The current user.""" + currentUser: JSON + + """The version of Prisma.""" + prismaVersion: String + + """The version of Redwood.""" + version: String +} + +scalar Time + +type Todo { + body: String! + id: Int! + status: String! +}" +`; diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/db/schema.prisma b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/db/schema.prisma new file mode 100644 index 000000000000..86078a61db0f --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/db/schema.prisma @@ -0,0 +1,21 @@ +datasource sqlite { + url = "file:./dev.sqlite" + provider = "sqlite" +} + +generator photonjs { + provider = "prisma-client-js" +} + +model Book { + id Int @id @default(autoincrement()) + title String @unique + Shelf Shelf? @relation(fields: [shelfId], references: [id]) + shelfId Int? +} + +model Shelf { + id Int @id @default(autoincrement()) + name String @unique + books Book[] +} diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/requireAuth/requireAuth.js b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/requireAuth/requireAuth.js new file mode 100644 index 000000000000..dfe68035c1ca --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/requireAuth/requireAuth.js @@ -0,0 +1,18 @@ +import gql from 'graphql-tag' + +import { createValidatorDirective } from '@redwoodjs/graphql-server' + +import { requireAuth as applicationRequireAuth } from 'src/lib/auth' + +export const schema = gql` + directive @requireAuth(roles: [String]) on FIELD_DEFINITION +` + +const validate = ({ directiveArgs }) => { + const { roles } = directiveArgs + applicationRequireAuth({ roles }) +} + +const requireAuth = createValidatorDirective(schema, validate) + +export default requireAuth diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/skipAuth/skipAuth.js b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/skipAuth/skipAuth.js new file mode 100644 index 000000000000..9a6d099f951a --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/directives/skipAuth/skipAuth.js @@ -0,0 +1,13 @@ +import gql from 'graphql-tag' + +import { createValidatorDirective } from '@redwoodjs/graphql-server' + +export const schema = gql` + directive @skipAuth on FIELD_DEFINITION +` + +const skipAuth = createValidatorDirective(schema, () => { + return +}) + +export default skipAuth diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/currentUser.sdl.ts b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/currentUser.sdl.ts new file mode 100644 index 000000000000..7065b0df33bb --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/currentUser.sdl.ts @@ -0,0 +1,7 @@ +export const schema = gql` + type Query { + currentUser: JSON + } +` + +const a = (x: string) => { return x } \ No newline at end of file diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/todos.sdl.js b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/todos.sdl.js new file mode 100644 index 000000000000..344d065e0636 --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/graphql/todos.sdl.js @@ -0,0 +1,18 @@ +export const schema = gql` + type Todo { + id: Int! + body: String! + status: String! + } + + type Query { + todos: [Todo] @skipAuth + todosCount: Int! @skipAuth + } + + type Mutation { + createTodo(body: String!): Todo @skipAuth + updateTodoStatus(id: Int!, status: String!): Todo @skipAuth + renameTodo(id: Int!, body: String!): Todo @skipAuth + } +` diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/lib/db.js b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/lib/db.js new file mode 100644 index 000000000000..465626a85be0 --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/lib/db.js @@ -0,0 +1,6 @@ +// See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor +// for options. + +import { PrismaClient } from '@prisma/client' + +export const db = new PrismaClient() diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/server.ts b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/server.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.js b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.js new file mode 100644 index 000000000000..a4cd235d3bd8 --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.js @@ -0,0 +1,21 @@ +import { db } from 'src/lib/db' + +export const todos = () => db.todo.findMany() + +export const createTodo = ({ body }) => db.todo.create({ data: { body } }) + +export const numTodos = () => { + return context.currentUser +} + +export const updateTodoStatus = ({ id, status }) => + db.todo.update({ + data: { status }, + where: { id }, + }) + +export const renameTodo = ({ id, body }) => + db.todo.update({ + data: { body }, + where: { id }, + }) diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.test.js b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.test.js new file mode 100644 index 000000000000..5eead15806db --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/api/src/services/todos/todos.test.js @@ -0,0 +1,11 @@ +import dog from 'src/lib/dog' + +jest.mock('src/lib/dog', () => { + return { + mockedModule: true + } +}) + +test('should always pass', () => { + expect(dog.mockedModule).toBe(true) +}) diff --git a/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/redwood.toml b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/redwood.toml new file mode 100644 index 000000000000..f68d0d5de4db --- /dev/null +++ b/packages/internal/src/__tests__/fixtures/graphqlCodeGen/realtime/redwood.toml @@ -0,0 +1,10 @@ +[web] + port = 8910 + apiProxyPath = "/api/functions" + +[api] + port = 8911 + [api.paths] + functions = './api/src/functions' + graphql = './api/src/graphql' + generated = './api/generated' diff --git a/packages/internal/src/__tests__/graphqlSchema.test.ts b/packages/internal/src/__tests__/graphqlSchema.test.ts index c215864e129c..6891379cdf5d 100644 --- a/packages/internal/src/__tests__/graphqlSchema.test.ts +++ b/packages/internal/src/__tests__/graphqlSchema.test.ts @@ -40,6 +40,27 @@ test('Generates GraphQL schema', async () => { expect(schemaPath).toMatch(expectedPath) }) +test('Includes live query directive if serverful and realtime ', async () => { + const fixturePath = path.resolve( + __dirname, + './fixtures/graphqlCodeGen/realtime' + ) + process.env.RWJS_CWD = fixturePath + + const expectedPath = path.join(fixturePath, '.redwood', 'schema.graphql') + + jest + .spyOn(fs, 'writeFileSync') + .mockImplementation( + (file: fs.PathOrFileDescriptor, data: string | ArrayBufferView) => { + expect(file).toMatch(expectedPath) + expect(data).toMatchSnapshot() + } + ) + + await generateGraphQLSchema() +}) + test('Prints error message when schema loading fails', async () => { const fixturePath = path.resolve( __dirname, diff --git a/packages/internal/src/generate/graphqlSchema.ts b/packages/internal/src/generate/graphqlSchema.ts index a30321f7ba3f..e6c0f4b08fd0 100644 --- a/packages/internal/src/generate/graphqlSchema.ts +++ b/packages/internal/src/generate/graphqlSchema.ts @@ -9,8 +9,8 @@ import chalk from 'chalk' import { DocumentNode, print } from 'graphql' import terminalLink from 'terminal-link' -import { rootSchema } from '@redwoodjs/graphql-server' -import { getPaths } from '@redwoodjs/project-config' +import { rootSchema, liveDirectiveTypeDefs } from '@redwoodjs/graphql-server' +import { getPaths, resolveFile } from '@redwoodjs/project-config' export const generateGraphQLSchema = async () => { const schemaPointerMap = { @@ -20,6 +20,11 @@ export const generateGraphQLSchema = async () => { 'subscriptions/**/*.{js,ts}': {}, } + // If we are serverful, we need to include the live directive for realtime support + if (resolveFile(`${getPaths().api.src}/server`)) { + schemaPointerMap[liveDirectiveTypeDefs] = {} + } + const loadSchemaConfig: LoadSchemaOptions = { assumeValidSDL: true, sort: true, diff --git a/yarn.lock b/yarn.lock index 5c11dba80e07..152202a5ca1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2602,6 +2602,22 @@ __metadata: languageName: node linkType: hard +"@envelop/live-query@npm:6.0.0": + version: 6.0.0 + resolution: "@envelop/live-query@npm:6.0.0" + dependencies: + "@graphql-tools/utils": ^10.0.0 + "@n1ru4l/graphql-live-query": ^0.10.0 + "@n1ru4l/graphql-live-query-patch": ^0.7.0 + "@n1ru4l/in-memory-live-query-store": ^0.10.0 + tslib: ^2.5.0 + peerDependencies: + "@envelop/core": ^4.0.0 + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 3a3a741c454abc09ea51308ebc7f1b78306647957cfa75b8323db2cae35139a8515c1a2760ccb4d19604ea3da768db5ed55b1d5dc9576986b37a3db1d771abfd + languageName: node + linkType: hard + "@envelop/on-resolve@npm:2.0.6": version: 2.0.6 resolution: "@envelop/on-resolve@npm:2.0.6" @@ -4422,7 +4438,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/utils@npm:^8.8.0": +"@graphql-tools/utils@npm:^8.5.2, @graphql-tools/utils@npm:^8.8.0": version: 8.13.1 resolution: "@graphql-tools/utils@npm:8.13.1" dependencies: @@ -4475,7 +4491,7 @@ __metadata: languageName: node linkType: hard -"@graphql-yoga/subscription@npm:^3.1.0": +"@graphql-yoga/subscription@npm:3.1.0, @graphql-yoga/subscription@npm:^3.1.0": version: 3.1.0 resolution: "@graphql-yoga/subscription@npm:3.1.0" dependencies: @@ -5109,6 +5125,39 @@ __metadata: languageName: node linkType: hard +"@n1ru4l/graphql-live-query-patch@npm:^0.7.0": + version: 0.7.0 + resolution: "@n1ru4l/graphql-live-query-patch@npm:0.7.0" + dependencies: + "@repeaterjs/repeater": ^3.0.4 + peerDependencies: + graphql: ^15.4.0 || ^16.0.0 + checksum: a7ee5825a277e5240a1908cf830c3d55f114a6d8fec520dc155081f170241c50b92309169a6cc79f66087b97f0aaad8895573b2c346fd7671402c6ce3dd81452 + languageName: node + linkType: hard + +"@n1ru4l/graphql-live-query@npm:0.10.0, @n1ru4l/graphql-live-query@npm:^0.10.0": + version: 0.10.0 + resolution: "@n1ru4l/graphql-live-query@npm:0.10.0" + peerDependencies: + graphql: ^15.4.0 || ^16.0.0 + checksum: 972bea6d6e4dcc443e15c19f2da373fc01f7e6c8d366569075dc89dc94e55da5a4029d0e8cff289d208c43edcca5e112fe286c6601717ab42d428fc3d5c2dfae + languageName: node + linkType: hard + +"@n1ru4l/in-memory-live-query-store@npm:0.10.0, @n1ru4l/in-memory-live-query-store@npm:^0.10.0": + version: 0.10.0 + resolution: "@n1ru4l/in-memory-live-query-store@npm:0.10.0" + dependencies: + "@graphql-tools/utils": ^8.5.2 + "@n1ru4l/graphql-live-query": 0.10.0 + "@repeaterjs/repeater": ^3.0.4 + peerDependencies: + graphql: ^15.4.0 || ^16.0.0 + checksum: c2b4c1c09fa68ac7bd776166a86d361ac6e2659028391e7c3cca25943e966d1d035271141e28d0a519d50409cc07728df163d01ee26cacf403f59a6dcf2d4243 + languageName: node + linkType: hard + "@ndelangen/get-tarball@npm:^3.0.7": version: 3.0.7 resolution: "@ndelangen/get-tarball@npm:3.0.7" @@ -7493,6 +7542,7 @@ __metadata: "@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 "@envelop/testing": 6.0.0 "@envelop/types": 3.0.2 @@ -7500,6 +7550,9 @@ __metadata: "@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