diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 65714616..6097f870 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -18,7 +18,7 @@ }, { "label": "Build all - watch", - "command": "pnpm watch", + "command": "turbo watch build", "type": "shell", "group": { "kind": "build" @@ -28,6 +28,16 @@ "id": "server-process" } }, + { + "label": "Lint all", + "command": "pnpm lint", + "type": "shell", + "icon": { + "color": "terminal.ansiYellow", + "id": "server-process" + }, + "problemMatcher": [] + }, { "label": "Test all", "command": "pnpm test", diff --git a/NEW-FEATURES.md b/NEW-FEATURES.md deleted file mode 100644 index b2aafc82..00000000 --- a/NEW-FEATURES.md +++ /dev/null @@ -1,2 +0,0 @@ -- Cross-field comparison (for read and mutations) -- Computed fields diff --git a/README.md b/README.md index 8cc0fc1a..af01e34a 100644 --- a/README.md +++ b/README.md @@ -280,16 +280,20 @@ You can use a plugin to achieve the following goals: #### 1. ORM query interception -ORM query interception allows you to intercept the high-level ORM API calls. +ORM query interception allows you to intercept the high-level ORM API calls. The interceptor's configuration is compatible with Prisma's [query client extension](https://www.prisma.io/docs/orm/prisma-client/client-extensions/query). ```ts client.$use({ id: 'cost-logger', - async onQuery({ model, operation, proceed, queryArgs }) { - const start = Date.now(); - const result = await proceed(queryArgs); - console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); - return result; + 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; + }, + }, }, }); ``` diff --git a/TODO.md b/TODO.md index 8f464ed5..9c61a5f1 100644 --- a/TODO.md +++ b/TODO.md @@ -51,10 +51,11 @@ - [x] Count - [x] Aggregate - [x] Group by + - [ ] Raw queries - [ ] Extensions - [x] Query builder API - [x] Computed fields - - [ ] Prisma client extension + - [x] Prisma client extension - [ ] Misc - [x] JSDoc for CRUD methods - [x] Cache validation schemas @@ -66,7 +67,8 @@ - [ ] Error system - [x] Custom table name - [x] Custom field name - - [ ] Implement changesets + - [ ] Strict undefined checks + - [ ] Benchmark - [ ] Polymorphism - [ ] Validation - [ ] Access Policy diff --git a/package.json b/package.json index 24888806..531ff97f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "ZenStack", "packageManager": "pnpm@10.12.1", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index a63ca4d4..e93188aa 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.4", + "version": "3.0.0-alpha.5", "type": "module", "author": { "name": "ZenStack Team" @@ -22,6 +22,7 @@ }, "scripts": { "build": "tsup-node", + "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "test": "vitest run", "pack": "pnpm pack" @@ -29,13 +30,13 @@ "dependencies": { "@zenstackhq/language": "workspace:*", "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/common-helpers": "workspace:*", "async-exit-hook": "^2.0.1", "colors": "1.4.0", "commander": "^8.3.0", "langium": "catalog:", "ora": "^5.4.1", "package-manager-detector": "^1.3.0", - "tiny-invariant": "^1.3.3", "ts-pattern": "catalog:" }, "peerDependencies": { diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index f377e2f3..9c19d968 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -1,10 +1,9 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { isPlugin, LiteralExpr, type Model } from '@zenstackhq/language/ast'; -import type { CliGenerator } from '@zenstackhq/runtime/client'; -import { PrismaSchemaGenerator, TsSchemaGenerator } from '@zenstackhq/sdk'; +import { PrismaSchemaGenerator, TsSchemaGenerator, type CliGenerator } from '@zenstackhq/sdk'; import colors from 'colors'; import fs from 'node:fs'; import path from 'node:path'; -import invariant from 'tiny-invariant'; import { getSchemaFile, loadSchemaDocument } from './action-utils'; type Options = { diff --git a/packages/common-helpers/eslint.config.js b/packages/common-helpers/eslint.config.js new file mode 100644 index 00000000..5698b991 --- /dev/null +++ b/packages/common-helpers/eslint.config.js @@ -0,0 +1,4 @@ +import config from '@zenstackhq/eslint-config/base.js'; + +/** @type {import("eslint").Linter.Config} */ +export default config; diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json new file mode 100644 index 00000000..028e6e6e --- /dev/null +++ b/packages/common-helpers/package.json @@ -0,0 +1,34 @@ +{ + "name": "@zenstackhq/common-helpers", + "version": "3.0.0-alpha.5", + "description": "ZenStack Common Helpers", + "type": "module", + "scripts": { + "build": "tsup-node", + "watch": "tsup-node --watch", + "lint": "eslint src --ext ts", + "pack": "pnpm pack" + }, + "keywords": [], + "author": "ZenStack Team", + "license": "MIT", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "devDependencies": { + "@zenstackhq/typescript-config": "workspace:*", + "@zenstackhq/eslint-config": "workspace:*" + } +} diff --git a/packages/common-helpers/src/index.ts b/packages/common-helpers/src/index.ts new file mode 100644 index 00000000..7f9c421b --- /dev/null +++ b/packages/common-helpers/src/index.ts @@ -0,0 +1,6 @@ +export * from './is-plain-object'; +export * from './lower-case-first'; +export * from './param-case'; +export * from './sleep'; +export * from './tiny-invariant'; +export * from './upper-case-first'; diff --git a/packages/common-helpers/src/is-plain-object.ts b/packages/common-helpers/src/is-plain-object.ts new file mode 100644 index 00000000..f5c13d6d --- /dev/null +++ b/packages/common-helpers/src/is-plain-object.ts @@ -0,0 +1,23 @@ +function isObject(o: unknown) { + return Object.prototype.toString.call(o) === '[object Object]'; +} + +export function isPlainObject(o: unknown) { + if (isObject(o) === false) return false; + + // If has modified constructor + const ctor = (o as { constructor: unknown }).constructor; + if (ctor === undefined) return true; + + // If has modified prototype + const prot = (ctor as { prototype: unknown }).prototype; + if (isObject(prot) === false) return false; + + // If constructor does not have an Object-specific method + if (Object.prototype.hasOwnProperty.call(prot, 'isPrototypeOf') === false) { + return false; + } + + // Most likely a plain Object + return true; +} diff --git a/packages/common-helpers/src/lower-case-first.ts b/packages/common-helpers/src/lower-case-first.ts new file mode 100644 index 00000000..a05a05be --- /dev/null +++ b/packages/common-helpers/src/lower-case-first.ts @@ -0,0 +1,3 @@ +export function lowerCaseFirst(input: string) { + return input.charAt(0).toLowerCase() + input.slice(1); +} diff --git a/packages/common-helpers/src/param-case.ts b/packages/common-helpers/src/param-case.ts new file mode 100644 index 00000000..3cb1f017 --- /dev/null +++ b/packages/common-helpers/src/param-case.ts @@ -0,0 +1,18 @@ +const DEFAULT_SPLIT_REGEXP_1 = /([a-z0-9])([A-Z])/g; +const DEFAULT_SPLIT_REGEXP_2 = /([A-Z])([A-Z][a-z])/g; +const DEFAULT_STRIP_REGEXP = /[^A-Z0-9]+/gi; + +export function paramCase(input: string) { + const result = input + .replace(DEFAULT_SPLIT_REGEXP_1, "$1\0$2") + .replace(DEFAULT_SPLIT_REGEXP_2, "$1\0$2") + .replace(DEFAULT_STRIP_REGEXP, "\0"); + + let start = 0; + let end = result.length; + + while (result.charAt(start) === "\0") start++; + while (result.charAt(end - 1) === "\0") end--; + + return result.slice(start, end).split("\0").map((str) => str.toLowerCase()).join("-"); +} diff --git a/packages/common-helpers/src/sleep.ts b/packages/common-helpers/src/sleep.ts new file mode 100644 index 00000000..5281ad0b --- /dev/null +++ b/packages/common-helpers/src/sleep.ts @@ -0,0 +1,5 @@ +export function sleep(timeout: number) { + return new Promise((resolve) => { + setTimeout(() => resolve(), timeout); + }); +} diff --git a/packages/common-helpers/src/tiny-invariant.ts b/packages/common-helpers/src/tiny-invariant.ts new file mode 100644 index 00000000..b4b2c216 --- /dev/null +++ b/packages/common-helpers/src/tiny-invariant.ts @@ -0,0 +1,14 @@ +const isProduction = process.env['NODE_ENV'] === 'production'; +const prefix = 'Invariant failed'; + +export function invariant(condition: unknown, message?: string): asserts condition { + if (condition) { + return; + } + + if (isProduction) { + throw new Error(prefix); + } + + throw new Error(message ? `${prefix}: ${message}` : prefix); +} diff --git a/packages/common-helpers/src/upper-case-first.ts b/packages/common-helpers/src/upper-case-first.ts new file mode 100644 index 00000000..16a6a6e4 --- /dev/null +++ b/packages/common-helpers/src/upper-case-first.ts @@ -0,0 +1,3 @@ +export function upperCaseFirst(input: string) { + return input.charAt(0).toUpperCase() + input.slice(1); +} diff --git a/packages/common-helpers/tsconfig.json b/packages/common-helpers/tsconfig.json new file mode 100644 index 00000000..7b9efb7a --- /dev/null +++ b/packages/common-helpers/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "@zenstackhq/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/common-helpers/tsup.config.ts b/packages/common-helpers/tsup.config.ts new file mode 100644 index 00000000..5a74a9dd --- /dev/null +++ b/packages/common-helpers/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + outDir: 'dist', + splitting: false, + sourcemap: true, + clean: true, + dts: true, + format: ['cjs', 'esm'], +}); diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 01505f38..a2a54f6b 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.4", + "version": "3.0.0-alpha.5", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index e54751ad..b0e1785a 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.4", + "version": "3.0.0-alpha.5", "type": "module", "private": true, "license": "MIT" diff --git a/packages/language/package.json b/packages/language/package.json index 5555b630..1eee56ea 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.4", + "version": "3.0.0-alpha.5", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 48108f49..3143bb84 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,10 +1,11 @@ { "name": "@zenstackhq/runtime", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "ZenStack Runtime", "type": "module", "scripts": { "build": "tsup-node", + "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "test": "vitest run && pnpm test:generate && pnpm test:typecheck", "test:generate": "tsx test/typing/generate.ts", @@ -64,13 +65,12 @@ } }, "dependencies": { + "@zenstackhq/common-helpers": "workspace:*", "@paralleldrive/cuid2": "^2.2.2", "decimal.js": "^10.4.3", - "is-plain-object": "^5.0.0", "json-stable-stringify": "^1.3.0", "kysely": "catalog:", "nanoid": "^5.0.9", - "tiny-invariant": "^1.3.3", "ts-pattern": "catalog:", "ulid": "^3.0.0", "utility-types": "^3.11.0", diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index 90831277..ce836d6e 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -1,3 +1,4 @@ +import { lowerCaseFirst } from '@zenstackhq/common-helpers'; import type { SqliteDialectConfig } from 'kysely'; import { DefaultConnectionProvider, @@ -12,8 +13,7 @@ import { import { match } from 'ts-pattern'; import type { GetModels, ProcedureDef, SchemaDef } from '../schema'; import type { AuthType } from '../schema/auth'; -import type { ClientConstructor, ClientContract } from './contract'; -import type { ModelOperations } from './crud-types'; +import type { ClientConstructor, ClientContract, ModelOperations } from './contract'; import { AggregateOperationHandler } from './crud/operations/aggregate'; import type { CrudOperation } from './crud/operations/base'; import { BaseOperationHandler } from './crud/operations/base'; @@ -40,7 +40,7 @@ import { ResultProcessor } from './result-processor'; */ export const ZenStackClient = function ( this: any, - schema: any, + schema: Schema, options: ClientOptions, ) { return new ClientImpl(schema, options); @@ -107,7 +107,7 @@ export class ClientImpl { this.kysely = new Kysely(this.kyselyProps); - return createClientProxy(this as unknown as ClientContract); + return createClientProxy(this); } public get $qb() { @@ -176,8 +176,16 @@ export class ClientImpl { $use(plugin: RuntimePlugin) { const newOptions = { ...this.options, - plugins: [...(this.options?.plugins ?? []), plugin], - } as ClientOptions; + plugins: [...(this.options.plugins ?? []), plugin], + }; + return new ClientImpl(this.schema, newOptions, this); + } + + $unuse(pluginId: string) { + const newOptions = { + ...this.options, + plugins: this.options.plugins?.filter((p) => p.id !== pluginId), + }; return new ClientImpl(this.schema, newOptions, this); } @@ -185,7 +193,7 @@ export class ClientImpl { const newOptions = { ...this.options, plugins: [] as RuntimePlugin[], - } as ClientOptions; + }; return new ClientImpl(this.schema, newOptions, this); } @@ -203,7 +211,7 @@ export class ClientImpl { } } -function createClientProxy(client: ClientContract): ClientImpl { +function createClientProxy(client: ClientImpl): ClientImpl { const inputValidator = new InputValidator(client.$schema); const resultProcessor = new ResultProcessor(client.$schema); @@ -216,7 +224,12 @@ function createClientProxy(client: ClientContract m.toLowerCase() === prop.toLowerCase()); if (model) { - return createModelCrudHandler(client, model as GetModels, inputValidator, resultProcessor); + return createModelCrudHandler( + client as ClientContract, + model as GetModels, + inputValidator, + resultProcessor, + ); } } @@ -254,18 +267,27 @@ function createModelCrudHandler plugin.onQuery!({ ...context, proceed: _proceed }); + if (plugin.onQuery && typeof plugin.onQuery === 'object') { + // for each model key or "$allModels" + for (const [_model, modelHooks] of Object.entries(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 }); + } + } + } + } + } + } } } diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index 0805029b..6a99af69 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -1,9 +1,31 @@ -import { type GetModels, type ProcedureDef, type SchemaDef } from '../schema'; import type { Decimal } from 'decimal.js'; +import { type GetModels, type ProcedureDef, type SchemaDef } from '../schema'; import type { AuthType } from '../schema/auth'; import type { OrUndefinedIf } from '../utils/type-utils'; -import type { ModelOperations, ModelResult } from './crud-types'; -import type { ClientOptions, HasComputedFields } from './options'; +import type { + AggregateArgs, + AggregateResult, + BatchResult, + CountArgs, + CountResult, + CreateArgs, + CreateManyAndReturnArgs, + CreateManyArgs, + DeleteArgs, + DeleteManyArgs, + FindArgs, + FindUniqueArgs, + GroupByArgs, + GroupByResult, + ModelResult, + SelectSubset, + Subset, + UpdateArgs, + UpdateManyAndReturnArgs, + UpdateManyArgs, + UpsertArgs, +} from './crud-types'; +import type { ClientOptions } from './options'; import type { RuntimePlugin } from './plugin'; import type { ToKysely } from './query-builder'; @@ -48,6 +70,11 @@ export type ClientContract = { */ $use(plugin: RuntimePlugin): ClientContract; + /** + * Returns a new client with the specified plugin removed. + */ + $unuse(pluginId: string): ClientContract; + /** * Returns a new client with all plugins removed. */ @@ -67,23 +94,21 @@ export type ClientContract = { [Key in GetModels as Uncapitalize]: ModelOperations; } & Procedures; -type MapType = T extends 'String' - ? string - : T extends 'Int' - ? number - : T extends 'Float' - ? number - : T extends 'BigInt' - ? bigint - : T extends 'Decimal' - ? Decimal - : T extends 'Boolean' - ? boolean - : T extends 'DateTime' - ? Date - : T extends GetModels - ? ModelResult - : unknown; +type _TypeMap = { + String: string; + Int: number; + Float: number; + BigInt: bigint; + Decimal: Decimal; + Boolean: boolean; + DateTime: Date; +}; + +type MapType = T extends keyof _TypeMap + ? _TypeMap[T] + : T extends GetModels + ? ModelResult + : unknown; export type Procedures = Schema['procedures'] extends Record @@ -108,9 +133,6 @@ type MapProcedureParams = { * Creates a new ZenStack client instance. */ export interface ClientConstructor { - new ( - schema: HasComputedFields extends false ? Schema : never, - ): ClientContract; new (schema: Schema, options: ClientOptions): ClientContract; } @@ -118,3 +140,561 @@ export interface ClientConstructor { * CRUD operations. */ export type CRUD = 'create' | 'read' | 'update' | 'delete'; + +//#region Model operations + +export interface ModelOperations> { + /** + * 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 client.user.findMany(); + * + * // find all users with name 'Alex' + * await client.user.findMany({ + * where: { + * name: 'Alex' + * } + * }); + * + * // select fields + * await client.user.findMany({ + * select: { + * name: true, + * email: true, + * } + * }); // result: `Array<{ name: string, email: string }>` + * + * // omit fields + * await client.user.findMany({ + * omit: { + * name: true, + * } + * }); // result: `Array<{ id: number; email: string; ... }>` + * + * // include relations (and all scalar fields) + * await client.user.findMany({ + * include: { + * posts: true, + * } + * }); // result: `Array<{ ...; posts: Post[] }>` + * + * // include relations with filter + * await client.user.findMany({ + * include: { + * posts: { + * where: { + * published: true + * } + * } + * } + * }); + * + * // pagination and sorting + * await client.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 client.user.findMany({ + * cursor: { id: 10 }, + * skip: 1, + * take: 10, + * orderBy: { id: 'asc' }, + * }); + * + * // distinct + * await client.user.findMany({ + * distinct: ['name'] + * }); + * + * // count all relations + * await client.user.findMany({ + * _count: true, + * }); // result: `{ _count: { posts: number; ... } }` + * + * // count selected relations + * await client.user.findMany({ + * _count: { select: { posts: true } }, + * }); // result: `{ _count: { posts: number } }` + * ``` + */ + findMany>( + args?: SelectSubset>, + ): Promise[]>; + + /** + * Returns a uniquely identified entity. + * @param args - query args + * @returns a single entity or null if not found + * @see {@link findMany} + */ + findUnique>( + args?: SelectSubset>, + ): Promise | 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>, + ): Promise>; + + /** + * Returns the first entity. + * @param args - query args + * @returns a single entity or null if not found + * @see {@link findMany} + */ + findFirst>( + args?: SelectSubset>, + ): Promise | 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>, + ): Promise>; + + /** + * Creates a new entity. + * @param args - create args + * @returns the created entity + * + * @example + * ```ts + * // simple create + * await client.user.create({ + * data: { name: 'Alex', email: 'alex@zenstack.dev' } + * }); + * + * // nested create with relation + * await client.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 client.user.create({ + * data: { + * email: 'alex@zenstack.dev', + * posts: { create: { title: 'Hello World' } } + * }, + * include: { posts: true } + * }); // result: `{ id: number; posts: Post[] }` + * + * // connect relations + * await client.user.create({ + * data: { + * email: 'alex@zenstack.dev', + * posts: { connect: { id: 1 } } + * } + * }); + * + * // connect relations, and create if not found + * await client.user.create({ + * data: { + * email: 'alex@zenstack.dev', + * posts: { + * connectOrCreate: { + * where: { id: 1 }, + * create: { title: 'Hello World' } + * } + * } + * } + * }); + * ``` + */ + create>( + args: SelectSubset>, + ): Promise>; + + /** + * 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 client.user.createMany({ + * data: [ + * { name: 'Alex', email: 'alex@zenstack.dev' }, + * { name: 'John', email: 'john@zenstack.dev' } + * ] + * }); + * + * // skip items that cause unique constraint violation + * await client.user.createMany({ + * data: [ + * { name: 'Alex', email: 'alex@zenstack.dev' }, + * { name: 'John', email: 'john@zenstack.dev' } + * ], + * skipDuplicates: true + * }); + * ``` + */ + createMany>( + args?: SelectSubset>, + ): Promise; + + /** + * 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 client.user.createManyAndReturn({ + * data: [ + * { name: 'Alex', email: 'alex@zenstack.dev' }, + * { name: 'John', email: 'john@zenstack.dev' } + * ], + * select: { id: true, email: true } + * }); + * ``` + */ + createManyAndReturn>( + args?: SelectSubset>, + ): Promise[]>; + + /** + * 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 client.user.update({ + * where: { id: 1 }, + * data: { name: 'Alex' } + * }); + * + * // connect a relation + * await client.user.update({ + * where: { id: 1 }, + * data: { posts: { connect: { id: 1 } } } + * }); + * + * // connect relation, and create if not found + * await client.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 client.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * createMany: { + * data: [{ title: 'Hello World' }, { title: 'Hello World 2' }], + * } + * } + * } + * }); + * + * // disconnect a one-to-many relation + * await client.user.update({ + * where: { id: 1 }, + * data: { posts: { disconnect: { id: 1 } } } + * }); + * + * // disconnect a one-to-one relation + * await client.user.update({ + * where: { id: 1 }, + * data: { profile: { disconnect: true } } + * }); + * + * // replace a relation (only available for one-to-many relations) + * await client.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * set: [{ id: 1 }, { id: 2 }] + * } + * } + * }); + * + * // update a relation + * await client.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * update: { where: { id: 1 }, data: { title: 'Hello World' } } + * } + * } + * }); + * + * // upsert a relation + * await client.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 client.user.update({ + * where: { id: 1 }, + * data: { + * posts: { + * updateMany: { + * where: { published: true }, + * data: { title: 'Hello World' } + * } + * } + * } + * }); + * + * // delete a one-to-many relation + * await client.user.update({ + * where: { id: 1 }, + * data: { posts: { delete: { id: 1 } } } + * }); + * + * // delete a one-to-one relation + * await client.user.update({ + * where: { id: 1 }, + * data: { profile: { delete: true } } + * }); + * ``` + */ + update>( + args: SelectSubset>, + ): Promise>; + + /** + * 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 client.user.updateMany({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' } + * }); + * + * // limit the number of updated entities + * await client.user.updateMany({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' }, + * limit: 10 + * }); + */ + updateMany>( + args: Subset>, + ): Promise; + + /** + * 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 client.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 client.user.updateManyAndReturn({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' }, + * limit: 10 + * }); + * ``` + */ + updateManyAndReturn>( + args: Subset>, + ): Promise[]>; + + /** + * Creates or updates an entity. + * @param args - upsert args + * @returns the upserted entity + * + * @example + * ```ts + * // upsert an entity + * await client.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>, + ): Promise>; + + /** + * 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 client.user.delete({ + * where: { id: 1 } + * }); + * + * // delete an entity and return selected fields + * await client.user.delete({ + * where: { id: 1 }, + * select: { id: true, email: true } + * }); // result: `{ id: string; email: string }` + * ``` + */ + delete>( + args: SelectSubset>, + ): Promise>; + + /** + * Deletes multiple entities. + * @param args - delete args + * @returns count of deleted entities: `{ count: number }` + * + * @example + * ```ts + * // delete many entities + * await client.user.deleteMany({ + * where: { email: { endsWith: '@zenstack.dev' } } + * }); + * + * // limit the number of deleted entities + * await client.user.deleteMany({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * limit: 10 + * }); + * ``` + */ + deleteMany>( + args?: Subset>, + ): Promise; + + /** + * Counts rows or field values. + * @param args - count args + * @returns `number`, or an object containing count of selected relations + * + * @example + * ```ts + * // count all + * await client.user.count(); + * + * // count with a filter + * await client.user.count({ where: { email: { endsWith: '@zenstack.dev' } } }); + * + * // count rows and field values + * await client.user.count({ + * select: { _all: true, email: true } + * }); // result: `{ _all: number, email: number }` + */ + count>( + args?: Subset>, + ): Promise>; + + /** + * Aggregates rows. + * @param args - aggregation args + * @returns an object containing aggregated values + * + * @example + * ```ts + * // aggregate rows + * await client.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>, + ): Promise>; + + /** + * Groups rows by columns. + * @param args - groupBy args + * @returns an object containing grouped values + * + * @example + * ```ts + * // group by a field + * await client.profile.groupBy({ + * by: 'country', + * _count: true + * }); // result: `Array<{ country: string, _count: number }>` + * + * // group by multiple fields + * await client.profile.groupBy({ + * by: ['country', 'city'], + * _count: true + * }); // result: `Array<{ country: string, city: string, _count: number }>` + * + * // group by with sorting, the `orderBy` fields must be in the `by` list + * await client.profile.groupBy({ + * by: 'country', + * orderBy: { country: 'desc' } + * }); + * + * // group by with having (post-aggregation filter), the `having` fields must + * // be in the `by` list + * await client.profile.groupBy({ + * by: 'country', + * having: { country: 'US' } + * }); + */ + groupBy>( + args: Subset>, + ): Promise>; +} + +//#endregion diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index b78038ac..d7c7a3e8 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -615,7 +615,11 @@ type ConnectOrCreatePayload< create: CreateInput; }; -type CreateManyPayload, Without extends string = never> = { +export type CreateManyPayload< + Schema extends SchemaDef, + Model extends GetModels, + Without extends string = never, +> = { data: OrArray, Without> & Omit, Without>>; skipDuplicates?: boolean; }; @@ -1074,559 +1078,3 @@ type NestedDeleteManyInput< > = OrArray, true>>; // #endregion - -//#region Client API - -export interface ModelOperations> { - /** - * 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 client.user.findMany(); - * - * // find all users with name 'Alex' - * await client.user.findMany({ - * where: { - * name: 'Alex' - * } - * }); - * - * // select fields - * await client.user.findMany({ - * select: { - * name: true, - * email: true, - * } - * }); // result: `Array<{ name: string, email: string }>` - * - * // omit fields - * await client.user.findMany({ - * omit: { - * name: true, - * } - * }); // result: `Array<{ id: number; email: string; ... }>` - * - * // include relations (and all scalar fields) - * await client.user.findMany({ - * include: { - * posts: true, - * } - * }); // result: `Array<{ ...; posts: Post[] }>` - * - * // include relations with filter - * await client.user.findMany({ - * include: { - * posts: { - * where: { - * published: true - * } - * } - * } - * }); - * - * // pagination and sorting - * await client.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 client.user.findMany({ - * cursor: { id: 10 }, - * skip: 1, - * take: 10, - * orderBy: { id: 'asc' }, - * }); - * - * // distinct - * await client.user.findMany({ - * distinct: ['name'] - * }); - * - * // count all relations - * await client.user.findMany({ - * _count: true, - * }); // result: `{ _count: { posts: number; ... } }` - * - * // count selected relations - * await client.user.findMany({ - * _count: { select: { posts: true } }, - * }); // result: `{ _count: { posts: number } }` - * ``` - */ - findMany>( - args?: SelectSubset>, - ): Promise[]>; - - /** - * Returns a uniquely identified entity. - * @param args - query args - * @returns a single entity or null if not found - * @see {@link findMany} - */ - findUnique>( - args?: SelectSubset>, - ): Promise | 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>, - ): Promise>; - - /** - * Returns the first entity. - * @param args - query args - * @returns a single entity or null if not found - * @see {@link findMany} - */ - findFirst>( - args?: SelectSubset>, - ): Promise | 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>, - ): Promise>; - - /** - * Creates a new entity. - * @param args - create args - * @returns the created entity - * - * @example - * ```ts - * // simple create - * await client.user.create({ - * data: { name: 'Alex', email: 'alex@zenstack.dev' } - * }); - * - * // nested create with relation - * await client.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 client.user.create({ - * data: { - * email: 'alex@zenstack.dev', - * posts: { create: { title: 'Hello World' } } - * }, - * include: { posts: true } - * }); // result: `{ id: number; posts: Post[] }` - * - * // connect relations - * await client.user.create({ - * data: { - * email: 'alex@zenstack.dev', - * posts: { connect: { id: 1 } } - * } - * }); - * - * // connect relations, and create if not found - * await client.user.create({ - * data: { - * email: 'alex@zenstack.dev', - * posts: { - * connectOrCreate: { - * where: { id: 1 }, - * create: { title: 'Hello World' } - * } - * } - * } - * }); - * ``` - */ - create>( - args: SelectSubset>, - ): Promise>; - - /** - * 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 client.user.createMany({ - * data: [ - * { name: 'Alex', email: 'alex@zenstack.dev' }, - * { name: 'John', email: 'john@zenstack.dev' } - * ] - * }); - * - * // skip items that cause unique constraint violation - * await client.user.createMany({ - * data: [ - * { name: 'Alex', email: 'alex@zenstack.dev' }, - * { name: 'John', email: 'john@zenstack.dev' } - * ], - * skipDuplicates: true - * }); - * ``` - */ - createMany(args?: CreateManyPayload): Promise; - - /** - * 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 client.user.createManyAndReturn({ - * data: [ - * { name: 'Alex', email: 'alex@zenstack.dev' }, - * { name: 'John', email: 'john@zenstack.dev' } - * ], - * select: { id: true, email: true } - * }); - * ``` - */ - createManyAndReturn>( - args?: SelectSubset>, - ): Promise[]>; - - /** - * 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 client.user.update({ - * where: { id: 1 }, - * data: { name: 'Alex' } - * }); - * - * // connect a relation - * await client.user.update({ - * where: { id: 1 }, - * data: { posts: { connect: { id: 1 } } } - * }); - * - * // connect relation, and create if not found - * await client.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 client.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * createMany: { - * data: [{ title: 'Hello World' }, { title: 'Hello World 2' }], - * } - * } - * } - * }); - * - * // disconnect a one-to-many relation - * await client.user.update({ - * where: { id: 1 }, - * data: { posts: { disconnect: { id: 1 } } } - * }); - * - * // disconnect a one-to-one relation - * await client.user.update({ - * where: { id: 1 }, - * data: { profile: { disconnect: true } } - * }); - * - * // replace a relation (only available for one-to-many relations) - * await client.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * set: [{ id: 1 }, { id: 2 }] - * } - * } - * }); - * - * // update a relation - * await client.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * update: { where: { id: 1 }, data: { title: 'Hello World' } } - * } - * } - * }); - * - * // upsert a relation - * await client.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 client.user.update({ - * where: { id: 1 }, - * data: { - * posts: { - * updateMany: { - * where: { published: true }, - * data: { title: 'Hello World' } - * } - * } - * } - * }); - * - * // delete a one-to-many relation - * await client.user.update({ - * where: { id: 1 }, - * data: { posts: { delete: { id: 1 } } } - * }); - * - * // delete a one-to-one relation - * await client.user.update({ - * where: { id: 1 }, - * data: { profile: { delete: true } } - * }); - * ``` - */ - update>( - args: SelectSubset>, - ): Promise>; - - /** - * 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 client.user.updateMany({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' } - * }); - * - * // limit the number of updated entities - * await client.user.updateMany({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' }, - * limit: 10 - * }); - */ - updateMany>( - args: Subset>, - ): Promise; - - /** - * 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 client.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 client.user.updateManyAndReturn({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' }, - * limit: 10 - * }); - * ``` - */ - updateManyAndReturn>( - args: Subset>, - ): Promise[]>; - - /** - * Creates or updates an entity. - * @param args - upsert args - * @returns the upserted entity - * - * @example - * ```ts - * // upsert an entity - * await client.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>, - ): Promise>; - - /** - * 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 client.user.delete({ - * where: { id: 1 } - * }); - * - * // delete an entity and return selected fields - * await client.user.delete({ - * where: { id: 1 }, - * select: { id: true, email: true } - * }); // result: `{ id: string; email: string }` - * ``` - */ - delete>( - args: SelectSubset>, - ): Promise>; - - /** - * Deletes multiple entities. - * @param args - delete args - * @returns count of deleted entities: `{ count: number }` - * - * @example - * ```ts - * // delete many entities - * await client.user.deleteMany({ - * where: { email: { endsWith: '@zenstack.dev' } } - * }); - * - * // limit the number of deleted entities - * await client.user.deleteMany({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * limit: 10 - * }); - * ``` - */ - deleteMany>( - args?: Subset>, - ): Promise; - - /** - * Counts rows or field values. - * @param args - count args - * @returns `number`, or an object containing count of selected relations - * - * @example - * ```ts - * // count all - * await client.user.count(); - * - * // count with a filter - * await client.user.count({ where: { email: { endsWith: '@zenstack.dev' } } }); - * - * // count rows and field values - * await client.user.count({ - * select: { _all: true, email: true } - * }); // result: `{ _all: number, email: number }` - */ - count>( - args?: Subset>, - ): Promise>; - - /** - * Aggregates rows. - * @param args - aggregation args - * @returns an object containing aggregated values - * - * @example - * ```ts - * // aggregate rows - * await client.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>, - ): Promise>; - - /** - * Groups rows by columns. - * @param args - groupBy args - * @returns an object containing grouped values - * - * @example - * ```ts - * // group by a field - * await client.profile.groupBy({ - * by: 'country', - * _count: true - * }); // result: `Array<{ country: string, _count: number }>` - * - * // group by multiple fields - * await client.profile.groupBy({ - * by: ['country', 'city'], - * _count: true - * }); // result: `Array<{ country: string, city: string, _count: number }>` - * - * // group by with sorting, the `orderBy` fields must be in the `by` list - * await client.profile.groupBy({ - * by: 'country', - * orderBy: { country: 'desc' } - * }); - * - * // group by with having (post-aggregation filter), the `having` fields must - * // be in the `by` list - * await client.profile.groupBy({ - * by: 'country', - * having: { country: 'US' } - * }); - */ - groupBy>( - args: Subset>, - ): Promise>; -} - -//#endregion diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index 5205f01c..3b14d637 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -1,11 +1,9 @@ +import { invariant, isPlainObject } from '@zenstackhq/common-helpers'; import type { Expression, ExpressionBuilder, ExpressionWrapper, SqlBool, ValueNode } from 'kysely'; import { sql, type SelectQueryBuilder } from 'kysely'; -import invariant from 'tiny-invariant'; import { match, P } from 'ts-pattern'; import type { BuiltinType, DataSourceProviderType, FieldDef, GetModels, SchemaDef } from '../../../schema'; import { enumerate } from '../../../utils/enumerate'; -// @ts-expect-error -import { isPlainObject } from 'is-plain-object'; import type { OrArray } from '../../../utils/type-utils'; import type { BooleanFilter, diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index f51e6bb1..63573819 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -1,3 +1,4 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { sql, type Expression, @@ -6,7 +7,6 @@ import { type RawBuilder, type SelectQueryBuilder, } from 'kysely'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema'; import type { FindArgs } from '../../crud-types'; diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index 2144317a..c8f20173 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -1,3 +1,4 @@ +import { invariant } from '@zenstackhq/common-helpers'; import type Decimal from 'decimal.js'; import { ExpressionWrapper, @@ -7,7 +8,6 @@ import { type RawBuilder, type SelectQueryBuilder, } from 'kysely'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import type { BuiltinType, GetModels, SchemaDef } from '../../../schema'; import type { FindArgs } from '../../crud-types'; diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index dbecfe8a..7dfbd43c 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -1,4 +1,5 @@ import { createId } from '@paralleldrive/cuid2'; +import { invariant } from '@zenstackhq/common-helpers'; import { DeleteResult, expressionBuilder, @@ -10,7 +11,6 @@ import { type SelectQueryBuilder, } from 'kysely'; import { nanoid } from 'nanoid'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import { ulid } from 'ulid'; import * as uuid from 'uuid'; diff --git a/packages/runtime/src/client/functions.ts b/packages/runtime/src/client/functions.ts index e05041df..1b246034 100644 --- a/packages/runtime/src/client/functions.ts +++ b/packages/runtime/src/client/functions.ts @@ -1,5 +1,5 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { sql, ValueNode, type Expression, type ExpressionBuilder } from 'kysely'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import type { ZModelFunction, ZModelFunctionContext } from './options'; diff --git a/packages/runtime/src/client/helpers/schema-db-pusher.ts b/packages/runtime/src/client/helpers/schema-db-pusher.ts index 4dd3b2d7..7042d7ec 100644 --- a/packages/runtime/src/client/helpers/schema-db-pusher.ts +++ b/packages/runtime/src/client/helpers/schema-db-pusher.ts @@ -1,5 +1,5 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { CreateTableBuilder, sql, type ColumnDataType, type OnModifyForeignAction } from 'kysely'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import { ExpressionUtils, diff --git a/packages/runtime/src/client/index.ts b/packages/runtime/src/client/index.ts index e0f29a7d..bafc558e 100644 --- a/packages/runtime/src/client/index.ts +++ b/packages/runtime/src/client/index.ts @@ -3,5 +3,5 @@ export type { ClientConstructor, ClientContract } from './contract'; export type * from './crud-types'; export * from './errors'; export type { ClientOptions } from './options'; -export type { CliGenerator } from './plugin'; +export { definePlugin } from './plugin'; export type { ToKysely } from './query-builder'; diff --git a/packages/runtime/src/client/plugin.ts b/packages/runtime/src/client/plugin.ts index 962aa260..6141c4e9 100644 --- a/packages/runtime/src/client/plugin.ts +++ b/packages/runtime/src/client/plugin.ts @@ -1,8 +1,8 @@ -import type { Model } from '@zenstackhq/language/ast'; import type { OperationNode, QueryResult, RootOperationNode, UnknownRow } from 'kysely'; 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 = { @@ -124,7 +124,7 @@ export interface RuntimePlugin { /** * Intercepts an ORM query. */ - onQuery?: (args: OnQueryArgs) => Promise; + onQuery?: OnQueryHooks; /** * Intercepts a Kysely query. @@ -154,14 +154,68 @@ export interface RuntimePlugin { afterEntityMutation?: (args: PluginAfterEntityMutationArgs) => MaybePromise; } -// TODO: move to SDK -export type CliGeneratorContext = { +type OnQueryHooks = { + [Model in GetModels as Uncapitalize]?: OnQueryOperationHooks; +} & { + $allModels?: OnQueryOperationHooks>; +}; + +type OnQueryOperationHooks> = { + [Operation in keyof ModelOperations]?: ( + ctx: OnQueryHookContext, + ) => ReturnType[Operation]>; +} & { + $allOperations?: (ctx: { + model: Model; + operation: CrudOperation; + args: unknown; + query: (args: unknown) => Promise; + }) => MaybePromise; +}; + +type OnQueryHookContext< + Schema extends SchemaDef, + Model extends GetModels, + Operation extends keyof ModelOperations, +> = { + /** + * The model that is being queried. + */ model: Model; - outputPath: string; - tsSchemaFile: string; + + /** + * The operation that is being performed. + */ + operation: Operation; + + /** + * The query arguments. + */ + args: Parameters[Operation]>[0]; + + /** + * The query function to proceed with the original query. + * It takes the same arguments as the operation method. + * + * @param args The query arguments. + * @param tx Optional transaction client to use for the query. + */ + query: ( + args: Parameters[Operation]>[0], + tx?: ClientContract, + ) => ReturnType[Operation]>; + + /** + * The ZenStack client that is performing the operation. + */ + client: ClientContract; }; -// TODO: move to SDK -export type CliGenerator = (context: CliGeneratorContext) => MaybePromise; +/** + * Defines a ZenStack runtime plugin. + */ +export function definePlugin(plugin: RuntimePlugin) { + return plugin; +} export { type CrudOperation } from './crud/operations/base'; diff --git a/packages/runtime/src/client/result-processor.ts b/packages/runtime/src/client/result-processor.ts index 6899346d..6a922c18 100644 --- a/packages/runtime/src/client/result-processor.ts +++ b/packages/runtime/src/client/result-processor.ts @@ -1,5 +1,5 @@ +import { invariant } from '@zenstackhq/common-helpers'; import Decimal from 'decimal.js'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../schema'; import { ensureArray, getField } from './query-utils'; diff --git a/packages/runtime/src/plugins/policy/expression-evaluator.ts b/packages/runtime/src/plugins/policy/expression-evaluator.ts index 77b1ec1e..a35530e5 100644 --- a/packages/runtime/src/plugins/policy/expression-evaluator.ts +++ b/packages/runtime/src/plugins/policy/expression-evaluator.ts @@ -1,4 +1,4 @@ -import invariant from 'tiny-invariant'; +import { invariant } from '@zenstackhq/common-helpers'; import { match } from 'ts-pattern'; import { ExpressionUtils, diff --git a/packages/runtime/src/plugins/policy/expression-transformer.ts b/packages/runtime/src/plugins/policy/expression-transformer.ts index 05f40bf1..cf378794 100644 --- a/packages/runtime/src/plugins/policy/expression-transformer.ts +++ b/packages/runtime/src/plugins/policy/expression-transformer.ts @@ -1,3 +1,4 @@ +import { invariant } from '@zenstackhq/common-helpers'; import type { OperandExpression } from 'kysely'; import { AliasNode, @@ -18,7 +19,6 @@ import { type ExpressionBuilder, type OperationNode, } from 'kysely'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import type { CRUD } from '../../client/contract'; import { getCrudDialect } from '../../client/crud/dialects'; @@ -26,6 +26,16 @@ import type { BaseCrudDialect } from '../../client/crud/dialects/base'; import { InternalError, QueryError } from '../../client/errors'; import type { ClientOptions } from '../../client/options'; import { getRelationForeignKeyFieldPairs, requireField } from '../../client/query-utils'; +import type { + BinaryExpression, + BinaryOperator, + BuiltinType, + FieldDef, + GetModels, + LiteralExpression, + MemberExpression, + UnaryExpression, +} from '../../schema'; import { ExpressionUtils, type ArrayExpression, @@ -34,16 +44,6 @@ import { type FieldExpression, type SchemaDef, } from '../../schema'; -import type { - BinaryExpression, - BinaryOperator, - LiteralExpression, - MemberExpression, - UnaryExpression, - BuiltinType, - FieldDef, - GetModels, -} from '../../schema'; import { ExpressionEvaluator } from './expression-evaluator'; import { conjunction, disjunction, logicalNot, trueNode } from './utils'; diff --git a/packages/runtime/src/plugins/policy/policy-handler.ts b/packages/runtime/src/plugins/policy/policy-handler.ts index f3cae07e..31b7174c 100644 --- a/packages/runtime/src/plugins/policy/policy-handler.ts +++ b/packages/runtime/src/plugins/policy/policy-handler.ts @@ -1,3 +1,4 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { AliasNode, BinaryOperationNode, @@ -22,7 +23,6 @@ import { type QueryResult, type RootOperationNode, } from 'kysely'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import type { ClientContract } from '../../client'; import type { CRUD } from '../../client/contract'; diff --git a/packages/runtime/src/utils/clone.ts b/packages/runtime/src/utils/clone.ts index 82355f6a..3160d319 100644 --- a/packages/runtime/src/utils/clone.ts +++ b/packages/runtime/src/utils/clone.ts @@ -1,5 +1,4 @@ -// @ts-expect-error -import { isPlainObject } from 'is-plain-object'; +import { isPlainObject } from '@zenstackhq/common-helpers'; /** * Clones the given object. Only arrays and plain objects are cloned. Other values are returned as is. @@ -14,7 +13,6 @@ export function clone(value: T): T { return value; } - const result: any = {}; for (const key of Object.keys(value)) { result[key] = clone(value[key as keyof T]); diff --git a/packages/runtime/test/plugin/query-lifecycle.test.ts b/packages/runtime/test/plugin/query-lifecycle.test.ts index 3e476187..af8d40c9 100644 --- a/packages/runtime/test/plugin/query-lifecycle.test.ts +++ b/packages/runtime/test/plugin/query-lifecycle.test.ts @@ -1,6 +1,6 @@ import SQLite from 'better-sqlite3'; import { beforeEach, describe, expect, it } from 'vitest'; -import { ZenStackClient, type ClientContract } from '../../src/client'; +import { definePlugin, ZenStackClient, type ClientContract } from '../../src/client'; import { schema } from '../test-schema'; describe('Query interception tests', () => { @@ -20,20 +20,84 @@ describe('Query interception tests', () => { 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(args) { - hooksCalled = true; - expect(args).toMatchObject({ - model: 'User', - operation: 'findFirst', - queryArgs: { where: { id: user.id } }, - }); - return args.proceed(args.queryArgs); + 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 }, @@ -50,9 +114,13 @@ describe('Query interception tests', () => { let hooksCalled = false; const client = _client.$use({ id: 'test-plugin', - onQuery(args) { - hooksCalled = true; - return args.proceed({ where: { id: 'non-exist' } }); + onQuery: { + user: { + findFirst: async (ctx) => { + hooksCalled = true; + return ctx.query({ where: { id: 'non-exist' } }); + }, + }, }, }); @@ -72,11 +140,15 @@ describe('Query interception tests', () => { let hooksCalled = false; const client = _client.$use({ id: 'test-plugin', - async onQuery({ proceed, queryArgs }) { - hooksCalled = true; - const result = await proceed(queryArgs); - (result as any).happy = true; - return result; + onQuery: { + user: { + findFirst: async (ctx) => { + hooksCalled = true; + const result = await ctx.query(ctx.args); + (result as any).happy = true; + return result; + }, + }, }, }); @@ -107,25 +179,33 @@ describe('Query interception tests', () => { const client = _client .$use({ id: 'test-plugin', - async onQuery(args) { - hooks1Called = true; - console.log('Plugin1 ready to proceed'); - const r = await args.proceed({ where: { id: user2.id } }); - (r as any).happy = true; - (r as any).source = 'plugin1'; - console.log('Plugin1 ready to return', r); - return r; + 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', - async onQuery(args) { - hooks2Called = true; - console.log('Plugin2 ready to proceed'); - const r = await args.proceed({ where: { id: user3.id } }); - (r as any).source = 'plugin2'; - console.log('Plugin2 ready to return', r); - return r; + 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; + }, + }, }, }); @@ -147,10 +227,14 @@ describe('Query interception tests', () => { let hooksCalled = false; const client = _client.$use({ id: 'test-plugin', - async onQuery(args) { - hooksCalled = true; - await args.proceed(args.queryArgs); - throw new Error('trigger error'); + onQuery: { + user: { + create: async (ctx) => { + hooksCalled = true; + await ctx.query(ctx.args); + throw new Error('trigger error'); + }, + }, }, }); @@ -174,12 +258,16 @@ describe('Query interception tests', () => { let hooksCalled = false; const client = _client.$use({ id: 'test-plugin', - async onQuery(args) { - hooksCalled = true; - return args.client.$transaction(async (tx) => { - await args.proceed(args.queryArgs, tx); - throw new Error('trigger error'); - }); + onQuery: { + user: { + create: async (ctx) => { + hooksCalled = true; + return ctx.client.$transaction(async (tx) => { + await ctx.query(ctx.args, tx); + throw new Error('trigger error'); + }); + }, + }, }, }); @@ -198,4 +286,33 @@ describe('Query interception tests', () => { }), ).toResolveNull(); }); + + 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/utils.ts b/packages/runtime/test/utils.ts index 1feb435b..ffce327e 100644 --- a/packages/runtime/test/utils.ts +++ b/packages/runtime/test/utils.ts @@ -1,3 +1,4 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { loadDocument } from '@zenstackhq/language'; import { PrismaSchemaGenerator } from '@zenstackhq/sdk'; import { generateTsSchema } from '@zenstackhq/testtools'; @@ -6,7 +7,6 @@ import { execSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { Client as PGClient, Pool } from 'pg'; -import invariant from 'tiny-invariant'; import type { ClientOptions } from '../src/client'; import { ZenStackClient } from '../src/client'; import type { SchemaDef } from '../src/schema'; diff --git a/packages/runtime/test/vitest.d.ts b/packages/runtime/test/vitest.d.ts index 2606a85a..b547127c 100644 --- a/packages/runtime/test/vitest.d.ts +++ b/packages/runtime/test/vitest.d.ts @@ -1,5 +1,3 @@ - - import 'vitest'; interface CustomMatchers { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index ae249e29..3cf45b01 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,10 +1,11 @@ { "name": "@zenstackhq/sdk", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "ZenStack SDK", "type": "module", "scripts": { "build": "tsup-node", + "watch": "tsup-node --watch", "lint": "eslint src --ext ts", "pack": "pnpm pack" }, @@ -38,8 +39,8 @@ }, "dependencies": { "@zenstackhq/language": "workspace:*", + "@zenstackhq/common-helpers": "workspace:*", "langium": "catalog:", - "tiny-invariant": "^1.3.3", "tmp": "^0.2.3", "ts-pattern": "catalog:", "typescript": "catalog:" diff --git a/packages/sdk/src/generator.ts b/packages/sdk/src/generator.ts new file mode 100644 index 00000000..3868b692 --- /dev/null +++ b/packages/sdk/src/generator.ts @@ -0,0 +1,10 @@ +import type { Model } from '@zenstackhq/language/ast'; +import type { MaybePromise } from 'langium'; + +export type CliGeneratorContext = { + model: Model; + outputPath: string; + tsSchemaFile: string; +}; + +export type CliGenerator = (context: CliGeneratorContext) => MaybePromise; diff --git a/packages/sdk/src/index.ts b/packages/sdk/src/index.ts index fccc7ce8..313d15ae 100644 --- a/packages/sdk/src/index.ts +++ b/packages/sdk/src/index.ts @@ -1,4 +1,5 @@ import * as ModelUtils from './model-utils'; +export * from './generator'; export { PrismaSchemaGenerator } from './prisma/prisma-schema-generator'; export * from './ts-schema-generator'; export * from './zmodel-code-generator'; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 1b1f6c56..8183c5b6 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1,3 +1,4 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { loadDocument } from '@zenstackhq/language'; import { ArrayExpr, @@ -34,7 +35,6 @@ import { } from '@zenstackhq/language/ast'; import fs from 'node:fs'; import path from 'node:path'; -import invariant from 'tiny-invariant'; import { match } from 'ts-pattern'; import * as ts from 'typescript'; import { ModelUtils } from '.'; diff --git a/packages/tanstack-query/package.json b/packages/tanstack-query/package.json index c633ed60..88a13e73 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.4", + "version": "3.0.0-alpha.5", "description": "", "main": "index.js", "type": "module", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index f810eaea..1260a67b 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/typescript-config/package.json b/packages/typescript-config/package.json index b7322080..8e04c3bb 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.4", + "version": "3.0.0-alpha.5", "private": true, "license": "MIT" } diff --git a/packages/zod/package.json b/packages/zod/package.json index 8d6c3879..e025e0ec 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "", "type": "module", "main": "index.js", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ccaec7a..df6a06cc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,9 @@ importers: packages/cli: dependencies: + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../common-helpers '@zenstackhq/language': specifier: workspace:* version: link:../language @@ -95,9 +98,6 @@ importers: prisma: specifier: 'catalog:' version: 6.9.0(typescript@5.8.3) - tiny-invariant: - specifier: ^1.3.3 - version: 1.3.3 ts-pattern: specifier: 'catalog:' version: 5.7.1 @@ -133,6 +133,15 @@ importers: specifier: ^0.2.3 version: 0.2.3 + packages/common-helpers: + devDependencies: + '@zenstackhq/eslint-config': + specifier: workspace:* + version: link:../eslint-config + '@zenstackhq/typescript-config': + specifier: workspace:* + version: link:../typescript-config + packages/create-zenstack: dependencies: colors: @@ -209,15 +218,15 @@ importers: '@paralleldrive/cuid2': specifier: ^2.2.2 version: 2.2.2 + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../common-helpers better-sqlite3: specifier: ^11.8.1 version: 11.8.1 decimal.js: specifier: ^10.4.3 version: 10.4.3 - is-plain-object: - specifier: ^5.0.0 - version: 5.0.0 json-stable-stringify: specifier: ^1.3.0 version: 1.3.0 @@ -230,9 +239,6 @@ importers: pg: specifier: ^8.13.1 version: 8.13.1 - tiny-invariant: - specifier: ^1.3.3 - version: 1.3.3 ts-pattern: specifier: 'catalog:' version: 5.7.1 @@ -282,15 +288,15 @@ importers: packages/sdk: dependencies: + '@zenstackhq/common-helpers': + specifier: workspace:* + version: link:../common-helpers '@zenstackhq/language': specifier: workspace:* version: link:../language langium: specifier: 'catalog:' version: 3.3.0 - tiny-invariant: - specifier: ^1.3.3 - version: 1.3.3 tmp: specifier: ^0.2.3 version: 0.2.3 @@ -1847,10 +1853,6 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} - is-plain-object@5.0.0: - resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} - engines: {node: '>=0.10.0'} - is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -2647,9 +2649,6 @@ packages: thenify@3.3.1: resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tiny-invariant@1.3.3: - resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -4405,8 +4404,6 @@ snapshots: is-number@7.0.0: {} - is-plain-object@5.0.0: {} - is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -5243,8 +5240,6 @@ snapshots: dependencies: any-promise: 1.3.0 - tiny-invariant@1.3.3: {} - tinybench@2.9.0: {} tinyexec@0.3.2: {} diff --git a/samples/blog/main.ts b/samples/blog/main.ts index ad3a0c34..93380b94 100644 --- a/samples/blog/main.ts +++ b/samples/blog/main.ts @@ -18,11 +18,15 @@ async function main() { }, }).$use({ id: 'cost-logger', - async onQuery({ model, operation, proceed, queryArgs }) { - const start = Date.now(); - const result = await proceed(queryArgs); - console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); - return result; + 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; + }, + }, }, }); diff --git a/samples/blog/package.json b/samples/blog/package.json index a37e25a0..41fe38f6 100644 --- a/samples/blog/package.json +++ b/samples/blog/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.0.0-alpha.4", + "version": "3.0.0-alpha.5", "description": "", "main": "index.js", "scripts": {