keystonejs / keystone

The most powerful headless CMS for Node.js — built with GraphQL and React
https://keystonejs.com
MIT License
8.98k stars 1.13k forks source link

feat(core/db): allow to extend PrismaClient #9114

Closed iamandrewluca closed 2 months ago

iamandrewluca commented 2 months ago

Using this feature, one can extend PrismaClient, and add for example hooks at prisma level

export default withAuth(
  config<TypeInfo>({
    db: {
      extendPrismaClient(client: PrismaClient) {
        return client.$extends({
          query: {
            async $allOperations({ operation, model, args, query }) {
              // Raw Query is executed
              if (model === undefined) {
                const result: unknown = await query(args);
                return result;
              }

              const hooks = getPrismaHooks(model, operation);

              await hooks?.before?.(client, args);
              const result: unknown = await query(args);
              await hooks?.after?.(client, args, result);

              return result;
            },
          },
        });
      },
    },
  }),
);
codesandbox-ci[bot] commented 2 months ago

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit e842f7d36c3fcc63a9e6aea1732629a9bb919b07:

Sandbox Source
@keystone-6/sandbox Configuration
gautamsi commented 2 months ago

I was looking to integrate @prisma/extension-read-replicas with this yesterday, right on time! hope this gets into the release.

iamandrewluca commented 2 months ago

We are already using this with a patch on top of KS

dcousens commented 2 months ago

LGTM, doesn't resolve the issues raised by https://github.com/keystonejs/keystone/pull/8847, but I think we'll look at that another day - and there exists a workaround for that

dcousens commented 2 months ago

Thanks @iamandrewluca! I'll try and release this asap

iamandrewluca commented 2 months ago

Thanks! That is great!

gautamsi commented 2 months ago

@iamandrewluca can you share the code of getPrismaHooks or is this exported by prisma?

iamandrewluca commented 2 months ago

I have a big object mapper for all models/operations/lifecycles, and getPrismaHooks returns me the functions based on model/operation

import { type PrismaClient, type Prisma } from '@prisma/client';
import type { TypeInfo } from '.keystone/types';

type PrismaHook = (
    prismaClient: PrismaClient,
    args: unknown,
) => Promise<void> | Promise<unknown>;

type PrismaExtendedOperation = {
    [ListType in keyof TypeInfo['lists']]?: {
        [OperationType in Prisma.DMMF.ModelAction]?: Partial<
            Record<'before' | 'after', PrismaHook>
        >;
    };
};

const PrismaHooks: PrismaExtendedOperation = {
    Order: {
        create: {
            before: async (prismaClient: PrismaClient, args: unknown) => {
                // ...
            },
        },
        update: {
            before: async (prismaClient: PrismaClient, args: unknown) => {
                // ...
            },
        },
    },
};

export function getPrismaHooks(
    model: keyof TypeInfo['lists'],
    operation: string,
): Partial<Record<'before' | 'after', PrismaHook>> | undefined {
    const modelHooks = PrismaHooks[model];
    return modelHooks?.[operation as Prisma.DMMF.ModelAction];
}
dcousens commented 2 months ago

@iamandrewluca out of interest, why use these by comparison to the Keystone hooks?

iamandrewluca commented 2 months ago

@dcousens, we had to create custom GQL mutations that use Prisma transactions, and doing that KS hooks did not work.