diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6097f8706..9b9e40bf3 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -18,7 +18,7 @@ }, { "label": "Build all - watch", - "command": "turbo watch build", + "command": "pnpm watch", "type": "shell", "group": { "kind": "build" @@ -50,6 +50,16 @@ "color": "terminal.ansiMagenta", "id": "server-process" } + }, + { + "label": "Submit PR", + "command": "pnpm pr", + "type": "shell", + "icon": { + "color": "terminal.ansiWhite", + "id": "server-process" + }, + "problemMatcher": [] } ] } diff --git a/packages/runtime/src/client/client-impl.ts b/packages/runtime/src/client/client-impl.ts index d646dd300..d2c5bfba8 100644 --- a/packages/runtime/src/client/client-impl.ts +++ b/packages/runtime/src/client/client-impl.ts @@ -397,7 +397,13 @@ function createModelCrudHandler - opHooks({ client, model, operation, args, query: _proceed }); + opHooks({ + client, + model, + operation, + args, + query: _proceed, + }) as Promise; } } } diff --git a/packages/runtime/src/client/contract.ts b/packages/runtime/src/client/contract.ts index 53c8a36f0..f91828456 100644 --- a/packages/runtime/src/client/contract.ts +++ b/packages/runtime/src/client/contract.ts @@ -1,5 +1,5 @@ import type { Decimal } from 'decimal.js'; -import { type GetModels, type ProcedureDef, type SchemaDef } from '../schema'; +import { type GetModels, type IsDelegateModel, type ProcedureDef, type SchemaDef } from '../schema'; import type { AuthType } from '../schema/auth'; import type { OrUndefinedIf, Simplify, UnwrapTuplePromises } from '../utils/type-utils'; import type { TRANSACTION_UNSUPPORTED_METHODS } from './constants'; @@ -215,558 +215,562 @@ 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>, - ): 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 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>, - ): 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 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>, - ): 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 client.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 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>, - ): 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 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>, - ): 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 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>, - ): ZenStackPromise>[]>; - - /** - * 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>, - ): 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 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>, - ): ZenStackPromise>>; - - /** - * 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>, - ): 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 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>, - ): ZenStackPromise>>; - - /** - * 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>, - ): ZenStackPromise>>; - - /** - * 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>, - ): 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 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>, + ): 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 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>, + ): 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 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>, + ): 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 client.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 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>, + ): 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 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>, + ): 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 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>, + ): ZenStackPromise>[]>; + + /** + * 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>, + ): 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 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>, + ): ZenStackPromise>>; + + /** + * 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>, + ): 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 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>, + ): ZenStackPromise>>; + + /** + * 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>, + ): ZenStackPromise>>; + + /** + * 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>, + ): ZenStackPromise>>; + }, + // exclude operations not applicable to delegate models + IsDelegateModel extends true ? 'create' | 'createMany' | 'createManyAndReturn' | 'upsert' : never +>; //#endregion diff --git a/packages/runtime/src/client/crud-types.ts b/packages/runtime/src/client/crud-types.ts index 821dac065..abe011f4f 100644 --- a/packages/runtime/src/client/crud-types.ts +++ b/packages/runtime/src/client/crud-types.ts @@ -4,6 +4,8 @@ import type { FieldDef, FieldHasDefault, FieldIsArray, + FieldIsDelegateDiscriminator, + FieldIsDelegateRelation, FieldIsRelation, FieldIsRelationArray, FieldType, @@ -11,13 +13,16 @@ import type { GetEnum, GetEnums, GetModel, + GetModelDiscriminator, GetModelField, GetModelFields, GetModelFieldType, GetModels, + GetSubModels, GetTypeDefField, GetTypeDefFields, GetTypeDefs, + IsDelegateModel, ModelFieldIsOptional, NonRelationFields, RelationFields, @@ -50,17 +55,29 @@ type DefaultModelResult< Optional = false, Array = false, > = WrapType< - { - [Key in NonRelationFields as Key extends keyof Omit - ? Omit[Key] extends true - ? never - : Key - : Key]: MapModelFieldType; - }, + IsDelegateModel extends true + ? // delegate model's selection result is a union of all sub-models + DelegateUnionResult, Omit> + : { + [Key in NonRelationFields as Key extends keyof Omit + ? Omit[Key] extends true + ? never + : Key + : Key]: MapModelFieldType; + }, Optional, Array >; +type DelegateUnionResult< + Schema extends SchemaDef, + Model extends GetModels, + SubModel extends GetModels, + Omit = undefined, +> = SubModel extends string // typescript union distribution + ? DefaultModelResult & { [K in GetModelDiscriminator]: SubModel } // fixate discriminated field + : never; + type ModelSelectResult, Select, Omit> = { [Key in keyof Select as Select[Key] extends false | undefined ? never @@ -178,7 +195,7 @@ export type TypeDefResult as TypeDefFieldIsOptional extends true ? Key - : never]: Key; + : never]: true; } >; @@ -596,7 +613,10 @@ type CreateScalarPayload]: ScalarCreatePayload; + [Key in ScalarFields as FieldIsDelegateDiscriminator extends true + ? // discriminator fields cannot be assigned + never + : Key]: ScalarCreatePayload; } >; @@ -626,13 +646,15 @@ type CreateRelationFieldPayload< Field extends RelationFields, > = Omit< { + connectOrCreate?: ConnectOrCreateInput; create?: NestedCreateInput; createMany?: NestedCreateManyInput; connect?: ConnectInput; - connectOrCreate?: ConnectOrCreateInput; }, // no "createMany" for non-array fields - FieldIsArray extends true ? never : 'createMany' + | (FieldIsArray extends true ? never : 'createMany') + // exclude operations not applicable to delegate models + | (FieldIsDelegateRelation extends true ? 'create' | 'createMany' | 'connectOrCreate' : never) >; type CreateRelationPayload> = OptionalWrap< @@ -745,7 +767,10 @@ type UpdateScalarInput< Without extends string = never, > = Omit< { - [Key in NonRelationFields]?: ScalarUpdatePayload; + [Key in NonRelationFields as FieldIsDelegateDiscriminator extends true + ? // discriminator fields cannot be assigned + never + : Key]?: ScalarUpdatePayload; }, Without >; @@ -802,36 +827,45 @@ type ToManyRelationUpdateInput< Schema extends SchemaDef, Model extends GetModels, Field extends RelationFields, -> = { - create?: NestedCreateInput; - createMany?: NestedCreateManyInput; - connect?: ConnectInput; - connectOrCreate?: ConnectOrCreateInput; - disconnect?: DisconnectInput; - update?: NestedUpdateInput; - upsert?: NestedUpsertInput; - updateMany?: NestedUpdateManyInput; - delete?: NestedDeleteInput; - deleteMany?: NestedDeleteManyInput; - set?: SetRelationInput; -}; +> = Omit< + { + create?: NestedCreateInput; + createMany?: NestedCreateManyInput; + connect?: ConnectInput; + connectOrCreate?: ConnectOrCreateInput; + disconnect?: DisconnectInput; + update?: NestedUpdateInput; + upsert?: NestedUpsertInput; + updateMany?: NestedUpdateManyInput; + delete?: NestedDeleteInput; + deleteMany?: NestedDeleteManyInput; + set?: SetRelationInput; + }, + // exclude + FieldIsDelegateRelation extends true + ? 'create' | 'createMany' | 'connectOrCreate' | 'upsert' + : never +>; type ToOneRelationUpdateInput< Schema extends SchemaDef, Model extends GetModels, Field extends RelationFields, -> = { - create?: NestedCreateInput; - connect?: ConnectInput; - connectOrCreate?: ConnectOrCreateInput; - update?: NestedUpdateInput; - upsert?: NestedUpsertInput; -} & (ModelFieldIsOptional extends true - ? { - disconnect?: DisconnectInput; - delete?: NestedDeleteInput; - } - : {}); +> = Omit< + { + create?: NestedCreateInput; + connect?: ConnectInput; + connectOrCreate?: ConnectOrCreateInput; + update?: NestedUpdateInput; + upsert?: NestedUpsertInput; + } & (ModelFieldIsOptional extends true + ? { + disconnect?: DisconnectInput; + delete?: NestedDeleteInput; + } + : {}), + FieldIsDelegateRelation extends true ? 'create' | 'connectOrCreate' | 'upsert' : never +>; // #endregion @@ -1144,7 +1178,7 @@ type NonOwnedRelationFields Promise; + client: ClientContract; }) => MaybePromise; }; @@ -195,7 +196,6 @@ type OnQueryHookContext< */ query: ( args: Parameters[Operation]>[0], - // tx?: ClientContract, ) => ReturnType[Operation]>; /** diff --git a/packages/runtime/src/client/query-builder.ts b/packages/runtime/src/client/query-builder.ts index 199970178..91ec4dfaa 100644 --- a/packages/runtime/src/client/query-builder.ts +++ b/packages/runtime/src/client/query-builder.ts @@ -3,6 +3,7 @@ import type { Generated, Kysely } from 'kysely'; import type { FieldHasDefault, ForeignKeyFields, + GetModelField, GetModelFields, GetModelFieldType, GetModels, @@ -18,11 +19,14 @@ export type ToKyselySchema = { export type ToKysely = Kysely>; type ToKyselyTable> = { - [Field in ScalarFields | ForeignKeyFields]: toKyselyFieldType< + [Field in ScalarFields | ForeignKeyFields as GetModelField< Schema, Model, Field - >; + >['originModel'] extends string + ? // query builder should not see fields inherited from delegate base model + never + : Field]: toKyselyFieldType; }; export type MapBaseType = T extends 'String' diff --git a/packages/runtime/test/schemas/delegate/input.ts b/packages/runtime/test/schemas/delegate/input.ts new file mode 100644 index 000000000..b8df49e49 --- /dev/null +++ b/packages/runtime/test/schemas/delegate/input.ts @@ -0,0 +1,150 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import type { FindManyArgs as $FindManyArgs, FindUniqueArgs as $FindUniqueArgs, FindFirstArgs as $FindFirstArgs, CreateArgs as $CreateArgs, CreateManyArgs as $CreateManyArgs, CreateManyAndReturnArgs as $CreateManyAndReturnArgs, UpdateArgs as $UpdateArgs, UpdateManyArgs as $UpdateManyArgs, UpdateManyAndReturnArgs as $UpdateManyAndReturnArgs, UpsertArgs as $UpsertArgs, DeleteArgs as $DeleteArgs, DeleteManyArgs as $DeleteManyArgs, CountArgs as $CountArgs, AggregateArgs as $AggregateArgs, GroupByArgs as $GroupByArgs, WhereInput as $WhereInput, SelectInput as $SelectInput, IncludeInput as $IncludeInput, OmitInput as $OmitInput } from "@zenstackhq/runtime"; +import type { SimplifiedModelResult as $SimplifiedModelResult, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/runtime"; +export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; +export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; +export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserCreateArgs = $CreateArgs<$Schema, "User">; +export type UserCreateManyArgs = $CreateManyArgs<$Schema, "User">; +export type UserCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "User">; +export type UserUpdateArgs = $UpdateArgs<$Schema, "User">; +export type UserUpdateManyArgs = $UpdateManyArgs<$Schema, "User">; +export type UserUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "User">; +export type UserUpsertArgs = $UpsertArgs<$Schema, "User">; +export type UserDeleteArgs = $DeleteArgs<$Schema, "User">; +export type UserDeleteManyArgs = $DeleteManyArgs<$Schema, "User">; +export type UserCountArgs = $CountArgs<$Schema, "User">; +export type UserAggregateArgs = $AggregateArgs<$Schema, "User">; +export type UserGroupByArgs = $GroupByArgs<$Schema, "User">; +export type UserWhereInput = $WhereInput<$Schema, "User">; +export type UserSelect = $SelectInput<$Schema, "User">; +export type UserInclude = $IncludeInput<$Schema, "User">; +export type UserOmit = $OmitInput<$Schema, "User">; +export type UserGetPayload> = $SimplifiedModelResult<$Schema, "User", Args>; +export type CommentFindManyArgs = $FindManyArgs<$Schema, "Comment">; +export type CommentFindUniqueArgs = $FindUniqueArgs<$Schema, "Comment">; +export type CommentFindFirstArgs = $FindFirstArgs<$Schema, "Comment">; +export type CommentCreateArgs = $CreateArgs<$Schema, "Comment">; +export type CommentCreateManyArgs = $CreateManyArgs<$Schema, "Comment">; +export type CommentCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Comment">; +export type CommentUpdateArgs = $UpdateArgs<$Schema, "Comment">; +export type CommentUpdateManyArgs = $UpdateManyArgs<$Schema, "Comment">; +export type CommentUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Comment">; +export type CommentUpsertArgs = $UpsertArgs<$Schema, "Comment">; +export type CommentDeleteArgs = $DeleteArgs<$Schema, "Comment">; +export type CommentDeleteManyArgs = $DeleteManyArgs<$Schema, "Comment">; +export type CommentCountArgs = $CountArgs<$Schema, "Comment">; +export type CommentAggregateArgs = $AggregateArgs<$Schema, "Comment">; +export type CommentGroupByArgs = $GroupByArgs<$Schema, "Comment">; +export type CommentWhereInput = $WhereInput<$Schema, "Comment">; +export type CommentSelect = $SelectInput<$Schema, "Comment">; +export type CommentInclude = $IncludeInput<$Schema, "Comment">; +export type CommentOmit = $OmitInput<$Schema, "Comment">; +export type CommentGetPayload> = $SimplifiedModelResult<$Schema, "Comment", Args>; +export type AssetFindManyArgs = $FindManyArgs<$Schema, "Asset">; +export type AssetFindUniqueArgs = $FindUniqueArgs<$Schema, "Asset">; +export type AssetFindFirstArgs = $FindFirstArgs<$Schema, "Asset">; +export type AssetCreateArgs = $CreateArgs<$Schema, "Asset">; +export type AssetCreateManyArgs = $CreateManyArgs<$Schema, "Asset">; +export type AssetCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Asset">; +export type AssetUpdateArgs = $UpdateArgs<$Schema, "Asset">; +export type AssetUpdateManyArgs = $UpdateManyArgs<$Schema, "Asset">; +export type AssetUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Asset">; +export type AssetUpsertArgs = $UpsertArgs<$Schema, "Asset">; +export type AssetDeleteArgs = $DeleteArgs<$Schema, "Asset">; +export type AssetDeleteManyArgs = $DeleteManyArgs<$Schema, "Asset">; +export type AssetCountArgs = $CountArgs<$Schema, "Asset">; +export type AssetAggregateArgs = $AggregateArgs<$Schema, "Asset">; +export type AssetGroupByArgs = $GroupByArgs<$Schema, "Asset">; +export type AssetWhereInput = $WhereInput<$Schema, "Asset">; +export type AssetSelect = $SelectInput<$Schema, "Asset">; +export type AssetInclude = $IncludeInput<$Schema, "Asset">; +export type AssetOmit = $OmitInput<$Schema, "Asset">; +export type AssetGetPayload> = $SimplifiedModelResult<$Schema, "Asset", Args>; +export type VideoFindManyArgs = $FindManyArgs<$Schema, "Video">; +export type VideoFindUniqueArgs = $FindUniqueArgs<$Schema, "Video">; +export type VideoFindFirstArgs = $FindFirstArgs<$Schema, "Video">; +export type VideoCreateArgs = $CreateArgs<$Schema, "Video">; +export type VideoCreateManyArgs = $CreateManyArgs<$Schema, "Video">; +export type VideoCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Video">; +export type VideoUpdateArgs = $UpdateArgs<$Schema, "Video">; +export type VideoUpdateManyArgs = $UpdateManyArgs<$Schema, "Video">; +export type VideoUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Video">; +export type VideoUpsertArgs = $UpsertArgs<$Schema, "Video">; +export type VideoDeleteArgs = $DeleteArgs<$Schema, "Video">; +export type VideoDeleteManyArgs = $DeleteManyArgs<$Schema, "Video">; +export type VideoCountArgs = $CountArgs<$Schema, "Video">; +export type VideoAggregateArgs = $AggregateArgs<$Schema, "Video">; +export type VideoGroupByArgs = $GroupByArgs<$Schema, "Video">; +export type VideoWhereInput = $WhereInput<$Schema, "Video">; +export type VideoSelect = $SelectInput<$Schema, "Video">; +export type VideoInclude = $IncludeInput<$Schema, "Video">; +export type VideoOmit = $OmitInput<$Schema, "Video">; +export type VideoGetPayload> = $SimplifiedModelResult<$Schema, "Video", Args>; +export type RatedVideoFindManyArgs = $FindManyArgs<$Schema, "RatedVideo">; +export type RatedVideoFindUniqueArgs = $FindUniqueArgs<$Schema, "RatedVideo">; +export type RatedVideoFindFirstArgs = $FindFirstArgs<$Schema, "RatedVideo">; +export type RatedVideoCreateArgs = $CreateArgs<$Schema, "RatedVideo">; +export type RatedVideoCreateManyArgs = $CreateManyArgs<$Schema, "RatedVideo">; +export type RatedVideoCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "RatedVideo">; +export type RatedVideoUpdateArgs = $UpdateArgs<$Schema, "RatedVideo">; +export type RatedVideoUpdateManyArgs = $UpdateManyArgs<$Schema, "RatedVideo">; +export type RatedVideoUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "RatedVideo">; +export type RatedVideoUpsertArgs = $UpsertArgs<$Schema, "RatedVideo">; +export type RatedVideoDeleteArgs = $DeleteArgs<$Schema, "RatedVideo">; +export type RatedVideoDeleteManyArgs = $DeleteManyArgs<$Schema, "RatedVideo">; +export type RatedVideoCountArgs = $CountArgs<$Schema, "RatedVideo">; +export type RatedVideoAggregateArgs = $AggregateArgs<$Schema, "RatedVideo">; +export type RatedVideoGroupByArgs = $GroupByArgs<$Schema, "RatedVideo">; +export type RatedVideoWhereInput = $WhereInput<$Schema, "RatedVideo">; +export type RatedVideoSelect = $SelectInput<$Schema, "RatedVideo">; +export type RatedVideoInclude = $IncludeInput<$Schema, "RatedVideo">; +export type RatedVideoOmit = $OmitInput<$Schema, "RatedVideo">; +export type RatedVideoGetPayload> = $SimplifiedModelResult<$Schema, "RatedVideo", Args>; +export type ImageFindManyArgs = $FindManyArgs<$Schema, "Image">; +export type ImageFindUniqueArgs = $FindUniqueArgs<$Schema, "Image">; +export type ImageFindFirstArgs = $FindFirstArgs<$Schema, "Image">; +export type ImageCreateArgs = $CreateArgs<$Schema, "Image">; +export type ImageCreateManyArgs = $CreateManyArgs<$Schema, "Image">; +export type ImageCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Image">; +export type ImageUpdateArgs = $UpdateArgs<$Schema, "Image">; +export type ImageUpdateManyArgs = $UpdateManyArgs<$Schema, "Image">; +export type ImageUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Image">; +export type ImageUpsertArgs = $UpsertArgs<$Schema, "Image">; +export type ImageDeleteArgs = $DeleteArgs<$Schema, "Image">; +export type ImageDeleteManyArgs = $DeleteManyArgs<$Schema, "Image">; +export type ImageCountArgs = $CountArgs<$Schema, "Image">; +export type ImageAggregateArgs = $AggregateArgs<$Schema, "Image">; +export type ImageGroupByArgs = $GroupByArgs<$Schema, "Image">; +export type ImageWhereInput = $WhereInput<$Schema, "Image">; +export type ImageSelect = $SelectInput<$Schema, "Image">; +export type ImageInclude = $IncludeInput<$Schema, "Image">; +export type ImageOmit = $OmitInput<$Schema, "Image">; +export type ImageGetPayload> = $SimplifiedModelResult<$Schema, "Image", Args>; +export type GalleryFindManyArgs = $FindManyArgs<$Schema, "Gallery">; +export type GalleryFindUniqueArgs = $FindUniqueArgs<$Schema, "Gallery">; +export type GalleryFindFirstArgs = $FindFirstArgs<$Schema, "Gallery">; +export type GalleryCreateArgs = $CreateArgs<$Schema, "Gallery">; +export type GalleryCreateManyArgs = $CreateManyArgs<$Schema, "Gallery">; +export type GalleryCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Gallery">; +export type GalleryUpdateArgs = $UpdateArgs<$Schema, "Gallery">; +export type GalleryUpdateManyArgs = $UpdateManyArgs<$Schema, "Gallery">; +export type GalleryUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Gallery">; +export type GalleryUpsertArgs = $UpsertArgs<$Schema, "Gallery">; +export type GalleryDeleteArgs = $DeleteArgs<$Schema, "Gallery">; +export type GalleryDeleteManyArgs = $DeleteManyArgs<$Schema, "Gallery">; +export type GalleryCountArgs = $CountArgs<$Schema, "Gallery">; +export type GalleryAggregateArgs = $AggregateArgs<$Schema, "Gallery">; +export type GalleryGroupByArgs = $GroupByArgs<$Schema, "Gallery">; +export type GalleryWhereInput = $WhereInput<$Schema, "Gallery">; +export type GallerySelect = $SelectInput<$Schema, "Gallery">; +export type GalleryInclude = $IncludeInput<$Schema, "Gallery">; +export type GalleryOmit = $OmitInput<$Schema, "Gallery">; +export type GalleryGetPayload> = $SimplifiedModelResult<$Schema, "Gallery", Args>; diff --git a/packages/runtime/test/schemas/delegate/models.ts b/packages/runtime/test/schemas/delegate/models.ts new file mode 100644 index 000000000..044f5d607 --- /dev/null +++ b/packages/runtime/test/schemas/delegate/models.ts @@ -0,0 +1,16 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaType as $Schema } from "./schema"; +import { type ModelResult as $ModelResult } from "@zenstackhq/runtime"; +export type User = $ModelResult<$Schema, "User">; +export type Comment = $ModelResult<$Schema, "Comment">; +export type Asset = $ModelResult<$Schema, "Asset">; +export type Video = $ModelResult<$Schema, "Video">; +export type RatedVideo = $ModelResult<$Schema, "RatedVideo">; +export type Image = $ModelResult<$Schema, "Image">; +export type Gallery = $ModelResult<$Schema, "Gallery">; diff --git a/packages/runtime/test/schemas/delegate/schema.ts b/packages/runtime/test/schemas/delegate/schema.ts new file mode 100644 index 000000000..a15d4f467 --- /dev/null +++ b/packages/runtime/test/schemas/delegate/schema.ts @@ -0,0 +1,465 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, ExpressionUtils } from "../../../dist/schema"; +export const schema = { + provider: { + type: "sqlite" + }, + models: { + User: { + name: "User", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + email: { + name: "email", + type: "String", + unique: true, + optional: true, + attributes: [{ name: "@unique" }] + }, + level: { + name: "level", + type: "Int", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }], + default: 0 + }, + assets: { + name: "assets", + type: "Asset", + array: true, + relation: { opposite: "owner" } + }, + ratedVideos: { + name: "ratedVideos", + type: "RatedVideo", + array: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("direct") }] }], + relation: { opposite: "user", name: "direct" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + email: { type: "String" } + } + }, + Comment: { + name: "Comment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + content: { + name: "content", + type: "String" + }, + asset: { + name: "asset", + type: "Asset", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("assetId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "comments", fields: ["assetId"], references: ["id"], onDelete: "Cascade" } + }, + assetId: { + name: "assetId", + type: "Int", + optional: true, + foreignKeyFor: [ + "asset" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Asset: { + name: "Asset", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] + }, + viewCount: { + name: "viewCount", + type: "Int", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }], + default: 0 + }, + owner: { + name: "owner", + type: "User", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } + }, + ownerId: { + name: "ownerId", + type: "Int", + optional: true, + foreignKeyFor: [ + "owner" + ] + }, + comments: { + name: "comments", + type: "Comment", + array: true, + relation: { opposite: "asset" } + }, + assetType: { + name: "assetType", + type: "String", + isDiscriminator: true + } + }, + attributes: [ + { name: "@@delegate", args: [{ name: "discriminator", value: ExpressionUtils.field("assetType") }] } + ], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + }, + isDelegate: true, + subModels: ["Video", "Image"] + }, + Video: { + name: "Video", + baseModel: "Asset", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Asset", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + originModel: "Asset", + attributes: [{ name: "@updatedAt" }] + }, + viewCount: { + name: "viewCount", + type: "Int", + originModel: "Asset", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }], + default: 0 + }, + owner: { + name: "owner", + type: "User", + optional: true, + originModel: "Asset", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } + }, + ownerId: { + name: "ownerId", + type: "Int", + optional: true, + originModel: "Asset", + foreignKeyFor: [ + "owner" + ] + }, + comments: { + name: "comments", + type: "Comment", + array: true, + originModel: "Asset", + relation: { opposite: "asset" } + }, + assetType: { + name: "assetType", + type: "String", + originModel: "Asset", + isDiscriminator: true + }, + duration: { + name: "duration", + type: "Int" + }, + url: { + name: "url", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] + }, + videoType: { + name: "videoType", + type: "String", + isDiscriminator: true + } + }, + attributes: [ + { name: "@@delegate", args: [{ name: "discriminator", value: ExpressionUtils.field("videoType") }] } + ], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + url: { type: "String" } + }, + isDelegate: true, + subModels: ["RatedVideo"] + }, + RatedVideo: { + name: "RatedVideo", + baseModel: "Video", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Asset", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + originModel: "Asset", + attributes: [{ name: "@updatedAt" }] + }, + viewCount: { + name: "viewCount", + type: "Int", + originModel: "Asset", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }], + default: 0 + }, + owner: { + name: "owner", + type: "User", + optional: true, + originModel: "Asset", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } + }, + ownerId: { + name: "ownerId", + type: "Int", + optional: true, + originModel: "Asset", + foreignKeyFor: [ + "owner" + ] + }, + comments: { + name: "comments", + type: "Comment", + array: true, + originModel: "Asset", + relation: { opposite: "asset" } + }, + assetType: { + name: "assetType", + type: "String", + originModel: "Asset", + isDiscriminator: true + }, + duration: { + name: "duration", + type: "Int", + originModel: "Video" + }, + url: { + name: "url", + type: "String", + unique: true, + originModel: "Video", + attributes: [{ name: "@unique" }] + }, + videoType: { + name: "videoType", + type: "String", + originModel: "Video", + isDiscriminator: true + }, + rating: { + name: "rating", + type: "Int" + }, + user: { + name: "user", + type: "User", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("direct") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "ratedVideos", name: "direct", fields: ["userId"], references: ["id"], onDelete: "Cascade" } + }, + userId: { + name: "userId", + type: "Int", + optional: true, + foreignKeyFor: [ + "user" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + url: { type: "String" } + } + }, + Image: { + name: "Image", + baseModel: "Asset", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Asset", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }], + default: ExpressionUtils.call("now") + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + originModel: "Asset", + attributes: [{ name: "@updatedAt" }] + }, + viewCount: { + name: "viewCount", + type: "Int", + originModel: "Asset", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }], + default: 0 + }, + owner: { + name: "owner", + type: "User", + optional: true, + originModel: "Asset", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } + }, + ownerId: { + name: "ownerId", + type: "Int", + optional: true, + originModel: "Asset", + foreignKeyFor: [ + "owner" + ] + }, + comments: { + name: "comments", + type: "Comment", + array: true, + originModel: "Asset", + relation: { opposite: "asset" } + }, + assetType: { + name: "assetType", + type: "String", + originModel: "Asset", + isDiscriminator: true + }, + format: { + name: "format", + type: "String" + }, + gallery: { + name: "gallery", + type: "Gallery", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("galleryId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + relation: { opposite: "images", fields: ["galleryId"], references: ["id"], onDelete: "Cascade" } + }, + galleryId: { + name: "galleryId", + type: "Int", + optional: true, + foreignKeyFor: [ + "gallery" + ] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Gallery: { + name: "Gallery", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + images: { + name: "images", + type: "Image", + array: true, + relation: { opposite: "gallery" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + } + }, + authType: "User", + plugins: {} +} as const satisfies SchemaDef; +export type SchemaType = typeof schema; diff --git a/packages/runtime/test/schemas/delegate/schema.zmodel b/packages/runtime/test/schemas/delegate/schema.zmodel new file mode 100644 index 000000000..9ecc5a830 --- /dev/null +++ b/packages/runtime/test/schemas/delegate/schema.zmodel @@ -0,0 +1,57 @@ +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +model User { + id Int @id @default(autoincrement()) + email String? @unique + level Int @default(0) + assets Asset[] + ratedVideos RatedVideo[] @relation('direct') +} + +model Comment { + id Int @id @default(autoincrement()) + content String + asset Asset? @relation(fields: [assetId], references: [id], onDelete: Cascade) + assetId Int? +} + +model Asset { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + viewCount Int @default(0) + owner User? @relation(fields: [ownerId], references: [id], onDelete: Cascade) + ownerId Int? + comments Comment[] + assetType String + + @@delegate(assetType) +} + +model Video extends Asset { + duration Int + url String @unique + videoType String + + @@delegate(videoType) +} + +model RatedVideo extends Video { + rating Int + user User? @relation(name: 'direct', fields: [userId], references: [id], onDelete: Cascade) + userId Int? +} + +model Image extends Asset { + format String + gallery Gallery? @relation(fields: [galleryId], references: [id], onDelete: Cascade) + galleryId Int? +} + +model Gallery { + id Int @id @default(autoincrement()) + images Image[] +} diff --git a/packages/runtime/test/schemas/delegate/typecheck.ts b/packages/runtime/test/schemas/delegate/typecheck.ts new file mode 100644 index 000000000..847289f28 --- /dev/null +++ b/packages/runtime/test/schemas/delegate/typecheck.ts @@ -0,0 +1,183 @@ +import SQLite from 'better-sqlite3'; +import { ZenStackClient } from '../../../dist'; +import { schema } from './schema'; + +const client = new ZenStackClient(schema, { + dialectConfig: { + database: new SQLite('./zenstack/test.db'), + }, +}); + +async function find() { + // delegate find should result in a discriminated union type + const r = await client.asset.findFirstOrThrow(); + console.log(r.assetType); + console.log(r.viewCount); + // @ts-expect-error + console.log(r.duration); + // @ts-expect-error + console.log(r.rating); + if (r.assetType === 'Video') { + // video + console.log(r.duration); + // only one choice `RatedVideo` + console.log(r.rating); + } else { + // image + console.log(r.format); + } + + // if fields are explicitly selected, then no sub-model fields are available + const r1 = await client.asset.findFirstOrThrow({ + select: { + id: true, + viewCount: true, + assetType: true, + }, + }); + // @ts-expect-error + console.log(r1.duration); + if (r1.assetType === 'Video') { + // @ts-expect-error + console.log(r1.duration); + } + + // same behavior when queried as a relation + const r2 = await client.user.findFirstOrThrow({ include: { assets: true } }); + console.log(r2.assets[0]?.assetType); + console.log(r2.assets[0]?.viewCount); + // @ts-expect-error + console.log(r2.assets[0]?.duration); + // @ts-expect-error + console.log(r2.assets[0]?.rating); + if (r2.assets[0]?.assetType === 'Video') { + // video + console.log(r2.assets[0]?.duration); + // only one choice `RatedVideo` + console.log(r2.assets[0]?.rating); + } else { + // image + console.log(r2.assets[0]?.format); + } + + // sub model behavior + const r3 = await client.ratedVideo.findFirstOrThrow(); + console.log(r3.assetType); + console.log(r3.viewCount); + console.log(r3.videoType); + console.log(r3.duration); + console.log(r3.rating); +} + +async function create() { + // delegate creation is not allowed + // @ts-expect-error + client.asset.create({ data: { assetType: 'Video' } }); + // @ts-expect-error + client.asset.createMany({ data: [{ assetType: 'Video' }] }); + // @ts-expect-error + client.asset.upsert({ where: { id: 1 }, create: { assetType: 'Video' }, update: { assetType: 'Video' } }); + + // nested creation is not allowed either + // @ts-expect-error + client.user.create({ data: { assets: { create: { assetType: 'Video' } } } }); + // @ts-expect-error + client.user.create({ data: { assets: { connectOrCreate: { where: { id: 1 }, create: { assetType: 'Video' } } } } }); + // @ts-expect-error + client.user.update({ where: { id: 1 }, data: { assets: { create: { assetType: 'Video' } } } }); + client.user.update({ + where: { id: 1 }, + // @ts-expect-error + data: { assets: { connectOrCreate: { where: { id: 1 }, create: { assetType: 'Video' } } } }, + }); + client.user.update({ + where: { id: 1 }, + data: { + // @ts-expect-error + assets: { upsert: { where: { id: 1 }, create: { assetType: 'Video' }, update: { assetType: 'Video' } } }, + }, + }); + + // discriminator fields cannot be assigned in create + await client.ratedVideo.create({ + data: { + url: 'abc', + rating: 5, + duration: 100, + // @ts-expect-error + assetType: 'Video', + }, + }); +} + +async function update() { + // delegate models can be updated normally + await client.ratedVideo.update({ + where: { id: 1 }, + data: { url: 'new-url', rating: 4, duration: 200 }, + }); + + await client.video.update({ + where: { id: 1 }, + data: { duration: 300, url: 'another-url' }, + }); + + // discriminator fields cannot be set in updates + await client.ratedVideo.update({ + where: { id: 1 }, + data: { + url: 'valid-update', + // @ts-expect-error + assetType: 'Video', + }, + }); + + await client.image.update({ + where: { id: 1 }, + data: { + format: 'jpg', + // @ts-expect-error + assetType: 'Image', + }, + }); + + // updateMany also cannot set discriminator fields + await client.ratedVideo.updateMany({ + where: { rating: { gt: 3 } }, + data: { + // @ts-expect-error + assetType: 'Video', + }, + }); + + // upsert cannot set discriminator fields in update clause + await client.ratedVideo.upsert({ + where: { id: 1 }, + create: { url: 'create-url', rating: 5, duration: 100 }, + update: { + rating: 4, + // @ts-expect-error + assetType: 'Video', + }, + }); +} + +async function queryBuilder() { + // query builder API should see the raw table fields + + client.$qb.selectFrom('Asset').select(['id', 'viewCount']).execute(); + // @ts-expect-error + client.$qb.selectFrom('Asset').select(['duration']).execute(); + client.$qb.selectFrom('Video').select(['id', 'duration']).execute(); + // @ts-expect-error + client.$qb.selectFrom('Video').select(['viewCount']).execute(); +} + +async function main() { + await create(); + await update(); + await find(); + await queryBuilder(); +} + +main(); diff --git a/packages/runtime/test/schemas/typing/verify-typing.ts b/packages/runtime/test/schemas/typing/typecheck.ts similarity index 100% rename from packages/runtime/test/schemas/typing/verify-typing.ts rename to packages/runtime/test/schemas/typing/typecheck.ts diff --git a/packages/sdk/src/schema/schema.ts b/packages/sdk/src/schema/schema.ts index d7a38f9e3..c6ea4d9bf 100644 --- a/packages/sdk/src/schema/schema.ts +++ b/packages/sdk/src/schema/schema.ts @@ -32,6 +32,7 @@ export type ModelDef = { idFields: string[]; computedFields?: Record; isDelegate?: boolean; + subModels?: string[]; }; export type AttributeApplication = { @@ -69,6 +70,7 @@ export type FieldDef = { foreignKeyFor?: string[]; computed?: boolean; originModel?: string; + isDiscriminator?: boolean; }; export type ProcedureParam = { name: string; type: string; optional?: boolean }; @@ -105,6 +107,17 @@ export type TypeDefDef = { export type GetModels = Extract; +export type GetDelegateModels = keyof { + [Key in GetModels as Schema['models'][Key]['isDelegate'] extends true ? Key : never]: true; +}; + +export type GetSubModels> = GetModel< + Schema, + Model +>['subModels'] extends string[] + ? Extract['subModels'][number], GetModels> + : never; + export type GetModel> = Schema['models'][Model]; export type GetEnums = keyof Schema['enums']; @@ -127,6 +140,14 @@ export type GetModelField< Field extends GetModelFields, > = GetModel['fields'][Field]; +export type GetModelDiscriminator> = keyof { + [Key in GetModelFields as FieldIsDelegateDiscriminator extends true + ? GetModelField['originModel'] extends string + ? never + : Key + : never]: true; +}; + export type GetModelFieldType< Schema extends SchemaDef, Model extends GetModels, @@ -239,4 +260,24 @@ export type FieldIsRelationArray< Field extends GetModelFields, > = FieldIsRelation extends true ? FieldIsArray : false; +export type IsDelegateModel< + Schema extends SchemaDef, + Model extends GetModels, +> = Schema['models'][Model]['isDelegate'] extends true ? true : false; + +export type FieldIsDelegateRelation< + Schema extends SchemaDef, + Model extends GetModels, + Field extends RelationFields, +> = + GetModelFieldType extends GetModels + ? IsDelegateModel> + : false; + +export type FieldIsDelegateDiscriminator< + Schema extends SchemaDef, + Model extends GetModels, + Field extends GetModelFields, +> = GetModelField['isDiscriminator'] extends true ? true : false; + //#endregion diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index ba4f31a59..1f55db3e0 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -36,7 +36,7 @@ import { UnaryExpr, type Model, } from '@zenstackhq/language/ast'; -import { getAllAttributes, getAllFields } from '@zenstackhq/language/utils'; +import { getAllAttributes, getAllFields, isDataFieldReference } from '@zenstackhq/language/utils'; import fs from 'node:fs'; import path from 'node:path'; import { match } from 'ts-pattern'; @@ -228,6 +228,7 @@ export class TsSchemaGenerator { } return true; }); + const subModels = this.getSubModels(dm); const fields: ts.PropertyAssignment[] = [ // name @@ -282,6 +283,18 @@ export class TsSchemaGenerator { ...(isDelegateModel(dm) ? [ts.factory.createPropertyAssignment('isDelegate', ts.factory.createTrue())] : []), + + // subModels + ...(subModels.length > 0 + ? [ + ts.factory.createPropertyAssignment( + 'subModels', + ts.factory.createArrayLiteralExpression( + subModels.map((subModel) => ts.factory.createStringLiteral(subModel)), + ), + ), + ] + : []), ]; const computedFields = dm.fields.filter((f) => hasAttribute(f, '@computed')); @@ -295,6 +308,13 @@ export class TsSchemaGenerator { return ts.factory.createObjectLiteralExpression(fields, true); } + private getSubModels(dm: DataModel) { + return dm.$container.declarations + .filter(isDataModel) + .filter((d) => d.baseModel?.ref === dm) + .map((d) => d.name); + } + private createTypeDefObject(td: TypeDef): ts.Expression { const allFields = getAllFields(td); const allAttributes = getAllAttributes(td); @@ -386,22 +406,6 @@ export class TsSchemaGenerator { ts.factory.createPropertyAssignment('type', this.generateFieldTypeLiteral(field)), ]; - if ( - contextModel && - // id fields are duplicated in inherited models - !isIdField(field, contextModel) && - field.$container !== contextModel && - isDelegateModel(field.$container) - ) { - // field is inherited from delegate - objectFields.push( - ts.factory.createPropertyAssignment( - 'originModel', - ts.factory.createStringLiteral(field.$container.name), - ), - ); - } - if (contextModel && ModelUtils.isIdField(field, contextModel)) { objectFields.push(ts.factory.createPropertyAssignment('id', ts.factory.createTrue())); } @@ -422,6 +426,28 @@ export class TsSchemaGenerator { objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ts.factory.createTrue())); } + // originModel + if ( + contextModel && + // id fields are duplicated in inherited models + !isIdField(field, contextModel) && + field.$container !== contextModel && + isDelegateModel(field.$container) + ) { + // field is inherited from delegate + objectFields.push( + ts.factory.createPropertyAssignment( + 'originModel', + ts.factory.createStringLiteral(field.$container.name), + ), + ); + } + + // discriminator + if (this.isDiscriminatorField(field)) { + objectFields.push(ts.factory.createPropertyAssignment('isDiscriminator', ts.factory.createTrue())); + } + // attributes if (field.attributes.length > 0) { objectFields.push( @@ -523,6 +549,16 @@ export class TsSchemaGenerator { return ts.factory.createObjectLiteralExpression(objectFields, true); } + private isDiscriminatorField(field: DataField) { + const origin = field.$container; + return getAttribute(origin, '@@delegate')?.args.some( + (arg) => + arg.$resolvedParam.name === 'discriminator' && + isDataFieldReference(arg.value) && + arg.value.target.ref === field, + ); + } + private getDataSourceProvider( model: Model, ): { type: string; env: undefined; url: string } | { type: string; env: string; url: undefined } { diff --git a/turbo.json b/turbo.json index 203466fce..ab312f312 100644 --- a/turbo.json +++ b/turbo.json @@ -6,6 +6,11 @@ "inputs": ["src/**", "zenstack/*.zmodel"], "outputs": ["dist/**"] }, + "watch": { + "dependsOn": ["^build"], + "inputs": ["src/**", "zenstack/*.zmodel"], + "outputs": [] + }, "lint": { "dependsOn": ["^lint"] },