From 0081d3ae607e2d6c4c547219fbfbfa0c79005d6e Mon Sep 17 00:00:00 2001 From: Josh GM Walker <56300765+Josh-Walker-GM@users.noreply.github.com> Date: Wed, 18 Sep 2024 18:36:07 +0100 Subject: [PATCH] fix(graphql): Allow including 'File' scalar by default to be disabled (#11540) There was a problem introduced in v8 when we included the `File` scalar by default. This meant a custom implementation by the user could be clobbered by the new default. This change allows the user to supply config to disable including it by default. This is not how I would have loved to have done things here. Config in two places is rubbish but given the organisation of this currently it was generally unavoidable. --- .changesets/11540.md | 5 +++ docs/docs/graphql.md | 37 ++++++++++++++++++ .../graphql-server/src/createGraphQLYoga.ts | 2 + .../graphql-server/src/makeMergedSchema.ts | 12 +++++- packages/graphql-server/src/rootSchema.ts | 8 +++- packages/graphql-server/src/types.ts | 12 ++++++ .../src/__tests__/clientPreset.test.ts | 18 ++++++--- .../internal/src/generate/graphqlCodeGen.ts | 39 +++++++++++++------ .../internal/src/generate/graphqlSchema.ts | 10 ++++- .../src/__tests__/config.test.ts | 3 ++ packages/project-config/src/config.ts | 9 ++++- 11 files changed, 133 insertions(+), 22 deletions(-) create mode 100644 .changesets/11540.md diff --git a/.changesets/11540.md b/.changesets/11540.md new file mode 100644 index 000000000000..06a02b0b1b96 --- /dev/null +++ b/.changesets/11540.md @@ -0,0 +1,5 @@ +- fix(graphql): Allow including 'File' scalar by default to be disabled (#11540) by @Josh-Walker-GM + +As of v8.0.0 a `File` scalar was added to your graphql schema by default. This could be problematic if you wanted to define your own `File` scalar. + +With this change it is now possible to disable including this scalar by default. To see how to do so look at the `Default Scalar` section of the `Graphql` docs [here](https://docs.redwoodjs.com/docs/graphql#default-scalars) diff --git a/docs/docs/graphql.md b/docs/docs/graphql.md index 7ecd3581cde5..8f0b6f67f758 100644 --- a/docs/docs/graphql.md +++ b/docs/docs/graphql.md @@ -689,6 +689,43 @@ api | - deletePost Mutation To fix these errors, simple declare with `@requireAuth` to enforce authentication or `@skipAuth` to keep the operation public on each as appropriate for your app's permissions needs. +## Default Scalars + +Redwood includes a selection of scalar types by default. + +Currently we allow you to control whether or not the `File` scalar is included automatically or not. By default we include the `File` scalar which maps to the standard `File` type. To disable this scalar you should add config to two places: + +1. In your `redwood.toml` file like so: + + ```toml + [graphql] + includeScalars.File = false + ``` + +2. In your `functions/graphql.ts` like so: + + ```typescript + export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, + // highlight-start + includeScalars: { + File: false, + }, + // highlight-end + }) + ``` + +With those two config values added your schema will no longer contain the `File` scalar by default and you are free to add your own or continue without one. + ## Custom Scalars GraphQL scalar types give data meaning and validate that their values makes sense. Out of the box, GraphQL comes with `Int`, `Float`, `String`, `Boolean` and `ID`. While those can cover a wide variety of use cases, you may need more specific scalar types to better describe and validate your application's data. diff --git a/packages/graphql-server/src/createGraphQLYoga.ts b/packages/graphql-server/src/createGraphQLYoga.ts index f6f71b4f86b9..ebfc8bf4269b 100644 --- a/packages/graphql-server/src/createGraphQLYoga.ts +++ b/packages/graphql-server/src/createGraphQLYoga.ts @@ -52,6 +52,7 @@ export const createGraphQLYoga = ({ realtime, trustedDocuments, openTelemetryOptions, + includeScalars, }: GraphQLYogaOptions) => { let schema: GraphQLSchema let redwoodDirectivePlugins = [] as Plugin[] @@ -85,6 +86,7 @@ export const createGraphQLYoga = ({ directives: projectDirectives, subscriptions: projectSubscriptions, schemaOptions, + includeScalars, }) } catch (e) { logger.fatal(e as Error, '\n ⚠️ GraphQL server crashed \n') diff --git a/packages/graphql-server/src/makeMergedSchema.ts b/packages/graphql-server/src/makeMergedSchema.ts index abf5ba73d883..eed8a0fad14f 100644 --- a/packages/graphql-server/src/makeMergedSchema.ts +++ b/packages/graphql-server/src/makeMergedSchema.ts @@ -26,6 +26,7 @@ import type { ServicesGlobImports, GraphQLTypeWithFields, SdlGlobImports, + RedwoodScalarConfig, } from './types' const wrapWithOpenTelemetry = async ( @@ -358,11 +359,13 @@ export const makeMergedSchema = ({ schemaOptions = {}, directives, subscriptions = [], + includeScalars, }: { sdls: SdlGlobImports services: ServicesGlobImports directives: RedwoodDirective[] subscriptions: RedwoodSubscription[] + includeScalars?: RedwoodScalarConfig /** * A list of options passed to [makeExecutableSchema](https://www.graphql-tools.com/docs/generate-schema/#makeexecutableschemaoptions). @@ -371,9 +374,16 @@ export const makeMergedSchema = ({ }) => { const sdlSchemas = Object.values(sdls).map(({ schema }) => schema) + const rootEntries = [rootGqlSchema.schema] + + // We cannot access the getConfig from project-config here so the user must supply it via a config option + if (includeScalars?.File !== false) { + rootEntries.push(rootGqlSchema.scalarSchemas.File) + } + const typeDefs = mergeTypes( [ - rootGqlSchema.schema, + ...rootEntries, ...directives.map((directive) => directive.schema), // pick out schemas from directives ...subscriptions.map((subscription) => subscription.schema), // pick out schemas from subscriptions ...sdlSchemas, // pick out the schemas from sdls diff --git a/packages/graphql-server/src/rootSchema.ts b/packages/graphql-server/src/rootSchema.ts index 62ba045d7c80..758808603461 100644 --- a/packages/graphql-server/src/rootSchema.ts +++ b/packages/graphql-server/src/rootSchema.ts @@ -27,7 +27,6 @@ export const schema = gql` scalar JSON scalar JSONObject scalar Byte - scalar File """ The RedwoodJS Root Schema @@ -52,6 +51,13 @@ export const schema = gql` } ` +export const scalarSchemas = { + File: gql` + scalar File + `, +} +export type ScalarSchemaKeys = keyof typeof scalarSchemas + export interface Resolvers { BigInt: typeof BigIntResolver Date: typeof DateResolver diff --git a/packages/graphql-server/src/types.ts b/packages/graphql-server/src/types.ts index 752017d23734..17a0e126eb2e 100644 --- a/packages/graphql-server/src/types.ts +++ b/packages/graphql-server/src/types.ts @@ -96,6 +96,10 @@ export interface RedwoodOpenTelemetryConfig { result: boolean } +export interface RedwoodScalarConfig { + File?: boolean +} + /** * GraphQLYogaOptions */ @@ -248,6 +252,14 @@ export type GraphQLYogaOptions = { * @description Configure OpenTelemetry plugin behaviour */ openTelemetryOptions?: RedwoodOpenTelemetryConfig + + /** + * @description Configure which scalars to include in the schema. This should match your + * `graphql.includeScalars` configuration in `redwood.toml`. + * + * The default is to include. You must set to `false` to exclude. + */ + includeScalars?: RedwoodScalarConfig } /** diff --git a/packages/internal/src/__tests__/clientPreset.test.ts b/packages/internal/src/__tests__/clientPreset.test.ts index 7cefbad11116..94c1d9287787 100644 --- a/packages/internal/src/__tests__/clientPreset.test.ts +++ b/packages/internal/src/__tests__/clientPreset.test.ts @@ -9,9 +9,9 @@ import { generateGraphQLSchema } from '../generate/graphqlSchema' const { mockedGetConfig } = vi.hoisted(() => { return { - mockedGetConfig: vi - .fn() - .mockReturnValue({ graphql: { trustedDocuments: false } }), + mockedGetConfig: vi.fn().mockReturnValue({ + graphql: { trustedDocuments: false, includeScalars: { File: true } }, + }), } }) @@ -34,12 +34,16 @@ beforeEach(() => { afterEach(() => { delete process.env.RWJS_CWD - mockedGetConfig.mockReturnValue({ graphql: { trustedDocuments: false } }) + mockedGetConfig.mockReturnValue({ + graphql: { trustedDocuments: false, includeScalars: { File: true } }, + }) }) describe('Generate client preset', () => { test('for web side', async () => { - mockedGetConfig.mockReturnValue({ graphql: { trustedDocuments: true } }) + mockedGetConfig.mockReturnValue({ + graphql: { trustedDocuments: true, includeScalars: { File: true } }, + }) await generateGraphQLSchema() const { clientPresetFiles, errors } = await generateClientPreset() @@ -62,7 +66,9 @@ describe('Generate client preset', () => { }) test('for api side', async () => { - mockedGetConfig.mockReturnValue({ graphql: { trustedDocuments: true } }) + mockedGetConfig.mockReturnValue({ + graphql: { trustedDocuments: true, includeScalars: { File: true } }, + }) await generateGraphQLSchema() const { trustedDocumentsStoreFile, errors } = await generateClientPreset() diff --git a/packages/internal/src/generate/graphqlCodeGen.ts b/packages/internal/src/generate/graphqlCodeGen.ts index b59f5973bf23..ec3f8b43c451 100644 --- a/packages/internal/src/generate/graphqlCodeGen.ts +++ b/packages/internal/src/generate/graphqlCodeGen.ts @@ -274,22 +274,37 @@ async function getPluginConfig(side: CodegenSide) { `MergePrismaWithSdlTypes, AllMappedModels>` }) + type ScalarKeys = + | 'BigInt' + | 'DateTime' + | 'Date' + | 'JSON' + | 'JSONObject' + | 'Time' + | 'Byte' + | 'File' + const scalars: Partial> = { + // We need these, otherwise these scalars are mapped to any + BigInt: 'number', + // @Note: DateTime fields can be valid Date-strings, or the Date object in the api side. They're always strings on the web side. + DateTime: side === CodegenSide.WEB ? 'string' : 'Date | string', + Date: side === CodegenSide.WEB ? 'string' : 'Date | string', + JSON: 'Prisma.JsonValue', + JSONObject: 'Prisma.JsonObject', + Time: side === CodegenSide.WEB ? 'string' : 'Date | string', + Byte: 'Buffer', + } + + const config = getConfig() + if (config.graphql.includeScalars.File) { + scalars.File = 'File' + } + const pluginConfig: CodegenTypes.PluginConfig & rwTypescriptResolvers.TypeScriptResolversPluginConfig = { makeResolverTypeCallable: true, namingConvention: 'keep', // to allow camelCased query names - scalars: { - // We need these, otherwise these scalars are mapped to any - BigInt: 'number', - // @Note: DateTime fields can be valid Date-strings, or the Date object in the api side. They're always strings on the web side. - DateTime: side === CodegenSide.WEB ? 'string' : 'Date | string', - Date: side === CodegenSide.WEB ? 'string' : 'Date | string', - JSON: 'Prisma.JsonValue', - JSONObject: 'Prisma.JsonObject', - Time: side === CodegenSide.WEB ? 'string' : 'Date | string', - Byte: 'Buffer', - File: 'File', - }, + scalars, // prevent type names being PetQueryQuery, RW generators already append // Query/Mutation/etc omitOperationSuffix: true, diff --git a/packages/internal/src/generate/graphqlSchema.ts b/packages/internal/src/generate/graphqlSchema.ts index c35cfec60e26..f1c61d0cb70c 100644 --- a/packages/internal/src/generate/graphqlSchema.ts +++ b/packages/internal/src/generate/graphqlSchema.ts @@ -13,10 +13,12 @@ import { print } from 'graphql' import terminalLink from 'terminal-link' import { rootSchema } from '@redwoodjs/graphql-server' -import { getPaths, resolveFile } from '@redwoodjs/project-config' +import type { ScalarSchemaKeys } from '@redwoodjs/graphql-server/src/rootSchema' +import { getPaths, getConfig, resolveFile } from '@redwoodjs/project-config' export const generateGraphQLSchema = async () => { const redwoodProjectPaths = getPaths() + const redwoodProjectConfig = getConfig() const schemaPointerMap = { [print(rootSchema.schema)]: {}, @@ -25,6 +27,12 @@ export const generateGraphQLSchema = async () => { 'subscriptions/**/*.{js,ts}': {}, } + for (const [name, schema] of Object.entries(rootSchema.scalarSchemas)) { + if (redwoodProjectConfig.graphql.includeScalars[name as ScalarSchemaKeys]) { + schemaPointerMap[print(schema)] = {} + } + } + // If we're serverful and the user is using realtime, we need to include the live directive for realtime support. // Note the `ERR_ prefix in`ERR_MODULE_NOT_FOUND`. Since we're using `await import`, // if the package (here, `@redwoodjs/realtime`) can't be found, it throws this error, with the prefix. diff --git a/packages/project-config/src/__tests__/config.test.ts b/packages/project-config/src/__tests__/config.test.ts index ff8de1065ad3..b721009655b5 100644 --- a/packages/project-config/src/__tests__/config.test.ts +++ b/packages/project-config/src/__tests__/config.test.ts @@ -81,6 +81,9 @@ describe('getConfig', () => { }, "graphql": { "fragments": false, + "includeScalars": { + "File": true, + }, "trustedDocuments": false, }, "notifications": { diff --git a/packages/project-config/src/config.ts b/packages/project-config/src/config.ts index 623f897ec2d9..d1ff478f0985 100644 --- a/packages/project-config/src/config.ts +++ b/packages/project-config/src/config.ts @@ -87,6 +87,9 @@ export interface Config { graphql: { fragments: boolean trustedDocuments: boolean + includeScalars: { + File: boolean + } } notifications: { versionUpdates: string[] @@ -145,7 +148,11 @@ const DEFAULT_CONFIG: Config = { serverConfig: './api/server.config.js', debugPort: 18911, }, - graphql: { fragments: false, trustedDocuments: false }, + graphql: { + fragments: false, + trustedDocuments: false, + includeScalars: { File: true }, + }, browser: { open: false, },