From b071d8f75fb905494733e939a94062a865a51d09 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Thu, 19 Mar 2026 07:29:33 -0400 Subject: [PATCH 1/5] feat(orm): add per-model entity mutation hook types --- packages/orm/src/client/plugin.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/packages/orm/src/client/plugin.ts b/packages/orm/src/client/plugin.ts index 2a2431637..d69cda65b 100644 --- a/packages/orm/src/client/plugin.ts +++ b/packages/orm/src/client/plugin.ts @@ -5,6 +5,7 @@ import type { GetModelFields, GetModels, NonRelationFields, SchemaDef } from '@z import type { MaybePromise } from '../utils/type-utils'; import type { MapModelFieldType } from './crud-types'; import type { AllCrudOperations, CoreCrudOperations } from './crud/operations/base'; +import type { DefaultModelResult } from './crud-types'; type AllowedExtQueryArgKeys = CoreCrudOperations | '$create' | '$read' | '$update' | '$delete' | '$all'; @@ -310,6 +311,11 @@ export type EntityMutationHooksDef = { * Defaults to `false`. */ runAfterMutationWithinTransaction?: boolean; +} & { + /** + * Per-model mutation hooks. Register hooks for specific models to get typed entity results. + */ + [M in GetModels]?: ModelEntityMutationHooksDef; }; type MutationHooksArgs = { @@ -381,6 +387,27 @@ export type PluginAfterEntityMutationArgs = MutationHo client: ClientContract; }; +export type PluginModelBeforeEntityMutationArgs> = + Omit, 'model'> & { + model: Model; + loadBeforeMutationEntities(): Promise[] | undefined>; + client: ClientContract; + }; + +export type PluginModelAfterEntityMutationArgs> = + Omit, 'model'> & { + model: Model; + loadAfterMutationEntities(): Promise[] | undefined>; + beforeMutationEntities?: DefaultModelResult[]; + client: ClientContract; + }; + +export type ModelEntityMutationHooksDef> = { + beforeEntityMutation?: (args: PluginModelBeforeEntityMutationArgs) => MaybePromise; + afterEntityMutation?: (args: PluginModelAfterEntityMutationArgs) => MaybePromise; + runAfterMutationWithinTransaction?: boolean; +}; + // #endregion // #region OnKyselyQuery hooks From 709fea750ef0787406c138f5f51468935cffaaa1 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Thu, 19 Mar 2026 07:33:03 -0400 Subject: [PATCH 2/5] feat(orm): dispatch per-model entity mutation hooks at runtime --- .../executor/zenstack-query-executor.ts | 74 ++++++++++++++----- 1 file changed, 56 insertions(+), 18 deletions(-) diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index ed4f6f6b1..92a3742a4 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -123,7 +123,12 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { } private get hasEntityMutationPluginsWithAfterMutationHooks() { - return (this.client.$options.plugins ?? []).some((plugin) => plugin.onEntityMutation?.afterEntityMutation); + return (this.client.$options.plugins ?? []).some((plugin) => { + if (!plugin.onEntityMutation) return false; + if (plugin.onEntityMutation.afterEntityMutation) return true; + const models = Object.keys(this.client.$schema.models); + return models.some((model) => (plugin.onEntityMutation as any)?.[model]?.afterEntityMutation); + }); } private get hasOnKyselyHooks() { @@ -351,18 +356,34 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { if (this.options.plugins) { for (const plugin of this.options.plugins) { const onEntityMutation = plugin.onEntityMutation; - if (!onEntityMutation?.beforeEntityMutation) { + if (!onEntityMutation) { continue; } - await onEntityMutation.beforeEntityMutation({ - model: mutationInfo.model, - action: mutationInfo.action, - queryNode, - loadBeforeMutationEntities, - client, - queryId, - }); + // catch-all hook + if (onEntityMutation.beforeEntityMutation) { + await onEntityMutation.beforeEntityMutation({ + model: mutationInfo.model, + action: mutationInfo.action, + queryNode, + loadBeforeMutationEntities, + client, + queryId, + }); + } + + // per-model hook + const modelHooks = (onEntityMutation as Record)[mutationInfo.model]; + if (modelHooks?.beforeEntityMutation) { + await modelHooks.beforeEntityMutation({ + model: mutationInfo.model, + action: mutationInfo.action, + queryNode, + loadBeforeMutationEntities, + client, + queryId, + }); + } } } } @@ -373,22 +394,39 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { const hooks: AfterEntityMutationCallback[] = []; - // tsc perf for (const plugin of this.options.plugins ?? []) { const onEntityMutation = plugin.onEntityMutation; - if (!onEntityMutation?.afterEntityMutation) { - continue; - } - if (filterFor === 'inTx' && !onEntityMutation.runAfterMutationWithinTransaction) { + if (!onEntityMutation) { continue; } - if (filterFor === 'outTx' && onEntityMutation.runAfterMutationWithinTransaction) { - continue; + // catch-all hook + if (onEntityMutation.afterEntityMutation) { + const runInTx = onEntityMutation.runAfterMutationWithinTransaction ?? false; + if ( + filterFor === 'all' || + (filterFor === 'inTx' && runInTx) || + (filterFor === 'outTx' && !runInTx) + ) { + hooks.push(onEntityMutation.afterEntityMutation.bind(plugin)); + } } - hooks.push(onEntityMutation.afterEntityMutation.bind(plugin)); + // per-model hook + const modelHooks = (onEntityMutation as Record)[mutationInfo.model]; + if (modelHooks?.afterEntityMutation) { + const runInTx = modelHooks.runAfterMutationWithinTransaction + ?? onEntityMutation.runAfterMutationWithinTransaction + ?? false; + if ( + filterFor === 'all' || + (filterFor === 'inTx' && runInTx) || + (filterFor === 'outTx' && !runInTx) + ) { + hooks.push(modelHooks.afterEntityMutation.bind(plugin)); + } + } } if (hooks.length === 0) { From 0ad25a9ae4d561e625cdf49f0662b45dc3988047 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Thu, 19 Mar 2026 07:38:28 -0400 Subject: [PATCH 3/5] test(orm): add tests for per-model entity mutation hooks --- .../entity-mutation-hooks.test.ts | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts index 8ddb35f7e..b7b719fe9 100644 --- a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts @@ -691,6 +691,139 @@ describe('Entity mutation hooks tests', () => { await expect(client.user.findMany()).toResolveWithLength(0); }); }); + + it('can use per-model hooks with typed entities', async () => { + let userBeforeEntities: any; + let postBeforeEntities: any; + let userAfterEntities: any; + let postAfterEntities: any; + + const client = _client.$use({ + id: 'test', + onEntityMutation: { + User: { + async beforeEntityMutation(args) { + expect(args.model).toBe('User'); + userBeforeEntities = await args.loadBeforeMutationEntities(); + }, + async afterEntityMutation(args) { + expect(args.model).toBe('User'); + userAfterEntities = await args.loadAfterMutationEntities(); + }, + }, + Post: { + async beforeEntityMutation(args) { + expect(args.model).toBe('Post'); + postBeforeEntities = await args.loadBeforeMutationEntities(); + }, + async afterEntityMutation(args) { + expect(args.model).toBe('Post'); + postAfterEntities = await args.loadAfterMutationEntities(); + }, + }, + }, + }); + + const user = await client.user.create({ + data: { email: 'u1@test.com' }, + }); + + // User after hook should have fired, Post hooks should not + expect(userAfterEntities).toEqual([expect.objectContaining({ email: 'u1@test.com' })]); + expect(postAfterEntities).toBeUndefined(); + + await client.user.update({ + where: { id: user.id }, + data: { email: 'u2@test.com' }, + }); + + // User before hook should have captured old entity + expect(userBeforeEntities).toEqual([expect.objectContaining({ email: 'u1@test.com' })]); + // User after hook should have captured new entity + expect(userAfterEntities).toEqual([expect.objectContaining({ email: 'u2@test.com' })]); + // Post hooks still untouched + expect(postBeforeEntities).toBeUndefined(); + }); + + it('can combine per-model and catch-all hooks', async () => { + const catchAllModels: string[] = []; + const perModelModels: string[] = []; + + const client = _client.$use({ + id: 'test', + onEntityMutation: { + afterEntityMutation(args) { + catchAllModels.push(args.model); + }, + User: { + afterEntityMutation(args) { + perModelModels.push(args.model); + }, + }, + }, + }); + + await client.user.create({ + data: { email: 'u1@test.com', posts: { create: { title: 'Post1' } } }, + }); + + // catch-all fires for both User and Post + expect(catchAllModels).toContain('User'); + expect(catchAllModels).toContain('Post'); + + // per-model fires only for User + expect(perModelModels).toEqual(['User']); + }); + + it('per-model hooks provide beforeMutationEntities in after hook', async () => { + let beforeEntitiesInAfterHook: any; + + const client = _client.$use({ + id: 'test', + onEntityMutation: { + User: { + async beforeEntityMutation(args) { + await args.loadBeforeMutationEntities(); + }, + async afterEntityMutation(args) { + beforeEntitiesInAfterHook = args.beforeMutationEntities; + }, + }, + }, + }); + + const user = await client.user.create({ + data: { email: 'u1@test.com' }, + }); + await client.user.update({ + where: { id: user.id }, + data: { email: 'u2@test.com' }, + }); + + expect(beforeEntitiesInAfterHook).toEqual([expect.objectContaining({ email: 'u1@test.com' })]); + }); + + it('per-model hooks infer correct entity types', async () => { + const client = _client.$use({ + id: 'test', + onEntityMutation: { + User: { + async afterEntityMutation(args) { + const entities = await args.loadAfterMutationEntities(); + if (entities) { + // This access compiles without casts because entities is User[] + const _email: string = entities[0].email; + expect(_email).toBeTruthy(); + } + }, + }, + }, + }); + + await client.user.create({ + data: { email: 'u1@test.com' }, + }); + }); }); describe('Entity mutation hooks - delegate model interaction', () => { From ce01cebbb5c1e400ec6c75bc63b2dc696784ad23 Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Wed, 22 Jul 2026 10:55:37 -0400 Subject: [PATCH 4/5] test(orm): guard typed hook entity access --- tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts index b7b719fe9..9d9330a49 100644 --- a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts @@ -811,8 +811,13 @@ describe('Entity mutation hooks tests', () => { async afterEntityMutation(args) { const entities = await args.loadAfterMutationEntities(); if (entities) { - // This access compiles without casts because entities is User[] - const _email: string = entities[0].email; + const entity = entities[0]; + expect(entity).toBeDefined(); + if (!entity) { + return; + } + // This access compiles without casts because entity is a User + const _email: string = entity.email; expect(_email).toBeTruthy(); } }, From 18fca8f3b21747e69ea9b0edc60879ed01d01f1a Mon Sep 17 00:00:00 2001 From: Eugen Istoc Date: Wed, 22 Jul 2026 11:08:11 -0400 Subject: [PATCH 5/5] feat(orm)!: move entity mutation catch-all hooks under $all --- BREAKINGCHANGES.md | 1 + .../executor/zenstack-query-executor.ts | 32 +- packages/orm/src/client/plugin.ts | 43 +- .../entity-mutation-hooks.test.ts | 420 ++++++++++-------- tests/regression/test/issue-586.test.ts | 8 +- 5 files changed, 272 insertions(+), 232 deletions(-) diff --git a/BREAKINGCHANGES.md b/BREAKINGCHANGES.md index 96c3d629d..8612ff374 100644 --- a/BREAKINGCHANGES.md +++ b/BREAKINGCHANGES.md @@ -4,3 +4,4 @@ 1. `@omit` and `@password` attributes have been removed 1. SWR plugin is removed 1. `makeModelSchema()` no longer includes relation fields by default — use `include` or `select` options to opt in, mirroring ORM behaviour +1. ORM entity mutation catch-all hooks now need to be registered under `onEntityMutation.$all` diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index 92a3742a4..4628be8fa 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -125,7 +125,7 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { private get hasEntityMutationPluginsWithAfterMutationHooks() { return (this.client.$options.plugins ?? []).some((plugin) => { if (!plugin.onEntityMutation) return false; - if (plugin.onEntityMutation.afterEntityMutation) return true; + if (plugin.onEntityMutation['$all']?.afterEntityMutation) return true; const models = Object.keys(this.client.$schema.models); return models.some((model) => (plugin.onEntityMutation as any)?.[model]?.afterEntityMutation); }); @@ -360,9 +360,9 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { continue; } - // catch-all hook - if (onEntityMutation.beforeEntityMutation) { - await onEntityMutation.beforeEntityMutation({ + // all-model hook + if (onEntityMutation['$all']?.beforeEntityMutation) { + await onEntityMutation['$all'].beforeEntityMutation({ model: mutationInfo.model, action: mutationInfo.action, queryNode, @@ -401,29 +401,19 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { continue; } - // catch-all hook - if (onEntityMutation.afterEntityMutation) { - const runInTx = onEntityMutation.runAfterMutationWithinTransaction ?? false; - if ( - filterFor === 'all' || - (filterFor === 'inTx' && runInTx) || - (filterFor === 'outTx' && !runInTx) - ) { - hooks.push(onEntityMutation.afterEntityMutation.bind(plugin)); + // all-model hook + if (onEntityMutation['$all']?.afterEntityMutation) { + const runInTx = onEntityMutation['$all'].runAfterMutationWithinTransaction ?? false; + if (filterFor === 'all' || (filterFor === 'inTx' && runInTx) || (filterFor === 'outTx' && !runInTx)) { + hooks.push(onEntityMutation['$all'].afterEntityMutation.bind(plugin)); } } // per-model hook const modelHooks = (onEntityMutation as Record)[mutationInfo.model]; if (modelHooks?.afterEntityMutation) { - const runInTx = modelHooks.runAfterMutationWithinTransaction - ?? onEntityMutation.runAfterMutationWithinTransaction - ?? false; - if ( - filterFor === 'all' || - (filterFor === 'inTx' && runInTx) || - (filterFor === 'outTx' && !runInTx) - ) { + const runInTx = modelHooks.runAfterMutationWithinTransaction ?? false; + if (filterFor === 'all' || (filterFor === 'inTx' && runInTx) || (filterFor === 'outTx' && !runInTx)) { hooks.push(modelHooks.afterEntityMutation.bind(plugin)); } } diff --git a/packages/orm/src/client/plugin.ts b/packages/orm/src/client/plugin.ts index d69cda65b..c9f0d791a 100644 --- a/packages/orm/src/client/plugin.ts +++ b/packages/orm/src/client/plugin.ts @@ -288,6 +288,12 @@ type OnQueryHookContext = { // #region OnEntityMutation hooks export type EntityMutationHooksDef = { + [M in '$all' | GetModels]?: M extends '$all' + ? AllEntityMutationHooksDef + : ModelEntityMutationHooksDef>; +}; + +export type AllEntityMutationHooksDef = { /** * Called before entities are mutated. */ @@ -311,11 +317,6 @@ export type EntityMutationHooksDef = { * Defaults to `false`. */ runAfterMutationWithinTransaction?: boolean; -} & { - /** - * Per-model mutation hooks. Register hooks for specific models to get typed entity results. - */ - [M in GetModels]?: ModelEntityMutationHooksDef; }; type MutationHooksArgs = { @@ -380,27 +381,31 @@ export type PluginAfterEntityMutationArgs = MutationHo /** * The ZenStack client you can use to perform additional operations. - * See {@link EntityMutationHooksDef.runAfterMutationWithinTransaction} for detailed transaction behavior. + * See {@link AllEntityMutationHooksDef.runAfterMutationWithinTransaction} for detailed transaction behavior. * * Mutations initiated from this client will NOT trigger entity mutation hooks to avoid infinite loops. */ client: ClientContract; }; -export type PluginModelBeforeEntityMutationArgs> = - Omit, 'model'> & { - model: Model; - loadBeforeMutationEntities(): Promise[] | undefined>; - client: ClientContract; - }; +export type PluginModelBeforeEntityMutationArgs> = Omit< + MutationHooksArgs, + 'model' +> & { + model: Model; + loadBeforeMutationEntities(): Promise[] | undefined>; + client: ClientContract; +}; -export type PluginModelAfterEntityMutationArgs> = - Omit, 'model'> & { - model: Model; - loadAfterMutationEntities(): Promise[] | undefined>; - beforeMutationEntities?: DefaultModelResult[]; - client: ClientContract; - }; +export type PluginModelAfterEntityMutationArgs> = Omit< + MutationHooksArgs, + 'model' +> & { + model: Model; + loadAfterMutationEntities(): Promise[] | undefined>; + beforeMutationEntities?: DefaultModelResult[]; + client: ClientContract; +}; export type ModelEntityMutationHooksDef> = { beforeEntityMutation?: (args: PluginModelBeforeEntityMutationArgs) => MaybePromise; diff --git a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts index 9d9330a49..0bc5514e3 100644 --- a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts @@ -22,20 +22,22 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - beforeEntityMutation(args) { - beforeCalled[args.action] = true; - if (args.action === 'create') { - expect(InsertQueryNode.is(args.queryNode)).toBe(true); - } - if (args.action === 'update') { - expect(UpdateQueryNode.is(args.queryNode)).toBe(true); - } - if (args.action === 'delete') { - expect(DeleteQueryNode.is(args.queryNode)).toBe(true); - } - }, - afterEntityMutation(args) { - afterCalled[args.action] = true; + $all: { + beforeEntityMutation(args) { + beforeCalled[args.action] = true; + if (args.action === 'create') { + expect(InsertQueryNode.is(args.queryNode)).toBe(true); + } + if (args.action === 'update') { + expect(UpdateQueryNode.is(args.queryNode)).toBe(true); + } + if (args.action === 'delete') { + expect(DeleteQueryNode.is(args.queryNode)).toBe(true); + } + }, + afterEntityMutation(args) { + afterCalled[args.action] = true; + }, }, }, }); @@ -72,24 +74,26 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async beforeEntityMutation(args) { - if (args.action === 'update' || args.action === 'delete') { - await expect(args.loadBeforeMutationEntities()).resolves.toEqual([ - expect.objectContaining({ - email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', - }), - ]); - queryIds[args.action].before = args.queryId.queryId; - } - }, - async afterEntityMutation(args) { - if (args.action === 'update' || args.action === 'delete') { - queryIds[args.action].after = args.queryId.queryId; - } - - if (args.action === 'update') { - beforeMutationEntitiesInAfterHooks = args.beforeMutationEntities; - } + $all: { + async beforeEntityMutation(args) { + if (args.action === 'update' || args.action === 'delete') { + await expect(args.loadBeforeMutationEntities()).resolves.toEqual([ + expect.objectContaining({ + email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', + }), + ]); + queryIds[args.action].before = args.queryId.queryId; + } + }, + async afterEntityMutation(args) { + if (args.action === 'update' || args.action === 'delete') { + queryIds[args.action].after = args.queryId.queryId; + } + + if (args.action === 'update') { + beforeMutationEntitiesInAfterHooks = args.beforeMutationEntities; + } + }, }, }, }); @@ -122,22 +126,24 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async afterEntityMutation(args) { - if (args.action === 'create' || args.action === 'update') { - if (args.action === 'create') { - userCreateIntercepted = true; - } - if (args.action === 'update') { - userUpdateIntercepted = true; + $all: { + async afterEntityMutation(args) { + if (args.action === 'create' || args.action === 'update') { + if (args.action === 'create') { + userCreateIntercepted = true; + } + if (args.action === 'update') { + userUpdateIntercepted = true; + } + await expect(args.loadAfterMutationEntities()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: args.action === 'create' ? 'u1@test.com' : 'u2@test.com', + }), + ]), + ); } - await expect(args.loadAfterMutationEntities()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - email: args.action === 'create' ? 'u1@test.com' : 'u2@test.com', - }), - ]), - ); - } + }, }, }, }); @@ -162,38 +168,40 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async afterEntityMutation(args) { - if (args.action === 'create') { - userCreateIntercepted = true; - await expect(args.loadAfterMutationEntities()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ email: 'u1@test.com' }), - expect.objectContaining({ email: 'u2@test.com' }), - ]), - ); - } else if (args.action === 'update') { - userUpdateIntercepted = true; - await expect(args.loadAfterMutationEntities()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ - email: 'u1@test.com', - name: 'A user', - }), - expect.objectContaining({ - email: 'u2@test.com', - name: 'A user', - }), - ]), - ); - } else if (args.action === 'delete') { - userDeleteIntercepted = true; - await expect(args.loadAfterMutationEntities()).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ email: 'u1@test.com' }), - expect.objectContaining({ email: 'u2@test.com' }), - ]), - ); - } + $all: { + async afterEntityMutation(args) { + if (args.action === 'create') { + userCreateIntercepted = true; + await expect(args.loadAfterMutationEntities()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'u1@test.com' }), + expect.objectContaining({ email: 'u2@test.com' }), + ]), + ); + } else if (args.action === 'update') { + userUpdateIntercepted = true; + await expect(args.loadAfterMutationEntities()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: 'u1@test.com', + name: 'A user', + }), + expect.objectContaining({ + email: 'u2@test.com', + name: 'A user', + }), + ]), + ); + } else if (args.action === 'delete') { + userDeleteIntercepted = true; + await expect(args.loadAfterMutationEntities()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'u1@test.com' }), + expect.objectContaining({ email: 'u2@test.com' }), + ]), + ); + } + }, }, }, }); @@ -216,18 +224,20 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async afterEntityMutation(args) { - if (args.action === 'create') { - if (args.model === 'Post') { - const afterEntities = await args.loadAfterMutationEntities(); - if ((afterEntities![0] as any).title === 'Post1') { - post1Intercepted = true; - } - if ((afterEntities![0] as any).title === 'Post2') { - post2Intercepted = true; + $all: { + async afterEntityMutation(args) { + if (args.action === 'create') { + if (args.model === 'Post') { + const afterEntities = await args.loadAfterMutationEntities(); + if ((afterEntities![0] as any).title === 'Post1') { + post1Intercepted = true; + } + if ((afterEntities![0] as any).title === 'Post2') { + post2Intercepted = true; + } } } - } + }, }, }, }); @@ -256,12 +266,14 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async afterEntityMutation(args) { - triggered.push({ - action: args.action, - model: args.model, - afterMutationEntities: await args.loadAfterMutationEntities(), - }); + $all: { + async afterEntityMutation(args) { + triggered.push({ + action: args.action, + model: args.model, + afterMutationEntities: await args.loadAfterMutationEntities(), + }); + }, }, }, }); @@ -303,17 +315,19 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async beforeEntityMutation(ctx) { - await ctx.client.profile.create({ - data: { bio: 'Bio1' }, - }); - }, - async afterEntityMutation(ctx) { - intercepted = true; - await ctx.client.user.update({ - where: { email: 'u1@test.com' }, - data: { email: 'u2@test.com' }, - }); + $all: { + async beforeEntityMutation(ctx) { + await ctx.client.profile.create({ + data: { bio: 'Bio1' }, + }); + }, + async afterEntityMutation(ctx) { + intercepted = true; + await ctx.client.user.update({ + where: { email: 'u1@test.com' }, + data: { email: 'u2@test.com' }, + }); + }, }, }, }); @@ -333,18 +347,20 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - runAfterMutationWithinTransaction: true, - async beforeEntityMutation(ctx) { - await ctx.client.profile.create({ - data: { bio: 'Bio1' }, - }); - }, - async afterEntityMutation(ctx) { - intercepted = true; - await ctx.client.user.update({ - where: { email: 'u1@test.com' }, - data: { email: 'u2@test.com' }, - }); + $all: { + runAfterMutationWithinTransaction: true, + async beforeEntityMutation(ctx) { + await ctx.client.profile.create({ + data: { bio: 'Bio1' }, + }); + }, + async afterEntityMutation(ctx) { + intercepted = true; + await ctx.client.user.update({ + where: { email: 'u1@test.com' }, + data: { email: 'u2@test.com' }, + }); + }, }, }, }); @@ -362,8 +378,10 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async beforeEntityMutation() { - throw new Error('trigger failure'); + $all: { + async beforeEntityMutation() { + throw new Error('trigger failure'); + }, }, }, }); @@ -384,9 +402,11 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async afterEntityMutation() { - intercepted = true; - throw new Error('trigger rollback'); + $all: { + async afterEntityMutation() { + intercepted = true; + throw new Error('trigger rollback'); + }, }, }, }); @@ -406,11 +426,13 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - runAfterMutationWithinTransaction: true, - async afterEntityMutation(ctx) { - intercepted = true; - await ctx.client.user.create({ data: { email: 'u2@test.com' } }); - throw new Error('trigger rollback'); + $all: { + runAfterMutationWithinTransaction: true, + async afterEntityMutation(ctx) { + intercepted = true; + await ctx.client.user.create({ data: { email: 'u2@test.com' } }); + throw new Error('trigger rollback'); + }, }, }, }); @@ -432,9 +454,11 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async afterEntityMutation(ctx) { - intercepted = true; - await ctx.client.user.create({ data: { email: 'u2@test.com' } }); + $all: { + async afterEntityMutation(ctx) { + intercepted = true; + await ctx.client.user.create({ data: { email: 'u2@test.com' } }); + }, }, }, }); @@ -461,10 +485,12 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - runAfterMutationWithinTransaction: true, - async afterEntityMutation(ctx) { - intercepted = true; - await ctx.client.user.create({ data: { email: 'u2@test.com' } }); + $all: { + runAfterMutationWithinTransaction: true, + async afterEntityMutation(ctx) { + intercepted = true; + await ctx.client.user.create({ data: { email: 'u2@test.com' } }); + }, }, }, }); @@ -492,13 +518,15 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async beforeEntityMutation(args) { - if (args.action === 'update') { - intercepted = true; - await expect(args.loadBeforeMutationEntities()).resolves.toEqual([ - expect.objectContaining({ email: 'u1@test.com' }), - ]); - } + $all: { + async beforeEntityMutation(args) { + if (args.action === 'update') { + intercepted = true; + await expect(args.loadBeforeMutationEntities()).resolves.toEqual([ + expect.objectContaining({ email: 'u1@test.com' }), + ]); + } + }, }, }, }); @@ -519,11 +547,13 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async beforeEntityMutation(ctx) { - intercepted = true; - await ctx.client.profile.create({ - data: { bio: 'Bio1' }, - }); + $all: { + async beforeEntityMutation(ctx) { + intercepted = true; + await ctx.client.profile.create({ + data: { bio: 'Bio1' }, + }); + }, }, }, }); @@ -549,27 +579,29 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async beforeEntityMutation(ctx) { - const r = await ctx.client.user.findUnique({ where: { email: 'u1@test.com' } }); - if (r) { - // second create - txVisible = true; - } else { - // first create - await ctx.client.profile.create({ - data: { bio: 'Bio1' }, + $all: { + async beforeEntityMutation(ctx) { + const r = await ctx.client.user.findUnique({ where: { email: 'u1@test.com' } }); + if (r) { + // second create + txVisible = true; + } else { + // first create + await ctx.client.profile.create({ + data: { bio: 'Bio1' }, + }); + } + }, + async afterEntityMutation(ctx) { + if (intercepted) { + return; + } + intercepted = true; + await ctx.client.user.update({ + where: { email: 'u1@test.com' }, + data: { email: 'u3@test.com' }, }); - } - }, - async afterEntityMutation(ctx) { - if (intercepted) { - return; - } - intercepted = true; - await ctx.client.user.update({ - where: { email: 'u1@test.com' }, - data: { email: 'u3@test.com' }, - }); + }, }, }, }); @@ -602,16 +634,18 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - runAfterMutationWithinTransaction: true, - async afterEntityMutation(ctx) { - if (intercepted) { - return; - } - intercepted = true; - await ctx.client.user.update({ - where: { email: 'u1@test.com' }, - data: { email: 'u3@test.com' }, - }); + $all: { + runAfterMutationWithinTransaction: true, + async afterEntityMutation(ctx) { + if (intercepted) { + return; + } + intercepted = true; + await ctx.client.user.update({ + where: { email: 'u1@test.com' }, + data: { email: 'u3@test.com' }, + }); + }, }, }, }); @@ -642,10 +676,12 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - async afterEntityMutation(ctx) { - intercepted = true; - await ctx.client.user.create({ data: { email: 'u2@test.com' } }); - throw new Error('trigger error'); + $all: { + async afterEntityMutation(ctx) { + intercepted = true; + await ctx.client.user.create({ data: { email: 'u2@test.com' } }); + throw new Error('trigger error'); + }, }, }, }); @@ -668,11 +704,13 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - runAfterMutationWithinTransaction: true, - async afterEntityMutation(ctx) { - intercepted = true; - await ctx.client.user.create({ data: { email: 'u2@test.com' } }); - throw new Error('trigger error'); + $all: { + runAfterMutationWithinTransaction: true, + async afterEntityMutation(ctx) { + intercepted = true; + await ctx.client.user.create({ data: { email: 'u2@test.com' } }); + throw new Error('trigger error'); + }, }, }, }); @@ -752,8 +790,10 @@ describe('Entity mutation hooks tests', () => { const client = _client.$use({ id: 'test', onEntityMutation: { - afterEntityMutation(args) { - catchAllModels.push(args.model); + $all: { + afterEntityMutation(args) { + catchAllModels.push(args.model); + }, }, User: { afterEntityMutation(args) { @@ -851,7 +891,9 @@ model Child extends Base { const withPlugin = client.$use({ id: 'test', onEntityMutation: { - afterEntityMutation() {}, + $all: { + afterEntityMutation() {}, + }, }, }); diff --git a/tests/regression/test/issue-586.test.ts b/tests/regression/test/issue-586.test.ts index 0c1244728..3a69d3590 100644 --- a/tests/regression/test/issue-586.test.ts +++ b/tests/regression/test/issue-586.test.ts @@ -46,9 +46,11 @@ describe('Regression for issue #586', () => { name: 'foo', description: 'foo', onEntityMutation: { - afterEntityMutation: async () => Promise.resolve(), - beforeEntityMutation: async () => Promise.resolve(), - runAfterMutationWithinTransaction: true, + $all: { + afterEntityMutation: async () => Promise.resolve(), + beforeEntityMutation: async () => Promise.resolve(), + runAfterMutationWithinTransaction: true, + }, }, }, ],