From 0a68e820abce809913c21165ea29fdff03c14418 Mon Sep 17 00:00:00 2001 From: mitchellhamilton Date: Wed, 27 Jul 2022 14:33:08 +1000 Subject: [PATCH] Fix for Json nulls --- .changeset/great-sloths-bake.md | 5 +++ .changeset/slow-games-greet.md | 5 +++ packages/core/src/artifacts.ts | 11 ++++-- packages/core/src/fields/types/json/index.ts | 14 ++------ packages/core/src/globals.d.ts | 0 .../src/lib/core/mutations/create-update.ts | 28 ++++++++++++--- packages/core/src/lib/core/utils.ts | 25 +++++++++++--- packages/core/src/lib/createSystem.ts | 8 +++-- packages/core/src/scripts/run/dev.ts | 34 ++++++++++++------- .../core/src/scripts/tests/migrations.test.ts | 2 +- tests/api-tests/access-control/schema.test.ts | 2 +- 11 files changed, 95 insertions(+), 39 deletions(-) create mode 100644 .changeset/great-sloths-bake.md create mode 100644 .changeset/slow-games-greet.md delete mode 100644 packages/core/src/globals.d.ts diff --git a/.changeset/great-sloths-bake.md b/.changeset/great-sloths-bake.md new file mode 100644 index 00000000000..eca51404548 --- /dev/null +++ b/.changeset/great-sloths-bake.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': major +--- + +Updates to Prisma 4.1.0 diff --git a/.changeset/slow-games-greet.md b/.changeset/slow-games-greet.md new file mode 100644 index 00000000000..a92ed5de500 --- /dev/null +++ b/.changeset/slow-games-greet.md @@ -0,0 +1,5 @@ +--- +'@keystone-6/core': major +--- + +Returning `null` from a `resolveInput` hook for json fields will now store a database null as expected diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts index fd4721b78a0..74ce4f76542 100644 --- a/packages/core/src/artifacts.ts +++ b/packages/core/src/artifacts.ts @@ -266,6 +266,13 @@ async function generatePrismaClient(cwd: string) { } } -export function requirePrismaClient(cwd: string) { - return require(path.join(cwd, 'node_modules/.prisma/client')).PrismaClient; +export type PrismaModule = { + PrismaClient: { + new (args: unknown): any; + }; + Prisma: { DbNull: unknown; JsonNull: unknown; [key: string]: unknown }; +}; + +export function requirePrismaClient(cwd: string): PrismaModule { + return require(path.join(cwd, 'node_modules/.prisma/client')); } diff --git a/packages/core/src/fields/types/json/index.ts b/packages/core/src/fields/types/json/index.ts index 7cd8a26f2a3..7a013edcb23 100644 --- a/packages/core/src/fields/types/json/index.ts +++ b/packages/core/src/fields/types/json/index.ts @@ -24,11 +24,6 @@ export const json = throw Error("isIndexed: 'unique' is not a supported option for field type json"); } - const resolve = (val: JSONValue | undefined) => - val === null && (meta.provider === 'postgresql' || meta.provider === 'mysql') - ? 'DbNull' - : val; - return jsonFieldTypePolyfilledForSQLite( meta.provider, { @@ -37,10 +32,10 @@ export const json = create: { arg: graphql.arg({ type: graphql.JSON }), resolve(val) { - return resolve(val === undefined ? defaultValue : val); + return val === undefined ? defaultValue : val; }, }, - update: { arg: graphql.arg({ type: graphql.JSON }), resolve }, + update: { arg: graphql.arg({ type: graphql.JSON }) }, }, output: graphql.field({ type: graphql.JSON }), views: resolveView('json/views'), @@ -50,10 +45,7 @@ export const json = default: defaultValue === null ? undefined - : { - kind: 'literal', - value: JSON.stringify(defaultValue), - }, + : { kind: 'literal', value: JSON.stringify(defaultValue) }, map: config.db?.map, } ); diff --git a/packages/core/src/globals.d.ts b/packages/core/src/globals.d.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/core/src/lib/core/mutations/create-update.ts b/packages/core/src/lib/core/mutations/create-update.ts index 2362ad930fb..6e7e7425d52 100644 --- a/packages/core/src/lib/core/mutations/create-update.ts +++ b/packages/core/src/lib/core/mutations/create-update.ts @@ -7,6 +7,7 @@ import { IdType, runWithPrisma, getWriteLimit, + getPrismaNamespace, } from '../utils'; import { InputFilter, resolveUniqueWhereInput, UniqueInputFilter } from '../where-inputs'; import { @@ -363,7 +364,7 @@ async function resolveInputForCreateOrUpdate( // Return the full resolved input (ready for prisma level operation), // and the afterOperation hook to be applied return { - data: flattenMultiDbFields(list.fields, hookArgs.resolvedData), + data: transformForPrismaClient(list.fields, hookArgs.resolvedData, context), afterOperation: async (updatedItem: BaseItem) => { await nestedMutationState.afterOperation(); await runSideEffectOnlyHook( @@ -381,19 +382,36 @@ async function resolveInputForCreateOrUpdate( }; } -function flattenMultiDbFields( +function transformInnerDBField( + dbField: Exclude, + context: KeystoneContext, + value: unknown +) { + if (dbField.kind === 'scalar' && dbField.scalar === 'Json' && value === null) { + const Prisma = getPrismaNamespace(context); + value = Prisma.DbNull; + } + return value; +} + +function transformForPrismaClient( fields: Record, - data: Record + data: Record, + context: KeystoneContext ) { return Object.fromEntries( Object.entries(data).flatMap(([fieldKey, value]) => { const { dbField } = fields[fieldKey]; if (dbField.kind === 'multi') { return Object.entries(value).map(([innerFieldKey, fieldValue]) => { - return [getDBFieldKeyForFieldOnMultiField(fieldKey, innerFieldKey), fieldValue]; + return [ + getDBFieldKeyForFieldOnMultiField(fieldKey, innerFieldKey), + transformInnerDBField(dbField.fields[innerFieldKey], context, fieldValue), + ]; }); } - return [[fieldKey, value]]; + + return [[fieldKey, transformInnerDBField(dbField, context, value)]]; }) ); } diff --git a/packages/core/src/lib/core/utils.ts b/packages/core/src/lib/core/utils.ts index 8c8b023a915..9ff2cf6b6d8 100644 --- a/packages/core/src/lib/core/utils.ts +++ b/packages/core/src/lib/core/utils.ts @@ -1,5 +1,6 @@ import { Limit } from 'p-limit'; import pluralize from 'pluralize'; +import { PrismaModule } from '../../artifacts'; import { BaseItem, KeystoneConfig, KeystoneContext } from '../../types'; import { humanize } from '../utils'; import { prismaError } from './graphql-errors'; @@ -164,16 +165,32 @@ export function getDBFieldKeyForFieldOnMultiField(fieldKey: string, subField: st // because even across requests, we want to apply the limit on SQLite const writeLimits = new WeakMap(); -export const setWriteLimit = (prismaClient: object, limit: Limit) => { +export function setWriteLimit(prismaClient: object, limit: Limit) { writeLimits.set(prismaClient, limit); -}; +} // this accepts the context instead of the prisma client because the prisma client on context is `any` // so by accepting the context, it'll be less likely the wrong thing will be passed. -export const getWriteLimit = (context: KeystoneContext) => { +export function getWriteLimit(context: KeystoneContext) { const limit = writeLimits.get(context.prisma); if (limit === undefined) { throw new Error('unexpected write limit not set for prisma client'); } return limit; -}; +} + +const prismaNamespaces = new WeakMap(); + +export function setPrismaNamespace(prismaClient: object, prismaNamespace: PrismaModule['Prisma']) { + prismaNamespaces.set(prismaClient, prismaNamespace); +} + +// this accepts the context instead of the prisma client because the prisma client on context is `any` +// so by accepting the context, it'll be less likely the wrong thing will be passed. +export function getPrismaNamespace(context: KeystoneContext) { + const limit = prismaNamespaces.get(context.prisma); + if (limit === undefined) { + throw new Error('unexpected prisma namespace not set for prisma client'); + } + return limit; +} diff --git a/packages/core/src/lib/createSystem.ts b/packages/core/src/lib/createSystem.ts index ae9f62602f1..71462c4c8a2 100644 --- a/packages/core/src/lib/createSystem.ts +++ b/packages/core/src/lib/createSystem.ts @@ -2,10 +2,11 @@ import pLimit from 'p-limit'; import { FieldData, KeystoneConfig, getGqlNames } from '../types'; import { createAdminMeta } from '../admin-ui/system/createAdminMeta'; +import { PrismaModule } from '../artifacts'; import { createGraphQLSchema } from './createGraphQLSchema'; import { makeCreateContext } from './context/createContext'; import { initialiseLists } from './core/types-for-lists'; -import { setWriteLimit } from './core/utils'; +import { setPrismaNamespace, setWriteLimit } from './core/utils'; function getSudoGraphQLSchema(config: KeystoneConfig) { // This function creates a GraphQLSchema based on a modified version of the provided config. @@ -72,12 +73,13 @@ export function createSystem(config: KeystoneConfig, isLiveReload?: boolean) { return { graphQLSchema, adminMeta, - getKeystone: (PrismaClient: any) => { - const prismaClient = new PrismaClient({ + getKeystone: (prismaModule: PrismaModule) => { + const prismaClient = new prismaModule.PrismaClient({ log: config.db.enableLogging ? ['query'] : undefined, datasources: { [config.db.provider]: { url: config.db.url } }, }); setWriteLimit(prismaClient, pLimit(config.db.provider === 'sqlite' ? 1 : Infinity)); + setPrismaNamespace(prismaClient, prismaModule.Prisma); prismaClient.$on('beforeExit', async () => { // Prisma is failing to properly clean up its child processes // https://github.com/keystonejs/keystone/issues/5477 diff --git a/packages/core/src/scripts/run/dev.ts b/packages/core/src/scripts/run/dev.ts index c36abb3ccde..b94c66de894 100644 --- a/packages/core/src/scripts/run/dev.ts +++ b/packages/core/src/scripts/run/dev.ts @@ -60,8 +60,15 @@ export const dev = async (cwd: string, shouldDropDatabase: boolean) => { const p = serializePathForImport( path.relative(path.join(getAdminPath(cwd), 'pages', 'api'), `${cwd}/keystone`) ); - const { adminMeta, graphQLSchema, createContext, prismaSchema, apolloServer, ...rest } = - await setupInitialKeystone(config, cwd, shouldDropDatabase); + const { + adminMeta, + graphQLSchema, + createContext, + prismaSchema, + apolloServer, + prismaClientModule, + ...rest + } = await setupInitialKeystone(config, cwd, shouldDropDatabase); const prismaClient = createContext().prisma; ({ disconnect, expressServer } = rest); // if you've disabled the Admin UI, sorry, no live reloading @@ -167,8 +174,11 @@ exports.default = function (req, res) { return res.send(x.toString()) } await generateNodeModulesArtifactsWithoutPrismaClient(graphQLSchema, newConfig, cwd); await generateAdminUI(newConfig, graphQLSchema, adminMeta, getAdminPath(cwd), true); - const keystone = getKeystone(function fakePrismaClientClass() { - return prismaClient; + const keystone = getKeystone({ + PrismaClient: function fakePrismaClientClass() { + return prismaClient; + } as unknown as new (args: unknown) => any, + Prisma: prismaClientModule.Prisma, }); await keystone.connect(); const servers = await createExpressServer( @@ -301,10 +311,8 @@ async function setupInitialKeystone( // Generate the Artifacts console.log('✨ Generating GraphQL and Prisma schemas'); const prismaSchema = (await generateCommittedArtifacts(graphQLSchema, config, cwd)).prisma; - let keystonePromise = generateNodeModulesArtifacts(graphQLSchema, config, cwd).then(() => { - const prismaClient = requirePrismaClient(cwd); - return getKeystone(prismaClient); - }); + + let prismaClientGenerationPromise = generateNodeModulesArtifacts(graphQLSchema, config, cwd); let migrationPromise: Promise; @@ -327,8 +335,9 @@ async function setupInitialKeystone( ); } - const [keystone] = await Promise.all([keystonePromise, migrationPromise]); - const { createContext } = keystone; + await Promise.all([prismaClientGenerationPromise, migrationPromise]); + const prismaClientModule = requirePrismaClient(cwd); + const keystone = getKeystone(prismaClientModule); // Connect to the Database console.log('✨ Connecting to the database'); @@ -339,7 +348,7 @@ async function setupInitialKeystone( const { apolloServer, expressServer } = await createExpressServer( config, graphQLSchema, - createContext + keystone.createContext ); console.log(`✅ GraphQL API ready`); @@ -357,8 +366,9 @@ async function setupInitialKeystone( expressServer, apolloServer, graphQLSchema, - createContext, + createContext: keystone.createContext, prismaSchema, + prismaClientModule, }; } diff --git a/packages/core/src/scripts/tests/migrations.test.ts b/packages/core/src/scripts/tests/migrations.test.ts index 01316797cb5..7cee3a8f8b7 100644 --- a/packages/core/src/scripts/tests/migrations.test.ts +++ b/packages/core/src/scripts/tests/migrations.test.ts @@ -41,7 +41,7 @@ async function setupAndStopDevServerForMigrations(cwd: string, resetDb: boolean } function getPrismaClient(cwd: string) { - const prismaClient = new (requirePrismaClient(cwd))({ + const prismaClient = new (requirePrismaClient(cwd).PrismaClient)({ datasources: { sqlite: { url: dbUrl } }, }); return prismaClient; diff --git a/tests/api-tests/access-control/schema.test.ts b/tests/api-tests/access-control/schema.test.ts index 8775afa4d22..f9b0c6c8d22 100644 --- a/tests/api-tests/access-control/schema.test.ts +++ b/tests/api-tests/access-control/schema.test.ts @@ -41,7 +41,7 @@ class FakePrismaClient { const { getKeystone } = createSystem(initConfig(config)); -const { createContext } = getKeystone(FakePrismaClient); +const { createContext } = getKeystone({ PrismaClient: FakePrismaClient, Prisma: {} as any }); const context = createContext();