diff --git a/.vscode/launch.json b/.vscode/launch.json index ea6451fe..7a65620b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,11 +10,7 @@ "request": "launch", "skipFiles": ["/**"], "type": "node", - "args": [ - "generate", - "--schema", - "${workspaceFolder}/samples/blog/zenstack/schema.zmodel" - ] + "args": ["generate", "--schema", "${workspaceFolder}/samples/blog/zenstack/schema.zmodel"] }, { "name": "Debug with TSX", @@ -44,15 +40,14 @@ // Ignore all dependencies (optional) "${workspaceFolder}/node_modules/**" - ] + ], + "cwd": "${fileDirname}" }, { "name": "Run Extension", "type": "extensionHost", "request": "launch", - "args": [ - "--extensionDevelopmentPath=${workspaceFolder}/packages/ide/vscode" - ], + "args": ["--extensionDevelopmentPath=${workspaceFolder}/packages/ide/vscode"], "sourceMaps": true, "outFiles": ["${workspaceFolder}/packages/ide/vscode/dist/**/*.js"] }, diff --git a/README.md b/README.md index 967c00ab..77bdfe1f 100644 --- a/README.md +++ b/README.md @@ -289,15 +289,11 @@ ORM query interception allows you to intercept the high-level ORM API calls. The ```ts db.$use({ id: 'cost-logger', - onQuery: { - $allModels: { - $allOperations: async ({ model, operation, args, query }) => { - const start = Date.now(); - const result = await query(args); - console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); - return result; - }, - }, + onQuery: async ({ model, operation, args, proceed }) => { + const start = Date.now(); + const result = await proceed(args); + console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); + return result; }, }); ``` @@ -333,11 +329,14 @@ Another popular interception use case is, instead of intercepting calls, "listen ```ts db.$use({ id: 'mutation-hook-plugin', - beforeEntityMutation({ model, action }) { - console.log(`Before ${model} ${action}`); - }, - afterEntityMutation({ model, action }) { - console.log(`After ${model} ${action}`); + onEntityMutation: { + beforeEntityMutation({ model, action }) { + console.log(`Before ${model} ${action}`); + }, + + afterEntityMutation({ model, action }) { + console.log(`After ${model} ${action}`); + }, }, }); ``` @@ -347,20 +346,24 @@ You can provide an extra `mutationInterceptionFilter` to control what to interce ```ts db.$use({ id: 'mutation-hook-plugin', - mutationInterceptionFilter: ({ model }) => { - return { - intercept: model === 'User', - // load entities affected before the mutation (defaults to false) - loadBeforeMutationEntities: true, - // load entities affected after the mutation (defaults to false) - loadAfterMutationEntities: true, - }; - }, - beforeEntityMutation({ model, action, entities }) { - console.log(`Before ${model} ${action}: ${entities}`); - }, - afterEntityMutation({ model, action, afterMutationEntities }) { - console.log(`After ${model} ${action}: ${afterMutationEntities}`); + onEntityMutation: { + mutationInterceptionFilter: ({ model }) => { + return { + intercept: model === 'User', + // load entities affected before the mutation (defaults to false) + loadBeforeMutationEntities: true, + // load entities affected after the mutation (defaults to false) + loadAfterMutationEntities: true, + }; + }, + + beforeEntityMutation({ model, action, entities }) { + console.log(`Before ${model} ${action}: ${entities}`); + }, + + afterEntityMutation({ model, action, afterMutationEntities }) { + console.log(`After ${model} ${action}: ${afterMutationEntities}`); + }, }, }); ``` diff --git a/TODO.md b/TODO.md index 7245aef9..3be3a477 100644 --- a/TODO.md +++ b/TODO.md @@ -99,6 +99,7 @@ - [ ] Short-circuit pre-create check for scalar-field only policies - [ ] Inject "replace into" - [ ] Inject "on conflict do update" + - [ ] Inject "insert into select from" - [x] Migration - [ ] Databases - [x] SQLite diff --git a/package.json b/package.json index 05d02903..237c9d63 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 66412a3c..ecb796a7 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack CLI", "description": "FullStack database toolkit with built-in access control and automatic API generation.", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index d8f4330f..bb2bbb72 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index e2107357..bba1dd12 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/dialects/sql.js/package.json b/packages/dialects/sql.js/package.json index 70c72273..7302a814 100644 --- a/packages/dialects/sql.js/package.json +++ b/packages/dialects/sql.js/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/kysely-sql-js", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "Kysely dialect for sql.js", "type": "module", "scripts": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index cb30249e..36d820f7 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "type": "module", "private": true, "license": "MIT" diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index ad9027ca..92ab6b1a 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack", "publisher": "zenstack", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "displayName": "ZenStack Language Tools", "description": "VSCode extension for ZenStack ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index 1650c808..e744294d 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/language", "description": "ZenStack ZModel language specification", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 76dde63f..a24e2ec7 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "ZenStack Runtime", "type": "module", "scripts": { diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index a75dffc2..d5f65a46 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -1,4 +1,4 @@ -import { invariant, lowerCaseFirst } from '@zenstackhq/common-helpers'; +import { invariant } from '@zenstackhq/common-helpers'; import type { QueryExecutor } from 'kysely'; import { CompiledQuery, @@ -358,9 +358,9 @@ function createModelCrudHandler { return createZenStackPromise(async (txClient?: ClientContract) => { - let proceed = async (_args?: unknown) => { + let proceed = async (_args: unknown) => { const _handler = txClient ? handler.withClient(txClient) : handler; - const r = await _handler.handle(operation, _args ?? args); + const r = await _handler.handle(operation, _args); if (!r && throwIfNoResult) { throw new NotFoundError(model); } @@ -376,30 +376,19 @@ function createModelCrudHandler(plugin.onQuery)) { - if (_model === lowerCaseFirst(model) || _model === '$allModels') { - if (modelHooks && typeof modelHooks === 'object') { - // for each operation key or "$allOperations" - for (const [op, opHooks] of Object.entries(modelHooks)) { - if (op === operation || op === '$allOperations') { - if (typeof opHooks === 'function') { - const _proceed = proceed; - proceed = () => - opHooks({ - client, - model, - operation, - args, - query: _proceed, - }) as Promise; - } - } - } - } - } - } + const onQuery = plugin.onQuery; + if (onQuery) { + const _proceed = proceed; + proceed = (_args: unknown) => + onQuery({ + client, + model, + operation, + // reflect the latest override if provided + args: _args, + // ensure inner overrides are propagated to the previous proceed + proceed: (nextArgs: unknown) => _proceed(nextArgs), + }) as Promise; } } diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index 06d131ab..94247354 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -215,561 +215,563 @@ export type CRUD = 'create' | 'read' | 'update' | 'delete'; //#region Model operations +export type AllModelOperations> = { + /** + * Returns a list of entities. + * @param args - query args + * @returns a list of entities + * + * @example + * ```ts + * // find all users and return all scalar fields + * await db.user.findMany(); + * + * // find all users with name 'Alex' + * await db.user.findMany({ + * where: { + * name: 'Alex' + * } + * }); + * + * // select fields + * await db.user.findMany({ + * select: { + * name: true, + * email: true, + * } + * }); // result: `Array<{ name: string, email: string }>` + * + * // omit fields + * await db.user.findMany({ + * omit: { + * name: true, + * } + * }); // result: `Array<{ id: number; email: string; ... }>` + * + * // include relations (and all scalar fields) + * await db.user.findMany({ + * include: { + * posts: true, + * } + * }); // result: `Array<{ ...; posts: Post[] }>` + * + * // include relations with filter + * await db.user.findMany({ + * include: { + * posts: { + * where: { + * published: true + * } + * } + * } + * }); + * + * // pagination and sorting + * await db.user.findMany({ + * skip: 10, + * take: 10, + * orderBy: [{ name: 'asc' }, { email: 'desc' }], + * }); + * + * // pagination with cursor (https://www.prisma.io/docs/orm/prisma-client/queries/pagination#cursor-based-pagination) + * await db.user.findMany({ + * cursor: { id: 10 }, + * skip: 1, + * take: 10, + * orderBy: { id: 'asc' }, + * }); + * + * // distinct + * await db.user.findMany({ + * distinct: ['name'] + * }); + * + * // count all relations + * await db.user.findMany({ + * _count: true, + * }); // result: `{ _count: { posts: number; ... } }` + * + * // count selected relations + * await db.user.findMany({ + * _count: { select: { posts: true } }, + * }); // result: `{ _count: { posts: number } }` + * ``` + */ + findMany>( + args?: SelectSubset>, + ): ZenStackPromise>[]>; + + /** + * Returns a uniquely identified entity. + * @param args - query args + * @returns a single entity or null if not found + * @see {@link findMany} + */ + findUnique>( + args: SelectSubset>, + ): ZenStackPromise> | null>; + + /** + * Returns a uniquely identified entity or throws `NotFoundError` if not found. + * @param args - query args + * @returns a single entity + * @see {@link findMany} + */ + findUniqueOrThrow>( + args: SelectSubset>, + ): ZenStackPromise>>; + + /** + * Returns the first entity. + * @param args - query args + * @returns a single entity or null if not found + * @see {@link findMany} + */ + findFirst>( + args?: SelectSubset>, + ): ZenStackPromise> | null>; + + /** + * Returns the first entity or throws `NotFoundError` if not found. + * @param args - query args + * @returns a single entity + * @see {@link findMany} + */ + findFirstOrThrow>( + args?: SelectSubset>, + ): ZenStackPromise>>; + + /** + * Creates a new entity. + * @param args - create args + * @returns the created entity + * + * @example + * ```ts + * // simple create + * await db.user.create({ + * data: { name: 'Alex', email: 'alex@zenstack.dev' } + * }); + * + * // nested create with relation + * await db.user.create({ + * data: { + * email: 'alex@zenstack.dev', + * posts: { create: { title: 'Hello World' } } + * } + * }); + * + * // you can use `select`, `omit`, and `include` to control + * // the fields returned by the query, as with `findMany` + * await db.user.create({ + * data: { + * email: 'alex@zenstack.dev', + * posts: { create: { title: 'Hello World' } } + * }, + * include: { posts: true } + * }); // result: `{ id: number; posts: Post[] }` + * + * // connect relations + * await db.user.create({ + * data: { + * email: 'alex@zenstack.dev', + * posts: { connect: { id: 1 } } + * } + * }); + * + * // connect relations, and create if not found + * await db.user.create({ + * data: { + * email: 'alex@zenstack.dev', + * posts: { + * connectOrCreate: { + * where: { id: 1 }, + * create: { title: 'Hello World' } + * } + * } + * } + * }); + * ``` + */ + create>( + args: SelectSubset>, + ): ZenStackPromise>>; + + /** + * Creates multiple entities. Only scalar fields are allowed. + * @param args - create args + * @returns count of created entities: `{ count: number }` + * + * @example + * ```ts + * // create multiple entities + * await db.user.createMany({ + * data: [ + * { name: 'Alex', email: 'alex@zenstack.dev' }, + * { name: 'John', email: 'john@zenstack.dev' } + * ] + * }); + * + * // skip items that cause unique constraint violation + * await db.user.createMany({ + * data: [ + * { name: 'Alex', email: 'alex@zenstack.dev' }, + * { name: 'John', email: 'john@zenstack.dev' } + * ], + * skipDuplicates: true + * }); + * ``` + */ + createMany>( + args?: SelectSubset>, + ): ZenStackPromise; + + /** + * Creates multiple entities and returns them. + * @param args - create args. See {@link createMany} for input. Use + * `select` and `omit` to control the fields returned. + * @returns the created entities + * + * @example + * ```ts + * // create multiple entities and return selected fields + * await db.user.createManyAndReturn({ + * data: [ + * { name: 'Alex', email: 'alex@zenstack.dev' }, + * { name: 'John', email: 'john@zenstack.dev' } + * ], + * select: { id: true, email: true } + * }); + * ``` + */ + createManyAndReturn>( + args?: SelectSubset>, + ): ZenStackPromise>[]>; + + /** + * Updates a uniquely identified entity. + * @param args - update args. See {@link findMany} for how to control + * fields and relations returned. + * @returns the updated entity. Throws `NotFoundError` if the entity is not found. + * + * @example + * ```ts + * // update fields + * await db.user.update({ + * where: { id: 1 }, + * data: { name: 'Alex' } + * }); + * + * // connect a relation + * await db.user.update({ + * where: { id: 1 }, + * data: { posts: { connect: { id: 1 } } } + * }); + * + * // connect relation, and create if not found + * await db.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * connectOrCreate: { + * where: { id: 1 }, + * create: { title: 'Hello World' } + * } + * } + * } + * }); + * + * // create many related entities (only available for one-to-many relations) + * await db.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * createMany: { + * data: [{ title: 'Hello World' }, { title: 'Hello World 2' }], + * } + * } + * } + * }); + * + * // disconnect a one-to-many relation + * await db.user.update({ + * where: { id: 1 }, + * data: { posts: { disconnect: { id: 1 } } } + * }); + * + * // disconnect a one-to-one relation + * await db.user.update({ + * where: { id: 1 }, + * data: { profile: { disconnect: true } } + * }); + * + * // replace a relation (only available for one-to-many relations) + * await db.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * set: [{ id: 1 }, { id: 2 }] + * } + * } + * }); + * + * // update a relation + * await db.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * update: { where: { id: 1 }, data: { title: 'Hello World' } } + * } + * } + * }); + * + * // upsert a relation + * await db.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * upsert: { + * where: { id: 1 }, + * create: { title: 'Hello World' }, + * update: { title: 'Hello World' } + * } + * } + * } + * }); + * + * // update many related entities (only available for one-to-many relations) + * await db.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * updateMany: { + * where: { published: true }, + * data: { title: 'Hello World' } + * } + * } + * } + * }); + * + * // delete a one-to-many relation + * await db.user.update({ + * where: { id: 1 }, + * data: { posts: { delete: { id: 1 } } } + * }); + * + * // delete a one-to-one relation + * await db.user.update({ + * where: { id: 1 }, + * data: { profile: { delete: true } } + * }); + * ``` + */ + update>( + args: SelectSubset>, + ): ZenStackPromise>>; + + /** + * Updates multiple entities. + * @param args - update args. Only scalar fields are allowed for data. + * @returns count of updated entities: `{ count: number }` + * + * @example + * ```ts + * // update many entities + * await db.user.updateMany({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' } + * }); + * + * // limit the number of updated entities + * await db.user.updateMany({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' }, + * limit: 10 + * }); + */ + updateMany>( + args: Subset>, + ): ZenStackPromise; + + /** + * Updates multiple entities and returns them. + * @param args - update args. Only scalar fields are allowed for data. + * @returns the updated entities + * + * @example + * ```ts + * // update many entities and return selected fields + * await db.user.updateManyAndReturn({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' }, + * select: { id: true, email: true } + * }); // result: `Array<{ id: string; email: string }>` + * + * // limit the number of updated entities + * await db.user.updateManyAndReturn({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' }, + * limit: 10 + * }); + * ``` + */ + updateManyAndReturn>( + args: Subset>, + ): ZenStackPromise>[]>; + + /** + * Creates or updates an entity. + * @param args - upsert args + * @returns the upserted entity + * + * @example + * ```ts + * // upsert an entity + * await db.user.upsert({ + * // `where` clause is used to find the entity + * where: { id: 1 }, + * // `create` clause is used if the entity is not found + * create: { email: 'alex@zenstack.dev', name: 'Alex' }, + * // `update` clause is used if the entity is found + * update: { name: 'Alex-new' }, + * // `select` and `omit` can be used to control the returned fields + * ... + * }); + * ``` + */ + upsert>( + args: SelectSubset>, + ): ZenStackPromise>>; + + /** + * Deletes a uniquely identifiable entity. + * @param args - delete args + * @returns the deleted entity. Throws `NotFoundError` if the entity is not found. + * + * @example + * ```ts + * // delete an entity + * await db.user.delete({ + * where: { id: 1 } + * }); + * + * // delete an entity and return selected fields + * await db.user.delete({ + * where: { id: 1 }, + * select: { id: true, email: true } + * }); // result: `{ id: string; email: string }` + * ``` + */ + delete>( + args: SelectSubset>, + ): ZenStackPromise>>; + + /** + * Deletes multiple entities. + * @param args - delete args + * @returns count of deleted entities: `{ count: number }` + * + * @example + * ```ts + * // delete many entities + * await db.user.deleteMany({ + * where: { email: { endsWith: '@zenstack.dev' } } + * }); + * + * // limit the number of deleted entities + * await db.user.deleteMany({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * limit: 10 + * }); + * ``` + */ + deleteMany>( + args?: Subset>, + ): ZenStackPromise; + + /** + * Counts rows or field values. + * @param args - count args + * @returns `number`, or an object containing count of selected relations + * + * @example + * ```ts + * // count all + * await db.user.count(); + * + * // count with a filter + * await db.user.count({ where: { email: { endsWith: '@zenstack.dev' } } }); + * + * // count rows and field values + * await db.user.count({ + * select: { _all: true, email: true } + * }); // result: `{ _all: number, email: number }` + */ + count>( + args?: Subset>, + ): ZenStackPromise>>; + + /** + * Aggregates rows. + * @param args - aggregation args + * @returns an object containing aggregated values + * + * @example + * ```ts + * // aggregate rows + * await db.profile.aggregate({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * _count: true, + * _avg: { age: true }, + * _sum: { age: true }, + * _min: { age: true }, + * _max: { age: true } + * }); // result: `{ _count: number, _avg: { age: number }, ... }` + */ + aggregate>( + args: Subset>, + ): ZenStackPromise>>; + + /** + * Groups rows by columns. + * @param args - groupBy args + * @returns an object containing grouped values + * + * @example + * ```ts + * // group by a field + * await db.profile.groupBy({ + * by: 'country', + * _count: true + * }); // result: `Array<{ country: string, _count: number }>` + * + * // group by multiple fields + * await db.profile.groupBy({ + * by: ['country', 'city'], + * _count: true + * }); // result: `Array<{ country: string, city: string, _count: number }>` + * + * // group by with sorting, the `orderBy` fields must be either an aggregation + * // or a field used in the `by` list + * await db.profile.groupBy({ + * by: 'country', + * orderBy: { country: 'desc' } + * }); + * + * // group by with having (post-aggregation filter), the fields used in `having` must + * // be either an aggregation, or a field used in the `by` list + * await db.profile.groupBy({ + * by: 'country', + * having: { country: 'US', age: { _avg: { gte: 18 } } } + * }); + */ + groupBy>( + args: Subset>, + ): ZenStackPromise>>; +}; + export type ModelOperations> = Omit< - { - /** - * Returns a list of entities. - * @param args - query args - * @returns a list of entities - * - * @example - * ```ts - * // find all users and return all scalar fields - * await db.user.findMany(); - * - * // find all users with name 'Alex' - * await db.user.findMany({ - * where: { - * name: 'Alex' - * } - * }); - * - * // select fields - * await db.user.findMany({ - * select: { - * name: true, - * email: true, - * } - * }); // result: `Array<{ name: string, email: string }>` - * - * // omit fields - * await db.user.findMany({ - * omit: { - * name: true, - * } - * }); // result: `Array<{ id: number; email: string; ... }>` - * - * // include relations (and all scalar fields) - * await db.user.findMany({ - * include: { - * posts: true, - * } - * }); // result: `Array<{ ...; posts: Post[] }>` - * - * // include relations with filter - * await db.user.findMany({ - * include: { - * posts: { - * where: { - * published: true - * } - * } - * } - * }); - * - * // pagination and sorting - * await db.user.findMany({ - * skip: 10, - * take: 10, - * orderBy: [{ name: 'asc' }, { email: 'desc' }], - * }); - * - * // pagination with cursor (https://www.prisma.io/docs/orm/prisma-client/queries/pagination#cursor-based-pagination) - * await db.user.findMany({ - * cursor: { id: 10 }, - * skip: 1, - * take: 10, - * orderBy: { id: 'asc' }, - * }); - * - * // distinct - * await db.user.findMany({ - * distinct: ['name'] - * }); - * - * // count all relations - * await db.user.findMany({ - * _count: true, - * }); // result: `{ _count: { posts: number; ... } }` - * - * // count selected relations - * await db.user.findMany({ - * _count: { select: { posts: true } }, - * }); // result: `{ _count: { posts: number } }` - * ``` - */ - findMany>( - args?: SelectSubset>, - ): ZenStackPromise>[]>; - - /** - * Returns a uniquely identified entity. - * @param args - query args - * @returns a single entity or null if not found - * @see {@link findMany} - */ - findUnique>( - args: SelectSubset>, - ): ZenStackPromise> | null>; - - /** - * Returns a uniquely identified entity or throws `NotFoundError` if not found. - * @param args - query args - * @returns a single entity - * @see {@link findMany} - */ - findUniqueOrThrow>( - args: SelectSubset>, - ): ZenStackPromise>>; - - /** - * Returns the first entity. - * @param args - query args - * @returns a single entity or null if not found - * @see {@link findMany} - */ - findFirst>( - args?: SelectSubset>, - ): ZenStackPromise> | null>; - - /** - * Returns the first entity or throws `NotFoundError` if not found. - * @param args - query args - * @returns a single entity - * @see {@link findMany} - */ - findFirstOrThrow>( - args?: SelectSubset>, - ): ZenStackPromise>>; - - /** - * Creates a new entity. - * @param args - create args - * @returns the created entity - * - * @example - * ```ts - * // simple create - * await db.user.create({ - * data: { name: 'Alex', email: 'alex@zenstack.dev' } - * }); - * - * // nested create with relation - * await db.user.create({ - * data: { - * email: 'alex@zenstack.dev', - * posts: { create: { title: 'Hello World' } } - * } - * }); - * - * // you can use `select`, `omit`, and `include` to control - * // the fields returned by the query, as with `findMany` - * await db.user.create({ - * data: { - * email: 'alex@zenstack.dev', - * posts: { create: { title: 'Hello World' } } - * }, - * include: { posts: true } - * }); // result: `{ id: number; posts: Post[] }` - * - * // connect relations - * await db.user.create({ - * data: { - * email: 'alex@zenstack.dev', - * posts: { connect: { id: 1 } } - * } - * }); - * - * // connect relations, and create if not found - * await db.user.create({ - * data: { - * email: 'alex@zenstack.dev', - * posts: { - * connectOrCreate: { - * where: { id: 1 }, - * create: { title: 'Hello World' } - * } - * } - * } - * }); - * ``` - */ - create>( - args: SelectSubset>, - ): ZenStackPromise>>; - - /** - * Creates multiple entities. Only scalar fields are allowed. - * @param args - create args - * @returns count of created entities: `{ count: number }` - * - * @example - * ```ts - * // create multiple entities - * await db.user.createMany({ - * data: [ - * { name: 'Alex', email: 'alex@zenstack.dev' }, - * { name: 'John', email: 'john@zenstack.dev' } - * ] - * }); - * - * // skip items that cause unique constraint violation - * await db.user.createMany({ - * data: [ - * { name: 'Alex', email: 'alex@zenstack.dev' }, - * { name: 'John', email: 'john@zenstack.dev' } - * ], - * skipDuplicates: true - * }); - * ``` - */ - createMany>( - args?: SelectSubset>, - ): ZenStackPromise; - - /** - * Creates multiple entities and returns them. - * @param args - create args. See {@link createMany} for input. Use - * `select` and `omit` to control the fields returned. - * @returns the created entities - * - * @example - * ```ts - * // create multiple entities and return selected fields - * await db.user.createManyAndReturn({ - * data: [ - * { name: 'Alex', email: 'alex@zenstack.dev' }, - * { name: 'John', email: 'john@zenstack.dev' } - * ], - * select: { id: true, email: true } - * }); - * ``` - */ - createManyAndReturn>( - args?: SelectSubset>, - ): ZenStackPromise>[]>; - - /** - * Updates a uniquely identified entity. - * @param args - update args. See {@link findMany} for how to control - * fields and relations returned. - * @returns the updated entity. Throws `NotFoundError` if the entity is not found. - * - * @example - * ```ts - * // update fields - * await db.user.update({ - * where: { id: 1 }, - * data: { name: 'Alex' } - * }); - * - * // connect a relation - * await db.user.update({ - * where: { id: 1 }, - * data: { posts: { connect: { id: 1 } } } - * }); - * - * // connect relation, and create if not found - * await db.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * connectOrCreate: { - * where: { id: 1 }, - * create: { title: 'Hello World' } - * } - * } - * } - * }); - * - * // create many related entities (only available for one-to-many relations) - * await db.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * createMany: { - * data: [{ title: 'Hello World' }, { title: 'Hello World 2' }], - * } - * } - * } - * }); - * - * // disconnect a one-to-many relation - * await db.user.update({ - * where: { id: 1 }, - * data: { posts: { disconnect: { id: 1 } } } - * }); - * - * // disconnect a one-to-one relation - * await db.user.update({ - * where: { id: 1 }, - * data: { profile: { disconnect: true } } - * }); - * - * // replace a relation (only available for one-to-many relations) - * await db.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * set: [{ id: 1 }, { id: 2 }] - * } - * } - * }); - * - * // update a relation - * await db.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * update: { where: { id: 1 }, data: { title: 'Hello World' } } - * } - * } - * }); - * - * // upsert a relation - * await db.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * upsert: { - * where: { id: 1 }, - * create: { title: 'Hello World' }, - * update: { title: 'Hello World' } - * } - * } - * } - * }); - * - * // update many related entities (only available for one-to-many relations) - * await db.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * updateMany: { - * where: { published: true }, - * data: { title: 'Hello World' } - * } - * } - * } - * }); - * - * // delete a one-to-many relation - * await db.user.update({ - * where: { id: 1 }, - * data: { posts: { delete: { id: 1 } } } - * }); - * - * // delete a one-to-one relation - * await db.user.update({ - * where: { id: 1 }, - * data: { profile: { delete: true } } - * }); - * ``` - */ - update>( - args: SelectSubset>, - ): ZenStackPromise>>; - - /** - * Updates multiple entities. - * @param args - update args. Only scalar fields are allowed for data. - * @returns count of updated entities: `{ count: number }` - * - * @example - * ```ts - * // update many entities - * await db.user.updateMany({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' } - * }); - * - * // limit the number of updated entities - * await db.user.updateMany({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' }, - * limit: 10 - * }); - */ - updateMany>( - args: Subset>, - ): ZenStackPromise; - - /** - * Updates multiple entities and returns them. - * @param args - update args. Only scalar fields are allowed for data. - * @returns the updated entities - * - * @example - * ```ts - * // update many entities and return selected fields - * await db.user.updateManyAndReturn({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' }, - * select: { id: true, email: true } - * }); // result: `Array<{ id: string; email: string }>` - * - * // limit the number of updated entities - * await db.user.updateManyAndReturn({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' }, - * limit: 10 - * }); - * ``` - */ - updateManyAndReturn>( - args: Subset>, - ): ZenStackPromise>[]>; - - /** - * Creates or updates an entity. - * @param args - upsert args - * @returns the upserted entity - * - * @example - * ```ts - * // upsert an entity - * await db.user.upsert({ - * // `where` clause is used to find the entity - * where: { id: 1 }, - * // `create` clause is used if the entity is not found - * create: { email: 'alex@zenstack.dev', name: 'Alex' }, - * // `update` clause is used if the entity is found - * update: { name: 'Alex-new' }, - * // `select` and `omit` can be used to control the returned fields - * ... - * }); - * ``` - */ - upsert>( - args: SelectSubset>, - ): ZenStackPromise>>; - - /** - * Deletes a uniquely identifiable entity. - * @param args - delete args - * @returns the deleted entity. Throws `NotFoundError` if the entity is not found. - * - * @example - * ```ts - * // delete an entity - * await db.user.delete({ - * where: { id: 1 } - * }); - * - * // delete an entity and return selected fields - * await db.user.delete({ - * where: { id: 1 }, - * select: { id: true, email: true } - * }); // result: `{ id: string; email: string }` - * ``` - */ - delete>( - args: SelectSubset>, - ): ZenStackPromise>>; - - /** - * Deletes multiple entities. - * @param args - delete args - * @returns count of deleted entities: `{ count: number }` - * - * @example - * ```ts - * // delete many entities - * await db.user.deleteMany({ - * where: { email: { endsWith: '@zenstack.dev' } } - * }); - * - * // limit the number of deleted entities - * await db.user.deleteMany({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * limit: 10 - * }); - * ``` - */ - deleteMany>( - args?: Subset>, - ): ZenStackPromise; - - /** - * Counts rows or field values. - * @param args - count args - * @returns `number`, or an object containing count of selected relations - * - * @example - * ```ts - * // count all - * await db.user.count(); - * - * // count with a filter - * await db.user.count({ where: { email: { endsWith: '@zenstack.dev' } } }); - * - * // count rows and field values - * await db.user.count({ - * select: { _all: true, email: true } - * }); // result: `{ _all: number, email: number }` - */ - count>( - args?: Subset>, - ): ZenStackPromise>>; - - /** - * Aggregates rows. - * @param args - aggregation args - * @returns an object containing aggregated values - * - * @example - * ```ts - * // aggregate rows - * await db.profile.aggregate({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * _count: true, - * _avg: { age: true }, - * _sum: { age: true }, - * _min: { age: true }, - * _max: { age: true } - * }); // result: `{ _count: number, _avg: { age: number }, ... }` - */ - aggregate>( - args: Subset>, - ): ZenStackPromise>>; - - /** - * Groups rows by columns. - * @param args - groupBy args - * @returns an object containing grouped values - * - * @example - * ```ts - * // group by a field - * await db.profile.groupBy({ - * by: 'country', - * _count: true - * }); // result: `Array<{ country: string, _count: number }>` - * - * // group by multiple fields - * await db.profile.groupBy({ - * by: ['country', 'city'], - * _count: true - * }); // result: `Array<{ country: string, city: string, _count: number }>` - * - * // group by with sorting, the `orderBy` fields must be either an aggregation - * // or a field used in the `by` list - * await db.profile.groupBy({ - * by: 'country', - * orderBy: { country: 'desc' } - * }); - * - * // group by with having (post-aggregation filter), the fields used in `having` must - * // be either an aggregation, or a field used in the `by` list - * await db.profile.groupBy({ - * by: 'country', - * having: { country: 'US', age: { _avg: { gte: 18 } } } - * }); - */ - groupBy>( - args: Subset>, - ): ZenStackPromise>>; - }, + AllModelOperations, // exclude operations not applicable to delegate models IsDelegateModel extends true ? 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' : never >; diff --git a/packages/runtime/src/client/executor/zenstack-query-executor.ts b/packages/runtime/src/client/executor/zenstack-query-executor.ts index 8aff9505..8a8496f8 100644 --- a/packages/runtime/src/client/executor/zenstack-query-executor.ts +++ b/packages/runtime/src/client/executor/zenstack-query-executor.ts @@ -230,9 +230,7 @@ export class ZenStackQueryExecutor extends DefaultQuer } private get hasMutationHooks() { - return this.client.$options.plugins?.some( - (plugin) => plugin.beforeEntityMutation || plugin.afterEntityMutation, - ); + return this.client.$options.plugins?.some((plugin) => !!plugin.onEntityMutation); } private getMutationModel(queryNode: OperationNode): GetModels { @@ -274,10 +272,16 @@ export class ZenStackQueryExecutor extends DefaultQuer .exhaustive(); for (const plugin of plugins) { - if (!plugin.mutationInterceptionFilter) { + const onEntityMutation = plugin.onEntityMutation; + if (!onEntityMutation) { + continue; + } + + if (!onEntityMutation.mutationInterceptionFilter) { + // by default intercept without loading entities result.intercept = true; } else { - const filterResult = await plugin.mutationInterceptionFilter({ + const filterResult = await onEntityMutation.mutationInterceptionFilter({ model: mutationModel, action, queryNode, @@ -316,8 +320,9 @@ export class ZenStackQueryExecutor extends DefaultQuer if (this.options.plugins) { const mutationModel = this.getMutationModel(queryNode); for (const plugin of this.options.plugins) { - if (plugin.beforeEntityMutation) { - await plugin.beforeEntityMutation({ + const onEntityMutation = plugin.onEntityMutation; + if (onEntityMutation?.beforeEntityMutation) { + await onEntityMutation.beforeEntityMutation({ model: mutationModel, action: mutationInterceptionInfo.action, queryNode, @@ -341,8 +346,9 @@ export class ZenStackQueryExecutor extends DefaultQuer const hooks: AfterEntityMutationCallback[] = []; // tsc perf for (const plugin of this.options.plugins ?? []) { - if (plugin.afterEntityMutation) { - hooks.push(plugin.afterEntityMutation.bind(plugin)); + const onEntityMutation = plugin.onEntityMutation; + if (onEntityMutation?.afterEntityMutation) { + hooks.push(onEntityMutation.afterEntityMutation.bind(plugin)); } } if (hooks.length === 0) { diff --git a/packages/runtime/src/client/plugin.ts b/packages/runtime/src/client/plugin.ts index 8ae24be6..7f087a5d 100644 --- a/packages/runtime/src/client/plugin.ts +++ b/packages/runtime/src/client/plugin.ts @@ -2,145 +2,91 @@ import type { OperationNode, QueryResult, RootOperationNode, UnknownRow } from ' import type { ClientContract, ToKysely } from '.'; import type { GetModels, SchemaDef } from '../schema'; import type { MaybePromise } from '../utils/type-utils'; -import type { ModelOperations } from './contract'; import type { CrudOperation } from './crud/operations/base'; -export type QueryContext = { - /** - * The ZenStack client that's invoking the plugin. - */ - client: ClientContract; - - /** - * The model that is being queried. - */ - model: GetModels; - - /** - * The query operation that is being performed. - */ - operation: CrudOperation; - - /** - * The query arguments. - */ - queryArgs: unknown; -}; - /** - * The result of the hooks interception filter. + * ZenStack runtime plugin. */ -export type MutationInterceptionFilterResult = { +export interface RuntimePlugin { /** - * Whether to intercept the mutation or not. + * Plugin ID. */ - intercept: boolean; + id: string; /** - * Whether entities should be loaded before the mutation. + * Plugin display name. */ - loadBeforeMutationEntities?: boolean; + name?: string; /** - * Whether entities should be loaded after the mutation. + * Plugin description. */ - loadAfterMutationEntities?: boolean; -}; + description?: string; -type MutationHooksArgs = { /** - * The model that is being mutated. + * Intercepts an ORM query. */ - model: GetModels; + onQuery?: OnQueryCallback; /** - * The mutation action that is being performed. + * Intercepts an entity mutation. */ - action: 'create' | 'update' | 'delete'; + onEntityMutation?: EntityMutationHooksDef; /** - * The mutation data. Only available for create and update actions. + * Intercepts a Kysely query. */ - queryNode: OperationNode; -}; - -export type OnQueryArgs = QueryContext & { - proceed: ProceedQueryFunction; -}; - -export type PluginBeforeEntityMutationArgs = MutationHooksArgs & { - entities?: Record[]; -}; - -export type PluginAfterEntityMutationArgs = MutationHooksArgs & { - beforeMutationEntities?: Record[]; - afterMutationEntities?: Record[]; -}; - -export type ProceedQueryFunction = ( - queryArgs: unknown, - tx?: ClientContract, -) => Promise; - -export type OnKyselyQueryTransactionCallback = (proceed: ProceedKyselyQueryFunction) => Promise>; - -export type OnKyselyQueryTransaction = (callback: OnKyselyQueryTransactionCallback) => Promise>; - -export type OnKyselyQueryArgs = { - kysely: ToKysely; - schema: SchemaDef; - client: ClientContract; - query: RootOperationNode; - proceed: ProceedKyselyQueryFunction; -}; - -export type ProceedKyselyQueryFunction = (query: RootOperationNode) => Promise>; + onKyselyQuery?: OnKyselyQueryCallback; +} -export type OnKyselyQueryCallback = ( - args: OnKyselyQueryArgs, -) => Promise>; +/** + * Defines a ZenStack runtime plugin. + */ +export function definePlugin(plugin: RuntimePlugin) { + return plugin; +} -export type MutationInterceptionFilter = ( - args: MutationHooksArgs, -) => MaybePromise; +export { type CrudOperation } from './crud/operations/base'; -export type BeforeEntityMutationCallback = ( - args: PluginBeforeEntityMutationArgs, -) => MaybePromise; +// #region OnQuery hooks -export type AfterEntityMutationCallback = ( - args: PluginAfterEntityMutationArgs, -) => MaybePromise; +type OnQueryCallback = (ctx: OnQueryHookContext) => Promise; -/** - * ZenStack runtime plugin. - */ -export interface RuntimePlugin { +type OnQueryHookContext = { /** - * Plugin ID. + * The model that is being queried. */ - id: string; + model: GetModels; /** - * Plugin display name. + * The operation that is being performed. */ - name?: string; + operation: CrudOperation; /** - * Plugin description. + * The query arguments. */ - description?: string; + args: unknown; /** - * Intercepts an ORM query. + * The function to proceed with the original query. + * It takes the same arguments as the operation method. + * + * @param args The query arguments. */ - onQuery?: OnQueryHooks; + proceed: (args: unknown) => Promise; /** - * Intercepts a Kysely query. + * The ZenStack client that is performing the operation. */ - onKyselyQuery?: OnKyselyQueryCallback; + client: ClientContract; +}; + +// #endregion +// #region OnEntityMutation hooks + +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. @@ -162,69 +108,95 @@ export interface RuntimePlugin { * return value of {@link RuntimePlugin.mutationInterceptionFilter}. */ afterEntityMutation?: AfterEntityMutationCallback; -} - -type OnQueryHooks = { - [Model in GetModels as Uncapitalize]?: OnQueryOperationHooks; -} & { - $allModels?: OnQueryOperationHooks>; }; -type OnQueryOperationHooks> = { - [Operation in keyof ModelOperations]?: ( - ctx: OnQueryHookContext, - ) => Promise[Operation]>>>; -} & { - $allOperations?: (ctx: { - model: Model; - operation: CrudOperation; - args: unknown; - query: (args: unknown) => Promise; - client: ClientContract; - }) => MaybePromise; +type MutationHooksArgs = { + /** + * The model that is being mutated. + */ + model: GetModels; + + /** + * The mutation action that is being performed. + */ + action: 'create' | 'update' | 'delete'; + + /** + * The mutation data. Only available for create and update actions. + */ + queryNode: OperationNode; }; -type OnQueryHookContext< - Schema extends SchemaDef, - Model extends GetModels, - Operation extends keyof ModelOperations, -> = { +export type MutationInterceptionFilter = ( + args: MutationHooksArgs, +) => MaybePromise; + +/** + * The result of the hooks interception filter. + */ +export type MutationInterceptionFilterResult = { /** - * The model that is being queried. + * Whether to intercept the mutation or not. */ - model: Model; + intercept: boolean; /** - * The operation that is being performed. + * Whether entities should be loaded before the mutation. */ - operation: Operation; + loadBeforeMutationEntities?: boolean; /** - * The query arguments. + * Whether entities should be loaded after the mutation. */ - args: Parameters[Operation]>[0]; + loadAfterMutationEntities?: boolean; +}; + +export type BeforeEntityMutationCallback = ( + args: PluginBeforeEntityMutationArgs, +) => MaybePromise; +export type AfterEntityMutationCallback = ( + args: PluginAfterEntityMutationArgs, +) => MaybePromise; + +export type PluginBeforeEntityMutationArgs = MutationHooksArgs & { /** - * The query function to proceed with the original query. - * It takes the same arguments as the operation method. - * - * @param args The query arguments. + * Entities that are about to be mutated. Only available if `loadBeforeMutationEntities` is set to + * true in the return value of {@link RuntimePlugin.mutationInterceptionFilter}. */ - query: ( - args: Parameters[Operation]>[0], - ) => ReturnType[Operation]>; + entities?: unknown[]; +}; +export type PluginAfterEntityMutationArgs = MutationHooksArgs & { /** - * The ZenStack client that is performing the operation. + * 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}. */ + afterMutationEntities?: unknown[]; +}; + +// #endregion + +// #region OnKyselyQuery hooks + +export type OnKyselyQueryArgs = { + kysely: ToKysely; + schema: SchemaDef; client: ClientContract; + query: RootOperationNode; + proceed: ProceedKyselyQueryFunction; }; -/** - * Defines a ZenStack runtime plugin. - */ -export function definePlugin(plugin: RuntimePlugin) { - return plugin; -} +export type ProceedKyselyQueryFunction = (query: RootOperationNode) => Promise>; -export { type CrudOperation } from './crud/operations/base'; +export type OnKyselyQueryCallback = ( + args: OnKyselyQueryArgs, +) => Promise>; + +// #endregion diff --git a/packages/runtime/test/plugin/mutation-hooks.test.ts b/packages/runtime/test/plugin/entity-mutation-hooks.test.ts similarity index 53% rename from packages/runtime/test/plugin/mutation-hooks.test.ts rename to packages/runtime/test/plugin/entity-mutation-hooks.test.ts index c249e0a6..6ca7f112 100644 --- a/packages/runtime/test/plugin/mutation-hooks.test.ts +++ b/packages/runtime/test/plugin/entity-mutation-hooks.test.ts @@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { ZenStackClient, type ClientContract } from '../../src'; import { schema } from '../schemas/basic'; -describe('Entity lifecycle tests', () => { +describe('Entity mutation hooks tests', () => { let _client: ClientContract; beforeEach(async () => { @@ -24,23 +24,25 @@ describe('Entity lifecycle tests', () => { const client = _client.$use({ id: 'test', - 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); - } - expect(args.entities).toBeUndefined(); - }, - afterEntityMutation(args) { - afterCalled[args.action] = true; - expect(args.beforeMutationEntities).toBeUndefined(); - expect(args.afterMutationEntities).toBeUndefined(); + 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); + } + expect(args.entities).toBeUndefined(); + }, + afterEntityMutation(args) { + afterCalled[args.action] = true; + expect(args.beforeMutationEntities).toBeUndefined(); + expect(args.afterMutationEntities).toBeUndefined(); + }, }, }); @@ -71,17 +73,19 @@ describe('Entity lifecycle tests', () => { const client = _client.$use({ id: 'test', - mutationInterceptionFilter: (args) => { - return { - intercept: args.action !== 'delete', - }; - }, - beforeEntityMutation(args) { - beforeCalled[args.action] = true; - expect(args.entities).toBeUndefined(); - }, - afterEntityMutation(args) { - afterCalled[args.action] = true; + onEntityMutation: { + mutationInterceptionFilter: (args) => { + return { + intercept: args.action !== 'delete', + }; + }, + beforeEntityMutation(args) { + beforeCalled[args.action] = true; + expect(args.entities).toBeUndefined(); + }, + afterEntityMutation(args) { + afterCalled[args.action] = true; + }, }, }); @@ -109,32 +113,34 @@ describe('Entity lifecycle tests', () => { it('can intercept with loading before mutation entities', async () => { const client = _client.$use({ id: 'test', - mutationInterceptionFilter: () => { - return { - intercept: true, - loadBeforeMutationEntities: true, - }; - }, - beforeEntityMutation(args) { - if (args.action === 'update' || args.action === 'delete') { - expect(args.entities).toEqual([ - expect.objectContaining({ - email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', - }), - ]); - } else { - expect(args.entities).toBeUndefined(); - } - }, - 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', - }), - ]); - } - expect(args.afterMutationEntities).toBeUndefined(); + onEntityMutation: { + mutationInterceptionFilter: () => { + return { + intercept: true, + loadBeforeMutationEntities: true, + }; + }, + beforeEntityMutation(args) { + if (args.action === 'update' || args.action === 'delete') { + expect(args.entities).toEqual([ + expect.objectContaining({ + email: args.action === 'update' ? 'u1@test.com' : 'u3@test.com', + }), + ]); + } else { + expect(args.entities).toBeUndefined(); + } + }, + 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', + }), + ]); + } + expect(args.afterMutationEntities).toBeUndefined(); + }, }, }); @@ -156,30 +162,32 @@ describe('Entity lifecycle tests', () => { let userUpdateIntercepted = false; const client = _client.$use({ id: 'test', - mutationInterceptionFilter: () => { - return { - intercept: true, - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { - if (args.action === 'create' || args.action === 'update') { - if (args.action === 'create') { - userCreateIntercepted = true; - } - if (args.action === 'update') { - userUpdateIntercepted = true; + onEntityMutation: { + mutationInterceptionFilter: () => { + return { + intercept: true, + loadAfterMutationEntities: true, + }; + }, + afterEntityMutation(args) { + if (args.action === 'create' || args.action === 'update') { + if (args.action === 'create') { + userCreateIntercepted = true; + } + if (args.action === 'update') { + userUpdateIntercepted = true; + } + expect(args.afterMutationEntities).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + email: args.action === 'create' ? 'u1@test.com' : 'u2@test.com', + }), + ]), + ); + } else { + expect(args.afterMutationEntities).toBeUndefined(); } - expect(args.afterMutationEntities).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - email: args.action === 'create' ? 'u1@test.com' : 'u2@test.com', - }), - ]), - ); - } else { - expect(args.afterMutationEntities).toBeUndefined(); - } + }, }, }); @@ -202,44 +210,46 @@ describe('Entity lifecycle tests', () => { const client = _client.$use({ id: 'test', - mutationInterceptionFilter: () => { - return { - intercept: true, - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { - if (args.action === 'create') { - userCreateIntercepted = true; - expect(args.afterMutationEntities).toEqual( - expect.arrayContaining([ - expect.objectContaining({ email: 'u1@test.com' }), - expect.objectContaining({ email: 'u2@test.com' }), - ]), - ); - } else if (args.action === 'update') { - userUpdateIntercepted = true; - expect(args.afterMutationEntities).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - email: 'u1@test.com', - name: 'A user', - }), - expect.objectContaining({ - email: 'u1@test.com', - name: 'A user', - }), - ]), - ); - } else if (args.action === 'delete') { - userDeleteIntercepted = true; - expect(args.afterMutationEntities).toEqual( - expect.arrayContaining([ - expect.objectContaining({ email: 'u1@test.com' }), - expect.objectContaining({ email: 'u2@test.com' }), - ]), - ); - } + onEntityMutation: { + mutationInterceptionFilter: () => { + return { + intercept: true, + loadAfterMutationEntities: true, + }; + }, + afterEntityMutation(args) { + if (args.action === 'create') { + userCreateIntercepted = true; + expect(args.afterMutationEntities).toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'u1@test.com' }), + expect.objectContaining({ email: 'u2@test.com' }), + ]), + ); + } else if (args.action === 'update') { + userUpdateIntercepted = true; + expect(args.afterMutationEntities).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; + expect(args.afterMutationEntities).toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'u1@test.com' }), + expect.objectContaining({ email: 'u2@test.com' }), + ]), + ); + } + }, }, }); @@ -260,23 +270,25 @@ describe('Entity lifecycle tests', () => { let post2Intercepted = false; const client = _client.$use({ id: 'test', - mutationInterceptionFilter: (args) => { - return { - intercept: args.action === 'create' || args.action === 'update', - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { - if (args.action === 'create') { - if (args.model === 'Post') { - if ((args.afterMutationEntities![0] as any).title === 'Post1') { - post1Intercepted = true; - } - if ((args.afterMutationEntities![0] as any).title === 'Post2') { - post2Intercepted = true; + onEntityMutation: { + mutationInterceptionFilter: (args) => { + return { + intercept: args.action === 'create' || args.action === 'update', + loadAfterMutationEntities: true, + }; + }, + afterEntityMutation(args) { + if (args.action === 'create') { + if (args.model === 'Post') { + if ((args.afterMutationEntities![0] as any).title === 'Post1') { + post1Intercepted = true; + } + if ((args.afterMutationEntities![0] as any).title === 'Post2') { + post2Intercepted = true; + } } } - } + }, }, }); @@ -303,9 +315,11 @@ describe('Entity lifecycle tests', () => { const client = _client.$use({ id: 'test', - afterEntityMutation() { - intercepted = true; - throw new Error('trigger rollback'); + onEntityMutation: { + afterEntityMutation() { + intercepted = true; + throw new Error('trigger rollback'); + }, }, }); @@ -322,8 +336,10 @@ describe('Entity lifecycle tests', () => { const client = _client.$use({ id: 'test', - afterEntityMutation() { - intercepted = true; + onEntityMutation: { + afterEntityMutation() { + intercepted = true; + }, }, }); @@ -347,15 +363,17 @@ describe('Entity lifecycle tests', () => { const client = _client.$use({ id: 'test', - mutationInterceptionFilter: () => { - return { - intercept: true, - loadBeforeMutationEntities: true, - loadAfterMutationEntities: true, - }; - }, - afterEntityMutation(args) { - triggered.push(args); + onEntityMutation: { + mutationInterceptionFilter: () => { + return { + intercept: true, + loadBeforeMutationEntities: true, + loadAfterMutationEntities: true, + }; + }, + afterEntityMutation(args) { + triggered.push(args); + }, }, }); diff --git a/packages/runtime/test/plugin/kysely-on-query.test.ts b/packages/runtime/test/plugin/on-kysely-query.test.ts similarity index 99% rename from packages/runtime/test/plugin/kysely-on-query.test.ts rename to packages/runtime/test/plugin/on-kysely-query.test.ts index 92d2df21..7e0ac024 100644 --- a/packages/runtime/test/plugin/kysely-on-query.test.ts +++ b/packages/runtime/test/plugin/on-kysely-query.test.ts @@ -4,7 +4,7 @@ import { beforeEach, describe, expect, it } from 'vitest'; import { ZenStackClient, type ClientContract } from '../../src/client'; import { schema } from '../schemas/basic'; -describe('Kysely onQuery tests', () => { +describe('On kysely query tests', () => { let _client: ClientContract; beforeEach(async () => { diff --git a/packages/runtime/test/plugin/on-query-hooks.test.ts b/packages/runtime/test/plugin/on-query-hooks.test.ts new file mode 100644 index 00000000..3e4c478b --- /dev/null +++ b/packages/runtime/test/plugin/on-query-hooks.test.ts @@ -0,0 +1,252 @@ +import SQLite from 'better-sqlite3'; +import { SqliteDialect } from 'kysely'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { definePlugin, ZenStackClient, type ClientContract } from '../../src/client'; +import { schema } from '../schemas/basic'; + +describe('On query hooks tests', () => { + let _client: ClientContract; + + beforeEach(async () => { + _client = new ZenStackClient(schema, { + dialect: new SqliteDialect({ database: new SQLite(':memory:') }), + }); + await _client.$pushSchema(); + }); + + afterEach(async () => { + await _client?.$disconnect(); + }); + + it('supports simple interception', async () => { + const user = await _client.user.create({ + data: { email: 'u1@test.com' }, + }); + + let findHookCalled = false; + let updateHookCalled = false; + + const client = _client.$use({ + id: 'test-plugin', + onQuery: (ctx) => { + if (ctx.operation === 'findFirst') { + findHookCalled = true; + expect(ctx).toMatchObject({ + model: 'User', + operation: 'findFirst', + args: { where: { id: user.id } }, + }); + } else if (ctx.operation === 'update') { + updateHookCalled = true; + } + return ctx.proceed(ctx.args); + }, + }); + + await expect( + client.user.findFirst({ + where: { id: user.id }, + }), + ).resolves.toMatchObject(user); + expect(findHookCalled).toBe(true); + expect(updateHookCalled).toBe(false); + }); + + it('supports all models interception', async () => { + const user = await _client.user.create({ + data: { email: 'u1@test.com' }, + }); + + let hooksCalled = false; + const client = _client.$use({ + id: 'test-plugin', + onQuery: (ctx) => { + if (ctx.operation === 'findFirst') { + hooksCalled = true; + expect(ctx.model).toBe('User'); + } + return ctx.proceed(ctx.args); + }, + }); + await expect( + client.user.findFirst({ + where: { id: user.id }, + }), + ).resolves.toMatchObject(user); + expect(hooksCalled).toBe(true); + }); + + it('supports all operations interception', async () => { + const user = await _client.user.create({ + data: { email: 'u1@test.com' }, + }); + + let hooksCalled = false; + const client = _client.$use({ + id: 'test-plugin', + onQuery: (ctx) => { + hooksCalled = true; + expect(ctx.model).toBe('User'); + expect(ctx.operation).toBe('findFirst'); + return ctx.proceed(ctx.args); + }, + }); + await expect( + client.user.findFirst({ + where: { id: user.id }, + }), + ).resolves.toMatchObject(user); + expect(hooksCalled).toBe(true); + }); + + it('supports modifying query args', async () => { + const user = await _client.user.create({ + data: { email: 'u1@test.com' }, + }); + + let hooksCalled = false; + const client = _client.$use({ + id: 'test-plugin', + onQuery: async (ctx) => { + if (ctx.model === 'User' && ctx.operation === 'findFirst') { + hooksCalled = true; + return ctx.proceed({ where: { id: 'non-exist' } }); + } else { + return ctx.proceed(ctx.args); + } + }, + }); + + await expect( + client.user.findFirst({ + where: { id: user.id }, + }), + ).toResolveNull(); + expect(hooksCalled).toBe(true); + }); + + it('supports modifying query result', async () => { + const user = await _client.user.create({ + data: { email: 'u1@test.com' }, + }); + + let hooksCalled = false; + const client = _client.$use({ + id: 'test-plugin', + onQuery: async (ctx) => { + if (ctx.model === 'User' && ctx.operation === 'findFirst') { + hooksCalled = true; + const result = await ctx.proceed(ctx.args); + (result as any).happy = true; + return result; + } else { + return ctx.proceed(ctx.args); + } + }, + }); + + await expect( + client.user.findFirst({ + where: { id: user.id }, + }), + ).resolves.toMatchObject({ + ...user, + happy: true, + }); + expect(hooksCalled).toBe(true); + }); + + it('persists the effect without transaction', async () => { + let hooksCalled = false; + const client = _client.$use({ + id: 'test-plugin', + onQuery: async (ctx) => { + if (ctx.model === 'User' && ctx.operation === 'create') { + hooksCalled = true; + await ctx.proceed(ctx.args); + throw new Error('trigger error'); + } else { + return ctx.proceed(ctx.args); + } + }, + }); + + try { + await client.user.create({ + data: { id: '1', email: 'u1@test.com' }, + }); + } catch { + // no-op + } + + expect(hooksCalled).toBe(true); + await expect( + _client.user.findFirst({ + where: { id: '1' }, + }), + ).toResolveTruthy(); + }); + + it('supports plugin encapsulation', async () => { + const user = await _client.user.create({ + data: { email: 'u1@test.com' }, + }); + + let findHookCalled = false; + + const plugin = definePlugin({ + id: 'test-plugin', + onQuery: (ctx) => { + findHookCalled = true; + return ctx.proceed(ctx.args); + }, + }); + + const client = _client.$use(plugin); + + await expect( + client.user.findFirst({ + where: { id: user.id }, + }), + ).resolves.toMatchObject(user); + expect(findHookCalled).toBe(true); + }); + + it('propagates overridden args across multiple onQuery plugins', async () => { + const user = await _client.user.create({ data: { email: 'u1@test.com' } }); + + let earlierSawOverridden = false; + + // Plugin A (registered first) should see the overridden args from Plugin B + const clientA = _client.$use({ + id: 'plugin-a', + onQuery: (ctx) => { + if (ctx.model === 'User' && ctx.operation === 'findFirst') { + // expect overridden where clause from Plugin B + // eslint-disable-next-line @typescript-eslint/no-explicit-any + earlierSawOverridden = (ctx.args as any)?.where?.id === 'non-exist'; + } + return ctx.proceed(ctx.args); + }, + }); + + // Plugin B (registered second) overrides args + const client = clientA.$use({ + id: 'plugin-b', + onQuery: (ctx) => { + if (ctx.model === 'User' && ctx.operation === 'findFirst') { + return ctx.proceed({ where: { id: 'non-exist' } }); + } + return ctx.proceed(ctx.args); + }, + }); + + await expect( + client.user.findFirst({ + where: { id: user.id }, + }), + ).toResolveNull(); + + expect(earlierSawOverridden).toBe(true); + }); +}); diff --git a/packages/runtime/test/plugin/query-lifecycle.test.ts b/packages/runtime/test/plugin/query-lifecycle.test.ts deleted file mode 100644 index 6cfcfe72..00000000 --- a/packages/runtime/test/plugin/query-lifecycle.test.ts +++ /dev/null @@ -1,284 +0,0 @@ -import SQLite from 'better-sqlite3'; -import { SqliteDialect } from 'kysely'; -import { beforeEach, describe, expect, it } from 'vitest'; -import { definePlugin, ZenStackClient, type ClientContract } from '../../src/client'; -import { schema } from '../schemas/basic'; - -describe('Query interception tests', () => { - let _client: ClientContract; - - beforeEach(async () => { - _client = new ZenStackClient(schema, { - dialect: new SqliteDialect({ database: new SQLite(':memory:') }), - }); - await _client.$pushSchema(); - }); - - it('supports simple interception', async () => { - const user = await _client.user.create({ - data: { email: 'u1@test.com' }, - }); - - let findHookCalled = false; - let updateHookCalled = false; - - const client = _client.$use({ - id: 'test-plugin', - onQuery: { - user: { - findFirst: (ctx) => { - findHookCalled = true; - expect(ctx).toMatchObject({ - model: 'User', - operation: 'findFirst', - args: { where: { id: user.id } }, - }); - return ctx.query(ctx.args); - }, - update: (ctx) => { - updateHookCalled = true; - return ctx.query(ctx.args); - }, - }, - }, - }); - - await expect( - client.user.findFirst({ - where: { id: user.id }, - }), - ).resolves.toMatchObject(user); - expect(findHookCalled).toBe(true); - expect(updateHookCalled).toBe(false); - }); - - it('supports all models interception', async () => { - const user = await _client.user.create({ - data: { email: 'u1@test.com' }, - }); - - let hooksCalled = false; - const client = _client.$use({ - id: 'test-plugin', - onQuery: { - $allModels: { - findFirst: (ctx) => { - hooksCalled = true; - expect(ctx.model).toBe('User'); - return ctx.query(ctx.args); - }, - }, - }, - }); - await expect( - client.user.findFirst({ - where: { id: user.id }, - }), - ).resolves.toMatchObject(user); - expect(hooksCalled).toBe(true); - }); - - it('supports all operations interception', async () => { - const user = await _client.user.create({ - data: { email: 'u1@test.com' }, - }); - - let hooksCalled = false; - const client = _client.$use({ - id: 'test-plugin', - onQuery: { - $allModels: { - $allOperations: (ctx) => { - hooksCalled = true; - expect(ctx.model).toBe('User'); - expect(ctx.operation).toBe('findFirst'); - return ctx.query(ctx.args); - }, - }, - }, - }); - await expect( - client.user.findFirst({ - where: { id: user.id }, - }), - ).resolves.toMatchObject(user); - expect(hooksCalled).toBe(true); - }); - - it('supports modifying query args', async () => { - const user = await _client.user.create({ - data: { email: 'u1@test.com' }, - }); - - let hooksCalled = false; - const client = _client.$use({ - id: 'test-plugin', - onQuery: { - user: { - findFirst: async (ctx) => { - hooksCalled = true; - return ctx.query({ where: { id: 'non-exist' } }); - }, - }, - }, - }); - - await expect( - client.user.findFirst({ - where: { id: user.id }, - }), - ).toResolveNull(); - expect(hooksCalled).toBe(true); - }); - - it('supports modifying query result', async () => { - const user = await _client.user.create({ - data: { email: 'u1@test.com' }, - }); - - let hooksCalled = false; - const client = _client.$use({ - id: 'test-plugin', - onQuery: { - user: { - findFirst: async (ctx) => { - hooksCalled = true; - const result = await ctx.query(ctx.args); - (result as any).happy = true; - return result; - }, - }, - }, - }); - - await expect( - client.user.findFirst({ - where: { id: user.id }, - }), - ).resolves.toMatchObject({ - ...user, - happy: true, - }); - expect(hooksCalled).toBe(true); - }); - - it('supports multiple interceptors', async () => { - const user1 = await _client.user.create({ - data: { email: 'u1@test.com' }, - }); - const user2 = await _client.user.create({ - data: { email: 'u2@test.com' }, - }); - const user3 = await _client.user.create({ - data: { email: 'u3@test.com' }, - }); - - let hooks1Called = false; - let hooks2Called = false; - const client = _client - .$use({ - id: 'test-plugin', - onQuery: { - user: { - findFirst: async (ctx) => { - hooks1Called = true; - console.log('Plugin1 ready to proceed'); - const r = await ctx.query({ where: { id: user2.id } }); - (r as any).happy = true; - (r as any).source = 'plugin1'; - console.log('Plugin1 ready to return', r); - return r; - }, - }, - }, - }) - .$use({ - id: 'test-plugin-2', - onQuery: { - user: { - findFirst: async (ctx) => { - hooks2Called = true; - console.log('Plugin2 ready to proceed'); - const r = await ctx.query({ where: { id: user3.id } }); - (r as any).source = 'plugin2'; - console.log('Plugin2 ready to return', r); - return r; - }, - }, - }, - }); - - // call order: - // 1. plugin2 pre proceed - // 2. plugin1 pre proceed - // 3. plugin1 post proceed - // 4. plugin2 post proceed - await expect( - client.user.findFirst({ - where: { id: user1.id }, - }), - ).resolves.toMatchObject({ ...user2, happy: true, source: 'plugin2' }); - expect(hooks1Called).toBe(true); - expect(hooks2Called).toBe(true); - }); - - it('persists the effect without transaction', async () => { - let hooksCalled = false; - const client = _client.$use({ - id: 'test-plugin', - onQuery: { - user: { - create: async (ctx) => { - hooksCalled = true; - await ctx.query(ctx.args); - throw new Error('trigger error'); - }, - }, - }, - }); - - try { - await client.user.create({ - data: { id: '1', email: 'u1@test.com' }, - }); - } catch { - // no-op - } - - expect(hooksCalled).toBe(true); - await expect( - _client.user.findFirst({ - where: { id: '1' }, - }), - ).toResolveTruthy(); - }); - - it('supports plugin encapsulation', async () => { - const user = await _client.user.create({ - data: { email: 'u1@test.com' }, - }); - - let findHookCalled = false; - - const plugin = definePlugin({ - id: 'test-plugin', - onQuery: { - user: { - findFirst: (ctx) => { - findHookCalled = true; - return ctx.query(ctx.args); - }, - }, - }, - }); - - const client = _client.$use(plugin); - - await expect( - client.user.findFirst({ - where: { id: user.id }, - }), - ).resolves.toMatchObject(user); - expect(findHookCalled).toBe(true); - }); -}); diff --git a/packages/runtime/test/policy/client-extensions.test.ts b/packages/runtime/test/policy/client-extensions.test.ts index 813ab518..f1f916b4 100644 --- a/packages/runtime/test/policy/client-extensions.test.ts +++ b/packages/runtime/test/policy/client-extensions.test.ts @@ -23,14 +23,10 @@ describe('client extensions tests for policies', () => { const ext = definePlugin({ id: 'prisma-extension-queryOverride', - onQuery: { - model: { - findMany({ args, query }: any) { - args = args ?? {}; - args.where = { ...args.where, y: { lt: 300 } }; - return query(args); - }, - }, + onQuery: async ({ args, proceed }: any) => { + args = args ?? {}; + args.where = { ...args.where, y: { lt: 300 } }; + return proceed(args); }, }); @@ -58,14 +54,10 @@ describe('client extensions tests for policies', () => { const ext = definePlugin({ id: 'prisma-extension-queryOverride', - onQuery: { - $allModels: { - async findMany({ args, query }: any) { - args = args ?? {}; - args.where = { ...args.where, y: { lt: 300 } }; - return query(args); - }, - }, + onQuery: async ({ args, proceed }: any) => { + args = args ?? {}; + args.where = { ...args.where, y: { lt: 300 } }; + return proceed(args); }, }); @@ -93,14 +85,10 @@ describe('client extensions tests for policies', () => { const ext = definePlugin({ id: 'prisma-extension-queryOverride', - onQuery: { - model: { - async $allOperations({ args, query }: any) { - args = args ?? {}; - args.where = { ...args.where, y: { lt: 300 } }; - return query(args); - }, - }, + onQuery: async ({ args, proceed }: any) => { + args = args ?? {}; + args.where = { ...args.where, y: { lt: 300 } }; + return proceed(args); }, }); @@ -128,14 +116,10 @@ describe('client extensions tests for policies', () => { const ext = definePlugin({ id: 'prisma-extension-queryOverride', - onQuery: { - $allModels: { - $allOperations({ args, query }: any) { - args = args ?? {}; - args.where = { ...args.where, y: { lt: 300 } }; - return query(args); - }, - }, + onQuery: async ({ args, proceed }: any) => { + args = args ?? {}; + args.where = { ...args.where, y: { lt: 300 } }; + return proceed(args); }, }); @@ -161,16 +145,12 @@ describe('client extensions tests for policies', () => { const ext = definePlugin({ id: 'prisma-extension-resultMutation', - onQuery: { - model: { - async findMany({ args, query }: any) { - const r: any = await query(args); - for (let i = 0; i < r.length; i++) { - r[i].value = r[i].value + 1; - } - return r; - }, - }, + onQuery: async ({ args, proceed }: any) => { + const r: any = await proceed(args); + for (let i = 0; i < r.length; i++) { + r[i].value = r[i].value + 1; + } + return r; }, }); diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 57415ab2..73120876 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index 91bdd7f9..08f82e6f 100644 --- a/packages/tanstack-query/package.json +++ b/packages/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 41c2813a..9bd7778a 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index c4ffcca2..31e6f850 100644 --- a/packages/typescript-config/package.json +++ b/packages/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "private": true, "license": "MIT" } diff --git a/packages/vitest-config/package.json b/packages/vitest-config/package.json index 7d84eb0c..cfa33a8a 100644 --- a/packages/vitest-config/package.json +++ b/packages/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "private": true, "license": "MIT", "exports": { diff --git a/packages/zod/package.json b/packages/zod/package.json index df55bfc2..c80efbc3 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "", "type": "module", "main": "index.js", diff --git a/samples/blog/main.ts b/samples/blog/main.ts index 1f126a57..46cb754e 100644 --- a/samples/blog/main.ts +++ b/samples/blog/main.ts @@ -17,15 +17,11 @@ async function main() { }, }).$use({ id: 'cost-logger', - onQuery: { - $allModels: { - $allOperations: async ({ model, operation, args, query }) => { - const start = Date.now(); - const result = await query(args); - console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); - return result; - }, - }, + onQuery: async ({ model, operation, args, proceed }) => { + const start = Date.now(); + const result = await proceed(args); + console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); + return result; }, }); diff --git a/samples/blog/package.json b/samples/blog/package.json index 587b4624..340dd20e 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "description": "", "main": "index.js", "scripts": { diff --git a/tests/e2e/package.json b/tests/e2e/package.json index dc4a5c0d..dc21249c 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.0.0-alpha.23", + "version": "3.0.0-alpha.24", "private": true, "type": "module", "scripts": {