Skip to content

Commit

Permalink
Fix for Json nulls
Browse files Browse the repository at this point in the history
  • Loading branch information
emmatown committed Jul 27, 2022
1 parent f0ddbc9 commit 0a68e82
Show file tree
Hide file tree
Showing 11 changed files with 95 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-sloths-bake.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@keystone-6/core': major
---

Updates to Prisma 4.1.0
5 changes: 5 additions & 0 deletions .changeset/slow-games-greet.md
Original file line number Diff line number Diff line change
@@ -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
11 changes: 9 additions & 2 deletions packages/core/src/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
}
14 changes: 3 additions & 11 deletions packages/core/src/fields/types/json/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
{
Expand All @@ -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'),
Expand All @@ -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,
}
);
Expand Down
Empty file removed packages/core/src/globals.d.ts
Empty file.
28 changes: 23 additions & 5 deletions packages/core/src/lib/core/mutations/create-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IdType,
runWithPrisma,
getWriteLimit,
getPrismaNamespace,
} from '../utils';
import { InputFilter, resolveUniqueWhereInput, UniqueInputFilter } from '../where-inputs';
import {
Expand Down Expand Up @@ -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(
Expand All @@ -381,19 +382,36 @@ async function resolveInputForCreateOrUpdate(
};
}

function flattenMultiDbFields(
function transformInnerDBField(
dbField: Exclude<ResolvedDBField, { kind: 'multi' }>,
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<string, { dbField: ResolvedDBField }>,
data: Record<string, any>
data: Record<string, any>,
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)]];
})
);
}
25 changes: 21 additions & 4 deletions packages/core/src/lib/core/utils.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<object, Limit>();

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<object, PrismaModule['Prisma']>();

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;
}
8 changes: 5 additions & 3 deletions packages/core/src/lib/createSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
34 changes: 22 additions & 12 deletions packages/core/src/scripts/run/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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<void>;

Expand All @@ -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');
Expand All @@ -339,7 +348,7 @@ async function setupInitialKeystone(
const { apolloServer, expressServer } = await createExpressServer(
config,
graphQLSchema,
createContext
keystone.createContext
);
console.log(`✅ GraphQL API ready`);

Expand All @@ -357,8 +366,9 @@ async function setupInitialKeystone(
expressServer,
apolloServer,
graphQLSchema,
createContext,
createContext: keystone.createContext,
prismaSchema,
prismaClientModule,
};
}

Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/scripts/tests/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/api-tests/access-control/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down

0 comments on commit 0a68e82

Please sign in to comment.