diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts index 7b82959e..768f65ae 100644 --- a/packages/runtime/src/client/executor/zenstack-query-executor.ts +++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts @@ -26,31 +26,21 @@ import type { GetModels, SchemaDef } from '../../schema'; import { type ClientImpl } from '../client-impl'; import { TransactionIsolationLevel, type ClientContract } from '../contract'; import { InternalError, QueryError } from '../errors'; -import type { - AfterEntityMutationCallback, - MutationInterceptionFilterResult, - OnKyselyQueryCallback, - RuntimePlugin, -} from '../plugin'; +import type { AfterEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; import { stripAlias } from './kysely-utils'; import { QueryNameMapper } from './name-mapper'; import type { ZenStackDriver } from './zenstack-driver'; type QueryId = { queryId: string }; -type MutationInterceptionInfo = Pick< - MutationInterceptionFilterResult, - 'loadBeforeMutationEntities' | 'loadAfterMutationEntities' -> & { +type MutationQueryNode = InsertQueryNode | UpdateQueryNode | DeleteQueryNode; + +type MutationInfo = { + model: GetModels; action: 'create' | 'update' | 'delete'; where: WhereNode | undefined; - beforeMutationEntities: Record[] | undefined; - mutationModel: GetModels; - perPlugin: Map, MutationInterceptionFilterResult>; }; -type MutationQueryNode = InsertQueryNode | UpdateQueryNode | DeleteQueryNode; - export class ZenStackQueryExecutor extends DefaultQueryExecutor { private readonly nameMapper: QueryNameMapper; @@ -75,11 +65,11 @@ export class ZenStackQueryExecutor extends DefaultQuer return this.client.$options; } - override async executeQuery(compiledQuery: CompiledQuery, _queryId: QueryId) { + override async executeQuery(compiledQuery: CompiledQuery, queryId: QueryId) { // proceed with the query with kysely interceptors // if the query is a raw query, we need to carry over the parameters const queryParams = (compiledQuery as any).$raw ? compiledQuery.parameters : undefined; - const result = await this.proceedQueryWithKyselyInterceptors(compiledQuery.query, queryParams); + const result = await this.proceedQueryWithKyselyInterceptors(compiledQuery.query, queryParams, queryId.queryId); return result.result; } @@ -87,8 +77,9 @@ export class ZenStackQueryExecutor extends DefaultQuer private async proceedQueryWithKyselyInterceptors( queryNode: RootOperationNode, parameters: readonly unknown[] | undefined, + queryId: string, ) { - let proceed = (q: RootOperationNode) => this.proceedQuery(q, parameters); + let proceed = (q: RootOperationNode) => this.proceedQuery(q, parameters, queryId); const hooks: OnKyselyQueryCallback[] = []; // tsc perf @@ -122,13 +113,33 @@ export class ZenStackQueryExecutor extends DefaultQuer return result; } - private async proceedQuery(query: RootOperationNode, parameters: readonly unknown[] | undefined) { + private getMutationInfo(queryNode: MutationQueryNode): MutationInfo { + const model = this.getMutationModel(queryNode); + const { action, where } = match(queryNode) + .when(InsertQueryNode.is, () => ({ + action: 'create' as const, + where: undefined, + })) + .when(UpdateQueryNode.is, (node) => ({ + action: 'update' as const, + where: node.where, + })) + .when(DeleteQueryNode.is, (node) => ({ + action: 'delete' as const, + where: node.where, + })) + .exhaustive(); + + return { model, action, where }; + } + + private async proceedQuery(query: RootOperationNode, parameters: readonly unknown[] | undefined, queryId: string) { let compiled: CompiledQuery | undefined; try { return await this.provideConnection(async (connection) => { if (this.suppressMutationHooks || !this.isMutationNode(query) || !this.hasEntityMutationPlugins) { - // non-mutation query or hooks suppressed, just proceed + // no need to handle mutation hooks, just proceed const finalQuery = this.nameMapper.transformNode(query); compiled = this.compileQuery(finalQuery); if (parameters) { @@ -138,14 +149,12 @@ export class ZenStackQueryExecutor extends DefaultQuer return { result }; } - const mutationInterceptionInfo = await this.callMutationInterceptionFilters(query, connection); - if ( (InsertQueryNode.is(query) || UpdateQueryNode.is(query)) && - mutationInterceptionInfo.loadAfterMutationEntities + this.hasEntityMutationPluginsWithAfterMutationHooks ) { - // need to make sure the query node has "returnAll" - // for insert and update queries + // need to make sure the query node has "returnAll" for insert and update queries + // so that after-mutation hooks can get the mutated entities with all fields query = { ...query, returning: ReturningNode.create([SelectionNode.createSelectAll()]), @@ -163,39 +172,62 @@ export class ZenStackQueryExecutor extends DefaultQuer const connectionClient = this.createClientForConnection(connection, currentlyInTx); + const mutationInfo = this.getMutationInfo(finalQuery); + + // cache already loaded before-mutation entities + let beforeMutationEntities: Record[] | undefined; + const loadBeforeMutationEntities = async () => { + if ( + beforeMutationEntities === undefined && + (UpdateQueryNode.is(query) || DeleteQueryNode.is(query)) + ) { + beforeMutationEntities = await this.loadEntities( + mutationInfo.model, + mutationInfo.where, + connection, + ); + } + return beforeMutationEntities; + }; + // call before mutation hooks - await this.callBeforeMutationHooks(finalQuery, mutationInterceptionInfo!, connectionClient); + await this.callBeforeMutationHooks( + finalQuery, + mutationInfo, + loadBeforeMutationEntities, + connectionClient, + queryId, + ); // if mutation interceptor demands to run afterMutation hook in the transaction but we're not already // inside one, we need to create one on the fly const shouldCreateTx = - mutationInterceptionInfo && - this.hasPluginRequestingAfterMutationWithinTransaction(mutationInterceptionInfo) && + this.hasPluginRequestingAfterMutationWithinTransaction && !this.driver.isTransactionConnection(connection); if (!shouldCreateTx) { // if no on-the-fly tx is needed, just proceed with the query as is const result = await connection.executeQuery(compiled); - invariant(mutationInterceptionInfo); - if (!this.driver.isTransactionConnection(connection)) { // not in a transaction, just call all after-mutation hooks await this.callAfterMutationHooks( result, finalQuery, - mutationInterceptionInfo, + mutationInfo, connectionClient, 'all', + queryId, ); } else { // run after-mutation hooks that are requested to be run inside tx await this.callAfterMutationHooks( result, finalQuery, - mutationInterceptionInfo, + mutationInfo, connectionClient, 'inTx', + queryId, ); // register other after-mutation hooks to be run after the tx is committed @@ -203,9 +235,10 @@ export class ZenStackQueryExecutor extends DefaultQuer this.callAfterMutationHooks( result, finalQuery, - mutationInterceptionInfo, + mutationInfo, connectionClient, 'outTx', + queryId, ), ); } @@ -224,9 +257,10 @@ export class ZenStackQueryExecutor extends DefaultQuer await this.callAfterMutationHooks( result, finalQuery, - mutationInterceptionInfo, + mutationInfo, connectionClient, 'inTx', + queryId, ); // commit the transaction @@ -236,9 +270,10 @@ export class ZenStackQueryExecutor extends DefaultQuer await this.callAfterMutationHooks( result, finalQuery, - mutationInterceptionInfo, + mutationInfo, connectionClient, 'outTx', + queryId, ); return { result }; @@ -269,11 +304,13 @@ export class ZenStackQueryExecutor extends DefaultQuer return (this.client.$options.plugins ?? []).some((plugin) => plugin.onEntityMutation); } - private hasPluginRequestingAfterMutationWithinTransaction( - mutationInterceptionInfo: MutationInterceptionInfo, - ) { - return [...mutationInterceptionInfo.perPlugin.values()].some( - (info) => info.intercept && info.runAfterMutationWithinTransaction, + private get hasEntityMutationPluginsWithAfterMutationHooks() { + return (this.client.$options.plugins ?? []).some((plugin) => plugin.onEntityMutation?.afterEntityMutation); + } + + private get hasPluginRequestingAfterMutationWithinTransaction() { + return (this.client.$options.plugins ?? []).some( + (plugin) => plugin.onEntityMutation?.runAfterMutationWithinTransaction, ); } @@ -367,104 +404,28 @@ export class ZenStackQueryExecutor extends DefaultQuer }) as GetModels; } - private async callMutationInterceptionFilters( - queryNode: UpdateQueryNode | InsertQueryNode | DeleteQueryNode, - connection: DatabaseConnection, - ): Promise> { - const mutationModel = this.getMutationModel(queryNode); - const { action, where } = match(queryNode) - .when(InsertQueryNode.is, () => ({ - action: 'create' as const, - where: undefined, - })) - .when(UpdateQueryNode.is, (node) => ({ - action: 'update' as const, - where: node.where, - })) - .when(DeleteQueryNode.is, (node) => ({ - action: 'delete' as const, - where: node.where, - })) - .exhaustive(); - - const plugins = this.client.$options.plugins; - const perPlugin = new Map, MutationInterceptionFilterResult>(); - if (plugins) { - const mergedResult: Pick< - MutationInterceptionFilterResult, - 'loadBeforeMutationEntities' | 'loadAfterMutationEntities' - > = {}; - - for (const plugin of plugins) { - const onEntityMutation = plugin.onEntityMutation; - if (!onEntityMutation) { - continue; - } - - if (!onEntityMutation.mutationInterceptionFilter) { - // by default intercept without loading entities - perPlugin.set(plugin, { intercept: true }); - } else { - const filterResult = await onEntityMutation.mutationInterceptionFilter({ - model: mutationModel, - action, - queryNode, - }); - mergedResult.loadBeforeMutationEntities ||= filterResult.loadBeforeMutationEntities; - mergedResult.loadAfterMutationEntities ||= filterResult.loadAfterMutationEntities; - perPlugin.set(plugin, filterResult); - } - } - - let beforeMutationEntities: Record[] | undefined; - if ( - mergedResult.loadBeforeMutationEntities && - (UpdateQueryNode.is(queryNode) || DeleteQueryNode.is(queryNode)) - ) { - beforeMutationEntities = await this.loadEntities(mutationModel, where, connection); - } - - return { - ...mergedResult, - mutationModel, - action, - where, - beforeMutationEntities, - perPlugin, - }; - } else { - return { - mutationModel, - action, - where, - beforeMutationEntities: undefined, - perPlugin, - }; - } - } - private async callBeforeMutationHooks( queryNode: OperationNode, - mutationInterceptionInfo: MutationInterceptionInfo, + mutationInfo: MutationInfo, + loadBeforeMutationEntities: () => Promise[] | undefined>, client: ClientContract, + queryId: string, ) { if (this.options.plugins) { - const mutationModel = this.getMutationModel(queryNode); for (const plugin of this.options.plugins) { - const info = mutationInterceptionInfo.perPlugin.get(plugin); - if (!info?.intercept) { - continue; - } const onEntityMutation = plugin.onEntityMutation; - if (onEntityMutation?.beforeEntityMutation) { - await onEntityMutation.beforeEntityMutation({ - model: mutationModel, - action: mutationInterceptionInfo.action, - queryNode, - entities: mutationInterceptionInfo.beforeMutationEntities, - client, - }); + if (!onEntityMutation?.beforeEntityMutation) { + continue; } + + await onEntityMutation.beforeEntityMutation({ + model: mutationInfo.model, + action: mutationInfo.action, + queryNode, + loadBeforeMutationEntities, + client, + queryId, + }); } } } @@ -472,31 +433,29 @@ export class ZenStackQueryExecutor extends DefaultQuer private async callAfterMutationHooks( queryResult: QueryResult, queryNode: OperationNode, - mutationInterceptionInfo: MutationInterceptionInfo, + mutationInfo: MutationInfo, client: ClientContract, filterFor: 'inTx' | 'outTx' | 'all', + queryId: string, ) { const hooks: AfterEntityMutationCallback[] = []; // tsc perf for (const plugin of this.options.plugins ?? []) { - const info = mutationInterceptionInfo.perPlugin.get(plugin); - if (!info?.intercept) { + const onEntityMutation = plugin.onEntityMutation; + + if (!onEntityMutation?.afterEntityMutation) { continue; } - - if (filterFor === 'inTx' && !info.runAfterMutationWithinTransaction) { + if (filterFor === 'inTx' && !onEntityMutation.runAfterMutationWithinTransaction) { continue; } - if (filterFor === 'outTx' && info.runAfterMutationWithinTransaction) { + if (filterFor === 'outTx' && onEntityMutation.runAfterMutationWithinTransaction) { continue; } - const onEntityMutation = plugin.onEntityMutation; - if (onEntityMutation?.afterEntityMutation) { - hooks.push(onEntityMutation.afterEntityMutation.bind(plugin)); - } + hooks.push(onEntityMutation.afterEntityMutation.bind(plugin)); } if (hooks.length === 0) { @@ -505,21 +464,22 @@ export class ZenStackQueryExecutor extends DefaultQuer const mutationModel = this.getMutationModel(queryNode); - for (const hook of hooks) { - let afterMutationEntities: Record[] | undefined = undefined; - if (mutationInterceptionInfo.loadAfterMutationEntities) { - if (InsertQueryNode.is(queryNode) || UpdateQueryNode.is(queryNode)) { - afterMutationEntities = queryResult.rows as Record[]; - } + const loadAfterMutationEntities = async () => { + if (mutationInfo.action === 'delete') { + return undefined; + } else { + return queryResult.rows as Record[]; } + }; + for (const hook of hooks) { await hook({ model: mutationModel, - action: mutationInterceptionInfo.action, + action: mutationInfo.action, queryNode, - beforeMutationEntities: mutationInterceptionInfo.beforeMutationEntities, - afterMutationEntities, + loadAfterMutationEntities, client, + queryId, }); } } diff --git a/packages/runtime/src/client/plugin.ts b/packages/runtime/src/client/plugin.ts index 69218be5..0a4c4a7f 100644 --- a/packages/runtime/src/client/plugin.ts +++ b/packages/runtime/src/client/plugin.ts @@ -88,26 +88,28 @@ type OnQueryHookContext = { export type EntityMutationHooksDef = { /** - * This callback determines whether a mutation should be intercepted, and if so, - * what data should be loaded before and after the mutation. + * Called before entities are mutated. */ - mutationInterceptionFilter?: MutationInterceptionFilter; + beforeEntityMutation?: BeforeEntityMutationCallback; /** - * Called before an entity is mutated. - * @param args.entity Only available if `loadBeforeMutationEntities` is set to true in the - * return value of {@link RuntimePlugin.mutationInterceptionFilter}. + * Called after entities are mutated. */ - beforeEntityMutation?: BeforeEntityMutationCallback; + afterEntityMutation?: AfterEntityMutationCallback; /** - * Called after an entity is mutated. - * @param args.beforeMutationEntity Only available if `loadBeforeMutationEntities` is set to true in the - * return value of {@link RuntimePlugin.mutationInterceptionFilter}. - * @param args.afterMutationEntity Only available if `loadAfterMutationEntities` is set to true in the - * return value of {@link RuntimePlugin.mutationInterceptionFilter}. + * Whether to run after-mutation hooks within the transaction that performs the mutation. + * + * If set to `true`, if the mutation already runs inside a transaction, the callbacks are + * executed immediately after the mutation within the transaction boundary. If the mutation + * is not running inside a transaction, a new transaction is created to run both the mutation + * and the callbacks. + * + * If set to `false`, the callbacks are executed after the mutation transaction is committed. + * + * Defaults to `false`. */ - afterEntityMutation?: AfterEntityMutationCallback; + runAfterMutationWithinTransaction?: boolean; }; type MutationHooksArgs = { @@ -125,44 +127,12 @@ type MutationHooksArgs = { * The mutation data. Only available for create and update actions. */ queryNode: OperationNode; -}; - -export type MutationInterceptionFilter = ( - args: MutationHooksArgs, -) => MaybePromise; - -/** - * The result of the hooks interception filter. - */ -export type MutationInterceptionFilterResult = { - /** - * Whether to intercept the mutation or not. - */ - intercept: boolean; - - /** - * Whether entities should be loaded before the mutation. - */ - loadBeforeMutationEntities?: boolean; /** - * Whether entities should be loaded after the mutation. + * A query ID that uniquely identifies the mutation operation. You can use it to correlate + * data between the before and after mutation hooks. */ - loadAfterMutationEntities?: boolean; - - /** - * Whether to run after-mutation hooks within the transaction that performs the mutation. - * - * If set to `true`, if the mutation already runs inside a transaction, the callbacks are - * executed immediately after the mutation within the transaction boundary. If the mutation - * is not running inside a transaction, a new transaction is created to run both the mutation - * and the callbacks. - * - * If set to `false`, the callbacks are executed after the mutation transaction is committed. - * - * Defaults to `false`. - */ - runAfterMutationWithinTransaction?: boolean; + queryId: string; }; export type BeforeEntityMutationCallback = ( @@ -175,10 +145,10 @@ export type AfterEntityMutationCallback = ( export type PluginBeforeEntityMutationArgs = MutationHooksArgs & { /** - * Entities that are about to be mutated. Only available if `loadBeforeMutationEntities` is set to - * true in the return value of {@link RuntimePlugin.mutationInterceptionFilter}. + * Loads the entities that are about to be mutated. The db operation that loads the entities is executed + * within the same transaction context as the mutation. */ - entities?: unknown[]; + loadBeforeMutationEntities(): Promise[] | undefined>; /** * The ZenStack client you can use to perform additional operations. The database operations initiated @@ -192,20 +162,13 @@ export type PluginBeforeEntityMutationArgs = MutationH export type PluginAfterEntityMutationArgs = MutationHooksArgs & { /** - * Entities that are about to be mutated. Only available if `loadBeforeMutationEntities` is set to - * true in the return value of {@link RuntimePlugin.mutationInterceptionFilter}. - */ - beforeMutationEntities?: unknown[]; - - /** - * Entities mutated. Only available if `loadAfterMutationEntities` is set to true in the return - * value of {@link RuntimePlugin.mutationInterceptionFilter}. + * Loads the entities that have been mutated. */ - afterMutationEntities?: unknown[]; + loadAfterMutationEntities(): Promise[] | undefined>; /** * The ZenStack client you can use to perform additional operations. - * See {@link MutationInterceptionFilterResult.runAfterMutationWithinTransaction} for detailed transaction behavior. + * See {@link EntityMutationHooksDef.runAfterMutationWithinTransaction} for detailed transaction behavior. * * Mutations initiated from this client will NOT trigger entity mutation hooks to avoid infinite loops. */ diff --git a/packages/runtime/test/plugin/entity-mutation-hooks.test.ts b/packages/runtime/test/plugin/entity-mutation-hooks.test.ts index 65c40eb6..96961c7c 100644 --- a/packages/runtime/test/plugin/entity-mutation-hooks.test.ts +++ b/packages/runtime/test/plugin/entity-mutation-hooks.test.ts @@ -40,12 +40,9 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons if (args.action === 'delete') { expect(DeleteQueryNode.is(args.queryNode)).toBe(true); } - expect(args.entities).toBeUndefined(); }, afterEntityMutation(args) { afterCalled[args.action] = true; - expect(args.beforeMutationEntities).toBeUndefined(); - expect(args.afterMutationEntities).toBeUndefined(); }, }, }); @@ -71,79 +68,29 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons }); }); - it('can intercept with filtering', async () => { - const beforeCalled = { create: false, update: false, delete: false }; - const afterCalled = { create: false, update: false, delete: false }; - - const client = _client.$use({ - id: 'test', - onEntityMutation: { - mutationInterceptionFilter: (args) => { - return { - intercept: args.action !== 'delete', - }; - }, - beforeEntityMutation(args) { - beforeCalled[args.action] = true; - expect(args.entities).toBeUndefined(); - }, - afterEntityMutation(args) { - afterCalled[args.action] = true; - }, - }, - }); - - const user = await client.user.create({ - data: { email: 'u1@test.com' }, - }); - await client.user.update({ - where: { id: user.id }, - data: { email: 'u2@test.com' }, - }); - await client.user.delete({ where: { id: user.id } }); - - expect(beforeCalled).toEqual({ - create: true, - update: true, - delete: false, - }); - expect(afterCalled).toEqual({ - create: true, - update: true, - delete: false, - }); - }); - it('can intercept with loading before mutation entities', async () => { + const queryIds = { + update: { before: '', after: '' }, + delete: { before: '', after: '' }, + }; + const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - loadBeforeMutationEntities: true, - }; - }, - beforeEntityMutation(args) { + async beforeEntityMutation(args) { if (args.action === 'update' || args.action === 'delete') { - expect(args.entities).toEqual([ + await expect(args.loadBeforeMutationEntities()).resolves.toEqual([ expect.objectContaining({ email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', }), ]); - } else { - expect(args.entities).toBeUndefined(); + queryIds[args.action].before = args.queryId; } }, - afterEntityMutation(args) { + async afterEntityMutation(args) { if (args.action === 'update' || args.action === 'delete') { - expect(args.beforeMutationEntities).toEqual([ - expect.objectContaining({ - email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', - }), - ]); + queryIds[args.action].after = args.queryId; } - expect(args.afterMutationEntities).toBeUndefined(); }, }, }); @@ -159,6 +106,11 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons data: { email: 'u3@test.com' }, }); await client.user.delete({ where: { id: user.id } }); + + expect(queryIds.update.before).toBeTruthy(); + expect(queryIds.delete.before).toBeTruthy(); + expect(queryIds.update.before).toBe(queryIds.update.after); + expect(queryIds.delete.before).toBe(queryIds.delete.after); }); it('can intercept with loading after mutation entities', async () => { @@ -167,13 +119,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { + async afterEntityMutation(args) { if (args.action === 'create' || args.action === 'update') { if (args.action === 'create') { userCreateIntercepted = true; @@ -181,15 +127,13 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons if (args.action === 'update') { userUpdateIntercepted = true; } - expect(args.afterMutationEntities).toEqual( + await expect(args.loadAfterMutationEntities()).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ email: args.action === 'create' ? 'u1@test.com' : 'u2@test.com', }), ]), ); - } else { - expect(args.afterMutationEntities).toBeUndefined(); } }, }, @@ -215,16 +159,10 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { + async afterEntityMutation(args) { if (args.action === 'create') { userCreateIntercepted = true; - expect(args.afterMutationEntities).toEqual( + await expect(args.loadAfterMutationEntities()).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ email: 'u1@test.com' }), expect.objectContaining({ email: 'u2@test.com' }), @@ -232,7 +170,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons ); } else if (args.action === 'update') { userUpdateIntercepted = true; - expect(args.afterMutationEntities).toEqual( + await expect(args.loadAfterMutationEntities()).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ email: 'u1@test.com', @@ -246,7 +184,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons ); } else if (args.action === 'delete') { userDeleteIntercepted = true; - expect(args.afterMutationEntities).toEqual( + await expect(args.loadAfterMutationEntities()).resolves.toEqual( expect.arrayContaining([ expect.objectContaining({ email: 'u1@test.com' }), expect.objectContaining({ email: 'u2@test.com' }), @@ -275,19 +213,14 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: (args) => { - return { - intercept: args.action === 'create' || args.action === 'update', - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { + async afterEntityMutation(args) { if (args.action === 'create') { if (args.model === 'Post') { - if ((args.afterMutationEntities![0] as any).title === 'Post1') { + const afterEntities = await args.loadAfterMutationEntities(); + if ((afterEntities![0] as any).title === 'Post1') { post1Intercepted = true; } - if ((args.afterMutationEntities![0] as any).title === 'Post2') { + if ((afterEntities![0] as any).title === 'Post2') { post2Intercepted = true; } } @@ -320,15 +253,12 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - loadBeforeMutationEntities: true, - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { - triggered.push(args); + async afterEntityMutation(args) { + triggered.push({ + action: args.action, + model: args.model, + afterMutationEntities: await args.loadAfterMutationEntities(), + }); }, }, }); @@ -348,19 +278,16 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons expect.objectContaining({ action: 'create', model: 'User', - beforeMutationEntities: undefined, afterMutationEntities: [expect.objectContaining({ email: 'u1@test.com' })], }), expect.objectContaining({ action: 'update', model: 'User', - beforeMutationEntities: [expect.objectContaining({ email: 'u1@test.com' })], afterMutationEntities: [expect.objectContaining({ email: 'u2@test.com' })], }), expect.objectContaining({ action: 'delete', model: 'User', - beforeMutationEntities: [expect.objectContaining({ email: 'u2@test.com' })], afterMutationEntities: undefined, }), ]); @@ -403,12 +330,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - runAfterMutationWithinTransaction: true, - }; - }, + runAfterMutationWithinTransaction: true, async beforeEntityMutation(ctx) { await ctx.client.profile.create({ data: { bio: 'Bio1' }, @@ -481,12 +403,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - runAfterMutationWithinTransaction: true, - }; - }, + runAfterMutationWithinTransaction: true, async afterEntityMutation(ctx) { intercepted = true; await ctx.client.user.create({ data: { email: 'u2@test.com' } }); @@ -541,12 +458,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - runAfterMutationWithinTransaction: true, - }; - }, + runAfterMutationWithinTransaction: true, async afterEntityMutation(ctx) { intercepted = true; await ctx.client.user.create({ data: { email: 'u2@test.com' } }); @@ -577,15 +489,13 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: (ctx) => { - return { - intercept: ctx.action === 'update', - loadBeforeMutationEntities: true, - }; - }, - async beforeEntityMutation(ctx) { - intercepted = true; - expect(ctx.entities).toEqual([expect.objectContaining({ email: 'u1@test.com' })]); + async beforeEntityMutation(args) { + if (args.action === 'update') { + intercepted = true; + await expect(args.loadBeforeMutationEntities()).resolves.toEqual([ + expect.objectContaining({ email: 'u1@test.com' }), + ]); + } }, }, }); @@ -689,12 +599,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - runAfterMutationWithinTransaction: true, - }; - }, + runAfterMutationWithinTransaction: true, async afterEntityMutation(ctx) { if (intercepted) { return; @@ -760,12 +665,7 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons const client = _client.$use({ id: 'test', onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - runAfterMutationWithinTransaction: true, - }; - }, + runAfterMutationWithinTransaction: true, async afterEntityMutation(ctx) { intercepted = true; await ctx.client.user.create({ data: { email: 'u2@test.com' } }); @@ -788,57 +688,5 @@ describe.each([{ provider: 'sqlite' as const }, { provider: 'postgresql' as cons await expect(client.user.findMany()).toResolveWithLength(0); }); }); - - it('triggers multiple afterEntityMutation hooks for multiple mutations', async () => { - const triggered: any[] = []; - - const client = _client.$use({ - id: 'test', - onEntityMutation: { - mutationInterceptionFilter: () => { - return { - intercept: true, - loadBeforeMutationEntities: true, - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { - triggered.push(args); - }, - }, - }); - - await client.$transaction(async (tx) => { - await tx.user.create({ - data: { email: 'u1@test.com' }, - }); - await tx.user.update({ - where: { email: 'u1@test.com' }, - data: { email: 'u2@test.com' }, - }); - await tx.user.delete({ where: { email: 'u2@test.com' } }); - }); - - expect(triggered).toEqual([ - expect.objectContaining({ - action: 'create', - model: 'User', - beforeMutationEntities: undefined, - afterMutationEntities: [expect.objectContaining({ email: 'u1@test.com' })], - }), - expect.objectContaining({ - action: 'update', - model: 'User', - beforeMutationEntities: [expect.objectContaining({ email: 'u1@test.com' })], - afterMutationEntities: [expect.objectContaining({ email: 'u2@test.com' })], - }), - expect.objectContaining({ - action: 'delete', - model: 'User', - beforeMutationEntities: [expect.objectContaining({ email: 'u2@test.com' })], - afterMutationEntities: undefined, - }), - ]); - }); }, );