From 233b47d2edfd7925ffa4ac1bc59c9452ee59c81a Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 16 Jan 2026 20:52:42 +0800 Subject: [PATCH 01/25] feat: plugin-extended query args (#599) https://discord.com/channels/1035538056146595961/1460921516936925388 --- packages/orm/src/client/client-impl.ts | 62 ++-- packages/orm/src/client/contract.ts | 138 +++++---- packages/orm/src/client/crud-types.ts | 8 +- .../orm/src/client/crud/operations/base.ts | 105 +++++-- .../orm/src/client/crud/operations/find.ts | 9 +- .../orm/src/client/crud/validator/index.ts | 168 +++++------ packages/orm/src/client/index.ts | 7 + packages/orm/src/client/options.ts | 8 +- packages/orm/src/client/plugin.ts | 36 ++- packages/orm/src/utils/type-utils.ts | 2 + .../policy/src/expression-transformer.ts | 3 +- packages/plugins/policy/src/plugin.ts | 8 +- pnpm-lock.yaml | 18 +- samples/orm/main.ts | 11 +- samples/orm/package.json | 3 +- tests/e2e/orm/client-api/find.test.ts | 2 +- .../orm/plugin-infra/ext-query-args.test.ts | 273 ++++++++++++++++++ .../orm/plugin-infra/ext-query-args/input.ts | 31 ++ .../orm/plugin-infra/ext-query-args/models.ts | 10 + .../orm/plugin-infra/ext-query-args/schema.ts | 38 +++ .../plugin-infra/ext-query-args/schema.zmodel | 8 + .../e2e/orm/policy/basic-schema-read.test.ts | 2 +- tests/e2e/package.json | 3 +- 23 files changed, 729 insertions(+), 224 deletions(-) create mode 100644 tests/e2e/orm/plugin-infra/ext-query-args.test.ts create mode 100644 tests/e2e/orm/plugin-infra/ext-query-args/input.ts create mode 100644 tests/e2e/orm/plugin-infra/ext-query-args/models.ts create mode 100644 tests/e2e/orm/plugin-infra/ext-query-args/schema.ts create mode 100644 tests/e2e/orm/plugin-infra/ext-query-args/schema.zmodel diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index ce2d4d428..4af8b0310 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -21,13 +21,13 @@ import type { TransactionIsolationLevel, } from './contract'; import { AggregateOperationHandler } from './crud/operations/aggregate'; -import type { AllCrudOperation, CoreCrudOperation } from './crud/operations/base'; +import type { AllCrudOperations, CoreCrudOperations } from './crud/operations/base'; import { BaseOperationHandler } from './crud/operations/base'; import { CountOperationHandler } from './crud/operations/count'; import { CreateOperationHandler } from './crud/operations/create'; import { DeleteOperationHandler } from './crud/operations/delete'; -import { FindOperationHandler } from './crud/operations/find'; import { ExistsOperationHandler } from './crud/operations/exists'; +import { FindOperationHandler } from './crud/operations/find'; import { GroupByOperationHandler } from './crud/operations/group-by'; import { UpdateOperationHandler } from './crud/operations/update'; import { InputValidator } from './crud/validator'; @@ -59,6 +59,7 @@ export class ClientImpl { public readonly $schema: SchemaDef; readonly kyselyProps: KyselyProps; private auth: AuthType | undefined; + inputValidator: InputValidator; constructor( private readonly schema: SchemaDef, @@ -114,6 +115,7 @@ export class ClientImpl { } this.kysely = new Kysely(this.kyselyProps); + this.inputValidator = baseClient?.inputValidator ?? new InputValidator(this as any); return createClientProxy(this); } @@ -242,8 +244,7 @@ export class ClientImpl { } // Validate inputs using the same validator infrastructure as CRUD operations. - const inputValidator = new InputValidator(this as any); - const validatedInput = inputValidator.validateProcedureInput(name, input); + const validatedInput = this.inputValidator.validateProcedureInput(name, input); const handler = procOptions[name] as Function; @@ -292,19 +293,22 @@ export class ClientImpl { await new SchemaDbPusher(this.schema, this.kysely).push(); } - $use(plugin: RuntimePlugin) { - // tsc perf - const newPlugins: RuntimePlugin[] = [...(this.$options.plugins ?? []), plugin]; + $use(plugin: RuntimePlugin) { + const newPlugins: RuntimePlugin[] = [...(this.$options.plugins ?? []), plugin]; const newOptions: ClientOptions = { ...this.options, plugins: newPlugins, }; - return new ClientImpl(this.schema, newOptions, this); + const newClient = new ClientImpl(this.schema, newOptions, this); + // create a new validator to have a fresh schema cache, because plugins may extend the + // query args schemas + newClient.inputValidator = new InputValidator(newClient as any); + return newClient; } $unuse(pluginId: string) { // tsc perf - const newPlugins: RuntimePlugin[] = []; + const newPlugins: RuntimePlugin[] = []; for (const plugin of this.options.plugins ?? []) { if (plugin.id !== pluginId) { newPlugins.push(plugin); @@ -314,16 +318,24 @@ export class ClientImpl { ...this.options, plugins: newPlugins, }; - return new ClientImpl(this.schema, newOptions, this); + const newClient = new ClientImpl(this.schema, newOptions, this); + // create a new validator to have a fresh schema cache, because plugins may + // extend the query args schemas + newClient.inputValidator = new InputValidator(newClient as any); + return newClient; } $unuseAll() { // tsc perf const newOptions: ClientOptions = { ...this.options, - plugins: [] as RuntimePlugin[], + plugins: [] as RuntimePlugin[], }; - return new ClientImpl(this.schema, newOptions, this); + const newClient = new ClientImpl(this.schema, newOptions, this); + // create a new validator to have a fresh schema cache, because plugins may + // extend the query args schemas + newClient.inputValidator = new InputValidator(newClient as any); + return newClient; } $setAuth(auth: AuthType | undefined) { @@ -340,10 +352,10 @@ export class ClientImpl { } $setOptions>(options: Options): ClientContract { - return new ClientImpl(this.schema, options as ClientOptions, this) as unknown as ClientContract< - SchemaDef, - Options - >; + const newClient = new ClientImpl(this.schema, options as ClientOptions, this); + // create a new validator to have a fresh schema cache, because options may change validation settings + newClient.inputValidator = new InputValidator(newClient as any); + return newClient as unknown as ClientContract; } $setInputValidation(enable: boolean) { @@ -351,7 +363,7 @@ export class ClientImpl { ...this.options, validateInput: enable, }; - return new ClientImpl(this.schema, newOptions, this); + return this.$setOptions(newOptions); } $executeRaw(query: TemplateStringsArray, ...values: any[]) { @@ -391,7 +403,6 @@ export class ClientImpl { } function createClientProxy(client: ClientImpl): ClientImpl { - const inputValidator = new InputValidator(client as any); const resultProcessor = new ResultProcessor(client.$schema, client.$options); return new Proxy(client, { @@ -403,7 +414,7 @@ function createClientProxy(client: ClientImpl): ClientImpl { if (typeof prop === 'string') { const model = Object.keys(client.$schema.models).find((m) => m.toLowerCase() === prop.toLowerCase()); if (model) { - return createModelCrudHandler(client as any, model, inputValidator, resultProcessor); + return createModelCrudHandler(client as any, model, client.inputValidator, resultProcessor); } } @@ -419,8 +430,8 @@ function createModelCrudHandler( resultProcessor: ResultProcessor, ): ModelOperations { const createPromise = ( - operation: CoreCrudOperation, - nominalOperation: AllCrudOperation, + operation: CoreCrudOperations, + nominalOperation: AllCrudOperations, args: unknown, handler: BaseOperationHandler, postProcess = false, @@ -448,8 +459,8 @@ function createModelCrudHandler( const onQuery = plugin.onQuery; if (onQuery) { const _proceed = proceed; - proceed = (_args: unknown) => - onQuery({ + proceed = (_args: unknown) => { + const ctx: any = { client, model, operation: nominalOperation, @@ -457,7 +468,9 @@ function createModelCrudHandler( args: _args, // ensure inner overrides are propagated to the previous proceed proceed: (nextArgs: unknown) => _proceed(nextArgs), - }) as Promise; + }; + return (onQuery as (ctx: any) => Promise)(ctx); + }; } } @@ -516,6 +529,7 @@ function createModelCrudHandler( args, new FindOperationHandler(client, model, inputValidator), true, + false, ); }, diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index af5e51389..0006cd2ab 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -40,13 +40,23 @@ import type { UpdateManyArgs, UpsertArgs, } from './crud-types'; +import type { CoreCrudOperations } from './crud/operations/base'; import type { ClientOptions, QueryOptions, ToQueryOptions } from './options'; -import type { RuntimePlugin } from './plugin'; +import type { ExtQueryArgsBase, RuntimePlugin } from './plugin'; import type { ZenStackPromise } from './promise'; import type { ToKysely } from './query-builder'; type TransactionUnsupportedMethods = (typeof TRANSACTION_UNSUPPORTED_METHODS)[number]; +/** + * Extracts extended query args for a specific operation. + */ +type ExtractExtQueryArgs = Operation extends keyof ExtQueryArgs + ? NonNullable + : 'all' extends keyof ExtQueryArgs + ? NonNullable + : {}; + /** * Transaction isolation levels. */ @@ -61,7 +71,11 @@ export enum TransactionIsolationLevel { /** * ZenStack client interface. */ -export type ClientContract = ClientOptions> = { +export type ClientContract< + Schema extends SchemaDef, + Options extends ClientOptions = ClientOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, +> = { /** * The schema definition. */ @@ -118,7 +132,7 @@ export type ClientContract | undefined): ClientContract; + $setAuth(auth: AuthType | undefined): ClientContract; /** * Returns a new client with new options applied. @@ -127,15 +141,15 @@ export type ClientContract>(options: Options): ClientContract; + $setOptions>(options: Options): ClientContract; /** * Returns a new client enabling/disabling input validations expressed with attributes like * `@email`, `@regex`, `@@validate`, etc. * - * @deprecated Use `$setOptions` instead. + * @deprecated Use {@link $setOptions} instead. */ - $setInputValidation(enable: boolean): ClientContract; + $setInputValidation(enable: boolean): ClientContract; /** * The Kysely query builder instance. @@ -151,7 +165,7 @@ export type ClientContract( - callback: (tx: Omit, TransactionUnsupportedMethods>) => Promise, + callback: (tx: TransactionClientContract) => Promise, options?: { isolationLevel?: TransactionIsolationLevel }, ): Promise; @@ -166,12 +180,14 @@ export type ClientContract): ClientContract; + $use( + plugin: RuntimePlugin, + ): ClientContract; /** * Returns a new client with the specified plugin removed. */ - $unuse(pluginId: string): ClientContract; + $unuse(pluginId: string): ClientContract; /** * Returns a new client with all plugins removed. @@ -194,16 +210,22 @@ export type ClientContract; } & { - [Key in GetModels as Uncapitalize]: ModelOperations>; + [Key in GetModels as Uncapitalize]: ModelOperations< + Schema, + Key, + ToQueryOptions, + ExtQueryArgs + >; } & ProcedureOperations; /** * The contract for a client in a transaction. */ -export type TransactionClientContract> = Omit< - ClientContract, - TransactionUnsupportedMethods ->; +export type TransactionClientContract< + Schema extends SchemaDef, + Options extends ClientOptions, + ExtQueryArgs extends ExtQueryArgsBase, +> = Omit, TransactionUnsupportedMethods>; export type ProcedureOperations = Schema['procedures'] extends Record @@ -253,6 +275,7 @@ export type AllModelOperations< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions, + ExtQueryArgs, > = { /** * Returns a list of entities. @@ -335,8 +358,8 @@ export type AllModelOperations< * }); // result: `{ _count: { posts: number } }` * ``` */ - findMany>( - args?: SelectSubset>, + findMany & ExtractExtQueryArgs>( + args?: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise[]>; /** @@ -345,8 +368,8 @@ export type AllModelOperations< * @returns a single entity or null if not found * @see {@link findMany} */ - findUnique>( - args: SelectSubset>, + findUnique & ExtractExtQueryArgs>( + args: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise | null>; /** @@ -355,8 +378,8 @@ export type AllModelOperations< * @returns a single entity * @see {@link findMany} */ - findUniqueOrThrow>( - args: SelectSubset>, + findUniqueOrThrow & ExtractExtQueryArgs>( + args: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise>; /** @@ -365,8 +388,8 @@ export type AllModelOperations< * @returns a single entity or null if not found * @see {@link findMany} */ - findFirst>( - args?: SelectSubset>, + findFirst & ExtractExtQueryArgs>( + args?: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise | null>; /** @@ -375,8 +398,8 @@ export type AllModelOperations< * @returns a single entity * @see {@link findMany} */ - findFirstOrThrow>( - args?: SelectSubset>, + findFirstOrThrow & ExtractExtQueryArgs>( + args?: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise>; /** @@ -431,8 +454,8 @@ export type AllModelOperations< * }); * ``` */ - create>( - args: SelectSubset>, + create & ExtractExtQueryArgs>( + args: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise>; /** @@ -460,8 +483,8 @@ export type AllModelOperations< * }); * ``` */ - createMany>( - args?: SelectSubset>, + createMany & ExtractExtQueryArgs>( + args?: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise; /** @@ -482,8 +505,13 @@ export type AllModelOperations< * }); * ``` */ - createManyAndReturn>( - args?: SelectSubset>, + createManyAndReturn< + T extends CreateManyAndReturnArgs & ExtractExtQueryArgs, + >( + args?: SelectSubset< + T, + CreateManyAndReturnArgs & ExtractExtQueryArgs + >, ): ZenStackPromise[]>; /** @@ -603,8 +631,8 @@ export type AllModelOperations< * }); * ``` */ - update>( - args: SelectSubset>, + update & ExtractExtQueryArgs>( + args: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise>; /** @@ -627,8 +655,8 @@ export type AllModelOperations< * limit: 10 * }); */ - updateMany>( - args: Subset>, + updateMany & ExtractExtQueryArgs>( + args: Subset & ExtractExtQueryArgs>, ): ZenStackPromise; /** @@ -653,8 +681,13 @@ export type AllModelOperations< * }); * ``` */ - updateManyAndReturn>( - args: Subset>, + updateManyAndReturn< + T extends UpdateManyAndReturnArgs & ExtractExtQueryArgs, + >( + args: Subset< + T, + UpdateManyAndReturnArgs & ExtractExtQueryArgs + >, ): ZenStackPromise[]>; /** @@ -677,8 +710,8 @@ export type AllModelOperations< * }); * ``` */ - upsert>( - args: SelectSubset>, + upsert & ExtractExtQueryArgs>( + args: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise>; /** @@ -700,8 +733,8 @@ export type AllModelOperations< * }); // result: `{ id: string; email: string }` * ``` */ - delete>( - args: SelectSubset>, + delete & ExtractExtQueryArgs>( + args: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise>; /** @@ -723,8 +756,8 @@ export type AllModelOperations< * }); * ``` */ - deleteMany>( - args?: Subset>, + deleteMany & ExtractExtQueryArgs>( + args?: Subset & ExtractExtQueryArgs>, ): ZenStackPromise; /** @@ -745,8 +778,8 @@ export type AllModelOperations< * select: { _all: true, email: true } * }); // result: `{ _all: number, email: number }` */ - count>( - args?: Subset>, + count & ExtractExtQueryArgs>( + args?: Subset & ExtractExtQueryArgs>, ): ZenStackPromise>>; /** @@ -766,8 +799,8 @@ export type AllModelOperations< * _max: { age: true } * }); // result: `{ _count: number, _avg: { age: number }, ... }` */ - aggregate>( - args: Subset>, + aggregate & ExtractExtQueryArgs>( + args: Subset & ExtractExtQueryArgs>, ): ZenStackPromise>>; /** @@ -803,8 +836,8 @@ export type AllModelOperations< * having: { country: 'US', age: { _avg: { gte: 18 } } } * }); */ - groupBy>( - args: Subset>, + groupBy & ExtractExtQueryArgs>( + args: Subset & ExtractExtQueryArgs>, ): ZenStackPromise>>; /** @@ -818,14 +851,14 @@ export type AllModelOperations< * await db.user.exists({ * where: { id: 1 }, * }); // result: `boolean` - * + * * // check with a relation * await db.user.exists({ * where: { posts: { some: { published: true } } }, * }); // result: `boolean` */ - exists>( - args?: Subset>, + exists & ExtractExtQueryArgs>( + args?: Subset & ExtractExtQueryArgs>, ): ZenStackPromise; }; @@ -835,8 +868,9 @@ export type ModelOperations< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions = QueryOptions, + ExtQueryArgs = {}, > = Omit< - AllModelOperations, + AllModelOperations, // exclude operations not applicable to delegate models IsDelegateModel extends true ? OperationsIneligibleForDelegateModels : never >; diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 1b6f3d3c4..5626931e7 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -215,10 +215,10 @@ export type ModelResult< FieldIsArray >; } & ('_count' extends keyof I - ? I['_count'] extends false | undefined - ? {} - : { _count: SelectCountResult } - : {}) + ? I['_count'] extends false | undefined + ? {} + : { _count: SelectCountResult } + : {}) : Args extends { omit: infer O } & Record ? DefaultModelResult : DefaultModelResult, diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 957b94ab8..c9c85121c 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -54,25 +54,90 @@ import { getCrudDialect } from '../dialects'; import type { BaseCrudDialect } from '../dialects/base-dialect'; import { InputValidator } from '../validator'; -export type CoreCrudOperation = - | 'findMany' - | 'findUnique' - | 'findFirst' - | 'create' - | 'createMany' - | 'createManyAndReturn' - | 'update' - | 'updateMany' - | 'updateManyAndReturn' - | 'upsert' - | 'delete' - | 'deleteMany' - | 'count' - | 'aggregate' - | 'groupBy' - | 'exists'; - -export type AllCrudOperation = CoreCrudOperation | 'findUniqueOrThrow' | 'findFirstOrThrow'; +/** + * List of core CRUD operations. It excludes the 'orThrow' variants. + */ +export const CoreCrudOperations = [ + 'findMany', + 'findUnique', + 'findFirst', + 'create', + 'createMany', + 'createManyAndReturn', + 'update', + 'updateMany', + 'updateManyAndReturn', + 'upsert', + 'delete', + 'deleteMany', + 'count', + 'aggregate', + 'groupBy', + 'exists', +] as const; + +/** + * List of core CRUD operations. It excludes the 'orThrow' variants. + */ +export type CoreCrudOperations = (typeof CoreCrudOperations)[number]; + +/** + * List of core read operations. It excludes the 'orThrow' variants. + */ +export const CoreReadOperations = [ + 'findMany', + 'findUnique', + 'findFirst', + 'count', + 'aggregate', + 'groupBy', + 'exists', +] as const; + +/** + * List of core read operations. It excludes the 'orThrow' variants. + */ +export type CoreReadOperations = (typeof CoreReadOperations)[number]; + +/** + * List of core write operations. + */ +export const CoreWriteOperations = [ + 'create', + 'createMany', + 'createManyAndReturn', + 'update', + 'updateMany', + 'updateManyAndReturn', + 'upsert', + 'delete', + 'deleteMany', +] as const; + +/** + * List of core write operations. + */ +export type CoreWriteOperations = (typeof CoreWriteOperations)[number]; + +/** + * List of all CRUD operations, including 'orThrow' variants. + */ +export const AllCrudOperations = [...CoreCrudOperations, 'findUniqueOrThrow', 'findFirstOrThrow'] as const; + +/** + * List of all CRUD operations, including 'orThrow' variants. + */ +export type AllCrudOperations = (typeof AllCrudOperations)[number]; + +/** + * List of all read operations, including 'orThrow' variants. + */ +export const AllReadOperations = [...CoreReadOperations, 'findUniqueOrThrow', 'findFirstOrThrow'] as const; + +/** + * List of all read operations, including 'orThrow' variants. + */ +export type AllReadOperations = (typeof AllReadOperations)[number]; // context for nested relation operations export type FromRelationContext = { @@ -109,7 +174,7 @@ export abstract class BaseOperationHandler { return this.client.$qb; } - abstract handle(operation: CoreCrudOperation, args: any): Promise; + abstract handle(operation: CoreCrudOperations, args: any): Promise; withClient(client: ClientContract) { return new (this.constructor as new (...args: any[]) => this)(client, this.model, this.inputValidator); diff --git a/packages/orm/src/client/crud/operations/find.ts b/packages/orm/src/client/crud/operations/find.ts index 49938c8c3..db087a3b5 100644 --- a/packages/orm/src/client/crud/operations/find.ts +++ b/packages/orm/src/client/crud/operations/find.ts @@ -1,9 +1,9 @@ import type { GetModels, SchemaDef } from '../../../schema'; import type { FindArgs } from '../../crud-types'; -import { BaseOperationHandler, type CoreCrudOperation } from './base'; +import { BaseOperationHandler, type CoreCrudOperations } from './base'; export class FindOperationHandler extends BaseOperationHandler { - async handle(operation: CoreCrudOperation, args: unknown, validateArgs = true): Promise { + async handle(operation: CoreCrudOperations, args: unknown, validateArgs = true): Promise { // normalize args to strip `undefined` fields const normalizedArgs = this.normalizeArgs(args); @@ -11,10 +11,7 @@ export class FindOperationHandler extends BaseOperatio // parse args let parsedArgs = validateArgs - ? this.inputValidator.validateFindArgs(this.model, normalizedArgs, { - unique: operation === 'findUnique', - findOne, - }) + ? this.inputValidator.validateFindArgs(this.model, normalizedArgs, operation) : (normalizedArgs as FindArgs, true> | undefined); if (findOne) { diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 50245c605..3556c6046 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -2,7 +2,7 @@ import { enumerate, invariant } from '@zenstackhq/common-helpers'; import Decimal from 'decimal.js'; import stableStringify from 'json-stable-stringify'; import { match, P } from 'ts-pattern'; -import { z, ZodType } from 'zod'; +import { z, ZodObject, ZodType } from 'zod'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; import { type AttributeApplication, @@ -46,6 +46,7 @@ import { requireField, requireModel, } from '../../query-utils'; +import type { CoreCrudOperations } from '../operations/base'; import { addBigIntValidation, addCustomValidation, @@ -55,11 +56,11 @@ import { addStringValidation, } from './utils'; -const schemaCache = new WeakMap>(); - -type GetSchemaFunc = (model: GetModels, options: Options) => ZodType; +type GetSchemaFunc = (model: GetModels) => ZodType; export class InputValidator { + private readonly schemaCache = new Map(); + constructor(private readonly client: ClientContract) {} private get schema() { @@ -191,19 +192,20 @@ export class InputValidator { validateFindArgs( model: GetModels, args: unknown, - options: { unique: boolean; findOne: boolean }, + operation: CoreCrudOperations, ): FindArgs, true> | undefined { - return this.validate< - FindArgs, true> | undefined, - Parameters[1] - >(model, 'find', options, (model, options) => this.makeFindSchema(model, options), args); + return this.validate, true> | undefined>( + model, + operation, + (model) => this.makeFindSchema(model, operation), + args, + ); } validateExistsArgs(model: GetModels, args: unknown): ExistsArgs> | undefined { return this.validate>>( model, 'exists', - undefined, (model) => this.makeExistsSchema(model), args, ); @@ -213,7 +215,6 @@ export class InputValidator { return this.validate>>( model, 'create', - undefined, (model) => this.makeCreateSchema(model), args, ); @@ -223,7 +224,6 @@ export class InputValidator { return this.validate>>( model, 'createMany', - undefined, (model) => this.makeCreateManySchema(model), args, ); @@ -236,7 +236,6 @@ export class InputValidator { return this.validate> | undefined>( model, 'createManyAndReturn', - undefined, (model) => this.makeCreateManyAndReturnSchema(model), args, ); @@ -246,7 +245,6 @@ export class InputValidator { return this.validate>>( model, 'update', - undefined, (model) => this.makeUpdateSchema(model), args, ); @@ -256,7 +254,6 @@ export class InputValidator { return this.validate>>( model, 'updateMany', - undefined, (model) => this.makeUpdateManySchema(model), args, ); @@ -269,7 +266,6 @@ export class InputValidator { return this.validate>>( model, 'updateManyAndReturn', - undefined, (model) => this.makeUpdateManyAndReturnSchema(model), args, ); @@ -279,7 +275,6 @@ export class InputValidator { return this.validate>>( model, 'upsert', - undefined, (model) => this.makeUpsertSchema(model), args, ); @@ -289,7 +284,6 @@ export class InputValidator { return this.validate>>( model, 'delete', - undefined, (model) => this.makeDeleteSchema(model), args, ); @@ -302,7 +296,6 @@ export class InputValidator { return this.validate> | undefined>( model, 'deleteMany', - undefined, (model) => this.makeDeleteManySchema(model), args, ); @@ -312,7 +305,6 @@ export class InputValidator { return this.validate> | undefined>( model, 'count', - undefined, (model) => this.makeCountSchema(model), args, ); @@ -322,7 +314,6 @@ export class InputValidator { return this.validate>>( model, 'aggregate', - undefined, (model) => this.makeAggregateSchema(model), args, ); @@ -332,49 +323,32 @@ export class InputValidator { return this.validate>>( model, 'groupBy', - undefined, (model) => this.makeGroupBySchema(model), args, ); } private getSchemaCache(cacheKey: string) { - let thisCache = schemaCache.get(this.schema); - if (!thisCache) { - thisCache = new Map(); - schemaCache.set(this.schema, thisCache); - } - return thisCache.get(cacheKey); + return this.schemaCache.get(cacheKey); } private setSchemaCache(cacheKey: string, schema: ZodType) { - let thisCache = schemaCache.get(this.schema); - if (!thisCache) { - thisCache = new Map(); - schemaCache.set(this.schema, thisCache); - } - return thisCache.set(cacheKey, schema); + return this.schemaCache.set(cacheKey, schema); } - private validate( - model: GetModels, - operation: string, - options: Options, - getSchema: GetSchemaFunc, - args: unknown, - ) { + private validate(model: GetModels, operation: string, getSchema: GetSchemaFunc, args: unknown) { const cacheKey = stableStringify({ type: 'model', model, operation, - options, extraValidationsEnabled: this.extraValidationsEnabled, }); let schema = this.getSchemaCache(cacheKey!); if (!schema) { - schema = getSchema(model, options); + schema = getSchema(model); this.setSchemaCache(cacheKey!, schema); } + const { error, data } = schema.safeParse(args); if (error) { throw createInvalidInputError( @@ -388,12 +362,27 @@ export class InputValidator { return data as T; } + private mergePluginArgsSchema(schema: ZodObject, operation: CoreCrudOperations) { + let result = schema; + for (const plugin of this.options.plugins ?? []) { + if (plugin.extQueryArgs) { + const pluginSchema = plugin.extQueryArgs.getValidationSchema(operation); + if (pluginSchema) { + result = result.extend(pluginSchema.shape); + } + } + } + return result.strict(); + } + // #region Find - private makeFindSchema(model: string, options: { unique: boolean; findOne: boolean }) { + private makeFindSchema(model: string, operation: CoreCrudOperations) { const fields: Record = {}; - const where = this.makeWhereSchema(model, options.unique); - if (options.unique) { + const unique = operation === 'findUnique'; + const findOne = operation === 'findUnique' || operation === 'findFirst'; + const where = this.makeWhereSchema(model, unique); + if (unique) { fields['where'] = where; } else { fields['where'] = where.optional(); @@ -403,9 +392,9 @@ export class InputValidator { fields['include'] = this.makeIncludeSchema(model).optional().nullable(); fields['omit'] = this.makeOmitSchema(model).optional().nullable(); - if (!options.unique) { + if (!unique) { fields['skip'] = this.makeSkipSchema().optional(); - if (options.findOne) { + if (findOne) { fields['take'] = z.literal(1).optional(); } else { fields['take'] = this.makeTakeSchema().optional(); @@ -415,22 +404,22 @@ export class InputValidator { fields['distinct'] = this.makeDistinctSchema(model).optional(); } - let result: ZodType = z.strictObject(fields); + const baseSchema = z.strictObject(fields); + let result: ZodType = this.mergePluginArgsSchema(baseSchema, operation); result = this.refineForSelectIncludeMutuallyExclusive(result); result = this.refineForSelectOmitMutuallyExclusive(result); - if (!options.unique) { + if (!unique) { result = result.optional(); } return result; } private makeExistsSchema(model: string) { - return z - .strictObject({ - where: this.makeWhereSchema(model, false).optional(), - }) - .optional(); + const baseSchema = z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + }); + return this.mergePluginArgsSchema(baseSchema, 'exists').optional(); } private makeScalarSchema(type: string, attributes?: readonly AttributeApplication[]) { @@ -1158,27 +1147,29 @@ export class InputValidator { private makeCreateSchema(model: string) { const dataSchema = this.makeCreateDataSchema(model, false); - let schema: ZodType = z.strictObject({ + const baseSchema = z.strictObject({ data: dataSchema, select: this.makeSelectSchema(model).optional().nullable(), include: this.makeIncludeSchema(model).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'create'); schema = this.refineForSelectIncludeMutuallyExclusive(schema); schema = this.refineForSelectOmitMutuallyExclusive(schema); return schema; } private makeCreateManySchema(model: string) { - return this.makeCreateManyDataSchema(model, []).optional(); + return this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(); } private makeCreateManyAndReturnSchema(model: string) { const base = this.makeCreateManyDataSchema(model, []); - const result = base.extend({ + let result: ZodObject = base.extend({ select: this.makeSelectSchema(model).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); + result = this.mergePluginArgsSchema(result, 'createManyAndReturn'); return this.refineForSelectOmitMutuallyExclusive(result).optional(); } @@ -1440,29 +1431,34 @@ export class InputValidator { // #region Update private makeUpdateSchema(model: string) { - let schema: ZodType = z.strictObject({ + const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), data: this.makeUpdateDataSchema(model), select: this.makeSelectSchema(model).optional().nullable(), include: this.makeIncludeSchema(model).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'update'); schema = this.refineForSelectIncludeMutuallyExclusive(schema); schema = this.refineForSelectOmitMutuallyExclusive(schema); return schema; } private makeUpdateManySchema(model: string) { - return z.strictObject({ - where: this.makeWhereSchema(model, false).optional(), - data: this.makeUpdateDataSchema(model, [], true), - limit: z.number().int().nonnegative().optional(), - }); + return this.mergePluginArgsSchema( + z.strictObject({ + where: this.makeWhereSchema(model, false).optional(), + data: this.makeUpdateDataSchema(model, [], true), + limit: z.number().int().nonnegative().optional(), + }), + 'updateMany', + ); } private makeUpdateManyAndReturnSchema(model: string) { - const base = this.makeUpdateManySchema(model); - let schema: ZodType = base.extend({ + // plugin extended args schema is merged in `makeUpdateManySchema` + const baseSchema: ZodObject = this.makeUpdateManySchema(model); + let schema: ZodType = baseSchema.extend({ select: this.makeSelectSchema(model).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); @@ -1471,7 +1467,7 @@ export class InputValidator { } private makeUpsertSchema(model: string) { - let schema: ZodType = z.strictObject({ + const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), create: this.makeCreateDataSchema(model, false), update: this.makeUpdateDataSchema(model), @@ -1479,6 +1475,7 @@ export class InputValidator { include: this.makeIncludeSchema(model).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'upsert'); schema = this.refineForSelectIncludeMutuallyExclusive(schema); schema = this.refineForSelectOmitMutuallyExclusive(schema); return schema; @@ -1595,25 +1592,26 @@ export class InputValidator { // #region Delete private makeDeleteSchema(model: GetModels) { - let schema: ZodType = z.strictObject({ + const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), select: this.makeSelectSchema(model).optional().nullable(), include: this.makeIncludeSchema(model).optional().nullable(), omit: this.makeOmitSchema(model).optional().nullable(), }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'delete'); schema = this.refineForSelectIncludeMutuallyExclusive(schema); schema = this.refineForSelectOmitMutuallyExclusive(schema); return schema; } private makeDeleteManySchema(model: GetModels) { - return z - .object({ + return this.mergePluginArgsSchema( + z.strictObject({ where: this.makeWhereSchema(model, false).optional(), limit: z.number().int().nonnegative().optional(), - }) - - .optional(); + }), + 'deleteMany', + ).optional(); } // #endregion @@ -1621,16 +1619,16 @@ export class InputValidator { // #region Count makeCountSchema(model: GetModels) { - return z - .object({ + return this.mergePluginArgsSchema( + z.strictObject({ where: this.makeWhereSchema(model, false).optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), orderBy: this.orArray(this.makeOrderBySchema(model, true, false), true).optional(), select: this.makeCountAggregateInputSchema(model).optional(), - }) - - .optional(); + }), + 'count', + ).optional(); } private makeCountAggregateInputSchema(model: GetModels) { @@ -1655,8 +1653,8 @@ export class InputValidator { // #region Aggregate makeAggregateSchema(model: GetModels) { - return z - .object({ + return this.mergePluginArgsSchema( + z.strictObject({ where: this.makeWhereSchema(model, false).optional(), skip: this.makeSkipSchema().optional(), take: this.makeTakeSchema().optional(), @@ -1666,9 +1664,9 @@ export class InputValidator { _sum: this.makeSumAvgInputSchema(model).optional(), _min: this.makeMinMaxInputSchema(model).optional(), _max: this.makeMinMaxInputSchema(model).optional(), - }) - - .optional(); + }), + 'aggregate', + ).optional(); } makeSumAvgInputSchema(model: GetModels) { @@ -1711,7 +1709,7 @@ export class InputValidator { ? this.orArray(z.enum(nonRelationFields as [string, ...string[]]), true) : z.never(); - let schema: z.ZodSchema = z.strictObject({ + const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, false).optional(), orderBy: this.orArray(this.makeOrderBySchema(model, false, true), true).optional(), by: bySchema, @@ -1725,6 +1723,8 @@ export class InputValidator { _max: this.makeMinMaxInputSchema(model).optional(), }); + let schema: ZodType = this.mergePluginArgsSchema(baseSchema, 'groupBy'); + // fields used in `having` must be either in the `by` list, or aggregations schema = schema.refine((value: any) => { const bys = typeof value.by === 'string' ? [value.by] : value.by; diff --git a/packages/orm/src/client/index.ts b/packages/orm/src/client/index.ts index e69e41802..bf17a9e6e 100644 --- a/packages/orm/src/client/index.ts +++ b/packages/orm/src/client/index.ts @@ -3,6 +3,13 @@ export * from './contract'; export type * from './crud-types'; export { getCrudDialect } from './crud/dialects'; export { BaseCrudDialect } from './crud/dialects/base-dialect'; +export { + AllCrudOperations, + AllReadOperations, + CoreCrudOperations, + CoreReadOperations, + CoreWriteOperations, +} from './crud/operations/base'; export { InputValidator } from './crud/validator'; export { ORMError, ORMErrorReason, RejectedByPolicyReason } from './errors'; export * from './options'; diff --git a/packages/orm/src/client/options.ts b/packages/orm/src/client/options.ts index c5e6c94d3..d1fa23ed7 100644 --- a/packages/orm/src/client/options.ts +++ b/packages/orm/src/client/options.ts @@ -59,7 +59,7 @@ export type ClientOptions = { /** * Plugins. */ - plugins?: RuntimePlugin[]; + plugins?: RuntimePlugin[]; /** * Logging configuration. @@ -85,7 +85,7 @@ export type ClientOptions = { /** * Options for omitting fields in ORM query results. */ - omit?: OmitOptions; + omit?: OmitConfig; /** * Whether to allow overriding omit settings at query time. Defaults to `true`. When set to @@ -111,9 +111,9 @@ export type ClientOptions = { : {}); /** - * Options for omitting fields in ORM query results. + * Config for omitting fields in ORM query results. */ -export type OmitOptions = { +export type OmitConfig = { [Model in GetModels]?: { [Field in GetModelFields as Field extends ScalarFields ? Field : never]?: boolean; }; diff --git a/packages/orm/src/client/plugin.ts b/packages/orm/src/client/plugin.ts index cd092f4ac..ee024f1e8 100644 --- a/packages/orm/src/client/plugin.ts +++ b/packages/orm/src/client/plugin.ts @@ -1,14 +1,19 @@ import type { OperationNode, QueryId, QueryResult, RootOperationNode, UnknownRow } from 'kysely'; -import type { ClientContract } from '.'; +import type { ZodObject } from 'zod'; +import type { ClientContract, ZModelFunction } from '.'; import type { GetModels, SchemaDef } from '../schema'; import type { MaybePromise } from '../utils/type-utils'; -import type { AllCrudOperation } from './crud/operations/base'; -import type { ZModelFunction } from './options'; +import type { AllCrudOperations, CoreCrudOperations } from './crud/operations/base'; + +/** + * Base shape of plugin-extended query args. + */ +export type ExtQueryArgsBase = { [K in CoreCrudOperations | 'all']?: object }; /** * ZenStack runtime plugin. */ -export interface RuntimePlugin { +export interface RuntimePlugin { /** * Plugin ID. */ @@ -50,17 +55,26 @@ export interface RuntimePlugin { * Intercepts a Kysely query. */ onKyselyQuery?: OnKyselyQueryCallback; -} + /** + * Extended query args configuration. + */ + extQueryArgs?: { + /** + * Callback for getting a Zod schema to validate the extended query args for the given operation. + */ + getValidationSchema: (operation: CoreCrudOperations) => ZodObject | undefined; + }; +} /** * Defines a ZenStack runtime plugin. */ -export function definePlugin(plugin: RuntimePlugin) { +export function definePlugin( + plugin: RuntimePlugin, +): RuntimePlugin { return plugin; } -export { type CoreCrudOperation as CrudOperation } from './crud/operations/base'; - // #region OnProcedure hooks type OnProcedureCallback = (ctx: OnProcedureHookContext) => Promise; @@ -110,12 +124,12 @@ type OnQueryHookContext = { /** * The operation that is being performed. */ - operation: AllCrudOperation; + operation: AllCrudOperations; /** * The query arguments. */ - args: unknown; + args: Record | undefined; /** * The function to proceed with the original query. @@ -123,7 +137,7 @@ type OnQueryHookContext = { * * @param args The query arguments. */ - proceed: (args: unknown) => Promise; + proceed: (args: Record | undefined) => Promise; /** * The ZenStack client that is performing the operation. diff --git a/packages/orm/src/utils/type-utils.ts b/packages/orm/src/utils/type-utils.ts index f1ad3d35c..85152e328 100644 --- a/packages/orm/src/utils/type-utils.ts +++ b/packages/orm/src/utils/type-utils.ts @@ -88,3 +88,5 @@ export type OrUndefinedIf = Condition extends true export type UnwrapTuplePromises = { [K in keyof T]: Awaited; }; + +export type Exact = T extends Shape ? (Exclude extends never ? T : never) : never; diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index 0ea84a97a..18320c69d 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -6,6 +6,7 @@ import { type BaseCrudDialect, type ClientContract, type CRUD_EXT, + type ZModelFunction, } from '@zenstackhq/orm'; import type { BinaryExpression, @@ -560,7 +561,7 @@ export class ExpressionTransformer { // check plugins for (const plugin of this.clientOptions.plugins ?? []) { if (plugin.functions?.[functionName]) { - func = plugin.functions[functionName]; + func = plugin.functions[functionName] as unknown as ZModelFunction; break; } } diff --git a/packages/plugins/policy/src/plugin.ts b/packages/plugins/policy/src/plugin.ts index b45f30bf9..e27b0cf54 100644 --- a/packages/plugins/policy/src/plugin.ts +++ b/packages/plugins/policy/src/plugin.ts @@ -3,9 +3,9 @@ import type { SchemaDef } from '@zenstackhq/orm/schema'; import { check } from './functions'; import { PolicyHandler } from './policy-handler'; -export class PolicyPlugin implements RuntimePlugin { +export class PolicyPlugin implements RuntimePlugin { get id() { - return 'policy'; + return 'policy' as const; } get name() { @@ -22,8 +22,8 @@ export class PolicyPlugin implements RuntimePlugin) { - const handler = new PolicyHandler(client); + onKyselyQuery({ query, client, proceed }: OnKyselyQueryArgs) { + const handler = new PolicyHandler(client); return handler.handle(query, proceed); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b686eb02..a60befaa5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -69,6 +69,9 @@ catalogs: react-dom: specifier: 19.2.0 version: 19.2.0 + sql.js: + specifier: ^1.13.0 + version: 1.13.0 svelte: specifier: 5.45.6 version: 5.45.6 @@ -84,6 +87,9 @@ catalogs: vue: specifier: 3.5.22 version: 3.5.22 + zod: + specifier: ^4.0.0 + version: 4.1.12 zod-validation-error: specifier: ^4.0.1 version: 4.0.1 @@ -909,6 +915,9 @@ importers: kysely: specifier: 'catalog:' version: 0.28.8 + zod: + specifier: 'catalog:' + version: 4.1.12 devDependencies: '@types/better-sqlite3': specifier: 'catalog:' @@ -1034,6 +1043,9 @@ importers: uuid: specifier: ^11.0.5 version: 11.0.5 + zod: + specifier: 'catalog:' + version: 4.1.12 devDependencies: '@zenstackhq/typescript-config': specifier: workspace:* @@ -12438,7 +12450,7 @@ snapshots: eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.29.0(jiti@2.6.1)) @@ -12471,7 +12483,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -12486,7 +12498,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 diff --git a/samples/orm/main.ts b/samples/orm/main.ts index 4b7f82ef1..3b9dd1294 100644 --- a/samples/orm/main.ts +++ b/samples/orm/main.ts @@ -21,18 +21,13 @@ async function main() { client.user.create({ data: { ...args }, }), - listPublicPosts: ({ client }) => - client.post.findMany({ - where: { - published: true, - }, - }), + listPublicPosts: ({}) => [], }, }).$use({ id: 'cost-logger', onQuery: async ({ model, operation, args, proceed }) => { const start = Date.now(); - const result = await proceed(args); + const result = await proceed(args as any); console.log(`[cost] ${model} ${operation} took ${Date.now() - start}ms`); return result; }, @@ -43,6 +38,8 @@ async function main() { await db.profile.deleteMany(); await db.user.deleteMany(); + db.user.findMany({ where: { id: '1' } }); + // create users and some posts const user1 = await db.user.create({ data: { diff --git a/samples/orm/package.json b/samples/orm/package.json index fd43fa6c4..b052aa9ea 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -18,7 +18,8 @@ "@zenstackhq/orm": "workspace:*", "@zenstackhq/plugin-policy": "workspace:*", "better-sqlite3": "catalog:", - "kysely": "catalog:" + "kysely": "catalog:", + "zod": "catalog:" }, "devDependencies": { "@types/better-sqlite3": "catalog:", diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 0a881288e..2eddd2146 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -907,7 +907,7 @@ describe('Client find tests ', () => { // @ts-expect-error include: { author: { where: { email: user.email } } }, }), - ).rejects.toThrow(`Invalid find args`); + ).rejects.toThrow(`Invalid findFirst args`); // sorting let u = await client.user.findUniqueOrThrow({ diff --git a/tests/e2e/orm/plugin-infra/ext-query-args.test.ts b/tests/e2e/orm/plugin-infra/ext-query-args.test.ts new file mode 100644 index 000000000..7a5630d1a --- /dev/null +++ b/tests/e2e/orm/plugin-infra/ext-query-args.test.ts @@ -0,0 +1,273 @@ +import { CoreReadOperations, CoreWriteOperations, definePlugin, type ClientContract } from '@zenstackhq/orm'; +import { createTestClient } from '@zenstackhq/testtools'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import z from 'zod'; +import { schema } from './ext-query-args/schema'; + +describe('Plugin extended query args', () => { + let db: ClientContract; + + const cacheSchema = z.object({ + cache: z + .strictObject({ + ttl: z.number().min(0).optional(), + }) + .optional(), + }); + + const cacheBustSchema = z.object({ + cache: z.strictObject({ + bust: z.boolean().optional(), + }), + }); + + type CacheOptions = z.infer; + type CacheBustOptions = z.infer; + + beforeEach(async () => { + db = await createTestClient(schema); + await db.user.deleteMany(); + }); + + afterEach(async () => { + await db?.$disconnect(); + }); + + it('should allow extending all operations', async () => { + let gotTTL: number | undefined = undefined; + + const extDb = db.$use( + definePlugin< + typeof schema, + { + all: CacheOptions; + } + >({ + id: 'cache', + extQueryArgs: { + getValidationSchema: () => cacheSchema, + }, + + onQuery: async ({ args, proceed }) => { + if (args && 'cache' in args) { + gotTTL = (args as CacheOptions).cache?.ttl; + } + return proceed(args); + }, + }), + ); + + // cache is optional + const alice = await extDb.user.create({ data: { name: 'Alice' } }); + + // ttl is optional + const bob = await extDb.user.create({ data: { name: 'Bob' }, cache: {} }); + + gotTTL = undefined; + await expect(extDb.user.findMany({ cache: { ttl: 5000 } })).toResolveWithLength(2); + expect(gotTTL).toBe(5000); + + await expect(extDb.user.findMany({ cache: { ttl: -1 } })).rejects.toThrow('Too small'); + + // reject unrecognized keys in extended args + // @ts-expect-error + await expect(extDb.user.findMany({ cache: { x: 1 } })).rejects.toThrow('Unrecognized key'); + + // still reject invalid original args + // @ts-expect-error + await expect(extDb.user.findMany({ where: { foo: 'bar' } })).rejects.toThrow('Unrecognized key'); + // @ts-expect-error + await expect(extDb.user.findMany({ foo: 'bar' })).rejects.toThrow('Unrecognized key'); + // @ts-expect-error + await expect(extDb.user.findMany({ where: { id: 'abc' } })).rejects.toThrow('expected number'); + + // validate all other operations + + const cacheOption = { cache: { ttl: 1000 } } as const; + + // read operations + await expect(extDb.user.findUnique({ where: { id: 1 }, ...cacheOption })).toResolveTruthy(); + await expect(extDb.user.findUniqueOrThrow({ where: { id: 1 }, ...cacheOption })).toResolveTruthy(); + await expect(extDb.user.findFirst(cacheOption)).toResolveTruthy(); + await expect(extDb.user.findFirstOrThrow(cacheOption)).toResolveTruthy(); + await expect(extDb.user.count(cacheOption)).resolves.toBe(2); + await expect(extDb.user.exists(cacheOption)).resolves.toBe(true); + await expect( + extDb.user.aggregate({ + _count: true, + ...cacheOption, + }), + ).resolves.toHaveProperty('_count'); + await expect( + extDb.user.groupBy({ + by: ['id'], + _count: { + id: true, + }, + ...cacheOption, + }), + ).resolves.toHaveLength(2); + + // create operations + await expect(extDb.user.createMany({ data: [{ name: 'Charlie' }], ...cacheOption })).resolves.toHaveProperty( + 'count', + ); + await expect(extDb.user.createManyAndReturn({ data: [{ name: 'David' }], ...cacheOption })).toResolveWithLength( + 1, + ); + + // update operations + await expect( + extDb.user.update({ where: { id: alice.id }, data: { name: 'Alice Updated' }, ...cacheOption }), + ).toResolveTruthy(); + await expect( + extDb.user.updateMany({ where: { name: 'Bob' }, data: { name: 'Bob Updated' }, ...cacheOption }), + ).resolves.toHaveProperty('count'); + await expect( + extDb.user.updateManyAndReturn({ + where: { name: 'Charlie' }, + data: { name: 'Charlie Updated' }, + ...cacheOption, + }), + ).toResolveTruthy(); + await expect( + extDb.user.upsert({ + where: { id: 999 }, + create: { name: 'Eve' }, + update: { name: 'Eve Updated' }, + ...cacheOption, + }), + ).resolves.toMatchObject({ name: 'Eve' }); + + // delete operations + await expect(extDb.user.delete({ where: { id: bob.id }, ...cacheOption })).toResolveTruthy(); + await expect(extDb.user.deleteMany({ where: { name: 'David' }, ...cacheOption })).resolves.toHaveProperty( + 'count', + ); + + // validate transaction + await extDb.$transaction(async (tx) => { + await expect(tx.user.findMany(cacheOption)).toResolveTruthy(); + }); + + // validate $use + await expect(extDb.$use({ id: 'foo' }).user.findMany(cacheOption)).toResolveTruthy(); + + // validate $setOptions + await expect( + extDb.$setOptions({ ...extDb.$options, validateInput: false }).user.findMany(cacheOption), + ).toResolveTruthy(); + + // validate $setAuth + await expect(extDb.$setAuth({ id: 1 }).user.findMany(cacheOption)).toResolveTruthy(); + }); + + it('should allow extending specific operations', async () => { + const extDb = db.$use( + definePlugin< + typeof schema, + { + [Op in CoreReadOperations]: CacheOptions; + } + >({ + id: 'cache', + extQueryArgs: { + getValidationSchema: (operation) => { + if (!(CoreReadOperations as readonly string[]).includes(operation)) { + return undefined; + } + return cacheSchema; + }, + }, + }), + ); + + // "create" is not extended + // @ts-expect-error + await expect(extDb.user.create({ data: { name: 'Bob' }, cache: {} })).rejects.toThrow('Unrecognized key'); + + await extDb.user.create({ data: { name: 'Alice' } }); + + await expect(extDb.user.findMany({ cache: { ttl: 100 } })).toResolveWithLength(1); + await expect(extDb.user.count({ where: { name: 'Alice' }, cache: { ttl: 200 } })).resolves.toBe(1); + }); + + it('should allow different extensions for different operations', async () => { + let gotTTL: number | undefined = undefined; + let gotBust: boolean | undefined = undefined; + + const extDb = db.$use( + definePlugin< + typeof schema, + { + [Op in CoreReadOperations]: CacheOptions; + } & { + [Op in CoreWriteOperations]: CacheBustOptions; + } + >({ + id: 'cache', + extQueryArgs: { + getValidationSchema: (operation) => { + if ((CoreReadOperations as readonly string[]).includes(operation)) { + return cacheSchema; + } else if ((CoreWriteOperations as readonly string[]).includes(operation)) { + return cacheBustSchema; + } + return undefined; + }, + }, + + onQuery: async ({ args, proceed }) => { + if (args && 'cache' in args) { + gotTTL = (args as CacheOptions).cache?.ttl; + gotBust = (args as CacheBustOptions).cache?.bust; + } + return proceed(args); + }, + }), + ); + + gotBust = undefined; + await extDb.user.create({ data: { name: 'Alice' }, cache: { bust: true } }); + expect(gotBust).toBe(true); + + // ttl extension is not applied to "create" + // @ts-expect-error + await expect(extDb.user.create({ data: { name: 'Bob' }, cache: { ttl: 100 } })).rejects.toThrow( + 'Unrecognized key', + ); + + gotTTL = undefined; + await expect(extDb.user.findMany({ cache: { ttl: 5000 } })).toResolveWithLength(1); + expect(gotTTL).toBe(5000); + + // bust extension is not applied to "findMany" + // @ts-expect-error + await expect(extDb.user.findMany({ cache: { bust: true } })).rejects.toThrow('Unrecognized key'); + }); + + it('should isolate validation schemas between clients', async () => { + const extDb = db.$use( + definePlugin< + typeof schema, + { + all: CacheOptions; + } + >({ + id: 'cache', + extQueryArgs: { + getValidationSchema: () => cacheSchema, + }, + }), + ); + + // @ts-expect-error + await expect(db.user.findMany({ cache: { ttl: 1000 } })).rejects.toThrow('Unrecognized key'); + await expect(extDb.user.findMany({ cache: { ttl: 1000 } })).toResolveWithLength(0); + + // do it again to make sure cache is not shared + // @ts-expect-error + await expect(db.user.findMany({ cache: { ttl: 2000 } })).rejects.toThrow('Unrecognized key'); + await expect(extDb.user.findMany({ cache: { ttl: 2000 } })).toResolveWithLength(0); + }); +}); diff --git a/tests/e2e/orm/plugin-infra/ext-query-args/input.ts b/tests/e2e/orm/plugin-infra/ext-query-args/input.ts new file mode 100644 index 000000000..22bdbfa73 --- /dev/null +++ b/tests/e2e/orm/plugin-infra/ext-query-args/input.ts @@ -0,0 +1,31 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// 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, ExistsArgs as $ExistsArgs, 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, QueryOptions as $QueryOptions } from "@zenstackhq/orm"; +import type { SimplifiedPlainResult as $Result, SelectIncludeOmit as $SelectIncludeOmit } from "@zenstackhq/orm"; +export type UserFindManyArgs = $FindManyArgs<$Schema, "User">; +export type UserFindUniqueArgs = $FindUniqueArgs<$Schema, "User">; +export type UserFindFirstArgs = $FindFirstArgs<$Schema, "User">; +export type UserExistsArgs = $ExistsArgs<$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, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "User", Args, Options>; diff --git a/tests/e2e/orm/plugin-infra/ext-query-args/models.ts b/tests/e2e/orm/plugin-infra/ext-query-args/models.ts new file mode 100644 index 000000000..7a605bdbc --- /dev/null +++ b/tests/e2e/orm/plugin-infra/ext-query-args/models.ts @@ -0,0 +1,10 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// 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/orm"; +export type User = $ModelResult<$Schema, "User">; diff --git a/tests/e2e/orm/plugin-infra/ext-query-args/schema.ts b/tests/e2e/orm/plugin-infra/ext-query-args/schema.ts new file mode 100644 index 000000000..a8f0ffb86 --- /dev/null +++ b/tests/e2e/orm/plugin-infra/ext-query-args/schema.ts @@ -0,0 +1,38 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// 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 "@zenstackhq/orm/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "sqlite" + } as const; + 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") + }, + name: { + name: "name", + type: "String" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + } + } as const; + authType = "User" as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/tests/e2e/orm/plugin-infra/ext-query-args/schema.zmodel b/tests/e2e/orm/plugin-infra/ext-query-args/schema.zmodel new file mode 100644 index 000000000..2e4b0dc03 --- /dev/null +++ b/tests/e2e/orm/plugin-infra/ext-query-args/schema.zmodel @@ -0,0 +1,8 @@ +datasource db { + provider = "sqlite" +} + +model User { + id Int @id @default(autoincrement()) + name String +} diff --git a/tests/e2e/orm/policy/basic-schema-read.test.ts b/tests/e2e/orm/policy/basic-schema-read.test.ts index 7464a38ce..d1bba66d6 100644 --- a/tests/e2e/orm/policy/basic-schema-read.test.ts +++ b/tests/e2e/orm/policy/basic-schema-read.test.ts @@ -23,7 +23,7 @@ describe('Read policy tests', () => { }); // anonymous auth context by default - const anonClient = client.$use(new PolicyPlugin()); + const anonClient = client.$use(new PolicyPlugin()); await expect(anonClient.user.findFirst()).toResolveNull(); const authClient = anonClient.$setAuth({ diff --git a/tests/e2e/package.json b/tests/e2e/package.json index c24650ba1..5022cf2e8 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -25,7 +25,8 @@ "kysely": "catalog:", "ulid": "^3.0.0", "uuid": "^11.0.5", - "cuid": "^3.0.0" + "cuid": "^3.0.0", + "zod": "catalog:" }, "devDependencies": { "@zenstackhq/cli": "workspace:*", From 33f648510f21dbbdbbf7d019a7487a95708796ec Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 16 Jan 2026 21:18:05 +0800 Subject: [PATCH 02/25] chore: add publish-canary command (#602) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0954f3520..639198d07 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "bump-patch": "gh workflow run .github/workflows/bump-version.yml --ref dev -f version_type=patch", "bump-minor": "gh workflow run .github/workflows/bump-version.yml --ref dev -f version_type=minor", "publish-all": "pnpm --filter \"./packages/**\" -r publish --access public", + "publish-canary": "pnpm --filter \"./packages/**\" -r publish --access public --tag canary --no-git-checks", "publish-preview": "pnpm --filter \"./packages/**\" -r publish --force --registry https://preview.registry.zenstack.dev/", "unpublish-preview": "pnpm --filter \"./packages/**\" -r --shell-mode exec -- npm unpublish -f --registry https://preview.registry.zenstack.dev/ \"\\$PNPM_PACKAGE_NAME\"" }, From 27e8910830d35ef9d0126787e29e78119dc50dda Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Fri, 16 Jan 2026 05:30:31 -0800 Subject: [PATCH 03/25] feat: `between` operator (#569) * feat: `between` operator * chore: add tests * Document inclusivity. * Add reversed order test. * chore: add REST server test * Fix test typo. * Verify correct object returned. * Additional test cases. * Make AI suggested changes. * Remove filter. * Fix styling. * Add more REST tests. * Add more int tests for REST. --- packages/orm/src/client/crud-types.ts | 5 + .../src/client/crud/dialects/base-dialect.ts | 6 + .../orm/src/client/crud/validator/index.ts | 1 + packages/server/src/api/rest/index.ts | 10 ++ packages/server/test/api/rest.test.ts | 88 +++++++++++- tests/e2e/orm/client-api/filter.test.ts | 134 +++++++++++++++++- 6 files changed, 241 insertions(+), 3 deletions(-) diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 5626931e7..93f770c60 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -450,6 +450,11 @@ type CommonPrimitiveFilter< */ gte?: DataType; + /** + * Checks if the value is between the specified values (inclusive). + */ + between?: [start: DataType, end: DataType]; + /** * Builds a negated filter. */ diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 26da88769..f4bf56ffb 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -765,6 +765,12 @@ export abstract class BaseCrudDialect { .with('lte', () => this.eb(lhs, '<=', rhs)) .with('gt', () => this.eb(lhs, '>', rhs)) .with('gte', () => this.eb(lhs, '>=', rhs)) + .with('between', () => { + invariant(Array.isArray(rhs), 'right hand side must be an array'); + invariant(rhs.length === 2, 'right hand side must have a length of 2'); + const [start, end] = rhs; + return this.eb.and([this.eb(lhs, '>=', start), this.eb(lhs, '<=', end)]); + }) .with('not', () => this.eb.not(recurse(value))) // aggregations .with(P.union(...AGGREGATE_OPERATORS), (op) => { diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 3556c6046..a6256da77 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -890,6 +890,7 @@ export class InputValidator { lte: baseSchema.optional(), gt: baseSchema.optional(), gte: baseSchema.optional(), + between: baseSchema.array().length(2).optional(), not: makeThis().optional(), ...(withAggregations?.includes('_count') ? { _count: this.makeNumberFilterSchema(z.number().int(), false, false).optional() } diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index db342ba6e..980af6ba7 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -106,6 +106,7 @@ const FilterOperations = [ 'lte', 'gt', 'gte', + 'between', 'contains', 'icontains', 'search', @@ -2058,6 +2059,15 @@ export class RestApiHandler implements Api } } } else { + if (op === 'between') { + const parts = value + .split(',') + .map((v) => this.coerce(fieldDef, v)); + if (parts.length !== 2) { + throw new InvalidValueError(`"between" expects exactly 2 comma-separated values`); + } + return { between: [parts[0]!, parts[1]!] }; + } const coerced = this.coerce(fieldDef, value); switch (op) { case 'icontains': diff --git a/packages/server/test/api/rest.test.ts b/packages/server/test/api/rest.test.ts index 124967edd..5cdd6f3a9 100644 --- a/packages/server/test/api/rest.test.ts +++ b/packages/server/test/api/rest.test.ts @@ -420,6 +420,8 @@ describe('REST server tests', () => { }); it('toplevel filtering', async () => { + const now = new Date(); + const past = new Date(now.getTime() - 1); await client.user.create({ data: { myId: 'user1', @@ -436,7 +438,7 @@ describe('REST server tests', () => { myId: 'user2', email: 'user2@abc.com', posts: { - create: { id: 2, title: 'Post2', viewCount: 1, published: true }, + create: { id: 2, title: 'Post2', viewCount: 1, published: true, publishedAt: now }, }, }, }); @@ -523,6 +525,38 @@ describe('REST server tests', () => { }); expect(r.body.data).toHaveLength(0); + r = await handler({ + method: 'get', + path: '/user', + query: { ['filter[email$between]']: ',user1@abc.com' }, + client, + }); + expect(r.body.data).toHaveLength(1); + + r = await handler({ + method: 'get', + path: '/user', + query: { ['filter[email$between]']: 'user1@abc.com,' }, + client, + }); + expect(r.body.data).toHaveLength(0); + + r = await handler({ + method: 'get', + path: '/user', + query: { ['filter[email$between]']: ',user2@abc.com' }, + client, + }); + expect(r.body.data).toHaveLength(2); + + r = await handler({ + method: 'get', + path: '/user', + query: { ['filter[email$between]']: 'user1@abc.com,user2@abc.com' }, + client, + }); + expect(r.body.data).toHaveLength(2); + // Int filter r = await handler({ method: 'get', @@ -568,6 +602,58 @@ describe('REST server tests', () => { expect(r.body.data).toHaveLength(1); expect(r.body.data[0]).toMatchObject({ id: 1 }); + r = await handler({ + method: 'get', + path: '/post', + query: { ['filter[viewCount$between]']: '1,2' }, + client, + }); + expect(r.body.data).toHaveLength(1); + expect(r.body.data[0]).toMatchObject({ id: 2 }); + + r = await handler({ + method: 'get', + path: '/post', + query: { ['filter[viewCount$between]']: '2,1' }, + client, + }); + expect(r.body.data).toHaveLength(0); + + r = await handler({ + method: 'get', + path: '/post', + query: { ['filter[viewCount$between]']: '0,2' }, + client, + }); + expect(r.body.data).toHaveLength(2); + + // DateTime filter + r = await handler({ + method: 'get', + path: '/post', + query: { ['filter[publishedAt$between]']: `${now.toISOString()},${now.toISOString()}` }, + client, + }); + expect(r.body.data).toHaveLength(1); + expect(r.body.data[0]).toMatchObject({ id: 2 }); + + r = await handler({ + method: 'get', + path: '/post', + query: { ['filter[publishedAt$between]']: `${past.toISOString()},${now.toISOString()}` }, + client, + }); + expect(r.body.data).toHaveLength(1); + expect(r.body.data[0]).toMatchObject({ id: 2 }); + + r = await handler({ + method: 'get', + path: '/post', + query: { ['filter[publishedAt$between]']: `${now.toISOString()},${past.toISOString()}` }, + client, + }); + expect(r.body.data).toHaveLength(0); + // Boolean filter r = await handler({ method: 'get', diff --git a/tests/e2e/orm/client-api/filter.test.ts b/tests/e2e/orm/client-api/filter.test.ts index f7774291b..d4594ab75 100644 --- a/tests/e2e/orm/client-api/filter.test.ts +++ b/tests/e2e/orm/client-api/filter.test.ts @@ -315,6 +315,58 @@ describe('Client filter tests ', () => { }), ).toResolveWithLength(2); + // between + await expect( + client.user.findMany({ + where: { email: { between: ['a@test.com', 'a@test.com'] } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { email: { between: ['a@test.com', 'b@test.com'] } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { email: { between: ['z@test.com', 'a@test.com'] } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { email: { between: ['u1@test.com', 'u1@test.com'] } }, + }), + ).toResolveWithLength(1); + await expect( + client.user.findMany({ + where: { email: { between: ['u2@test.com', 'u2@test.com'] } }, + }), + ).toResolveWithLength(1); + await expect( + client.user.findMany({ + where: { email: { between: ['u1@test.com', 'u2@test.com'] } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { email: { between: ['u2@test.com', 'u3%@test.com'] } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { email: { between: ['a@test.com', 'u3%@test.com'] } }, + }), + ).toResolveWithLength(3); + await expect( + client.user.findMany({ + where: { email: { between: ['a@test.com', 'z@test.com'] } }, + }), + ).toResolveWithLength(3); + await expect( + client.user.findMany({ + where: { email: { between: ['u1@test.com', 'u3%@test.com'] } }, + }), + ).toResolveWithLength(3); + // contains await expect( client.user.findFirst({ @@ -409,6 +461,14 @@ describe('Client filter tests ', () => { await expect(client.profile.findMany({ where: { age: { gte: 20 } } })).toResolveWithLength(1); await expect(client.profile.findMany({ where: { age: { gte: 21 } } })).toResolveWithLength(0); + // between + await expect(client.profile.findMany({ where: { age: { between: [20, 20] } } })).toResolveWithLength(1); + await expect(client.profile.findMany({ where: { age: { between: [19, 20] } } })).toResolveWithLength(1); + await expect(client.profile.findMany({ where: { age: { between: [20, 21] } } })).toResolveWithLength(1); + await expect(client.profile.findMany({ where: { age: { between: [19, 19] } } })).toResolveWithLength(0); + await expect(client.profile.findMany({ where: { age: { between: [21, 21] } } })).toResolveWithLength(0); + await expect(client.profile.findMany({ where: { age: { between: [21, 20] } } })).toResolveWithLength(0); + // not await expect( client.profile.findFirst({ @@ -460,11 +520,14 @@ describe('Client filter tests ', () => { }); it('supports date filters', async () => { + const now = new Date(); + const past = new Date(now.getTime() - 1); + const future = new Date(now.getTime() + 2); const user1 = await createUser('u1@test.com', { - createdAt: new Date(), + createdAt: now, }); const user2 = await createUser('u2@test.com', { - createdAt: new Date(Date.now() + 1000), + createdAt: new Date(now.getTime() + 1), }); // equals @@ -577,6 +640,73 @@ describe('Client filter tests ', () => { }), ).resolves.toMatchObject(user2); + // between + await expect( + client.user.findMany({ + where: { createdAt: { between: [user1.createdAt, user1.createdAt] } }, + }), + ).toResolveWithLength(1); + await expect( + client.user.findMany({ + where: { createdAt: { between: [user1.createdAt, user2.createdAt] } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { createdAt: { between: [user2.createdAt, user2.createdAt] } }, + }), + ).toResolveWithLength(1); + await expect( + client.user.findMany({ + where: { createdAt: { between: [user2.createdAt, user1.createdAt] } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { createdAt: { between: [past, past] } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { createdAt: { between: [past, user1.createdAt] } }, + }), + ).toResolveWithLength(1); + await expect( + client.user.findMany({ + where: { createdAt: { between: [past.toISOString(), user1.createdAt] } }, + }), + ).toResolveWithLength(1); + await expect( + client.user.findMany({ + where: { createdAt: { between: [past, user2.createdAt] } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { createdAt: { between: [past, future] } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { createdAt: { between: [past.toISOString(), future.toISOString()] } }, + }), + ).toResolveWithLength(2); + await expect( + client.user.findMany({ + where: { createdAt: { between: [future, past] } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { createdAt: { between: [future, user1.createdAt] } }, + }), + ).toResolveWithLength(0); + await expect( + client.user.findMany({ + where: { createdAt: { between: [future, future] } }, + }), + ).toResolveWithLength(0); + // not await expect( client.user.findFirst({ From 2c1ffaca593f24f1ab75c52ac6bb38c40d742ed2 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 18 Jan 2026 13:04:03 +0800 Subject: [PATCH 04/25] feat(orm): implement client API extensions, refactor query args extension (#603) * feat(orm): implement client API extensions, refactor query args extension * address PR comments * Fix upsert validation to merge $create and $update schemas (#604) * Initial plan * fix: merge $create and $update schemas for upsert validation - Handle upsert operation specially to match TypeScript type behavior - When both $create and $update schemas exist, merge them for upsert - Add test case to verify the fix works correctly Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * fix: improve comment accuracy about Zod merge behavior Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * minor fixes --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- packages/orm/src/client/client-impl.ts | 20 +- packages/orm/src/client/contract.ts | 64 ++-- .../orm/src/client/crud/operations/base.ts | 30 ++ .../orm/src/client/crud/validator/index.ts | 84 +++++- packages/orm/src/client/index.ts | 3 + packages/orm/src/client/options.ts | 4 +- packages/orm/src/client/plugin.ts | 43 ++- packages/plugins/policy/src/plugin.ts | 2 +- .../orm/plugin-infra/client-members.test.ts | 239 +++++++++++++++ .../orm/plugin-infra/ext-query-args.test.ts | 275 +++++++++++------- .../orm/plugin-infra/on-query-hooks.test.ts | 2 +- 11 files changed, 618 insertions(+), 148 deletions(-) create mode 100644 tests/e2e/orm/plugin-infra/client-members.test.ts diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index 4af8b0310..719f90f3d 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -37,7 +37,7 @@ import { ZenStackQueryExecutor } from './executor/zenstack-query-executor'; import * as BuiltinFunctions from './functions'; import { SchemaDbPusher } from './helpers/schema-db-pusher'; import type { ClientOptions, ProceduresOptions } from './options'; -import type { RuntimePlugin } from './plugin'; +import type { AnyPlugin } from './plugin'; import { createZenStackPromise, type ZenStackPromise } from './promise'; import { ResultProcessor } from './result-processor'; @@ -293,8 +293,8 @@ export class ClientImpl { await new SchemaDbPusher(this.schema, this.kysely).push(); } - $use(plugin: RuntimePlugin) { - const newPlugins: RuntimePlugin[] = [...(this.$options.plugins ?? []), plugin]; + $use(plugin: AnyPlugin) { + const newPlugins: AnyPlugin[] = [...(this.$options.plugins ?? []), plugin]; const newOptions: ClientOptions = { ...this.options, plugins: newPlugins, @@ -308,7 +308,7 @@ export class ClientImpl { $unuse(pluginId: string) { // tsc perf - const newPlugins: RuntimePlugin[] = []; + const newPlugins: AnyPlugin[] = []; for (const plugin of this.options.plugins ?? []) { if (plugin.id !== pluginId) { newPlugins.push(plugin); @@ -329,7 +329,7 @@ export class ClientImpl { // tsc perf const newOptions: ClientOptions = { ...this.options, - plugins: [] as RuntimePlugin[], + plugins: [] as AnyPlugin[], }; const newClient = new ClientImpl(this.schema, newOptions, this); // create a new validator to have a fresh schema cache, because plugins may @@ -408,6 +408,16 @@ function createClientProxy(client: ClientImpl): ClientImpl { return new Proxy(client, { get: (target, prop, receiver) => { if (typeof prop === 'string' && prop.startsWith('$')) { + // Check for plugin-provided members (search in reverse order so later plugins win) + const plugins = target.$options.plugins ?? []; + for (let i = plugins.length - 1; i >= 0; i--) { + const plugin = plugins[i]; + const clientMembers = plugin?.client as Record | undefined; + if (clientMembers && prop in clientMembers) { + return clientMembers[prop]; + } + } + // Fall through to built-in $ methods return Reflect.get(target, prop, receiver); } diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index 0006cd2ab..945f36457 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -40,9 +40,15 @@ import type { UpdateManyArgs, UpsertArgs, } from './crud-types'; -import type { CoreCrudOperations } from './crud/operations/base'; +import type { + CoreCreateOperations, + CoreCrudOperations, + CoreDeleteOperations, + CoreReadOperations, + CoreUpdateOperations, +} from './crud/operations/base'; import type { ClientOptions, QueryOptions, ToQueryOptions } from './options'; -import type { ExtQueryArgsBase, RuntimePlugin } from './plugin'; +import type { ExtClientMembersBase, ExtQueryArgsBase, RuntimePlugin } from './plugin'; import type { ZenStackPromise } from './promise'; import type { ToKysely } from './query-builder'; @@ -51,11 +57,26 @@ type TransactionUnsupportedMethods = (typeof TRANSACTION_UNSUPPORTED_METHODS)[nu /** * Extracts extended query args for a specific operation. */ -type ExtractExtQueryArgs = Operation extends keyof ExtQueryArgs - ? NonNullable - : 'all' extends keyof ExtQueryArgs - ? NonNullable - : {}; +type ExtractExtQueryArgs = (Operation extends keyof ExtQueryArgs + ? ExtQueryArgs[Operation] + : {}) & + ('$create' extends keyof ExtQueryArgs + ? Operation extends CoreCreateOperations + ? ExtQueryArgs['$create'] + : {} + : {}) & + ('$read' extends keyof ExtQueryArgs ? (Operation extends CoreReadOperations ? ExtQueryArgs['$read'] : {}) : {}) & + ('$update' extends keyof ExtQueryArgs + ? Operation extends CoreUpdateOperations + ? ExtQueryArgs['$update'] + : {} + : {}) & + ('$delete' extends keyof ExtQueryArgs + ? Operation extends CoreDeleteOperations + ? ExtQueryArgs['$delete'] + : {} + : {}) & + ('$all' extends keyof ExtQueryArgs ? ExtQueryArgs['$all'] : {}); /** * Transaction isolation levels. @@ -75,6 +96,7 @@ export type ClientContract< Schema extends SchemaDef, Options extends ClientOptions = ClientOptions, ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtClientMembers extends ExtClientMembersBase = {}, > = { /** * The schema definition. @@ -132,7 +154,7 @@ export type ClientContract< /** * Sets the current user identity. */ - $setAuth(auth: AuthType | undefined): ClientContract; + $setAuth(auth: AuthType | undefined): ClientContract; /** * Returns a new client with new options applied. @@ -141,7 +163,9 @@ export type ClientContract< * const dbNoValidation = db.$setOptions({ ...db.$options, validateInput: false }); * ``` */ - $setOptions>(options: Options): ClientContract; + $setOptions>( + options: NewOptions, + ): ClientContract; /** * Returns a new client enabling/disabling input validations expressed with attributes like @@ -149,7 +173,7 @@ export type ClientContract< * * @deprecated Use {@link $setOptions} instead. */ - $setInputValidation(enable: boolean): ClientContract; + $setInputValidation(enable: boolean): ClientContract; /** * The Kysely query builder instance. @@ -165,7 +189,7 @@ export type ClientContract< * Starts an interactive transaction. */ $transaction( - callback: (tx: TransactionClientContract) => Promise, + callback: (tx: TransactionClientContract) => Promise, options?: { isolationLevel?: TransactionIsolationLevel }, ): Promise; @@ -180,14 +204,18 @@ export type ClientContract< /** * Returns a new client with the specified plugin installed. */ - $use( - plugin: RuntimePlugin, - ): ClientContract; + $use< + PluginSchema extends SchemaDef = Schema, + PluginExtQueryArgs extends ExtQueryArgsBase = {}, + PluginExtClientMembers extends ExtClientMembersBase = {}, + >( + plugin: RuntimePlugin, + ): ClientContract; /** * Returns a new client with the specified plugin removed. */ - $unuse(pluginId: string): ClientContract; + $unuse(pluginId: string): ClientContract; /** * Returns a new client with all plugins removed. @@ -216,7 +244,8 @@ export type ClientContract< ToQueryOptions, ExtQueryArgs >; -} & ProcedureOperations; +} & ProcedureOperations & + ExtClientMembers; /** * The contract for a client in a transaction. @@ -225,7 +254,8 @@ export type TransactionClientContract< Schema extends SchemaDef, Options extends ClientOptions, ExtQueryArgs extends ExtQueryArgsBase, -> = Omit, TransactionUnsupportedMethods>; + ExtClientMembers extends ExtClientMembersBase, +> = Omit, TransactionUnsupportedMethods>; export type ProcedureOperations = Schema['procedures'] extends Record diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index c9c85121c..5eb4b2d1d 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -119,6 +119,36 @@ export const CoreWriteOperations = [ */ export type CoreWriteOperations = (typeof CoreWriteOperations)[number]; +/** + * List of core create operations. + */ +export const CoreCreateOperations = ['create', 'createMany', 'createManyAndReturn', 'upsert'] as const; + +/** + * List of core create operations. + */ +export type CoreCreateOperations = (typeof CoreCreateOperations)[number]; + +/** + * List of core update operations. + */ +export const CoreUpdateOperations = ['update', 'updateMany', 'updateManyAndReturn', 'upsert'] as const; + +/** + * List of core update operations. + */ +export type CoreUpdateOperations = (typeof CoreUpdateOperations)[number]; + +/** + * List of core delete operations. + */ +export const CoreDeleteOperations = ['delete', 'deleteMany'] as const; + +/** + * List of core delete operations. + */ +export type CoreDeleteOperations = (typeof CoreDeleteOperations)[number]; + /** * List of all CRUD operations, including 'orThrow' variants. */ diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index a6256da77..76dd58529 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -35,6 +35,7 @@ import { type UpsertArgs, } from '../../crud-types'; import { createInternalError, createInvalidInputError } from '../../errors'; +import type { AnyPlugin } from '../../plugin'; import { fieldHasDefaultValue, getDiscriminatorField, @@ -46,7 +47,13 @@ import { requireField, requireModel, } from '../../query-utils'; -import type { CoreCrudOperations } from '../operations/base'; +import { + CoreCreateOperations, + CoreDeleteOperations, + CoreReadOperations, + CoreUpdateOperations, + type CoreCrudOperations, +} from '../operations/base'; import { addBigIntValidation, addCustomValidation, @@ -365,8 +372,8 @@ export class InputValidator { private mergePluginArgsSchema(schema: ZodObject, operation: CoreCrudOperations) { let result = schema; for (const plugin of this.options.plugins ?? []) { - if (plugin.extQueryArgs) { - const pluginSchema = plugin.extQueryArgs.getValidationSchema(operation); + if (plugin.queryArgs) { + const pluginSchema = this.getPluginExtQueryArgsSchema(plugin, operation); if (pluginSchema) { result = result.extend(pluginSchema.shape); } @@ -375,6 +382,77 @@ export class InputValidator { return result.strict(); } + private getPluginExtQueryArgsSchema(plugin: AnyPlugin, operation: string): ZodObject | undefined { + if (!plugin.queryArgs) { + return undefined; + } + + let result: ZodType | undefined; + + if (operation in plugin.queryArgs && plugin.queryArgs[operation]) { + // most specific operation takes highest precedence + result = plugin.queryArgs[operation]; + } else if (operation === 'upsert') { + // upsert is special: it's in both CoreCreateOperations and CoreUpdateOperations + // so we need to merge both $create and $update schemas to match the type system + const createSchema = + '$create' in plugin.queryArgs && plugin.queryArgs['$create'] ? plugin.queryArgs['$create'] : undefined; + const updateSchema = + '$update' in plugin.queryArgs && plugin.queryArgs['$update'] ? plugin.queryArgs['$update'] : undefined; + + if (createSchema && updateSchema) { + invariant( + createSchema instanceof z.ZodObject, + 'Plugin extended query args schema must be a Zod object', + ); + invariant( + updateSchema instanceof z.ZodObject, + 'Plugin extended query args schema must be a Zod object', + ); + // merge both schemas (combines their properties) + result = createSchema.extend(updateSchema.shape); + } else if (createSchema) { + result = createSchema; + } else if (updateSchema) { + result = updateSchema; + } + } else if ( + // then comes grouped operations: $create, $read, $update, $delete + CoreCreateOperations.includes(operation as CoreCreateOperations) && + '$create' in plugin.queryArgs && + plugin.queryArgs['$create'] + ) { + result = plugin.queryArgs['$create']; + } else if ( + CoreReadOperations.includes(operation as CoreReadOperations) && + '$read' in plugin.queryArgs && + plugin.queryArgs['$read'] + ) { + result = plugin.queryArgs['$read']; + } else if ( + CoreUpdateOperations.includes(operation as CoreUpdateOperations) && + '$update' in plugin.queryArgs && + plugin.queryArgs['$update'] + ) { + result = plugin.queryArgs['$update']; + } else if ( + CoreDeleteOperations.includes(operation as CoreDeleteOperations) && + '$delete' in plugin.queryArgs && + plugin.queryArgs['$delete'] + ) { + result = plugin.queryArgs['$delete']; + } else if ('$all' in plugin.queryArgs && plugin.queryArgs['$all']) { + // finally comes $all + result = plugin.queryArgs['$all']; + } + + invariant( + result === undefined || result instanceof z.ZodObject, + 'Plugin extended query args schema must be a Zod object', + ); + return result; + } + // #region Find private makeFindSchema(model: string, operation: CoreCrudOperations) { diff --git a/packages/orm/src/client/index.ts b/packages/orm/src/client/index.ts index bf17a9e6e..00bbf1b6d 100644 --- a/packages/orm/src/client/index.ts +++ b/packages/orm/src/client/index.ts @@ -6,8 +6,11 @@ export { BaseCrudDialect } from './crud/dialects/base-dialect'; export { AllCrudOperations, AllReadOperations, + CoreCreateOperations, CoreCrudOperations, + CoreDeleteOperations, CoreReadOperations, + CoreUpdateOperations, CoreWriteOperations, } from './crud/operations/base'; export { InputValidator } from './crud/validator'; diff --git a/packages/orm/src/client/options.ts b/packages/orm/src/client/options.ts index d1fa23ed7..6439e3996 100644 --- a/packages/orm/src/client/options.ts +++ b/packages/orm/src/client/options.ts @@ -4,7 +4,7 @@ import type { PrependParameter } from '../utils/type-utils'; import type { ClientContract, CRUD_EXT } from './contract'; import type { GetProcedureNames, ProcedureHandlerFunc } from './crud-types'; import type { BaseCrudDialect } from './crud/dialects/base-dialect'; -import type { RuntimePlugin } from './plugin'; +import type { AnyPlugin } from './plugin'; import type { ToKyselySchema } from './query-builder'; export type ZModelFunctionContext = { @@ -59,7 +59,7 @@ export type ClientOptions = { /** * Plugins. */ - plugins?: RuntimePlugin[]; + plugins?: AnyPlugin[]; /** * Logging configuration. diff --git a/packages/orm/src/client/plugin.ts b/packages/orm/src/client/plugin.ts index ee024f1e8..81dff0ec5 100644 --- a/packages/orm/src/client/plugin.ts +++ b/packages/orm/src/client/plugin.ts @@ -1,19 +1,33 @@ import type { OperationNode, QueryId, QueryResult, RootOperationNode, UnknownRow } from 'kysely'; -import type { ZodObject } from 'zod'; +import type { ZodType } from 'zod'; import type { ClientContract, ZModelFunction } from '.'; import type { GetModels, SchemaDef } from '../schema'; import type { MaybePromise } from '../utils/type-utils'; import type { AllCrudOperations, CoreCrudOperations } from './crud/operations/base'; +type AllowedExtQueryArgKeys = CoreCrudOperations | '$create' | '$read' | '$update' | '$delete' | '$all'; + /** * Base shape of plugin-extended query args. */ -export type ExtQueryArgsBase = { [K in CoreCrudOperations | 'all']?: object }; +export type ExtQueryArgsBase = { + [K in AllowedExtQueryArgKeys]?: object; +}; + +/** + * Base type for plugin-extended client members (methods and properties). + * Member names should start with '$' to avoid model name conflicts. + */ +export type ExtClientMembersBase = Record; /** * ZenStack runtime plugin. */ -export interface RuntimePlugin { +export interface RuntimePlugin< + Schema extends SchemaDef, + ExtQueryArgs extends ExtQueryArgsBase, + ExtClientMembers extends Record, +> { /** * Plugin ID. */ @@ -59,19 +73,26 @@ export interface RuntimePlugin ZodObject | undefined; + queryArgs?: { + [K in keyof ExtQueryArgs]: ZodType; }; + + /** + * Extended client members (methods and properties). + */ + client?: ExtClientMembers; } + +export type AnyPlugin = RuntimePlugin; + /** * Defines a ZenStack runtime plugin. */ -export function definePlugin( - plugin: RuntimePlugin, -): RuntimePlugin { +export function definePlugin< + Schema extends SchemaDef, + const ExtQueryArgs extends ExtQueryArgsBase = {}, + const ExtClientMembers extends Record = {}, +>(plugin: RuntimePlugin): RuntimePlugin { return plugin; } diff --git a/packages/plugins/policy/src/plugin.ts b/packages/plugins/policy/src/plugin.ts index e27b0cf54..3f67309f3 100644 --- a/packages/plugins/policy/src/plugin.ts +++ b/packages/plugins/policy/src/plugin.ts @@ -3,7 +3,7 @@ import type { SchemaDef } from '@zenstackhq/orm/schema'; import { check } from './functions'; import { PolicyHandler } from './policy-handler'; -export class PolicyPlugin implements RuntimePlugin { +export class PolicyPlugin implements RuntimePlugin { get id() { return 'policy' as const; } diff --git a/tests/e2e/orm/plugin-infra/client-members.test.ts b/tests/e2e/orm/plugin-infra/client-members.test.ts new file mode 100644 index 000000000..cf9e6f9b7 --- /dev/null +++ b/tests/e2e/orm/plugin-infra/client-members.test.ts @@ -0,0 +1,239 @@ +import { definePlugin, type ClientContract } from '@zenstackhq/orm'; +import { createTestClient } from '@zenstackhq/testtools'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import z from 'zod'; +import { schema } from './ext-query-args/schema'; + +describe('Plugin client members', () => { + let db: ClientContract; + + beforeEach(async () => { + db = await createTestClient(schema); + await db.user.deleteMany(); + }); + + afterEach(async () => { + await db?.$disconnect(); + }); + + it('should allow adding methods and props to client', async () => { + let methodCalled = false; + + const extDb = db.$use( + definePlugin({ + id: 'test-plugin', + client: { + // method + $invalidateCache(model?: string) { + methodCalled = true; + return model ?? 'hello'; + }, + + // dynamic property + get $cacheStats() { + return { hits: 10, misses: 5 }; + }, + + // constant property + $cacheStats1: { + hits: 20, + misses: 10, + }, + }, + }), + ); + + const result = extDb.$invalidateCache(); + expect(result).toBe('hello'); + expect(methodCalled).toBe(true); + + expect(extDb.$invalidateCache('user')).toBe('user'); + + // @ts-expect-error + extDb.$invalidateCache(1); + + expect(extDb.$cacheStats.hits).toBe(10); + expect(extDb.$cacheStats.misses).toBe(5); + + expect(extDb.$cacheStats1.hits).toBe(20); + expect(extDb.$cacheStats1.misses).toBe(10); + }); + + it('should support multiple plugins with different members', async () => { + const plugin1 = definePlugin({ + id: 'plugin1', + client: { + $method1: () => 'from-plugin1', + }, + }); + + const plugin2 = definePlugin({ + id: 'plugin2', + client: { + $method2: () => 'from-plugin2', + }, + }); + + const extDb = db.$use(plugin1).$use(plugin2); + + expect(extDb.$method1()).toBe('from-plugin1'); + expect(extDb.$method2()).toBe('from-plugin2'); + }); + + it('should make later plugin win for conflicting members', async () => { + const plugin1 = definePlugin({ + id: 'plugin1', + client: { + $conflicting: () => 'from-plugin1', + }, + }); + + const plugin2 = definePlugin({ + id: 'plugin2', + client: { + $conflicting: () => 'from-plugin2', + }, + }); + + const extDb = db.$use(plugin1).$use(plugin2); + + // Later plugin wins + expect(extDb.$conflicting()).toBe('from-plugin2'); + }); + + it('should make members available in transactions', async () => { + const extDb = db.$use( + definePlugin({ + id: 'test-plugin', + client: { + $txHelper: () => 'in-transaction', + }, + }), + ); + + await extDb.$transaction(async (tx) => { + expect(tx.$txHelper()).toBe('in-transaction'); + await tx.user.create({ data: { name: 'Bob' } }); + }); + }); + + it('should remove members when plugin is removed via $unuse', async () => { + const extDb = db.$use( + definePlugin({ + id: 'removable-plugin', + client: { + $toBeRemoved: () => 'exists', + }, + }), + ); + + expect(extDb.$toBeRemoved()).toBe('exists'); + + const removedDb = extDb.$unuse('removable-plugin'); + + // After $unuse, the method should not be available + // TypeScript would complain, but at runtime it should be undefined + expect(removedDb.$toBeRemoved).toBeUndefined(); + }); + + it('should remove all members when $unuseAll is called', async () => { + const extDb = db + .$use( + definePlugin({ + id: 'p1', + client: { $m1: () => 'a' }, + }), + ) + .$use( + definePlugin({ + id: 'p2', + client: { $m2: () => 'b' }, + }), + ); + + expect(extDb.$m1()).toBe('a'); + expect(extDb.$m2()).toBe('b'); + + const cleanDb = extDb.$unuseAll(); + + expect((cleanDb as any).$m1).toBeUndefined(); + expect((cleanDb as any).$m2).toBeUndefined(); + }); + + it('should isolate members between client instances', async () => { + const extDb = db.$use( + definePlugin({ + id: 'isolated-plugin', + client: { + $isolated: () => 'only-on-extDb', + }, + }), + ); + + expect(extDb.$isolated()).toBe('only-on-extDb'); + + // Original db should not have the method + expect((db as any).$isolated).toBeUndefined(); + }); + + it('should preserve members through $setAuth', async () => { + const extDb = db.$use( + definePlugin({ + id: 'test-plugin', + client: { + $preserved: () => 'still-here', + }, + }), + ); + + const authDb = extDb.$setAuth({ id: 1 }); + + expect(authDb.$preserved()).toBe('still-here'); + }); + + it('should preserve members through $setOptions', async () => { + const extDb = db.$use( + definePlugin({ + id: 'test-plugin', + client: { + $preserved: () => 'still-here', + }, + }), + ); + + const newOptionsDb = extDb.$setOptions({ ...extDb.$options, validateInput: false }); + + expect(newOptionsDb.$preserved()).toBe('still-here'); + }); + + it('should work with both extQueryArgs and client members', async () => { + let gotTTL: number | undefined; + + const extDb = db.$use( + definePlugin({ + id: 'cache-plugin', + queryArgs: { + $all: z.object({ + cache: z + .object({ + ttl: z.number().optional(), + }) + .optional(), + }), + }, + onQuery: async ({ args, proceed }) => { + if (args && 'cache' in args) { + gotTTL = (args as any).cache?.ttl; + } + return proceed(args); + }, + client: { + $getCachedTTL: () => gotTTL, + }, + }), + ); + + await extDb.user.create({ data: { name: 'Test' }, cache: { ttl: 1000 } }); + expect(extDb.$getCachedTTL()).toBe(1000); + }); +}); diff --git a/tests/e2e/orm/plugin-infra/ext-query-args.test.ts b/tests/e2e/orm/plugin-infra/ext-query-args.test.ts index 7a5630d1a..95794626d 100644 --- a/tests/e2e/orm/plugin-infra/ext-query-args.test.ts +++ b/tests/e2e/orm/plugin-infra/ext-query-args.test.ts @@ -1,4 +1,4 @@ -import { CoreReadOperations, CoreWriteOperations, definePlugin, type ClientContract } from '@zenstackhq/orm'; +import { definePlugin, type ClientContract } from '@zenstackhq/orm'; import { createTestClient } from '@zenstackhq/testtools'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import z from 'zod'; @@ -16,13 +16,14 @@ describe('Plugin extended query args', () => { }); const cacheBustSchema = z.object({ - cache: z.strictObject({ - bust: z.boolean().optional(), - }), + cache: z + .strictObject({ + bust: z.boolean().optional(), + }) + .optional(), }); type CacheOptions = z.infer; - type CacheBustOptions = z.infer; beforeEach(async () => { db = await createTestClient(schema); @@ -33,34 +34,32 @@ describe('Plugin extended query args', () => { await db?.$disconnect(); }); - it('should allow extending all operations', async () => { + it('should allow extending grouped operations', async () => { let gotTTL: number | undefined = undefined; - const extDb = db.$use( - definePlugin< - typeof schema, - { - all: CacheOptions; + const cachePlugin = definePlugin({ + id: 'cache', + queryArgs: { + $read: cacheSchema, + $create: cacheBustSchema, + $update: cacheBustSchema, + $delete: cacheBustSchema, + }, + + onQuery: async ({ args, proceed }) => { + if (args && 'cache' in args) { + gotTTL = (args as CacheOptions).cache?.ttl; } - >({ - id: 'cache', - extQueryArgs: { - getValidationSchema: () => cacheSchema, - }, + return proceed(args); + }, + }); - onQuery: async ({ args, proceed }) => { - if (args && 'cache' in args) { - gotTTL = (args as CacheOptions).cache?.ttl; - } - return proceed(args); - }, - }), - ); + const extDb = db.$use(cachePlugin); // cache is optional const alice = await extDb.user.create({ data: { name: 'Alice' } }); - // ttl is optional + // bust is optional const bob = await extDb.user.create({ data: { name: 'Bob' }, cache: {} }); gotTTL = undefined; @@ -81,9 +80,20 @@ describe('Plugin extended query args', () => { // @ts-expect-error await expect(extDb.user.findMany({ where: { id: 'abc' } })).rejects.toThrow('expected number'); + // read args are not allowed in create + // @ts-expect-error + await expect(extDb.user.create({ data: { name: 'Charlie' }, cache: { ttl: 1000 } })).rejects.toThrow( + 'Unrecognized key', + ); + + // create args are not allowed in read + // @ts-expect-error + await expect(extDb.user.findMany({ cache: { bust: true } })).rejects.toThrow('Unrecognized key'); + // validate all other operations const cacheOption = { cache: { ttl: 1000 } } as const; + const cacheBustOption = { cache: { bust: true } } as const; // read operations await expect(extDb.user.findUnique({ where: { id: 1 }, ...cacheOption })).toResolveTruthy(); @@ -109,25 +119,25 @@ describe('Plugin extended query args', () => { ).resolves.toHaveLength(2); // create operations - await expect(extDb.user.createMany({ data: [{ name: 'Charlie' }], ...cacheOption })).resolves.toHaveProperty( - 'count', - ); - await expect(extDb.user.createManyAndReturn({ data: [{ name: 'David' }], ...cacheOption })).toResolveWithLength( - 1, - ); + await expect( + extDb.user.createMany({ data: [{ name: 'Charlie' }], ...cacheBustOption }), + ).resolves.toHaveProperty('count'); + await expect( + extDb.user.createManyAndReturn({ data: [{ name: 'David' }], ...cacheBustOption }), + ).toResolveWithLength(1); // update operations await expect( - extDb.user.update({ where: { id: alice.id }, data: { name: 'Alice Updated' }, ...cacheOption }), + extDb.user.update({ where: { id: alice.id }, data: { name: 'Alice Updated' }, ...cacheBustOption }), ).toResolveTruthy(); await expect( - extDb.user.updateMany({ where: { name: 'Bob' }, data: { name: 'Bob Updated' }, ...cacheOption }), + extDb.user.updateMany({ where: { name: 'Bob' }, data: { name: 'Bob Updated' }, ...cacheBustOption }), ).resolves.toHaveProperty('count'); await expect( extDb.user.updateManyAndReturn({ where: { name: 'Charlie' }, data: { name: 'Charlie Updated' }, - ...cacheOption, + ...cacheBustOption, }), ).toResolveTruthy(); await expect( @@ -135,13 +145,13 @@ describe('Plugin extended query args', () => { where: { id: 999 }, create: { name: 'Eve' }, update: { name: 'Eve Updated' }, - ...cacheOption, + ...cacheBustOption, }), ).resolves.toMatchObject({ name: 'Eve' }); // delete operations - await expect(extDb.user.delete({ where: { id: bob.id }, ...cacheOption })).toResolveTruthy(); - await expect(extDb.user.deleteMany({ where: { name: 'David' }, ...cacheOption })).resolves.toHaveProperty( + await expect(extDb.user.delete({ where: { id: bob.id }, ...cacheBustOption })).toResolveTruthy(); + await expect(extDb.user.deleteMany({ where: { name: 'David' }, ...cacheBustOption })).resolves.toHaveProperty( 'count', ); @@ -162,101 +172,51 @@ describe('Plugin extended query args', () => { await expect(extDb.$setAuth({ id: 1 }).user.findMany(cacheOption)).toResolveTruthy(); }); - it('should allow extending specific operations', async () => { + it('should allow extending all operations', async () => { const extDb = db.$use( - definePlugin< - typeof schema, - { - [Op in CoreReadOperations]: CacheOptions; - } - >({ + definePlugin({ id: 'cache', - extQueryArgs: { - getValidationSchema: (operation) => { - if (!(CoreReadOperations as readonly string[]).includes(operation)) { - return undefined; - } - return cacheSchema; - }, + queryArgs: { + $all: cacheSchema, }, }), ); - // "create" is not extended - // @ts-expect-error - await expect(extDb.user.create({ data: { name: 'Bob' }, cache: {} })).rejects.toThrow('Unrecognized key'); - - await extDb.user.create({ data: { name: 'Alice' } }); - + const alice = await extDb.user.create({ data: { name: 'Alice' }, cache: {} }); await expect(extDb.user.findMany({ cache: { ttl: 100 } })).toResolveWithLength(1); await expect(extDb.user.count({ where: { name: 'Alice' }, cache: { ttl: 200 } })).resolves.toBe(1); + await expect( + extDb.user.update({ where: { id: alice.id }, data: { name: 'Alice Updated' }, cache: { ttl: 300 } }), + ).toResolveTruthy(); + await expect(extDb.user.delete({ where: { id: alice.id }, cache: { ttl: 400 } })).toResolveTruthy(); }); - it('should allow different extensions for different operations', async () => { - let gotTTL: number | undefined = undefined; - let gotBust: boolean | undefined = undefined; - + it('should allow extending specific operations', async () => { const extDb = db.$use( - definePlugin< - typeof schema, - { - [Op in CoreReadOperations]: CacheOptions; - } & { - [Op in CoreWriteOperations]: CacheBustOptions; - } - >({ + definePlugin({ id: 'cache', - extQueryArgs: { - getValidationSchema: (operation) => { - if ((CoreReadOperations as readonly string[]).includes(operation)) { - return cacheSchema; - } else if ((CoreWriteOperations as readonly string[]).includes(operation)) { - return cacheBustSchema; - } - return undefined; - }, - }, - - onQuery: async ({ args, proceed }) => { - if (args && 'cache' in args) { - gotTTL = (args as CacheOptions).cache?.ttl; - gotBust = (args as CacheBustOptions).cache?.bust; - } - return proceed(args); + queryArgs: { + $read: cacheSchema, }, }), ); - gotBust = undefined; - await extDb.user.create({ data: { name: 'Alice' }, cache: { bust: true } }); - expect(gotBust).toBe(true); - - // ttl extension is not applied to "create" + // "create" is not extended // @ts-expect-error - await expect(extDb.user.create({ data: { name: 'Bob' }, cache: { ttl: 100 } })).rejects.toThrow( - 'Unrecognized key', - ); + await expect(extDb.user.create({ data: { name: 'Bob' }, cache: {} })).rejects.toThrow('Unrecognized key'); - gotTTL = undefined; - await expect(extDb.user.findMany({ cache: { ttl: 5000 } })).toResolveWithLength(1); - expect(gotTTL).toBe(5000); + await extDb.user.create({ data: { name: 'Alice' } }); - // bust extension is not applied to "findMany" - // @ts-expect-error - await expect(extDb.user.findMany({ cache: { bust: true } })).rejects.toThrow('Unrecognized key'); + await expect(extDb.user.findMany({ cache: { ttl: 100 } })).toResolveWithLength(1); + await expect(extDb.user.count({ where: { name: 'Alice' }, cache: { ttl: 200 } })).resolves.toBe(1); }); it('should isolate validation schemas between clients', async () => { const extDb = db.$use( - definePlugin< - typeof schema, - { - all: CacheOptions; - } - >({ + definePlugin({ id: 'cache', - extQueryArgs: { - getValidationSchema: () => cacheSchema, + queryArgs: { + $all: cacheSchema, }, }), ); @@ -270,4 +230,103 @@ describe('Plugin extended query args', () => { await expect(db.user.findMany({ cache: { ttl: 2000 } })).rejects.toThrow('Unrecognized key'); await expect(extDb.user.findMany({ cache: { ttl: 2000 } })).toResolveWithLength(0); }); + + it('should merge $create and $update schemas for upsert operation', async () => { + // Define different schemas for $create and $update + const createOnlySchema = z.object({ + tracking: z + .strictObject({ + source: z.string().optional(), + }) + .optional(), + }); + + const updateOnlySchema = z.object({ + audit: z + .strictObject({ + reason: z.string().optional(), + }) + .optional(), + }); + + const extDb = db.$use( + definePlugin({ + id: 'test', + queryArgs: { + $create: createOnlySchema, + $update: updateOnlySchema, + }, + }), + ); + + // upsert should accept both tracking (from $create) and audit (from $update) + await expect( + extDb.user.upsert({ + where: { id: 999 }, + create: { name: 'Alice' }, + update: { name: 'Alice Updated' }, + tracking: { source: 'test' }, + audit: { reason: 'testing merge' }, + }), + ).resolves.toMatchObject({ name: 'Alice' }); + + // upsert should reject tracking-only in update operations + await expect( + extDb.user.update({ + where: { id: 1 }, + data: { name: 'Test' }, + // @ts-expect-error - tracking is only for $create + tracking: { source: 'test' }, + }), + ).rejects.toThrow('Unrecognized key'); + + // upsert should reject audit-only in create operations + await expect( + extDb.user.create({ + data: { name: 'Bob' }, + // @ts-expect-error - audit is only for $update + audit: { reason: 'test' }, + }), + ).rejects.toThrow('Unrecognized key'); + + // verify that upsert without both is fine + await expect( + extDb.user.upsert({ + where: { id: 888 }, + create: { name: 'Charlie' }, + update: { name: 'Charlie Updated' }, + }), + ).resolves.toMatchObject({ name: 'Charlie' }); + + // verify that upsert with only tracking is fine + await expect( + extDb.user.upsert({ + where: { id: 777 }, + create: { name: 'David' }, + update: { name: 'David Updated' }, + tracking: { source: 'test' }, + }), + ).resolves.toMatchObject({ name: 'David' }); + + // verify that upsert with only audit is fine + await expect( + extDb.user.upsert({ + where: { id: 666 }, + create: { name: 'Eve' }, + update: { name: 'Eve Updated' }, + audit: { reason: 'testing' }, + }), + ).resolves.toMatchObject({ name: 'Eve' }); + + // verify that upsert with both is fine + await expect( + extDb.user.upsert({ + where: { id: 555 }, + create: { name: 'Frank' }, + update: { name: 'Frank Updated' }, + tracking: { source: 'test' }, + audit: { reason: 'testing both' }, + }), + ).resolves.toMatchObject({ name: 'Frank' }); + }); }); diff --git a/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts b/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts index 0b74e76fc..46d0eb413 100644 --- a/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/on-query-hooks.test.ts @@ -190,7 +190,7 @@ describe('On query hooks tests', () => { let findHookCalled = false; - const plugin = definePlugin({ + const plugin = definePlugin({ id: 'test-plugin', onQuery: (ctx) => { findHookCalled = true; From 077f03f7322a7f25362c52b5d4891b7685cc9644 Mon Sep 17 00:00:00 2001 From: Mike Willbanks Date: Sun, 18 Jan 2026 00:08:10 -0600 Subject: [PATCH 05/25] feat(zmodel): collection predicate binding (#548) * feat: audit policy collection aliases provides a means to alias collections in @@allow collections by extending the ast this allows for utilizing collections inside of @@allow like: ``` memberships?[m, auth().memberships?[ tenantId == m.tenantId ... ] ] ``` * fix: code review comments + syntax fixes * refactor: extract collection predicate binding to its own language construct (#2) - adjusted language processing chain accordingly - fixed several issues in policy transformer/evaluator - more test cases * addressing PR comments --------- Co-authored-by: Yiming Cao Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/language/src/generated/ast.ts | 36 +- packages/language/src/generated/grammar.ts | 302 +++++++----- .../attribute-application-validator.ts | 14 +- .../src/validators/expression-validator.ts | 17 +- .../language/src/zmodel-code-generator.ts | 8 +- packages/language/src/zmodel-linker.ts | 30 +- packages/language/src/zmodel-scope.ts | 59 ++- packages/language/src/zmodel.langium | 8 +- .../test/expression-validation.test.ts | 303 +++++++++++- .../orm/src/client/crud/validator/utils.ts | 3 + packages/orm/src/utils/schema-utils.ts | 4 + .../policy/src/expression-evaluator.ts | 43 +- .../policy/src/expression-transformer.ts | 136 +++++- packages/schema/src/expression-utils.ts | 13 +- packages/schema/src/expression.ts | 7 + packages/sdk/src/ts-schema-generator.ts | 30 +- tests/e2e/orm/policy/auth-access.test.ts | 42 ++ .../orm/policy/collection-predicate.test.ts | 447 ++++++++++++++++++ 18 files changed, 1290 insertions(+), 212 deletions(-) create mode 100644 tests/e2e/orm/policy/collection-predicate.test.ts diff --git a/packages/language/src/generated/ast.ts b/packages/language/src/generated/ast.ts index 7d6a589cc..527868d41 100644 --- a/packages/language/src/generated/ast.ts +++ b/packages/language/src/generated/ast.ts @@ -144,7 +144,7 @@ export function isMemberAccessTarget(item: unknown): item is MemberAccessTarget return reflection.isInstance(item, MemberAccessTarget); } -export type ReferenceTarget = DataField | EnumField | FunctionParam; +export type ReferenceTarget = CollectionPredicateBinding | DataField | EnumField | FunctionParam; export const ReferenceTarget = 'ReferenceTarget'; @@ -258,6 +258,7 @@ export function isAttributeParamType(item: unknown): item is AttributeParamType export interface BinaryExpr extends langium.AstNode { readonly $container: Argument | ArrayExpr | AttributeArg | BinaryExpr | FieldInitializer | FunctionDecl | MemberAccessExpr | ReferenceArg | UnaryExpr; readonly $type: 'BinaryExpr'; + binding?: CollectionPredicateBinding; left: Expression; operator: '!' | '!=' | '&&' | '<' | '<=' | '==' | '>' | '>=' | '?' | '^' | 'in' | '||'; right: Expression; @@ -281,6 +282,18 @@ export function isBooleanLiteral(item: unknown): item is BooleanLiteral { return reflection.isInstance(item, BooleanLiteral); } +export interface CollectionPredicateBinding extends langium.AstNode { + readonly $container: BinaryExpr; + readonly $type: 'CollectionPredicateBinding'; + name: RegularID; +} + +export const CollectionPredicateBinding = 'CollectionPredicateBinding'; + +export function isCollectionPredicateBinding(item: unknown): item is CollectionPredicateBinding { + return reflection.isInstance(item, CollectionPredicateBinding); +} + export interface ConfigArrayExpr extends langium.AstNode { readonly $container: ConfigField; readonly $type: 'ConfigArrayExpr'; @@ -775,6 +788,7 @@ export type ZModelAstType = { AttributeParamType: AttributeParamType BinaryExpr: BinaryExpr BooleanLiteral: BooleanLiteral + CollectionPredicateBinding: CollectionPredicateBinding ConfigArrayExpr: ConfigArrayExpr ConfigExpr: ConfigExpr ConfigField: ConfigField @@ -822,7 +836,7 @@ export type ZModelAstType = { export class ZModelAstReflection extends langium.AbstractAstReflection { getAllTypes(): string[] { - return [AbstractDeclaration, Argument, ArrayExpr, Attribute, AttributeArg, AttributeParam, AttributeParamType, BinaryExpr, BooleanLiteral, ConfigArrayExpr, ConfigExpr, ConfigField, ConfigInvocationArg, ConfigInvocationExpr, DataField, DataFieldAttribute, DataFieldType, DataModel, DataModelAttribute, DataSource, Enum, EnumField, Expression, FieldInitializer, FunctionDecl, FunctionParam, FunctionParamType, GeneratorDecl, InternalAttribute, InvocationExpr, LiteralExpr, MemberAccessExpr, MemberAccessTarget, Model, ModelImport, NullExpr, NumberLiteral, ObjectExpr, Plugin, PluginField, Procedure, ProcedureParam, ReferenceArg, ReferenceExpr, ReferenceTarget, StringLiteral, ThisExpr, TypeDeclaration, TypeDef, UnaryExpr, UnsupportedFieldType]; + return [AbstractDeclaration, Argument, ArrayExpr, Attribute, AttributeArg, AttributeParam, AttributeParamType, BinaryExpr, BooleanLiteral, CollectionPredicateBinding, ConfigArrayExpr, ConfigExpr, ConfigField, ConfigInvocationArg, ConfigInvocationExpr, DataField, DataFieldAttribute, DataFieldType, DataModel, DataModelAttribute, DataSource, Enum, EnumField, Expression, FieldInitializer, FunctionDecl, FunctionParam, FunctionParamType, GeneratorDecl, InternalAttribute, InvocationExpr, LiteralExpr, MemberAccessExpr, MemberAccessTarget, Model, ModelImport, NullExpr, NumberLiteral, ObjectExpr, Plugin, PluginField, Procedure, ProcedureParam, ReferenceArg, ReferenceExpr, ReferenceTarget, StringLiteral, ThisExpr, TypeDeclaration, TypeDef, UnaryExpr, UnsupportedFieldType]; } protected override computeIsSubtype(subtype: string, supertype: string): boolean { @@ -850,6 +864,11 @@ export class ZModelAstReflection extends langium.AbstractAstReflection { case StringLiteral: { return this.isSubtype(LiteralExpr, supertype); } + case CollectionPredicateBinding: + case EnumField: + case FunctionParam: { + return this.isSubtype(ReferenceTarget, supertype); + } case ConfigArrayExpr: { return this.isSubtype(ConfigExpr, supertype); } @@ -861,10 +880,6 @@ export class ZModelAstReflection extends langium.AbstractAstReflection { case TypeDef: { return this.isSubtype(AbstractDeclaration, supertype) || this.isSubtype(TypeDeclaration, supertype); } - case EnumField: - case FunctionParam: { - return this.isSubtype(ReferenceTarget, supertype); - } case InvocationExpr: case LiteralExpr: { return this.isSubtype(ConfigExpr, supertype) || this.isSubtype(Expression, supertype); @@ -975,6 +990,7 @@ export class ZModelAstReflection extends langium.AbstractAstReflection { return { name: BinaryExpr, properties: [ + { name: 'binding' }, { name: 'left' }, { name: 'operator' }, { name: 'right' } @@ -989,6 +1005,14 @@ export class ZModelAstReflection extends langium.AbstractAstReflection { ] }; } + case CollectionPredicateBinding: { + return { + name: CollectionPredicateBinding, + properties: [ + { name: 'name' } + ] + }; + } case ConfigArrayExpr: { return { name: ConfigArrayExpr, diff --git a/packages/language/src/generated/grammar.ts b/packages/language/src/generated/grammar.ts index 53688b82f..925d4dcb6 100644 --- a/packages/language/src/generated/grammar.ts +++ b/packages/language/src/generated/grammar.ts @@ -70,7 +70,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] } @@ -119,42 +119,42 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@44" + "$ref": "#/rules@45" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@46" + "$ref": "#/rules@47" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@53" + "$ref": "#/rules@54" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@50" + "$ref": "#/rules@51" }, "arguments": [] } @@ -176,7 +176,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -192,7 +192,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -236,7 +236,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -252,7 +252,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -296,7 +296,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -308,7 +308,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -347,7 +347,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -363,7 +363,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -407,7 +407,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -419,7 +419,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -474,7 +474,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "definition": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@33" + "$ref": "#/rules@34" }, "arguments": [] }, @@ -495,7 +495,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@70" + "$ref": "#/rules@71" }, "arguments": [] } @@ -517,7 +517,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] } @@ -539,7 +539,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@63" + "$ref": "#/rules@64" }, "arguments": [] } @@ -663,7 +663,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@68" + "$ref": "#/rules@69" }, "arguments": [] } @@ -761,7 +761,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@68" + "$ref": "#/rules@69" }, "arguments": [] } @@ -970,7 +970,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@53" }, "arguments": [] }, @@ -1069,7 +1069,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@68" + "$ref": "#/rules@69" }, "arguments": [] } @@ -1183,14 +1183,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@69" + "$ref": "#/rules@70" }, "arguments": [] } @@ -1235,7 +1235,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@46" + "$ref": "#/rules@47" }, "deprecatedSyntax": false } @@ -1247,7 +1247,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@35" + "$ref": "#/rules@36" }, "arguments": [], "cardinality": "?" @@ -1278,7 +1278,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@34" + "$ref": "#/rules@35" }, "arguments": [] }, @@ -1418,6 +1418,28 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "$type": "Keyword", "value": "[" }, + { + "$type": "Group", + "elements": [ + { + "$type": "Assignment", + "feature": "binding", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@30" + }, + "arguments": [] + } + }, + { + "$type": "Keyword", + "value": "," + } + ], + "cardinality": "?" + }, { "$type": "Assignment", "feature": "right", @@ -1446,6 +1468,28 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "parameters": [], "wildcard": false }, + { + "$type": "ParserRule", + "name": "CollectionPredicateBinding", + "definition": { + "$type": "Assignment", + "feature": "name", + "operator": "=", + "terminal": { + "$type": "RuleCall", + "rule": { + "$ref": "#/rules@52" + }, + "arguments": [] + } + }, + "definesHiddenTokens": false, + "entry": false, + "fragment": false, + "hiddenTokens": [], + "parameters": [], + "wildcard": false + }, { "$type": "ParserRule", "name": "InExpr", @@ -1521,7 +1565,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@31" }, "arguments": [] }, @@ -1570,7 +1614,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@30" + "$ref": "#/rules@31" }, "arguments": [] } @@ -1600,7 +1644,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@32" }, "arguments": [] }, @@ -1641,7 +1685,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@31" + "$ref": "#/rules@32" }, "arguments": [] } @@ -1671,7 +1715,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@33" }, "arguments": [] }, @@ -1712,7 +1756,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@32" + "$ref": "#/rules@33" }, "arguments": [] } @@ -1838,7 +1882,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@37" }, "arguments": [] } @@ -1857,7 +1901,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@36" + "$ref": "#/rules@37" }, "arguments": [] } @@ -1908,7 +1952,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [] }, @@ -1931,7 +1975,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -1942,14 +1986,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] }, @@ -1959,14 +2003,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] } @@ -1978,14 +2022,14 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@39" + "$ref": "#/rules@40" }, "arguments": [] } @@ -2015,7 +2059,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2038,7 +2082,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" }, "arguments": [] } @@ -2050,7 +2094,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [] } @@ -2089,7 +2133,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "deprecatedSyntax": false } @@ -2109,7 +2153,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" }, "deprecatedSyntax": false } @@ -2143,7 +2187,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" }, "deprecatedSyntax": false } @@ -2169,7 +2213,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [] }, @@ -2182,7 +2226,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@53" }, "arguments": [] } @@ -2194,7 +2238,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@41" + "$ref": "#/rules@42" }, "arguments": [] } @@ -2206,7 +2250,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] }, @@ -2237,7 +2281,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@62" + "$ref": "#/rules@63" }, "arguments": [] } @@ -2249,7 +2293,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@43" + "$ref": "#/rules@44" }, "arguments": [] } @@ -2266,7 +2310,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] }, @@ -2326,7 +2370,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [] }, @@ -2343,7 +2387,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2351,7 +2395,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@38" + "$ref": "#/rules@39" }, "arguments": [], "cardinality": "?" @@ -2370,7 +2414,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" }, "arguments": [] } @@ -2382,7 +2426,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [] } @@ -2455,7 +2499,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [] }, @@ -2472,7 +2516,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2491,7 +2535,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" }, "arguments": [] } @@ -2503,7 +2547,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@57" + "$ref": "#/rules@58" }, "arguments": [] } @@ -2537,7 +2581,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [] }, @@ -2550,7 +2594,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@52" + "$ref": "#/rules@53" }, "arguments": [] } @@ -2562,7 +2606,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@56" + "$ref": "#/rules@57" }, "arguments": [] }, @@ -2586,7 +2630,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -2602,7 +2646,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2621,7 +2665,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [] } @@ -2640,7 +2684,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" }, "arguments": [] } @@ -2666,7 +2710,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2699,7 +2743,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -2723,7 +2767,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -2735,7 +2779,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2751,7 +2795,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2791,7 +2835,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@61" + "$ref": "#/rules@62" }, "arguments": [] } @@ -2808,7 +2852,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] }, @@ -2854,7 +2898,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -2866,7 +2910,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2882,7 +2926,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -2915,7 +2959,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -2941,7 +2985,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -2960,7 +3004,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -2979,7 +3023,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@49" + "$ref": "#/rules@50" }, "arguments": [] } @@ -3005,7 +3049,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@48" + "$ref": "#/rules@49" }, "arguments": [] } @@ -3017,7 +3061,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -3042,7 +3086,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@68" + "$ref": "#/rules@69" }, "arguments": [] }, @@ -3105,7 +3149,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] }, @@ -3187,7 +3231,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [] }, @@ -3207,21 +3251,21 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@65" + "$ref": "#/rules@66" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] }, { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@68" }, "arguments": [] } @@ -3242,7 +3286,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] } @@ -3261,7 +3305,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@54" + "$ref": "#/rules@55" }, "arguments": [] } @@ -3283,7 +3327,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -3311,7 +3355,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [] }, @@ -3334,7 +3378,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -3350,7 +3394,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@55" + "$ref": "#/rules@56" }, "arguments": [] } @@ -3362,7 +3406,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@58" + "$ref": "#/rules@59" }, "arguments": [] }, @@ -3396,7 +3440,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@61" + "$ref": "#/rules@62" }, "arguments": [] }, @@ -3427,7 +3471,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] }, @@ -3487,12 +3531,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@53" + "$ref": "#/rules@54" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@67" + "$ref": "#/rules@68" }, "arguments": [] }, @@ -3509,7 +3553,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "?" @@ -3539,7 +3583,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@71" + "$ref": "#/rules@72" }, "arguments": [], "cardinality": "*" @@ -3551,12 +3595,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@53" + "$ref": "#/rules@54" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@66" + "$ref": "#/rules@67" }, "arguments": [] }, @@ -3573,7 +3617,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "?" @@ -3607,12 +3651,12 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "CrossReference", "type": { - "$ref": "#/rules@53" + "$ref": "#/rules@54" }, "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@65" + "$ref": "#/rules@66" }, "arguments": [] }, @@ -3629,7 +3673,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "RuleCall", "rule": { - "$ref": "#/rules@59" + "$ref": "#/rules@60" }, "arguments": [], "cardinality": "?" @@ -3664,7 +3708,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@61" }, "arguments": [] } @@ -3683,7 +3727,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@60" + "$ref": "#/rules@61" }, "arguments": [] } @@ -3715,7 +3759,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "terminal": { "$type": "RuleCall", "rule": { - "$ref": "#/rules@51" + "$ref": "#/rules@52" }, "arguments": [] } @@ -4011,19 +4055,25 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@47" + "$ref": "#/rules@48" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@45" + "$ref": "#/rules@46" + } + }, + { + "$type": "SimpleType", + "typeRef": { + "$ref": "#/rules@30" } } ] @@ -4035,7 +4085,7 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel "type": { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@40" + "$ref": "#/rules@41" } } }, @@ -4048,19 +4098,19 @@ export const ZModelGrammar = (): Grammar => loadedZModelGrammar ?? (loadedZModel { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@37" + "$ref": "#/rules@38" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@42" + "$ref": "#/rules@43" } }, { "$type": "SimpleType", "typeRef": { - "$ref": "#/rules@44" + "$ref": "#/rules@45" } } ] diff --git a/packages/language/src/validators/attribute-application-validator.ts b/packages/language/src/validators/attribute-application-validator.ts index 79db61cce..28983e822 100644 --- a/packages/language/src/validators/attribute-application-validator.ts +++ b/packages/language/src/validators/attribute-application-validator.ts @@ -11,7 +11,6 @@ import { DataFieldAttribute, DataModelAttribute, InternalAttribute, - ReferenceExpr, isArrayExpr, isAttribute, isConfigArrayExpr, @@ -554,9 +553,16 @@ function isValidAttributeTarget(attrDecl: Attribute, targetDecl: DataField) { return true; } - const fieldTypes = (targetField.args[0].value as ArrayExpr).items.map( - (item) => (item as ReferenceExpr).target.ref?.name, - ); + const fieldTypes = (targetField.args[0].value as ArrayExpr).items + .map((item) => { + if (!isReferenceExpr(item)) { + return undefined; + } + + const ref = item.target.ref; + return ref && 'name' in ref && typeof ref.name === 'string' ? ref.name : undefined; + }) + .filter((name): name is string => !!name); let allowed = false; for (const allowedType of fieldTypes) { diff --git a/packages/language/src/validators/expression-validator.ts b/packages/language/src/validators/expression-validator.ts index c2848c145..3efe6d916 100644 --- a/packages/language/src/validators/expression-validator.ts +++ b/packages/language/src/validators/expression-validator.ts @@ -3,6 +3,7 @@ import { BinaryExpr, Expression, isArrayExpr, + isCollectionPredicateBinding, isDataModel, isDataModelAttribute, isEnum, @@ -12,6 +13,7 @@ import { isReferenceExpr, isThisExpr, MemberAccessExpr, + ReferenceExpr, UnaryExpr, type ExpressionType, } from '../generated/ast'; @@ -51,7 +53,7 @@ export default class ExpressionValidator implements AstValidator { } return false; }); - if (!hasReferenceResolutionError) { + if (hasReferenceResolutionError) { // report silent errors not involving linker errors accept('error', 'Expression cannot be resolved', { node: expr, @@ -62,6 +64,9 @@ export default class ExpressionValidator implements AstValidator { // extra validations by expression type switch (expr.$type) { + case 'ReferenceExpr': + this.validateReferenceExpr(expr, accept); + break; case 'MemberAccessExpr': this.validateMemberAccessExpr(expr, accept); break; @@ -74,6 +79,16 @@ export default class ExpressionValidator implements AstValidator { } } + private validateReferenceExpr(expr: ReferenceExpr, accept: ValidationAcceptor) { + // reference to collection predicate's binding can't be used standalone like: + // `items?[e, e]`, `items?[e, e != null]`, etc. + if (isCollectionPredicateBinding(expr.target.ref) && !isMemberAccessExpr(expr.$container)) { + accept('error', 'Collection predicate binding cannot be used without a member access', { + node: expr, + }); + } + } + private validateMemberAccessExpr(expr: MemberAccessExpr, accept: ValidationAcceptor) { if (isBeforeInvocation(expr.operand) && isDataModel(expr.$resolvedType?.decl)) { accept('error', 'relation fields cannot be accessed from `before()`', { node: expr }); diff --git a/packages/language/src/zmodel-code-generator.ts b/packages/language/src/zmodel-code-generator.ts index 55efb5fc4..1e0366ede 100644 --- a/packages/language/src/zmodel-code-generator.ts +++ b/packages/language/src/zmodel-code-generator.ts @@ -252,13 +252,15 @@ ${ast.fields.map((x) => this.indent + this.generate(x)).join('\n')}${ const { left: isLeftParenthesis, right: isRightParenthesis } = this.isParenthesesNeededForBinaryExpr(ast); + const collectionPredicate = isCollectionPredicate + ? `[${ast.binding ? `${ast.binding}, ${rightExpr}` : rightExpr}]` + : rightExpr; + return `${isLeftParenthesis ? '(' : ''}${this.generate(ast.left)}${ isLeftParenthesis ? ')' : '' }${isCollectionPredicate ? '' : this.binaryExprSpace}${operator}${ isCollectionPredicate ? '' : this.binaryExprSpace - }${isRightParenthesis ? '(' : ''}${ - isCollectionPredicate ? `[${rightExpr}]` : rightExpr - }${isRightParenthesis ? ')' : ''}`; + }${isRightParenthesis ? '(' : ''}${collectionPredicate}${isRightParenthesis ? ')' : ''}`; } @gen(ReferenceExpr) diff --git a/packages/language/src/zmodel-linker.ts b/packages/language/src/zmodel-linker.ts index 3bb451340..fc3a7f0dd 100644 --- a/packages/language/src/zmodel-linker.ts +++ b/packages/language/src/zmodel-linker.ts @@ -24,10 +24,8 @@ import { DataFieldType, DataModel, Enum, - EnumField, type ExpressionType, FunctionDecl, - FunctionParam, FunctionParamType, InvocationExpr, LiteralExpr, @@ -43,10 +41,13 @@ import { UnaryExpr, isArrayExpr, isBooleanLiteral, + isCollectionPredicateBinding, isDataField, isDataFieldType, isDataModel, isEnum, + isEnumField, + isFunctionParam, isNumberLiteral, isReferenceExpr, isStringLiteral, @@ -249,13 +250,21 @@ export class ZModelLinker extends DefaultLinker { private resolveReference(node: ReferenceExpr, document: LangiumDocument, extraScopes: ScopeProvider[]) { this.resolveDefault(node, document, extraScopes); - - if (node.target.ref) { - // resolve type - if (node.target.ref.$type === EnumField) { - this.resolveToBuiltinTypeOrDecl(node, node.target.ref.$container); - } else { - this.resolveToDeclaredType(node, (node.target.ref as DataField | FunctionParam).type); + const target = node.target.ref; + + if (target) { + if (isCollectionPredicateBinding(target)) { + // collection predicate's binding is resolved to the element type of the collection + const collectionType = target.$container.left.$resolvedType; + if (collectionType) { + node.$resolvedType = { ...collectionType, array: false }; + } + } else if (isEnumField(target)) { + // enum field is resolved to its containing enum + this.resolveToBuiltinTypeOrDecl(node, target.$container); + } else if (isDataField(target) || isFunctionParam(target)) { + // other references are resolved to their declared type + this.resolveToDeclaredType(node, target.type); } } } @@ -506,6 +515,9 @@ export class ZModelLinker extends DefaultLinker { //#region Utils private resolveToDeclaredType(node: AstNode, type: FunctionParamType | DataFieldType) { + if (!type) { + return; + } let nullable = false; if (isDataFieldType(type)) { nullable = type.optional; diff --git a/packages/language/src/zmodel-scope.ts b/packages/language/src/zmodel-scope.ts index 6fd866f05..30b77e293 100644 --- a/packages/language/src/zmodel-scope.ts +++ b/packages/language/src/zmodel-scope.ts @@ -18,7 +18,9 @@ import { import { match } from 'ts-pattern'; import { BinaryExpr, + Expression, MemberAccessExpr, + isCollectionPredicateBinding, isDataField, isDataModel, isEnumField, @@ -124,7 +126,7 @@ export class ZModelScopeProvider extends DefaultScopeProvider { // when reference expression is resolved inside a collection predicate, the scope is the collection const containerCollectionPredicate = getCollectionPredicateContext(context.container); if (containerCollectionPredicate) { - return this.getCollectionPredicateScope(context, containerCollectionPredicate as BinaryExpr); + return this.getCollectionPredicateScope(context, containerCollectionPredicate); } } @@ -145,10 +147,20 @@ export class ZModelScopeProvider extends DefaultScopeProvider { .when(isReferenceExpr, (operand) => { // operand is a reference, it can only be a model/type-def field const ref = operand.target.ref; - if (isDataField(ref)) { - return this.createScopeForContainer(ref.type.reference?.ref, globalScope, allowTypeDefScope); - } - return EMPTY_SCOPE; + return match(ref) + .when(isDataField, (r) => + // build a scope with model/typedef members + this.createScopeForContainer(r.type.reference?.ref, globalScope, allowTypeDefScope), + ) + .when(isCollectionPredicateBinding, (r) => + // build a scope from the collection predicate's collection + this.createScopeForCollectionPredicateCollection( + r.$container.left, + globalScope, + allowTypeDefScope, + ), + ) + .otherwise(() => EMPTY_SCOPE); }) .when(isMemberAccessExpr, (operand) => { // operand is a member access, it must be resolved to a non-array model/typedef type @@ -179,15 +191,44 @@ export class ZModelScopeProvider extends DefaultScopeProvider { .otherwise(() => EMPTY_SCOPE); } - private getCollectionPredicateScope(context: ReferenceInfo, collectionPredicate: BinaryExpr) { - const referenceType = this.reflection.getReferenceType(context); - const globalScope = this.getGlobalScope(referenceType, context); + private getCollectionPredicateScope(context: ReferenceInfo, collectionPredicate: BinaryExpr): Scope { + // walk up to collect all collection predicate bindings, which are all available in the scope + let currPredicate: BinaryExpr | undefined = collectionPredicate; + const bindingStack: AstNode[] = []; + while (currPredicate) { + if (currPredicate.binding) { + bindingStack.unshift(currPredicate.binding); + } + currPredicate = AstUtils.getContainerOfType(currPredicate.$container, isCollectionPredicate); + } + + // build a scope chain: global scope -> bindings' scope -> collection scope + const globalScope = this.getGlobalScope(this.reflection.getReferenceType(context), context); + const parentScope = bindingStack.reduce( + (scope, binding) => this.createScopeForNodes([binding], scope), + globalScope, + ); + const collection = collectionPredicate.left; // TODO: full support of typedef member access - // // typedef's fields are only added to the scope if the access starts with `auth().` + // typedef's fields are only added to the scope if the access starts with `auth().` const allowTypeDefScope = isAuthOrAuthMemberAccess(collection); + const collectionScope = this.createScopeForCollectionPredicateCollection( + collection, + parentScope, + allowTypeDefScope, + ); + + return collectionScope; + } + + private createScopeForCollectionPredicateCollection( + collection: Expression, + globalScope: Scope, + allowTypeDefScope: boolean, + ) { return match(collection) .when(isReferenceExpr, (expr) => { // collection is a reference - model or typedef field diff --git a/packages/language/src/zmodel.langium b/packages/language/src/zmodel.langium index 9f09dbf89..e7c33ad2a 100644 --- a/packages/language/src/zmodel.langium +++ b/packages/language/src/zmodel.langium @@ -66,7 +66,7 @@ ConfigArrayExpr: ConfigExpr: LiteralExpr | InvocationExpr | ConfigArrayExpr; -type ReferenceTarget = FunctionParam | DataField | EnumField; +type ReferenceTarget = FunctionParam | DataField | EnumField | CollectionPredicateBinding; ThisExpr: value='this'; @@ -109,13 +109,17 @@ UnaryExpr: // binary operator precedence follow Javascript's rules: // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence#table +// TODO: promote CollectionPredicateExpr to a first-class expression type CollectionPredicateExpr infers Expression: MemberAccessExpr ( {infer BinaryExpr.left=current} operator=('?'|'!'|'^') - '[' right=Expression ']' + '[' (binding=CollectionPredicateBinding ',')? right=Expression ']' )*; +CollectionPredicateBinding: + name=RegularID; + InExpr infers Expression: CollectionPredicateExpr ( {infer BinaryExpr.left=current} diff --git a/packages/language/test/expression-validation.test.ts b/packages/language/test/expression-validation.test.ts index 100f02b2c..40bad7712 100644 --- a/packages/language/test/expression-validation.test.ts +++ b/packages/language/test/expression-validation.test.ts @@ -2,9 +2,10 @@ import { describe, it } from 'vitest'; import { loadSchema, loadSchemaWithError } from './utils'; describe('Expression Validation Tests', () => { - it('should reject model comparison1', async () => { - await loadSchemaWithError( - ` + describe('Model Comparison Tests', () => { + it('should reject model comparison1', async () => { + await loadSchemaWithError( + ` model User { id Int @id name String @@ -19,13 +20,13 @@ describe('Expression Validation Tests', () => { @@allow('all', author == this) } `, - 'comparison between models is not supported', - ); - }); + 'comparison between models is not supported', + ); + }); - it('should reject model comparison2', async () => { - await loadSchemaWithError( - ` + it('should reject model comparison2', async () => { + await loadSchemaWithError( + ` model User { id Int @id name String @@ -48,13 +49,13 @@ describe('Expression Validation Tests', () => { userId Int @unique } `, - 'comparison between models is not supported', - ); - }); + 'comparison between models is not supported', + ); + }); - it('should allow auth comparison with auth type', async () => { - await loadSchema( - ` + it('should allow auth comparison with auth type', async () => { + await loadSchema( + ` datasource db { provider = 'sqlite' url = 'file:./dev.db' @@ -75,12 +76,12 @@ describe('Expression Validation Tests', () => { @@allow('read', auth() == user) } `, - ); - }); + ); + }); - it('should reject auth comparison with non-auth type', async () => { - await loadSchemaWithError( - ` + it('should reject auth comparison with non-auth type', async () => { + await loadSchemaWithError( + ` model User { id Int @id name String @@ -95,7 +96,265 @@ describe('Expression Validation Tests', () => { @@allow('read', auth() == this) } `, - 'incompatible operand types', - ); + 'incompatible operand types', + ); + }); + }); + + describe('Collection Predicate Tests', () => { + it('should reject standalone binding access', async () => { + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + @@allow('read', memberships?[m, m != null]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + } + `, + 'binding cannot be used without a member access', + ); + }); + + it('should allow referencing binding', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + @@allow('read', memberships?[m, m.tenantId == id]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + } + `); + }); + + it('should keep supporting unbound collection predicate syntax', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + @@allow('read', memberships?[tenantId == id]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + } + `); + }); + + it('should support mixing bound and unbound syntax', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + @@allow('read', memberships?[m, m.tenantId == id && tenantId == id]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + } + `); + }); + + it('should allow disambiguation with this', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + value Int + @@allow('read', memberships?[m, m.value == this.value]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + value Int + } + `); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + value String + @@allow('read', memberships?[m, m.value == this.value]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + value Int + } + `, + 'incompatible operand types', + ); + + await loadSchemaWithError( + ` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + value String + @@allow('read', memberships?[value == this.value]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + value Int + } + `, + 'incompatible operand types', + ); + }); + + it('should support accessing binding from deep context', async () => { + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + @@allow('read', memberships?[m, roles?[value == m.value]]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + value Int + roles Role[] + } + + model Role { + id Int @id + membership Membership @relation(fields: [membershipId], references: [id]) + membershipId Int + value Int + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + @@allow('read', memberships?[m, roles?[r, r.value == m.value]]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + value Int + roles Role[] + } + + model Role { + id Int @id + membership Membership @relation(fields: [membershipId], references: [id]) + membershipId Int + value Int + } + `); + + await loadSchema(` + datasource db { + provider = 'sqlite' + url = 'file:./dev.db' + } + + model User { + id Int @id + memberships Membership[] + x Int + @@allow('read', memberships?[m, roles?[this.x == m.value]]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + value Int + roles Role[] + } + + model Role { + id Int @id + membership Membership @relation(fields: [membershipId], references: [id]) + membershipId Int + value Int + } + `); + }); }); }); diff --git a/packages/orm/src/client/crud/validator/utils.ts b/packages/orm/src/client/crud/validator/utils.ts index bbd35900e..c9909a702 100644 --- a/packages/orm/src/client/crud/validator/utils.ts +++ b/packages/orm/src/client/crud/validator/utils.ts @@ -310,6 +310,9 @@ function evalExpression(data: any, expr: Expression): unknown { .with({ kind: 'call' }, (e) => evalCall(data, e)) .with({ kind: 'this' }, () => data ?? null) .with({ kind: 'null' }, () => null) + .with({ kind: 'binding' }, () => { + throw new Error('Binding expression is not supported in validation expressions'); + }) .exhaustive(); } diff --git a/packages/orm/src/utils/schema-utils.ts b/packages/orm/src/utils/schema-utils.ts index b928b0782..17fb31495 100644 --- a/packages/orm/src/utils/schema-utils.ts +++ b/packages/orm/src/utils/schema-utils.ts @@ -2,6 +2,7 @@ import { match } from 'ts-pattern'; import type { ArrayExpression, BinaryExpression, + BindingExpression, CallExpression, Expression, FieldExpression, @@ -24,6 +25,7 @@ export class ExpressionVisitor { .with({ kind: 'binary' }, (e) => this.visitBinary(e)) .with({ kind: 'unary' }, (e) => this.visitUnary(e)) .with({ kind: 'call' }, (e) => this.visitCall(e)) + .with({ kind: 'binding' }, (e) => this.visitBinding(e)) .with({ kind: 'this' }, (e) => this.visitThis(e)) .with({ kind: 'null' }, (e) => this.visitNull(e)) .exhaustive(); @@ -68,6 +70,8 @@ export class ExpressionVisitor { } } + protected visitBinding(_e: BindingExpression): VisitResult {} + protected visitThis(_e: ThisExpression): VisitResult {} protected visitNull(_e: NullExpression): VisitResult {} diff --git a/packages/plugins/policy/src/expression-evaluator.ts b/packages/plugins/policy/src/expression-evaluator.ts index 45c7b8554..85e97a03e 100644 --- a/packages/plugins/policy/src/expression-evaluator.ts +++ b/packages/plugins/policy/src/expression-evaluator.ts @@ -1,9 +1,9 @@ import { invariant } from '@zenstackhq/common-helpers'; -import { match } from 'ts-pattern'; import { ExpressionUtils, type ArrayExpression, type BinaryExpression, + type BindingExpression, type CallExpression, type Expression, type FieldExpression, @@ -11,10 +11,13 @@ import { type MemberExpression, type UnaryExpression, } from '@zenstackhq/orm/schema'; +import { match } from 'ts-pattern'; type ExpressionEvaluatorContext = { auth?: any; thisValue?: any; + // scope for resolving references to collection predicate bindings + bindingScope?: Record; }; /** @@ -30,6 +33,7 @@ export class ExpressionEvaluator { .when(ExpressionUtils.isMember, (expr) => this.evaluateMember(expr, context)) .when(ExpressionUtils.isUnary, (expr) => this.evaluateUnary(expr, context)) .when(ExpressionUtils.isCall, (expr) => this.evaluateCall(expr, context)) + .when(ExpressionUtils.isBinding, (expr) => this.evaluateBinding(expr, context)) .when(ExpressionUtils.isThis, () => context.thisValue) .when(ExpressionUtils.isNull, () => null) .exhaustive(); @@ -64,6 +68,9 @@ export class ExpressionEvaluator { } private evaluateField(expr: FieldExpression, context: ExpressionEvaluatorContext): any { + if (context.bindingScope && expr.field in context.bindingScope) { + return context.bindingScope[expr.field]; + } return context.thisValue?.[expr.field]; } @@ -113,8 +120,28 @@ export class ExpressionEvaluator { invariant(Array.isArray(left), 'expected array'); return match(op) - .with('?', () => left.some((item: any) => this.evaluate(expr.right, { ...context, thisValue: item }))) - .with('!', () => left.every((item: any) => this.evaluate(expr.right, { ...context, thisValue: item }))) + .with('?', () => + left.some((item: any) => + this.evaluate(expr.right, { + ...context, + thisValue: item, + bindingScope: expr.binding + ? { ...(context.bindingScope ?? {}), [expr.binding]: item } + : context.bindingScope, + }), + ), + ) + .with('!', () => + left.every((item: any) => + this.evaluate(expr.right, { + ...context, + thisValue: item, + bindingScope: expr.binding + ? { ...(context.bindingScope ?? {}), [expr.binding]: item } + : context.bindingScope, + }), + ), + ) .with( '^', () => @@ -122,9 +149,19 @@ export class ExpressionEvaluator { this.evaluate(expr.right, { ...context, thisValue: item, + bindingScope: expr.binding + ? { ...(context.bindingScope ?? {}), [expr.binding]: item } + : context.bindingScope, }), ), ) .exhaustive(); } + + private evaluateBinding(expr: BindingExpression, context: ExpressionEvaluatorContext): any { + if (!context.bindingScope || !(expr.name in context.bindingScope)) { + throw new Error(`Unresolved binding: ${expr.name}`); + } + return context.bindingScope[expr.name]; + } } diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index 18320c69d..7977ccb28 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -11,6 +11,7 @@ import { import type { BinaryExpression, BinaryOperator, + BindingExpression, BuiltinType, FieldDef, GetModels, @@ -59,6 +60,8 @@ import { trueNode, } from './utils'; +type BindingScope = Record; + /** * Context for transforming a policy expression */ @@ -93,6 +96,11 @@ export type ExpressionTransformerContext = { */ contextValue?: Record; + /** + * Additional named collection predicate bindings available during transformation + */ + bindingScope?: BindingScope; + /** * The model or type name that `this` keyword refers to */ @@ -311,7 +319,11 @@ export class ExpressionTransformer { // LHS of the expression is evaluated as a value const evaluator = new ExpressionEvaluator(); - const receiver = evaluator.evaluate(expr.left, { thisValue: context.contextValue, auth: this.auth }); + const receiver = evaluator.evaluate(expr.left, { + thisValue: context.contextValue, + auth: this.auth, + bindingScope: this.getEvaluationBindingScope(context.bindingScope), + }); // get LHS's type const baseType = this.isAuthMember(expr.left) ? this.authType : context.modelOrType; @@ -335,21 +347,38 @@ export class ExpressionTransformer { newContextModel = fieldDef.type; } else { invariant( - ExpressionUtils.isMember(expr.left) && ExpressionUtils.isField(expr.left.receiver), + ExpressionUtils.isMember(expr.left) && + (ExpressionUtils.isField(expr.left.receiver) || ExpressionUtils.isBinding(expr.left.receiver)), 'left operand must be member access with field receiver', ); - const fieldDef = QueryUtils.requireField(this.schema, context.modelOrType, expr.left.receiver.field); - newContextModel = fieldDef.type; + if (ExpressionUtils.isField(expr.left.receiver)) { + // collection is a field access, context model is the field's type + const fieldDef = QueryUtils.requireField(this.schema, context.modelOrType, expr.left.receiver.field); + newContextModel = fieldDef.type; + } else { + // collection is a binding reference, get type from binding scope + const binding = this.requireBindingScope(expr.left.receiver, context); + newContextModel = binding.type; + } + for (const member of expr.left.members) { const memberDef = QueryUtils.requireField(this.schema, newContextModel, member); newContextModel = memberDef.type; } } + const bindingScope = expr.binding + ? { + ...(context.bindingScope ?? {}), + [expr.binding]: { type: newContextModel, alias: newContextModel }, + } + : context.bindingScope; + let predicateFilter = this.transform(expr.right, { ...context, modelOrType: newContextModel, alias: undefined, + bindingScope: bindingScope, }); if (expr.op === '!') { @@ -392,6 +421,7 @@ export class ExpressionTransformer { const value = new ExpressionEvaluator().evaluate(expr, { auth: this.auth, thisValue: context.contextValue, + bindingScope: this.getEvaluationBindingScope(context.bindingScope), }); return this.transformValue(value, 'Boolean'); } else { @@ -403,15 +433,27 @@ export class ExpressionTransformer { // e.g.: `auth().profiles[age == this.age]`, each `auth().profiles` element (which is a value) // is used to build an expression for the RHS `age == this.age` // the transformation happens recursively for nested collection predicates - const components = receiver.map((item) => - this.transform(expr.right, { + const components = receiver.map((item) => { + const bindingScope = expr.binding + ? { + ...(context.bindingScope ?? {}), + [expr.binding]: { + type: context.modelOrType, + alias: context.thisAlias ?? context.modelOrType, + value: item, + }, + } + : context.bindingScope; + + return this.transform(expr.right, { operation: context.operation, thisType: context.thisType, thisAlias: context.thisAlias, modelOrType: context.modelOrType, contextValue: item, - }), - ); + bindingScope: bindingScope, + }); + }); // compose the components based on the operator return ( @@ -601,6 +643,14 @@ export class ExpressionTransformer { @expr('member') // @ts-ignore private _member(expr: MemberExpression, context: ExpressionTransformerContext) { + if (ExpressionUtils.isBinding(expr.receiver)) { + // if the binding has a plain value in the scope, evaluate directly + const scope = this.requireBindingScope(expr.receiver, context); + if (scope.value !== undefined) { + return this.valueMemberAccess(scope.value, expr, scope.type); + } + } + // `auth()` member access if (this.isAuthCall(expr.receiver)) { return this.valueMemberAccess(this.auth, expr, this.authType); @@ -616,12 +666,15 @@ export class ExpressionTransformer { } invariant( - ExpressionUtils.isField(expr.receiver) || ExpressionUtils.isThis(expr.receiver), - 'expect receiver to be field expression or "this"', + ExpressionUtils.isField(expr.receiver) || + ExpressionUtils.isThis(expr.receiver) || + ExpressionUtils.isBinding(expr.receiver), + 'expect receiver to be field expression, collection predicate binding, or "this"', ); let members = expr.members; let receiver: OperationNode; + let startType: string | undefined; const { memberFilter, memberSelect, ...restContext } = context; if (ExpressionUtils.isThis(expr.receiver)) { @@ -639,6 +692,32 @@ export class ExpressionTransformer { const firstMemberFieldDef = QueryUtils.requireField(this.schema, context.thisType, expr.members[0]!); receiver = this.transformRelationAccess(expr.members[0]!, firstMemberFieldDef.type, restContext); members = expr.members.slice(1); + // startType should be the type of the relation access + startType = firstMemberFieldDef.type; + } + } else if (ExpressionUtils.isBinding(expr.receiver)) { + if (expr.members.length === 1) { + const bindingScope = this.requireBindingScope(expr.receiver, context); + // `binding.relation` case, equivalent to field access + return this._field(ExpressionUtils.field(expr.members[0]!), { + ...context, + modelOrType: bindingScope.type, + alias: bindingScope.alias, + thisType: context.thisType, + contextValue: undefined, + }); + } else { + // transform the first segment into a relation access, then continue with the rest of the members + const bindingScope = this.requireBindingScope(expr.receiver, context); + const firstMemberFieldDef = QueryUtils.requireField(this.schema, bindingScope.type, expr.members[0]!); + receiver = this.transformRelationAccess(expr.members[0]!, firstMemberFieldDef.type, { + ...restContext, + modelOrType: bindingScope.type, + alias: bindingScope.alias, + }); + members = expr.members.slice(1); + // startType should be the type of the relation access + startType = firstMemberFieldDef.type; } } else { receiver = this.transform(expr.receiver, restContext); @@ -646,13 +725,14 @@ export class ExpressionTransformer { invariant(SelectQueryNode.is(receiver), 'expected receiver to be select query'); - let startType: string; - if (ExpressionUtils.isField(expr.receiver)) { - const receiverField = QueryUtils.requireField(this.schema, context.modelOrType, expr.receiver.field); - startType = receiverField.type; - } else { - // "this." case - startType = context.thisType; + if (startType === undefined) { + if (ExpressionUtils.isField(expr.receiver)) { + const receiverField = QueryUtils.requireField(this.schema, context.modelOrType, expr.receiver.field); + startType = receiverField.type; + } else { + // "this." case - already handled above if members were sliced + startType = context.thisType; + } } // traverse forward to collect member types @@ -706,6 +786,12 @@ export class ExpressionTransformer { }; } + private requireBindingScope(expr: BindingExpression, context: ExpressionTransformerContext) { + const binding = context.bindingScope?.[expr.name]; + invariant(binding, `binding not found: ${expr.name}`); + return binding; + } + private valueMemberAccess(receiver: any, expr: MemberExpression, receiverType: string) { if (!receiver) { return ValueNode.createImmediate(null); @@ -834,6 +920,22 @@ export class ExpressionTransformer { return this.buildDelegateBaseFieldSelect(context.modelOrType, tableName, column, fieldDef.originModel); } + // convert transformer's binding scope to equivalent expression evaluator binding scope + private getEvaluationBindingScope(scope?: BindingScope) { + if (!scope) { + return undefined; + } + + const result: Record = {}; + for (const [key, value] of Object.entries(scope)) { + if (value.value !== undefined) { + result[key] = value.value; + } + } + + return Object.keys(result).length > 0 ? result : undefined; + } + private buildDelegateBaseFieldSelect(model: string, modelAlias: string, field: string, baseModel: string) { const idFields = QueryUtils.requireIdFields(this.client.$schema, model); return { diff --git a/packages/schema/src/expression-utils.ts b/packages/schema/src/expression-utils.ts index ee48aeccd..07ee1c115 100644 --- a/packages/schema/src/expression-utils.ts +++ b/packages/schema/src/expression-utils.ts @@ -2,6 +2,7 @@ import type { ArrayExpression, BinaryExpression, BinaryOperator, + BindingExpression, CallExpression, Expression, FieldExpression, @@ -39,12 +40,13 @@ export const ExpressionUtils = { }; }, - binary: (left: Expression, op: BinaryOperator, right: Expression): BinaryExpression => { + binary: (left: Expression, op: BinaryOperator, right: Expression, binding?: string): BinaryExpression => { return { kind: 'binary', op, left, right, + binding, }; }, @@ -71,6 +73,13 @@ export const ExpressionUtils = { }; }, + binding: (name: string): BindingExpression => { + return { + kind: 'binding', + name, + }; + }, + _this: (): ThisExpression => { return { kind: 'this', @@ -117,6 +126,8 @@ export const ExpressionUtils = { isMember: (value: unknown): value is MemberExpression => ExpressionUtils.is(value, 'member'), + isBinding: (value: unknown): value is BindingExpression => ExpressionUtils.is(value, 'binding'), + getLiteralValue: (expr: Expression): string | number | boolean | undefined => { return ExpressionUtils.isLiteral(expr) ? expr.value : undefined; }, diff --git a/packages/schema/src/expression.ts b/packages/schema/src/expression.ts index 3ce3c2d12..1828b9cc3 100644 --- a/packages/schema/src/expression.ts +++ b/packages/schema/src/expression.ts @@ -6,6 +6,7 @@ export type Expression = | CallExpression | UnaryExpression | BinaryExpression + | BindingExpression | ThisExpression | NullExpression; @@ -30,6 +31,11 @@ export type MemberExpression = { members: string[]; }; +export type BindingExpression = { + kind: 'binding'; + name: string; +}; + export type UnaryExpression = { kind: 'unary'; op: UnaryOperator; @@ -41,6 +47,7 @@ export type BinaryExpression = { op: BinaryOperator; left: Expression; right: Expression; + binding?: string; }; export type CallExpression = { diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 6baeff425..4a5e0a548 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -13,6 +13,7 @@ import { InvocationExpr, isArrayExpr, isBinaryExpr, + isCollectionPredicateBinding, isDataField, isDataModel, isDataSource, @@ -1257,11 +1258,17 @@ export class TsSchemaGenerator { } private createBinaryExpression(expr: BinaryExpr) { - return this.createExpressionUtilsCall('binary', [ + const args = [ this.createExpression(expr.left), this.createLiteralNode(expr.operator), this.createExpression(expr.right), - ]); + ]; + + if (expr.binding) { + args.push(this.createLiteralNode(expr.binding.name)); + } + + return this.createExpressionUtilsCall('binary', args); } private createUnaryExpression(expr: UnaryExpr) { @@ -1278,13 +1285,18 @@ export class TsSchemaGenerator { } private createRefExpression(expr: ReferenceExpr): any { - if (isDataField(expr.target.ref)) { - return this.createExpressionUtilsCall('field', [this.createLiteralNode(expr.target.$refText)]); - } else if (isEnumField(expr.target.ref)) { - return this.createLiteralExpression('StringLiteral', expr.target.$refText); - } else { - throw new Error(`Unsupported reference type: ${expr.target.$refText}`); - } + const target = expr.target.ref; + return match(target) + .when(isDataField, () => + this.createExpressionUtilsCall('field', [this.createLiteralNode(expr.target.$refText)]), + ) + .when(isEnumField, () => this.createLiteralExpression('StringLiteral', expr.target.$refText)) + .when(isCollectionPredicateBinding, () => + this.createExpressionUtilsCall('binding', [this.createLiteralNode(expr.target.$refText)]), + ) + .otherwise(() => { + throw Error(`Unsupported reference type: ${expr.target.$refText}`); + }); } private createCallExpression(expr: InvocationExpr) { diff --git a/tests/e2e/orm/policy/auth-access.test.ts b/tests/e2e/orm/policy/auth-access.test.ts index b994324f0..56942de49 100644 --- a/tests/e2e/orm/policy/auth-access.test.ts +++ b/tests/e2e/orm/policy/auth-access.test.ts @@ -130,6 +130,48 @@ model Foo { await expect(db.$setAuth({ profiles: [{ age: 15 }, { age: 20 }] }).foo.findFirst()).toResolveTruthy(); }); + it('uses iterator binding inside collection predicate for auth model', async () => { + const db = await createPolicyTestClient( + ` +model User { + id Int @id + tenantId Int + memberships Membership[] @relation("UserMemberships") +} + +model Membership { + id Int @id + tenantId Int + userId Int + user User @relation("UserMemberships", fields: [userId], references: [id]) +} + +model Foo { + id Int @id + tenantId Int + @@allow('read', auth().memberships?[m, m.tenantId == auth().tenantId]) +} +`, + ); + + await db.$unuseAll().foo.createMany({ + data: [ + { id: 1, tenantId: 1 }, + { id: 2, tenantId: 2 }, + ], + }); + + // allowed because iterator binding matches tenantId = 1 + await expect( + db.$setAuth({ tenantId: 1, memberships: [{ id: 10, tenantId: 1 }] }).foo.findMany(), + ).toResolveWithLength(2); + + // denied because membership tenantId doesn't match + await expect( + db.$setAuth({ tenantId: 1, memberships: [{ id: 20, tenantId: 3 }] }).foo.findMany(), + ).toResolveWithLength(0); + }); + it('works with shallow auth model collection predicates involving fields - some', async () => { const db = await createPolicyTestClient( ` diff --git a/tests/e2e/orm/policy/collection-predicate.test.ts b/tests/e2e/orm/policy/collection-predicate.test.ts new file mode 100644 index 000000000..09e22228e --- /dev/null +++ b/tests/e2e/orm/policy/collection-predicate.test.ts @@ -0,0 +1,447 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Collection Predicate Tests', () => { + it('should support collection predicates without binding', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + @@allow('create', true) + @@allow('read', memberships?[tenantId == id]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { id: 1, memberships: { create: [{ id: 1, tenantId: 1 }] } }, + }); + await db.$unuseAll().user.create({ + data: { id: 2, memberships: { create: [{ id: 2, tenantId: 1 }] } }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should support referencing binding', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + @@allow('create', true) + @@allow('read', memberships?[m, m.tenantId == id]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { id: 1, memberships: { create: [{ id: 1, tenantId: 1 }] } }, + }); + await db.$unuseAll().user.create({ + data: { id: 2, memberships: { create: [{ id: 2, tenantId: 1 }] } }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should support mixing bound and unbound syntax', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + @@allow('create', true) + @@allow('read', memberships?[m, m.tenantId == id && tenantId == id]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { id: 1, memberships: { create: [{ id: 1, tenantId: 1 }] } }, + }); + await db.$unuseAll().user.create({ + data: { id: 2, memberships: { create: [{ id: 2, tenantId: 1 }] } }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should allow disambiguation with this', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + tenantId Int + @@allow('create', true) + @@allow('read', memberships?[m, m.tenantId == this.tenantId]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { id: 1, tenantId: 1, memberships: { create: [{ id: 1, tenantId: 1 }] } }, + }); + await db.$unuseAll().user.create({ + data: { id: 2, tenantId: 2, memberships: { create: [{ id: 2, tenantId: 1 }] } }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should support accessing binding from deep context - case 1', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + @@allow('create', true) + @@allow('read', memberships?[m, roles?[tenantId == m.tenantId]]) + } + + model Membership { + id Int @id + user User @relation(fields: [userId], references: [id]) + userId Int + tenantId Int + roles Role[] + @@allow('all', true) + } + + model Role { + id Int @id + membership Membership @relation(fields: [membershipId], references: [id]) + membershipId Int + tenantId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { + id: 1, + memberships: { create: [{ id: 1, tenantId: 1, roles: { create: { id: 1, tenantId: 1 } } }] }, + }, + }); + await db.$unuseAll().user.create({ + data: { + id: 2, + memberships: { create: [{ id: 2, tenantId: 2, roles: { create: { id: 2, tenantId: 1 } } }] }, + }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should support accessing binding from deep context - case 2', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + tenantId Int + @@allow('create', true) + @@allow('read', memberships?[m, roles?[this.tenantId == m.tenantId]]) + } + + model Membership { + id Int @id + user User @relation(fields: [userId], references: [id]) + userId Int + tenantId Int + roles Role[] + @@allow('all', true) + } + + model Role { + id Int @id + membership Membership @relation(fields: [membershipId], references: [id]) + membershipId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { + id: 1, + tenantId: 1, + memberships: { create: [{ id: 1, tenantId: 1, roles: { create: { id: 1 } } }] }, + }, + }); + await db.$unuseAll().user.create({ + data: { + id: 2, + tenantId: 2, + memberships: { create: [{ id: 2, tenantId: 1, roles: { create: { id: 2 } } }] }, + }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should support accessing to-one relation from binding', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + tenants Tenant[] + @@allow('create', true) + @@allow('read', memberships?[m, m.tenant.ownerId == id]) + } + + model Tenant { + id Int @id + ownerId Int + owner User @relation(fields: [ownerId], references: [id]) + memberships Membership[] + @@allow('all', true) + } + + model Membership { + id Int @id + tenant Tenant @relation(fields: [tenantId], references: [id]) + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { + id: 1, + memberships: { + create: [{ id: 1, tenant: { create: { id: 1, ownerId: 1 } } }], + }, + }, + }); + await db.$unuseAll().user.create({ + data: { + id: 2, + memberships: { + create: [{ id: 2, tenant: { create: { id: 2, ownerId: 1 } } }], + }, + }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should support multiple bindings in nested predicates', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + @@allow('create', true) + @@allow('read', memberships?[m, m.roles?[r, r.tenantId == m.tenantId]]) + } + + model Membership { + id Int @id + tenantId Int + user User @relation(fields: [userId], references: [id]) + userId Int + roles Role[] + @@allow('all', true) + } + + model Role { + id Int @id + tenantId Int + membership Membership @relation(fields: [membershipId], references: [id]) + membershipId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { + id: 1, + memberships: { + create: [{ id: 1, tenantId: 1, roles: { create: { id: 1, tenantId: 1 } } }], + }, + }, + }); + await db.$unuseAll().user.create({ + data: { + id: 2, + memberships: { + create: [{ id: 2, tenantId: 2, roles: { create: { id: 2, tenantId: 1 } } }], + }, + }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should work with inner binding masking outer binding names', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + memberships Membership[] + tenantId Int + @@allow('create', true) + @@allow('read', memberships?[m, m.roles?[m, m.tenantId == this.tenantId]]) + } + + model Membership { + id Int @id + user User @relation(fields: [userId], references: [id]) + userId Int + roles Role[] + @@allow('all', true) + } + + model Role { + id Int @id + tenantId Int + membership Membership @relation(fields: [membershipId], references: [id]) + membershipId Int + @@allow('all', true) + } +`, + ); + await db.$unuseAll().user.create({ + data: { + id: 1, + tenantId: 1, + memberships: { create: [{ id: 1, roles: { create: { id: 1, tenantId: 1 } } }] }, + }, + }); + await db.$unuseAll().user.create({ + data: { + id: 2, + tenantId: 2, + memberships: { create: [{ id: 2, roles: { create: { id: 2, tenantId: 1 } } }] }, + }, + }); + await expect(db.user.findUnique({ where: { id: 1 } })).toResolveTruthy(); + await expect(db.user.findUnique({ where: { id: 2 } })).toResolveNull(); + }); + + it('should work with bindings with auth collection predicates', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + companies Company[] + test Int + + @@allow('read', auth().companies?[c, c.staff?[s, s.companyId == this.test]]) + } + + model Company { + id Int @id + user User @relation(fields: [userId], references: [id]) + userId Int + + staff Staff[] + @@allow('read', true) + } + + model Staff { + id Int @id + + company Company @relation(fields: [companyId], references: [id]) + companyId Int + + @@allow('read', true) + } + `, + ); + await db.$unuseAll().user.create({ + data: { + id: 1, + test: 1, + companies: { create: { id: 1, staff: { create: { id: 1 } } } }, + }, + }); + + await expect( + db + .$setAuth({ id: 1, companies: [{ id: 1, staff: [{ id: 1, companyId: 1 }] }], test: 1 }) + .user.findUnique({ where: { id: 1 } }), + ).toResolveTruthy(); + }); + + it('should work with bindings with auth collection predicates - pure value', async () => { + const db = await createPolicyTestClient( + ` + model User { + id Int @id + companies Company[] + + @@allow('read', auth().companies?[c, c.staff?[s, s.companyId == c.id]]) + } + + model Company { + id Int @id + user User @relation(fields: [userId], references: [id]) + userId Int + + staff Staff[] + @@allow('read', true) + } + + model Staff { + id Int @id + + company Company @relation(fields: [companyId], references: [id]) + companyId Int + + @@allow('read', true) + } + `, + ); + await db.$unuseAll().user.create({ + data: { + id: 1, + companies: { create: { id: 1, staff: { create: { id: 1 } } } }, + }, + }); + + await expect( + db + .$setAuth({ id: 1, companies: [{ id: 1, staff: [{ id: 1, companyId: 1 }] }] }) + .user.findUnique({ where: { id: 1 } }), + ).toResolveTruthy(); + await expect( + db + .$setAuth({ id: 1, companies: [{ id: 1, staff: [{ id: 1, companyId: 2 }] }] }) + .user.findUnique({ where: { id: 1 } }), + ).toResolveNull(); + }); +}); From 94c5c8a1d5132dffe4257c04b55cacc5f3df924b Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Sun, 18 Jan 2026 21:13:14 +0800 Subject: [PATCH 06/25] feat(cli): ZenStack proxy (#597) * feat(cli): ZenStack proxy * add the missing change * resolve comments * feat(cli): add alias for proxy command and improve console messages * fix(cli): update output option description for ZenStack proxy command --- packages/cli/package.json | 11 +- packages/cli/src/actions/action-utils.ts | 12 ++ packages/cli/src/actions/generate.ts | 91 ++++++------ packages/cli/src/actions/index.ts | 3 +- packages/cli/src/actions/proxy.ts | 177 +++++++++++++++++++++++ packages/cli/src/index.ts | 21 ++- pnpm-lock.yaml | 58 +++++++- 7 files changed, 314 insertions(+), 59 deletions(-) create mode 100644 packages/cli/src/actions/proxy.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 401c80e24..9bfcc5f95 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -38,30 +38,37 @@ "dependencies": { "@zenstackhq/common-helpers": "workspace:*", "@zenstackhq/language": "workspace:*", + "@zenstackhq/orm": "workspace:*", "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/server": "workspace:*", + "better-sqlite3": "catalog:", "chokidar": "^5.0.0", "colors": "1.4.0", "commander": "^8.3.0", + "cors": "^2.8.5", "execa": "^9.6.0", + "express": "^5.0.0", "jiti": "^2.6.1", "langium": "catalog:", "mixpanel": "^0.18.1", "ora": "^5.4.1", "package-manager-detector": "^1.3.0", + "pg": "catalog:", "prisma": "catalog:", "semver": "^7.7.2", "ts-pattern": "catalog:" }, "devDependencies": { "@types/better-sqlite3": "catalog:", + "@types/cors": "^2.8.19", + "@types/express": "^5.0.0", + "@types/pg": "^8.16.0", "@types/semver": "^7.7.0", "@types/tmp": "catalog:", "@zenstackhq/eslint-config": "workspace:*", - "@zenstackhq/orm": "workspace:*", "@zenstackhq/testtools": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", - "better-sqlite3": "catalog:", "tmp": "catalog:" }, "engines": { diff --git a/packages/cli/src/actions/action-utils.ts b/packages/cli/src/actions/action-utils.ts index d3d0dacf7..d2e0ca2e9 100644 --- a/packages/cli/src/actions/action-utils.ts +++ b/packages/cli/src/actions/action-utils.ts @@ -144,3 +144,15 @@ export async function requireDataSourceUrl(schemaFile: string) { throw new CliError('The schema\'s "datasource" must have a "url" field to use this command.'); } } + +export function getOutputPath(options: { output?: string }, schemaFile: string) { + if (options.output) { + return options.output; + } + const pkgJsonConfig = getPkgJsonConfig(process.cwd()); + if (pkgJsonConfig.output) { + return pkgJsonConfig.output; + } else { + return path.dirname(schemaFile); + } +} diff --git a/packages/cli/src/actions/generate.ts b/packages/cli/src/actions/generate.ts index 16e3826c7..7ac6db6b2 100644 --- a/packages/cli/src/actions/generate.ts +++ b/packages/cli/src/actions/generate.ts @@ -12,7 +12,7 @@ import { watch } from 'chokidar'; import ora, { type Ora } from 'ora'; import { CliError } from '../cli-error'; import * as corePlugins from '../plugins'; -import { getPkgJsonConfig, getSchemaFile, loadSchemaDocument } from './action-utils'; +import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils'; type Options = { schema?: string; @@ -39,15 +39,16 @@ export async function run(options: Options) { const schemaExtensions = ZModelLanguageMetaData.fileExtensions; // Get real models file path (cuz its merged into single document -> we need use cst nodes) - const getRootModelWatchPaths = (model: Model) => new Set( - ( - model.declarations.filter( - (v) => - v.$cstNode?.parent?.element.$type === 'Model' && - !!v.$cstNode.parent.element.$document?.uri?.fsPath, - ) as AbstractDeclaration[] - ).map((v) => v.$cstNode!.parent!.element.$document!.uri!.fsPath), - ); + const getRootModelWatchPaths = (model: Model) => + new Set( + ( + model.declarations.filter( + (v) => + v.$cstNode?.parent?.element.$type === 'Model' && + !!v.$cstNode.parent.element.$document?.uri?.fsPath, + ) as AbstractDeclaration[] + ).map((v) => v.$cstNode!.parent!.element.$document!.uri!.fsPath), + ); const watchedPaths = getRootModelWatchPaths(model); @@ -64,40 +65,44 @@ export async function run(options: Options) { }); // prevent save multiple files and run multiple times - const reGenerateSchema = singleDebounce(async () => { - if (logsEnabled) { - console.log('Got changes, run generation!'); - } + const reGenerateSchema = singleDebounce( + async () => { + if (logsEnabled) { + console.log('Got changes, run generation!'); + } - try { - const newModel = await pureGenerate(options, true); - const allModelsPaths = getRootModelWatchPaths(newModel); - const newModelPaths = [...allModelsPaths].filter((at) => !watchedPaths.has(at)); - const removeModelPaths = [...watchedPaths].filter((at) => !allModelsPaths.has(at)); + try { + const newModel = await pureGenerate(options, true); + const allModelsPaths = getRootModelWatchPaths(newModel); + const newModelPaths = [...allModelsPaths].filter((at) => !watchedPaths.has(at)); + const removeModelPaths = [...watchedPaths].filter((at) => !allModelsPaths.has(at)); - if (newModelPaths.length) { - if (logsEnabled) { - const logPaths = newModelPaths.map((at) => `- ${at}`).join('\n'); - console.log(`Added file(s) to watch:\n${logPaths}`); + if (newModelPaths.length) { + if (logsEnabled) { + const logPaths = newModelPaths.map((at) => `- ${at}`).join('\n'); + console.log(`Added file(s) to watch:\n${logPaths}`); + } + + newModelPaths.forEach((at) => watchedPaths.add(at)); + watcher.add(newModelPaths); } - newModelPaths.forEach((at) => watchedPaths.add(at)); - watcher.add(newModelPaths); - } + if (removeModelPaths.length) { + if (logsEnabled) { + const logPaths = removeModelPaths.map((at) => `- ${at}`).join('\n'); + console.log(`Removed file(s) from watch:\n${logPaths}`); + } - if (removeModelPaths.length) { - if (logsEnabled) { - const logPaths = removeModelPaths.map((at) => `- ${at}`).join('\n'); - console.log(`Removed file(s) from watch:\n${logPaths}`); + removeModelPaths.forEach((at) => watchedPaths.delete(at)); + watcher.unwatch(removeModelPaths); } - - removeModelPaths.forEach((at) => watchedPaths.delete(at)); - watcher.unwatch(removeModelPaths); + } catch (e) { + console.error(e); } - } catch (e) { - console.error(e); - } - }, 500, true); + }, + 500, + true, + ); watcher.on('unlink', (pathAt) => { if (logsEnabled) { @@ -148,18 +153,6 @@ Check documentation: https://zenstack.dev/docs/`); return model; } -function getOutputPath(options: Options, schemaFile: string) { - if (options.output) { - return options.output; - } - const pkgJsonConfig = getPkgJsonConfig(process.cwd()); - if (pkgJsonConfig.output) { - return pkgJsonConfig.output; - } else { - return path.dirname(schemaFile); - } -} - async function runPlugins(schemaFile: string, model: Model, outputPath: string, options: Options) { const plugins = model.declarations.filter(isPlugin); const processedPlugins: { cliPlugin: CliPlugin; pluginOptions: Record }[] = []; diff --git a/packages/cli/src/actions/index.ts b/packages/cli/src/actions/index.ts index 88bce15c3..e421cdca0 100644 --- a/packages/cli/src/actions/index.ts +++ b/packages/cli/src/actions/index.ts @@ -6,5 +6,6 @@ import { run as info } from './info'; import { run as init } from './init'; import { run as migrate } from './migrate'; import { run as seed } from './seed'; +import { run as proxy } from './proxy'; -export { check, db, format, generate, info, init, migrate, seed }; +export { check, db, format, generate, info, init, migrate, seed, proxy }; diff --git a/packages/cli/src/actions/proxy.ts b/packages/cli/src/actions/proxy.ts new file mode 100644 index 000000000..45375b05d --- /dev/null +++ b/packages/cli/src/actions/proxy.ts @@ -0,0 +1,177 @@ +import { isDataSource } from '@zenstackhq/language/ast'; +import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils'; +import { CliError } from '../cli-error'; +import { ZModelCodeGenerator } from '@zenstackhq/language'; +import { getStringLiteral } from '@zenstackhq/language/utils'; +import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite'; +import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres'; +import SQLite from 'better-sqlite3'; +import { Pool } from 'pg'; +import path from 'node:path'; +import { ZenStackClient, type ClientContract } from '@zenstackhq/orm'; +import { RPCApiHandler } from '@zenstackhq/server/api'; +import { ZenStackMiddleware } from '@zenstackhq/server/express'; +import express from 'express'; +import colors from 'colors'; +import { createJiti } from 'jiti'; +import { getVersion } from '../utils/version-utils'; +import cors from 'cors'; + +type Options = { + output?: string; + schema?: string; + port?: number; + logLevel?: string[]; + databaseUrl?: string; +}; + +export async function run(options: Options) { + const schemaFile = getSchemaFile(options.schema); + console.log(colors.gray(`Loading ZModel schema from: ${schemaFile}`)); + + let outputPath = getOutputPath(options, schemaFile); + + // Ensure outputPath is absolute + if (!path.isAbsolute(outputPath)) { + outputPath = path.resolve(process.cwd(), outputPath); + } + + const model = await loadSchemaDocument(schemaFile); + + const dataSource = model.declarations.find(isDataSource); + + let databaseUrl = options.databaseUrl; + + if (!databaseUrl) { + const schemaUrl = dataSource?.fields.find((f) => f.name === 'url')?.value; + + if (!schemaUrl) { + throw new CliError( + `The schema's "datasource" does not have a "url" field, please provide it with -d option.`, + ); + } + const zModelGenerator = new ZModelCodeGenerator(); + const url = zModelGenerator.generate(schemaUrl); + + databaseUrl = evaluateUrl(url); + } + + const provider = getStringLiteral(dataSource?.fields.find((f) => f.name === 'provider')?.value)!; + + const dialect = createDialect(provider, databaseUrl!, outputPath); + + const jiti = createJiti(import.meta.url); + + const schemaModule = (await jiti.import(path.join(outputPath, 'schema'))) as any; + + const allowedLogLevels = ['error', 'query'] as const; + const log = options.logLevel?.filter((level): level is (typeof allowedLogLevels)[number] => + allowedLogLevels.includes(level as any), + ); + + const db = new ZenStackClient(schemaModule.schema, { + dialect: dialect, + log: log && log.length > 0 ? log : undefined, + }); + + // check whether the database is reachable + try { + await db.$connect(); + } catch (err) { + throw new CliError(`Failed to connect to the database: ${err instanceof Error ? err.message : String(err)}`); + } + + startServer(db, schemaModule.schema, options); +} + +function evaluateUrl(value: string): string { + // Create env helper function + const env = (varName: string) => { + const envValue = process.env[varName]; + if (!envValue) { + throw new CliError(`Environment variable ${varName} is not set`); + } + return envValue; + }; + + try { + // Use Function constructor to evaluate the url value + const urlFn = new Function('env', `return ${value}`); + const url = urlFn(env); + return url; + } catch (err) { + if (err instanceof CliError) { + throw err; + } + throw new CliError('Could not evaluate datasource url from schema, you could provide it via -d option.'); + } +} + +function createDialect(provider: string, databaseUrl: string, outputPath: string) { + switch (provider) { + case 'sqlite': { + let resolvedUrl = databaseUrl.trim(); + if (resolvedUrl.startsWith('file:')) { + const filePath = resolvedUrl.substring('file:'.length); + if (!path.isAbsolute(filePath)) { + resolvedUrl = path.join(outputPath, filePath); + } + } + console.log(colors.gray(`Connecting to SQLite database at: ${resolvedUrl}`)); + return new SqliteDialect({ + database: new SQLite(resolvedUrl), + }); + } + case 'postgresql': + console.log(colors.gray(`Connecting to PostgreSQL database at: ${databaseUrl}`)); + return new PostgresDialect({ + pool: new Pool({ + connectionString: databaseUrl, + }), + }); + default: + throw new CliError(`Unsupported database provider: ${provider}`); + } +} + +function startServer(client: ClientContract, schema: any, options: Options) { + const app = express(); + app.use(cors()); + app.use(express.json({ limit: '5mb' })); + app.use(express.urlencoded({ extended: true, limit: '5mb' })); + + app.use( + '/api/model', + ZenStackMiddleware({ + apiHandler: new RPCApiHandler({ schema }), + getClient: () => client, + }), + ); + + app.get('/api/schema', (_req, res: express.Response) => { + res.json({ ...schema, zenstackVersion: getVersion() }); + }); + + const server = app.listen(options.port, () => { + console.log(`ZenStack proxy server is running on port: ${options.port}`); + console.log(`You can visit ZenStack Studio at: ${colors.blue('https://studio.zenstack.dev')}`); + }); + + // Graceful shutdown + process.on('SIGTERM', async () => { + server.close(() => { + console.log('\nZenStack proxy server closed'); + }); + + await client.$disconnect(); + process.exit(0); + }); + + process.on('SIGINT', async () => { + server.close(() => { + console.log('\nZenStack proxy server closed'); + }); + await client.$disconnect(); + process.exit(0); + }); +} diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 0d663044c..836823f06 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -38,6 +38,10 @@ const seedAction = async (options: Parameters[0], args: str await telemetry.trackCommand('db seed', () => actions.seed(options, args)); }; +const proxyAction = async (options: Parameters[0]): Promise => { + await telemetry.trackCommand('proxy', () => actions.proxy(options)); +}; + function createProgram() { const program = new Command('zen') .alias('zenstack') @@ -186,6 +190,18 @@ Arguments following -- are passed to the seed script. E.g.: "zen db seed -- --us .addOption(noVersionCheckOption) .action(formatAction); + program + .command('proxy') + .alias('studio') + .description('Start the ZenStack proxy server') + .addOption(schemaOption) + .addOption(new Option('-p, --port ', 'port to run the proxy server on').default(8008)) + .addOption(new Option('-o, --output ', 'output directory for `zen generate` command')) + .addOption(new Option('-d, --databaseUrl ', 'database connection URL')) + .addOption(new Option('-l, --logLevel ', 'Query log levels (e.g., query, error)')) + .addOption(noVersionCheckOption) + .action(proxyAction); + program.addHelpCommand('help [command]', 'Display help for a command'); program.hook('preAction', async (_thisCommand, actionCommand) => { @@ -221,7 +237,10 @@ async function main() { } } - if (program.args.includes('generate') && (program.args.includes('-w') || program.args.includes('--watch'))) { + if ( + (program.args.includes('generate') && (program.args.includes('-w') || program.args.includes('--watch'))) || + ['proxy', 'studio'].some((cmd) => program.args.includes(cmd)) + ) { // A "hack" way to prevent the process from terminating because we don't want to stop it. return; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a60befaa5..6b6ee103e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,9 +183,18 @@ importers: '@zenstackhq/language': specifier: workspace:* version: link:../language + '@zenstackhq/orm': + specifier: workspace:* + version: link:../orm '@zenstackhq/sdk': specifier: workspace:* version: link:../sdk + '@zenstackhq/server': + specifier: workspace:* + version: link:../server + better-sqlite3: + specifier: 'catalog:' + version: 12.5.0 chokidar: specifier: ^5.0.0 version: 5.0.0 @@ -195,9 +204,15 @@ importers: commander: specifier: ^8.3.0 version: 8.3.0 + cors: + specifier: ^2.8.5 + version: 2.8.5 execa: specifier: ^9.6.0 version: 9.6.0 + express: + specifier: ^5.0.0 + version: 5.1.0 jiti: specifier: ^2.6.1 version: 2.6.1 @@ -213,6 +228,9 @@ importers: package-manager-detector: specifier: ^1.3.0 version: 1.3.0 + pg: + specifier: 'catalog:' + version: 8.16.3 prisma: specifier: 'catalog:' version: 6.19.0(magicast@0.3.5)(typescript@5.9.3) @@ -226,6 +244,15 @@ importers: '@types/better-sqlite3': specifier: 'catalog:' version: 7.6.13 + '@types/cors': + specifier: ^2.8.19 + version: 2.8.19 + '@types/express': + specifier: ^5.0.0 + version: 5.0.3 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 '@types/semver': specifier: ^7.7.0 version: 7.7.0 @@ -235,9 +262,6 @@ importers: '@zenstackhq/eslint-config': specifier: workspace:* version: link:../config/eslint-config - '@zenstackhq/orm': - specifier: workspace:* - version: link:../orm '@zenstackhq/testtools': specifier: workspace:* version: link:../testtools @@ -247,9 +271,6 @@ importers: '@zenstackhq/vitest-config': specifier: workspace:* version: link:../config/vitest-config - better-sqlite3: - specifier: 'catalog:' - version: 12.5.0 tmp: specifier: 'catalog:' version: 0.2.3 @@ -3464,6 +3485,9 @@ packages: '@types/cookiejar@2.1.5': resolution: {integrity: sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==} + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -3510,6 +3534,9 @@ packages: '@types/pg@8.15.6': resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + '@types/pluralize@0.0.33': resolution: {integrity: sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg==} @@ -4522,6 +4549,10 @@ packages: core-util-is@1.0.3: resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + crc-32@1.2.2: resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==} engines: {node: '>=0.8'} @@ -10680,6 +10711,10 @@ snapshots: '@types/cookiejar@2.1.5': {} + '@types/cors@2.8.19': + dependencies: + '@types/node': 20.19.24 + '@types/deep-eql@4.0.2': {} '@types/emscripten@1.40.1': {} @@ -10733,6 +10768,12 @@ snapshots: pg-protocol: 1.10.3 pg-types: 2.2.0 + '@types/pg@8.16.0': + dependencies: + '@types/node': 20.19.24 + pg-protocol: 1.10.3 + pg-types: 2.2.0 + '@types/pluralize@0.0.33': {} '@types/qs@6.14.0': {} @@ -11924,6 +11965,11 @@ snapshots: core-util-is@1.0.3: {} + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + crc-32@1.2.2: {} crc32-stream@6.0.0: From 9e819e811beb0043408a583ea694ad0bca9cbe61 Mon Sep 17 00:00:00 2001 From: Lukas Kahwe Smith Date: Sat, 24 Jan 2026 04:12:58 +0100 Subject: [PATCH 07/25] add test for pg computed values across multiple schema (#615) * add test for pg computed values across multiple schema * Add an any cast to type check test file --------- Co-authored-by: Yiming Cao --- .../pg-computed-multischema.test.ts | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 tests/e2e/orm/client-api/pg-computed-multischema.test.ts diff --git a/tests/e2e/orm/client-api/pg-computed-multischema.test.ts b/tests/e2e/orm/client-api/pg-computed-multischema.test.ts new file mode 100644 index 000000000..98a223c1d --- /dev/null +++ b/tests/e2e/orm/client-api/pg-computed-multischema.test.ts @@ -0,0 +1,53 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Postgres multi-schema with computed fields', () => { + it('supports computed fields on models in custom schemas', async () => { + const db = await createTestClient( + ` + datasource db { + provider = "postgresql" + url = '$DB_URL' + schemas = ["public", "mySchema1", "mySchema2"] + } + + model Author { + id Int @id @default(autoincrement()) + name String + books Book[] + @@schema("mySchema1") + } + + model Book { + id Int @id @default(autoincrement()) + title String + authorId Int + author Author @relation(fields: [authorId], references: [id]) + authorName String @computed() + @@schema("mySchema2") + } + `, + { + provider: 'postgresql', + usePrismaPush: true, + computedFields: { + Book: { + authorName: (eb: any) => eb + .selectFrom('mySchema1.Author') + .select('Author.name') + .whereRef('Author.id', '=', 'authorId') + .limit(1) + }, + }, + } as any + ); + + // Create author and book + const author = await db.author.create({ data: { name: 'Jane Doe' } }); + const book = await db.book.create({ data: { title: 'ZenStack Guide', authorId: author.id } }); + + // Fetch book and check computed field + const fetched = await db.book.findUnique({ where: { id: book.id } }); + expect(fetched?.authorName).toBe('Jane Doe'); + }); +}); From 99f68e2f7b6903b9991aa2a5f289e226d8928751 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sat, 24 Jan 2026 23:21:36 +0800 Subject: [PATCH 08/25] feat(orm): mysql support (#616) * WIP(orm): mysql support * WIP: more progress with fixing tests * WIP: get all client api tests pass * WIP: get all tests pass * fix executor * add MySQL to CI matrix * fix sqlite test runs * fix test * fix delete readback check * set mysql container max connections * fix tests * fix test * refactor: extract duplicated mysql/pg code into base class * address PR comments * refactor: remove order by duplicated code * refactor: optimize stripTableReference * addressing PR comments * fix tests --- .github/workflows/build-test.yml | 19 +- TODO.md | 5 +- package.json | 3 +- packages/language/src/constants.ts | 14 +- packages/language/src/document.ts | 45 +- .../src/validators/datamodel-validator.ts | 23 - packages/orm/package.json | 14 + packages/orm/src/client/client-impl.ts | 12 +- packages/orm/src/client/contract.ts | 129 ++-- packages/orm/src/client/crud-types.ts | 21 +- .../src/client/crud/dialects/base-dialect.ts | 222 +++--- .../orm/src/client/crud/dialects/index.ts | 2 + .../dialects/lateral-join-dialect-base.ts | 291 ++++++++ .../orm/src/client/crud/dialects/mysql.ts | 379 ++++++++++ .../src/client/crud/dialects/postgresql.ts | 418 ++++------- .../orm/src/client/crud/dialects/sqlite.ts | 117 +++- .../src/client/crud/operations/aggregate.ts | 19 +- .../orm/src/client/crud/operations/base.ts | 551 +++++++++++---- .../orm/src/client/crud/operations/count.ts | 6 +- .../orm/src/client/crud/operations/create.ts | 3 +- .../orm/src/client/crud/operations/delete.ts | 5 +- .../src/client/crud/operations/group-by.ts | 8 +- .../orm/src/client/crud/operations/update.ts | 6 + .../orm/src/client/executor/name-mapper.ts | 15 +- .../executor/zenstack-query-executor.ts | 661 ++++++++++++------ packages/orm/src/client/functions.ts | 7 +- .../src/client/helpers/schema-db-pusher.ts | 152 +++- packages/orm/src/client/plugin.ts | 6 + packages/orm/src/client/query-utils.ts | 28 +- packages/orm/src/dialects/mysql.ts | 1 + packages/orm/tsup.config.ts | 1 + .../policy/src/expression-transformer.ts | 2 +- packages/plugins/policy/src/functions.ts | 11 +- packages/plugins/policy/src/policy-handler.ts | 352 ++++++---- packages/plugins/policy/src/utils.ts | 4 +- packages/schema/src/schema.ts | 2 +- packages/server/test/api/rest.test.ts | 15 +- packages/server/test/api/rpc.test.ts | 8 +- packages/testtools/package.json | 1 + packages/testtools/src/client.ts | 104 ++- packages/testtools/src/schema.ts | 28 +- pnpm-lock.yaml | 111 ++- pnpm-workspace.yaml | 1 + .../orm/client-api/computed-fields.test.ts | 70 +- .../client-api/create-many-and-return.test.ts | 10 + tests/e2e/orm/client-api/delegate.test.ts | 10 +- .../e2e/orm/client-api/error-handling.test.ts | 11 +- tests/e2e/orm/client-api/find.test.ts | 10 +- tests/e2e/orm/client-api/json-filter.test.ts | 4 +- tests/e2e/orm/client-api/mixin.test.ts | 2 +- tests/e2e/orm/client-api/name-mapping.test.ts | 129 ++-- tests/e2e/orm/client-api/raw-query.test.ts | 32 +- .../client-api/relation/self-relation.test.ts | 2 +- .../e2e/orm/client-api/type-coverage.test.ts | 3 +- tests/e2e/orm/client-api/update-many.test.ts | 5 + tests/e2e/orm/client-api/update.test.ts | 26 +- .../entity-mutation-hooks.test.ts | 12 +- .../orm/plugin-infra/ext-query-args.test.ts | 29 +- .../orm/plugin-infra/on-kysely-query.test.ts | 22 +- tests/e2e/orm/policy/crud/create.test.ts | 11 +- tests/e2e/orm/policy/crud/read.test.ts | 6 + tests/e2e/orm/policy/crud/update.test.ts | 78 ++- tests/e2e/orm/policy/migrated/auth.test.ts | 56 +- .../migrated/create-many-and-return.test.ts | 17 +- .../orm/policy/migrated/current-model.test.ts | 32 +- .../policy/migrated/current-operation.test.ts | 8 +- .../orm/policy/migrated/deep-nested.test.ts | 2 +- .../policy/migrated/multi-id-fields.test.ts | 120 +++- .../orm/policy/migrated/nested-to-one.test.ts | 35 +- .../migrated/toplevel-operations.test.ts | 62 +- .../migrated/update-many-and-return.test.ts | 15 +- tests/e2e/orm/policy/migrated/view.test.ts | 8 +- .../e2e/orm/policy/nonexistent-models.test.ts | 12 +- tests/e2e/orm/policy/policy-functions.test.ts | 12 +- .../orm/query-builder/query-builder.test.ts | 4 +- tests/e2e/orm/schemas/basic/input.ts | 21 + tests/e2e/orm/schemas/basic/models.ts | 1 + tests/e2e/orm/schemas/basic/schema.ts | 20 + tests/e2e/orm/schemas/basic/schema.zmodel | 4 + tests/e2e/package.json | 6 +- tests/regression/package.json | 5 +- tests/regression/test/issue-493.test.ts | 2 +- .../test/v2-migrated/issue-1576.test.ts | 62 +- .../test/v2-migrated/issue-1681.test.ts | 6 +- .../test/v2-migrated/issue-1894.test.ts | 4 +- 85 files changed, 3376 insertions(+), 1432 deletions(-) create mode 100644 packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts create mode 100644 packages/orm/src/client/crud/dialects/mysql.ts create mode 100644 packages/orm/src/dialects/mysql.ts diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 053ae9ff8..632042c5a 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -31,10 +31,23 @@ jobs: ports: - 5432:5432 + mysql: + image: mysql:8.4 + env: + MYSQL_ROOT_PASSWORD: mysql + ports: + - 3306:3306 + # Set health checks to wait until mysql has started + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + strategy: matrix: node-version: [22.x] - provider: [sqlite, postgresql] + provider: [sqlite, postgresql, mysql] steps: - name: Checkout @@ -81,5 +94,9 @@ jobs: - name: Lint run: pnpm run lint + - name: Set MySQL max_connections + run: | + mysql -h 127.0.0.1 -uroot -pmysql -e "SET GLOBAL max_connections=500;" + - name: Test run: TEST_DB_PROVIDER=${{ matrix.provider }} pnpm run test diff --git a/TODO.md b/TODO.md index 7c7a767cd..d36f96991 100644 --- a/TODO.md +++ b/TODO.md @@ -18,7 +18,6 @@ - [ ] ZModel - [x] Import - [ ] View support - - [ ] Datasource provider-scoped attributes - [ ] ORM - [x] Create - [x] Input validation @@ -72,7 +71,7 @@ - [x] Query builder API - [x] Computed fields - [x] Plugin - - [ ] Custom procedures + - [x] Custom procedures - [ ] Misc - [x] JSDoc for CRUD methods - [x] Cache validation schemas @@ -110,4 +109,4 @@ - [x] SQLite - [x] PostgreSQL - [x] Multi-schema - - [ ] MySQL + - [x] MySQL diff --git a/package.json b/package.json index 639198d07..ba1c74796 100644 --- a/package.json +++ b/package.json @@ -9,8 +9,9 @@ "watch": "turbo run watch build", "lint": "turbo run lint", "test": "turbo run test", - "test:all": "pnpm run test:sqlite && pnpm run test:pg", + "test:all": "pnpm run test:sqlite && pnpm run test:pg && pnpm run test:mysql", "test:pg": "TEST_DB_PROVIDER=postgresql turbo run test", + "test:mysql": "TEST_DB_PROVIDER=mysql turbo run test", "test:sqlite": "TEST_DB_PROVIDER=sqlite turbo run test", "test:coverage": "vitest run --coverage", "format": "prettier --write \"**/*.{ts,tsx,md}\"", diff --git a/packages/language/src/constants.ts b/packages/language/src/constants.ts index a39b49132..9fd79bf75 100644 --- a/packages/language/src/constants.ts +++ b/packages/language/src/constants.ts @@ -1,14 +1,7 @@ /** * Supported db providers */ -export const SUPPORTED_PROVIDERS = [ - 'sqlite', - 'postgresql', - // TODO: other providers - // 'mysql', - // 'sqlserver', - // 'cockroachdb', -]; +export const SUPPORTED_PROVIDERS = ['sqlite', 'postgresql', 'mysql']; /** * All scalar types @@ -41,3 +34,8 @@ export enum ExpressionContext { ValidationRule = 'ValidationRule', Index = 'Index', } + +/** + * Database providers that support list field types. + */ +export const DB_PROVIDERS_SUPPORTING_LIST_TYPE = ['postgresql']; diff --git a/packages/language/src/document.ts b/packages/language/src/document.ts index 17146f852..9642e61d5 100644 --- a/packages/language/src/document.ts +++ b/packages/language/src/document.ts @@ -1,3 +1,4 @@ +import { invariant } from '@zenstackhq/common-helpers'; import { isAstNode, TextDocument, @@ -10,10 +11,18 @@ import { import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { isDataSource, type Model } from './ast'; -import { STD_LIB_MODULE_NAME } from './constants'; +import { isDataModel, isDataSource, type Model } from './ast'; +import { DB_PROVIDERS_SUPPORTING_LIST_TYPE, STD_LIB_MODULE_NAME } from './constants'; import { createZModelServices } from './module'; -import { getDataModelAndTypeDefs, getDocument, hasAttribute, resolveImport, resolveTransitiveImports } from './utils'; +import { + getAllFields, + getDataModelAndTypeDefs, + getDocument, + getLiteral, + hasAttribute, + resolveImport, + resolveTransitiveImports, +} from './utils'; import type { ZModelFormatter } from './zmodel-formatter'; /** @@ -207,6 +216,24 @@ function validationAfterImportMerge(model: Model) { if (authDecls.length > 1) { errors.push('Validation error: Multiple `@@auth` declarations are not allowed'); } + + // check for usages incompatible with the datasource provider + const provider = getDataSourceProvider(model); + invariant(provider !== undefined, 'Datasource provider should be defined at this point'); + + for (const decl of model.declarations.filter(isDataModel)) { + const fields = getAllFields(decl, true); + for (const field of fields) { + if (field.type.array && !isDataModel(field.type.reference?.ref)) { + if (!DB_PROVIDERS_SUPPORTING_LIST_TYPE.includes(provider)) { + errors.push( + `Validation error: List type is not supported for "${provider}" provider (model: "${decl.name}", field: "${field.name}")`, + ); + } + } + } + } + return errors; } @@ -226,3 +253,15 @@ export async function formatDocument(content: string) { const edits = await formatter.formatDocument(document, { options, textDocument: identifier }); return TextDocument.applyEdits(document.textDocument, edits); } + +function getDataSourceProvider(model: Model) { + const dataSource = model.declarations.find(isDataSource); + if (!dataSource) { + return undefined; + } + const provider = dataSource?.fields.find((f) => f.name === 'provider'); + if (!provider) { + return undefined; + } + return getLiteral(provider.value); +} diff --git a/packages/language/src/validators/datamodel-validator.ts b/packages/language/src/validators/datamodel-validator.ts index a1caba5d4..6c5d18ffd 100644 --- a/packages/language/src/validators/datamodel-validator.ts +++ b/packages/language/src/validators/datamodel-validator.ts @@ -5,20 +5,16 @@ import { ArrayExpr, DataField, DataModel, - Model, ReferenceExpr, TypeDef, isDataModel, - isDataSource, isEnum, - isModel, isStringLiteral, isTypeDef, } from '../generated/ast'; import { getAllAttributes, getAllFields, - getLiteral, getModelIdFields, getModelUniqueFields, getUniqueFields, @@ -105,13 +101,6 @@ export default class DataModelValidator implements AstValidator { accept('error', 'Unsupported type argument must be a string literal', { node: field.type.unsupported }); } - if (field.type.array && !isDataModel(field.type.reference?.ref)) { - const provider = this.getDataSourceProvider(AstUtils.getContainerOfType(field, isModel)!); - if (provider === 'sqlite') { - accept('error', `List type is not supported for "${provider}" provider.`, { node: field.type }); - } - } - field.attributes.forEach((attr) => validateAttributeApplication(attr, accept)); if (isTypeDef(field.type.reference?.ref)) { @@ -121,18 +110,6 @@ export default class DataModelValidator implements AstValidator { } } - private getDataSourceProvider(model: Model) { - const dataSource = model.declarations.find(isDataSource); - if (!dataSource) { - return undefined; - } - const provider = dataSource?.fields.find((f) => f.name === 'provider'); - if (!provider) { - return undefined; - } - return getLiteral(provider.value); - } - private validateAttributes(dm: DataModel, accept: ValidationAcceptor) { getAllAttributes(dm).forEach((attr) => validateAttributeApplication(attr, accept, dm)); } diff --git a/packages/orm/package.json b/packages/orm/package.json index ccd94a9c6..eb5ee6684 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -56,6 +56,16 @@ "default": "./dist/dialects/postgres.cjs" } }, + "./dialects/mysql": { + "import": { + "types": "./dist/dialects/mysql.d.ts", + "default": "./dist/dialects/mysql.js" + }, + "require": { + "types": "./dist/dialects/mysql.d.cts", + "default": "./dist/dialects/mysql.cjs" + } + }, "./dialects/sql.js": { "import": { "types": "./dist/dialects/sql.js.d.ts", @@ -100,6 +110,7 @@ "peerDependencies": { "better-sqlite3": "catalog:", "pg": "catalog:", + "mysql2": "catalog:", "sql.js": "catalog:", "zod": "catalog:" }, @@ -110,6 +121,9 @@ "pg": { "optional": true }, + "mysql2": { + "optional": true + }, "sql.js": { "optional": true } diff --git a/packages/orm/src/client/client-impl.ts b/packages/orm/src/client/client-impl.ts index 719f90f3d..fc8f92c7c 100644 --- a/packages/orm/src/client/client-impl.ts +++ b/packages/orm/src/client/client-impl.ts @@ -31,7 +31,7 @@ import { FindOperationHandler } from './crud/operations/find'; import { GroupByOperationHandler } from './crud/operations/group-by'; import { UpdateOperationHandler } from './crud/operations/update'; import { InputValidator } from './crud/validator'; -import { createConfigError, createNotFoundError } from './errors'; +import { createConfigError, createNotFoundError, createNotSupportedError } from './errors'; import { ZenStackDriver } from './executor/zenstack-driver'; import { ZenStackQueryExecutor } from './executor/zenstack-query-executor'; import * as BuiltinFunctions from './functions'; @@ -564,6 +564,11 @@ function createModelCrudHandler( }, createManyAndReturn: (args: unknown) => { + if (client.$schema.provider.type === 'mysql') { + throw createNotSupportedError( + '"createManyAndReturn" is not supported by "mysql" provider. Use "createMany" or multiple "create" calls instead.', + ); + } return createPromise( 'createManyAndReturn', 'createManyAndReturn', @@ -594,6 +599,11 @@ function createModelCrudHandler( }, updateManyAndReturn: (args: unknown) => { + if (client.$schema.provider.type === 'mysql') { + throw createNotSupportedError( + '"updateManyAndReturn" is not supported by "mysql" provider. Use "updateMany" or multiple "update" calls instead.', + ); + } return createPromise( 'updateManyAndReturn', 'updateManyAndReturn', diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index 945f36457..7492c02bf 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -306,6 +306,77 @@ export type AllModelOperations< Model extends GetModels, Options extends QueryOptions, ExtQueryArgs, +> = CommonModelOperations & + // provider-specific operations + (Schema['provider']['type'] extends 'mysql' + ? {} + : { + /** + * Creates multiple entities and returns them. + * @param args - create args. See {@link createMany} for input. Use + * `select` and `omit` to control the fields returned. + * @returns the created entities + * + * @example + * ```ts + * // create multiple entities and return selected fields + * await db.user.createManyAndReturn({ + * data: [ + * { name: 'Alex', email: 'alex@zenstack.dev' }, + * { name: 'John', email: 'john@zenstack.dev' } + * ], + * select: { id: true, email: true } + * }); + * ``` + */ + createManyAndReturn< + T extends CreateManyAndReturnArgs & + ExtractExtQueryArgs, + >( + args?: SelectSubset< + T, + CreateManyAndReturnArgs & ExtractExtQueryArgs + >, + ): ZenStackPromise[]>; + + /** + * Updates multiple entities and returns them. + * @param args - update args. Only scalar fields are allowed for data. + * @returns the updated entities + * + * @example + * ```ts + * // update many entities and return selected fields + * await db.user.updateManyAndReturn({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' }, + * select: { id: true, email: true } + * }); // result: `Array<{ id: string; email: string }>` + * + * // limit the number of updated entities + * await db.user.updateManyAndReturn({ + * where: { email: { endsWith: '@zenstack.dev' } }, + * data: { role: 'ADMIN' }, + * limit: 10 + * }); + * ``` + */ + updateManyAndReturn< + T extends UpdateManyAndReturnArgs & + ExtractExtQueryArgs, + >( + args: Subset< + T, + UpdateManyAndReturnArgs & ExtractExtQueryArgs + >, + ): ZenStackPromise[]>; + }); + +type CommonModelOperations< + Schema extends SchemaDef, + Model extends GetModels, + Options extends QueryOptions, + ExtQueryArgs, > = { /** * Returns a list of entities. @@ -517,33 +588,6 @@ export type AllModelOperations< args?: SelectSubset & ExtractExtQueryArgs>, ): ZenStackPromise; - /** - * Creates multiple entities and returns them. - * @param args - create args. See {@link createMany} for input. Use - * `select` and `omit` to control the fields returned. - * @returns the created entities - * - * @example - * ```ts - * // create multiple entities and return selected fields - * await db.user.createManyAndReturn({ - * data: [ - * { name: 'Alex', email: 'alex@zenstack.dev' }, - * { name: 'John', email: 'john@zenstack.dev' } - * ], - * select: { id: true, email: true } - * }); - * ``` - */ - createManyAndReturn< - T extends CreateManyAndReturnArgs & ExtractExtQueryArgs, - >( - args?: SelectSubset< - T, - CreateManyAndReturnArgs & ExtractExtQueryArgs - >, - ): ZenStackPromise[]>; - /** * Updates a uniquely identified entity. * @param args - update args. See {@link findMany} for how to control @@ -689,37 +733,6 @@ export type AllModelOperations< args: Subset & ExtractExtQueryArgs>, ): ZenStackPromise; - /** - * Updates multiple entities and returns them. - * @param args - update args. Only scalar fields are allowed for data. - * @returns the updated entities - * - * @example - * ```ts - * // update many entities and return selected fields - * await db.user.updateManyAndReturn({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' }, - * select: { id: true, email: true } - * }); // result: `Array<{ id: string; email: string }>` - * - * // limit the number of updated entities - * await db.user.updateManyAndReturn({ - * where: { email: { endsWith: '@zenstack.dev' } }, - * data: { role: 'ADMIN' }, - * limit: 10 - * }); - * ``` - */ - updateManyAndReturn< - T extends UpdateManyAndReturnArgs & ExtractExtQueryArgs, - >( - args: Subset< - T, - UpdateManyAndReturnArgs & ExtractExtQueryArgs - >, - ): ZenStackPromise[]>; - /** * Creates or updates an entity. * @param args - upsert args diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index 93f770c60..bfee7ff14 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -1079,12 +1079,15 @@ export type FindArgs< Collection extends boolean, AllowFilter extends boolean = true, > = (Collection extends true - ? SortAndTakeArgs & { - /** - * Distinct fields - */ - distinct?: OrArray>; - } + ? SortAndTakeArgs & + (ProviderSupportsDistinct extends true + ? { + /** + * Distinct fields. Only supported by providers that natively support SQL "DISTINCT ON". + */ + distinct?: OrArray>; + } + : {}) : {}) & (AllowFilter extends true ? FilterArgs : {}) & SelectIncludeOmit; @@ -2108,8 +2111,8 @@ type MapType = T extends keyof TypeM ? EnumValue : unknown; -// type ProviderSupportsDistinct = Schema['provider']['type'] extends 'postgresql' -// ? true -// : false; +type ProviderSupportsDistinct = Schema['provider']['type'] extends 'postgresql' + ? true + : false; // #endregion diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index f4bf56ffb..515878d9e 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -1,5 +1,5 @@ import { enumerate, invariant, isPlainObject } from '@zenstackhq/common-helpers'; -import type { Expression, ExpressionBuilder, ExpressionWrapper, SqlBool, ValueNode } from 'kysely'; +import type { AliasableExpression, Expression, ExpressionBuilder, ExpressionWrapper, SqlBool, ValueNode } from 'kysely'; import { expressionBuilder, sql, type SelectQueryBuilder } from 'kysely'; import { match, P } from 'ts-pattern'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; @@ -44,14 +44,63 @@ export abstract class BaseCrudDialect { protected readonly options: ClientOptions, ) {} - transformPrimitive(value: unknown, _type: BuiltinType, _forArrayField: boolean) { + // #region capability flags + + /** + * Whether the dialect supports updating with a limit on the number of updated rows. + */ + abstract get supportsUpdateWithLimit(): boolean; + + /** + * Whether the dialect supports deleting with a limit on the number of deleted rows. + */ + abstract get supportsDeleteWithLimit(): boolean; + + /** + * Whether the dialect supports DISTINCT ON. + */ + abstract get supportsDistinctOn(): boolean; + + /** + * Whether the dialect support inserting with `DEFAULT` as field value. + */ + abstract get supportsDefaultAsFieldValue(): boolean; + + /** + * Whether the dialect supports the RETURNING clause in INSERT/UPDATE/DELETE statements. + */ + abstract get supportsReturning(): boolean; + + /** + * Whether the dialect supports `INSERT INTO ... DEFAULT VALUES` syntax. + */ + abstract get supportsInsertDefaultValues(): boolean; + + /** + * How to perform insert ignore operation. + */ + abstract get insertIgnoreMethod(): 'onConflict' | 'ignore'; + + // #endregion + + // #region value transformation + + /** + * Transforms input value before sending to database. + */ + transformInput(value: unknown, _type: BuiltinType, _forArrayField: boolean) { return value; } + /** + * Transforms output value received from database. + */ transformOutput(value: unknown, _type: BuiltinType, _array: boolean) { return value; } + // #endregion + // #region common query builders buildSelectModel(model: string, modelAlias: string) { @@ -90,7 +139,7 @@ export abstract class BaseCrudDialect { result = this.buildSkipTake(result, skip, take); // orderBy - result = this.buildOrderBy(result, model, modelAlias, args.orderBy, negateOrderBy); + result = this.buildOrderBy(result, model, modelAlias, args.orderBy, negateOrderBy, take); // distinct if ('distinct' in args && (args as any).distinct) { @@ -160,8 +209,8 @@ export abstract class BaseCrudDialect { private buildCursorFilter( model: string, query: SelectQueryBuilder, - cursor: FindArgs, true>['cursor'], - orderBy: FindArgs, true>['orderBy'], + cursor: object, + orderBy: OrArray> | undefined, negateOrderBy: boolean, modelAlias: string, ) { @@ -394,51 +443,35 @@ export abstract class BaseCrudDialect { continue; } - switch (key) { - case 'some': { - result = this.and( - result, - this.eb( + const countSelect = (negate: boolean) => { + const filter = this.buildFilter(relationModel, relationFilterSelectAlias, subPayload); + return ( + this.eb + // the outer select is needed to avoid mysql's scope issue + .selectFrom( this.buildSelectModel(relationModel, relationFilterSelectAlias) .select(() => this.eb.fn.count(this.eb.lit(1)).as('$count')) .where(buildPkFkWhereRefs(this.eb)) - .where(() => this.buildFilter(relationModel, relationFilterSelectAlias, subPayload)), - '>', - 0, - ), - ); + .where(() => (negate ? this.eb.not(filter) : filter)) + .as('$sub'), + ) + .select('$count') + ); + }; + + switch (key) { + case 'some': { + result = this.and(result, this.eb(countSelect(false), '>', 0)); break; } case 'every': { - result = this.and( - result, - this.eb( - this.buildSelectModel(relationModel, relationFilterSelectAlias) - .select((eb1) => eb1.fn.count(eb1.lit(1)).as('$count')) - .where(buildPkFkWhereRefs(this.eb)) - .where(() => - this.eb.not(this.buildFilter(relationModel, relationFilterSelectAlias, subPayload)), - ), - '=', - 0, - ), - ); + result = this.and(result, this.eb(countSelect(true), '=', 0)); break; } case 'none': { - result = this.and( - result, - this.eb( - this.buildSelectModel(relationModel, relationFilterSelectAlias) - .select(() => this.eb.fn.count(this.eb.lit(1)).as('$count')) - .where(buildPkFkWhereRefs(this.eb)) - .where(() => this.buildFilter(relationModel, relationFilterSelectAlias, subPayload)), - '=', - 0, - ), - ); + result = this.and(result, this.eb(countSelect(false), '=', 0)); break; } } @@ -456,7 +489,7 @@ export abstract class BaseCrudDialect { continue; } - const value = this.transformPrimitive(_value, fieldType, !!fieldDef.array); + const value = this.transformInput(_value, fieldType, !!fieldDef.array); switch (key) { case 'equals': { @@ -550,7 +583,7 @@ export abstract class BaseCrudDialect { const path = filter.path; const jsonReceiver = this.buildJsonPathSelection(receiver, path); - const stringReceiver = this.eb.cast(jsonReceiver, 'text'); + const stringReceiver = this.castText(jsonReceiver); const mode = filter.mode ?? 'default'; invariant(mode === 'default' || mode === 'insensitive', 'Invalid JSON filter mode'); @@ -658,7 +691,7 @@ export abstract class BaseCrudDialect { const clauses: Expression[] = []; if (filter === null) { - return this.eb(receiver, '=', 'null'); + return this.eb(receiver, '=', this.transformInput(null, 'Json', false)); } invariant(filter && typeof filter === 'object', 'Typed JSON filter payload must be an object'); @@ -688,7 +721,7 @@ export abstract class BaseCrudDialect { let _receiver = fieldReceiver; if (fieldDef.type === 'String') { // trim quotes for string fields - _receiver = this.eb.fn('trim', [this.eb.cast(fieldReceiver, 'text'), sql.lit('"')]); + _receiver = this.trimTextQuotes(this.castText(fieldReceiver)); } clauses.push(this.buildPrimitiveFilter(_receiver, fieldDef, value)); } @@ -702,17 +735,24 @@ export abstract class BaseCrudDialect { if (value instanceof DbNullClass) { return this.eb(lhs, 'is', null); } else if (value instanceof JsonNullClass) { - return this.eb.and([this.eb(lhs, '=', 'null'), this.eb(lhs, 'is not', null)]); + return this.eb.and([ + this.eb(lhs, '=', this.transformInput(null, 'Json', false)), + this.eb(lhs, 'is not', null), + ]); } else if (value instanceof AnyNullClass) { // AnyNull matches both DB NULL and JSON null - return this.eb.or([this.eb(lhs, 'is', null), this.eb(lhs, '=', 'null')]); + return this.eb.or([this.eb(lhs, 'is', null), this.eb(lhs, '=', this.transformInput(null, 'Json', false))]); } else { - return this.buildLiteralFilter(lhs, 'Json', value); + return this.buildJsonEqualityFilter(lhs, value); } } + protected buildJsonEqualityFilter(lhs: Expression, rhs: unknown) { + return this.buildLiteralFilter(lhs, 'Json', rhs); + } + private buildLiteralFilter(lhs: Expression, type: BuiltinType, rhs: unknown) { - return this.eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type, false) : rhs); + return this.eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformInput(rhs, type, false) : rhs); } private buildStandardFilter( @@ -869,7 +909,7 @@ export abstract class BaseCrudDialect { private buildStringLike(receiver: Expression, pattern: string, insensitive: boolean) { const { supportsILike } = this.getStringCasingBehavior(); const op = insensitive && supportsILike ? 'ilike' : 'like'; - return sql`${receiver} ${sql.raw(op)} ${sql.val(pattern)} escape '\\'`; + return sql`${receiver} ${sql.raw(op)} ${sql.val(pattern)} escape ${sql.val('\\')}`; } private prepStringCasing( @@ -895,7 +935,7 @@ export abstract class BaseCrudDialect { type, payload, fieldRef, - (value) => this.transformPrimitive(value, type, false), + (value) => this.transformInput(value, type, false), (value) => this.buildNumberFilter(fieldRef, type, value), ); return this.and(...conditions); @@ -906,7 +946,7 @@ export abstract class BaseCrudDialect { 'Boolean', payload, fieldRef, - (value) => this.transformPrimitive(value, 'Boolean', false), + (value) => this.transformInput(value, 'Boolean', false), (value) => this.buildBooleanFilter(fieldRef, value as BooleanFilter), true, ['equals', 'not'], @@ -919,7 +959,7 @@ export abstract class BaseCrudDialect { 'DateTime', payload, fieldRef, - (value) => this.transformPrimitive(value, 'DateTime', false), + (value) => this.transformInput(value, 'DateTime', false), (value) => this.buildDateTimeFilter(fieldRef, value as DateTimeFilter), true, ); @@ -931,7 +971,7 @@ export abstract class BaseCrudDialect { 'Bytes', payload, fieldRef, - (value) => this.transformPrimitive(value, 'Bytes', false), + (value) => this.transformInput(value, 'Bytes', false), (value) => this.buildBytesFilter(fieldRef, value as BytesFilter), true, ['equals', 'in', 'notIn', 'not'], @@ -958,6 +998,7 @@ export abstract class BaseCrudDialect { modelAlias: string, orderBy: OrArray, boolean, boolean>> | undefined, negated: boolean, + take: number | undefined, ) { if (!orderBy) { return query; @@ -980,7 +1021,7 @@ export abstract class BaseCrudDialect { // aggregations if (['_count', '_avg', '_sum', '_min', '_max'].includes(field)) { - invariant(value && typeof value === 'object', `invalid orderBy value for field "${field}"`); + invariant(typeof value === 'object', `invalid orderBy value for field "${field}"`); for (const [k, v] of Object.entries(value)) { invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); result = result.orderBy( @@ -991,22 +1032,6 @@ export abstract class BaseCrudDialect { continue; } - switch (field) { - case '_count': { - invariant(value && typeof value === 'object', 'invalid orderBy value for field "_count"'); - for (const [k, v] of Object.entries(value)) { - invariant(v === 'asc' || v === 'desc', `invalid orderBy value for field "${field}"`); - result = result.orderBy( - (eb) => eb.fn.count(buildFieldRef(model, k, modelAlias)), - this.negateSort(v, negated), - ); - } - continue; - } - default: - break; - } - const fieldDef = requireField(this.schema, model, field); if (!fieldDef.relation) { @@ -1014,19 +1039,18 @@ export abstract class BaseCrudDialect { if (value === 'asc' || value === 'desc') { result = result.orderBy(fieldRef, this.negateSort(value, negated)); } else if ( - value && typeof value === 'object' && 'nulls' in value && 'sort' in value && (value.sort === 'asc' || value.sort === 'desc') && (value.nulls === 'first' || value.nulls === 'last') ) { - result = result.orderBy(fieldRef, (ob) => { - const dir = this.negateSort(value.sort, negated); - ob = dir === 'asc' ? ob.asc() : ob.desc(); - ob = value.nulls === 'first' ? ob.nullsFirst() : ob.nullsLast(); - return ob; - }); + result = this.buildOrderByField( + result, + fieldRef, + this.negateSort(value.sort, negated), + value.nulls, + ); } } else { // order by relation @@ -1069,7 +1093,7 @@ export abstract class BaseCrudDialect { ), ); }); - result = this.buildOrderBy(result, relationModel, joinAlias, value, negated); + result = this.buildOrderBy(result, relationModel, joinAlias, value, negated, take); } } } @@ -1253,16 +1277,16 @@ export abstract class BaseCrudDialect { // #region utils - private negateSort(sort: SortOrder, negated: boolean) { + protected negateSort(sort: SortOrder, negated: boolean) { return negated ? (sort === 'asc' ? 'desc' : 'asc') : sort; } public true(): Expression { - return this.eb.lit(this.transformPrimitive(true, 'Boolean', false) as boolean); + return this.eb.lit(this.transformInput(true, 'Boolean', false) as boolean); } public false(): Expression { - return this.eb.lit(this.transformPrimitive(false, 'Boolean', false) as boolean); + return this.eb.lit(this.transformInput(false, 'Boolean', false) as boolean); } public isTrue(expression: Expression) { @@ -1388,37 +1412,32 @@ export abstract class BaseCrudDialect { /** * Builds an Kysely expression that returns a JSON object for the given key-value pairs. */ - abstract buildJsonObject(value: Record>): ExpressionWrapper; + abstract buildJsonObject(value: Record>): AliasableExpression; /** * Builds an Kysely expression that returns the length of an array. */ - abstract buildArrayLength(array: Expression): ExpressionWrapper; + abstract buildArrayLength(array: Expression): AliasableExpression; /** * Builds an array literal SQL string for the given values. */ - abstract buildArrayLiteralSQL(values: unknown[]): string; + abstract buildArrayLiteralSQL(values: unknown[]): AliasableExpression; /** - * Whether the dialect supports updating with a limit on the number of updated rows. - */ - abstract get supportsUpdateWithLimit(): boolean; - - /** - * Whether the dialect supports deleting with a limit on the number of deleted rows. + * Casts the given expression to an integer type. */ - abstract get supportsDeleteWithLimit(): boolean; + abstract castInt>(expression: T): T; /** - * Whether the dialect supports DISTINCT ON. + * Casts the given expression to a text type. */ - abstract get supportsDistinctOn(): boolean; + abstract castText>(expression: T): T; /** - * Whether the dialect support inserting with `DEFAULT` as field value. + * Trims double quotes from the start and end of a text expression. */ - abstract get supportInsertWithDefault(): boolean; + abstract trimTextQuotes>(expression: T): T; /** * Gets the SQL column type for the given field definition. @@ -1430,6 +1449,11 @@ export abstract class BaseCrudDialect { */ abstract getStringCasingBehavior(): { supportsILike: boolean; likeCaseSensitive: boolean }; + /** + * Builds a VALUES table and select all fields from it. + */ + abstract buildValuesTableSelect(fields: FieldDef[], rows: unknown[][]): SelectQueryBuilder; + /** * Builds a JSON path selection expression. */ @@ -1452,5 +1476,15 @@ export abstract class BaseCrudDialect { buildFilter: (elem: Expression) => Expression, ): Expression; + /** + * Builds an ORDER BY clause for a field with NULLS FIRST/LAST support. + */ + protected abstract buildOrderByField( + query: SelectQueryBuilder, + field: Expression, + sort: SortOrder, + nulls: 'first' | 'last', + ): SelectQueryBuilder; + // #endregion } diff --git a/packages/orm/src/client/crud/dialects/index.ts b/packages/orm/src/client/crud/dialects/index.ts index ede19cdd8..fb9a7379f 100644 --- a/packages/orm/src/client/crud/dialects/index.ts +++ b/packages/orm/src/client/crud/dialects/index.ts @@ -2,6 +2,7 @@ import { match } from 'ts-pattern'; import type { SchemaDef } from '../../../schema'; import type { ClientOptions } from '../../options'; import type { BaseCrudDialect } from './base-dialect'; +import { MySqlCrudDialect } from './mysql'; import { PostgresCrudDialect } from './postgresql'; import { SqliteCrudDialect } from './sqlite'; @@ -12,5 +13,6 @@ export function getCrudDialect( return match(schema.provider.type) .with('sqlite', () => new SqliteCrudDialect(schema, options)) .with('postgresql', () => new PostgresCrudDialect(schema, options)) + .with('mysql', () => new MySqlCrudDialect(schema, options)) .exhaustive(); } diff --git a/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts new file mode 100644 index 000000000..6bc1f887c --- /dev/null +++ b/packages/orm/src/client/crud/dialects/lateral-join-dialect-base.ts @@ -0,0 +1,291 @@ +import { invariant } from '@zenstackhq/common-helpers'; +import { type AliasableExpression, type Expression, type ExpressionBuilder, type SelectQueryBuilder } from 'kysely'; +import type { FieldDef, GetModels, SchemaDef } from '../../../schema'; +import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants'; +import type { FindArgs } from '../../crud-types'; +import { + buildJoinPairs, + getDelegateDescendantModels, + getManyToManyRelation, + isRelationField, + requireField, + requireIdFields, + requireModel, +} from '../../query-utils'; +import { BaseCrudDialect } from './base-dialect'; + +/** + * Base class for dialects that support lateral joins (MySQL and PostgreSQL). + * Contains common logic for building relation selections using lateral joins and JSON aggregation. + */ +export abstract class LateralJoinDialectBase extends BaseCrudDialect { + /** + * Builds an array aggregation expression. + */ + protected abstract buildArrayAgg(arg: Expression): AliasableExpression; + + override buildRelationSelection( + query: SelectQueryBuilder, + model: string, + relationField: string, + parentAlias: string, + payload: true | FindArgs, true>, + ): SelectQueryBuilder { + const relationResultName = `${parentAlias}$${relationField}`; + const joinedQuery = this.buildRelationJSON( + model, + query, + relationField, + parentAlias, + payload, + relationResultName, + ); + return joinedQuery.select(`${relationResultName}.$data as ${relationField}`); + } + + private buildRelationJSON( + model: string, + qb: SelectQueryBuilder, + relationField: string, + parentAlias: string, + payload: true | FindArgs, true>, + resultName: string, + ) { + const relationFieldDef = requireField(this.schema, model, relationField); + const relationModel = relationFieldDef.type as GetModels; + + return qb.leftJoinLateral( + (eb) => { + const relationSelectName = `${resultName}$sub`; + const relationModelDef = requireModel(this.schema, relationModel); + + let tbl: SelectQueryBuilder; + + if (this.canJoinWithoutNestedSelect(relationModelDef, payload)) { + // build join directly + tbl = this.buildModelSelect(relationModel, relationSelectName, payload, false); + + // parent join filter + tbl = this.buildRelationJoinFilter( + tbl, + model, + relationField, + relationModel, + relationSelectName, + parentAlias, + ); + } else { + // join with a nested query + tbl = eb.selectFrom(() => { + let subQuery = this.buildModelSelect(relationModel, `${relationSelectName}$t`, payload, true); + + // parent join filter + subQuery = this.buildRelationJoinFilter( + subQuery, + model, + relationField, + relationModel, + `${relationSelectName}$t`, + parentAlias, + ); + + if (typeof payload !== 'object' || payload.take === undefined) { + // force adding a limit otherwise the ordering is ignored by some databases + // during JSON array aggregation + subQuery = subQuery.limit(Number.MAX_SAFE_INTEGER); + } + + return subQuery.as(relationSelectName); + }); + } + + // select relation result + tbl = this.buildRelationObjectSelect( + relationModel, + relationSelectName, + relationFieldDef, + tbl, + payload, + resultName, + ); + + // add nested joins for each relation + tbl = this.buildRelationJoins(tbl, relationModel, relationSelectName, payload, resultName); + + // alias the join table + return tbl.as(resultName); + }, + (join) => join.onTrue(), + ); + } + + private buildRelationJoinFilter( + query: SelectQueryBuilder, + model: string, + relationField: string, + relationModel: GetModels, + relationModelAlias: string, + parentAlias: string, + ) { + const m2m = getManyToManyRelation(this.schema, model, relationField); + if (m2m) { + // many-to-many relation + const parentIds = requireIdFields(this.schema, model); + const relationIds = requireIdFields(this.schema, relationModel); + invariant(parentIds.length === 1, 'many-to-many relation must have exactly one id field'); + invariant(relationIds.length === 1, 'many-to-many relation must have exactly one id field'); + query = query.where((eb) => + eb( + eb.ref(`${relationModelAlias}.${relationIds[0]}`), + 'in', + eb + .selectFrom(m2m.joinTable) + .select(`${m2m.joinTable}.${m2m.otherFkName}`) + .whereRef(`${parentAlias}.${parentIds[0]}`, '=', `${m2m.joinTable}.${m2m.parentFkName}`), + ), + ); + } else { + const joinPairs = buildJoinPairs(this.schema, model, parentAlias, relationField, relationModelAlias); + query = query.where((eb) => + this.and(...joinPairs.map(([left, right]) => eb(this.eb.ref(left), '=', this.eb.ref(right)))), + ); + } + return query; + } + + private buildRelationObjectSelect( + relationModel: string, + relationModelAlias: string, + relationFieldDef: FieldDef, + qb: SelectQueryBuilder, + payload: true | FindArgs, true>, + parentResultName: string, + ) { + qb = qb.select((eb) => { + const objArgs = this.buildRelationObjectArgs( + relationModel, + relationModelAlias, + eb, + payload, + parentResultName, + ); + + if (relationFieldDef.array) { + return this.buildArrayAgg(this.buildJsonObject(objArgs)).as('$data'); + } else { + return this.buildJsonObject(objArgs).as('$data'); + } + }); + + return qb; + } + + private buildRelationObjectArgs( + relationModel: string, + relationModelAlias: string, + eb: ExpressionBuilder, + payload: true | FindArgs, true>, + parentResultName: string, + ) { + const relationModelDef = requireModel(this.schema, relationModel); + const objArgs: Record> = {}; + + const descendantModels = getDelegateDescendantModels(this.schema, relationModel); + if (descendantModels.length > 0) { + // select all JSONs built from delegate descendants + Object.assign( + objArgs, + ...descendantModels.map((subModel) => ({ + [`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`]: eb.ref( + `${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`, + ), + })), + ); + } + + if (payload === true || !payload.select) { + // select all scalar fields except for omitted + const omit = typeof payload === 'object' ? payload.omit : undefined; + + Object.assign( + objArgs, + ...Object.entries(relationModelDef.fields) + .filter(([, value]) => !value.relation) + .filter(([name]) => !this.shouldOmitField(omit, relationModel, name)) + .map(([field]) => ({ + [field]: this.fieldRef(relationModel, field, relationModelAlias, false), + })), + ); + } else if (payload.select) { + // select specific fields + Object.assign( + objArgs, + ...Object.entries(payload.select) + .filter(([, value]) => value) + .map(([field, value]) => { + if (field === '_count') { + const subJson = this.buildCountJson( + relationModel as GetModels, + eb, + relationModelAlias, + value, + ); + return { [field]: subJson }; + } else { + const fieldDef = requireField(this.schema, relationModel, field); + const fieldValue = fieldDef.relation + ? // reference the synthesized JSON field + eb.ref(`${parentResultName}$${field}.$data`) + : // reference a plain field + this.fieldRef(relationModel, field, relationModelAlias, false); + return { [field]: fieldValue }; + } + }), + ); + } + + if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') { + // include relation fields + + Object.assign( + objArgs, + ...Object.entries(payload.include) + .filter(([, value]) => value) + .map(([field]) => ({ + [field]: eb.ref(`${parentResultName}$${field}.$data`), + })), + ); + } + + return objArgs; + } + + private buildRelationJoins( + query: SelectQueryBuilder, + relationModel: string, + relationModelAlias: string, + payload: true | FindArgs, true>, + parentResultName: string, + ) { + let result = query; + if (typeof payload === 'object') { + const selectInclude = payload.include ?? payload.select; + if (selectInclude && typeof selectInclude === 'object') { + Object.entries(selectInclude) + .filter(([, value]) => value) + .filter(([field]) => isRelationField(this.schema, relationModel, field)) + .forEach(([field, value]) => { + result = this.buildRelationJSON( + relationModel, + result, + field, + relationModelAlias, + value, + `${parentResultName}$${field}`, + ); + }); + } + } + return result; + } +} diff --git a/packages/orm/src/client/crud/dialects/mysql.ts b/packages/orm/src/client/crud/dialects/mysql.ts new file mode 100644 index 000000000..e196323a3 --- /dev/null +++ b/packages/orm/src/client/crud/dialects/mysql.ts @@ -0,0 +1,379 @@ +import { invariant } from '@zenstackhq/common-helpers'; +import Decimal from 'decimal.js'; +import type { AliasableExpression, TableExpression } from 'kysely'; +import { + expressionBuilder, + sql, + type Expression, + type ExpressionWrapper, + type SelectQueryBuilder, + type SqlBool, +} from 'kysely'; +import { match } from 'ts-pattern'; +import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; +import type { BuiltinType, FieldDef, SchemaDef } from '../../../schema'; +import type { SortOrder } from '../../crud-types'; +import { createInternalError, createInvalidInputError, createNotSupportedError } from '../../errors'; +import type { ClientOptions } from '../../options'; +import { isTypeDef } from '../../query-utils'; +import { LateralJoinDialectBase } from './lateral-join-dialect-base'; + +export class MySqlCrudDialect extends LateralJoinDialectBase { + constructor(schema: Schema, options: ClientOptions) { + super(schema, options); + } + + override get provider() { + return 'mysql' as const; + } + + // #region capabilities + + override get supportsUpdateWithLimit(): boolean { + return true; + } + + override get supportsDeleteWithLimit(): boolean { + return true; + } + + override get supportsDistinctOn(): boolean { + return false; + } + + override get supportsReturning(): boolean { + return false; + } + + override get supportsInsertDefaultValues(): boolean { + return false; + } + + override get supportsDefaultAsFieldValue() { + return true; + } + + override get insertIgnoreMethod() { + return 'ignore' as const; + } + + // #endregion + + // #region value transformation + + override transformInput(value: unknown, type: BuiltinType, forArrayField: boolean): unknown { + if (value === undefined) { + return value; + } + + // Handle special null classes for JSON fields + if (value instanceof JsonNullClass) { + return this.eb.cast(sql.lit('null'), 'json'); + } else if (value instanceof DbNullClass) { + return null; + } else if (value instanceof AnyNullClass) { + invariant(false, 'should not reach here: AnyNull is not a valid input value'); + } + + if (isTypeDef(this.schema, type)) { + // type-def fields (regardless array or scalar) are stored as scalar `Json` and + // their input values need to be stringified if not already (i.e., provided in + // default values) + if (typeof value !== 'string') { + return this.transformInput(value, 'Json', forArrayField); + } else { + return value; + } + } else if (Array.isArray(value)) { + if (type === 'Json') { + // type-def arrays reach here + return JSON.stringify(value); + } else { + throw createNotSupportedError(`MySQL does not support array literals`); + } + } else { + return match(type) + .with('Boolean', () => (value ? 1 : 0)) // MySQL uses 1/0 for boolean like SQLite + .with('DateTime', () => { + // MySQL DATETIME format: 'YYYY-MM-DD HH:MM:SS.mmm' + if (value instanceof Date) { + // force UTC + return value.toISOString().replace('Z', '+00:00'); + } else if (typeof value === 'string') { + // parse and force UTC + return new Date(value).toISOString().replace('Z', '+00:00'); + } else { + return value; + } + }) + .with('Decimal', () => (value !== null ? value.toString() : value)) + .with('Json', () => { + return this.eb.cast(this.eb.val(JSON.stringify(value)), 'json'); + }) + .with('Bytes', () => + Buffer.isBuffer(value) ? value : value instanceof Uint8Array ? Buffer.from(value) : value, + ) + .otherwise(() => value); + } + } + + override transformOutput(value: unknown, type: BuiltinType, array: boolean) { + if (value === null || value === undefined) { + return value; + } + return match(type) + .with('Boolean', () => this.transformOutputBoolean(value)) + .with('DateTime', () => this.transformOutputDate(value)) + .with('Bytes', () => this.transformOutputBytes(value)) + .with('BigInt', () => this.transformOutputBigInt(value)) + .with('Decimal', () => this.transformDecimal(value)) + .otherwise(() => super.transformOutput(value, type, array)); + } + + private transformOutputBoolean(value: unknown) { + return !!value; + } + + private transformOutputBigInt(value: unknown) { + if (typeof value === 'bigint') { + return value; + } + invariant( + typeof value === 'string' || typeof value === 'number', + `Expected string or number, got ${typeof value}`, + ); + return BigInt(value); + } + + private transformDecimal(value: unknown) { + if (value instanceof Decimal) { + return value; + } + invariant( + typeof value === 'string' || typeof value === 'number' || value instanceof Decimal, + `Expected string, number or Decimal, got ${typeof value}`, + ); + return new Decimal(value); + } + + private transformOutputDate(value: unknown) { + if (typeof value === 'string') { + // MySQL DateTime columns are returned as strings (non-ISO but parsable as JS Date), + // convert to ISO Date by appending 'Z' if not present + return new Date(!value.endsWith('Z') ? value + 'Z' : value); + } else if (value instanceof Date) { + return value; + } else { + return value; + } + } + + private transformOutputBytes(value: unknown) { + return Buffer.isBuffer(value) ? Uint8Array.from(value) : value; + } + + // #endregion + + // #region other overrides + + protected buildArrayAgg(arg: Expression): AliasableExpression { + return this.eb.fn.coalesce(sql`JSON_ARRAYAGG(${arg})`, sql`JSON_ARRAY()`); + } + + override buildSkipTake( + query: SelectQueryBuilder, + skip: number | undefined, + take: number | undefined, + ) { + if (take !== undefined) { + query = query.limit(take); + } + if (skip !== undefined) { + query = query.offset(skip); + if (take === undefined) { + // MySQL requires offset to be used with limit + query = query.limit(Number.MAX_SAFE_INTEGER); + } + } + return query; + } + + override buildJsonObject(value: Record>) { + return this.eb.fn( + 'JSON_OBJECT', + Object.entries(value).flatMap(([key, value]) => [sql.lit(key), value]), + ); + } + + override castInt>(expression: T): T { + return this.eb.cast(expression, sql.raw('unsigned')) as unknown as T; + } + + override castText>(expression: T): T { + // Use utf8mb4 character set collation to match MySQL 8.0+ default and avoid + // collation conflicts when comparing with VALUES ROW columns + return sql`CAST(${expression} AS CHAR CHARACTER SET utf8mb4)` as unknown as T; + } + + override trimTextQuotes>(expression: T): T { + return sql`TRIM(BOTH ${sql.lit('"')} FROM ${expression})` as unknown as T; + } + + override buildArrayLength(array: Expression): AliasableExpression { + return this.eb.fn('JSON_LENGTH', [array]); + } + + override buildArrayLiteralSQL(_values: unknown[]): AliasableExpression { + throw new Error('MySQL does not support array literals'); + } + + protected override buildJsonEqualityFilter( + lhs: Expression, + rhs: unknown, + ): ExpressionWrapper { + // MySQL's JSON equality comparison is key-order sensitive, use bi-directional JSON_CONTAINS + // instead to achieve key-order insensitive comparison + return this.eb.and([ + this.eb.fn('JSON_CONTAINS', [lhs, this.eb.val(JSON.stringify(rhs))]), + this.eb.fn('JSON_CONTAINS', [this.eb.val(JSON.stringify(rhs)), lhs]), + ]); + } + + protected override buildJsonPathSelection(receiver: Expression, path: string | undefined) { + if (path) { + return this.eb.fn('JSON_EXTRACT', [receiver, this.eb.val(path)]); + } else { + return receiver; + } + } + + protected override buildJsonArrayFilter( + lhs: Expression, + operation: 'array_contains' | 'array_starts_with' | 'array_ends_with', + value: unknown, + ) { + return match(operation) + .with('array_contains', () => { + const v = Array.isArray(value) ? value : [value]; + return sql`JSON_CONTAINS(${lhs}, ${sql.val(JSON.stringify(v))})`; + }) + .with('array_starts_with', () => + this.eb( + this.eb.fn('JSON_EXTRACT', [lhs, this.eb.val('$[0]')]), + '=', + this.transformInput(value, 'Json', false), + ), + ) + .with('array_ends_with', () => + this.eb( + sql`JSON_EXTRACT(${lhs}, CONCAT('$[', JSON_LENGTH(${lhs}) - 1, ']'))`, + '=', + this.transformInput(value, 'Json', false), + ), + ) + .exhaustive(); + } + + protected override buildJsonArrayExistsPredicate( + receiver: Expression, + buildFilter: (elem: Expression) => Expression, + ) { + // MySQL doesn't have a direct json_array_elements, we need to use JSON_TABLE or a different approach + // For simplicity, we'll use EXISTS with a subquery that unnests the JSON array + return this.eb.exists( + this.eb + .selectFrom(sql`JSON_TABLE(${receiver}, '$[*]' COLUMNS(value JSON PATH '$'))`.as('$items')) + .select(this.eb.lit(1).as('$t')) + .where(buildFilter(this.eb.ref('$items.value'))), + ); + } + + override getFieldSqlType(fieldDef: FieldDef) { + // TODO: respect `@db.x` attributes + if (fieldDef.relation) { + throw createInternalError('Cannot get SQL type of a relation field'); + } + + let result: string; + + if (this.schema.enums?.[fieldDef.type]) { + // enums are treated as text/varchar + result = 'varchar(255)'; + } else { + result = match(fieldDef.type) + .with('String', () => 'varchar(255)') + .with('Boolean', () => 'tinyint(1)') // MySQL uses tinyint(1) for boolean + .with('Int', () => 'int') + .with('BigInt', () => 'bigint') + .with('Float', () => 'double') + .with('Decimal', () => 'decimal') + .with('DateTime', () => 'datetime') + .with('Bytes', () => 'blob') + .with('Json', () => 'json') + // fallback to text + .otherwise(() => 'text'); + } + + if (fieldDef.array) { + // MySQL stores arrays as JSON + result = 'json'; + } + + return result; + } + + override getStringCasingBehavior() { + // MySQL LIKE is case-insensitive by default (depends on collation), no ILIKE support + return { supportsILike: false, likeCaseSensitive: false }; + } + + override buildValuesTableSelect(fields: FieldDef[], rows: unknown[][]) { + const cols = rows[0]?.length ?? 0; + + if (fields.length !== cols) { + throw createInvalidInputError('Number of fields must match number of columns in each row'); + } + + // check all rows have the same length + for (const row of rows) { + if (row.length !== cols) { + throw createInvalidInputError('All rows must have the same number of columns'); + } + } + + // build final alias name as `$values(f1, f2, ...)` + const aliasWithColumns = `$values(${fields.map((f) => f.name).join(', ')})`; + + const eb = expressionBuilder(); + + return eb + .selectFrom( + sql`(VALUES ${sql.join( + rows.map((row) => sql`ROW(${sql.join(row.map((v) => sql.val(v)))})`), + sql.raw(', '), + )}) as ${sql.raw(aliasWithColumns)}` as unknown as TableExpression, + ) + .selectAll(); + } + + protected override buildOrderByField( + query: SelectQueryBuilder, + field: Expression, + sort: SortOrder, + nulls: 'first' | 'last', + ) { + let result = query; + if (nulls === 'first') { + // NULLS FIRST: order by IS NULL DESC (nulls=1 first), then the actual field + result = result.orderBy(sql`${field} IS NULL`, 'desc'); + result = result.orderBy(field, sort); + } else { + // NULLS LAST: order by IS NULL ASC (nulls=0 last), then the actual field + result = result.orderBy(sql`${field} IS NULL`, 'asc'); + result = result.orderBy(field, sort); + } + return result; + } + + // #endregion +} diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index bae049077..02e9435fa 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -1,11 +1,10 @@ import { invariant } from '@zenstackhq/common-helpers'; import Decimal from 'decimal.js'; import { + expressionBuilder, sql, + type AliasableExpression, type Expression, - type ExpressionBuilder, - type ExpressionWrapper, - type RawBuilder, type SelectQueryBuilder, type SqlBool, } from 'kysely'; @@ -13,26 +12,14 @@ import { parse as parsePostgresArray } from 'postgres-array'; import { match } from 'ts-pattern'; import z from 'zod'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; -import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema'; -import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants'; -import type { FindArgs } from '../../crud-types'; -import { createInternalError } from '../../errors'; +import type { BuiltinType, FieldDef, SchemaDef } from '../../../schema'; +import type { SortOrder } from '../../crud-types'; +import { createInternalError, createInvalidInputError } from '../../errors'; import type { ClientOptions } from '../../options'; -import { - buildJoinPairs, - getDelegateDescendantModels, - getEnum, - getManyToManyRelation, - isEnum, - isRelationField, - isTypeDef, - requireField, - requireIdFields, - requireModel, -} from '../../query-utils'; -import { BaseCrudDialect } from './base-dialect'; - -export class PostgresCrudDialect extends BaseCrudDialect { +import { getEnum, isEnum, isTypeDef } from '../../query-utils'; +import { LateralJoinDialectBase } from './lateral-join-dialect-base'; + +export class PostgresCrudDialect extends LateralJoinDialectBase { private isoDateSchema = z.iso.datetime({ local: true, offset: true }); constructor(schema: Schema, options: ClientOptions) { @@ -43,7 +30,41 @@ export class PostgresCrudDialect extends BaseCrudDiale return 'postgresql' as const; } - override transformPrimitive(value: unknown, type: BuiltinType, forArrayField: boolean): unknown { + // #region capabilities + + override get supportsUpdateWithLimit(): boolean { + return false; + } + + override get supportsDeleteWithLimit(): boolean { + return false; + } + + override get supportsDistinctOn(): boolean { + return true; + } + + override get supportsReturning(): boolean { + return true; + } + + override get supportsDefaultAsFieldValue() { + return true; + } + + override get supportsInsertDefaultValues(): boolean { + return true; + } + + override get insertIgnoreMethod() { + return 'onConflict' as const; + } + + // #endregion + + // #region value transformation + + override transformInput(value: unknown, type: BuiltinType, forArrayField: boolean): unknown { if (value === undefined) { return value; } @@ -79,14 +100,14 @@ export class PostgresCrudDialect extends BaseCrudDiale // cast to enum array `CAST(ARRAY[...] AS "enum_type"[])` return this.eb.cast( sql`ARRAY[${sql.join( - value.map((v) => this.transformPrimitive(v, type, false)), + value.map((v) => this.transformInput(v, type, false)), sql.raw(','), )}]`, this.createSchemaQualifiedEnumType(type, true), ); } else { // `Json[]` fields need their input as array (not stringified) - return value.map((v) => this.transformPrimitive(v, type, false)); + return value.map((v) => this.transformInput(v, type, false)); } } else { return match(type) @@ -99,7 +120,12 @@ export class PostgresCrudDialect extends BaseCrudDiale ) .with('Decimal', () => (value !== null ? value.toString() : value)) .with('Json', () => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { + if ( + value === null || + typeof value === 'string' || + typeof value === 'number' || + typeof value === 'boolean' + ) { // postgres requires simple JSON values to be stringified return JSON.stringify(value); } else { @@ -219,264 +245,12 @@ export class PostgresCrudDialect extends BaseCrudDiale return value; } - override buildRelationSelection( - query: SelectQueryBuilder, - model: string, - relationField: string, - parentAlias: string, - payload: true | FindArgs, true>, - ): SelectQueryBuilder { - const relationResultName = `${parentAlias}$${relationField}`; - const joinedQuery = this.buildRelationJSON( - model, - query, - relationField, - parentAlias, - payload, - relationResultName, - ); - return joinedQuery.select(`${relationResultName}.$data as ${relationField}`); - } + // #endregion - private buildRelationJSON( - model: string, - qb: SelectQueryBuilder, - relationField: string, - parentAlias: string, - payload: true | FindArgs, true>, - resultName: string, - ) { - const relationFieldDef = requireField(this.schema, model, relationField); - const relationModel = relationFieldDef.type as GetModels; - - return qb.leftJoinLateral( - (eb) => { - const relationSelectName = `${resultName}$sub`; - const relationModelDef = requireModel(this.schema, relationModel); - - let tbl: SelectQueryBuilder; - - if (this.canJoinWithoutNestedSelect(relationModelDef, payload)) { - // build join directly - tbl = this.buildModelSelect(relationModel, relationSelectName, payload, false); - - // parent join filter - tbl = this.buildRelationJoinFilter( - tbl, - model, - relationField, - relationModel, - relationSelectName, - parentAlias, - ); - } else { - // join with a nested query - tbl = eb.selectFrom(() => { - let subQuery = this.buildModelSelect(relationModel, `${relationSelectName}$t`, payload, true); - - // parent join filter - subQuery = this.buildRelationJoinFilter( - subQuery, - model, - relationField, - relationModel, - `${relationSelectName}$t`, - parentAlias, - ); - - return subQuery.as(relationSelectName); - }); - } - - // select relation result - tbl = this.buildRelationObjectSelect( - relationModel, - relationSelectName, - relationFieldDef, - tbl, - payload, - resultName, - ); - - // add nested joins for each relation - tbl = this.buildRelationJoins(tbl, relationModel, relationSelectName, payload, resultName); - - // alias the join table - return tbl.as(resultName); - }, - (join) => join.onTrue(), - ); - } + // #region other overrides - private buildRelationJoinFilter( - query: SelectQueryBuilder, - model: string, - relationField: string, - relationModel: GetModels, - relationModelAlias: string, - parentAlias: string, - ) { - const m2m = getManyToManyRelation(this.schema, model, relationField); - if (m2m) { - // many-to-many relation - const parentIds = requireIdFields(this.schema, model); - const relationIds = requireIdFields(this.schema, relationModel); - invariant(parentIds.length === 1, 'many-to-many relation must have exactly one id field'); - invariant(relationIds.length === 1, 'many-to-many relation must have exactly one id field'); - query = query.where((eb) => - eb( - eb.ref(`${relationModelAlias}.${relationIds[0]}`), - 'in', - eb - .selectFrom(m2m.joinTable) - .select(`${m2m.joinTable}.${m2m.otherFkName}`) - .whereRef(`${parentAlias}.${parentIds[0]}`, '=', `${m2m.joinTable}.${m2m.parentFkName}`), - ), - ); - } else { - const joinPairs = buildJoinPairs(this.schema, model, parentAlias, relationField, relationModelAlias); - query = query.where((eb) => - this.and(...joinPairs.map(([left, right]) => eb(this.eb.ref(left), '=', this.eb.ref(right)))), - ); - } - return query; - } - - private buildRelationObjectSelect( - relationModel: string, - relationModelAlias: string, - relationFieldDef: FieldDef, - qb: SelectQueryBuilder, - payload: true | FindArgs, true>, - parentResultName: string, - ) { - qb = qb.select((eb) => { - const objArgs = this.buildRelationObjectArgs( - relationModel, - relationModelAlias, - eb, - payload, - parentResultName, - ); - - if (relationFieldDef.array) { - return eb.fn - .coalesce(sql`jsonb_agg(jsonb_build_object(${sql.join(objArgs)}))`, sql`'[]'::jsonb`) - .as('$data'); - } else { - return sql`jsonb_build_object(${sql.join(objArgs)})`.as('$data'); - } - }); - - return qb; - } - - private buildRelationObjectArgs( - relationModel: string, - relationModelAlias: string, - eb: ExpressionBuilder, - payload: true | FindArgs, true>, - parentResultName: string, - ) { - const relationModelDef = requireModel(this.schema, relationModel); - const objArgs: Array< - string | ExpressionWrapper | SelectQueryBuilder | RawBuilder - > = []; - - const descendantModels = getDelegateDescendantModels(this.schema, relationModel); - if (descendantModels.length > 0) { - // select all JSONs built from delegate descendants - objArgs.push( - ...descendantModels - .map((subModel) => [ - sql.lit(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`), - eb.ref(`${DELEGATE_JOINED_FIELD_PREFIX}${subModel.name}`), - ]) - .flatMap((v) => v), - ); - } - - if (payload === true || !payload.select) { - // select all scalar fields except for omitted - const omit = typeof payload === 'object' ? payload.omit : undefined; - objArgs.push( - ...Object.entries(relationModelDef.fields) - .filter(([, value]) => !value.relation) - .filter(([name]) => !this.shouldOmitField(omit, relationModel, name)) - .map(([field]) => [sql.lit(field), this.fieldRef(relationModel, field, relationModelAlias, false)]) - .flatMap((v) => v), - ); - } else if (payload.select) { - // select specific fields - objArgs.push( - ...Object.entries(payload.select) - .filter(([, value]) => value) - .map(([field, value]) => { - if (field === '_count') { - const subJson = this.buildCountJson( - relationModel as GetModels, - eb, - relationModelAlias, - value, - ); - return [sql.lit(field), subJson]; - } else { - const fieldDef = requireField(this.schema, relationModel, field); - const fieldValue = fieldDef.relation - ? // reference the synthesized JSON field - eb.ref(`${parentResultName}$${field}.$data`) - : // reference a plain field - this.fieldRef(relationModel, field, relationModelAlias, false); - return [sql.lit(field), fieldValue]; - } - }) - .flatMap((v) => v), - ); - } - - if (typeof payload === 'object' && payload.include && typeof payload.include === 'object') { - // include relation fields - objArgs.push( - ...Object.entries(payload.include) - .filter(([, value]) => value) - .map(([field]) => [ - sql.lit(field), - // reference the synthesized JSON field - eb.ref(`${parentResultName}$${field}.$data`), - ]) - .flatMap((v) => v), - ); - } - return objArgs; - } - - private buildRelationJoins( - query: SelectQueryBuilder, - relationModel: string, - relationModelAlias: string, - payload: true | FindArgs, true>, - parentResultName: string, - ) { - let result = query; - if (typeof payload === 'object') { - const selectInclude = payload.include ?? payload.select; - if (selectInclude && typeof selectInclude === 'object') { - Object.entries(selectInclude) - .filter(([, value]) => value) - .filter(([field]) => isRelationField(this.schema, relationModel, field)) - .forEach(([field, value]) => { - result = this.buildRelationJSON( - relationModel, - result, - field, - relationModelAlias, - value, - `${parentResultName}$${field}`, - ); - }); - } - } - return result; + protected buildArrayAgg(arg: Expression) { + return this.eb.fn.coalesce(sql`jsonb_agg(${arg})`, sql`'[]'::jsonb`); } override buildSkipTake( @@ -500,27 +274,30 @@ export class PostgresCrudDialect extends BaseCrudDiale ); } - override get supportsUpdateWithLimit(): boolean { - return false; + override castInt>(expression: T): T { + return this.eb.cast(expression, 'integer') as unknown as T; } - override get supportsDeleteWithLimit(): boolean { - return false; + override castText>(expression: T): T { + return this.eb.cast(expression, 'text') as unknown as T; } - override get supportsDistinctOn(): boolean { - return true; + override trimTextQuotes>(expression: T): T { + return this.eb.fn('trim', [expression, sql.lit('"')]) as unknown as T; } - override buildArrayLength(array: Expression): ExpressionWrapper { + override buildArrayLength(array: Expression): AliasableExpression { return this.eb.fn('array_length', [array]); } - override buildArrayLiteralSQL(values: unknown[]): string { + override buildArrayLiteralSQL(values: unknown[]): AliasableExpression { if (values.length === 0) { - return '{}'; + return sql`{}`; } else { - return `ARRAY[${values.map((v) => (typeof v === 'string' ? `'${v}'` : v))}]`; + return sql`ARRAY[${sql.join( + values.map((v) => sql.val(v)), + sql.raw(','), + )}]`; } } @@ -546,14 +323,14 @@ export class PostgresCrudDialect extends BaseCrudDiale this.eb( this.eb.fn('jsonb_extract_path', [lhs, this.eb.val('0')]), '=', - this.transformPrimitive(value, 'Json', false), + this.transformInput(value, 'Json', false), ), ) .with('array_ends_with', () => this.eb( this.eb.fn('jsonb_extract_path', [lhs, sql`(jsonb_array_length(${lhs}) - 1)::text`]), '=', - this.transformPrimitive(value, 'Json', false), + this.transformInput(value, 'Json', false), ), ) .exhaustive(); @@ -571,10 +348,6 @@ export class PostgresCrudDialect extends BaseCrudDiale ); } - override get supportInsertWithDefault() { - return true; - } - override getFieldSqlType(fieldDef: FieldDef) { // TODO: respect `@db.x` attributes if (fieldDef.relation) { @@ -612,4 +385,53 @@ export class PostgresCrudDialect extends BaseCrudDiale // Postgres `LIKE` is case-sensitive, `ILIKE` is case-insensitive return { supportsILike: true, likeCaseSensitive: true }; } + + override buildValuesTableSelect(fields: FieldDef[], rows: unknown[][]) { + if (rows.length === 0) { + throw createInvalidInputError('At least one row is required to build values table'); + } + + // check all rows have the same length + const rowLength = rows[0]!.length; + + if (fields.length !== rowLength) { + throw createInvalidInputError('Number of fields must match number of columns in each row'); + } + + for (const row of rows) { + if (row.length !== rowLength) { + throw createInvalidInputError('All rows must have the same number of columns'); + } + } + + const eb = expressionBuilder(); + + return eb + .selectFrom( + sql`(VALUES ${sql.join( + rows.map((row) => sql`(${sql.join(row.map((v) => sql.val(v)))})`), + sql.raw(', '), + )})`.as('$values'), + ) + .select( + fields.map((f, i) => + sql`CAST(${sql.ref(`$values.column${i + 1}`)} AS ${sql.raw(this.getFieldSqlType(f))})`.as(f.name), + ), + ); + } + + protected override buildOrderByField( + query: SelectQueryBuilder, + field: Expression, + sort: SortOrder, + nulls: 'first' | 'last', + ) { + return query.orderBy(field, (ob) => { + ob = sort === 'asc' ? ob.asc() : ob.desc(); + ob = nulls === 'first' ? ob.nullsFirst() : ob.nullsLast(); + return ob; + }); + } + + // #endregion } diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 32bb4e4ed..4ef87a464 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -1,8 +1,9 @@ import { invariant } from '@zenstackhq/common-helpers'; import Decimal from 'decimal.js'; import { - ExpressionWrapper, + expressionBuilder, sql, + type AliasableExpression, type Expression, type ExpressionBuilder, type RawBuilder, @@ -13,8 +14,8 @@ import { match } from 'ts-pattern'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; import type { BuiltinType, FieldDef, GetModels, SchemaDef } from '../../../schema'; import { DELEGATE_JOINED_FIELD_PREFIX } from '../../constants'; -import type { FindArgs } from '../../crud-types'; -import { createInternalError, createNotSupportedError } from '../../errors'; +import type { FindArgs, SortOrder } from '../../crud-types'; +import { createInternalError, createInvalidInputError, createNotSupportedError } from '../../errors'; import { getDelegateDescendantModels, getManyToManyRelation, @@ -30,7 +31,41 @@ export class SqliteCrudDialect extends BaseCrudDialect return 'sqlite' as const; } - override transformPrimitive(value: unknown, type: BuiltinType, _forArrayField: boolean): unknown { + // #region capabilities + + override get supportsUpdateWithLimit() { + return false; + } + + override get supportsDeleteWithLimit() { + return false; + } + + override get supportsDistinctOn() { + return false; + } + + override get supportsReturning() { + return true; + } + + override get supportsDefaultAsFieldValue() { + return false; + } + + override get supportsInsertDefaultValues(): boolean { + return true; + } + + override get insertIgnoreMethod() { + return 'onConflict' as const; + } + + // #endregion + + // #region value transformation + + override transformInput(value: unknown, type: BuiltinType, _forArrayField: boolean): unknown { if (value === undefined) { return value; } @@ -50,7 +85,7 @@ export class SqliteCrudDialect extends BaseCrudDialect } if (Array.isArray(value)) { - return value.map((v) => this.transformPrimitive(v, type, false)); + return value.map((v) => this.transformInput(v, type, false)); } else { return match(type) .with('Boolean', () => (value ? 1 : 0)) @@ -137,6 +172,10 @@ export class SqliteCrudDialect extends BaseCrudDialect return value; } + // #endregion + + // #region other overrides + override buildRelationSelection( query: SelectQueryBuilder, model: string, @@ -404,28 +443,24 @@ export class SqliteCrudDialect extends BaseCrudDialect ); } - override get supportsUpdateWithLimit() { - return false; - } - - override get supportsDeleteWithLimit() { - return false; + override buildArrayLength(array: Expression): AliasableExpression { + return this.eb.fn('json_array_length', [array]); } - override get supportsDistinctOn() { - return false; + override buildArrayLiteralSQL(_values: unknown[]): AliasableExpression { + throw new Error('SQLite does not support array literals'); } - override buildArrayLength(array: Expression): ExpressionWrapper { - return this.eb.fn('json_array_length', [array]); + override castInt>(expression: T): T { + return expression; } - override buildArrayLiteralSQL(_values: unknown[]): string { - throw new Error('SQLite does not support array literals'); + override castText>(expression: T): T { + return this.eb.cast(expression, 'text') as unknown as T; } - override get supportInsertWithDefault() { - return false; + override trimTextQuotes>(expression: T): T { + return this.eb.fn('trim', [expression, sql.lit('"')]) as unknown as T; } override getFieldSqlType(fieldDef: FieldDef) { @@ -462,4 +497,48 @@ export class SqliteCrudDialect extends BaseCrudDialect // SQLite `LIKE` is case-insensitive, and there is no `ILIKE` return { supportsILike: false, likeCaseSensitive: false }; } + + override buildValuesTableSelect(fields: FieldDef[], rows: unknown[][]) { + if (rows.length === 0) { + throw createInvalidInputError('At least one row is required to build values table'); + } + + // check all rows have the same length + const rowLength = rows[0]!.length; + + if (fields.length !== rowLength) { + throw createInvalidInputError('Number of fields must match number of columns in each row'); + } + + for (const row of rows) { + if (row.length !== rowLength) { + throw createInvalidInputError('All rows must have the same number of columns'); + } + } + + const eb = expressionBuilder(); + + return eb + .selectFrom( + sql`(VALUES ${sql.join( + rows.map((row) => sql`(${sql.join(row.map((v) => sql.val(v)))})`), + sql.raw(', '), + )})`.as('$values'), + ) + .select(fields.map((f, i) => eb.ref(`$values.column${i + 1}`).as(f.name))); + } + + protected override buildOrderByField( + query: SelectQueryBuilder, + field: Expression, + sort: SortOrder, + nulls: 'first' | 'last', + ) { + return query.orderBy(field, (ob) => { + ob = sort === 'asc' ? ob.asc() : ob.desc(); + ob = nulls === 'first' ? ob.nullsFirst() : ob.nullsLast(); + return ob; + }); + } + // #endregion } diff --git a/packages/orm/src/client/crud/operations/aggregate.ts b/packages/orm/src/client/crud/operations/aggregate.ts index f92a85184..da7af7b1d 100644 --- a/packages/orm/src/client/crud/operations/aggregate.ts +++ b/packages/orm/src/client/crud/operations/aggregate.ts @@ -52,7 +52,14 @@ export class AggregateOperationHandler extends BaseOpe subQuery = this.dialect.buildSkipTake(subQuery, skip, take); // orderBy - subQuery = this.dialect.buildOrderBy(subQuery, this.model, this.model, parsedArgs.orderBy, negateOrderBy); + subQuery = this.dialect.buildOrderBy( + subQuery, + this.model, + this.model, + parsedArgs.orderBy, + negateOrderBy, + take, + ); return subQuery.as('$sub'); }); @@ -62,18 +69,18 @@ export class AggregateOperationHandler extends BaseOpe switch (key) { case '_count': { if (value === true) { - query = query.select((eb) => eb.cast(eb.fn.countAll(), 'integer').as('_count')); + query = query.select((eb) => this.dialect.castInt(eb.fn.countAll()).as('_count')); } else { Object.entries(value).forEach(([field, val]) => { if (val === true) { if (field === '_all') { query = query.select((eb) => - eb.cast(eb.fn.countAll(), 'integer').as(`_count._all`), + this.dialect.castInt(eb.fn.countAll()).as(`_count._all`), ); } else { query = query.select((eb) => - eb - .cast(eb.fn.count(eb.ref(`$sub.${field}` as any)), 'integer') + this.dialect + .castInt(eb.fn.count(eb.ref(`$sub.${field}`))) .as(`${key}.${field}`), ); } @@ -96,7 +103,7 @@ export class AggregateOperationHandler extends BaseOpe .with('_max', () => eb.fn.max) .with('_min', () => eb.fn.min) .exhaustive(); - return fn(eb.ref(`$sub.${field}` as any)).as(`${key}.${field}`); + return fn(eb.ref(`$sub.${field}`)).as(`${key}.${field}`); }); } }); diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 5eb4b2d1d..33cc1a641 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -8,6 +8,7 @@ import { sql, UpdateResult, type Compilable, + type ExpressionBuilder, type IsolationLevel, type QueryResult, type SelectQueryBuilder, @@ -430,13 +431,9 @@ export abstract class BaseOperationHandler { Array.isArray(value.set) ) { // deal with nested "set" for scalar lists - createFields[field] = this.dialect.transformPrimitive( - value.set, - fieldDef.type as BuiltinType, - true, - ); + createFields[field] = this.dialect.transformInput(value.set, fieldDef.type as BuiltinType, true); } else { - createFields[field] = this.dialect.transformPrimitive( + createFields[field] = this.dialect.transformInput( value, fieldDef.type as BuiltinType, !!fieldDef.array, @@ -469,19 +466,77 @@ export abstract class BaseOperationHandler { // return id fields if no returnFields specified returnFields = returnFields ?? requireIdFields(this.schema, model); - const query = kysely - .insertInto(model) - .$if(Object.keys(updatedData).length === 0, (qb) => qb.defaultValues()) - .$if(Object.keys(updatedData).length > 0, (qb) => qb.values(updatedData)) - .returning(returnFields as any) - .modifyEnd( - this.makeContextComment({ - model, - operation: 'create', - }), - ); + let createdEntity: any; + + if (this.dialect.supportsReturning) { + const query = kysely + .insertInto(model) + .$if(Object.keys(updatedData).length === 0, (qb) => + qb + // case for `INSERT INTO ... DEFAULT VALUES` syntax + .$if(this.dialect.supportsInsertDefaultValues, () => qb.defaultValues()) + // case for `INSERT INTO ... VALUES ({})` syntax + .$if(!this.dialect.supportsInsertDefaultValues, () => qb.values({})), + ) + .$if(Object.keys(updatedData).length > 0, (qb) => qb.values(updatedData)) + .returning(returnFields as any) + .modifyEnd( + this.makeContextComment({ + model, + operation: 'create', + }), + ); + + createdEntity = await this.executeQueryTakeFirst(kysely, query, 'create'); + } else { + // Fallback for databases that don't support RETURNING (e.g., MySQL) + const insertQuery = kysely + .insertInto(model) + .$if(Object.keys(updatedData).length === 0, (qb) => + qb + // case for `INSERT INTO ... DEFAULT VALUES` syntax + .$if(this.dialect.supportsInsertDefaultValues, () => qb.defaultValues()) + // case for `INSERT INTO ... VALUES ({})` syntax + .$if(!this.dialect.supportsInsertDefaultValues, () => qb.values({})), + ) + .$if(Object.keys(updatedData).length > 0, (qb) => qb.values(updatedData)) + .modifyEnd( + this.makeContextComment({ + model, + operation: 'create', + }), + ); + + const insertResult = await this.executeQuery(kysely, insertQuery, 'create'); + + // Build WHERE clause to find the inserted record + const idFields = requireIdFields(this.schema, model); + const idValues: Record = {}; - const createdEntity = await this.executeQueryTakeFirst(kysely, query, 'create'); + for (const idField of idFields) { + if (insertResult.insertId !== undefined && insertResult.insertId !== null) { + const fieldDef = this.requireField(model, idField); + if (this.isAutoIncrementField(fieldDef)) { + // auto-generated id value + idValues[idField] = insertResult.insertId; + continue; + } + } + + if (updatedData[idField] !== undefined) { + // ID was provided in the insert + idValues[idField] = updatedData[idField]; + } else { + throw createInternalError( + `Cannot determine ID field "${idField}" value for created model "${model}"`, + ); + } + } + + // for dialects that don't support RETURNING, the outside logic will always + // read back the created record, we just return the id fields here + createdEntity = idValues; + } if (Object.keys(postCreateRelations).length > 0) { // process nested creates that need to happen after the current entity is created @@ -513,6 +568,14 @@ export abstract class BaseOperationHandler { return createdEntity; } + private isAutoIncrementField(fieldDef: FieldDef) { + return ( + fieldDef.default && + ExpressionUtils.isCall(fieldDef.default) && + fieldDef.default.function === 'autoincrement' + ); + } + private async processBaseModelCreate(kysely: ToKysely, model: string, createFields: any, forModel: string) { const thisCreateFields: any = {}; const remainingFields: any = {}; @@ -618,7 +681,12 @@ export abstract class BaseOperationHandler { A: sortedRecords[0]!.entity[firstIds[0]!], B: sortedRecords[1]!.entity[secondIds[0]!], } as any) - .onConflict((oc) => oc.columns(['A', 'B'] as any).doNothing()) + // case for `INSERT IGNORE` or `ON CONFLICT DO NOTHING` syntax + .$if(this.dialect.insertIgnoreMethod === 'onConflict', (qb) => + qb.onConflict((oc) => oc.columns(['A', 'B'] as any).doNothing()), + ) + // case for `INSERT IGNORE` syntax + .$if(this.dialect.insertIgnoreMethod === 'ignore', (qb) => qb.ignore()) .execute(); return result[0] as any; } else { @@ -794,7 +862,7 @@ export abstract class BaseOperationHandler { const modelDef = this.requireModel(model); - let relationKeyPairs: { fk: string; pk: string }[] = []; + const relationKeyPairs: { fk: string; pk: string }[] = []; if (fromRelation) { const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( this.schema, @@ -804,7 +872,7 @@ export abstract class BaseOperationHandler { if (ownedByModel) { throw createInvalidInputError('incorrect relation hierarchy for createMany', model); } - relationKeyPairs = keyPairs; + relationKeyPairs.push(...keyPairs); } let createData = enumerate(input.data).map((item) => { @@ -812,7 +880,7 @@ export abstract class BaseOperationHandler { for (const [name, value] of Object.entries(item)) { const fieldDef = this.requireField(model, name); invariant(!fieldDef.relation, 'createMany does not support relations'); - newItem[name] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType, !!fieldDef.array); + newItem[name] = this.dialect.transformInput(value, fieldDef.type as BuiltinType, !!fieldDef.array); } if (fromRelation) { for (const { fk, pk } of relationKeyPairs) { @@ -822,7 +890,7 @@ export abstract class BaseOperationHandler { return this.fillGeneratedAndDefaultValues(modelDef, newItem); }); - if (!this.dialect.supportInsertWithDefault) { + if (!this.dialect.supportsDefaultAsFieldValue) { // if the dialect doesn't support `DEFAULT` as insert field values, // we need to double check if data rows have mismatching fields, and // if so, make sure all fields have default value filled if not provided @@ -846,7 +914,7 @@ export abstract class BaseOperationHandler { fieldDef.default !== null && typeof fieldDef.default !== 'object' ) { - item[field] = this.dialect.transformPrimitive( + item[field] = this.dialect.transformInput( fieldDef.default, fieldDef.type as BuiltinType, !!fieldDef.array, @@ -876,7 +944,13 @@ export abstract class BaseOperationHandler { const query = kysely .insertInto(model) .values(createData) - .$if(!!input.skipDuplicates, (qb) => qb.onConflict((oc) => oc.doNothing())) + .$if(!!input.skipDuplicates, (qb) => + qb + // case for `INSERT ... ON CONFLICT DO NOTHING` syntax + .$if(this.dialect.insertIgnoreMethod === 'onConflict', () => qb.onConflict((oc) => oc.doNothing())) + // case for `INSERT IGNORE` syntax + .$if(this.dialect.insertIgnoreMethod === 'ignore', () => qb.ignore()), + ) .modifyEnd( this.makeContextComment({ model, @@ -889,8 +963,21 @@ export abstract class BaseOperationHandler { return { count: Number(result.numAffectedRows) } as Result; } else { fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model); - const result = await query.returning(fieldsToReturn as any).execute(); - return result as Result; + + if (this.dialect.supportsReturning) { + const result = await query.returning(fieldsToReturn as any).execute(); + return result as Result; + } else { + // Fallback for databases that don't support RETURNING (e.g., MySQL) + // For createMany without RETURNING, we can't reliably get all inserted records + // especially with auto-increment IDs. The best we can do is return the count. + // If users need the created records, they should use multiple create() calls + // or the application should query after insertion. + throw createNotSupportedError( + `\`createManyAndReturn\` is not supported for ${this.dialect.provider}. ` + + `Use multiple \`create\` calls or query the records after insertion.`, + ); + } } } @@ -923,12 +1010,21 @@ export abstract class BaseOperationHandler { } // create base model entity - const baseEntities = await this.createMany( - kysely, - model as GetModels, - { data: thisCreateRows, skipDuplicates }, - true, - ); + let baseEntities: unknown[]; + if (this.dialect.supportsReturning) { + baseEntities = await this.createMany( + kysely, + model as GetModels, + { data: thisCreateRows, skipDuplicates }, + true, + ); + } else { + // fall back to multiple creates if RETURNING is not supported + baseEntities = []; + for (const row of thisCreateRows) { + baseEntities.push(await this.create(kysely, model, row, undefined, true)); + } + } // copy over id fields from base model for (let i = 0; i < baseEntities.length; i++) { @@ -950,7 +1046,7 @@ export abstract class BaseOperationHandler { if (typeof fieldDef?.default === 'object' && 'kind' in fieldDef.default) { const generated = this.evalGenerator(fieldDef.default); if (generated !== undefined) { - values[field] = this.dialect.transformPrimitive( + values[field] = this.dialect.transformInput( generated, fieldDef.type as BuiltinType, !!fieldDef.array, @@ -958,7 +1054,7 @@ export abstract class BaseOperationHandler { } } else if (fieldDef?.updatedAt) { // TODO: should this work at kysely level instead? - values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); + values[field] = this.dialect.transformInput(new Date(), 'DateTime', false); } else if (fieldDef?.default !== undefined) { let value = fieldDef.default; if (fieldDef.type === 'Json') { @@ -969,11 +1065,7 @@ export abstract class BaseOperationHandler { value = JSON.parse(value); } } - values[field] = this.dialect.transformPrimitive( - value, - fieldDef.type as BuiltinType, - !!fieldDef.array, - ); + values[field] = this.dialect.transformInput(value, fieldDef.type as BuiltinType, !!fieldDef.array); } } } @@ -1043,39 +1135,7 @@ export abstract class BaseOperationHandler { throw createInvalidInputError('data must be an object'); } - const parentWhere: any = {}; - let m2m: ReturnType = undefined; - - if (fromRelation) { - m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); - if (!m2m) { - // merge foreign key conditions from the relation - const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( - this.schema, - fromRelation.model, - fromRelation.field, - ); - if (ownedByModel) { - const fromEntity = await this.readUnique(kysely, fromRelation.model as GetModels, { - where: fromRelation.ids, - }); - for (const { fk, pk } of keyPairs) { - parentWhere[pk] = fromEntity[fk]; - } - } else { - for (const { fk, pk } of keyPairs) { - parentWhere[fk] = fromRelation.ids[pk]; - } - } - } else { - // many-to-many relation, filter for parent with "some" - const fromRelationFieldDef = this.requireField(fromRelation.model, fromRelation.field); - invariant(fromRelationFieldDef.relation?.opposite); - parentWhere[fromRelationFieldDef.relation.opposite] = { - some: fromRelation.ids, - }; - } - } + const parentWhere = await this.buildUpdateParentRelationFilter(kysely, fromRelation); let combinedWhere: WhereInput, false> = where ?? {}; if (Object.keys(parentWhere).length > 0) { @@ -1088,11 +1148,11 @@ export abstract class BaseOperationHandler { // fill in automatically updated fields const autoUpdatedFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { - if (fieldDef.updatedAt) { + if (fieldDef.updatedAt && finalData[fieldName] === undefined) { if (finalData === data) { finalData = clone(data); } - finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); + finalData[fieldName] = this.dialect.transformInput(new Date(), 'DateTime', false); autoUpdatedFields.push(fieldName); } } @@ -1114,11 +1174,18 @@ export abstract class BaseOperationHandler { } let needIdRead = false; - if (modelDef.baseModel && !this.isIdFilter(model, combinedWhere)) { - // when updating a model with delegate base, base fields may be referenced in the filter, - // so we read the id out if the filter is not ready an id filter, and and use it as the - // update filter instead - needIdRead = true; + if (!this.isIdFilter(model, combinedWhere)) { + if (modelDef.baseModel) { + // when updating a model with delegate base, base fields may be referenced in the filter, + // so we read the id out if the filter is not ready an id filter, and and use it as the + // update filter instead + needIdRead = true; + } + if (!this.dialect.supportsReturning) { + // for dialects that don't support RETURNING, we need to read the id fields + // to identify the updated entity + needIdRead = true; + } } if (needIdRead) { @@ -1192,19 +1259,77 @@ export abstract class BaseOperationHandler { return thisEntity; } else { fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model); - const query = kysely - .updateTable(model) - .where(() => this.dialect.buildFilter(model, model, combinedWhere)) - .set(updateFields) - .returning(fieldsToReturn as any) - .modifyEnd( - this.makeContextComment({ - model, - operation: 'update', - }), - ); - const updatedEntity = await this.executeQueryTakeFirst(kysely, query, 'update'); + let updatedEntity: any; + + if (this.dialect.supportsReturning) { + const query = kysely + .updateTable(model) + .where(() => this.dialect.buildFilter(model, model, combinedWhere)) + .set(updateFields) + .returning(fieldsToReturn as any) + .modifyEnd( + this.makeContextComment({ + model, + operation: 'update', + }), + ); + + updatedEntity = await this.executeQueryTakeFirst(kysely, query, 'update'); + } else { + // Fallback for databases that don't support RETURNING (e.g., MySQL) + const updateQuery = kysely + .updateTable(model) + .where(() => this.dialect.buildFilter(model, model, combinedWhere)) + .set(updateFields) + .modifyEnd( + this.makeContextComment({ + model, + operation: 'update', + }), + ); + + const updateResult = await this.executeQuery(kysely, updateQuery, 'update'); + if (!updateResult.numAffectedRows) { + // no rows updated + updatedEntity = null; + } else { + // collect id field/values from the original filter + const idFields = requireIdFields(this.schema, model); + const filterIdValues: any = {}; + for (const key of idFields) { + if (combinedWhere[key] !== undefined && typeof combinedWhere[key] !== 'object') { + filterIdValues[key] = combinedWhere[key]; + } + } + + // check if we are updating any id fields + const updatingIdFields = idFields.some((idField) => idField in updateFields); + + if (Object.keys(filterIdValues).length === idFields.length && !updatingIdFields) { + // if we have all id fields in the original filter and ids are not being updated, + // we can simply return the id values as the update result + updatedEntity = filterIdValues; + } else { + // otherwise we need to re-query the updated entity + + // replace id fields in the filter with updated values if they are being updated + const readFilter: any = { ...combinedWhere }; + for (const idField of idFields) { + if (idField in updateFields && updateFields[idField] !== undefined) { + // if id fields are being updated, use the new values + readFilter[idField] = updateFields[idField]; + } + } + const selectQuery = kysely + .selectFrom(model) + .select(fieldsToReturn as any) + .where(() => this.dialect.buildFilter(model, model, readFilter)); + updatedEntity = await this.executeQueryTakeFirst(kysely, selectQuery, 'update'); + } + } + } + if (!updatedEntity) { if (throwIfNotFound) { throw createNotFoundError(model); @@ -1217,6 +1342,42 @@ export abstract class BaseOperationHandler { } } + private async buildUpdateParentRelationFilter(kysely: AnyKysely, fromRelation: FromRelationContext | undefined) { + const parentWhere: any = {}; + let m2m: ReturnType = undefined; + if (fromRelation) { + m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); + if (!m2m) { + // merge foreign key conditions from the relation + const { ownedByModel, keyPairs } = getRelationForeignKeyFieldPairs( + this.schema, + fromRelation.model, + fromRelation.field, + ); + if (ownedByModel) { + const fromEntity = await this.readUnique(kysely, fromRelation.model, { + where: fromRelation.ids, + }); + for (const { fk, pk } of keyPairs) { + parentWhere[pk] = fromEntity[fk]; + } + } else { + for (const { fk, pk } of keyPairs) { + parentWhere[fk] = fromRelation.ids[pk]; + } + } + } else { + // many-to-many relation, filter for parent with "some" + const fromRelationFieldDef = this.requireField(fromRelation.model, fromRelation.field); + invariant(fromRelationFieldDef.relation?.opposite); + parentWhere[fromRelationFieldDef.relation.opposite] = { + some: fromRelation.ids, + }; + } + } + return parentWhere; + } + private processScalarFieldUpdateData(model: string, field: string, data: any): any { const fieldDef = this.requireField(model, field); if (this.isNumericIncrementalUpdate(fieldDef, data[field])) { @@ -1229,7 +1390,7 @@ export abstract class BaseOperationHandler { return this.transformScalarListUpdate(model, field, fieldDef, data[field]); } - return this.dialect.transformPrimitive(data[field], fieldDef.type as BuiltinType, !!fieldDef.array); + return this.dialect.transformInput(data[field], fieldDef.type as BuiltinType, !!fieldDef.array); } private isNumericIncrementalUpdate(fieldDef: FieldDef, value: any) { @@ -1294,7 +1455,7 @@ export abstract class BaseOperationHandler { ); const key = Object.keys(payload)[0]; - const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, false); + const value = this.dialect.transformInput(payload[key!], fieldDef.type as BuiltinType, false); const eb = expressionBuilder(); const fieldRef = this.dialect.fieldRef(model, field); @@ -1317,7 +1478,7 @@ export abstract class BaseOperationHandler { ) { invariant(Object.keys(payload).length === 1, 'Only one of "set", "push" can be provided'); const key = Object.keys(payload)[0]; - const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, true); + const value = this.dialect.transformInput(payload[key!], fieldDef.type as BuiltinType, true); const eb = expressionBuilder(); const fieldRef = this.dialect.fieldRef(model, field); @@ -1351,6 +1512,7 @@ export abstract class BaseOperationHandler { limit: number | undefined, returnData: ReturnData, filterModel?: string, + fromRelation?: FromRelationContext, fieldsToReturn?: readonly string[], ): Promise { if (typeof data !== 'object') { @@ -1366,6 +1528,12 @@ export abstract class BaseOperationHandler { throw createNotSupportedError('Updating with a limit is not supported for polymorphic models'); } + const parentWhere = await this.buildUpdateParentRelationFilter(kysely, fromRelation); + let combinedWhere: WhereInput, false> = where ?? {}; + if (Object.keys(parentWhere).length > 0) { + combinedWhere = Object.keys(combinedWhere).length > 0 ? { AND: [parentWhere, combinedWhere] } : parentWhere; + } + filterModel ??= model; let updateFields: any = {}; @@ -1376,27 +1544,12 @@ export abstract class BaseOperationHandler { updateFields[field] = this.processScalarFieldUpdateData(model, field, data); } - let shouldFallbackToIdFilter = false; - - if (limit !== undefined && !this.dialect.supportsUpdateWithLimit) { - // if the dialect doesn't support update with limit natively, we'll - // simulate it by filtering by id with a limit - shouldFallbackToIdFilter = true; - } - - if (modelDef.isDelegate || modelDef.baseModel) { - // if the model is in a delegate hierarchy, we'll need to filter by - // id because the filter may involve fields in different models in - // the hierarchy - shouldFallbackToIdFilter = true; - } - let resultFromBaseModel: any = undefined; if (modelDef.baseModel) { const baseResult = await this.processBaseModelUpdateMany( kysely, modelDef.baseModel, - where, + combinedWhere, updateFields, filterModel, ); @@ -1410,12 +1563,27 @@ export abstract class BaseOperationHandler { return resultFromBaseModel ?? ((returnData ? [] : { count: 0 }) as Result); } + let shouldFallbackToIdFilter = false; + + if (limit !== undefined && !this.dialect.supportsUpdateWithLimit) { + // if the dialect doesn't support update with limit natively, we'll + // simulate it by filtering by id with a limit + shouldFallbackToIdFilter = true; + } + + if (modelDef.isDelegate || modelDef.baseModel) { + // if the model is in a delegate hierarchy, we'll need to filter by + // id because the filter may involve fields in different models in + // the hierarchy + shouldFallbackToIdFilter = true; + } + let query = kysely.updateTable(model).set(updateFields); if (!shouldFallbackToIdFilter) { // simple filter query = query - .where(() => this.dialect.buildFilter(model, model, where)) + .where(() => this.dialect.buildFilter(model, model, combinedWhere)) .$if(limit !== undefined, (qb) => qb.limit(limit!)); } else { query = query.where((eb) => @@ -1425,11 +1593,17 @@ export abstract class BaseOperationHandler { ...this.buildIdFieldRefs(kysely, model), ), 'in', - this.dialect - .buildSelectModel(filterModel, filterModel) - .where(this.dialect.buildFilter(filterModel, filterModel, where)) - .select(this.buildIdFieldRefs(kysely, filterModel)) - .$if(limit !== undefined, (qb) => qb.limit(limit!)), + // the outer "select *" is needed to isolate the sub query (as needed for dialects like mysql) + eb + .selectFrom( + this.dialect + .buildSelectModel(filterModel, filterModel) + .where(this.dialect.buildFilter(filterModel, filterModel, combinedWhere)) + .select(this.buildIdFieldRefs(kysely, filterModel)) + .$if(limit !== undefined, (qb) => qb.limit(limit!)) + .as('$sub'), + ) + .selectAll(), ), ); } @@ -1441,9 +1615,71 @@ export abstract class BaseOperationHandler { return { count: Number(result.numAffectedRows) } as Result; } else { fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model); - const finalQuery = query.returning(fieldsToReturn as any); - const result = await this.executeQuery(kysely, finalQuery, 'update'); - return result.rows as Result; + + if (this.dialect.supportsReturning) { + const finalQuery = query.returning(fieldsToReturn as any); + const result = await this.executeQuery(kysely, finalQuery, 'update'); + return result.rows as Result; + } else { + // Fallback for databases that don't support RETURNING (e.g., MySQL) + // First, select the records to be updated + let selectQuery = kysely.selectFrom(model).selectAll(); + + if (!shouldFallbackToIdFilter) { + selectQuery = selectQuery + .where(() => this.dialect.buildFilter(model, model, combinedWhere)) + .$if(limit !== undefined, (qb) => qb.limit(limit!)); + } else { + selectQuery = selectQuery.where((eb) => + eb( + eb.refTuple( + // @ts-expect-error + ...this.buildIdFieldRefs(kysely, model), + ), + 'in', + this.dialect + .buildSelectModel(filterModel, filterModel) + .where(this.dialect.buildFilter(filterModel, filterModel, combinedWhere)) + .select(this.buildIdFieldRefs(kysely, filterModel)) + .$if(limit !== undefined, (qb) => qb.limit(limit!)), + ), + ); + } + + const recordsToUpdate = await this.executeQuery(kysely, selectQuery, 'update'); + + // Execute the update + await this.executeQuery(kysely, query, 'update'); + + // Return the IDs of updated records, then query them back with updated values + if (recordsToUpdate.rows.length === 0) { + return [] as Result; + } + + const idFields = requireIdFields(this.schema, model); + const updatedIds = recordsToUpdate.rows.map((row: any) => { + const id: Record = {}; + for (const idField of idFields) { + id[idField] = row[idField]; + } + return id; + }); + + // Query back the updated records + const resultQuery = kysely + .selectFrom(model) + .selectAll() + .where((eb) => { + const conditions = updatedIds.map((id) => { + const idConditions = Object.entries(id).map(([field, value]) => eb.eb(field, '=', value)); + return eb.and(idConditions); + }); + return eb.or(conditions); + }); + + const result = await this.executeQuery(kysely, resultQuery, 'update'); + return result.rows as Result; + } } } @@ -1593,8 +1829,17 @@ export abstract class BaseOperationHandler { case 'updateMany': { for (const _item of enumerate(value)) { - const item = _item as { where: any; data: any }; - await this.update(kysely, fieldModel, item.where, item.data, fromRelationContext, false, false); + const item = _item as { where: any; data: any; limit: number | undefined }; + await this.updateMany( + kysely, + fieldModel, + item.where, + item.data, + item.limit, + false, + fieldModel, + fromRelationContext, + ); } break; } @@ -1680,9 +1925,7 @@ export abstract class BaseOperationHandler { if (!relationFieldDef.array) { const query = kysely .updateTable(model) - .where((eb) => - eb.and(keyPairs.map(({ fk, pk }) => eb(eb.ref(fk as any), '=', fromRelation.ids[pk]))), - ) + .where((eb) => eb.and(keyPairs.map(({ fk, pk }) => eb(eb.ref(fk), '=', fromRelation.ids[pk])))) .set(keyPairs.reduce((acc, { fk }) => ({ ...acc, [fk]: null }), {} as any)) .modifyEnd( this.makeContextComment({ @@ -1987,7 +2230,7 @@ export abstract class BaseOperationHandler { expectedDeleteCount = deleteConditions.length; } - let deleteResult: QueryResult; + let deleteResult: Awaited>; let deleteFromModel: string; const m2m = getManyToManyRelation(this.schema, fromRelation.model, fromRelation.field); @@ -2052,7 +2295,7 @@ export abstract class BaseOperationHandler { } // validate result - if (throwForNotFound && expectedDeleteCount > deleteResult.rows.length) { + if (throwForNotFound && expectedDeleteCount > (deleteResult.numAffectedRows ?? 0)) { // some entities were not deleted throw createNotFoundError(deleteFromModel); } @@ -2085,7 +2328,6 @@ export abstract class BaseOperationHandler { } fieldsToReturn = fieldsToReturn ?? requireIdFields(this.schema, model); - let query = kysely.deleteFrom(model).returning(fieldsToReturn as any); let needIdFilter = false; @@ -2102,32 +2344,42 @@ export abstract class BaseOperationHandler { needIdFilter = true; } - if (!needIdFilter) { - query = query.where(() => this.dialect.buildFilter(model, model, where)); - } else { - query = query.where((eb) => - eb( - eb.refTuple( - // @ts-expect-error - ...this.buildIdFieldRefs(kysely, model), - ), - 'in', - this.dialect - .buildSelectModel(filterModel, filterModel) - .where(() => this.dialect.buildFilter(filterModel, filterModel, where)) - .select(this.buildIdFieldRefs(kysely, filterModel)) - .$if(limit !== undefined, (qb) => qb.limit(limit!)), - ), - ); - } + const deleteFilter = needIdFilter + ? (eb: ExpressionBuilder) => + eb( + eb.refTuple( + // @ts-expect-error + ...this.buildIdFieldRefs(kysely, model), + ), + 'in', + // the outer "select *" is needed to isolate the sub query (as needed for dialects like mysql) + eb + .selectFrom( + this.dialect + .buildSelectModel(filterModel, filterModel) + .where(() => this.dialect.buildFilter(filterModel, filterModel, where)) + .select(this.buildIdFieldRefs(kysely, filterModel)) + .$if(limit !== undefined, (qb) => qb.limit(limit!)) + .as('$sub'), + ) + .selectAll(), + ) + : () => this.dialect.buildFilter(model, model, where); // if the model being deleted has a relation to a model that extends a delegate model, and if that // relation is set to trigger a cascade delete from this model, the deletion will not automatically // clean up the base hierarchy of the relation side (because polymorphic model's cascade deletion // works downward not upward). We need to take care of the base deletions manually here. + await this.processDelegateRelationDelete(kysely, modelDef, where, limit); - query = query.modifyEnd(this.makeContextComment({ model, operation: 'delete' })); + const query = kysely + .deleteFrom(model) + .where(deleteFilter) + .$if(this.dialect.supportsReturning, (qb) => qb.returning(fieldsToReturn)) + .$if(limit !== undefined && this.dialect.supportsDeleteWithLimit, (qb) => qb.limit(limit!)) + .modifyEnd(this.makeContextComment({ model, operation: 'delete' })); + return this.executeQuery(kysely, query, 'delete'); } @@ -2269,6 +2521,11 @@ export abstract class BaseOperationHandler { return { needReadBack: true, selectedFields: undefined }; } + if (!this.dialect.supportsReturning) { + // if the dialect doesn't support RETURNING, we always need read back + return { needReadBack: true, selectedFields: undefined }; + } + if (args.include && typeof args.include === 'object' && Object.keys(args.include).length > 0) { // includes present, need read back to fetch relations return { needReadBack: true, selectedFields: undefined }; diff --git a/packages/orm/src/client/crud/operations/count.ts b/packages/orm/src/client/crud/operations/count.ts index 0b31d7957..fd9861754 100644 --- a/packages/orm/src/client/crud/operations/count.ts +++ b/packages/orm/src/client/crud/operations/count.ts @@ -38,15 +38,15 @@ export class CountOperationHandler extends BaseOperati query = query.select((eb) => Object.keys(parsedArgs.select!).map((key) => key === '_all' - ? eb.cast(eb.fn.countAll(), 'integer').as('_all') - : eb.cast(eb.fn.count(eb.ref(`${subQueryName}.${key}` as any)), 'integer').as(key), + ? this.dialect.castInt(eb.fn.countAll()).as('_all') + : this.dialect.castInt(eb.fn.count(eb.ref(`${subQueryName}.${key}`))).as(key), ), ); const result = await this.executeQuery(this.kysely, query, 'count'); return result.rows[0]; } else { // simple count all - query = query.select((eb) => eb.cast(eb.fn.countAll(), 'integer').as('count')); + query = query.select((eb) => this.dialect.castInt(eb.fn.countAll()).as('count')); const result = await this.executeQuery(this.kysely, query, 'count'); return (result.rows[0] as any).count as number; } diff --git a/packages/orm/src/client/crud/operations/create.ts b/packages/orm/src/client/crud/operations/create.ts index b58871c09..eeb0802d5 100644 --- a/packages/orm/src/client/crud/operations/create.ts +++ b/packages/orm/src/client/crud/operations/create.ts @@ -62,7 +62,8 @@ export class CreateOperationHandler extends BaseOperat if (args === undefined) { return { count: 0 }; } - return this.createMany(this.kysely, this.model, args, false); + + return this.safeTransaction((tx) => this.createMany(tx, this.model, args, false)); } private async runCreateManyAndReturn(args?: CreateManyAndReturnArgs>) { diff --git a/packages/orm/src/client/crud/operations/delete.ts b/packages/orm/src/client/crud/operations/delete.ts index af9942a93..e0c3875b5 100644 --- a/packages/orm/src/client/crud/operations/delete.ts +++ b/packages/orm/src/client/crud/operations/delete.ts @@ -33,9 +33,10 @@ export class DeleteOperationHandler extends BaseOperat }); } const deleteResult = await this.delete(tx, this.model, args.where, undefined, undefined, selectedFields); - if (deleteResult.rows.length === 0) { + if (!deleteResult.numAffectedRows) { throw createNotFoundError(this.model); } + return needReadBack ? preDeleteRead : deleteResult.rows[0]; }); @@ -53,7 +54,7 @@ export class DeleteOperationHandler extends BaseOperat async runDeleteMany(args: DeleteManyArgs> | undefined) { return await this.safeTransaction(async (tx) => { const result = await this.delete(tx, this.model, args?.where, args?.limit); - return { count: result.rows.length }; + return { count: Number(result.numAffectedRows ?? 0) }; }); } } diff --git a/packages/orm/src/client/crud/operations/group-by.ts b/packages/orm/src/client/crud/operations/group-by.ts index cae9f65fd..ad429cf36 100644 --- a/packages/orm/src/client/crud/operations/group-by.ts +++ b/packages/orm/src/client/crud/operations/group-by.ts @@ -32,7 +32,7 @@ export class GroupByOperationHandler extends BaseOpera query = this.dialect.buildSkipTake(query, skip, take); // orderBy - query = this.dialect.buildOrderBy(query, this.model, this.model, parsedArgs.orderBy, negateOrderBy); + query = this.dialect.buildOrderBy(query, this.model, this.model, parsedArgs.orderBy, negateOrderBy, take); // having if (parsedArgs.having) { @@ -49,17 +49,17 @@ export class GroupByOperationHandler extends BaseOpera switch (key) { case '_count': { if (value === true) { - query = query.select((eb) => eb.cast(eb.fn.countAll(), 'integer').as('_count')); + query = query.select((eb) => this.dialect.castInt(eb.fn.countAll()).as('_count')); } else { Object.entries(value).forEach(([field, val]) => { if (val === true) { if (field === '_all') { query = query.select((eb) => - eb.cast(eb.fn.countAll(), 'integer').as(`_count._all`), + this.dialect.castInt(eb.fn.countAll()).as(`_count._all`), ); } else { query = query.select((eb) => - eb.cast(eb.fn.count(fieldRef(field)), 'integer').as(`${key}.${field}`), + this.dialect.castInt(eb.fn.count(fieldRef(field))).as(`${key}.${field}`), ); } } diff --git a/packages/orm/src/client/crud/operations/update.ts b/packages/orm/src/client/crud/operations/update.ts index 477f03f50..d8bd57b53 100644 --- a/packages/orm/src/client/crud/operations/update.ts +++ b/packages/orm/src/client/crud/operations/update.ts @@ -101,6 +101,7 @@ export class UpdateOperationHandler extends BaseOperat args.limit, true, undefined, + undefined, selectedFields, ); @@ -189,6 +190,11 @@ export class UpdateOperationHandler extends BaseOperat return baseResult; } + if (!this.dialect.supportsReturning) { + // if dialect doesn't support "returning", we always need to read back + return { needReadBack: true, selectedFields: undefined }; + } + // further check if we're not updating any non-relation fields, because if so, // SQL "returning" is not effective, we need to always read back diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index 379188756..ad6f18326 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -27,6 +27,9 @@ import { ValuesNode, } from 'kysely'; import type { EnumDef, EnumField, FieldDef, ModelDef, SchemaDef } from '../../schema'; +import type { ClientContract } from '../contract'; +import { getCrudDialect } from '../crud/dialects'; +import type { BaseCrudDialect } from '../crud/dialects/base-dialect'; import { extractFieldName, extractModelName, @@ -50,10 +53,12 @@ export class QueryNameMapper extends OperationNodeTransformer { private readonly modelToTableMap = new Map(); private readonly fieldToColumnMap = new Map(); private readonly scopes: Scope[] = []; + private readonly dialect: BaseCrudDialect; - constructor(private readonly schema: SchemaDef) { + constructor(private readonly client: ClientContract) { super(); - for (const [modelName, modelDef] of Object.entries(schema.models)) { + this.dialect = getCrudDialect(client.$schema, client.$options); + for (const [modelName, modelDef] of Object.entries(client.$schema.models)) { const mappedName = this.getMappedName(modelDef); if (mappedName) { this.modelToTableMap.set(modelName, mappedName); @@ -68,6 +73,10 @@ export class QueryNameMapper extends OperationNodeTransformer { } } + private get schema() { + return this.client.$schema; + } + // #region overrides protected override transformSelectQuery(node: SelectQueryNode) { @@ -761,7 +770,7 @@ export class QueryNameMapper extends OperationNodeTransformer { } // the explicit cast to "text" is needed to address postgres's case-when type inference issue - const finalExpr = caseWhen!.else(eb.cast(new ExpressionWrapper(node), 'text')).end(); + const finalExpr = caseWhen!.else(this.dialect.castText(new ExpressionWrapper(node))).end(); if (aliasName) { return finalExpr.as(aliasName).toOperationNode() as SelectionNodeChild; } else { diff --git a/packages/orm/src/client/executor/zenstack-query-executor.ts b/packages/orm/src/client/executor/zenstack-query-executor.ts index 9012cff8b..4fa8b1896 100644 --- a/packages/orm/src/client/executor/zenstack-query-executor.ts +++ b/packages/orm/src/client/executor/zenstack-query-executor.ts @@ -2,17 +2,23 @@ import { invariant } from '@zenstackhq/common-helpers'; import type { QueryId } from 'kysely'; import { AndNode, + ColumnNode, + ColumnUpdateNode, CompiledQuery, createQueryId, DefaultQueryExecutor, DeleteQueryNode, + expressionBuilder, InsertQueryNode, + PrimitiveValueListNode, ReturningNode, SelectionNode, SelectQueryNode, SingleConnectionProvider, TableNode, UpdateQueryNode, + ValueNode, + ValuesNode, WhereNode, type ConnectionProvider, type DatabaseConnection, @@ -27,9 +33,11 @@ import { match } from 'ts-pattern'; import type { ModelDef, SchemaDef, TypeDefDef } from '../../schema'; import { type ClientImpl } from '../client-impl'; import { TransactionIsolationLevel, type ClientContract } from '../contract'; +import { getCrudDialect } from '../crud/dialects'; +import type { BaseCrudDialect } from '../crud/dialects/base-dialect'; import { createDBQueryError, createInternalError, ORMError } from '../errors'; import type { AfterEntityMutationCallback, OnKyselyQueryCallback } from '../plugin'; -import { stripAlias } from '../query-utils'; +import { requireIdFields, stripAlias } from '../query-utils'; import { QueryNameMapper } from './name-mapper'; import type { ZenStackDriver } from './zenstack-driver'; @@ -41,8 +49,31 @@ type MutationInfo = { where: WhereNode | undefined; }; +type CallBeforeMutationHooksArgs = { + queryNode: OperationNode; + mutationInfo: MutationInfo; + loadBeforeMutationEntities: () => Promise[] | undefined>; + client: ClientContract; + queryId: QueryId; +}; + +type CallAfterMutationHooksArgs = { + queryResult: QueryResult; + queryNode: OperationNode; + mutationInfo: MutationInfo; + client: ClientContract; + filterFor: 'inTx' | 'outTx' | 'all'; + connection: DatabaseConnection; + queryId: QueryId; + beforeMutationEntities?: Record[]; + afterMutationEntities?: Record[]; +}; + export class ZenStackQueryExecutor extends DefaultQueryExecutor { + // #region constructor, fields and props + private readonly nameMapper: QueryNameMapper | undefined; + private readonly dialect: BaseCrudDialect; constructor( private client: ClientImpl, @@ -59,8 +90,10 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { client.$schema.provider.type === 'postgresql' || // postgres queries need to be schema-qualified this.schemaHasMappedNames(client.$schema) ) { - this.nameMapper = new QueryNameMapper(client.$schema); + this.nameMapper = new QueryNameMapper(client as unknown as ClientContract); } + + this.dialect = getCrudDialect(client.$schema, client.$options); } private schemaHasMappedNames(schema: SchemaDef) { @@ -82,12 +115,24 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { return this.client.$options; } - override executeQuery(compiledQuery: CompiledQuery) { + private get hasEntityMutationPlugins() { + return (this.client.$options.plugins ?? []).some((plugin) => plugin.onEntityMutation); + } + + private get hasEntityMutationPluginsWithAfterMutationHooks() { + return (this.client.$options.plugins ?? []).some((plugin) => plugin.onEntityMutation?.afterEntityMutation); + } + + // #endregion + + // #region main entry point + + override async executeQuery(compiledQuery: CompiledQuery) { // proceed with the query with kysely interceptors // if the query is a raw query, we need to carry over the parameters const queryParams = (compiledQuery as any).$raw ? compiledQuery.parameters : undefined; - return this.provideConnection(async (connection) => { + const result = await this.provideConnection(async (connection) => { let startedTx = false; try { // mutations are wrapped in tx if not already in one @@ -124,6 +169,8 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { } } }); + + return this.ensureProperQueryResult(compiledQuery.query, result); } private async proceedQueryWithKyselyInterceptors( @@ -161,63 +208,39 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { return result; } - private getMutationInfo(queryNode: MutationQueryNode): MutationInfo { - const model = this.getMutationModel(queryNode); - const { action, where } = match(queryNode) - .when(InsertQueryNode.is, () => ({ - action: 'create' as const, - where: undefined, - })) - .when(UpdateQueryNode.is, (node) => ({ - action: 'update' as const, - where: node.where, - })) - .when(DeleteQueryNode.is, (node) => ({ - action: 'delete' as const, - where: node.where, - })) - .exhaustive(); - - return { model, action, where }; - } - private async proceedQuery( connection: DatabaseConnection, query: RootOperationNode, parameters: readonly unknown[] | undefined, queryId: QueryId, ) { - let compiled: CompiledQuery | undefined; - if (this.suppressMutationHooks || !this.isMutationNode(query) || !this.hasEntityMutationPlugins) { // no need to handle mutation hooks, just proceed - const finalQuery = this.processNameMapping(query); - - // inherit the original queryId - compiled = this.compileQuery(finalQuery, queryId); - if (parameters) { - compiled = { ...compiled, parameters }; - } - return this.internalExecuteQuery(connection, compiled); + return this.internalExecuteQuery(query, connection, queryId, parameters); } - if ( + let preUpdateIds: Record[] | undefined; + const mutationModel = this.getMutationModel(query); + const needLoadAfterMutationEntities = (InsertQueryNode.is(query) || UpdateQueryNode.is(query)) && - this.hasEntityMutationPluginsWithAfterMutationHooks - ) { - // need to make sure the query node has "returnAll" for insert and update queries - // so that after-mutation hooks can get the mutated entities with all fields - query = { - ...query, - returning: ReturningNode.create([SelectionNode.createSelectAll()]), - }; - } - const finalQuery = this.processNameMapping(query); - - // inherit the original queryId - compiled = this.compileQuery(finalQuery, queryId); - if (parameters) { - compiled = { ...compiled, parameters }; + this.hasEntityMutationPluginsWithAfterMutationHooks; + + if (needLoadAfterMutationEntities) { + if (this.dialect.supportsReturning) { + // need to make sure the query node has "returnAll" for insert and update queries + // so that after-mutation hooks can get the mutated entities with all fields + query = { + ...query, + returning: ReturningNode.create([SelectionNode.createSelectAll()]), + }; + } else { + if (UpdateQueryNode.is(query)) { + // if we're updating and the dialect doesn't support RETURNING, need to load + // entity IDs before the update in so we can use them to load the entities + // after the update + preUpdateIds = await this.getPreUpdateIds(mutationModel, query, connection); + } + } } // the client passed to hooks needs to be in sync with current in-transaction @@ -226,163 +249,89 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { const connectionClient = this.createClientForConnection(connection, currentlyInTx); - const mutationInfo = this.getMutationInfo(finalQuery); + const mutationInfo = this.getMutationInfo(query); // cache already loaded before-mutation entities let beforeMutationEntities: Record[] | undefined; const loadBeforeMutationEntities = async () => { if (beforeMutationEntities === undefined && (UpdateQueryNode.is(query) || DeleteQueryNode.is(query))) { - beforeMutationEntities = await this.loadEntities(mutationInfo.model, mutationInfo.where, connection); + beforeMutationEntities = await this.loadEntities( + mutationInfo.model, + mutationInfo.where, + connection, + undefined, + ); } return beforeMutationEntities; }; // call before mutation hooks - await this.callBeforeMutationHooks( - finalQuery, + await this.callBeforeMutationHooks({ + queryNode: query, mutationInfo, loadBeforeMutationEntities, - connectionClient, + client: connectionClient, queryId, - ); + }); + + // execute the final query + const result = await this.internalExecuteQuery(query, connection, queryId, parameters); + + let afterMutationEntities: Record[] | undefined; + if (needLoadAfterMutationEntities) { + afterMutationEntities = await this.loadAfterMutationEntities( + mutationInfo, + query, + result, + connection, + preUpdateIds, + ); + } - const result = await this.internalExecuteQuery(connection, compiled); + const baseArgs: CallAfterMutationHooksArgs = { + queryResult: result, + queryNode: query, + mutationInfo, + filterFor: 'all', + client: connectionClient, + connection, + queryId, + beforeMutationEntities, + afterMutationEntities, + }; if (!this.driver.isTransactionConnection(connection)) { // not in a transaction, just call all after-mutation hooks - await this.callAfterMutationHooks(result, finalQuery, mutationInfo, connectionClient, 'all', queryId); + await this.callAfterMutationHooks({ + ...baseArgs, + filterFor: 'all', + }); } else { // run after-mutation hooks that are requested to be run inside tx - await this.callAfterMutationHooks(result, finalQuery, mutationInfo, connectionClient, 'inTx', queryId); + await this.callAfterMutationHooks({ + ...baseArgs, + filterFor: 'inTx', + }); // register other after-mutation hooks to be run after the tx is committed this.driver.registerTransactionCommitCallback(connection, () => - this.callAfterMutationHooks(result, finalQuery, mutationInfo, connectionClient, 'outTx', queryId), + this.callAfterMutationHooks({ + ...baseArgs, + filterFor: 'outTx', + }), ); } return result; } - private processNameMapping(query: Node): Node { - return this.nameMapper?.transformNode(query) ?? query; - } - - private createClientForConnection(connection: DatabaseConnection, inTx: boolean) { - const innerExecutor = this.withConnectionProvider(new SingleConnectionProvider(connection)); - innerExecutor.suppressMutationHooks = true; - const innerClient = this.client.withExecutor(innerExecutor); - if (inTx) { - innerClient.forceTransaction(); - } - return innerClient as unknown as ClientContract; - } - - private get hasEntityMutationPlugins() { - return (this.client.$options.plugins ?? []).some((plugin) => plugin.onEntityMutation); - } - - private get hasEntityMutationPluginsWithAfterMutationHooks() { - return (this.client.$options.plugins ?? []).some((plugin) => plugin.onEntityMutation?.afterEntityMutation); - } - - private isMutationNode(queryNode: RootOperationNode): queryNode is MutationQueryNode { - return InsertQueryNode.is(queryNode) || UpdateQueryNode.is(queryNode) || DeleteQueryNode.is(queryNode); - } - - override withPlugin(plugin: KyselyPlugin) { - return new ZenStackQueryExecutor( - this.client, - this.driver, - this.compiler, - this.adapter, - this.connectionProvider, - [...this.plugins, plugin], - this.suppressMutationHooks, - ); - } - - override withPlugins(plugins: ReadonlyArray) { - return new ZenStackQueryExecutor( - this.client, - this.driver, - this.compiler, - this.adapter, - this.connectionProvider, - [...this.plugins, ...plugins], - this.suppressMutationHooks, - ); - } + // #endregion - override withPluginAtFront(plugin: KyselyPlugin) { - return new ZenStackQueryExecutor( - this.client, - this.driver, - this.compiler, - this.adapter, - this.connectionProvider, - [plugin, ...this.plugins], - this.suppressMutationHooks, - ); - } - - override withoutPlugins() { - return new ZenStackQueryExecutor( - this.client, - this.driver, - this.compiler, - this.adapter, - this.connectionProvider, - [], - this.suppressMutationHooks, - ); - } - - override withConnectionProvider(connectionProvider: ConnectionProvider) { - const newExecutor = new ZenStackQueryExecutor( - this.client, - this.driver, - this.compiler, - this.adapter, - connectionProvider, - this.plugins as KyselyPlugin[], - this.suppressMutationHooks, - ); - // replace client with a new one associated with the new executor - newExecutor.client = this.client.withExecutor(newExecutor); - return newExecutor; - } + // #region before and after mutation hooks - private getMutationModel(queryNode: OperationNode): string { - return match(queryNode) - .when(InsertQueryNode.is, (node) => { - invariant(node.into, 'InsertQueryNode must have an into clause'); - return node.into.table.identifier.name; - }) - .when(UpdateQueryNode.is, (node) => { - invariant(node.table, 'UpdateQueryNode must have a table'); - const { node: tableNode } = stripAlias(node.table); - invariant(TableNode.is(tableNode), 'UpdateQueryNode must use a TableNode'); - return tableNode.table.identifier.name; - }) - .when(DeleteQueryNode.is, (node) => { - invariant(node.from.froms.length === 1, 'Delete query must have exactly one from table'); - const { node: tableNode } = stripAlias(node.from.froms[0]!); - invariant(TableNode.is(tableNode), 'DeleteQueryNode must use a TableNode'); - return tableNode.table.identifier.name; - }) - .otherwise((node) => { - throw createInternalError(`Invalid query node: ${node}`); - }) as string; - } + private async callBeforeMutationHooks(args: CallBeforeMutationHooksArgs) { + const { queryNode, mutationInfo, loadBeforeMutationEntities, client, queryId } = args; - private async callBeforeMutationHooks( - queryNode: OperationNode, - mutationInfo: MutationInfo, - loadBeforeMutationEntities: () => Promise[] | undefined>, - client: ClientContract, - queryId: QueryId, - ) { if (this.options.plugins) { for (const plugin of this.options.plugins) { const onEntityMutation = plugin.onEntityMutation; @@ -402,14 +351,10 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { } } - private async callAfterMutationHooks( - queryResult: QueryResult, - queryNode: OperationNode, - mutationInfo: MutationInfo, - client: ClientContract, - filterFor: 'inTx' | 'outTx' | 'all', - queryId: QueryId, - ) { + private async callAfterMutationHooks(args: CallAfterMutationHooksArgs) { + const { queryNode, mutationInfo, client, filterFor, queryId, beforeMutationEntities, afterMutationEntities } = + args; + const hooks: AfterEntityMutationCallback[] = []; // tsc perf @@ -434,46 +379,261 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { return; } - const mutationModel = this.getMutationModel(queryNode); - - const loadAfterMutationEntities = async () => { - if (mutationInfo.action === 'delete') { - return undefined; - } else { - return queryResult.rows as Record[]; - } - }; - for (const hook of hooks) { await hook({ - model: mutationModel, + model: mutationInfo.model, action: mutationInfo.action, queryNode, - loadAfterMutationEntities, + loadAfterMutationEntities: () => Promise.resolve(afterMutationEntities), + beforeMutationEntities, client, queryId, }); } } + private async loadAfterMutationEntities( + mutationInfo: MutationInfo, + queryNode: OperationNode, + queryResult: QueryResult, + connection: DatabaseConnection, + preUpdateIds: Record[] | undefined, + ): Promise[] | undefined> { + if (mutationInfo.action === 'delete') { + return undefined; + } + + if (this.dialect.supportsReturning) { + // entities are returned in the query result + return queryResult.rows as Record[]; + } else { + const mutatedIds = InsertQueryNode.is(queryNode) + ? this.getInsertIds(mutationInfo.model, queryNode, queryResult) + : preUpdateIds; + + if (mutatedIds) { + const idFields = requireIdFields(this.client.$schema, mutationInfo.model); + const eb = expressionBuilder(); + const filter = eb( + // @ts-ignore + eb.refTuple(...idFields), + 'in', + mutatedIds.map((idObj) => + eb.tuple( + // @ts-ignore + ...idFields.map((idField) => eb.val(idObj[idField] as any)), + ), + ), + ); + const entities = await this.loadEntities( + mutationInfo.model, + WhereNode.create(filter.toOperationNode()), + connection, + undefined, + ); + return entities; + } else { + console.warn( + `Unable to load after-mutation entities for hooks: model "${mutationInfo.model}", operation "${mutationInfo.action}". +This happens when the following conditions are met: + +1. The database does not support RETURNING clause for INSERT/UPDATE, e.g., MySQL. +2. The mutation creates or updates multiple entities at once. +3. For create: the model does not have all ID fields explicitly set in the mutation data. +4. For update: the mutation modifies ID fields. + +In such cases, ZenStack cannot reliably determine the IDs of the mutated entities to reload them. +`, + ); + return []; + } + } + } + + private async getPreUpdateIds(mutationModel: string, query: UpdateQueryNode, connection: DatabaseConnection) { + // Get the ID fields for this model + const idFields = requireIdFields(this.client.$schema, mutationModel); + + // Check if the update modifies any ID fields + if (query.updates) { + for (const update of query.updates) { + if (ColumnUpdateNode.is(update)) { + // Extract the column name from the update + const columnNode = update.column; + if (ColumnNode.is(columnNode)) { + const columnName = columnNode.column.name; + if (idFields.includes(columnName)) { + // ID field is being updated, return undefined + return undefined; + } + } + } + } + } + + // No ID fields are being updated, load the entities + return await this.loadEntities(this.getMutationModel(query), query.where, connection, idFields); + } + + private getInsertIds( + mutationModel: string, + query: InsertQueryNode, + queryResult: QueryResult, + ): Record[] | undefined { + const idFields = requireIdFields(this.client.$schema, mutationModel); + + if ( + InsertQueryNode.is(query) && + queryResult.numAffectedRows === 1n && + queryResult.insertId && + idFields.length === 1 + ) { + // single row creation, return the insertId directly + return [ + { + [idFields[0]!]: queryResult.insertId, + }, + ]; + } + + const columns = query.columns; + if (!columns) { + return undefined; + } + + const values = query.values; + if (!values || !ValuesNode.is(values)) { + return undefined; + } + + // Extract ID values for each row + const allIds: Record[] = []; + for (const valuesItem of values.values) { + const rowIds: Record = {}; + + if (PrimitiveValueListNode.is(valuesItem)) { + // PrimitiveValueListNode case + invariant(valuesItem.values.length === columns.length, 'Values count must match columns count'); + for (const idField of idFields) { + const colIndex = columns.findIndex((col) => col.column.name === idField); + if (colIndex === -1) { + // ID field not included in insert columns + return undefined; + } + rowIds[idField] = valuesItem.values[colIndex]; + } + } else { + // ValueListNode case + invariant(valuesItem.values.length === columns.length, 'Values count must match columns count'); + for (const idField of idFields) { + const colIndex = columns.findIndex((col) => col.column.name === idField); + if (colIndex === -1) { + // ID field not included in insert columns + return undefined; + } + const valueNode = valuesItem.values[colIndex]; + if (!valueNode || !ValueNode.is(valueNode)) { + // not a literal value + return undefined; + } + rowIds[idField] = valueNode.value; + } + } + + allIds.push(rowIds); + } + + return allIds; + } + private async loadEntities( model: string, where: WhereNode | undefined, connection: DatabaseConnection, + fieldsToLoad: readonly string[] | undefined, ): Promise[]> { - const selectQuery = this.kysely.selectFrom(model).selectAll(); + let selectQuery = this.kysely.selectFrom(model); + if (fieldsToLoad) { + selectQuery = selectQuery.select(fieldsToLoad); + } else { + selectQuery = selectQuery.selectAll(); + } let selectQueryNode = selectQuery.toOperationNode() as SelectQueryNode; selectQueryNode = { ...selectQueryNode, where: this.andNodes(selectQueryNode.where, where), }; - const compiled = this.compileQuery(selectQueryNode, createQueryId()); // execute the query directly with the given connection to avoid triggering // any other side effects - const result = await this.internalExecuteQuery(connection, compiled); + const result = await this.internalExecuteQuery(selectQueryNode, connection); return result.rows as Record[]; } + // #endregion + + // #region utilities + + private getMutationInfo(queryNode: MutationQueryNode): MutationInfo { + const model = this.getMutationModel(queryNode); + const { action, where } = match(queryNode) + .when(InsertQueryNode.is, () => ({ + action: 'create' as const, + where: undefined, + })) + .when(UpdateQueryNode.is, (node) => ({ + action: 'update' as const, + where: node.where, + })) + .when(DeleteQueryNode.is, (node) => ({ + action: 'delete' as const, + where: node.where, + })) + .exhaustive(); + + return { model, action, where }; + } + + private isMutationNode(queryNode: RootOperationNode): queryNode is MutationQueryNode { + return InsertQueryNode.is(queryNode) || UpdateQueryNode.is(queryNode) || DeleteQueryNode.is(queryNode); + } + + private getMutationModel(queryNode: OperationNode): string { + return match(queryNode) + .when(InsertQueryNode.is, (node) => { + invariant(node.into, 'InsertQueryNode must have an into clause'); + return node.into.table.identifier.name; + }) + .when(UpdateQueryNode.is, (node) => { + invariant(node.table, 'UpdateQueryNode must have a table'); + const { node: tableNode } = stripAlias(node.table); + invariant(TableNode.is(tableNode), 'UpdateQueryNode must use a TableNode'); + return tableNode.table.identifier.name; + }) + .when(DeleteQueryNode.is, (node) => { + invariant(node.from.froms.length === 1, 'Delete query must have exactly one from table'); + const { node: tableNode } = stripAlias(node.from.froms[0]!); + invariant(TableNode.is(tableNode), 'DeleteQueryNode must use a TableNode'); + return tableNode.table.identifier.name; + }) + .otherwise((node) => { + throw createInternalError(`Invalid query node: ${node}`); + }) as string; + } + + private processNameMapping(query: Node): Node { + return this.nameMapper?.transformNode(query) ?? query; + } + + private createClientForConnection(connection: DatabaseConnection, inTx: boolean) { + const innerExecutor = this.withConnectionProvider(new SingleConnectionProvider(connection)); + innerExecutor.suppressMutationHooks = true; + const innerClient = this.client.withExecutor(innerExecutor); + if (inTx) { + innerClient.forceTransaction(); + } + return innerClient as unknown as ClientContract; + } + private andNodes(condition1: WhereNode | undefined, condition2: WhereNode | undefined) { if (condition1 && condition2) { return WhereNode.create(AndNode.create(condition1, condition2)); @@ -484,9 +644,24 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { } } - private async internalExecuteQuery(connection: DatabaseConnection, compiledQuery: CompiledQuery) { + private async internalExecuteQuery( + query: RootOperationNode, + connection: DatabaseConnection, + queryId?: QueryId, + parameters?: readonly unknown[], + ) { + // no need to handle mutation hooks, just proceed + const finalQuery = this.processNameMapping(query); + + // inherit the original queryId + let compiledQuery = this.compileQuery(finalQuery, queryId ?? createQueryId()); + if (parameters) { + compiledQuery = { ...compiledQuery, parameters: parameters }; + } + try { - return await connection.executeQuery(compiledQuery); + const result = await connection.executeQuery(compiledQuery); + return this.ensureProperQueryResult(compiledQuery.query, result); } catch (err) { throw createDBQueryError( `Failed to execute query: ${err}`, @@ -496,4 +671,88 @@ export class ZenStackQueryExecutor extends DefaultQueryExecutor { ); } } + + private ensureProperQueryResult(query: RootOperationNode, result: QueryResult) { + let finalResult = result; + + if (this.isMutationNode(query)) { + // Kysely dialects don't consistently set numAffectedRows, so we fix it here + // to simplify the consumer's code + finalResult = { + ...result, + numAffectedRows: result.numAffectedRows ?? BigInt(result.rows.length), + }; + } + + return finalResult; + } + + // #endregion + + // #region other overrides + + override withPlugin(plugin: KyselyPlugin) { + return new ZenStackQueryExecutor( + this.client, + this.driver, + this.compiler, + this.adapter, + this.connectionProvider, + [...this.plugins, plugin], + this.suppressMutationHooks, + ); + } + + override withPlugins(plugins: ReadonlyArray) { + return new ZenStackQueryExecutor( + this.client, + this.driver, + this.compiler, + this.adapter, + this.connectionProvider, + [...this.plugins, ...plugins], + this.suppressMutationHooks, + ); + } + + override withPluginAtFront(plugin: KyselyPlugin) { + return new ZenStackQueryExecutor( + this.client, + this.driver, + this.compiler, + this.adapter, + this.connectionProvider, + [plugin, ...this.plugins], + this.suppressMutationHooks, + ); + } + + override withoutPlugins() { + return new ZenStackQueryExecutor( + this.client, + this.driver, + this.compiler, + this.adapter, + this.connectionProvider, + [], + this.suppressMutationHooks, + ); + } + + override withConnectionProvider(connectionProvider: ConnectionProvider) { + const newExecutor = new ZenStackQueryExecutor( + this.client, + this.driver, + this.compiler, + this.adapter, + connectionProvider, + this.plugins as KyselyPlugin[], + this.suppressMutationHooks, + ); + // replace client with a new one associated with the new executor + newExecutor.client = this.client.withExecutor(newExecutor); + return newExecutor; + } + + // #endregion } diff --git a/packages/orm/src/client/functions.ts b/packages/orm/src/client/functions.ts index 5690a1e45..1f3ff6ce8 100644 --- a/packages/orm/src/client/functions.ts +++ b/packages/orm/src/client/functions.ts @@ -53,8 +53,11 @@ const textMatch = ( op = 'like'; } + // coalesce to empty string to consistently handle nulls across databases + searchExpr = eb.fn.coalesce(searchExpr, sql.lit('')); + // escape special characters in search string - const escapedSearch = sql`REPLACE(REPLACE(REPLACE(CAST(${searchExpr} as text), '\\', '\\\\'), '%', '\\%'), '_', '\\_')`; + const escapedSearch = sql`REPLACE(REPLACE(REPLACE(${dialect.castText(searchExpr)}, ${sql.val('\\')}, ${sql.val('\\\\')}), ${sql.val('%')}, ${sql.val('\\%')}), ${sql.val('_')}, ${sql.val('\\_')})`; searchExpr = match(method) .with('contains', () => eb.fn('CONCAT', [sql.lit('%'), escapedSearch, sql.lit('%')])) @@ -62,7 +65,7 @@ const textMatch = ( .with('endsWith', () => eb.fn('CONCAT', [sql.lit('%'), escapedSearch])) .exhaustive(); - return sql`${fieldExpr} ${sql.raw(op)} ${searchExpr} escape '\\'`; + return sql`${fieldExpr} ${sql.raw(op)} ${searchExpr} escape ${sql.val('\\')}`; }; export const has: ZModelFunction = (eb, args) => { diff --git a/packages/orm/src/client/helpers/schema-db-pusher.ts b/packages/orm/src/client/helpers/schema-db-pusher.ts index 01b265c46..38df33cee 100644 --- a/packages/orm/src/client/helpers/schema-db-pusher.ts +++ b/packages/orm/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 { CreateTableBuilder, sql, type ColumnDataType, type OnModifyForeignAction, type RawBuilder } from 'kysely'; import toposort from 'toposort'; import { match } from 'ts-pattern'; import { @@ -13,6 +13,11 @@ import { import type { ToKysely } from '../query-builder'; import { requireModel } from '../query-utils'; +/** + * This class is for testing purposes only. It should never be used in production. + * + * @private + */ export class SchemaDbPusher { constructor( private readonly schema: Schema, @@ -21,7 +26,7 @@ export class SchemaDbPusher { async push() { await this.kysely.transaction().execute(async (tx) => { - if (this.schema.enums && this.schema.provider.type === 'postgresql') { + if (this.schema.enums && this.providerSupportsNativeEnum) { for (const [name, enumDef] of Object.entries(this.schema.enums)) { let enumValues: string[]; if (enumDef.fields) { @@ -57,6 +62,10 @@ export class SchemaDbPusher { }); } + private get providerSupportsNativeEnum() { + return ['postgresql'].includes(this.schema.provider.type); + } + private sortModels(models: ModelDef[]): ModelDef[] { const graph: [ModelDef, ModelDef | undefined][] = []; @@ -114,7 +123,7 @@ export class SchemaDbPusher { // create fk constraint const baseModelDef = requireModel(this.schema, modelDef.baseModel); table = table.addForeignKeyConstraint( - `fk_${modelDef.baseModel}_delegate`, + `fk_${modelDef.baseModel}_${modelDef.name}_delegate`, baseModelDef.idFields as string[], modelDef.baseModel, baseModelDef.idFields as string[], @@ -213,13 +222,25 @@ export class SchemaDbPusher { } // @default - if (fieldDef.default !== undefined) { + if (fieldDef.default !== undefined && this.isDefaultValueSupportedForType(fieldDef.type)) { if (typeof fieldDef.default === 'object' && 'kind' in fieldDef.default) { if (ExpressionUtils.isCall(fieldDef.default) && fieldDef.default.function === 'now') { - col = col.defaultTo(sql`CURRENT_TIMESTAMP`); + col = + this.schema.provider.type === 'mysql' + ? col.defaultTo(sql`CURRENT_TIMESTAMP(3)`) + : col.defaultTo(sql`CURRENT_TIMESTAMP`); } } else { - col = col.defaultTo(fieldDef.default); + if ( + this.schema.provider.type === 'mysql' && + fieldDef.type === 'DateTime' && + typeof fieldDef.default === 'string' + ) { + const defaultValue = new Date(fieldDef.default).toISOString().replace('Z', '+00:00'); + col = col.defaultTo(defaultValue); + } else { + col = col.defaultTo(fieldDef.default); + } } } @@ -233,7 +254,7 @@ export class SchemaDbPusher { col = col.notNull(); } - if (this.isAutoIncrement(fieldDef) && this.schema.provider.type === 'sqlite') { + if (this.isAutoIncrement(fieldDef) && this.columnSupportsAutoIncrement()) { col = col.autoIncrement(); } @@ -241,9 +262,43 @@ export class SchemaDbPusher { }); } + private isDefaultValueSupportedForType(type: string) { + return match(this.schema.provider.type) + .with('postgresql', () => true) + .with('sqlite', () => true) + .with('mysql', () => !['Json', 'Bytes'].includes(type)) + .exhaustive(); + } + private mapFieldType(fieldDef: FieldDef) { if (this.schema.enums?.[fieldDef.type]) { - return this.schema.provider.type === 'postgresql' ? sql.ref(fieldDef.type) : 'text'; + if (this.schema.provider.type === 'postgresql') { + return sql.ref(fieldDef.type); + } else if (this.schema.provider.type === 'mysql') { + // MySQL requires inline ENUM definition + const enumDef = this.schema.enums[fieldDef.type]!; + let enumValues: string[]; + if (enumDef.fields) { + enumValues = Object.values(enumDef.fields).map((f) => { + const mapAttr = f.attributes?.find((a) => a.name === '@map'); + if (!mapAttr || !mapAttr.args?.[0]) { + return f.name; + } else { + const mappedName = ExpressionUtils.getLiteralValue(mapAttr.args[0].value); + invariant( + mappedName && typeof mappedName === 'string', + `Invalid @map attribute for enum field ${f.name}`, + ); + return mappedName; + } + }); + } else { + enumValues = Object.values(enumDef.values); + } + return sql.raw(`enum(${enumValues.map((v) => `'${v}'`).join(', ')})`); + } else { + return 'text'; + } } if (this.isAutoIncrement(fieldDef) && this.schema.provider.type === 'postgresql') { @@ -251,20 +306,20 @@ export class SchemaDbPusher { } if (this.isCustomType(fieldDef.type)) { - return 'jsonb'; + return this.jsonType; } const type = fieldDef.type as BuiltinType; - const result = match(type) - .with('String', () => 'text') - .with('Boolean', () => 'boolean') - .with('Int', () => 'integer') - .with('Float', () => 'real') - .with('BigInt', () => 'bigint') - .with('Decimal', () => 'decimal') - .with('DateTime', () => 'timestamp') - .with('Bytes', () => (this.schema.provider.type === 'postgresql' ? 'bytea' : 'blob')) - .with('Json', () => 'jsonb') + const result = match>(type) + .with('String', () => this.stringType) + .with('Boolean', () => this.booleanType) + .with('Int', () => this.intType) + .with('Float', () => this.floatType) + .with('BigInt', () => this.bigIntType) + .with('Decimal', () => this.decimalType) + .with('DateTime', () => this.dateTimeType) + .with('Bytes', () => this.bytesType) + .with('Json', () => this.jsonType) .otherwise(() => { throw new Error(`Unsupported field type: ${type}`); }); @@ -339,4 +394,63 @@ export class SchemaDbPusher { .with('SetDefault', () => 'set default') .exhaustive(); } + + // #region Type mappings and capabilities + + private get jsonType(): ColumnDataType { + return match(this.schema.provider.type) + .with('mysql', () => 'json') + .otherwise(() => 'jsonb'); + } + + private get bytesType(): ColumnDataType { + return match(this.schema.provider.type) + .with('postgresql', () => 'bytea') + .with('mysql', () => 'blob') + .otherwise(() => 'blob'); + } + + private get stringType() { + return match>(this.schema.provider.type) + .with('mysql', () => sql.raw('varchar(255)')) + .otherwise(() => 'text'); + } + + private get booleanType() { + return match>(this.schema.provider.type) + .with('mysql', () => sql.raw('tinyint(1)')) + .otherwise(() => 'boolean'); + } + + private get intType(): ColumnDataType { + return 'integer'; + } + + private get floatType() { + return match>(this.schema.provider.type) + .with('mysql', () => sql.raw('double')) + .otherwise(() => 'real'); + } + + private get bigIntType(): ColumnDataType { + return 'bigint'; + } + + private get decimalType() { + return match>(this.schema.provider.type) + .with('mysql', () => sql.raw('decimal(65, 30)')) + .otherwise(() => 'decimal'); + } + + private get dateTimeType() { + return match>(this.schema.provider.type) + .with('mysql', () => sql.raw('datetime(3)')) + .otherwise(() => 'timestamp'); + } + + private columnSupportsAutoIncrement() { + return ['sqlite', 'mysql'].includes(this.schema.provider.type); + } + + // #endregion } diff --git a/packages/orm/src/client/plugin.ts b/packages/orm/src/client/plugin.ts index 81dff0ec5..e531242e3 100644 --- a/packages/orm/src/client/plugin.ts +++ b/packages/orm/src/client/plugin.ts @@ -250,6 +250,12 @@ export type PluginAfterEntityMutationArgs = MutationHo */ loadAfterMutationEntities(): Promise[] | undefined>; + /** + * The entities before mutation. Only available if `beforeEntityMutation` hook is provided and + * the `loadBeforeMutationEntities` function is called in that hook. + */ + beforeMutationEntities?: Record[]; + /** * The ZenStack client you can use to perform additional operations. * See {@link EntityMutationHooksDef.runAfterMutationWithinTransaction} for detailed transaction behavior. diff --git a/packages/orm/src/client/query-utils.ts b/packages/orm/src/client/query-utils.ts index 51096722f..66e41c404 100644 --- a/packages/orm/src/client/query-utils.ts +++ b/packages/orm/src/client/query-utils.ts @@ -12,7 +12,6 @@ import { match } from 'ts-pattern'; import { ExpressionUtils, type FieldDef, type GetModels, type ModelDef, type SchemaDef } from '../schema'; import { extractFields } from '../utils/object-utils'; import type { AGGREGATE_OPERATORS } from './constants'; -import type { OrderBy } from './crud-types'; import { createInternalError } from './errors'; export function hasModel(schema: SchemaDef, model: string) { @@ -70,6 +69,29 @@ export function requireField(schema: SchemaDef, modelOrType: string, field: stri throw createInternalError(`Model or type "${modelOrType}" not found in schema`, modelOrType); } +/** + * Gets all model fields, by default non-relation, non-computed, non-inherited fields only. + */ +export function getModelFields( + schema: SchemaDef, + model: string, + options?: { relations?: boolean; computed?: boolean; inherited?: boolean }, +) { + const modelDef = requireModel(schema, model); + return Object.values(modelDef.fields).filter((f) => { + if (f.relation && !options?.relations) { + return false; + } + if (f.computed && !options?.computed) { + return false; + } + if (f.originModel && !options?.inherited) { + return false; + } + return true; + }); +} + export function getIdFields(schema: SchemaDef, model: GetModels) { const modelDef = getModel(schema, model); return modelDef?.idFields; @@ -222,9 +244,9 @@ export function buildJoinPairs( }); } -export function makeDefaultOrderBy(schema: SchemaDef, model: string) { +export function makeDefaultOrderBy(schema: SchemaDef, model: string) { const idFields = requireIdFields(schema, model); - return idFields.map((f) => ({ [f]: 'asc' }) as OrderBy, true, false>); + return idFields.map((f) => ({ [f]: 'asc' }) as const); } export function getManyToManyRelation(schema: SchemaDef, model: string, field: string) { diff --git a/packages/orm/src/dialects/mysql.ts b/packages/orm/src/dialects/mysql.ts new file mode 100644 index 000000000..d641eb033 --- /dev/null +++ b/packages/orm/src/dialects/mysql.ts @@ -0,0 +1 @@ +export { MysqlDialect, type MysqlDialectConfig } from 'kysely'; diff --git a/packages/orm/tsup.config.ts b/packages/orm/tsup.config.ts index 14ebdca23..8318a0491 100644 --- a/packages/orm/tsup.config.ts +++ b/packages/orm/tsup.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ helpers: 'src/helpers.ts', 'dialects/sqlite': 'src/dialects/sqlite.ts', 'dialects/postgres': 'src/dialects/postgres.ts', + 'dialects/mysql': 'src/dialects/mysql.ts', 'dialects/sql.js': 'src/dialects/sql.js/index.ts', }, outDir: 'dist', diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index 7977ccb28..f33de0ea3 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -545,7 +545,7 @@ export class ExpressionTransformer { } else if (value === false) { return falseNode(this.dialect); } else { - const transformed = this.dialect.transformPrimitive(value, type, false) ?? null; + const transformed = this.dialect.transformInput(value, type, false) ?? null; if (!Array.isArray(transformed)) { // simple primitives can be immediate values return ValueNode.createImmediate(transformed); diff --git a/packages/plugins/policy/src/functions.ts b/packages/plugins/policy/src/functions.ts index a42de65b6..1cb8a95c5 100644 --- a/packages/plugins/policy/src/functions.ts +++ b/packages/plugins/policy/src/functions.ts @@ -94,9 +94,14 @@ export const check: ZModelFunction = ( // build the final nested select that evaluates the policy condition const result = eb - .selectFrom(relationModel) - .where(joinCondition) - .select(new ExpressionWrapper(policyCondition).as('$condition')); + .selectFrom( + eb + .selectFrom(relationModel) + .where(joinCondition) + .select(new ExpressionWrapper(policyCondition).as('$condition')) + .as('$sub'), + ) + .selectAll(); return result; }; diff --git a/packages/plugins/policy/src/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts index 54169064e..ec12d1a7f 100644 --- a/packages/plugins/policy/src/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -16,7 +16,6 @@ import { expressionBuilder, ExpressionWrapper, FromNode, - FunctionNode, IdentifierNode, InsertQueryNode, JoinNode, @@ -24,7 +23,6 @@ import { OperatorNode, ParensNode, PrimitiveValueListNode, - RawNode, ReferenceNode, ReturningNode, SelectAllNode, @@ -33,10 +31,10 @@ import { sql, TableNode, UpdateQueryNode, - ValueListNode, ValueNode, ValuesNode, WhereNode, + type Expression as KyselyExpression, type OperationNode, type QueryResult, type RootOperationNode, @@ -67,6 +65,7 @@ type FieldLevelPolicyOperations = Exclude; export class PolicyHandler extends OperationNodeTransformer { private readonly dialect: BaseCrudDialect; + private readonly eb = expressionBuilder(); constructor(private readonly client: ClientContract) { super(); @@ -112,11 +111,17 @@ export class PolicyHandler extends OperationNodeTransf } // post-update: load before-update entities if needed - const hasPostUpdatePolicies = UpdateQueryNode.is(node) && this.hasPostUpdatePolicies(mutationModel); - + const needsPostUpdateCheck = UpdateQueryNode.is(node) && this.hasPostUpdatePolicies(mutationModel); let beforeUpdateInfo: Awaited> | undefined; - if (hasPostUpdatePolicies) { - beforeUpdateInfo = await this.loadBeforeUpdateEntities(mutationModel, node.where, proceed); + if (needsPostUpdateCheck) { + beforeUpdateInfo = await this.loadBeforeUpdateEntities( + mutationModel, + node.where, + proceed, + // force load pre-update entities if dialect doesn't support returning, + // so we can rely on pre-update ids to read back updated entities + !this.dialect.supportsReturning, + ); } // #endregion @@ -129,86 +134,12 @@ export class PolicyHandler extends OperationNodeTransf // #region Post mutation work - if (hasPostUpdatePolicies && result.rows.length > 0) { - // verify if before-update rows and post-update rows still id-match - if (beforeUpdateInfo) { - invariant(beforeUpdateInfo.rows.length === result.rows.length); - const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); - for (const postRow of result.rows) { - const beforeRow = beforeUpdateInfo.rows.find((r) => idFields.every((f) => r[f] === postRow[f])); - if (!beforeRow) { - throw createRejectedByPolicyError( - mutationModel, - RejectedByPolicyReason.OTHER, - 'Before-update and after-update rows do not match by id. If you have post-update policies on a model, updating id fields is not supported.', - ); - } - } - } - - // entities updated filter - const idConditions = this.buildIdConditions(mutationModel, result.rows); - - // post-update policy filter - const postUpdateFilter = this.buildPolicyFilter(mutationModel, undefined, 'post-update'); - - // read the post-update row with filter applied - - const eb = expressionBuilder(); - - // create a `SELECT column1 as field1, column2 as field2, ... FROM (VALUES (...))` table for before-update rows - const beforeUpdateTable: SelectQueryNode | undefined = beforeUpdateInfo - ? { - kind: 'SelectQueryNode', - from: FromNode.create([ - ParensNode.create( - ValuesNode.create( - beforeUpdateInfo!.rows.map((r) => - PrimitiveValueListNode.create(beforeUpdateInfo!.fields.map((f) => r[f])), - ), - ), - ), - ]), - selections: beforeUpdateInfo.fields.map((name, index) => { - const def = QueryUtils.requireField(this.client.$schema, mutationModel, name); - const castedColumnRef = - sql`CAST(${eb.ref(`column${index + 1}`)} as ${sql.raw(this.dialect.getFieldSqlType(def))})`.as( - name, - ); - return SelectionNode.create(castedColumnRef.toOperationNode()); - }), - } - : undefined; - - const postUpdateQuery = eb - .selectFrom(mutationModel) - .select(() => [eb(eb.fn('COUNT', [eb.lit(1)]), '=', result.rows.length).as('$condition')]) - .where(() => new ExpressionWrapper(conjunction(this.dialect, [idConditions, postUpdateFilter]))) - .$if(!!beforeUpdateInfo, (qb) => - qb.leftJoin( - () => new ExpressionWrapper(beforeUpdateTable!).as('$before'), - (join) => { - const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); - return idFields.reduce( - (acc, f) => acc.onRef(`${mutationModel}.${f}`, '=', `$before.${f}`), - join, - ); - }, - ), - ); - - const postUpdateResult = await proceed(postUpdateQuery.toOperationNode()); - if (!postUpdateResult.rows[0]?.$condition) { - throw createRejectedByPolicyError( - mutationModel, - RejectedByPolicyReason.NO_ACCESS, - 'some or all updated rows failed to pass post-update policy check', - ); - } - - // #endregion + if ((result.numAffectedRows ?? 0) > 0 && needsPostUpdateCheck) { + await this.postUpdateCheck(mutationModel, beforeUpdateInfo, result, proceed); } + // #endregion + // #region Read back if (!node.returning || this.onlyReturningId(node)) { @@ -272,13 +203,13 @@ export class PolicyHandler extends OperationNodeTransf // build a query to count rows that will be rejected by field-level policies // `SELECT COALESCE(SUM((not ) as integer), 0) AS $filteredCount WHERE AND ` - const preUpdateCheckQuery = expressionBuilder() + const preUpdateCheckQuery = this.eb .selectFrom(mutationModel) .select((eb) => eb.fn .coalesce( eb.fn.sum( - eb.cast(new ExpressionWrapper(logicalNot(this.dialect, fieldLevelFilter)), 'integer'), + this.dialect.castInt(new ExpressionWrapper(logicalNot(this.dialect, fieldLevelFilter))), ), eb.lit(0), ) @@ -296,6 +227,105 @@ export class PolicyHandler extends OperationNodeTransf } } + private async postUpdateCheck( + model: string, + beforeUpdateInfo: Awaited>, + updateResult: QueryResult, + proceed: ProceedKyselyQueryFunction, + ) { + let postUpdateRows: Record[]; + if (this.dialect.supportsReturning) { + // if dialect supports returning, use returned rows directly + postUpdateRows = updateResult.rows; + } else { + // otherwise, need to read back updated rows using pre-update ids + + invariant(beforeUpdateInfo, 'beforeUpdateInfo must be defined for dialects not supporting returning'); + + const idConditions = this.buildIdConditions(model, beforeUpdateInfo!.rows); + const idFields = QueryUtils.requireIdFields(this.client.$schema, model); + const postUpdateQuery: SelectQueryNode = { + kind: 'SelectQueryNode', + from: FromNode.create([TableNode.create(model)]), + where: WhereNode.create(idConditions), + selections: idFields.map((field) => SelectionNode.create(ColumnNode.create(field))), + }; + const postUpdateQueryResult = await proceed(postUpdateQuery); + postUpdateRows = postUpdateQueryResult.rows; + } + + if (beforeUpdateInfo) { + // verify if before-update rows and post-update rows still id-match + if (beforeUpdateInfo.rows.length !== postUpdateRows.length) { + throw createRejectedByPolicyError( + model, + RejectedByPolicyReason.OTHER, + 'Before-update and after-update rows do not match. If you have post-update policies on a model, updating id fields is not supported.', + ); + } + const idFields = QueryUtils.requireIdFields(this.client.$schema, model); + for (const postRow of postUpdateRows) { + const beforeRow = beforeUpdateInfo.rows.find((r) => idFields.every((f) => r[f] === postRow[f])); + if (!beforeRow) { + throw createRejectedByPolicyError( + model, + RejectedByPolicyReason.OTHER, + 'Before-update and after-update rows do not match. If you have post-update policies on a model, updating id fields is not supported.', + ); + } + } + } + + // entities updated filter + const idConditions = this.buildIdConditions(model, postUpdateRows); + + // post-update policy filter + const postUpdateFilter = this.buildPolicyFilter(model, undefined, 'post-update'); + + // read the post-update row with filter applied + + const eb = expressionBuilder(); + + // before update table is joined if fields from `before()` are used in post-update policies + const needsBeforeUpdateJoin = !!beforeUpdateInfo?.fields; + + let beforeUpdateTable: SelectQueryNode | undefined = undefined; + + if (needsBeforeUpdateJoin) { + // create a `SELECT column1 as field1, column2 as field2, ... FROM (VALUES (...))` table for before-update rows + const fieldDefs = beforeUpdateInfo.fields!.map((name) => + QueryUtils.requireField(this.client.$schema, model, name), + ); + const rows = beforeUpdateInfo.rows.map((r) => beforeUpdateInfo!.fields!.map((f) => r[f])); + beforeUpdateTable = this.dialect.buildValuesTableSelect(fieldDefs, rows).toOperationNode(); + } + + const postUpdateQuery = eb + .selectFrom(model) + .select(() => [ + eb(eb.fn('COUNT', [eb.lit(1)]), '=', Number(updateResult.numAffectedRows ?? 0)).as('$condition'), + ]) + .where(() => new ExpressionWrapper(conjunction(this.dialect, [idConditions, postUpdateFilter]))) + .$if(needsBeforeUpdateJoin, (qb) => + qb.leftJoin( + () => new ExpressionWrapper(beforeUpdateTable!).as('$before'), + (join) => { + const idFields = QueryUtils.requireIdFields(this.client.$schema, model); + return idFields.reduce((acc, f) => acc.onRef(`${model}.${f}`, '=', `$before.${f}`), join); + }, + ), + ); + + const postUpdateResult = await proceed(postUpdateQuery.toOperationNode()); + if (!postUpdateResult.rows[0]?.$condition) { + throw createRejectedByPolicyError( + model, + RejectedByPolicyReason.NO_ACCESS, + 'some or all updated rows failed to pass post-update policy check', + ); + } + } + // #endregion // #region Transformations @@ -395,8 +425,9 @@ export class PolicyHandler extends OperationNodeTransf protected override transformInsertQuery(node: InsertQueryNode) { // pre-insert check is done in `handle()` - let onConflict = node.onConflict; + let processedNode = node; + let onConflict = node.onConflict; if (onConflict?.updates) { // for "on conflict do update", we need to apply policy filter to the "where" clause const { mutationModel, alias } = this.getMutationModel(node); @@ -412,10 +443,50 @@ export class PolicyHandler extends OperationNodeTransf updateWhere: WhereNode.create(filter), }; } + processedNode = { ...node, onConflict }; } - // merge updated onConflict - const processedNode = onConflict ? { ...node, onConflict } : node; + let onDuplicateKey = node.onDuplicateKey; + if (onDuplicateKey?.updates) { + // for "on duplicate key update", we need to wrap updates in IF(filter, newValue, oldValue) + // so that updates only happen when the policy filter is satisfied + const { mutationModel } = this.getMutationModel(node); + + // Build the filter without alias, but will still contain model name as table reference + const filterWithTableRef = this.buildPolicyFilter(mutationModel, undefined, 'update'); + + // Strip table references from the filter since ON DUPLICATE KEY UPDATE doesn't support them + const filter = this.stripTableReferences(filterWithTableRef, mutationModel); + + // transform each update to: IF(filter, newValue, oldValue) + const wrappedUpdates = onDuplicateKey.updates.map((update) => { + // For each column update, wrap it with IF condition + // IF(filter, newValue, columnName) - columnName references the existing row value + const columnName = ColumnNode.is(update.column) ? update.column.column.name : undefined; + if (!columnName) { + // keep original update if we can't extract column name + return update; + } + + // Create the wrapped value: IF(filter, newValue, columnName) + // In MySQL's ON DUPLICATE KEY UPDATE context: + // - VALUES(col) = the value from the INSERT statement + // - col = the existing row value before update + const wrappedValue = + sql`IF(${new ExpressionWrapper(filter)}, ${new ExpressionWrapper(update.value)}, ${sql.ref(columnName)})`.toOperationNode(); + + return { + ...update, + value: wrappedValue, + }; + }); + + onDuplicateKey = { + ...onDuplicateKey, + updates: wrappedUpdates, + }; + processedNode = { ...processedNode, onDuplicateKey }; + } const result = super.transformInsertQuery(processedNode); @@ -458,7 +529,7 @@ export class PolicyHandler extends OperationNodeTransf // 2. if there are post-update policies, we need to make sure id fields are selected for joining with // before-update rows - if (returning || this.hasPostUpdatePolicies(mutationModel)) { + if (this.dialect.supportsReturning && (returning || this.hasPostUpdatePolicies(mutationModel))) { const idFields = QueryUtils.requireIdFields(this.client.$schema, mutationModel); returning = ReturningNode.create(idFields.map((f) => SelectionNode.create(ColumnNode.create(f)))); } @@ -500,21 +571,23 @@ export class PolicyHandler extends OperationNodeTransf model: string, where: WhereNode | undefined, proceed: ProceedKyselyQueryFunction, + forceLoad: boolean = false, ) { const beforeUpdateAccessFields = this.getFieldsAccessForBeforeUpdatePolicies(model); - if (!beforeUpdateAccessFields || beforeUpdateAccessFields.length === 0) { + if (!forceLoad && (!beforeUpdateAccessFields || beforeUpdateAccessFields.length === 0)) { return undefined; } // combine update's where with policy filter const policyFilter = this.buildPolicyFilter(model, model, 'update'); const combinedFilter = where ? conjunction(this.dialect, [where.where, policyFilter]) : policyFilter; + const selections = beforeUpdateAccessFields ?? QueryUtils.requireIdFields(this.client.$schema, model); const query: SelectQueryNode = { kind: 'SelectQueryNode', from: FromNode.create([TableNode.create(model)]), where: WhereNode.create(combinedFilter), - selections: [...beforeUpdateAccessFields.map((f) => SelectionNode.create(ColumnNode.create(f)))], + selections: selections.map((f) => SelectionNode.create(ColumnNode.create(f))), }; const result = await proceed(query); return { fields: beforeUpdateAccessFields, rows: result.rows }; @@ -792,62 +865,30 @@ export class PolicyHandler extends OperationNodeTransf values: OperationNode[], proceed: ProceedKyselyQueryFunction, ) { - const allFields = Object.entries(QueryUtils.requireModel(this.client.$schema, model).fields).filter( - ([, def]) => !def.relation, - ); - const allValues: OperationNode[] = []; + const allFields = QueryUtils.getModelFields(this.client.$schema, model, { inherited: true }); + const allValues: KyselyExpression[] = []; - for (const [name, _def] of allFields) { - const index = fields.indexOf(name); + for (const def of allFields) { + const index = fields.indexOf(def.name); if (index >= 0) { - allValues.push(values[index]!); + allValues.push(new ExpressionWrapper(values[index]!)); } else { // set non-provided fields to null - allValues.push(ValueNode.createImmediate(null)); + allValues.push(this.eb.lit(null)); } } // create a `SELECT column1 as field1, column2 as field2, ... FROM (VALUES (...))` table for policy evaluation - const eb = expressionBuilder(); - - const constTable: SelectQueryNode = { - kind: 'SelectQueryNode', - from: FromNode.create([ - AliasNode.create( - ParensNode.create(ValuesNode.create([ValueListNode.create(allValues)])), - IdentifierNode.create('$t'), - ), - ]), - selections: allFields.map(([name, def], index) => { - const castedColumnRef = - sql`CAST(${eb.ref(`column${index + 1}`)} as ${sql.raw(this.dialect.getFieldSqlType(def))})`.as( - name, - ); - return SelectionNode.create(castedColumnRef.toOperationNode()); - }), - }; + const valuesTable = this.dialect.buildValuesTableSelect(allFields, [allValues]); const filter = this.buildPolicyFilter(model, undefined, 'create'); - const preCreateCheck: SelectQueryNode = { - kind: 'SelectQueryNode', - from: FromNode.create([AliasNode.create(constTable, IdentifierNode.create(model))]), - selections: [ - SelectionNode.create( - AliasNode.create( - BinaryOperationNode.create( - FunctionNode.create('COUNT', [ValueNode.createImmediate(1)]), - OperatorNode.create('>'), - ValueNode.createImmediate(0), - ), - IdentifierNode.create('$condition'), - ), - ), - ], - where: WhereNode.create(filter), - }; + const preCreateCheck = this.eb + .selectFrom(valuesTable.as(model)) + .select(this.eb(this.eb.fn.count(this.eb.lit(1)), '>', 0).as('$condition')) + .where(() => new ExpressionWrapper(filter)); - const result = await proceed(preCreateCheck); + const result = await proceed(preCreateCheck.toOperationNode()); if (!result.rows[0]?.$condition) { throw createRejectedByPolicyError(model, RejectedByPolicyReason.NO_ACCESS); } @@ -883,7 +924,7 @@ export class PolicyHandler extends OperationNodeTransf invariant(item.kind === 'ValueNode', 'expecting a ValueNode'); result.push({ node: ValueNode.create( - this.dialect.transformPrimitive( + this.dialect.transformInput( (item as ValueNode).value, fieldDef.type as BuiltinType, !!fieldDef.array, @@ -899,11 +940,13 @@ export class PolicyHandler extends OperationNodeTransf // are all foreign keys if (!isImplicitManyToManyJoinTable) { const fieldDef = QueryUtils.requireField(this.client.$schema, model, fields[i]!); - value = this.dialect.transformPrimitive(item, fieldDef.type as BuiltinType, !!fieldDef.array); + value = this.dialect.transformInput(item, fieldDef.type as BuiltinType, !!fieldDef.array); } + + // handle the case for list column if (Array.isArray(value)) { result.push({ - node: RawNode.createWithSql(this.dialect.buildArrayLiteralSQL(value)), + node: this.dialect.buildArrayLiteralSQL(value).toOperationNode(), raw: value, }); } else { @@ -1244,10 +1287,9 @@ export class PolicyHandler extends OperationNodeTransf // - mutation: requires both sides to be updatable const checkForOperation = operation === 'read' ? 'read' : 'update'; - const eb = expressionBuilder(); const joinTable = alias ?? tableName; - const aQuery = eb + const aQuery = this.eb .selectFrom(m2m.firstModel) .whereRef(`${m2m.firstModel}.${m2m.firstIdField}`, '=', `${joinTable}.A`) .select(() => @@ -1256,7 +1298,7 @@ export class PolicyHandler extends OperationNodeTransf ), ); - const bQuery = eb + const bQuery = this.eb .selectFrom(m2m.secondModel) .whereRef(`${m2m.secondModel}.${m2m.secondIdField}`, '=', `${joinTable}.B`) .select(() => @@ -1265,7 +1307,7 @@ export class PolicyHandler extends OperationNodeTransf ), ); - return eb.and([aQuery, bQuery]).toOperationNode(); + return this.eb.and([aQuery, bQuery]).toOperationNode(); } private tryRejectNonexistentModel(model: string) { @@ -1297,5 +1339,27 @@ export class PolicyHandler extends OperationNodeTransf } } + // strips table references from an OperationNode + private stripTableReferences(node: OperationNode, modelName: string): OperationNode { + return new TableReferenceStripper().strip(node, modelName); + } +} + +class TableReferenceStripper extends OperationNodeTransformer { + private tableName: string = ''; + + strip(node: OperationNode, tableName: string) { + this.tableName = tableName; + return this.transformNode(node); + } + + protected override transformReference(node: ReferenceNode) { + if (ColumnNode.is(node.column) && node.table?.table.identifier.name === this.tableName) { + // strip the table part + return ReferenceNode.create(this.transformNode(node.column)); + } + return super.transformReference(node); + } + // #endregion } diff --git a/packages/plugins/policy/src/utils.ts b/packages/plugins/policy/src/utils.ts index f42370cc8..5deef04bd 100644 --- a/packages/plugins/policy/src/utils.ts +++ b/packages/plugins/policy/src/utils.ts @@ -20,14 +20,14 @@ import { * Creates a `true` value node. */ export function trueNode(dialect: BaseCrudDialect) { - return ValueNode.createImmediate(dialect.transformPrimitive(true, 'Boolean', false)); + return ValueNode.createImmediate(dialect.transformInput(true, 'Boolean', false)); } /** * Creates a `false` value node. */ export function falseNode(dialect: BaseCrudDialect) { - return ValueNode.createImmediate(dialect.transformPrimitive(false, 'Boolean', false)); + return ValueNode.createImmediate(dialect.transformInput(false, 'Boolean', false)); } /** diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index 40f7d8bd8..58fc1bc5b 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -1,7 +1,7 @@ import type Decimal from 'decimal.js'; import type { Expression } from './expression'; -export type DataSourceProviderType = 'sqlite' | 'postgresql'; +export type DataSourceProviderType = 'sqlite' | 'postgresql' | 'mysql'; export type DataSourceProvider = { type: DataSourceProviderType; diff --git a/packages/server/test/api/rest.test.ts b/packages/server/test/api/rest.test.ts index 5cdd6f3a9..96b469ba7 100644 --- a/packages/server/test/api/rest.test.ts +++ b/packages/server/test/api/rest.test.ts @@ -1416,8 +1416,8 @@ describe('REST server tests', () => { email: `user1@abc.com`, posts: { create: [...Array(10).keys()].map((i) => ({ - id: i, - title: `Post${i}`, + id: i + 1, + title: `Post${i + 1}`, })), }, }, @@ -1478,8 +1478,8 @@ describe('REST server tests', () => { email: `user1@abc.com`, posts: { create: [...Array(10).keys()].map((i) => ({ - id: i, - title: `Post${i}`, + id: i + 1, + title: `Post${i + 1}`, })), }, }, @@ -3352,7 +3352,8 @@ mutation procedure sum(a: Int, b: Int): Int const b = args?.b as number | undefined; return (a ?? 0) + (b ?? 0); }, - sumIds: async ({ args }: ProcCtx) => (args.ids as number[]).reduce((acc, x) => acc + x, 0), + sumIds: async ({ args }: ProcCtx) => + (args.ids as number[]).reduce((acc, x) => acc + x, 0), echoRole: async ({ args }: ProcCtx) => args.r, echoOverview: async ({ args }: ProcCtx) => args.o, sum: async ({ args }: ProcCtx) => args.a + args.b, @@ -3373,7 +3374,7 @@ mutation procedure sum(a: Int, b: Int): Int const r = await handler({ method: 'get', path: '/$procs/echoDecimal', - query: { ...json as object, meta: { serialization: meta } } as any, + query: { ...(json as object), meta: { serialization: meta } } as any, client, }); @@ -3486,7 +3487,7 @@ mutation procedure sum(a: Int, b: Int): Int const r = await handler({ method: 'post', path: '/$procs/sum', - requestBody: { ...json as object, meta: { serialization: meta } } as any, + requestBody: { ...(json as object), meta: { serialization: meta } } as any, client, }); diff --git a/packages/server/test/api/rpc.test.ts b/packages/server/test/api/rpc.test.ts index 9493ed2a3..30818ae8a 100644 --- a/packages/server/test/api/rpc.test.ts +++ b/packages/server/test/api/rpc.test.ts @@ -30,7 +30,7 @@ describe('RPC API Handler Tests', () => { r = await handleRequest({ method: 'get', path: '/user/exists', - query: { q: JSON.stringify({ where: { id: 'user1' }})}, + query: { q: JSON.stringify({ where: { id: 'user1' } }) }, client: rawClient, }); expect(r.status).toBe(200); @@ -69,7 +69,7 @@ describe('RPC API Handler Tests', () => { r = await handleRequest({ method: 'get', path: '/user/exists', - query: { q: JSON.stringify({ where: { id: 'user1' }})}, + query: { q: JSON.stringify({ where: { id: 'user1' } }) }, client: rawClient, }); expect(r.status).toBe(200); @@ -167,7 +167,7 @@ procedure getUndefined(): Undefined getFalse: async () => false, getUndefined: async () => undefined, }, - }); + } as any); const handler = new RPCApiHandler({ schema: procClient.$schema }); const handleProcRequest = async (args: any) => { @@ -267,7 +267,7 @@ procedure echoOverview(o: Overview): Overview echoRole: async ({ args }: any) => args.r, echoOverview: async ({ args }: any) => args.o, }, - }); + } as any); const handler = new RPCApiHandler({ schema: procClient.$schema }); const handleProcRequest = async (args: any) => { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index ed5f9bc92..7ec0135d1 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -39,6 +39,7 @@ "@zenstackhq/plugin-policy": "workspace:*", "glob": "^11.1.0", "kysely": "catalog:", + "mysql2": "catalog:", "prisma": "catalog:", "tmp": "catalog:", "ts-pattern": "catalog:" diff --git a/packages/testtools/src/client.ts b/packages/testtools/src/client.ts index 696596618..c82a6b780 100644 --- a/packages/testtools/src/client.ts +++ b/packages/testtools/src/client.ts @@ -1,17 +1,19 @@ import { invariant } from '@zenstackhq/common-helpers'; import type { Model } from '@zenstackhq/language/ast'; import { ZenStackClient, type ClientContract, type ClientOptions } from '@zenstackhq/orm'; -import type { SchemaDef } from '@zenstackhq/orm/schema'; +import type { DataSourceProviderType, SchemaDef } from '@zenstackhq/orm/schema'; import { PolicyPlugin } from '@zenstackhq/plugin-policy'; import { PrismaSchemaGenerator } from '@zenstackhq/sdk'; import SQLite from 'better-sqlite3'; import { glob } from 'glob'; -import { PostgresDialect, SqliteDialect, type LogEvent } from 'kysely'; +import { MysqlDialect, PostgresDialect, SqliteDialect, type LogEvent } from 'kysely'; +import { createPool as createMysqlPool } from 'mysql2'; import { execSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import fs from 'node:fs'; import path from 'node:path'; import { Client as PGClient, Pool } from 'pg'; +import { match } from 'ts-pattern'; import { expect } from 'vitest'; import { createTestProject } from './project'; import { generateTsSchema } from './schema'; @@ -19,10 +21,10 @@ import { loadDocumentWithPlugins } from './utils'; export function getTestDbProvider() { const val = process.env['TEST_DB_PROVIDER'] ?? 'sqlite'; - if (!['sqlite', 'postgresql'].includes(val!)) { + if (!['sqlite', 'postgresql', 'mysql'].includes(val!)) { throw new Error(`Invalid TEST_DB_PROVIDER value: ${val}`); } - return val as 'sqlite' | 'postgresql'; + return val as 'sqlite' | 'postgresql' | 'mysql'; } export const TEST_PG_CONFIG = { @@ -34,11 +36,21 @@ export const TEST_PG_CONFIG = { export const TEST_PG_URL = `postgres://${TEST_PG_CONFIG.user}:${TEST_PG_CONFIG.password}@${TEST_PG_CONFIG.host}:${TEST_PG_CONFIG.port}`; +export const TEST_MYSQL_CONFIG = { + host: process.env['TEST_MYSQL_HOST'] ?? 'localhost', + port: process.env['TEST_MYSQL_PORT'] ? parseInt(process.env['TEST_MYSQL_PORT']) : 3306, + user: process.env['TEST_MYSQL_USER'] ?? 'root', + password: process.env['TEST_MYSQL_PASSWORD'] ?? 'mysql', + timezone: 'Z', +}; + +export const TEST_MYSQL_URL = `mysql://${TEST_MYSQL_CONFIG.user}:${TEST_MYSQL_CONFIG.password}@${TEST_MYSQL_CONFIG.host}:${TEST_MYSQL_CONFIG.port}`; + type ExtraTestClientOptions = { /** * Database provider */ - provider?: 'sqlite' | 'postgresql'; + provider?: 'sqlite' | 'postgresql' | 'mysql'; /** * The main ZModel file. Only used when `usePrismaPush` is true and `schema` is an object. @@ -106,8 +118,11 @@ export async function createTestClient( let _schema: SchemaDef; const provider = options?.provider ?? getTestDbProvider() ?? 'sqlite'; const dbName = options?.dbName ?? getTestDbName(provider); - const dbUrl = provider === 'sqlite' ? `file:${dbName}` : `${TEST_PG_URL}/${dbName}`; - + const dbUrl = match(provider) + .with('sqlite', () => `file:${dbName}`) + .with('mysql', () => `${TEST_MYSQL_URL}/${dbName}`) + .with('postgresql', () => `${TEST_PG_URL}/${dbName}`) + .exhaustive(); let model: Model | undefined; if (typeof schema === 'string') { @@ -158,7 +173,7 @@ export async function createTestClient( if (options?.debug) { console.log(`Work directory: ${workDir}`); console.log(`Database name: ${dbName}`); - _options.log = testLogger; + _options.log ??= testLogger; } // copy db file to workDir if specified @@ -208,29 +223,12 @@ export async function createTestClient( stdio: options.debug ? 'inherit' : 'ignore', }); } else { - if (provider === 'postgresql') { - invariant(dbName, 'dbName is required'); - const pgClient = new PGClient(TEST_PG_CONFIG); - await pgClient.connect(); - await pgClient.query(`DROP DATABASE IF EXISTS "${dbName}"`); - await pgClient.query(`CREATE DATABASE "${dbName}"`); - await pgClient.end(); - } + await prepareDatabase(provider, dbName); } } - if (provider === 'postgresql') { - _options.dialect = new PostgresDialect({ - pool: new Pool({ - ...TEST_PG_CONFIG, - database: dbName, - }), - }); - } else { - _options.dialect = new SqliteDialect({ - database: new SQLite(path.join(workDir!, dbName)), - }); - } + // create Kysely dialect + _options.dialect = createDialect(provider, dbName, workDir); let client = new ZenStackClient(_schema, _options); @@ -238,6 +236,7 @@ export async function createTestClient( await client.$pushSchema(); } + // install plugins if (plugins) { for (const plugin of plugins) { client = client.$use(plugin); @@ -247,6 +246,55 @@ export async function createTestClient( return client; } +function createDialect(provider: DataSourceProviderType, dbName: string, workDir: string) { + return match(provider) + .with( + 'postgresql', + () => + new PostgresDialect({ + pool: new Pool({ + ...TEST_PG_CONFIG, + database: dbName, + }), + }), + ) + .with( + 'mysql', + () => + new MysqlDialect({ + pool: createMysqlPool({ + ...TEST_MYSQL_CONFIG, + database: dbName, + }), + }), + ) + .with( + 'sqlite', + () => + new SqliteDialect({ + database: new SQLite(path.join(workDir!, dbName)), + }), + ) + .exhaustive(); +} + +async function prepareDatabase(provider: string, dbName: string) { + if (provider === 'postgresql') { + invariant(dbName, 'dbName is required'); + const pgClient = new PGClient(TEST_PG_CONFIG); + await pgClient.connect(); + await pgClient.query(`DROP DATABASE IF EXISTS "${dbName}"`); + await pgClient.query(`CREATE DATABASE "${dbName}"`); + await pgClient.end(); + } else if (provider === 'mysql') { + invariant(dbName, 'dbName is required'); + const mysqlPool = createMysqlPool(TEST_MYSQL_CONFIG); + await mysqlPool.promise().query(`DROP DATABASE IF EXISTS \`${dbName}\``); + await mysqlPool.promise().query(`CREATE DATABASE \`${dbName}\``); + await mysqlPool.promise().end(); + } +} + export async function createPolicyTestClient( schema: Schema, options?: CreateTestClientOptions, diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 1ecb015c3..44f4483c9 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -1,5 +1,5 @@ import { invariant } from '@zenstackhq/common-helpers'; -import type { SchemaDef } from '@zenstackhq/schema'; +import type { DataSourceProviderType, SchemaDef } from '@zenstackhq/schema'; import { TsSchemaGenerator } from '@zenstackhq/sdk'; import { execSync } from 'node:child_process'; import crypto from 'node:crypto'; @@ -11,7 +11,7 @@ import { expect } from 'vitest'; import { createTestProject } from './project'; import { loadDocumentWithPlugins } from './utils'; -function makePrelude(provider: 'sqlite' | 'postgresql', dbUrl?: string) { +function makePrelude(provider: DataSourceProviderType, dbUrl?: string) { return match(provider) .with('sqlite', () => { return ` @@ -27,19 +27,37 @@ datasource db { provider = 'postgresql' url = '${dbUrl ?? 'postgres://postgres:postgres@localhost:5432/db'}' } +`; + }) + .with('mysql', () => { + return ` +datasource db { + provider = 'mysql' + url = '${dbUrl ?? 'mysql://root:mysql@localhost:3306/db'}' +} `; }) .exhaustive(); } -function replacePlaceholders(schemaText: string, provider: 'sqlite' | 'postgresql', dbUrl: string | undefined) { - const url = dbUrl ?? (provider === 'sqlite' ? 'file:./test.db' : 'postgres://postgres:postgres@localhost:5432/db'); +function replacePlaceholders( + schemaText: string, + provider: 'sqlite' | 'postgresql' | 'mysql', + dbUrl: string | undefined, +) { + const url = + dbUrl ?? + (provider === 'sqlite' + ? 'file:./test.db' + : provider === 'mysql' + ? 'mysql://root:mysql@localhost:3306/db' + : 'postgres://postgres:postgres@localhost:5432/db'); return schemaText.replace(/\$DB_URL/g, url).replace(/\$PROVIDER/g, provider); } export async function generateTsSchema( schemaText: string, - provider: 'sqlite' | 'postgresql' = 'sqlite', + provider: DataSourceProviderType = 'sqlite', dbUrl?: string, extraSourceFiles?: Record, withLiteSchema?: boolean, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6b6ee103e..41ae6dcc4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ catalogs: langium-cli: specifier: 3.5.0 version: 3.5.0 + mysql2: + specifier: ^3.16.1 + version: 3.16.1 next: specifier: 16.0.10 version: 16.0.10 @@ -516,6 +519,9 @@ importers: kysely: specifier: 'catalog:' version: 0.28.8 + mysql2: + specifier: 'catalog:' + version: 3.16.1 nanoid: specifier: ^5.0.9 version: 5.0.9 @@ -724,7 +730,7 @@ importers: version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) nuxt: specifier: 'catalog:' - version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) supertest: specifier: ^7.1.4 version: 7.1.4 @@ -761,6 +767,9 @@ importers: kysely: specifier: 'catalog:' version: 0.28.8 + mysql2: + specifier: 'catalog:' + version: 3.16.1 pg: specifier: 'catalog:' version: 8.16.3 @@ -904,7 +913,7 @@ importers: version: 2.0.8 nuxt: specifier: 'catalog:' - version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -1058,6 +1067,9 @@ importers: kysely: specifier: 'catalog:' version: 0.28.8 + ts-pattern: + specifier: 'catalog:' + version: 5.7.1 ulid: specifier: ^3.0.0 version: 3.0.0 @@ -4169,6 +4181,10 @@ packages: avvio@9.1.0: resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + aws-ssl-profiles@1.1.2: + resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} + engines: {node: '>= 6.0.0'} + axe-core@4.11.0: resolution: {integrity: sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==} engines: {node: '>=4'} @@ -5322,6 +5338,9 @@ packages: resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} engines: {node: '>=10'} + generate-function@2.3.1: + resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + generator-function@2.0.1: resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} engines: {node: '>= 0.4'} @@ -5699,6 +5718,9 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-property@1.0.2: + resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -6091,6 +6113,9 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -6110,6 +6135,10 @@ packages: lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru.min@1.1.3: + resolution: {integrity: sha512-Lkk/vx6ak3rYkRR0Nhu4lFUT2VDnQSxBe8Hbl7f36358p6ow8Bnvr8lrLt98H8J1aGxfhbX4Fs5tYg2+FTwr5Q==} + engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} + lz-string@1.5.0: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true @@ -6279,9 +6308,17 @@ packages: muggle-string@0.4.1: resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + mysql2@3.16.1: + resolution: {integrity: sha512-b75qsDB3ieYEzMsT1uRGsztM/sy6vWPY40uPZlVVl8eefAotFCoS7jaDB5DxDNtlW5kdVGd9jptSpkvujNxI2A==} + engines: {node: '>= 8.0'} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + named-placeholders@1.1.6: + resolution: {integrity: sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==} + engines: {node: '>=8.0.0'} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -7364,6 +7401,9 @@ packages: resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} engines: {node: '>= 18'} + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -7515,6 +7555,10 @@ packages: sql.js@1.13.0: resolution: {integrity: sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + srvx@0.9.8: resolution: {integrity: sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ==} engines: {node: '>=20.16.0'} @@ -9597,7 +9641,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)': + '@nuxt/nitro-server@4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.2.2(magicast@0.5.1) @@ -9614,15 +9658,15 @@ snapshots: impound: 1.0.0 klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.12.9(better-sqlite3@12.5.0) - nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + nitropack: 2.12.9(better-sqlite3@12.5.0)(mysql2@3.16.1) + nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) pathe: 2.0.3 pkg-types: 2.3.0 radix3: 1.1.2 std-env: 3.10.0 ufo: 1.6.1 unctx: 2.4.1 - unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2) + unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2) vue: 3.5.26(typescript@5.9.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 @@ -9686,7 +9730,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': + '@nuxt/vite-builder@4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': dependencies: '@nuxt/kit': 4.2.2(magicast@0.5.1) '@rollup/plugin-replace': 6.0.3(rollup@4.52.5) @@ -9706,7 +9750,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.0 mocked-exports: 0.1.1 - nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 @@ -11603,6 +11647,8 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.19.1 + aws-ssl-profiles@1.1.2: {} + axe-core@4.11.0: {} axobject-query@4.1.0: {} @@ -12108,9 +12154,10 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - db0@0.3.4(better-sqlite3@12.5.0): + db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1): optionalDependencies: better-sqlite3: 12.5.0 + mysql2: 3.16.1 debug@3.2.7: dependencies: @@ -12985,6 +13032,10 @@ snapshots: fuse.js@7.1.0: {} + generate-function@2.3.1: + dependencies: + is-property: 1.0.2 + generator-function@2.0.1: {} gensync@1.0.0-beta.2: {} @@ -13377,6 +13428,8 @@ snapshots: is-promise@4.0.0: {} + is-property@1.0.2: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -13775,6 +13828,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + long@5.3.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -13791,6 +13846,8 @@ snapshots: dependencies: yallist: 3.1.1 + lru.min@1.1.3: {} + lz-string@1.5.0: {} magic-regexp@0.10.0: @@ -13939,12 +13996,28 @@ snapshots: muggle-string@0.4.1: {} + mysql2@3.16.1: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.0 + long: 5.3.2 + lru.min: 1.1.3 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + mz@2.7.0: dependencies: any-promise: 1.3.0 object-assign: 4.1.1 thenify-all: 1.6.0 + named-placeholders@1.1.6: + dependencies: + lru.min: 1.1.3 + nanoid@3.3.11: {} nanoid@5.0.9: {} @@ -13988,7 +14061,7 @@ snapshots: nice-try@1.0.5: {} - nitropack@2.12.9(better-sqlite3@12.5.0): + nitropack@2.12.9(better-sqlite3@12.5.0)(mysql2@3.16.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.52.5) @@ -14009,7 +14082,7 @@ snapshots: cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.4(better-sqlite3@12.5.0) + db0: 0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1) defu: 6.1.4 destr: 2.0.5 dot-prop: 10.1.0 @@ -14055,7 +14128,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.5.0 unplugin-utils: 0.3.1 - unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2) + unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.13 @@ -14154,16 +14227,16 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2): + nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2): dependencies: '@dxup/nuxt': 0.2.2(magicast@0.5.1) '@nuxt/cli': 3.31.3(cac@6.7.14)(magicast@0.5.1) '@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) '@nuxt/kit': 4.2.2(magicast@0.5.1) - '@nuxt/nitro-server': 4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3) + '@nuxt/nitro-server': 4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3) '@nuxt/schema': 4.2.2 '@nuxt/telemetry': 2.6.6(magicast@0.5.1) - '@nuxt/vite-builder': 4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) + '@nuxt/vite-builder': 4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) '@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3)) '@vue/shared': 3.5.26 c12: 3.3.3(magicast@0.5.1) @@ -15236,6 +15309,8 @@ snapshots: transitivePeerDependencies: - supports-color + seq-queue@0.0.5: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -15426,6 +15501,8 @@ snapshots: sql.js@1.13.0: {} + sqlstring@2.3.3: {} + srvx@0.9.8: {} stable-hash@0.0.5: {} @@ -16111,7 +16188,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.3(db0@0.3.4(better-sqlite3@12.5.0))(ioredis@5.8.2): + unstorage@1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -16122,7 +16199,7 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.1 optionalDependencies: - db0: 0.3.4(better-sqlite3@12.5.0) + db0: 0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1) ioredis: 5.8.2 untun@0.1.3: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index f5987bd80..ca0942a89 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -22,6 +22,7 @@ catalog: nuxt: 4.2.2 '@sveltejs/kit': 2.49.1 pg: ^8.13.1 + mysql2: ^3.16.1 prisma: ^6.19.0 react: 19.2.0 react-dom: 19.2.0 diff --git a/tests/e2e/orm/client-api/computed-fields.test.ts b/tests/e2e/orm/client-api/computed-fields.test.ts index 2476b67d3..c6470a720 100644 --- a/tests/e2e/orm/client-api/computed-fields.test.ts +++ b/tests/e2e/orm/client-api/computed-fields.test.ts @@ -1,4 +1,5 @@ import { createTestClient } from '@zenstackhq/testtools'; +import { sql } from 'kysely'; import { describe, expect, it } from 'vitest'; describe('Computed fields tests', () => { @@ -7,14 +8,15 @@ describe('Computed fields tests', () => { ` model User { id Int @id @default(autoincrement()) - name String - upperName String @computed + firstName String + lastName String + fullName String @computed } `, { computedFields: { User: { - upperName: (eb: any) => eb.fn('upper', ['name']), + fullName: (eb: any) => eb.fn('concat', [eb.ref('firstName'), sql.lit(' '), eb.ref('lastName')]), }, }, } as any, @@ -22,70 +24,70 @@ model User { await expect( db.user.create({ - data: { id: 1, name: 'Alex' }, + data: { id: 1, firstName: 'Alex', lastName: 'Smith' }, }), ).resolves.toMatchObject({ - upperName: 'ALEX', + fullName: 'Alex Smith', }); await expect( db.user.findUnique({ where: { id: 1 }, - select: { upperName: true }, + select: { fullName: true }, }), ).resolves.toMatchObject({ - upperName: 'ALEX', + fullName: 'Alex Smith', }); await expect( db.user.findFirst({ - where: { upperName: 'ALEX' }, + where: { fullName: 'Alex Smith' }, }), ).resolves.toMatchObject({ - upperName: 'ALEX', + fullName: 'Alex Smith', }); await expect( db.user.findFirst({ - where: { upperName: 'Alex' }, + where: { fullName: 'Alex' }, }), ).toResolveNull(); await expect( db.user.findFirst({ - orderBy: { upperName: 'desc' }, + orderBy: { fullName: 'desc' }, }), ).resolves.toMatchObject({ - upperName: 'ALEX', + fullName: 'Alex Smith', }); await expect( db.user.findFirst({ - orderBy: { upperName: 'desc' }, + orderBy: { fullName: 'desc' }, take: 1, }), ).resolves.toMatchObject({ - upperName: 'ALEX', + fullName: 'Alex Smith', }); await expect( db.user.aggregate({ - _count: { upperName: true }, + _count: { fullName: true }, }), ).resolves.toMatchObject({ - _count: { upperName: 1 }, + _count: { fullName: 1 }, }); await expect( db.user.groupBy({ - by: ['upperName'], - _count: { upperName: true }, - _max: { upperName: true }, + by: ['fullName'], + _count: { fullName: true }, + _max: { fullName: true }, }), ).resolves.toEqual([ expect.objectContaining({ - _count: { upperName: 1 }, - _max: { upperName: 'ALEX' }, + _count: { fullName: 1 }, + _max: { fullName: 'Alex Smith' }, }), ]); }); @@ -259,17 +261,19 @@ model Post extends Content { } as any, ); - const posts = await db.post.createManyAndReturn({ - data: [ - { id: 1, title: 'latest news', body: 'some news content' }, - { id: 2, title: 'random post', body: 'some other content' }, - ], - }); - expect(posts).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: 1, isNews: true }), - expect.objectContaining({ id: 2, isNews: false }), - ]), - ); + if (db.$schema.provider.type !== 'mysql') { + const posts = await db.post.createManyAndReturn({ + data: [ + { id: 1, title: 'latest news', body: 'some news content' }, + { id: 2, title: 'random post', body: 'some other content' }, + ], + }); + expect(posts).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: 1, isNews: true }), + expect.objectContaining({ id: 2, isNews: false }), + ]), + ); + } }); }); diff --git a/tests/e2e/orm/client-api/create-many-and-return.test.ts b/tests/e2e/orm/client-api/create-many-and-return.test.ts index 8a8697b14..663c8fa7f 100644 --- a/tests/e2e/orm/client-api/create-many-and-return.test.ts +++ b/tests/e2e/orm/client-api/create-many-and-return.test.ts @@ -15,6 +15,11 @@ describe('Client createManyAndReturn tests', () => { }); it('works with toplevel createManyAndReturn', async () => { + if (client.$schema.provider.type === ('mysql' as any)) { + // mysql doesn't support createManyAndReturn + return; + } + // empty await expect(client.user.createManyAndReturn()).toResolveWithLength(0); @@ -59,6 +64,11 @@ describe('Client createManyAndReturn tests', () => { }); it('works with select and omit', async () => { + if (client.$schema.provider.type === ('mysql' as any)) { + // mysql doesn't support createManyAndReturn + return; + } + const r = await client.user.createManyAndReturn({ data: [{ email: 'u1@test.com', name: 'name' }], select: { email: true }, diff --git a/tests/e2e/orm/client-api/delegate.test.ts b/tests/e2e/orm/client-api/delegate.test.ts index c5f59c70e..86010b4e5 100644 --- a/tests/e2e/orm/client-api/delegate.test.ts +++ b/tests/e2e/orm/client-api/delegate.test.ts @@ -135,6 +135,10 @@ describe('Delegate model tests ', () => { }); it('works with createManyAndReturn', async () => { + if (client.$schema.provider.type === ('mysql' as any)) { + return; + } + await expect( client.ratedVideo.createManyAndReturn({ data: [ @@ -179,7 +183,7 @@ describe('Delegate model tests ', () => { rating: 3, }, }), - ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().includes('constraint')); + ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().match(/(constraint)|(duplicate)/i)); await expect(client.ratedVideo.findMany()).toResolveWithLength(1); await expect(client.video.findMany()).toResolveWithLength(1); @@ -790,6 +794,10 @@ describe('Delegate model tests ', () => { }); it('works with updateManyAndReturn', async () => { + if (client.$schema.provider.type === ('mysql' as any)) { + return; + } + await client.ratedVideo.create({ data: { id: 2, viewCount: 1, duration: 200, url: 'abc', rating: 5 }, }); diff --git a/tests/e2e/orm/client-api/error-handling.test.ts b/tests/e2e/orm/client-api/error-handling.test.ts index 1227e525e..7be2002a8 100644 --- a/tests/e2e/orm/client-api/error-handling.test.ts +++ b/tests/e2e/orm/client-api/error-handling.test.ts @@ -1,5 +1,6 @@ import { ORMError, ORMErrorReason, RejectedByPolicyReason } from '@zenstackhq/orm'; import { createPolicyTestClient, createTestClient } from '@zenstackhq/testtools'; +import { match } from 'ts-pattern'; import { describe, expect, it } from 'vitest'; describe('Error handling tests', () => { @@ -39,14 +40,20 @@ model User { await db.user.create({ data: { email: 'user1@example.com' } }); const provider = db.$schema.provider.type; - const expectedCode = provider === 'sqlite' ? 'SQLITE_CONSTRAINT_UNIQUE' : '23505'; + const expectedCode = match(provider) + .with('sqlite', () => 'SQLITE_CONSTRAINT_UNIQUE') + .with('postgresql', () => '23505') + .with('mysql', () => 'ER_DUP_ENTRY') + .otherwise(() => { + throw new Error(`Unsupported provider: ${provider}`); + }); await expect(db.user.create({ data: { email: 'user1@example.com' } })).rejects.toSatisfy( (e) => e instanceof ORMError && e.reason === ORMErrorReason.DB_QUERY_ERROR && e.dbErrorCode === expectedCode && - !!e.dbErrorMessage?.includes('constraint'), + !!e.dbErrorMessage?.match(/(constraint)|(duplicate)/i), ); }); }); diff --git a/tests/e2e/orm/client-api/find.test.ts b/tests/e2e/orm/client-api/find.test.ts index 2eddd2146..5b6ab5639 100644 --- a/tests/e2e/orm/client-api/find.test.ts +++ b/tests/e2e/orm/client-api/find.test.ts @@ -262,6 +262,11 @@ describe('Client find tests ', () => { }); it('works with distinct', async () => { + if (['sqlite', 'mysql'].includes(client.$schema.provider.type)) { + await expect(client.user.findMany({ distinct: ['role'] } as any)).rejects.toThrow('not supported'); + return; + } + const user1 = await createUser(client, 'u1@test.com', { name: 'Admin1', role: 'ADMIN', @@ -282,11 +287,6 @@ describe('Client find tests ', () => { role: 'USER', }); - if (client.$schema.provider.type === 'sqlite') { - await expect(client.user.findMany({ distinct: ['role'] } as any)).rejects.toThrow('not supported'); - return; - } - // single field distinct let r: any = await client.user.findMany({ distinct: ['role'] } as any); expect(r).toHaveLength(2); diff --git a/tests/e2e/orm/client-api/json-filter.test.ts b/tests/e2e/orm/client-api/json-filter.test.ts index 4260d439b..6b127a813 100644 --- a/tests/e2e/orm/client-api/json-filter.test.ts +++ b/tests/e2e/orm/client-api/json-filter.test.ts @@ -7,7 +7,9 @@ import { schema as typedJsonSchema } from '../schemas/typed-json/schema'; describe('Json filter tests', () => { it('works with simple equality filter', async () => { const db = await createTestClient(schema); - await db.foo.create({ data: { data: { hello: 'world' } } }); + await expect(db.foo.create({ data: { data: { hello: 'world' } } })).resolves.toMatchObject({ + data: { hello: 'world' }, + }); await expect(db.foo.findFirst({ where: { data: { equals: { hello: 'world' } } } })).resolves.toMatchObject({ data: { hello: 'world' }, diff --git a/tests/e2e/orm/client-api/mixin.test.ts b/tests/e2e/orm/client-api/mixin.test.ts index e373b8fe6..4dc1001bd 100644 --- a/tests/e2e/orm/client-api/mixin.test.ts +++ b/tests/e2e/orm/client-api/mixin.test.ts @@ -75,7 +75,7 @@ model Bar with CommonFields { description: 'Bar', }, }), - ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().includes('constraint')); + ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().match(/(constraint)|(duplicate)/i)); }); it('supports multiple-level mixins', async () => { diff --git a/tests/e2e/orm/client-api/name-mapping.test.ts b/tests/e2e/orm/client-api/name-mapping.test.ts index 2b70cd46b..f45bddeec 100644 --- a/tests/e2e/orm/client-api/name-mapping.test.ts +++ b/tests/e2e/orm/client-api/name-mapping.test.ts @@ -85,20 +85,34 @@ describe('Name mapping tests', () => { user_role: 'MODERATOR', }); - await expect( - db.$qb + const mysql = db.$schema.provider.type === ('mysql' as any); + + if (!mysql) { + await expect( + db.$qb + .insertInto('User') + .values({ + email: 'u2@test.com', + role: 'ADMIN', + }) + .returning(['id', 'email', 'role']) + .executeTakeFirst(), + ).resolves.toMatchObject({ + id: expect.any(Number), + email: 'u2@test.com', + role: 'ADMIN', + }); + } else { + // mysql doesn't support returning, simply insert + await db.$qb .insertInto('User') .values({ email: 'u2@test.com', role: 'ADMIN', }) - .returning(['id', 'email', 'role']) - .executeTakeFirst(), - ).resolves.toMatchObject({ - id: expect.any(Number), - email: 'u2@test.com', - role: 'ADMIN', - }); + .executeTakeFirst(); + } + rawRead = await db.$qbRaw .selectFrom('users') .where('user_email', '=', 'u2@test.com') @@ -108,32 +122,34 @@ describe('Name mapping tests', () => { user_role: 'role_admin', }); - await expect( - db.$qb - .insertInto('User') - .values({ - email: 'u3@test.com', - }) - .returning(['User.id', 'User.email']) - .executeTakeFirst(), - ).resolves.toMatchObject({ - id: expect.any(Number), - email: 'u3@test.com', - }); - - await expect( - db.$qb - .insertInto('User') - .values({ - email: 'u4@test.com', - }) - .returningAll() - .executeTakeFirst(), - ).resolves.toMatchObject({ - id: expect.any(Number), - email: 'u4@test.com', - role: 'USER', - }); + if (!mysql) { + await expect( + db.$qb + .insertInto('User') + .values({ + email: 'u3@test.com', + }) + .returning(['User.id', 'User.email']) + .executeTakeFirst(), + ).resolves.toMatchObject({ + id: expect.any(Number), + email: 'u3@test.com', + }); + + await expect( + db.$qb + .insertInto('User') + .values({ + email: 'u4@test.com', + }) + .returningAll() + .executeTakeFirst(), + ).resolves.toMatchObject({ + id: expect.any(Number), + email: 'u4@test.com', + role: 'USER', + }); + } }); it('works with find', async () => { @@ -379,18 +395,20 @@ describe('Name mapping tests', () => { posts: [expect.objectContaining({ title: 'Post2' })], }); - await expect( - db.$qb - .updateTable('User') - .set({ email: (eb) => eb.fn('upper', [eb.ref('email')]), role: 'USER' }) - .where('email', '=', 'u2@test.com') - .returning(['email', 'role']) - .executeTakeFirst(), - ).resolves.toMatchObject({ email: 'U2@TEST.COM', role: 'USER' }); - - await expect( - db.$qb.updateTable('User as u').set({ email: 'u3@test.com' }).returningAll().executeTakeFirst(), - ).resolves.toMatchObject({ id: expect.any(Number), email: 'u3@test.com', role: 'USER' }); + if (db.$schema.provider.type !== ('mysql' as any)) { + await expect( + db.$qb + .updateTable('User') + .set({ email: (eb) => eb.fn('upper', [eb.ref('email')]), role: 'USER' }) + .where('email', '=', 'u2@test.com') + .returning(['email', 'role']) + .executeTakeFirst(), + ).resolves.toMatchObject({ email: 'U2@TEST.COM', role: 'USER' }); + + await expect( + db.$qb.updateTable('User as u').set({ email: 'u3@test.com' }).returningAll().executeTakeFirst(), + ).resolves.toMatchObject({ id: expect.any(Number), email: 'u3@test.com', role: 'USER' }); + } }); it('works with delete', async () => { @@ -406,12 +424,17 @@ describe('Name mapping tests', () => { }, }); - await expect( - db.$qb.deleteFrom('Post').where('title', '=', 'Post1').returning(['id', 'title']).executeTakeFirst(), - ).resolves.toMatchObject({ - id: user.id, - title: 'Post1', - }); + if (db.$schema.provider.type !== ('mysql' as any)) { + await expect( + db.$qb.deleteFrom('Post').where('title', '=', 'Post1').returning(['id', 'title']).executeTakeFirst(), + ).resolves.toMatchObject({ + id: user.id, + title: 'Post1', + }); + } else { + // mysql doesn't support returning, simply delete + await db.$qb.deleteFrom('Post').where('title', '=', 'Post1').executeTakeFirst(); + } await expect( db.user.delete({ diff --git a/tests/e2e/orm/client-api/raw-query.test.ts b/tests/e2e/orm/client-api/raw-query.test.ts index 7fe0ecbcd..1e6e889e6 100644 --- a/tests/e2e/orm/client-api/raw-query.test.ts +++ b/tests/e2e/orm/client-api/raw-query.test.ts @@ -2,6 +2,9 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import type { ClientContract } from '@zenstackhq/orm'; import { schema } from '../schemas/basic'; import { createTestClient } from '@zenstackhq/testtools'; +import { sql } from 'kysely'; +import { match } from 'ts-pattern'; +import type { DataSourceProviderType } from '@zenstackhq/schema'; describe('Client raw query tests', () => { let client: ClientContract; @@ -14,6 +17,10 @@ describe('Client raw query tests', () => { await client?.$disconnect(); }); + function ref(col: string) { + return client.$schema.provider.type === ('mysql' as any) ? sql.raw(`\`${col}\``) : sql.raw(`"${col}"`); + } + it('works with executeRaw', async () => { await client.user.create({ data: { @@ -23,7 +30,7 @@ describe('Client raw query tests', () => { }); await expect( - client.$executeRaw`UPDATE "User" SET "email" = ${'u2@test.com'} WHERE "id" = ${'1'}`, + client.$executeRaw`UPDATE ${ref('User')} SET ${ref('email')} = ${'u2@test.com'} WHERE ${ref('id')} = ${'1'}`, ).resolves.toBe(1); await expect(client.user.findFirst()).resolves.toMatchObject({ email: 'u2@test.com' }); }); @@ -36,11 +43,11 @@ describe('Client raw query tests', () => { }, }); - const sql = - // @ts-ignore - client.$schema.provider.type === 'postgresql' - ? `UPDATE "User" SET "email" = $1 WHERE "id" = $2` - : `UPDATE "User" SET "email" = ? WHERE "id" = ?`; + const sql = match(client.$schema.provider.type as DataSourceProviderType) + .with('postgresql', () => `UPDATE "User" SET "email" = $1 WHERE "id" = $2`) + .with('mysql', () => 'UPDATE `User` SET `email` = ? WHERE `id` = ?') + .with('sqlite', () => 'UPDATE "User" SET "email" = ? WHERE "id" = ?') + .exhaustive(); await expect(client.$executeRawUnsafe(sql, 'u2@test.com', '1')).resolves.toBe(1); await expect(client.user.findFirst()).resolves.toMatchObject({ email: 'u2@test.com' }); }); @@ -56,7 +63,7 @@ describe('Client raw query tests', () => { const uid = '1'; const users = await client.$queryRaw< { id: string; email: string }[] - >`SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = ${uid}`; + >`SELECT ${ref('User')}.${ref('id')}, ${ref('User')}.${ref('email')} FROM ${ref('User')} WHERE ${ref('User')}.${ref('id')} = ${uid}`; expect(users).toEqual([{ id: '1', email: 'u1@test.com' }]); }); @@ -68,11 +75,12 @@ describe('Client raw query tests', () => { }, }); - const sql = - // @ts-ignore - client.$schema.provider.type === 'postgresql' - ? `SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = $1` - : `SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = ?`; + const sql = match(client.$schema.provider.type as DataSourceProviderType) + .with('postgresql', () => `SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = $1`) + .with('mysql', () => 'SELECT `User`.`id`, `User`.`email` FROM `User` WHERE `User`.`id` = ?') + .with('sqlite', () => 'SELECT "User"."id", "User"."email" FROM "User" WHERE "User"."id" = ?') + .exhaustive(); + const users = await client.$queryRawUnsafe<{ id: string; email: string }[]>(sql, '1'); expect(users).toEqual([{ id: '1', email: 'u1@test.com' }]); }); diff --git a/tests/e2e/orm/client-api/relation/self-relation.test.ts b/tests/e2e/orm/client-api/relation/self-relation.test.ts index 9e70ca007..8dc7e5b5a 100644 --- a/tests/e2e/orm/client-api/relation/self-relation.test.ts +++ b/tests/e2e/orm/client-api/relation/self-relation.test.ts @@ -1,5 +1,5 @@ -import { afterEach, describe, expect, it } from 'vitest'; import { createTestClient } from '@zenstackhq/testtools'; +import { afterEach, describe, expect, it } from 'vitest'; describe('Self relation tests', () => { let client: any; diff --git a/tests/e2e/orm/client-api/type-coverage.test.ts b/tests/e2e/orm/client-api/type-coverage.test.ts index a0c24880c..bbe872773 100644 --- a/tests/e2e/orm/client-api/type-coverage.test.ts +++ b/tests/e2e/orm/client-api/type-coverage.test.ts @@ -35,6 +35,7 @@ describe('Zmodel type coverage tests', () => { Json Json } `, + { usePrismaPush: true }, ); await db.foo.create({ data }); @@ -80,7 +81,7 @@ describe('Zmodel type coverage tests', () => { }); it('supports all types - array', async () => { - if (getTestDbProvider() === 'sqlite') { + if (getTestDbProvider() !== 'postgresql') { return; } diff --git a/tests/e2e/orm/client-api/update-many.test.ts b/tests/e2e/orm/client-api/update-many.test.ts index 61776e3ec..dfb598d9c 100644 --- a/tests/e2e/orm/client-api/update-many.test.ts +++ b/tests/e2e/orm/client-api/update-many.test.ts @@ -80,6 +80,11 @@ describe('Client updateMany tests', () => { }); it('works with updateManyAndReturn', async () => { + if (client.$schema.provider.type === ('mysql' as any)) { + // skip for mysql as it does not support returning + return; + } + await client.user.create({ data: { id: '1', email: 'u1@test.com', name: 'User1' }, }); diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index c79396d7f..08377e518 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -115,6 +115,30 @@ describe('Client update tests', () => { ).resolves.toMatchObject({ id: 'user2' }); }); + it('works with update with unchanged data or no data', async () => { + const user = await createUser(client, 'u1@test.com'); + await expect( + client.user.update({ + where: { id: user.id }, + data: { + email: user.email, + // force a no-op update + updatedAt: user.updatedAt, + }, + }), + ).resolves.toEqual(user); + + await expect( + client.user.update({ + where: { id: user.id }, + data: {}, + }), + ).resolves.toEqual(user); + + const plain = await client.plain.create({ data: { value: 42 } }); + await expect(client.plain.update({ where: { id: plain.id }, data: { value: 42 } })).resolves.toEqual(plain); + }); + it('does not update updatedAt if no other scalar fields are updated', async () => { const user = await createUser(client, 'u1@test.com'); const originalUpdatedAt = user.updatedAt; @@ -1050,7 +1074,7 @@ describe('Client update tests', () => { }, }, }), - ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().includes('constraint')); + ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().match(/(constraint)|(duplicate)/i)); // transaction fails as a whole await expect(client.comment.findUnique({ where: { id: '3' } })).resolves.toMatchObject({ content: 'Comment3', diff --git a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts index 4c97d1a9b..eb8a25f16 100644 --- a/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts +++ b/tests/e2e/orm/plugin-infra/entity-mutation-hooks.test.ts @@ -8,7 +8,7 @@ describe('Entity mutation hooks tests', () => { let _client: ClientContract; beforeEach(async () => { - _client = (await createTestClient(schema, {})) as any; + _client = await createTestClient(schema); }); afterEach(async () => { @@ -67,6 +67,8 @@ describe('Entity mutation hooks tests', () => { delete: { before: '', after: '' }, }; + let beforeMutationEntitiesInAfterHooks: Record[] | undefined; + const client = _client.$use({ id: 'test', onEntityMutation: { @@ -84,6 +86,10 @@ describe('Entity mutation hooks tests', () => { if (args.action === 'update' || args.action === 'delete') { queryIds[args.action].after = args.queryId.queryId; } + + if (args.action === 'update') { + beforeMutationEntitiesInAfterHooks = args.beforeMutationEntities; + } }, }, }); @@ -94,10 +100,14 @@ describe('Entity mutation hooks tests', () => { await client.user.create({ data: { email: 'u2@test.com' }, }); + await client.user.update({ where: { id: user.id }, data: { email: 'u3@test.com' }, }); + // beforeMutationEntities in after hooks is available because we called loadBeforeMutationEntities in before hook + expect(beforeMutationEntitiesInAfterHooks).toEqual([expect.objectContaining({ email: 'u1@test.com' })]); + await client.user.delete({ where: { id: user.id } }); expect(queryIds.update.before).toBeTruthy(); diff --git a/tests/e2e/orm/plugin-infra/ext-query-args.test.ts b/tests/e2e/orm/plugin-infra/ext-query-args.test.ts index 95794626d..3a15321e9 100644 --- a/tests/e2e/orm/plugin-infra/ext-query-args.test.ts +++ b/tests/e2e/orm/plugin-infra/ext-query-args.test.ts @@ -122,9 +122,14 @@ describe('Plugin extended query args', () => { await expect( extDb.user.createMany({ data: [{ name: 'Charlie' }], ...cacheBustOption }), ).resolves.toHaveProperty('count'); - await expect( - extDb.user.createManyAndReturn({ data: [{ name: 'David' }], ...cacheBustOption }), - ).toResolveWithLength(1); + + const isMySql = db.$schema.provider.type === ('mysql' as any); + + if (!isMySql) { + await expect( + extDb.user.createManyAndReturn({ data: [{ name: 'David' }], ...cacheBustOption }), + ).toResolveWithLength(1); + } // update operations await expect( @@ -133,13 +138,17 @@ describe('Plugin extended query args', () => { await expect( extDb.user.updateMany({ where: { name: 'Bob' }, data: { name: 'Bob Updated' }, ...cacheBustOption }), ).resolves.toHaveProperty('count'); - await expect( - extDb.user.updateManyAndReturn({ - where: { name: 'Charlie' }, - data: { name: 'Charlie Updated' }, - ...cacheBustOption, - }), - ).toResolveTruthy(); + + if (!isMySql) { + await expect( + extDb.user.updateManyAndReturn({ + where: { name: 'Charlie' }, + data: { name: 'Charlie Updated' }, + ...cacheBustOption, + }), + ).toResolveTruthy(); + } + await expect( extDb.user.upsert({ where: { id: 999 }, diff --git a/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts b/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts index 686026133..d6e17c04b 100644 --- a/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts +++ b/tests/e2e/orm/plugin-infra/on-kysely-query.test.ts @@ -1,6 +1,6 @@ import { type ClientContract } from '@zenstackhq/orm'; import { createTestClient } from '@zenstackhq/testtools'; -import { InsertQueryNode, Kysely, PrimitiveValueListNode, ValuesNode, type QueryResult } from 'kysely'; +import { InsertQueryNode, PrimitiveValueListNode, ValuesNode } from 'kysely'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import { schema } from '../schemas/basic'; @@ -93,7 +93,14 @@ describe('On kysely query tests', () => { const result = await proceed(query); // create a post for the user - await proceed(createPost(client.$qb, result)); + const now = new Date().toISOString().replace('Z', '+00:00'); // for mysql compatibility + const createPost = client.$qb.insertInto('Post').values({ + id: '1', + title: 'Post1', + authorId: '1', + updatedAt: now, + }); + await proceed(createPost.toOperationNode()); return result; }, @@ -218,14 +225,3 @@ describe('On kysely query tests', () => { await expect(client.user.findFirst()).toResolveNull(); }); }); - -function createPost(kysely: Kysely, userRows: QueryResult) { - const now = new Date().toISOString(); - const createPost = kysely.insertInto('Post').values({ - id: '1', - title: 'Post1', - authorId: (userRows.rows[0] as any).id, - updatedAt: now, - }); - return createPost.toOperationNode(); -} diff --git a/tests/e2e/orm/policy/crud/create.test.ts b/tests/e2e/orm/policy/crud/create.test.ts index 6aecba293..124a23eb8 100644 --- a/tests/e2e/orm/policy/crud/create.test.ts +++ b/tests/e2e/orm/policy/crud/create.test.ts @@ -16,12 +16,11 @@ model Foo { await expect(db.foo.create({ data: { x: 0 } })).toBeRejectedByPolicy(); await expect(db.foo.create({ data: { x: 1 } })).resolves.toMatchObject({ x: 1 }); - await expect( - db.$qb.insertInto('Foo').values({ x: 0 }).returningAll().executeTakeFirst(), - ).toBeRejectedByPolicy(); - await expect( - db.$qb.insertInto('Foo').values({ x: 1 }).returningAll().executeTakeFirst(), - ).resolves.toMatchObject({ x: 1 }); + await expect(db.$qb.insertInto('Foo').values({ x: 0 }).executeTakeFirst()).toBeRejectedByPolicy(); + + await expect(db.$qb.insertInto('Foo').values({ x: 1 }).executeTakeFirst()).toResolveTruthy(); + + await expect(db.foo.findMany({ where: { x: 1 } })).resolves.toHaveLength(2); }); it('works with this scalar member check', async () => { diff --git a/tests/e2e/orm/policy/crud/read.test.ts b/tests/e2e/orm/policy/crud/read.test.ts index fd767b519..febe35032 100644 --- a/tests/e2e/orm/policy/crud/read.test.ts +++ b/tests/e2e/orm/policy/crud/read.test.ts @@ -632,8 +632,14 @@ model Bar { @@allow('read', y > 0) } `, + { provider: 'postgresql' }, ); + if (db.$schema.provider.type !== 'postgresql') { + // skip for non-postgresql as from is only supported there + return; + } + await db.$unuseAll().foo.create({ data: { id: 1, x: 1 } }); await db.$unuseAll().bar.create({ data: { id: 1, y: 0 } }); diff --git a/tests/e2e/orm/policy/crud/update.test.ts b/tests/e2e/orm/policy/crud/update.test.ts index 95b173065..d7f3a8a21 100644 --- a/tests/e2e/orm/policy/crud/update.test.ts +++ b/tests/e2e/orm/policy/crud/update.test.ts @@ -23,9 +23,12 @@ model Foo { await expect( db.$qb.updateTable('Foo').set({ x: 1 }).where('id', '=', 1).executeTakeFirst(), ).resolves.toMatchObject({ numUpdatedRows: 0n }); - await expect( - db.$qb.updateTable('Foo').set({ x: 3 }).where('id', '=', 2).returningAll().execute(), - ).resolves.toMatchObject([{ id: 2, x: 3 }]); + + if (db.$schema.provider.type !== 'mysql') { + await expect( + db.$qb.updateTable('Foo').set({ x: 3 }).where('id', '=', 2).returningAll().execute(), + ).resolves.toMatchObject([{ id: 2, x: 3 }]); + } }); it('works with this scalar member check', async () => { @@ -758,7 +761,7 @@ model Post { }, }, }), - ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().includes('constraint')); + ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().match(/(constraint)|(duplicate)/)); await db.$unuseAll().post.update({ where: { id: 1 }, data: { title: 'Bar Post' } }); // can update await expect( @@ -1124,7 +1127,7 @@ model Foo { // can't update, but create violates unique constraint await expect( db.foo.upsert({ where: { id: 1 }, create: { id: 1, x: 1 }, update: { x: 1 } }), - ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().includes('constraint')); + ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().match(/(constraint)|(duplicate)/)); await db.$unuseAll().foo.update({ where: { id: 1 }, data: { x: 2 } }); // can update now await expect( @@ -1226,38 +1229,51 @@ model Foo { ], }); + const mysql = db.$schema.provider.type === 'mysql'; + // #1 not updatable - await expect( - db.$qb - .insertInto('Foo') - .values({ id: 1, x: 5 }) - .onConflict((oc: any) => oc.column('id').doUpdateSet({ x: 5 })) - .executeTakeFirst(), - ).resolves.toMatchObject({ numInsertedOrUpdatedRows: 0n }); - await expect(db.foo.count()).resolves.toBe(3); + const r = await db.$qb + .insertInto('Foo') + .values({ id: 1, x: 5 }) + .$if(mysql, (qb) => qb.onDuplicateKeyUpdate({ x: 5 })) + .$if(!mysql, (qb) => qb.onConflict((oc: any) => oc.column('id').doUpdateSet({ x: 5 }))) + .executeTakeFirst(); + + if (!mysql) { + expect(r).toMatchObject({ numInsertedOrUpdatedRows: 0n }); + } else { + // mysql's on duplicate key update returns rows affected even if no values are changed + expect(r).toMatchObject({ numInsertedOrUpdatedRows: 1n }); + } + // verify not updated await expect(db.foo.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ x: 1 }); - // with where, #1 not updatable - await expect( - db.$qb - .insertInto('Foo') - .values({ id: 1, x: 5 }) - .onConflict((oc: any) => oc.column('id').doUpdateSet({ x: 5 }).where('Foo.id', '=', 1)) - .executeTakeFirst(), - ).resolves.toMatchObject({ numInsertedOrUpdatedRows: 0n }); await expect(db.foo.count()).resolves.toBe(3); await expect(db.foo.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ x: 1 }); - // with where, #2 updatable - await expect( - db.$qb - .insertInto('Foo') - .values({ id: 2, x: 5 }) - .onConflict((oc: any) => oc.column('id').doUpdateSet({ x: 6 }).where('Foo.id', '=', 2)) - .executeTakeFirst(), - ).resolves.toMatchObject({ numInsertedOrUpdatedRows: 1n }); - await expect(db.foo.count()).resolves.toBe(3); - await expect(db.foo.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ x: 6 }); + if (!mysql) { + // with where, #1 not updatable + await expect( + db.$qb + .insertInto('Foo') + .values({ id: 1, x: 5 }) + .onConflict((oc: any) => oc.column('id').doUpdateSet({ x: 5 }).where('Foo.id', '=', 1)) + .executeTakeFirst(), + ).resolves.toMatchObject({ numInsertedOrUpdatedRows: 0n }); + await expect(db.foo.count()).resolves.toBe(3); + await expect(db.foo.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ x: 1 }); + + // with where, #2 updatable + await expect( + db.$qb + .insertInto('Foo') + .values({ id: 2, x: 5 }) + .onConflict((oc: any) => oc.column('id').doUpdateSet({ x: 6 }).where('Foo.id', '=', 2)) + .executeTakeFirst(), + ).resolves.toMatchObject({ numInsertedOrUpdatedRows: 1n }); + await expect(db.foo.count()).resolves.toBe(3); + await expect(db.foo.findUnique({ where: { id: 2 } })).resolves.toMatchObject({ x: 6 }); + } }); }); }); diff --git a/tests/e2e/orm/policy/migrated/auth.test.ts b/tests/e2e/orm/policy/migrated/auth.test.ts index b3e499802..c612e1204 100644 --- a/tests/e2e/orm/policy/migrated/auth.test.ts +++ b/tests/e2e/orm/policy/migrated/auth.test.ts @@ -325,15 +325,18 @@ model Post { await expect(userDb.post.count({ where: { authorName: 'user1', score: 10 } })).resolves.toBe(1); await expect(userDb.post.createMany({ data: [{ title: 'def' }] })).resolves.toMatchObject({ count: 1 }); - const r = await userDb.post.createManyAndReturn({ - data: [{ title: 'xxx' }, { title: 'yyy' }], - }); - expect(r).toEqual( - expect.arrayContaining([ - expect.objectContaining({ title: 'xxx', score: 10 }), - expect.objectContaining({ title: 'yyy', score: 10 }), - ]), - ); + + if (userDb.$schema.provider.type !== 'mysql') { + const r = await userDb.post.createManyAndReturn({ + data: [{ title: 'xxx' }, { title: 'yyy' }], + }); + expect(r).toEqual( + expect.arrayContaining([ + expect.objectContaining({ title: 'xxx', score: 10 }), + expect.objectContaining({ title: 'yyy', score: 10 }), + ]), + ); + } }); it('respects explicitly passed field values even when default is set', async () => { @@ -363,10 +366,12 @@ model Post { await expect(userDb.post.createMany({ data: [{ authorName: overrideName }] })).toResolveTruthy(); await expect(userDb.post.count({ where: { authorName: overrideName } })).resolves.toBe(2); - const r = await userDb.post.createManyAndReturn({ - data: [{ authorName: overrideName }], - }); - expect(r[0]).toMatchObject({ authorName: overrideName }); + if (userDb.$schema.provider.type !== 'mysql') { + const r = await userDb.post.createManyAndReturn({ + data: [{ authorName: overrideName }], + }); + expect(r[0]).toMatchObject({ authorName: overrideName }); + } }); it('works with using auth value as default for foreign key', async () => { @@ -439,11 +444,13 @@ model Post { const r = await db.post.findFirst({ where: { title: 'post5' } }); expect(r).toMatchObject({ authorId: 'userId-1' }); - // default auth effective for createManyAndReturn - const r1 = await db.post.createManyAndReturn({ - data: { title: 'post6' }, - }); - expect(r1[0]).toMatchObject({ authorId: 'userId-1' }); + if (db.$schema.provider.type !== 'mysql') { + // default auth effective for createManyAndReturn + const r1 = await db.post.createManyAndReturn({ + data: { title: 'post6' }, + }); + expect(r1[0]).toMatchObject({ authorId: 'userId-1' }); + } }); it('works with using nested auth value as default', async () => { @@ -537,7 +544,7 @@ model Post { await expect(db.user.create({ data: { id: 'userId-1' } })).toResolveTruthy(); await expect(db.post.create({ data: { title: 'title' } })).rejects.toSatisfy((e) => - e.cause.message.toLowerCase().includes('constraint'), + e.cause.message.toLowerCase().match(/(constraint)|(cannot be null)/), ); await expect(db.post.findMany({})).toResolveTruthy(); }); @@ -591,10 +598,13 @@ model Post { }); await db.stats.create({ data: { id: 'stats-3', viewCount: 10 } }); - const r = await db.post.createManyAndReturn({ - data: [{ title: 'title3', statsId: 'stats-3' }], - }); - expect(r[0]).toMatchObject({ statsId: 'stats-3' }); + + if (db.$schema.provider.type !== 'mysql') { + const r = await db.post.createManyAndReturn({ + data: [{ title: 'title3', statsId: 'stats-3' }], + }); + expect(r[0]).toMatchObject({ statsId: 'stats-3' }); + } // checked context await db.stats.create({ data: { id: 'stats-4', viewCount: 10 } }); diff --git a/tests/e2e/orm/policy/migrated/create-many-and-return.test.ts b/tests/e2e/orm/policy/migrated/create-many-and-return.test.ts index 5191853c6..d11723d78 100644 --- a/tests/e2e/orm/policy/migrated/create-many-and-return.test.ts +++ b/tests/e2e/orm/policy/migrated/create-many-and-return.test.ts @@ -25,6 +25,12 @@ describe('createManyAndReturn tests', () => { } `, ); + + if (db.$schema.provider.type === 'mysql') { + // MySQL does not support createManyAndReturn + return; + } + const rawDb = db.$unuseAll(); await rawDb.user.createMany({ @@ -60,8 +66,7 @@ describe('createManyAndReturn tests', () => { await expect(rawDb.post.findMany()).resolves.toHaveLength(3); }); - // TODO: field-level policies support - it.skip('field-level policies', async () => { + it('field-level policies', async () => { const db = await createPolicyTestClient( ` model Post { @@ -73,6 +78,12 @@ describe('createManyAndReturn tests', () => { } `, ); + + if (db.$schema.provider.type === 'mysql') { + // MySQL does not support createManyAndReturn + return; + } + const rawDb = db.$unuseAll(); // create should succeed but one result's title field can't be read back const r = await db.post.createManyAndReturn({ @@ -84,7 +95,7 @@ describe('createManyAndReturn tests', () => { expect(r.length).toBe(2); expect(r[0].title).toBeTruthy(); - expect(r[1].title).toBeUndefined(); + expect(r[1].title).toBeNull(); // check posts are created await expect(rawDb.post.findMany()).resolves.toHaveLength(2); diff --git a/tests/e2e/orm/policy/migrated/current-model.test.ts b/tests/e2e/orm/policy/migrated/current-model.test.ts index 0ceb96c32..7272e74a4 100644 --- a/tests/e2e/orm/policy/migrated/current-model.test.ts +++ b/tests/e2e/orm/policy/migrated/current-model.test.ts @@ -41,7 +41,13 @@ describe('currentModel tests', () => { ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + + if (db.$schema.provider.type !== 'mysql') { + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + } else { + // mysql string comparison is case insensitive by default + await expect(db.post.create({ data: { id: 1 } })).toResolveTruthy(); + } }); it('works with lower case', async () => { @@ -62,7 +68,13 @@ describe('currentModel tests', () => { ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + + if (db.$schema.provider.type !== 'mysql') { + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + } else { + // mysql string comparison is case insensitive by default + await expect(db.post.create({ data: { id: 1 } })).toResolveTruthy(); + } }); it('works with capitalization', async () => { @@ -83,7 +95,13 @@ describe('currentModel tests', () => { ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + + if (db.$schema.provider.type !== 'mysql') { + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + } else { + // mysql string comparison is case insensitive by default + await expect(db.post.create({ data: { id: 1 } })).toResolveTruthy(); + } }); it('works with uncapitalization', async () => { @@ -104,7 +122,13 @@ describe('currentModel tests', () => { ); await expect(db.USER.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.POST.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + + if (db.$schema.provider.type !== 'mysql') { + await expect(db.POST.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + } else { + // mysql string comparison is case insensitive by default + await expect(db.POST.create({ data: { id: 1 } })).toResolveTruthy(); + } }); it('works when inherited from abstract base', async () => { diff --git a/tests/e2e/orm/policy/migrated/current-operation.test.ts b/tests/e2e/orm/policy/migrated/current-operation.test.ts index 3cbae4cae..374c7cf98 100644 --- a/tests/e2e/orm/policy/migrated/current-operation.test.ts +++ b/tests/e2e/orm/policy/migrated/current-operation.test.ts @@ -99,7 +99,13 @@ describe('currentOperation tests', () => { ); await expect(db.user.create({ data: { id: 1 } })).toResolveTruthy(); - await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + + if (db.$schema.provider.type !== 'mysql') { + await expect(db.post.create({ data: { id: 1 } })).toBeRejectedByPolicy(); + } else { + // mysql string comparison is case insensitive by default + await expect(db.post.create({ data: { id: 1 } })).toResolveTruthy(); + } }); it('works with uncapitalization', async () => { diff --git a/tests/e2e/orm/policy/migrated/deep-nested.test.ts b/tests/e2e/orm/policy/migrated/deep-nested.test.ts index f8bcea930..a118757c1 100644 --- a/tests/e2e/orm/policy/migrated/deep-nested.test.ts +++ b/tests/e2e/orm/policy/migrated/deep-nested.test.ts @@ -482,7 +482,7 @@ describe('deep nested operations tests', () => { }, }, }), - ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().includes('constraint')); + ).rejects.toSatisfy((e) => e.cause.message.toLowerCase().match(/(constraint)|(duplicate)/)); // createMany skip duplicate await db.m1.update({ diff --git a/tests/e2e/orm/policy/migrated/multi-id-fields.test.ts b/tests/e2e/orm/policy/migrated/multi-id-fields.test.ts index 0c3bdf2a2..e056a3156 100644 --- a/tests/e2e/orm/policy/migrated/multi-id-fields.test.ts +++ b/tests/e2e/orm/policy/migrated/multi-id-fields.test.ts @@ -96,13 +96,25 @@ describe('Policy tests multiple id fields', () => { db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 0 } }), ).toBeRejectedByPolicy(); - await expect( - db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }), - ).resolves.toMatchObject({ - x: '2', - y: 3, - value: 2, - }); + const mysql = db.$schema.provider.type === 'mysql'; + + if (!mysql) { + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }), + ).resolves.toMatchObject({ + x: '2', + y: 3, + value: 2, + }); + } else { + // mysql doesn't support post-update policies with id updates + await expect( + db.a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }), + ).toBeRejectedByPolicy(); + + // force update + await db.$unuseAll().a.update({ where: { x_y: { x: '1', y: 2 } }, data: { x: '2', y: 3, value: 2 } }); + } await expect( db.a.upsert({ @@ -112,17 +124,28 @@ describe('Policy tests multiple id fields', () => { }), ).toBeRejectedByPolicy(); - await expect( - db.a.upsert({ - where: { x_y: { x: '2', y: 3 } }, - update: { x: '3', y: 4, value: 3 }, - create: { x: '4', y: 5, value: 5 }, - }), - ).resolves.toMatchObject({ - x: '3', - y: 4, - value: 3, - }); + if (!mysql) { + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 3 }, + create: { x: '4', y: 5, value: 5 }, + }), + ).resolves.toMatchObject({ + x: '3', + y: 4, + value: 3, + }); + } else { + // mysql doesn't support post-update policies with id updates + await expect( + db.a.upsert({ + where: { x_y: { x: '2', y: 3 } }, + update: { x: '3', y: 4, value: 3 }, + create: { x: '4', y: 5, value: 5 }, + }), + ).toBeRejectedByPolicy(); + } }); it('multi-id auth', async () => { @@ -353,13 +376,34 @@ describe('Policy tests multiple id fields', () => { }), ).toBeRejectedByPolicy(); - await expect( - db.b.update({ + const mysql = db.$schema.provider.type === 'mysql'; + + if (!mysql) { + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 2 } } } }, + include: { a: true }, + }), + ).resolves.toMatchObject({ + a: expect.arrayContaining([expect.objectContaining({ x: '2', y: 2, value: 2 })]), + }); + } else { + // mysql doesn't support post-update policies with id updates + await expect( + db.b.update({ + where: { id: 1 }, + data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 2 } } } }, + include: { a: true }, + }), + ).toBeRejectedByPolicy(); + + // force update + await db.$unuseAll().b.update({ where: { id: 1 }, data: { a: { update: { where: { x_y: { x: '1', y: 1 } }, data: { x: '2', y: 2, value: 2 } } } }, - include: { a: true }, - }), - ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '2', y: 2, value: 2 })]) }); + }); + } await expect( db.b.update({ @@ -376,20 +420,24 @@ describe('Policy tests multiple id fields', () => { }), ).toBeRejectedByPolicy(); - await expect( - db.b.update({ - where: { id: 1 }, - data: { - a: { - upsert: { - where: { x_y: { x: '2', y: 2 } }, - update: { x: '3', y: 3, value: 3 }, - create: { x: '4', y: 4, value: 4 }, + if (!mysql) { + await expect( + db.b.update({ + where: { id: 1 }, + data: { + a: { + upsert: { + where: { x_y: { x: '2', y: 2 } }, + update: { x: '3', y: 3, value: 3 }, + create: { x: '4', y: 4, value: 4 }, + }, }, }, - }, - include: { a: true }, - }), - ).resolves.toMatchObject({ a: expect.arrayContaining([expect.objectContaining({ x: '3', y: 3, value: 3 })]) }); + include: { a: true }, + }), + ).resolves.toMatchObject({ + a: expect.arrayContaining([expect.objectContaining({ x: '3', y: 3, value: 3 })]), + }); + } }); }); diff --git a/tests/e2e/orm/policy/migrated/nested-to-one.test.ts b/tests/e2e/orm/policy/migrated/nested-to-one.test.ts index f839336cb..12481ae06 100644 --- a/tests/e2e/orm/policy/migrated/nested-to-one.test.ts +++ b/tests/e2e/orm/policy/migrated/nested-to-one.test.ts @@ -243,17 +243,32 @@ describe('With Policy:nested to-one', () => { }), ).toBeRejectedByPolicy(); - await expect( - db.m1.update({ - where: { id: '1' }, - data: { - m2: { - update: { id: '2', value: 3 }, + if (db.$schema.provider.type !== 'mysql') { + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 3 }, + }, }, - }, - include: { m2: true }, - }), - ).resolves.toMatchObject({ m2: expect.objectContaining({ id: '2', value: 3 }) }); + include: { m2: true }, + }), + ).resolves.toMatchObject({ m2: expect.objectContaining({ id: '2', value: 3 }) }); + } else { + // mysql does not support post-update with id updates + await expect( + db.m1.update({ + where: { id: '1' }, + data: { + m2: { + update: { id: '2', value: 3 }, + }, + }, + include: { m2: true }, + }), + ).toBeRejectedByPolicy(); + } }); it('nested create', async () => { diff --git a/tests/e2e/orm/policy/migrated/toplevel-operations.test.ts b/tests/e2e/orm/policy/migrated/toplevel-operations.test.ts index 086efaf05..564b89b14 100644 --- a/tests/e2e/orm/policy/migrated/toplevel-operations.test.ts +++ b/tests/e2e/orm/policy/migrated/toplevel-operations.test.ts @@ -166,16 +166,38 @@ describe('Policy toplevel operations tests', () => { }), ).toBeRejectedByPolicy(); - // update success - await expect( - db.model.update({ + if (db.$schema.provider.type !== 'mysql') { + // update success + await expect( + db.model.update({ + where: { id: '1' }, + data: { + id: '2', + value: 3, + }, + }), + ).resolves.toMatchObject({ id: '2', value: 3 }); + } else { + // mysql doesn't support post-update with id updates + await expect( + db.model.update({ + where: { id: '1' }, + data: { + id: '2', + value: 3, + }, + }), + ).toBeRejectedByPolicy(); + + // force update + await db.$unuseAll().model.update({ where: { id: '1' }, data: { id: '2', value: 3, }, - }), - ).resolves.toMatchObject({ id: '2', value: 3 }); + }); + } // upsert denied await expect( @@ -192,20 +214,22 @@ describe('Policy toplevel operations tests', () => { }), ).toBeRejectedByPolicy(); - // upsert success - await expect( - db.model.upsert({ - where: { id: '2' }, - update: { - id: '3', - value: 4, - }, - create: { - id: '4', - value: 5, - }, - }), - ).resolves.toMatchObject({ id: '3', value: 4 }); + if (db.$schema.provider.type !== 'mysql') { + // upsert success + await expect( + db.model.upsert({ + where: { id: '2' }, + update: { + id: '3', + value: 4, + }, + create: { + id: '4', + value: 5, + }, + }), + ).resolves.toMatchObject({ id: '3', value: 4 }); + } }); it('delete tests', async () => { diff --git a/tests/e2e/orm/policy/migrated/update-many-and-return.test.ts b/tests/e2e/orm/policy/migrated/update-many-and-return.test.ts index 32367c359..38f336900 100644 --- a/tests/e2e/orm/policy/migrated/update-many-and-return.test.ts +++ b/tests/e2e/orm/policy/migrated/update-many-and-return.test.ts @@ -26,6 +26,11 @@ describe('Policy updateManyAndReturn tests', () => { `, ); + if (db.$schema.provider.type === 'mysql') { + // skip mysql as it doesn't support updateManyAndReturn + return; + } + const rawDb = db.$unuseAll(); await rawDb.user.createMany({ @@ -82,8 +87,7 @@ describe('Policy updateManyAndReturn tests', () => { await expect(db.$unuseAll().post.findUnique({ where: { id: 1 } })).resolves.toMatchObject({ published: false }); }); - // TODO: field-level policy support - it.skip('field-level policies', async () => { + it('field-level policies', async () => { const db = await createPolicyTestClient( ` model Post { @@ -96,6 +100,11 @@ describe('Policy updateManyAndReturn tests', () => { `, ); + if (db.$schema.provider.type === 'mysql') { + // skip mysql as it doesn't support updateManyAndReturn + return; + } + const rawDb = db.$unuseAll(); // update should succeed but one result's title field can't be read back @@ -112,7 +121,7 @@ describe('Policy updateManyAndReturn tests', () => { expect(r.length).toBe(2); expect(r[0].title).toBeTruthy(); - expect(r[1].title).toBeUndefined(); + expect(r[1].title).toBeNull(); // check posts are updated await expect(rawDb.post.findMany({ where: { title: 'foo' } })).resolves.toHaveLength(2); diff --git a/tests/e2e/orm/policy/migrated/view.test.ts b/tests/e2e/orm/policy/migrated/view.test.ts index 7a8afe285..6251b37fa 100644 --- a/tests/e2e/orm/policy/migrated/view.test.ts +++ b/tests/e2e/orm/policy/migrated/view.test.ts @@ -36,7 +36,13 @@ describe('View Policy Test', () => { const rawDb = db.$unuseAll(); - await rawDb.$executeRaw`CREATE VIEW "UserInfo" as select "User"."id", "User"."name", "User"."email", "User"."id" as "userId", count("Post"."id") as "postCount" from "User" left join "Post" on "User"."id" = "Post"."authorId" group by "User"."id";`; + if (['postgresql', 'sqlite'].includes(rawDb.$schema.provider.type)) { + await rawDb.$executeRaw`CREATE VIEW "UserInfo" as select "User"."id", "User"."name", "User"."email", "User"."id" as "userId", count("Post"."id") as "postCount" from "User" left join "Post" on "User"."id" = "Post"."authorId" group by "User"."id";`; + } else if (rawDb.$schema.provider.type === 'mysql') { + await rawDb.$executeRaw`CREATE VIEW UserInfo as select User.id, User.name, User.email, User.id as userId, count(Post.id) as postCount from User left join Post on User.id = Post.authorId group by User.id;`; + } else { + throw new Error(`Unsupported provider: ${rawDb.$schema.provider.type}`); + } await rawDb.user.create({ data: { diff --git a/tests/e2e/orm/policy/nonexistent-models.test.ts b/tests/e2e/orm/policy/nonexistent-models.test.ts index 70fd0ecb1..6cde1054a 100644 --- a/tests/e2e/orm/policy/nonexistent-models.test.ts +++ b/tests/e2e/orm/policy/nonexistent-models.test.ts @@ -15,9 +15,15 @@ describe('Policy tests for nonexistent models and fields', () => { const dbRaw = db.$unuseAll(); // create a Bar table - await dbRaw.$executeRawUnsafe( - `CREATE TABLE "Bar" ("id" TEXT PRIMARY KEY, "string" TEXT, "fooId" TEXT, FOREIGN KEY ("fooId") REFERENCES "Foo" ("id"));`, - ); + if (['postgresql', 'sqlite'].includes(dbRaw.$schema.provider.type)) { + await dbRaw.$executeRawUnsafe( + `CREATE TABLE "Bar" ("id" TEXT PRIMARY KEY, "string" TEXT, "fooId" TEXT, FOREIGN KEY ("fooId") REFERENCES "Foo" ("id"));`, + ); + } else { + await dbRaw.$executeRawUnsafe( + `CREATE TABLE Bar (id VARCHAR(191) PRIMARY KEY, string VARCHAR(191), fooId VARCHAR(191), FOREIGN KEY (fooId) REFERENCES Foo (id));`, + ); + } await dbRaw.$qb.insertInto('Foo').values({ id: '1', string: 'test' }).execute(); await dbRaw.$qb.insertInto('Bar').values({ id: '1', string: 'test', fooId: '1' }).execute(); diff --git a/tests/e2e/orm/policy/policy-functions.test.ts b/tests/e2e/orm/policy/policy-functions.test.ts index b30ea5ce8..9d12defb8 100644 --- a/tests/e2e/orm/policy/policy-functions.test.ts +++ b/tests/e2e/orm/policy/policy-functions.test.ts @@ -14,8 +14,8 @@ describe('policy functions tests', () => { ); await expect(db.foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy(); - if (db.$schema.provider.type === 'sqlite') { - // sqlite is always case-insensitive + if (['sqlite', 'mysql'].includes(db.$schema.provider.type)) { + // sqlite and mysql are always case-insensitive await expect(db.foo.create({ data: { string: 'Acd' } })).toResolveTruthy(); } else { await expect(db.foo.create({ data: { string: 'Acd' } })).toBeRejectedByPolicy(); @@ -51,8 +51,8 @@ describe('policy functions tests', () => { ); await expect(db.foo.create({ data: { string: 'bcd' } })).toBeRejectedByPolicy(); - if (db.$schema.provider.type === 'sqlite') { - // sqlite is always case-insensitive + if (['sqlite', 'mysql'].includes(db.$schema.provider.type)) { + // sqlite and mysql are always case-insensitive await expect(db.foo.create({ data: { string: 'Acd' } })).toResolveTruthy(); } else { await expect(db.foo.create({ data: { string: 'Acd' } })).toBeRejectedByPolicy(); @@ -93,8 +93,8 @@ describe('policy functions tests', () => { await expect(db.foo.create({ data: {} })).toBeRejectedByPolicy(); await expect(db.$setAuth({ id: 'user1', name: 'bcd' }).foo.create({ data: {} })).toBeRejectedByPolicy(); await expect(db.$setAuth({ id: 'user1', name: 'bac' }).foo.create({ data: {} })).toResolveTruthy(); - if (db.$schema.provider.type === 'sqlite') { - // sqlite is always case-insensitive + if (['sqlite', 'mysql'].includes(db.$schema.provider.type)) { + // sqlite and mysql are always case-insensitive await expect(db.$setAuth({ id: 'user1', name: 'Abc' }).foo.create({ data: {} })).toResolveTruthy(); } else { await expect(db.$setAuth({ id: 'user1', name: 'Abc' }).foo.create({ data: {} })).toBeRejectedByPolicy(); diff --git a/tests/e2e/orm/query-builder/query-builder.test.ts b/tests/e2e/orm/query-builder/query-builder.test.ts index 563118a4e..ea4aded20 100644 --- a/tests/e2e/orm/query-builder/query-builder.test.ts +++ b/tests/e2e/orm/query-builder/query-builder.test.ts @@ -17,7 +17,7 @@ describe('Client API tests', () => { .values({ id: uid, email: 'a@b.com', - updatedAt: new Date().toISOString(), + updatedAt: new Date().toISOString().replace('Z', '+00:00'), }) .execute(); @@ -31,7 +31,7 @@ describe('Client API tests', () => { authorId: uid, title: 'Post1', content: 'My post', - updatedAt: new Date().toISOString(), + updatedAt: new Date().toISOString().replace('Z', '+00:00'), }) .execute(); diff --git a/tests/e2e/orm/schemas/basic/input.ts b/tests/e2e/orm/schemas/basic/input.ts index 4bbec22c6..90babcce0 100644 --- a/tests/e2e/orm/schemas/basic/input.ts +++ b/tests/e2e/orm/schemas/basic/input.ts @@ -92,3 +92,24 @@ export type ProfileSelect = $SelectInput<$Schema, "Profile">; export type ProfileInclude = $IncludeInput<$Schema, "Profile">; export type ProfileOmit = $OmitInput<$Schema, "Profile">; export type ProfileGetPayload, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Profile", Args, Options>; +export type PlainFindManyArgs = $FindManyArgs<$Schema, "Plain">; +export type PlainFindUniqueArgs = $FindUniqueArgs<$Schema, "Plain">; +export type PlainFindFirstArgs = $FindFirstArgs<$Schema, "Plain">; +export type PlainExistsArgs = $ExistsArgs<$Schema, "Plain">; +export type PlainCreateArgs = $CreateArgs<$Schema, "Plain">; +export type PlainCreateManyArgs = $CreateManyArgs<$Schema, "Plain">; +export type PlainCreateManyAndReturnArgs = $CreateManyAndReturnArgs<$Schema, "Plain">; +export type PlainUpdateArgs = $UpdateArgs<$Schema, "Plain">; +export type PlainUpdateManyArgs = $UpdateManyArgs<$Schema, "Plain">; +export type PlainUpdateManyAndReturnArgs = $UpdateManyAndReturnArgs<$Schema, "Plain">; +export type PlainUpsertArgs = $UpsertArgs<$Schema, "Plain">; +export type PlainDeleteArgs = $DeleteArgs<$Schema, "Plain">; +export type PlainDeleteManyArgs = $DeleteManyArgs<$Schema, "Plain">; +export type PlainCountArgs = $CountArgs<$Schema, "Plain">; +export type PlainAggregateArgs = $AggregateArgs<$Schema, "Plain">; +export type PlainGroupByArgs = $GroupByArgs<$Schema, "Plain">; +export type PlainWhereInput = $WhereInput<$Schema, "Plain">; +export type PlainSelect = $SelectInput<$Schema, "Plain">; +export type PlainInclude = $IncludeInput<$Schema, "Plain">; +export type PlainOmit = $OmitInput<$Schema, "Plain">; +export type PlainGetPayload, Options extends $QueryOptions<$Schema> = $QueryOptions<$Schema>> = $Result<$Schema, "Plain", Args, Options>; diff --git a/tests/e2e/orm/schemas/basic/models.ts b/tests/e2e/orm/schemas/basic/models.ts index be1978796..733e7df68 100644 --- a/tests/e2e/orm/schemas/basic/models.ts +++ b/tests/e2e/orm/schemas/basic/models.ts @@ -11,6 +11,7 @@ export type User = $ModelResult<$Schema, "User">; export type Post = $ModelResult<$Schema, "Post">; export type Comment = $ModelResult<$Schema, "Comment">; export type Profile = $ModelResult<$Schema, "Profile">; +export type Plain = $ModelResult<$Schema, "Plain">; export type CommonFields = $TypeDefResult<$Schema, "CommonFields">; export const Role = $schema.enums.Role.values; export type Role = (typeof Role)[keyof typeof Role]; diff --git a/tests/e2e/orm/schemas/basic/schema.ts b/tests/e2e/orm/schemas/basic/schema.ts index 5f067685e..1e559cc79 100644 --- a/tests/e2e/orm/schemas/basic/schema.ts +++ b/tests/e2e/orm/schemas/basic/schema.ts @@ -246,6 +246,26 @@ export class SchemaType implements SchemaDef { id: { type: "String" }, userId: { type: "String" } } + }, + Plain: { + name: "Plain", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }], + default: ExpressionUtils.call("autoincrement") + }, + value: { + name: "value", + type: "Int" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } } } as const; typeDefs = { diff --git a/tests/e2e/orm/schemas/basic/schema.zmodel b/tests/e2e/orm/schemas/basic/schema.zmodel index a831b827a..9d3a7d91d 100644 --- a/tests/e2e/orm/schemas/basic/schema.zmodel +++ b/tests/e2e/orm/schemas/basic/schema.zmodel @@ -64,3 +64,7 @@ model Foo { @@ignore } +model Plain { + id Int @id @default(autoincrement()) + value Int +} diff --git a/tests/e2e/package.json b/tests/e2e/package.json index 5022cf2e8..f37d8b29d 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -9,7 +9,8 @@ "test:typecheck": "tsc --noEmit", "test": "vitest run", "test:sqlite": "TEST_DB_PROVIDER=sqlite vitest run", - "test:postgresql": "TEST_DB_PROVIDER=postgresql vitest run" + "test:postgresql": "TEST_DB_PROVIDER=postgresql vitest run", + "test:mysql": "TEST_DB_PROVIDER=mysql vitest run" }, "dependencies": { "@paralleldrive/cuid2": "^2.2.2", @@ -26,7 +27,8 @@ "ulid": "^3.0.0", "uuid": "^11.0.5", "cuid": "^3.0.0", - "zod": "catalog:" + "zod": "catalog:", + "ts-pattern": "catalog:" }, "devDependencies": { "@zenstackhq/cli": "workspace:*", diff --git a/tests/regression/package.json b/tests/regression/package.json index 1e66dafb0..d6b14dac5 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -6,7 +6,10 @@ "scripts": { "build": "pnpm run test:generate", "test:generate": "tsx ../../scripts/test-generate.ts ./test", - "test": "pnpm test:generate && tsc && vitest run" + "test": "pnpm test:generate && tsc && vitest run", + "test:sqlite": "TEST_DB_PROVIDER=sqlite vitest run", + "test:postgresql": "TEST_DB_PROVIDER=postgresql vitest run", + "test:mysql": "TEST_DB_PROVIDER=mysql vitest run" }, "dependencies": { "@zenstackhq/testtools": "workspace:*", diff --git a/tests/regression/test/issue-493.test.ts b/tests/regression/test/issue-493.test.ts index 269a68f33..fb30ef3dc 100644 --- a/tests/regression/test/issue-493.test.ts +++ b/tests/regression/test/issue-493.test.ts @@ -40,7 +40,7 @@ model Foo { } `; - const db = await createTestClient(schema, { provider: 'postgresql', debug: true }); + const db = await createTestClient(schema, { provider: 'postgresql' }); // plain JSON non-array await expect( diff --git a/tests/regression/test/v2-migrated/issue-1576.test.ts b/tests/regression/test/v2-migrated/issue-1576.test.ts index 91870b3e7..3c62af01f 100644 --- a/tests/regression/test/v2-migrated/issue-1576.test.ts +++ b/tests/regression/test/v2-migrated/issue-1576.test.ts @@ -40,24 +40,48 @@ describe('Regression for issue #1576', () => { }, }); - await expect( - db.goldItem.createManyAndReturn({ - data: [ - { - profileId: profile.id, - inventory: true, - }, - { - profileId: profile.id, - inventory: true, - }, - ], - }), - ).resolves.toEqual( - expect.arrayContaining([ - expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), - expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), - ]), - ); + if (db.$schema.provider.type !== 'mysql') { + await expect( + db.goldItem.createManyAndReturn({ + data: [ + { + profileId: profile.id, + inventory: true, + }, + { + profileId: profile.id, + inventory: true, + }, + ], + }), + ).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + ]), + ); + } else { + // mysql doesn't support createManyAndReturn + await expect( + db.goldItem.createMany({ + data: [ + { + profileId: profile.id, + inventory: true, + }, + { + profileId: profile.id, + inventory: true, + }, + ], + }), + ).toResolveTruthy(); + await expect(db.goldItem.findMany()).resolves.toEqual( + expect.arrayContaining([ + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + expect.objectContaining({ profileId: profile.id, type: 'GoldItem', inventory: true }), + ]), + ); + } }); }); diff --git a/tests/regression/test/v2-migrated/issue-1681.test.ts b/tests/regression/test/v2-migrated/issue-1681.test.ts index 0483a1c34..ba867dbf1 100644 --- a/tests/regression/test/v2-migrated/issue-1681.test.ts +++ b/tests/regression/test/v2-migrated/issue-1681.test.ts @@ -25,7 +25,9 @@ describe('Regression for issue #1681', () => { const user = await db.user.create({ data: {} }); await expect(authDb.post.createMany({ data: [{ title: 'Post1' }] })).resolves.toMatchObject({ count: 1 }); - const r = await authDb.post.createManyAndReturn({ data: [{ title: 'Post2' }] }); - expect(r[0].authorId).toBe(user.id); + if (db.$schema.provider.type !== 'mysql') { + const r = await authDb.post.createManyAndReturn({ data: [{ title: 'Post2' }] }); + expect(r[0].authorId).toBe(user.id); + } }); }); diff --git a/tests/regression/test/v2-migrated/issue-1894.test.ts b/tests/regression/test/v2-migrated/issue-1894.test.ts index 8d745851b..76d3d61fd 100644 --- a/tests/regression/test/v2-migrated/issue-1894.test.ts +++ b/tests/regression/test/v2-migrated/issue-1894.test.ts @@ -42,7 +42,7 @@ describe('Regression for issue #1894', () => { }, ); - await db.a.create({ data: { id: 0 } }); - await expect(db.c.create({ data: { a: { connect: { id: 0 } } } })).toResolveTruthy(); + const r = await db.a.create({ data: { id: 0 } }); + await expect(db.c.create({ data: { a: { connect: { id: r.id } } } })).toResolveTruthy(); }); }); From c7ad7d78984fd7def8131d28975fef3bd1907646 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Sun, 25 Jan 2026 21:01:39 +0800 Subject: [PATCH 09/25] test: add basic memory stress test (#620) --- tests/e2e/performance/memory-test.test.ts | 308 ++++++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 tests/e2e/performance/memory-test.test.ts diff --git a/tests/e2e/performance/memory-test.test.ts b/tests/e2e/performance/memory-test.test.ts new file mode 100644 index 000000000..b7572df60 --- /dev/null +++ b/tests/e2e/performance/memory-test.test.ts @@ -0,0 +1,308 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +describe.skip('Memory usage test with repeated CRUD operations', () => { + let client: any; + + beforeEach(async () => { + client = await createTestClient( + ` +model User { + id String @id @default(cuid()) + email String @unique + name String + createdAt DateTime @default(now()) + posts Post[] + comments Comment[] +} + +model Post { + id String @id @default(cuid()) + title String + content String + published Boolean @default(false) + createdAt DateTime @default(now()) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String + comments Comment[] +} + +model Comment { + id String @id @default(cuid()) + content String + createdAt DateTime @default(now()) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) + postId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String +} +`, + ); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + it('repeatedly executes CRUD operations with random data and tracks memory', async () => { + // ============ CONFIGURATION ============ + // Adjust these values to test different workload scenarios + const iterations = 100; // Number of complete CRUD cycles to execute + const usersCount = 10; // Number of users to create per iteration + const postsPerUser = 5; // Number of posts per user + const commentsPerPost = 3; // Number of comments per post + + // Calculated totals + const totalPosts = usersCount * postsPerUser; + const totalComments = totalPosts * commentsPerPost; + + const memorySnapshots: Array<{ + iteration: number; + rss: number; + heapTotal: number; + heapUsed: number; + external: number; + }> = []; + + // Helper function to generate random string + const randomString = (length: number) => { + return Math.random() + .toString(36) + .substring(2, 2 + length); + }; + + // Helper function to generate random content + const randomContent = () => { + const paragraphs = Math.floor(Math.random() * 5) + 1; + return Array.from({ length: paragraphs }, () => randomString(100)).join('\n\n'); + }; + + console.log(`\nStarting ${iterations} iterations of CRUD operations...\n`); + + for (let i = 0; i < iterations; i++) { + // ============ CREATE ============ + + // Create users + const users = await Promise.all( + Array.from({ length: usersCount }, (_, idx) => + client.user.create({ + data: { + email: `user${i}-${idx + 1}-${randomString(8)}@test.com`, + name: `User ${i}-${idx + 1} ${randomString(10)}`, + }, + }), + ), + ); + + // Create posts per user + const posts: any[] = []; + for (const user of users) { + for (let j = 0; j < postsPerUser; j++) { + const post = await client.post.create({ + data: { + title: `Post ${i}-${j} - ${randomString(20)}`, + content: randomContent(), + published: Math.random() > 0.5, + authorId: user.id, + }, + }); + posts.push(post); + } + } + + // Create comments per post + const comments: any[] = []; + for (const post of posts) { + for (let k = 0; k < commentsPerPost; k++) { + const randomAuthor = users[Math.floor(Math.random() * users.length)]!; + const comment = await client.comment.create({ + data: { + content: randomString(100), + postId: post.id, + authorId: randomAuthor.id, + }, + }); + comments.push(comment); + } + } + + // ============ READ ============ + + // Read all users with posts and comments + const allUsers = await client.user.findMany({ + include: { + posts: { + include: { + comments: true, + }, + }, + comments: true, + }, + }); + expect(allUsers).toHaveLength(usersCount); + + // Read all posts with filtering + await client.post.findMany({ + where: { + published: true, + }, + include: { + author: true, + comments: true, + }, + }); + + // Read individual comments + await client.comment.findMany({ + include: { + post: true, + author: true, + }, + }); + + // Aggregate operations + const userCount = await client.user.count(); + const postCount = await client.post.count(); + const commentCount = await client.comment.count(); + + expect(userCount).toBeGreaterThanOrEqual(usersCount); + expect(postCount).toBeGreaterThanOrEqual(totalPosts); + expect(commentCount).toBeGreaterThanOrEqual(totalComments); + + // ============ UPDATE ============ + + // Update random posts + const postsToUpdate = posts.slice(0, 5); + for (const post of postsToUpdate) { + await client.post.update({ + where: { id: post.id }, + data: { + title: `Updated - ${randomString(20)}`, + content: randomContent(), + }, + }); + } + + // Update random users + const userToUpdate = users[0]!; + await client.user.update({ + where: { id: userToUpdate.id }, + data: { + name: `Updated User - ${randomString(10)}`, + }, + }); + + // Update many comments + await client.comment.updateMany({ + where: { + postId: posts[0]!.id, + }, + data: { + content: `Bulk updated - ${randomString(50)}`, + }, + }); + + // ============ DELETE (Cleanup) ============ + + // Delete all comments first (due to foreign key constraints) + await client.comment.deleteMany({}); + + // Delete all posts + await client.post.deleteMany({}); + + // Delete all users + await client.user.deleteMany({}); + + // Verify cleanup + const remainingUsers = await client.user.count(); + const remainingPosts = await client.post.count(); + const remainingComments = await client.comment.count(); + + expect(remainingUsers).toBe(0); + expect(remainingPosts).toBe(0); + expect(remainingComments).toBe(0); + + // ============ MEMORY SNAPSHOT ============ + + // Force garbage collection if available (run tests with --expose-gc flag) + if (global.gc) { + global.gc(); + } + + const memUsage = process.memoryUsage(); + memorySnapshots.push({ + iteration: i + 1, + rss: memUsage.rss, + heapTotal: memUsage.heapTotal, + heapUsed: memUsage.heapUsed, + external: memUsage.external, + }); + + // Log progress every 10 iterations + if ((i + 1) % 10 === 0) { + console.log(`Completed ${i + 1}/${iterations} iterations`); + console.log( + ` Memory: ${(memUsage.heapUsed / 1024 / 1024).toFixed(2)} MB heap used, ${(memUsage.rss / 1024 / 1024).toFixed(2)} MB RSS`, + ); + } + } + + // ============ MEMORY ANALYSIS ============ + + console.log('\n=== Memory Usage Summary ===\n'); + + const firstSnapshot = memorySnapshots[0]!; + const lastSnapshot = memorySnapshots[memorySnapshots.length - 1]!; + const maxHeapUsed = Math.max(...memorySnapshots.map((s) => s.heapUsed)); + const minHeapUsed = Math.min(...memorySnapshots.map((s) => s.heapUsed)); + const avgHeapUsed = memorySnapshots.reduce((sum, s) => sum + s.heapUsed, 0) / memorySnapshots.length; + + const formatMB = (bytes: number) => (bytes / 1024 / 1024).toFixed(2); + + console.log('Heap Used:'); + console.log(` Initial: ${formatMB(firstSnapshot.heapUsed)} MB`); + console.log(` Final: ${formatMB(lastSnapshot.heapUsed)} MB`); + console.log(` Min: ${formatMB(minHeapUsed)} MB`); + console.log(` Max: ${formatMB(maxHeapUsed)} MB`); + console.log(` Average: ${formatMB(avgHeapUsed)} MB`); + console.log( + ` Growth: ${formatMB(lastSnapshot.heapUsed - firstSnapshot.heapUsed)} MB (${(((lastSnapshot.heapUsed - firstSnapshot.heapUsed) / firstSnapshot.heapUsed) * 100).toFixed(2)}%)`, + ); + + console.log('\nRSS (Resident Set Size):'); + console.log(` Initial: ${formatMB(firstSnapshot.rss)} MB`); + console.log(` Final: ${formatMB(lastSnapshot.rss)} MB`); + console.log( + ` Growth: ${formatMB(lastSnapshot.rss - firstSnapshot.rss)} MB (${(((lastSnapshot.rss - firstSnapshot.rss) / firstSnapshot.rss) * 100).toFixed(2)}%)`, + ); + + console.log('\nHeap Total:'); + console.log(` Initial: ${formatMB(firstSnapshot.heapTotal)} MB`); + console.log(` Final: ${formatMB(lastSnapshot.heapTotal)} MB`); + + console.log('\n=== Test Summary ==='); + console.log(`Total iterations: ${iterations}`); + console.log(`Operations per iteration:`); + console.log(` - Created: ${usersCount} users, ${totalPosts} posts, ${totalComments} comments`); + console.log(` - Read: Multiple queries with includes and filters`); + console.log(` - Updated: 5 posts, 1 user, bulk comment updates`); + console.log(` - Deleted: All data (cleanup)`); + const opsPerIteration = usersCount + totalPosts + totalComments + 10; // approximate CRUD ops + console.log(`Total operations: ~${iterations * opsPerIteration}`); + + // Check for significant memory leaks (> 50% growth is concerning) + const heapGrowthPercent = ((lastSnapshot.heapUsed - firstSnapshot.heapUsed) / firstSnapshot.heapUsed) * 100; + if (heapGrowthPercent > 50) { + console.log( + `\n⚠️ Warning: Heap usage grew by ${heapGrowthPercent.toFixed(2)}% which may indicate a memory leak`, + ); + } else { + console.log(`\n✓ Memory usage appears stable (${heapGrowthPercent.toFixed(2)}% growth)`); + } + + console.log('\n'); + + // Store snapshots for potential further analysis + expect(memorySnapshots).toHaveLength(iterations); + }, 120000); // 2 minute timeout for the test +}); From 8fdd3498f724613a93ab4a8327866602b1f2803d Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Mon, 26 Jan 2026 09:11:07 +0800 Subject: [PATCH 10/25] fix(better-auth): support custom table enum field types and defaults (#621) * fix(better-auth): support custom table enum field types and defaults fixes #592 * stricter enum type check * update better-auth packages --- .../auth-adapters/better-auth/package.json | 11 +- .../better-auth/src/schema-generator.ts | 25 + .../better-auth/test/auth-custom.ts | 33 + .../auth-adapters/better-auth/test/auth.ts | 8 + .../better-auth/test/cli-generate.test.ts | 65 ++ pnpm-lock.yaml | 1031 ++++++++++++----- 6 files changed, 893 insertions(+), 280 deletions(-) create mode 100644 packages/auth-adapters/better-auth/test/auth-custom.ts create mode 100644 packages/auth-adapters/better-auth/test/auth.ts create mode 100644 packages/auth-adapters/better-auth/test/cli-generate.test.ts diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index 9239f3b86..8fc58ae84 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -7,6 +7,7 @@ "build": "tsc --noEmit && tsup-node", "watch": "tsup-node --watch", "lint": "eslint src --ext ts", + "test": "vitest run", "pack": "pnpm pack" }, "keywords": [ @@ -45,10 +46,14 @@ "better-auth": "^1.3.0" }, "devDependencies": { - "@better-auth/core": "^1.3.0", - "better-auth": "^1.3.0", + "@better-auth/core": "1.4.17", + "better-auth": "1.4.17", + "@better-auth/cli": "1.4.17", + "@types/tmp": "catalog:", + "@zenstackhq/cli": "workspace:*", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", - "@zenstackhq/vitest-config": "workspace:*" + "@zenstackhq/vitest-config": "workspace:*", + "tmp": "catalog:" } } diff --git a/packages/auth-adapters/better-auth/src/schema-generator.ts b/packages/auth-adapters/better-auth/src/schema-generator.ts index ea7ac4e00..1c31b4dd2 100644 --- a/packages/auth-adapters/better-auth/src/schema-generator.ts +++ b/packages/auth-adapters/better-auth/src/schema-generator.ts @@ -16,6 +16,7 @@ import { InvocationExpr, isDataModel, Model, + NumberLiteral, ReferenceExpr, StringLiteral, } from '@zenstackhq/language/ast'; @@ -259,6 +260,13 @@ function getMappedFieldType({ bigint, type }: DBFieldAttribute) { .with('json', () => ({ type: 'Json' })) .with('string[]', () => ({ type: 'String', array: true })) .with('number[]', () => ({ type: 'Int', array: true })) + .when( + (v) => Array.isArray(v) && v.every((e) => typeof e === 'string'), + () => { + // Handle enum types (e.g., ['user', 'admin']), map them to String type for now + return { type: 'String' }; + }, + ) .otherwise(() => { throw new Error(`Unsupported field type: ${type}`); }); @@ -332,6 +340,10 @@ function addOrUpdateModel( addDefaultNow(df); } else if (typeof field.defaultValue === 'boolean') { addFieldAttribute(df, '@default', [createBooleanAttributeArg(field.defaultValue)]); + } else if (typeof field.defaultValue === 'string') { + addFieldAttribute(df, '@default', [createStringAttributeArg(field.defaultValue)]); + } else if (typeof field.defaultValue === 'number') { + addFieldAttribute(df, '@default', [createNumberAttributeArg(field.defaultValue)]); } else if (typeof field.defaultValue === 'function') { // For other function-based defaults, we'll need to check what they return const defaultVal = field.defaultValue(); @@ -537,6 +549,19 @@ function createBooleanAttributeArg(value: boolean) { return arg; } +function createNumberAttributeArg(value: number) { + const arg: AttributeArg = { + $type: 'AttributeArg', + } as any; + const expr: NumberLiteral = { + $type: 'NumberLiteral', + value: value.toString(), + $container: arg, + }; + arg.value = expr; + return arg; +} + function createStringAttributeArg(value: string) { const arg: AttributeArg = { $type: 'AttributeArg', diff --git a/packages/auth-adapters/better-auth/test/auth-custom.ts b/packages/auth-adapters/better-auth/test/auth-custom.ts new file mode 100644 index 000000000..df3c7f24b --- /dev/null +++ b/packages/auth-adapters/better-auth/test/auth-custom.ts @@ -0,0 +1,33 @@ +import { zenstackAdapter } from '../src/adapter'; +import { betterAuth } from 'better-auth'; + +export const auth = betterAuth({ + database: zenstackAdapter({} as any, { + provider: 'postgresql', + }), + user: { + additionalFields: { + role: { + type: ['user', 'admin'], + required: false, + defaultValue: 'user', + input: false, // don't allow user to set role + }, + lang: { + type: 'string', + required: false, + defaultValue: 'en', + }, + age: { + type: 'number', + required: true, + defaultValue: 18, + }, + admin: { + type: 'boolean', + required: false, + defaultValue: false, + }, + }, + }, +}); diff --git a/packages/auth-adapters/better-auth/test/auth.ts b/packages/auth-adapters/better-auth/test/auth.ts new file mode 100644 index 000000000..856d826c1 --- /dev/null +++ b/packages/auth-adapters/better-auth/test/auth.ts @@ -0,0 +1,8 @@ +import { zenstackAdapter } from '../src/adapter'; +import { betterAuth } from 'better-auth'; + +export const auth = betterAuth({ + database: zenstackAdapter({} as any, { + provider: 'postgresql', + }), +}); diff --git a/packages/auth-adapters/better-auth/test/cli-generate.test.ts b/packages/auth-adapters/better-auth/test/cli-generate.test.ts new file mode 100644 index 000000000..527710da9 --- /dev/null +++ b/packages/auth-adapters/better-auth/test/cli-generate.test.ts @@ -0,0 +1,65 @@ +import { execSync } from 'node:child_process'; +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { fileURLToPath } from 'node:url'; +import tmp from 'tmp'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +/** + * Helper function to generate schema using better-auth CLI + */ +function generateSchema(configFile: string): string { + const { name: workDir } = tmp.dirSync({ unsafeCleanup: true }); + const schemaPath = path.join(workDir, 'schema.zmodel'); + const configPath = path.join(__dirname, configFile); + + execSync(`pnpm better-auth generate --config ${configPath} --output ${schemaPath} --yes`, { + cwd: __dirname, + stdio: 'pipe', + }); + + return schemaPath; +} + +/** + * Helper function to verify schema with zenstack check + */ +function verifySchema(schemaPath: string) { + const cliPath = path.join(__dirname, '../../../cli/dist/index.js'); + const workDir = path.dirname(schemaPath); + + expect(fs.existsSync(schemaPath)).toBe(true); + + expect(() => { + execSync(`node ${cliPath} check --schema ${schemaPath}`, { + cwd: workDir, + stdio: 'pipe', + }); + }).not.toThrow(); +} + +describe('Cli schema generation tests', () => { + it('works with simple config', async () => { + const schemaPath = generateSchema('auth.ts'); + verifySchema(schemaPath); + }); + + it('works with custom config', async () => { + const schemaPath = generateSchema('auth-custom.ts'); + verifySchema(schemaPath); + + // Verify that the generated schema contains the expected default values + const schemaContent = fs.readFileSync(schemaPath, 'utf-8'); + expect(schemaContent).toMatch(/role\s+String/); + expect(schemaContent).toContain("@default('user')"); + expect(schemaContent).toMatch(/lang\s+String/); + expect(schemaContent).toContain("@default('en')"); + expect(schemaContent).toMatch(/age\s+Int/); + expect(schemaContent).toContain('@default(18)'); + expect(schemaContent).toMatch(/admin\s+Boolean/); + expect(schemaContent).toContain('@default(false)'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41ae6dcc4..c202a4423 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -162,9 +162,18 @@ importers: specifier: 'catalog:' version: 5.7.1 devDependencies: + '@better-auth/cli': + specifier: 1.4.17 + version: 1.4.17(@better-fetch/fetch@1.1.21)(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-call@1.1.8(zod@4.3.6))(bun-types@1.3.3)(jose@6.1.2)(kysely@0.28.8)(magicast@0.5.1)(mysql2@3.16.1)(nanostores@1.0.1)(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sql.js@1.13.0)(svelte@5.46.1)(vitest@4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) '@better-auth/core': - specifier: ^1.3.0 - version: 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + specifier: 1.4.17 + version: 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@types/tmp': + specifier: 'catalog:' + version: 0.2.6 + '@zenstackhq/cli': + specifier: workspace:* + version: link:../../cli '@zenstackhq/eslint-config': specifier: workspace:* version: link:../../config/eslint-config @@ -175,8 +184,11 @@ importers: specifier: workspace:* version: link:../../config/vitest-config better-auth: - specifier: ^1.3.0 - version: 1.3.34 + specifier: 1.4.17 + version: 1.4.17(16db68be254775c88fdefd75497fac00) + tmp: + specifier: 'catalog:' + version: 0.2.3 packages/cli: dependencies: @@ -730,7 +742,7 @@ importers: version: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) nuxt: specifier: 'catalog:' - version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + version: 4.2.2(6ed21839bfb37f6518a04a21db8dd1a5) supertest: specifier: ^7.1.4 version: 7.1.4 @@ -913,7 +925,7 @@ importers: version: 2.0.8 nuxt: specifier: 'catalog:' - version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + version: 4.2.2(6ed21839bfb37f6518a04a21db8dd1a5) tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -1009,7 +1021,7 @@ importers: version: link:../../packages/cli prettier-plugin-tailwindcss: specifier: ^0.7.2 - version: 0.7.2(prettier@3.5.3) + version: 0.7.2(prettier@3.8.1) svelte: specifier: 'catalog:' version: 5.45.6 @@ -1211,6 +1223,10 @@ packages: resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} engines: {node: '>=6.9.0'} + '@babel/code-frame@7.28.6': + resolution: {integrity: sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==} + engines: {node: '>=6.9.0'} + '@babel/compat-data@7.28.5': resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==} engines: {node: '>=6.9.0'} @@ -1223,6 +1239,10 @@ packages: resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} + '@babel/generator@7.28.6': + resolution: {integrity: sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==} + engines: {node: '>=6.9.0'} + '@babel/helper-annotate-as-pure@7.27.3': resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} engines: {node: '>=6.9.0'} @@ -1249,12 +1269,22 @@ packages: resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.28.3': resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + '@babel/helper-optimise-call-expression@7.27.1': resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} engines: {node: '>=6.9.0'} @@ -1263,6 +1293,10 @@ packages: resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} engines: {node: '>=6.9.0'} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} + engines: {node: '>=6.9.0'} + '@babel/helper-replace-supers@7.27.1': resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} engines: {node: '>=6.9.0'} @@ -1294,24 +1328,77 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + '@babel/parser@7.28.6': + resolution: {integrity: sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==} + engines: {node: '>=6.0.0'} + hasBin: true + '@babel/plugin-syntax-jsx@7.27.1': resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-syntax-typescript@7.27.1': resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-modules-commonjs@7.28.6': + resolution: {integrity: sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-display-name@7.28.0': + resolution: {integrity: sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-development@7.27.1': + resolution: {integrity: sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx@7.28.6': + resolution: {integrity: sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-pure-annotations@7.27.1': + resolution: {integrity: sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-typescript@7.28.5': resolution: {integrity: sha512-x2Qa+v/CuEoX7Dr31iAfr0IhInrVOWZU/2vJMJ00FOR/2nM0BcBEclpaf9sWCDc+v5e9dMrhSH8/atq/kX7+bA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 + '@babel/preset-react@7.28.5': + resolution: {integrity: sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.28.5': + resolution: {integrity: sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -1320,36 +1407,54 @@ packages: resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} engines: {node: '>=6.9.0'} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.5': resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==} engines: {node: '>=6.9.0'} + '@babel/traverse@7.28.6': + resolution: {integrity: sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==} + engines: {node: '>=6.9.0'} + '@babel/types@7.28.5': resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} engines: {node: '>=6.9.0'} + '@babel/types@7.28.6': + resolution: {integrity: sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==} + engines: {node: '>=6.9.0'} + '@bcoe/v8-coverage@1.0.2': resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} engines: {node: '>=18'} - '@better-auth/core@1.3.34': - resolution: {integrity: sha512-rt/Bgl0Xa8OQ2DUMKCZEJ8vL9kUw4NCJsBP9Sj9uRhbsK8NEMPiznUOFMkUY2FvrslvfKN7H/fivwyHz9c7HzQ==} + '@better-auth/cli@1.4.17': + resolution: {integrity: sha512-GT0epRZCRAxwDnDNVsAVXx2W4PNjBfm8NwrtjxfYKRHsnrCHkT0N/cZyKCYG1sReN/wlsyYk8bJ6f1N1P4VT8w==} + hasBin: true + + '@better-auth/core@1.4.17': + resolution: {integrity: sha512-WSaEQDdUO6B1CzAmissN6j0lx9fM9lcslEYzlApB5UzFaBeAOHNUONTdglSyUs6/idiZBoRvt0t/qMXCgIU8ug==} peerDependencies: '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - better-call: 1.0.19 + '@better-fetch/fetch': 1.1.21 + better-call: 1.1.8 jose: ^6.1.0 kysely: ^0.28.5 nanostores: ^1.0.1 - '@better-auth/telemetry@1.3.34': - resolution: {integrity: sha512-aQZ3wN90YMqV49diWxAMe1k7s2qb55KCsedCZne5PlgCjU4s3YtnqyjC5FEpzw2KY8l8rvR7DMAsDl13NjObKA==} + '@better-auth/telemetry@1.4.17': + resolution: {integrity: sha512-R1BC4e/bNjQbXu7lG6ubpgmsPj7IMqky5DvMlzAtnAJWJhh99pMh/n6w5gOHa0cqDZgEAuj75IPTxv+q3YiInA==} + peerDependencies: + '@better-auth/core': 1.4.17 '@better-auth/utils@0.3.0': resolution: {integrity: sha512-W+Adw6ZA6mgvnSnhOki270rwJ42t4XzSK6YWGF//BbVXL6SwCLWfyzBc1lN2m/4RM28KubdBKQ4X5VMoLRNPQw==} - '@better-fetch/fetch@1.1.18': - resolution: {integrity: sha512-rEFOE1MYIsBmoMJtQbl32PGHHXuG2hDxvEd7rUHE0vCBoFQVSDqaVs9hkZEtHCxRoY+CljXKFCOuJ8uxqw1LcA==} + '@better-fetch/fetch@1.1.21': + resolution: {integrity: sha512-/ImESw0sskqlVR94jB+5+Pxjf+xBwDZF/N5+y2/q4EqD7IARUTSpPfIo8uf39SYpCxyOCtbyYpUrZ3F/k0zT4A==} '@bomb.sh/tab@0.0.10': resolution: {integrity: sha512-6ALS2rh/4LKn0Yxwm35V6LcgQuSiECHbqQo7+9g4rkgGyXZ0siOc8K+IuWIq/4u0Zkv2mevP9QSqgKhGIvLJMw==} @@ -1369,24 +1474,42 @@ packages: '@borewit/text-codec@0.1.1': resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + '@chevrotain/cst-dts-gen@10.5.0': + resolution: {integrity: sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==} + '@chevrotain/cst-dts-gen@11.0.3': resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + '@chevrotain/gast@10.5.0': + resolution: {integrity: sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==} + '@chevrotain/gast@11.0.3': resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} '@chevrotain/regexp-to-ast@11.0.3': resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + '@chevrotain/types@10.5.0': + resolution: {integrity: sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==} + '@chevrotain/types@11.0.3': resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + '@chevrotain/utils@10.5.0': + resolution: {integrity: sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==} + '@chevrotain/utils@11.0.3': resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + '@clack/core@0.5.0': + resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} + '@clack/core@1.0.0-alpha.7': resolution: {integrity: sha512-3vdh6Ar09D14rVxJZIm3VQJkU+ZOKKT5I5cC0cOVazy70CNyYYjiwRj9unwalhESndgxx6bGc/m6Hhs4EKF5XQ==} + '@clack/prompts@0.11.0': + resolution: {integrity: sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==} + '@clack/prompts@1.0.0-alpha.8': resolution: {integrity: sha512-YZGC4BmTKSF5OturNKEz/y4xNjYGmGk6NI785CQucJ7OEdX0qbMmL/zok+9bL6c7qE3WSYffyK5grh2RnkGNtQ==} @@ -1974,9 +2097,6 @@ packages: '@fastify/proxy-addr@5.1.0': resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} - '@hexagon/base64@1.1.28': - resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==} - '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -2181,14 +2301,15 @@ packages: '@kwsites/promise-deferred@1.1.1': resolution: {integrity: sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==} - '@levischuck/tiny-cbor@0.2.11': - resolution: {integrity: sha512-llBRm4dT4Z89aRsm6u2oEZ8tfwL/2l6BwpZ7JcyieouniDECM5AqNgr/y08zalEIvW3RSK4upYyybDcmjXqAow==} - '@mapbox/node-pre-gyp@2.0.0': resolution: {integrity: sha512-llMXd39jtP0HpQLVI37Bf1m2ADlEb35GYSh1SDSLsBhR+5iCxiNGlT31yqbNtVHygHAtMy6dWFERpU2JgufhPg==} engines: {node: '>=18'} hasBin: true + '@mrleebo/prisma-ast@0.13.1': + resolution: {integrity: sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==} + engines: {node: '>=16'} + '@mswjs/interceptors@0.39.8': resolution: {integrity: sha512-2+BzZbjRO7Ct61k8fMNHEtoKjeWI9pIlHFTqBwZ5icHpqszIgEZbjb1MW5Z0+bITTCTl3gk4PDBxs9tA/csXvA==} engines: {node: '>=18'} @@ -2712,43 +2833,6 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} - '@peculiar/asn1-android@2.6.0': - resolution: {integrity: sha512-cBRCKtYPF7vJGN76/yG8VbxRcHLPF3HnkoHhKOZeHpoVtbMYfY9ROKtH3DtYUY9m8uI1Mh47PRhHf2hSK3xcSQ==} - - '@peculiar/asn1-cms@2.6.0': - resolution: {integrity: sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==} - - '@peculiar/asn1-csr@2.6.0': - resolution: {integrity: sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==} - - '@peculiar/asn1-ecc@2.6.0': - resolution: {integrity: sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==} - - '@peculiar/asn1-pfx@2.6.0': - resolution: {integrity: sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==} - - '@peculiar/asn1-pkcs8@2.6.0': - resolution: {integrity: sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==} - - '@peculiar/asn1-pkcs9@2.6.0': - resolution: {integrity: sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==} - - '@peculiar/asn1-rsa@2.6.0': - resolution: {integrity: sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==} - - '@peculiar/asn1-schema@2.6.0': - resolution: {integrity: sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==} - - '@peculiar/asn1-x509-attr@2.6.0': - resolution: {integrity: sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==} - - '@peculiar/asn1-x509@2.6.0': - resolution: {integrity: sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==} - - '@peculiar/x509@1.14.2': - resolution: {integrity: sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==} - engines: {node: '>=22.0.0'} - '@pinojs/redact@0.4.0': resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} @@ -2768,6 +2852,15 @@ packages: '@poppinss/exception@1.2.2': resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==} + '@prisma/client@5.22.0': + resolution: {integrity: sha512-M0SVXfyHnQREBKxCgyo7sffrKttwE6R8PMq330MIUF0pTwjUhLbW84pFDlf06B27XyCR++VtjugEnIHdr07SVA==} + engines: {node: '>=16.13'} + peerDependencies: + prisma: '*' + peerDependenciesMeta: + prisma: + optional: true + '@prisma/config@6.19.0': resolution: {integrity: sha512-zwCayme+NzI/WfrvFEtkFhhOaZb/hI+X8TTjzjJ252VbPxAl2hWHK5NMczmnG9sXck2lsXrxIZuK524E25UNmg==} @@ -3080,13 +3173,6 @@ packages: '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} - '@simplewebauthn/browser@13.2.2': - resolution: {integrity: sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==} - - '@simplewebauthn/server@13.2.2': - resolution: {integrity: sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==} - engines: {node: '>=20.0.0'} - '@sinclair/typebox@0.34.41': resolution: {integrity: sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==} @@ -4132,10 +4218,6 @@ packages: asap@2.0.6: resolution: {integrity: sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==} - asn1js@3.0.6: - resolution: {integrity: sha512-UOCGPYbl0tv8+006qks/dTgV9ajs97X2p0FAbyS2iyCRrmLSRolDaHdp+v/CLgnzHc3fVB+CwYiUmei7ndFcgA==} - engines: {node: '>=12.0.0'} - ast-kit@2.1.3: resolution: {integrity: sha512-TH+b3Lv6pUjy/Nu0m6A2JULtdzLpmqF9x1Dhj00ZoEiML8qvVA9j1flkzTKNYgdEhWrjDwtWNpyyCUbfQe514g==} engines: {node: '>=20.19.0'} @@ -4223,24 +4305,55 @@ packages: resolution: {integrity: sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==} hasBin: true - better-auth@1.3.34: - resolution: {integrity: sha512-LWA52SlvnUBJRbN8VLSTLILPomZY3zZAiLxVJCeSQ5uVmaIKkMBhERitkfJcXB9RJcfl4uP+3EqKkb6hX1/uiw==} + better-auth@1.4.17: + resolution: {integrity: sha512-VmHGQyKsEahkEs37qguROKg/6ypYpNF13D7v/lkbO7w7Aivz0Bv2h+VyUkH4NzrGY0QBKXi1577mGhDCVwp0ew==} peerDependencies: '@lynx-js/react': '*' - '@sveltejs/kit': '*' - next: '*' - react: '*' - react-dom: '*' - solid-js: '*' - svelte: '*' - vue: '*' + '@prisma/client': ^5.0.0 || ^6.0.0 || ^7.0.0 + '@sveltejs/kit': ^2.0.0 + '@tanstack/react-start': ^1.0.0 + '@tanstack/solid-start': ^1.0.0 + better-sqlite3: ^12.0.0 + drizzle-kit: '>=0.31.4' + drizzle-orm: '>=0.41.0' + mongodb: ^6.0.0 || ^7.0.0 + mysql2: ^3.0.0 + next: ^14.0.0 || ^15.0.0 || ^16.0.0 + pg: ^8.0.0 + prisma: ^5.0.0 || ^6.0.0 || ^7.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + solid-js: ^1.0.0 + svelte: ^4.0.0 || ^5.0.0 + vitest: ^2.0.0 || ^3.0.0 || ^4.0.0 + vue: ^3.0.0 peerDependenciesMeta: '@lynx-js/react': optional: true + '@prisma/client': + optional: true '@sveltejs/kit': optional: true + '@tanstack/react-start': + optional: true + '@tanstack/solid-start': + optional: true + better-sqlite3: + optional: true + drizzle-kit: + optional: true + drizzle-orm: + optional: true + mongodb: + optional: true + mysql2: + optional: true next: optional: true + pg: + optional: true + prisma: + optional: true react: optional: true react-dom: @@ -4249,11 +4362,18 @@ packages: optional: true svelte: optional: true + vitest: + optional: true vue: optional: true - better-call@1.0.19: - resolution: {integrity: sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==} + better-call@1.1.8: + resolution: {integrity: sha512-XMQ2rs6FNXasGNfMjzbyroSwKwYbZ/T3IxruSS6U2MJRsSYh3wYtG3o6H00ZlKZ/C/UPOAD97tqgQJNsxyeTXw==} + peerDependencies: + zod: ^4.0.0 + peerDependenciesMeta: + zod: + optional: true better-sqlite3@12.5.0: resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==} @@ -4389,11 +4509,18 @@ packages: resolution: {integrity: sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chalk@5.6.2: + resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} + engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} + chevrotain-allstar@0.3.1: resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} peerDependencies: chevrotain: ^11.0.0 + chevrotain@10.5.0: + resolution: {integrity: sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==} + chevrotain@11.0.3: resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} @@ -4478,6 +4605,10 @@ packages: resolution: {integrity: sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==} engines: {node: '>=16'} + commander@12.1.0: + resolution: {integrity: sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==} + engines: {node: '>=18'} + commander@2.20.3: resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} @@ -4848,6 +4979,95 @@ packages: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} + drizzle-orm@0.41.0: + resolution: {integrity: sha512-7A4ZxhHk9gdlXmTdPj/lREtP+3u8KvZ4yEN6MYVxBzZGex5Wtdc+CWSbu7btgF6TB0N+MNPrvW7RKBbxJchs/Q==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=4' + '@electric-sql/pglite': '>=0.2.0' + '@libsql/client': '>=0.10.0' + '@libsql/client-wasm': '>=0.10.0' + '@neondatabase/serverless': '>=0.10.0' + '@op-engineering/op-sqlite': '>=2' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@prisma/client': '*' + '@tidbcloud/serverless': '*' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/sql.js': '*' + '@vercel/postgres': '>=0.8.0' + '@xata.io/client': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=14.0.0' + gel: '>=2' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + prisma: '*' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@electric-sql/pglite': + optional: true + '@libsql/client': + optional: true + '@libsql/client-wasm': + optional: true + '@neondatabase/serverless': + optional: true + '@op-engineering/op-sqlite': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@prisma/client': + optional: true + '@tidbcloud/serverless': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + '@xata.io/client': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + gel: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + prisma: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} @@ -6055,6 +6275,10 @@ packages: resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} engines: {node: '>= 12.0.0'} + lilconfig@2.1.0: + resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} + engines: {node: '>=10'} + lilconfig@3.1.3: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} @@ -7069,6 +7293,11 @@ packages: engines: {node: '>=14'} hasBin: true + prettier@3.8.1: + resolution: {integrity: sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@7.1.0: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} @@ -7132,13 +7361,6 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - pvtsutils@1.3.6: - resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} - - pvutils@1.1.5: - resolution: {integrity: sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==} - engines: {node: '>=16.0.0'} - qs@6.13.0: resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} engines: {node: '>=0.6'} @@ -7233,13 +7455,13 @@ packages: resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} engines: {node: '>=4'} - reflect-metadata@0.2.2: - resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} - reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} + regexp-to-ast@0.5.0: + resolution: {integrity: sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==} + regexp-tree@0.1.27: resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==} hasBin: true @@ -7317,8 +7539,8 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true - rou3@0.5.1: - resolution: {integrity: sha512-OXMmJ3zRk2xeXFGfA3K+EOPHC5u7RDFG7lIOx0X1pdnhUkI8MdVrbV+sNsD80ElpUZ+MRHdyxPnFthq9VHs8uQ==} + rou3@0.7.12: + resolution: {integrity: sha512-iFE4hLDuloSWcD7mjdCDhx2bKcIsYbtOTpfH5MHHLSKMOUyjqQXTeZVa289uuwEGEKFoE/BAPbhaU4B774nceg==} router@2.2.0: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} @@ -7900,9 +8122,6 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} - tslib@1.14.1: - resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -7930,10 +8149,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - tsyringe@4.10.0: - resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} - engines: {node: '>= 6.0.0'} - tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -8574,6 +8789,10 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yocto-spinner@0.2.3: + resolution: {integrity: sha512-sqBChb33loEnkoXte1bLg45bEBsOP9N1kzQh5JZNKj/0rik4zAPTNSAVPj3uQAdc6slYJ0Ksc403G2XgxsJQFQ==} + engines: {node: '>=18.19'} + yoctocolors@2.1.2: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} @@ -8600,6 +8819,9 @@ packages: zod@4.1.12: resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@acemir/cssom@0.9.23': @@ -8634,6 +8856,12 @@ snapshots: js-tokens: 4.0.0 picocolors: 1.1.1 + '@babel/code-frame@7.28.6': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + '@babel/compat-data@7.28.5': {} '@babel/core@7.28.5': @@ -8664,6 +8892,14 @@ snapshots: '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.1.0 + '@babel/generator@7.28.6': + dependencies: + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + '@babel/helper-annotate-as-pure@7.27.3': dependencies: '@babel/types': 7.28.5 @@ -8705,6 +8941,13 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.28.6 + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -8714,12 +8957,23 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/helper-module-transforms@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.28.6 + transitivePeerDependencies: + - supports-color + '@babel/helper-optimise-call-expression@7.27.1': dependencies: '@babel/types': 7.28.5 '@babel/helper-plugin-utils@7.27.1': {} + '@babel/helper-plugin-utils@7.28.6': {} + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -8751,16 +9005,62 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@babel/parser@7.28.6': + dependencies: + '@babel/types': 7.28.6 + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-transform-modules-commonjs@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.28.5) + '@babel/helper-plugin-utils': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-display-name@7.28.0(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + + '@babel/plugin-transform-react-jsx-development@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-jsx@7.28.6(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.5) + '@babel/types': 7.28.6 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-react-pure-annotations@7.27.1(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/plugin-transform-typescript@7.28.5(@babel/core@7.28.5)': dependencies: '@babel/core': 7.28.5 @@ -8772,6 +9072,29 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/preset-react@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-transform-react-display-name': 7.28.0(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.28.5) + '@babel/plugin-transform-react-jsx-development': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-react-pure-annotations': 7.27.1(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.28.5(@babel/core@7.28.5)': + dependencies: + '@babel/core': 7.28.5 + '@babel/helper-plugin-utils': 7.28.6 + '@babel/helper-validator-option': 7.27.1 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.28.5) + '@babel/plugin-transform-modules-commonjs': 7.28.6(@babel/core@7.28.5) + '@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5) + transitivePeerDependencies: + - supports-color + '@babel/runtime@7.28.4': {} '@babel/template@7.27.2': @@ -8780,6 +9103,12 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 + '@babel/traverse@7.28.5': dependencies: '@babel/code-frame': 7.27.1 @@ -8792,37 +9121,122 @@ snapshots: transitivePeerDependencies: - supports-color + '@babel/traverse@7.28.6': + dependencies: + '@babel/code-frame': 7.28.6 + '@babel/generator': 7.28.6 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.6 + '@babel/template': 7.28.6 + '@babel/types': 7.28.6 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + '@babel/types@7.28.5': dependencies: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@babel/types@7.28.6': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + '@bcoe/v8-coverage@1.0.2': {} - '@better-auth/core@1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)': + '@better-auth/cli@1.4.17(@better-fetch/fetch@1.1.21)(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(@types/better-sqlite3@7.6.13)(@types/sql.js@1.4.9)(better-call@1.1.8(zod@4.3.6))(bun-types@1.3.3)(jose@6.1.2)(kysely@0.28.8)(magicast@0.5.1)(mysql2@3.16.1)(nanostores@1.0.1)(next@16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sql.js@1.13.0)(svelte@5.46.1)(vitest@4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))': + dependencies: + '@babel/core': 7.28.5 + '@babel/preset-react': 7.28.5(@babel/core@7.28.5) + '@babel/preset-typescript': 7.28.5(@babel/core@7.28.5) + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)) + '@better-auth/utils': 0.3.0 + '@clack/prompts': 0.11.0 + '@mrleebo/prisma-ast': 0.13.1 + '@prisma/client': 5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)) + '@types/pg': 8.16.0 + better-auth: 1.4.17(16db68be254775c88fdefd75497fac00) + better-sqlite3: 12.5.0 + c12: 3.3.3(magicast@0.5.1) + chalk: 5.6.2 + commander: 12.1.0 + dotenv: 17.2.3 + drizzle-orm: 0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0) + open: 10.2.0 + pg: 8.16.3 + prettier: 3.8.1 + prompts: 2.4.2 + semver: 7.7.3 + yocto-spinner: 0.2.3 + zod: 4.3.6 + transitivePeerDependencies: + - '@aws-sdk/client-rds-data' + - '@better-fetch/fetch' + - '@cloudflare/workers-types' + - '@electric-sql/pglite' + - '@libsql/client' + - '@libsql/client-wasm' + - '@lynx-js/react' + - '@neondatabase/serverless' + - '@op-engineering/op-sqlite' + - '@opentelemetry/api' + - '@planetscale/database' + - '@sveltejs/kit' + - '@tanstack/react-start' + - '@tanstack/solid-start' + - '@tidbcloud/serverless' + - '@types/better-sqlite3' + - '@types/sql.js' + - '@vercel/postgres' + - '@xata.io/client' + - better-call + - bun-types + - drizzle-kit + - expo-sqlite + - gel + - jose + - knex + - kysely + - magicast + - mongodb + - mysql2 + - nanostores + - next + - pg-native + - postgres + - prisma + - react + - react-dom + - solid-js + - sql.js + - sqlite3 + - supports-color + - svelte + - vitest + - vue + + '@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)': dependencies: '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - better-call: 1.0.19 + '@better-fetch/fetch': 1.1.21 + '@standard-schema/spec': 1.0.0 + better-call: 1.1.8(zod@4.3.6) jose: 6.1.2 kysely: 0.28.8 nanostores: 1.0.1 - zod: 4.1.12 + zod: 4.3.6 - '@better-auth/telemetry@1.3.34(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)': + '@better-auth/telemetry@1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1))': dependencies: - '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - transitivePeerDependencies: - - better-call - - jose - - kysely - - nanostores + '@better-fetch/fetch': 1.1.21 '@better-auth/utils@0.3.0': {} - '@better-fetch/fetch@1.1.18': {} + '@better-fetch/fetch@1.1.21': {} '@bomb.sh/tab@0.0.10(cac@6.7.14)(citty@0.1.6)': optionalDependencies: @@ -8831,12 +9245,23 @@ snapshots: '@borewit/text-codec@0.1.1': {} + '@chevrotain/cst-dts-gen@10.5.0': + dependencies: + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + '@chevrotain/cst-dts-gen@11.0.3': dependencies: '@chevrotain/gast': 11.0.3 '@chevrotain/types': 11.0.3 lodash-es: 4.17.21 + '@chevrotain/gast@10.5.0': + dependencies: + '@chevrotain/types': 10.5.0 + lodash: 4.17.21 + '@chevrotain/gast@11.0.3': dependencies: '@chevrotain/types': 11.0.3 @@ -8844,15 +9269,30 @@ snapshots: '@chevrotain/regexp-to-ast@11.0.3': {} + '@chevrotain/types@10.5.0': {} + '@chevrotain/types@11.0.3': {} + '@chevrotain/utils@10.5.0': {} + '@chevrotain/utils@11.0.3': {} + '@clack/core@0.5.0': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@clack/core@1.0.0-alpha.7': dependencies: picocolors: 1.1.1 sisteransi: 1.0.5 + '@clack/prompts@0.11.0': + dependencies: + '@clack/core': 0.5.0 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@clack/prompts@1.0.0-alpha.8': dependencies: '@clack/core': 1.0.0-alpha.7 @@ -9233,8 +9673,6 @@ snapshots: '@fastify/forwarded': 3.0.1 ipaddr.js: 2.2.0 - '@hexagon/base64@1.1.28': {} - '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -9405,8 +9843,6 @@ snapshots: '@kwsites/promise-deferred@1.1.1': {} - '@levischuck/tiny-cbor@0.2.11': {} - '@mapbox/node-pre-gyp@2.0.0': dependencies: consola: 3.4.2 @@ -9420,6 +9856,11 @@ snapshots: - encoding - supports-color + '@mrleebo/prisma-ast@0.13.1': + dependencies: + chevrotain: 10.5.0 + lilconfig: 2.1.0 + '@mswjs/interceptors@0.39.8': dependencies: '@open-draft/deferred-promise': 2.2.0 @@ -9641,7 +10082,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/nitro-server@4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3)': + '@nuxt/nitro-server@4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(6ed21839bfb37f6518a04a21db8dd1a5))(typescript@5.9.3)': dependencies: '@nuxt/devalue': 2.0.2 '@nuxt/kit': 4.2.2(magicast@0.5.1) @@ -9658,15 +10099,15 @@ snapshots: impound: 1.0.0 klona: 2.0.6 mocked-exports: 0.1.1 - nitropack: 2.12.9(better-sqlite3@12.5.0)(mysql2@3.16.1) - nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + nitropack: 2.12.9(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1) + nuxt: 4.2.2(6ed21839bfb37f6518a04a21db8dd1a5) pathe: 2.0.3 pkg-types: 2.3.0 radix3: 1.1.2 std-env: 3.10.0 ufo: 1.6.1 unctx: 2.4.1 - unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2) + unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(ioredis@5.8.2) vue: 3.5.26(typescript@5.9.3) vue-bundle-renderer: 2.2.0 vue-devtools-stub: 0.1.0 @@ -9730,7 +10171,7 @@ snapshots: transitivePeerDependencies: - magicast - '@nuxt/vite-builder@4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': + '@nuxt/vite-builder@4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(6ed21839bfb37f6518a04a21db8dd1a5))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': dependencies: '@nuxt/kit': 4.2.2(magicast@0.5.1) '@rollup/plugin-replace': 6.0.3(rollup@4.52.5) @@ -9750,7 +10191,7 @@ snapshots: magic-string: 0.30.21 mlly: 1.8.0 mocked-exports: 0.1.1 - nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2) + nuxt: 4.2.2(6ed21839bfb37f6518a04a21db8dd1a5) pathe: 2.0.3 pkg-types: 2.3.0 postcss: 8.5.6 @@ -10010,102 +10451,6 @@ snapshots: '@parcel/watcher-win32-ia32': 2.5.1 '@parcel/watcher-win32-x64': 2.5.1 - '@peculiar/asn1-android@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-cms@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - '@peculiar/asn1-x509-attr': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-csr@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-ecc@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-pfx@2.6.0': - dependencies: - '@peculiar/asn1-cms': 2.6.0 - '@peculiar/asn1-pkcs8': 2.6.0 - '@peculiar/asn1-rsa': 2.6.0 - '@peculiar/asn1-schema': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-pkcs8@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-pkcs9@2.6.0': - dependencies: - '@peculiar/asn1-cms': 2.6.0 - '@peculiar/asn1-pfx': 2.6.0 - '@peculiar/asn1-pkcs8': 2.6.0 - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - '@peculiar/asn1-x509-attr': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-rsa@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-schema@2.6.0': - dependencies: - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - - '@peculiar/asn1-x509-attr@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - asn1js: 3.0.6 - tslib: 2.8.1 - - '@peculiar/asn1-x509@2.6.0': - dependencies: - '@peculiar/asn1-schema': 2.6.0 - asn1js: 3.0.6 - pvtsutils: 1.3.6 - tslib: 2.8.1 - - '@peculiar/x509@1.14.2': - dependencies: - '@peculiar/asn1-cms': 2.6.0 - '@peculiar/asn1-csr': 2.6.0 - '@peculiar/asn1-ecc': 2.6.0 - '@peculiar/asn1-pkcs9': 2.6.0 - '@peculiar/asn1-rsa': 2.6.0 - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - pvtsutils: 1.3.6 - reflect-metadata: 0.2.2 - tslib: 2.8.1 - tsyringe: 4.10.0 - '@pinojs/redact@0.4.0': {} '@pkgjs/parseargs@0.11.0': @@ -10125,6 +10470,10 @@ snapshots: '@poppinss/exception@1.2.2': {} + '@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))': + optionalDependencies: + prisma: 6.19.0(magicast@0.5.1)(typescript@5.9.3) + '@prisma/config@6.19.0(magicast@0.3.5)': dependencies: c12: 3.1.0(magicast@0.3.5) @@ -10134,6 +10483,16 @@ snapshots: transitivePeerDependencies: - magicast + '@prisma/config@6.19.0(magicast@0.5.1)': + dependencies: + c12: 3.1.0(magicast@0.5.1) + deepmerge-ts: 7.1.5 + effect: 3.18.4 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + optional: true + '@prisma/debug@6.19.0': {} '@prisma/engines-version@6.19.0-26.2ba551f319ab1df4bc874a89965d8b3641056773': {} @@ -10352,19 +10711,6 @@ snapshots: '@sec-ant/readable-stream@0.4.1': {} - '@simplewebauthn/browser@13.2.2': {} - - '@simplewebauthn/server@13.2.2': - dependencies: - '@hexagon/base64': 1.1.28 - '@levischuck/tiny-cbor': 0.2.11 - '@peculiar/asn1-android': 2.6.0 - '@peculiar/asn1-ecc': 2.6.0 - '@peculiar/asn1-rsa': 2.6.0 - '@peculiar/asn1-schema': 2.6.0 - '@peculiar/asn1-x509': 2.6.0 - '@peculiar/x509': 1.14.2 - '@sinclair/typebox@0.34.41': {} '@sindresorhus/is@7.1.0': {} @@ -10421,6 +10767,26 @@ snapshots: svelte: 5.45.6 vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + '@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.6(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.6.1 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.2 + sirv: 3.0.2 + svelte: 5.46.1 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + optional: true + '@sveltejs/package@2.5.7(svelte@5.45.6)(typescript@5.9.3)': dependencies: chokidar: 5.0.0 @@ -11197,6 +11563,15 @@ snapshots: optionalDependencies: vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.0) + '@vitest/mocker@4.0.14(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.14 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + optional: true + '@vitest/pretty-format@4.0.14': dependencies: tinyrainbow: 3.0.3 @@ -11595,12 +11970,6 @@ snapshots: asap@2.0.6: {} - asn1js@3.0.6: - dependencies: - pvtsutils: 1.3.6 - pvutils: 1.1.5 - tslib: 2.8.1 - ast-kit@2.1.3: dependencies: '@babel/parser': 7.28.5 @@ -11665,30 +12034,43 @@ snapshots: baseline-browser-mapping@2.9.11: {} - better-auth@1.3.34: + better-auth@1.4.17(16db68be254775c88fdefd75497fac00): dependencies: - '@better-auth/core': 1.3.34(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.18)(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) - '@better-auth/telemetry': 1.3.34(better-call@1.0.19)(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/core': 1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1) + '@better-auth/telemetry': 1.4.17(@better-auth/core@1.4.17(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.2)(kysely@0.28.8)(nanostores@1.0.1)) '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 + '@better-fetch/fetch': 1.1.21 '@noble/ciphers': 2.0.1 '@noble/hashes': 2.0.1 - '@simplewebauthn/browser': 13.2.2 - '@simplewebauthn/server': 13.2.2 - better-call: 1.0.19 + better-call: 1.1.8(zod@4.3.6) defu: 6.1.4 jose: 6.1.2 kysely: 0.28.8 nanostores: 1.0.1 - zod: 4.1.12 + zod: 4.3.6 + optionalDependencies: + '@prisma/client': 5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)) + '@sveltejs/kit': 2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + better-sqlite3: 12.5.0 + drizzle-orm: 0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0) + mysql2: 3.16.1 + next: 16.0.10(@babel/core@7.28.5)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + pg: 8.16.3 + prisma: 6.19.0(magicast@0.5.1)(typescript@5.9.3) + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + svelte: 5.46.1 + vitest: 4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + vue: 3.5.26(typescript@5.9.3) - better-call@1.0.19: + better-call@1.1.8(zod@4.3.6): dependencies: '@better-auth/utils': 0.3.0 - '@better-fetch/fetch': 1.1.18 - rou3: 0.5.1 + '@better-fetch/fetch': 1.1.21 + rou3: 0.7.12 set-cookie-parser: 2.7.2 - uncrypto: 0.1.3 + optionalDependencies: + zod: 4.3.6 better-sqlite3@12.5.0: dependencies: @@ -11807,6 +12189,24 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@3.1.0(magicast@0.5.1): + dependencies: + chokidar: 4.0.3 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 16.6.1 + exsolve: 1.0.7 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 1.0.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.5.1 + optional: true + c12@3.3.3(magicast@0.5.1): dependencies: chokidar: 5.0.0 @@ -11869,11 +12269,22 @@ snapshots: chalk@5.3.0: {} + chalk@5.6.2: {} + chevrotain-allstar@0.3.1(chevrotain@11.0.3): dependencies: chevrotain: 11.0.3 lodash-es: 4.17.21 + chevrotain@10.5.0: + dependencies: + '@chevrotain/cst-dts-gen': 10.5.0 + '@chevrotain/gast': 10.5.0 + '@chevrotain/types': 10.5.0 + '@chevrotain/utils': 10.5.0 + lodash: 4.17.21 + regexp-to-ast: 0.5.0 + chevrotain@11.0.3: dependencies: '@chevrotain/cst-dts-gen': 11.0.3 @@ -11949,6 +12360,8 @@ snapshots: commander@11.1.0: {} + commander@12.1.0: {} + commander@2.20.3: {} commander@4.1.1: {} @@ -12154,9 +12567,10 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1): + db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1): optionalDependencies: better-sqlite3: 12.5.0 + drizzle-orm: 0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0) mysql2: 3.16.1 debug@3.2.7: @@ -12273,6 +12687,20 @@ snapshots: dotenv@17.2.3: {} + drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0): + optionalDependencies: + '@prisma/client': 5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)) + '@types/better-sqlite3': 7.6.13 + '@types/pg': 8.16.0 + '@types/sql.js': 1.4.9 + better-sqlite3: 12.5.0 + bun-types: 1.3.3 + kysely: 0.28.8 + mysql2: 3.16.1 + pg: 8.16.3 + prisma: 6.19.0(magicast@0.5.1)(typescript@5.9.3) + sql.js: 1.13.0 + dunder-proto@1.0.1: dependencies: call-bind-apply-helpers: 1.0.2 @@ -13761,6 +14189,8 @@ snapshots: lightningcss-win32-arm64-msvc: 1.30.2 lightningcss-win32-x64-msvc: 1.30.2 + lilconfig@2.1.0: {} + lilconfig@3.1.3: {} lines-and-columns@1.2.4: {} @@ -13874,8 +14304,8 @@ snapshots: magicast@0.3.5: dependencies: - '@babel/parser': 7.28.5 - '@babel/types': 7.28.5 + '@babel/parser': 7.28.6 + '@babel/types': 7.28.6 source-map-js: 1.2.1 optional: true @@ -14061,7 +14491,7 @@ snapshots: nice-try@1.0.5: {} - nitropack@2.12.9(better-sqlite3@12.5.0)(mysql2@3.16.1): + nitropack@2.12.9(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1): dependencies: '@cloudflare/kv-asset-handler': 0.4.0 '@rollup/plugin-alias': 5.1.1(rollup@4.52.5) @@ -14082,7 +14512,7 @@ snapshots: cookie-es: 2.0.0 croner: 9.1.0 crossws: 0.3.5 - db0: 0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1) + db0: 0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1) defu: 6.1.4 destr: 2.0.5 dot-prop: 10.1.0 @@ -14128,7 +14558,7 @@ snapshots: unenv: 2.0.0-rc.24 unimport: 5.5.0 unplugin-utils: 0.3.1 - unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2) + unstorage: 1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(ioredis@5.8.2) untyped: 2.0.0 unwasm: 0.3.11 youch: 4.1.0-beta.13 @@ -14227,16 +14657,16 @@ snapshots: dependencies: boolbase: 1.0.0 - nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2): + nuxt@4.2.2(6ed21839bfb37f6518a04a21db8dd1a5): dependencies: '@dxup/nuxt': 0.2.2(magicast@0.5.1) '@nuxt/cli': 3.31.3(cac@6.7.14)(magicast@0.5.1) '@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) '@nuxt/kit': 4.2.2(magicast@0.5.1) - '@nuxt/nitro-server': 4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(typescript@5.9.3) + '@nuxt/nitro-server': 4.2.2(better-sqlite3@12.5.0)(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(ioredis@5.8.2)(magicast@0.5.1)(mysql2@3.16.1)(nuxt@4.2.2(6ed21839bfb37f6518a04a21db8dd1a5))(typescript@5.9.3) '@nuxt/schema': 4.2.2 '@nuxt/telemetry': 2.6.6(magicast@0.5.1) - '@nuxt/vite-builder': 4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@20.19.24)(@vue/compiler-sfc@3.5.26)(better-sqlite3@12.5.0)(cac@6.7.14)(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(eslint@9.29.0(jiti@2.6.1))(ioredis@5.8.2)(lightningcss@1.30.2)(magicast@0.5.1)(mysql2@3.16.1)(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) + '@nuxt/vite-builder': 4.2.2(@types/node@20.19.24)(eslint@9.29.0(jiti@2.6.1))(lightningcss@1.30.2)(magicast@0.5.1)(nuxt@4.2.2(6ed21839bfb37f6518a04a21db8dd1a5))(optionator@0.9.4)(rollup@4.52.5)(terser@5.44.0)(tsx@4.20.3)(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) '@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3)) '@vue/shared': 3.5.26 c12: 3.3.3(magicast@0.5.1) @@ -14927,12 +15357,14 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-tailwindcss@0.7.2(prettier@3.5.3): + prettier-plugin-tailwindcss@0.7.2(prettier@3.8.1): dependencies: - prettier: 3.5.3 + prettier: 3.8.1 prettier@3.5.3: {} + prettier@3.8.1: {} + pretty-bytes@7.1.0: {} pretty-format@27.5.1: @@ -14954,6 +15386,16 @@ snapshots: transitivePeerDependencies: - magicast + prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3): + dependencies: + '@prisma/config': 6.19.0(magicast@0.5.1) + '@prisma/engines': 6.19.0 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - magicast + optional: true + process-nextick-args@2.0.1: {} process-warning@4.0.1: {} @@ -14991,12 +15433,6 @@ snapshots: pure-rand@6.1.0: {} - pvtsutils@1.3.6: - dependencies: - tslib: 2.8.1 - - pvutils@1.1.5: {} - qs@6.13.0: dependencies: side-channel: 1.1.0 @@ -15097,8 +15533,6 @@ snapshots: dependencies: redis-errors: 1.2.0 - reflect-metadata@0.2.2: {} - reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -15110,6 +15544,8 @@ snapshots: get-proto: 1.0.1 which-builtin-type: 1.2.1 + regexp-to-ast@0.5.0: {} + regexp-tree@0.1.27: {} regexp.prototype.flags@1.5.4: @@ -15219,7 +15655,7 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.52.5 fsevents: 2.3.3 - rou3@0.5.1: {} + rou3@0.7.12: {} router@2.2.0: dependencies: @@ -15916,8 +16352,6 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tslib@1.14.1: {} - tslib@2.8.1: {} tsup@8.5.0(@swc/core@1.12.5)(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.3)(typescript@5.9.3)(yaml@2.8.0): @@ -15956,10 +16390,6 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - tsyringe@4.10.0: - dependencies: - tslib: 1.14.1 - tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -16188,7 +16618,7 @@ snapshots: '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 - unstorage@1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1))(ioredis@5.8.2): + unstorage@1.17.3(db0@0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1))(ioredis@5.8.2): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -16199,7 +16629,7 @@ snapshots: ofetch: 1.5.1 ufo: 1.6.1 optionalDependencies: - db0: 0.3.4(better-sqlite3@12.5.0)(mysql2@3.16.1) + db0: 0.3.4(better-sqlite3@12.5.0)(drizzle-orm@0.41.0(@prisma/client@5.22.0(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3)))(@types/better-sqlite3@7.6.13)(@types/pg@8.16.0)(@types/sql.js@1.4.9)(better-sqlite3@12.5.0)(bun-types@1.3.3)(kysely@0.28.8)(mysql2@3.16.1)(pg@8.16.3)(prisma@6.19.0(magicast@0.5.1)(typescript@5.9.3))(sql.js@1.13.0))(mysql2@3.16.1) ioredis: 5.8.2 untun@0.1.3: @@ -16407,6 +16837,47 @@ snapshots: - tsx - yaml + vitest@4.0.14(@edge-runtime/vm@5.0.0)(@types/node@20.19.24)(happy-dom@20.0.10)(jiti@2.6.1)(jsdom@27.1.0)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.14 + '@vitest/mocker': 4.0.14(vite@7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.14 + '@vitest/runner': 4.0.14 + '@vitest/snapshot': 4.0.14 + '@vitest/spy': 4.0.14 + '@vitest/utils': 4.0.14 + es-module-lexer: 1.7.0 + expect-type: 1.2.2 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.0(@types/node@20.19.24)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.20.3)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@edge-runtime/vm': 5.0.0 + '@types/node': 20.19.24 + happy-dom: 20.0.10 + jsdom: 27.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + optional: true + vscode-jsonrpc@8.2.0: {} vscode-languageclient@9.0.1: @@ -16629,6 +17100,10 @@ snapshots: yocto-queue@0.1.0: {} + yocto-spinner@0.2.3: + dependencies: + yoctocolors: 2.1.2 + yoctocolors@2.1.2: {} youch-core@0.3.3: @@ -16657,3 +17132,5 @@ snapshots: zod: 4.1.12 zod@4.1.12: {} + + zod@4.3.6: {} From 84d1e60ae1785ed8318336e06638a16f4c73adc0 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Tue, 27 Jan 2026 11:30:05 +0800 Subject: [PATCH 11/25] perf(orm): more aggressive caching of validation zod schemas (#623) * WIP: more aggressive caching of validation zod schemas * refactor: use a decorator-based approach for caching * update * update * update * update * update * remove object-type args from cache key * update cache key --- .../client/crud/validator/cache-decorator.ts | 54 +++ .../orm/src/client/crud/validator/index.ts | 416 ++++++++++-------- packages/schema/src/schema.ts | 1 + packages/sdk/src/ts-schema-generator.ts | 2 + pnpm-lock.yaml | 11 +- samples/orm/zenstack/schema.ts | 1 + tests/e2e/apps/rally/zenstack/schema.ts | 14 + tests/e2e/github-repos/cal.com/schema.ts | 34 ++ tests/e2e/github-repos/formbricks/schema.ts | 24 + tests/e2e/github-repos/trigger.dev/schema.ts | 34 ++ tests/e2e/orm/schemas/basic/schema.ts | 1 + tests/e2e/orm/schemas/name-mapping/schema.ts | 1 + tests/e2e/orm/schemas/procedures/schema.ts | 1 + tests/e2e/orm/schemas/typed-json/schema.ts | 1 + tests/e2e/orm/schemas/typing/schema.ts | 2 + tests/regression/package.json | 1 + tests/regression/test/issue-204/schema.ts | 1 + 17 files changed, 416 insertions(+), 183 deletions(-) create mode 100644 packages/orm/src/client/crud/validator/cache-decorator.ts diff --git a/packages/orm/src/client/crud/validator/cache-decorator.ts b/packages/orm/src/client/crud/validator/cache-decorator.ts new file mode 100644 index 000000000..bd8dd452f --- /dev/null +++ b/packages/orm/src/client/crud/validator/cache-decorator.ts @@ -0,0 +1,54 @@ +import stableStringify from 'json-stable-stringify'; + +/** + * Method decorator that caches the return value based on method name and arguments. + * + * Requirements: + * - Class must have a `getCache(key: string)` method + * - Class must have a `setCache(key: string, value: any)` method + */ +export function cache() { + return function (_target: any, propertyKey: string, descriptor: PropertyDescriptor) { + const originalMethod = descriptor.value; + + descriptor.value = function ( + this: { + getCache: (key: string) => unknown; + setCache: (key: string, value: unknown) => void; + } & Record, + ...args: any[] + ) { + // Build cache key object + const cacheKeyObj: Record = { + $call: propertyKey, + $args: args.map((arg) => { + if (Array.isArray(arg)) { + // sort array arguments for consistent cache keys + return [...arg].sort(); + } else { + return arg; + } + }), + }; + + // Generate stable string key + const cacheKey = stableStringify(cacheKeyObj)!; + + // Check cache + const cached = this.getCache(cacheKey); + if (cached !== undefined) { + return cached; + } + + // Execute original method + const result = originalMethod.apply(this, args); + + // Store in cache + this.setCache(cacheKey, result); + + return result; + }; + + return descriptor; + }; +} diff --git a/packages/orm/src/client/crud/validator/index.ts b/packages/orm/src/client/crud/validator/index.ts index 76dd58529..8cad792e9 100644 --- a/packages/orm/src/client/crud/validator/index.ts +++ b/packages/orm/src/client/crud/validator/index.ts @@ -1,16 +1,13 @@ import { enumerate, invariant } from '@zenstackhq/common-helpers'; import Decimal from 'decimal.js'; -import stableStringify from 'json-stable-stringify'; import { match, P } from 'ts-pattern'; import { z, ZodObject, ZodType } from 'zod'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; import { type AttributeApplication, type BuiltinType, - type EnumDef, type FieldDef, type GetModels, - type ModelDef, type ProcedureDef, type SchemaDef, } from '../../../schema'; @@ -54,6 +51,7 @@ import { CoreUpdateOperations, type CoreCrudOperations, } from '../operations/base'; +import { cache } from './cache-decorator'; import { addBigIntValidation, addCustomValidation, @@ -82,119 +80,7 @@ export class InputValidator { return this.client.$options.validateInput !== false; } - validateProcedureInput(proc: string, input: unknown): unknown { - const procDef = (this.schema.procedures ?? {})[proc] as ProcedureDef | undefined; - invariant(procDef, `Procedure "${proc}" not found in schema`); - - const params = Object.values(procDef.params ?? {}); - - // For procedures where every parameter is optional, allow omitting the input entirely. - if (typeof input === 'undefined') { - if (params.length === 0) { - return undefined; - } - if (params.every((p) => p.optional)) { - return undefined; - } - throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); - } - - if (typeof input !== 'object') { - throw createInvalidInputError('Procedure input must be an object', `$procs.${proc}`); - } - - const envelope = input as Record; - const argsPayload = Object.prototype.hasOwnProperty.call(envelope, 'args') ? (envelope as any).args : undefined; - - if (params.length === 0) { - if (typeof argsPayload === 'undefined') { - return input; - } - if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { - throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); - } - if (Object.keys(argsPayload as any).length === 0) { - return input; - } - throw createInvalidInputError('Procedure does not accept arguments', `$procs.${proc}`); - } - - if (typeof argsPayload === 'undefined') { - if (params.every((p) => p.optional)) { - return input; - } - throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); - } - - if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { - throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); - } - - const obj = argsPayload as Record; - - for (const param of params) { - const value = (obj as any)[param.name]; - - if (!Object.prototype.hasOwnProperty.call(obj, param.name)) { - if (param.optional) { - continue; - } - throw createInvalidInputError(`Missing procedure argument: ${param.name}`, `$procs.${proc}`); - } - - if (typeof value === 'undefined') { - if (param.optional) { - continue; - } - throw createInvalidInputError( - `Invalid procedure argument: ${param.name} is required`, - `$procs.${proc}`, - ); - } - - const schema = this.makeProcedureParamSchema(param); - const parsed = schema.safeParse(value); - if (!parsed.success) { - throw createInvalidInputError( - `Invalid procedure argument: ${param.name}: ${formatError(parsed.error)}`, - `$procs.${proc}`, - ); - } - } - - return input; - } - - private makeProcedureParamSchema(param: { type: string; array?: boolean; optional?: boolean }): z.ZodType { - let schema: z.ZodType; - - if (isTypeDef(this.schema, param.type)) { - schema = this.makeTypeDefSchema(param.type); - } else if (isEnum(this.schema, param.type)) { - schema = this.makeEnumSchema(param.type); - } else if (param.type in (this.schema.models ?? {})) { - // For model-typed values, accept any object (no deep shape validation). - schema = z.record(z.string(), z.unknown()); - } else { - // Builtin scalar types. - schema = this.makeScalarSchema(param.type as BuiltinType); - - // If a type isn't recognized by any of the above branches, `makeScalarSchema` returns `unknown`. - // Treat it as configuration/schema error. - if (schema instanceof z.ZodUnknown) { - throw createInternalError(`Unsupported procedure parameter type: ${param.type}`); - } - } - - if (param.array) { - schema = schema.array(); - } - if (param.optional) { - schema = schema.optional(); - } - - return schema; - } + // #region Entry points validateFindArgs( model: GetModels, @@ -335,27 +221,96 @@ export class InputValidator { ); } - private getSchemaCache(cacheKey: string) { - return this.schemaCache.get(cacheKey); - } + // TODO: turn it into a Zod schema and cache + validateProcedureInput(proc: string, input: unknown): unknown { + const procDef = (this.schema.procedures ?? {})[proc] as ProcedureDef | undefined; + invariant(procDef, `Procedure "${proc}" not found in schema`); - private setSchemaCache(cacheKey: string, schema: ZodType) { - return this.schemaCache.set(cacheKey, schema); - } + const params = Object.values(procDef.params ?? {}); - private validate(model: GetModels, operation: string, getSchema: GetSchemaFunc, args: unknown) { - const cacheKey = stableStringify({ - type: 'model', - model, - operation, - extraValidationsEnabled: this.extraValidationsEnabled, - }); - let schema = this.getSchemaCache(cacheKey!); - if (!schema) { - schema = getSchema(model); - this.setSchemaCache(cacheKey!, schema); + // For procedures where every parameter is optional, allow omitting the input entirely. + if (typeof input === 'undefined') { + if (params.length === 0) { + return undefined; + } + if (params.every((p) => p.optional)) { + return undefined; + } + throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); + } + + if (typeof input !== 'object' || input === null || Array.isArray(input)) { + throw createInvalidInputError('Procedure input must be an object', `$procs.${proc}`); } + const envelope = input as Record; + const argsPayload = Object.prototype.hasOwnProperty.call(envelope, 'args') ? (envelope as any).args : undefined; + + if (params.length === 0) { + if (typeof argsPayload === 'undefined') { + return input; + } + if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { + throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); + } + if (Object.keys(argsPayload as any).length === 0) { + return input; + } + throw createInvalidInputError('Procedure does not accept arguments', `$procs.${proc}`); + } + + if (typeof argsPayload === 'undefined') { + if (params.every((p) => p.optional)) { + return input; + } + throw createInvalidInputError('Missing procedure arguments', `$procs.${proc}`); + } + + if (!argsPayload || typeof argsPayload !== 'object' || Array.isArray(argsPayload)) { + throw createInvalidInputError('Procedure `args` must be an object', `$procs.${proc}`); + } + + const obj = argsPayload as Record; + + for (const param of params) { + const value = (obj as any)[param.name]; + + if (!Object.prototype.hasOwnProperty.call(obj, param.name)) { + if (param.optional) { + continue; + } + throw createInvalidInputError(`Missing procedure argument: ${param.name}`, `$procs.${proc}`); + } + + if (typeof value === 'undefined') { + if (param.optional) { + continue; + } + throw createInvalidInputError( + `Invalid procedure argument: ${param.name} is required`, + `$procs.${proc}`, + ); + } + + const schema = this.makeProcedureParamSchema(param); + const parsed = schema.safeParse(value); + if (!parsed.success) { + throw createInvalidInputError( + `Invalid procedure argument: ${param.name}: ${formatError(parsed.error)}`, + `$procs.${proc}`, + ); + } + } + + return input; + } + + // #endregion + + // #region Validation helpers + + private validate(model: GetModels, operation: string, getSchema: GetSchemaFunc, args: unknown) { + const schema = getSchema(model); const { error, data } = schema.safeParse(args); if (error) { throw createInvalidInputError( @@ -453,8 +408,11 @@ export class InputValidator { return result; } + // #endregion + // #region Find + @cache() private makeFindSchema(model: string, operation: CoreCrudOperations) { const fields: Record = {}; const unique = operation === 'findUnique'; @@ -493,6 +451,7 @@ export class InputValidator { return result; } + @cache() private makeExistsSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -539,35 +498,18 @@ export class InputValidator { } } + @cache() private makeEnumSchema(type: string) { - const key = stableStringify({ - type: 'enum', - name: type, - }); - let schema = this.getSchemaCache(key!); - if (schema) { - return schema; - } const enumDef = getEnum(this.schema, type); invariant(enumDef, `Enum "${type}" not found in schema`); - schema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); - this.setSchemaCache(key!, schema); - return schema; + return z.enum(Object.keys(enumDef.values) as [string, ...string[]]); } + @cache() private makeTypeDefSchema(type: string): z.ZodType { - const key = stableStringify({ - type: 'typedef', - name: type, - extraValidationsEnabled: this.extraValidationsEnabled, - }); - let schema = this.getSchemaCache(key!); - if (schema) { - return schema; - } const typeDef = getTypeDef(this.schema, type); invariant(typeDef, `Type definition "${type}" not found in schema`); - schema = z.looseObject( + const schema = z.looseObject( Object.fromEntries( Object.entries(typeDef.fields).map(([field, def]) => { let fieldSchema = this.makeScalarSchema(def.type); @@ -592,10 +534,10 @@ export class InputValidator { } }); - this.setSchemaCache(key!, finalSchema); return finalSchema; } + @cache() private makeWhereSchema( model: string, unique: boolean, @@ -644,7 +586,7 @@ export class InputValidator { // enum if (Object.keys(enumDef.values).length > 0) { fieldSchema = this.makeEnumFilterSchema( - enumDef, + fieldDef.type, !!fieldDef.optional, withAggregations, !!fieldDef.array, @@ -686,7 +628,7 @@ export class InputValidator { // enum if (Object.keys(enumDef.values).length > 0) { fieldSchema = this.makeEnumFilterSchema( - enumDef, + def.type, !!def.optional, false, false, @@ -754,6 +696,7 @@ export class InputValidator { return result; } + @cache() private makeTypedJsonFilterSchema(type: string, optional: boolean, array: boolean) { const typeDef = getTypeDef(this.schema, type); invariant(typeDef, `Type definition "${type}" not found in schema`); @@ -776,7 +719,7 @@ export class InputValidator { const enumDef = getEnum(this.schema, fieldDef.type); if (enumDef) { fieldSchemas[fieldName] = this.makeEnumFilterSchema( - enumDef, + fieldDef.type, !!fieldDef.optional, false, !!fieldDef.array, @@ -832,7 +775,10 @@ export class InputValidator { return this.schema.typeDefs && type in this.schema.typeDefs; } - private makeEnumFilterSchema(enumDef: EnumDef, optional: boolean, withAggregations: boolean, array: boolean) { + @cache() + private makeEnumFilterSchema(enumName: string, optional: boolean, withAggregations: boolean, array: boolean) { + const enumDef = getEnum(this.schema, enumName); + invariant(enumDef, `Enum "${enumName}" not found in schema`); const baseSchema = z.enum(Object.keys(enumDef.values) as [string, ...string[]]); if (array) { return this.internalMakeArrayFilterSchema(baseSchema); @@ -840,13 +786,14 @@ export class InputValidator { const components = this.makeCommonPrimitiveFilterComponents( baseSchema, optional, - () => z.lazy(() => this.makeEnumFilterSchema(enumDef, optional, withAggregations, array)), + () => z.lazy(() => this.makeEnumFilterSchema(enumName, optional, withAggregations, array)), ['equals', 'in', 'notIn', 'not'], withAggregations ? ['_count', '_min', '_max'] : undefined, ); return z.union([this.nullableIf(baseSchema, optional), z.strictObject(components)]); } + @cache() private makeArrayFilterSchema(type: BuiltinType) { return this.internalMakeArrayFilterSchema(this.makeScalarSchema(type)); } @@ -861,6 +808,7 @@ export class InputValidator { }); } + @cache() private makePrimitiveFilterSchema(type: BuiltinType, optional: boolean, withAggregations: boolean) { return match(type) .with('String', () => this.makeStringFilterSchema(optional, withAggregations)) @@ -902,6 +850,7 @@ export class InputValidator { return this.nullableIf(schema, nullable); } + @cache() private makeJsonFilterSchema(optional: boolean) { const valueSchema = this.makeJsonValueSchema(optional, true); return z.strictObject({ @@ -918,6 +867,7 @@ export class InputValidator { }); } + @cache() private makeDateTimeFilterSchema(optional: boolean, withAggregations: boolean): ZodType { return this.makeCommonPrimitiveFilterSchema( z.union([z.iso.datetime(), z.date()]), @@ -927,6 +877,7 @@ export class InputValidator { ); } + @cache() private makeBooleanFilterSchema(optional: boolean, withAggregations: boolean): ZodType { const components = this.makeCommonPrimitiveFilterComponents( z.boolean(), @@ -938,6 +889,7 @@ export class InputValidator { return z.union([this.nullableIf(z.boolean(), optional), z.strictObject(components)]); } + @cache() private makeBytesFilterSchema(optional: boolean, withAggregations: boolean): ZodType { const baseSchema = z.instanceof(Uint8Array); const components = this.makeCommonPrimitiveFilterComponents( @@ -1035,27 +987,30 @@ export class InputValidator { return z.union([z.literal('default'), z.literal('insensitive')]); } + @cache() private makeSelectSchema(model: string) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + fields[field] = this.makeRelationSelectIncludeSchema(model, field).optional(); } else { fields[field] = z.boolean().optional(); } } - const _countSchema = this.makeCountSelectionSchema(modelDef); - if (_countSchema) { + const _countSchema = this.makeCountSelectionSchema(model); + if (!(_countSchema instanceof z.ZodNever)) { fields['_count'] = _countSchema; } return z.strictObject(fields); } - private makeCountSelectionSchema(modelDef: ModelDef) { + @cache() + private makeCountSelectionSchema(model: string) { + const modelDef = requireModel(this.schema, model); const toManyRelations = Object.values(modelDef.fields).filter((def) => def.relation && def.array); if (toManyRelations.length > 0) { return z @@ -1082,11 +1037,13 @@ export class InputValidator { ]) .optional(); } else { - return undefined; + return z.never(); } } - private makeRelationSelectIncludeSchema(fieldDef: FieldDef) { + @cache() + private makeRelationSelectIncludeSchema(model: string, field: string) { + const fieldDef = requireField(this.schema, model, field); let objSchema: z.ZodType = z.strictObject({ ...(fieldDef.array || fieldDef.optional ? { @@ -1126,6 +1083,7 @@ export class InputValidator { return z.union([z.boolean(), objSchema]); } + @cache() private makeOmitSchema(model: string) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; @@ -1144,24 +1102,26 @@ export class InputValidator { return z.strictObject(fields); } + @cache() private makeIncludeSchema(model: string) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; for (const field of Object.keys(modelDef.fields)) { const fieldDef = requireField(this.schema, model, field); if (fieldDef.relation) { - fields[field] = this.makeRelationSelectIncludeSchema(fieldDef).optional(); + fields[field] = this.makeRelationSelectIncludeSchema(model, field).optional(); } } - const _countSchema = this.makeCountSelectionSchema(modelDef); - if (_countSchema) { + const _countSchema = this.makeCountSelectionSchema(model); + if (!(_countSchema instanceof z.ZodNever)) { fields['_count'] = _countSchema; } return z.strictObject(fields); } + @cache() private makeOrderBySchema(model: string, withRelation: boolean, WithAggregation: boolean) { const modelDef = requireModel(this.schema, model); const fields: Record = {}; @@ -1210,6 +1170,7 @@ export class InputValidator { return z.strictObject(fields); } + @cache() private makeDistinctSchema(model: string) { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); @@ -1217,6 +1178,7 @@ export class InputValidator { } private makeCursorSchema(model: string) { + // `makeWhereSchema` is already cached return this.makeWhereSchema(model, true, true).optional(); } @@ -1224,6 +1186,7 @@ export class InputValidator { // #region Create + @cache() private makeCreateSchema(model: string) { const dataSchema = this.makeCreateDataSchema(model, false); const baseSchema = z.strictObject({ @@ -1238,10 +1201,12 @@ export class InputValidator { return schema; } + @cache() private makeCreateManySchema(model: string) { return this.mergePluginArgsSchema(this.makeCreateManyDataSchema(model, []), 'createMany').optional(); } + @cache() private makeCreateManyAndReturnSchema(model: string) { const base = this.makeCreateManyDataSchema(model, []); let result: ZodObject = base.extend({ @@ -1252,6 +1217,7 @@ export class InputValidator { return this.refineForSelectOmitMutuallyExclusive(result).optional(); } + @cache() private makeCreateDataSchema( model: string, canBeArray: boolean, @@ -1294,7 +1260,7 @@ export class InputValidator { } let fieldSchema: ZodType = z.lazy(() => - this.makeRelationManipulationSchema(fieldDef, excludeFields, 'create'), + this.makeRelationManipulationSchema(model, field, excludeFields, 'create'), ); if (fieldDef.optional || fieldDef.array) { @@ -1387,7 +1353,14 @@ export class InputValidator { return discriminatorField === fieldDef.name; } - private makeRelationManipulationSchema(fieldDef: FieldDef, withoutFields: string[], mode: 'create' | 'update') { + @cache() + private makeRelationManipulationSchema( + model: string, + field: string, + withoutFields: string[], + mode: 'create' | 'update', + ) { + const fieldDef = requireField(this.schema, model, field); const fieldType = fieldDef.type; const array = !!fieldDef.array; const fields: Record = { @@ -1461,14 +1434,17 @@ export class InputValidator { return z.strictObject(fields); } + @cache() private makeSetDataSchema(model: string, canBeArray: boolean) { return this.orArray(this.makeWhereSchema(model, true), canBeArray); } + @cache() private makeConnectDataSchema(model: string, canBeArray: boolean) { return this.orArray(this.makeWhereSchema(model, true), canBeArray); } + @cache() private makeDisconnectDataSchema(model: string, canBeArray: boolean) { if (canBeArray) { // to-many relation, must be unique filters @@ -1480,12 +1456,14 @@ export class InputValidator { } } + @cache() private makeDeleteRelationDataSchema(model: string, toManyRelation: boolean, uniqueFilter: boolean) { return toManyRelation ? this.orArray(this.makeWhereSchema(model, uniqueFilter), true) : z.union([z.boolean(), this.makeWhereSchema(model, uniqueFilter)]); } + @cache() private makeConnectOrCreateDataSchema(model: string, canBeArray: boolean, withoutFields: string[]) { const whereSchema = this.makeWhereSchema(model, true); const createSchema = this.makeCreateDataSchema(model, false, withoutFields); @@ -1498,6 +1476,7 @@ export class InputValidator { ); } + @cache() private makeCreateManyDataSchema(model: string, withoutFields: string[]) { return z.strictObject({ data: this.makeCreateDataSchema(model, true, withoutFields, true), @@ -1509,6 +1488,7 @@ export class InputValidator { // #region Update + @cache() private makeUpdateSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1523,6 +1503,7 @@ export class InputValidator { return schema; } + @cache() private makeUpdateManySchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ @@ -1534,6 +1515,7 @@ export class InputValidator { ); } + @cache() private makeUpdateManyAndReturnSchema(model: string) { // plugin extended args schema is merged in `makeUpdateManySchema` const baseSchema: ZodObject = this.makeUpdateManySchema(model); @@ -1545,6 +1527,7 @@ export class InputValidator { return schema; } + @cache() private makeUpsertSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), @@ -1560,6 +1543,7 @@ export class InputValidator { return schema; } + @cache() private makeUpdateDataSchema(model: string, withoutFields: string[] = [], withoutRelationFields = false) { const uncheckedVariantFields: Record = {}; const checkedVariantFields: Record = {}; @@ -1588,7 +1572,7 @@ export class InputValidator { } } let fieldSchema: ZodType = z - .lazy(() => this.makeRelationManipulationSchema(fieldDef, excludeFields, 'update')) + .lazy(() => this.makeRelationManipulationSchema(model, field, excludeFields, 'update')) .optional(); // optional to-one relation can be null if (fieldDef.optional && !fieldDef.array) { @@ -1670,7 +1654,8 @@ export class InputValidator { // #region Delete - private makeDeleteSchema(model: GetModels) { + @cache() + private makeDeleteSchema(model: string) { const baseSchema = z.strictObject({ where: this.makeWhereSchema(model, true), select: this.makeSelectSchema(model).optional().nullable(), @@ -1683,7 +1668,8 @@ export class InputValidator { return schema; } - private makeDeleteManySchema(model: GetModels) { + @cache() + private makeDeleteManySchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -1697,7 +1683,8 @@ export class InputValidator { // #region Count - makeCountSchema(model: GetModels) { + @cache() + makeCountSchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -1710,7 +1697,8 @@ export class InputValidator { ).optional(); } - private makeCountAggregateInputSchema(model: GetModels) { + @cache() + private makeCountAggregateInputSchema(model: string) { const modelDef = requireModel(this.schema, model); return z.union([ z.literal(true), @@ -1731,7 +1719,8 @@ export class InputValidator { // #region Aggregate - makeAggregateSchema(model: GetModels) { + @cache() + makeAggregateSchema(model: string) { return this.mergePluginArgsSchema( z.strictObject({ where: this.makeWhereSchema(model, false).optional(), @@ -1748,7 +1737,8 @@ export class InputValidator { ).optional(); } - makeSumAvgInputSchema(model: GetModels) { + @cache() + makeSumAvgInputSchema(model: string) { const modelDef = requireModel(this.schema, model); return z.strictObject( Object.keys(modelDef.fields).reduce( @@ -1764,7 +1754,8 @@ export class InputValidator { ); } - makeMinMaxInputSchema(model: GetModels) { + @cache() + makeMinMaxInputSchema(model: string) { const modelDef = requireModel(this.schema, model); return z.strictObject( Object.keys(modelDef.fields).reduce( @@ -1780,7 +1771,8 @@ export class InputValidator { ); } - private makeGroupBySchema(model: GetModels) { + @cache() + private makeGroupBySchema(model: string) { const modelDef = requireModel(this.schema, model); const nonRelationFields = Object.keys(modelDef.fields).filter((field) => !modelDef.fields[field]?.relation); const bySchema = @@ -1866,18 +1858,79 @@ export class InputValidator { return true; } - private makeHavingSchema(model: GetModels) { + private makeHavingSchema(model: string) { + // `makeWhereSchema` is cached return this.makeWhereSchema(model, false, true, true); } // #endregion + // #region Procedures + + @cache() + private makeProcedureParamSchema(param: { type: string; array?: boolean; optional?: boolean }): z.ZodType { + let schema: z.ZodType; + + if (isTypeDef(this.schema, param.type)) { + schema = this.makeTypeDefSchema(param.type); + } else if (isEnum(this.schema, param.type)) { + schema = this.makeEnumSchema(param.type); + } else if (param.type in (this.schema.models ?? {})) { + // For model-typed values, accept any object (no deep shape validation). + schema = z.record(z.string(), z.unknown()); + } else { + // Builtin scalar types. + schema = this.makeScalarSchema(param.type as BuiltinType); + + // If a type isn't recognized by any of the above branches, `makeScalarSchema` returns `unknown`. + // Treat it as configuration/schema error. + if (schema instanceof z.ZodUnknown) { + throw createInternalError(`Unsupported procedure parameter type: ${param.type}`); + } + } + + if (param.array) { + schema = schema.array(); + } + if (param.optional) { + schema = schema.optional(); + } + + return schema; + } + + // #endregion + + // #region Cache Management + + getCache(cacheKey: string) { + return this.schemaCache.get(cacheKey); + } + + setCache(cacheKey: string, schema: ZodType) { + return this.schemaCache.set(cacheKey, schema); + } + + // @ts-ignore + private printCacheStats(detailed = false) { + console.log('Schema cache size:', this.schemaCache.size); + if (detailed) { + for (const key of this.schemaCache.keys()) { + console.log(`\t${key}`); + } + } + } + + // #endregion + // #region Helpers + @cache() private makeSkipSchema() { return z.number().int().nonnegative(); } + @cache() private makeTakeSchema() { return z.number().int(); } @@ -1911,5 +1964,6 @@ export class InputValidator { private get providerSupportsCaseSensitivity() { return this.schema.provider.type === 'postgresql'; } + // #endregion } diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index 58fc1bc5b..08b0726f6 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -106,6 +106,7 @@ export type EnumField = { }; export type EnumDef = { + name: string; fields?: Record; values: Record; attributes?: readonly AttributeApplication[]; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 4a5e0a548..16f81bae2 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1030,6 +1030,8 @@ export class TsSchemaGenerator { private createEnumObject(e: Enum) { return ts.factory.createObjectLiteralExpression( [ + ts.factory.createPropertyAssignment('name', ts.factory.createStringLiteral(e.name)), + ts.factory.createPropertyAssignment( 'values', ts.factory.createObjectLiteralExpression( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c202a4423..02aef9843 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1120,6 +1120,9 @@ importers: '@zenstackhq/orm': specifier: workspace:* version: link:../../packages/orm + '@zenstackhq/plugin-policy': + specifier: workspace:* + version: link:../../packages/plugins/policy '@zenstackhq/sdk': specifier: workspace:* version: link:../../packages/sdk @@ -13073,8 +13076,8 @@ snapshots: '@babel/parser': 7.28.5 eslint: 9.29.0(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.1.12 - zod-validation-error: 4.0.1(zod@4.1.12) + zod: 4.3.6 + zod-validation-error: 4.0.1(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -17131,6 +17134,10 @@ snapshots: dependencies: zod: 4.1.12 + zod-validation-error@4.0.1(zod@4.3.6): + dependencies: + zod: 4.3.6 + zod@4.1.12: {} zod@4.3.6: {} diff --git a/samples/orm/zenstack/schema.ts b/samples/orm/zenstack/schema.ts index e3c02e5c6..908df1e71 100644 --- a/samples/orm/zenstack/schema.ts +++ b/samples/orm/zenstack/schema.ts @@ -232,6 +232,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" diff --git a/tests/e2e/apps/rally/zenstack/schema.ts b/tests/e2e/apps/rally/zenstack/schema.ts index 7d20facea..d9ddfdb78 100644 --- a/tests/e2e/apps/rally/zenstack/schema.ts +++ b/tests/e2e/apps/rally/zenstack/schema.ts @@ -2391,6 +2391,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { TimeFormat: { + name: "TimeFormat", values: { hours12: "hours12", hours24: "hours24" @@ -2400,6 +2401,7 @@ export class SchemaType implements SchemaDef { ] }, UserRole: { + name: "UserRole", values: { admin: "admin", user: "user" @@ -2409,6 +2411,7 @@ export class SchemaType implements SchemaDef { ] }, ParticipantVisibility: { + name: "ParticipantVisibility", values: { full: "full", scoresOnly: "scoresOnly", @@ -2419,6 +2422,7 @@ export class SchemaType implements SchemaDef { ] }, PollStatus: { + name: "PollStatus", values: { live: "live", paused: "paused", @@ -2429,6 +2433,7 @@ export class SchemaType implements SchemaDef { ] }, VoteType: { + name: "VoteType", values: { yes: "yes", no: "no", @@ -2439,12 +2444,14 @@ export class SchemaType implements SchemaDef { ] }, SpaceMemberRole: { + name: "SpaceMemberRole", values: { ADMIN: "ADMIN", MEMBER: "MEMBER" } }, SpaceTier: { + name: "SpaceTier", values: { hobby: "hobby", pro: "pro" @@ -2454,6 +2461,7 @@ export class SchemaType implements SchemaDef { ] }, SubscriptionStatus: { + name: "SubscriptionStatus", values: { incomplete: "incomplete", incomplete_expired: "incomplete_expired", @@ -2469,6 +2477,7 @@ export class SchemaType implements SchemaDef { ] }, SubscriptionInterval: { + name: "SubscriptionInterval", values: { month: "month", year: "year" @@ -2478,6 +2487,7 @@ export class SchemaType implements SchemaDef { ] }, ScheduledEventStatus: { + name: "ScheduledEventStatus", values: { confirmed: "confirmed", canceled: "canceled", @@ -2488,6 +2498,7 @@ export class SchemaType implements SchemaDef { ] }, ScheduledEventInviteStatus: { + name: "ScheduledEventInviteStatus", values: { pending: "pending", accepted: "accepted", @@ -2499,11 +2510,13 @@ export class SchemaType implements SchemaDef { ] }, CredentialType: { + name: "CredentialType", values: { OAUTH: "OAUTH" } }, LicenseType: { + name: "LicenseType", values: { PLUS: "PLUS", ORGANIZATION: "ORGANIZATION", @@ -2511,6 +2524,7 @@ export class SchemaType implements SchemaDef { } }, LicenseStatus: { + name: "LicenseStatus", values: { ACTIVE: "ACTIVE", REVOKED: "REVOKED" diff --git a/tests/e2e/github-repos/cal.com/schema.ts b/tests/e2e/github-repos/cal.com/schema.ts index 7715a296c..47bb801a4 100644 --- a/tests/e2e/github-repos/cal.com/schema.ts +++ b/tests/e2e/github-repos/cal.com/schema.ts @@ -9185,6 +9185,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { SchedulingType: { + name: "SchedulingType", values: { ROUND_ROBIN: "ROUND_ROBIN", COLLECTIVE: "COLLECTIVE", @@ -9212,6 +9213,7 @@ export class SchemaType implements SchemaDef { } }, PeriodType: { + name: "PeriodType", values: { UNLIMITED: "UNLIMITED", ROLLING: "ROLLING", @@ -9246,6 +9248,7 @@ export class SchemaType implements SchemaDef { } }, CreationSource: { + name: "CreationSource", values: { API_V1: "API_V1", API_V2: "API_V2", @@ -9273,6 +9276,7 @@ export class SchemaType implements SchemaDef { } }, IdentityProvider: { + name: "IdentityProvider", values: { CAL: "CAL", GOOGLE: "GOOGLE", @@ -9280,18 +9284,21 @@ export class SchemaType implements SchemaDef { } }, UserPermissionRole: { + name: "UserPermissionRole", values: { USER: "USER", ADMIN: "ADMIN" } }, CreditType: { + name: "CreditType", values: { MONTHLY: "MONTHLY", ADDITIONAL: "ADDITIONAL" } }, MembershipRole: { + name: "MembershipRole", values: { MEMBER: "MEMBER", ADMIN: "ADMIN", @@ -9299,6 +9306,7 @@ export class SchemaType implements SchemaDef { } }, BookingStatus: { + name: "BookingStatus", values: { CANCELLED: "CANCELLED", ACCEPTED: "ACCEPTED", @@ -9340,6 +9348,7 @@ export class SchemaType implements SchemaDef { } }, EventTypeCustomInputType: { + name: "EventTypeCustomInputType", values: { TEXT: "TEXT", TEXTLONG: "TEXTLONG", @@ -9388,17 +9397,20 @@ export class SchemaType implements SchemaDef { } }, ReminderType: { + name: "ReminderType", values: { PENDING_BOOKING_CONFIRMATION: "PENDING_BOOKING_CONFIRMATION" } }, PaymentOption: { + name: "PaymentOption", values: { ON_BOOKING: "ON_BOOKING", HOLD: "HOLD" } }, WebhookTriggerEvents: { + name: "WebhookTriggerEvents", values: { BOOKING_CREATED: "BOOKING_CREATED", BOOKING_PAYMENT_INITIATED: "BOOKING_PAYMENT_INITIATED", @@ -9421,6 +9433,7 @@ export class SchemaType implements SchemaDef { } }, AppCategories: { + name: "AppCategories", values: { calendar: "calendar", messaging: "messaging", @@ -9435,6 +9448,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowTriggerEvents: { + name: "WorkflowTriggerEvents", values: { BEFORE_EVENT: "BEFORE_EVENT", EVENT_CANCELLED: "EVENT_CANCELLED", @@ -9446,6 +9460,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowActions: { + name: "WorkflowActions", values: { EMAIL_HOST: "EMAIL_HOST", EMAIL_ATTENDEE: "EMAIL_ATTENDEE", @@ -9457,6 +9472,7 @@ export class SchemaType implements SchemaDef { } }, TimeUnit: { + name: "TimeUnit", values: { DAY: "DAY", HOUR: "HOUR", @@ -9484,6 +9500,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowTemplates: { + name: "WorkflowTemplates", values: { REMINDER: "REMINDER", CUSTOM: "CUSTOM", @@ -9494,6 +9511,7 @@ export class SchemaType implements SchemaDef { } }, WorkflowMethods: { + name: "WorkflowMethods", values: { EMAIL: "EMAIL", SMS: "SMS", @@ -9501,6 +9519,7 @@ export class SchemaType implements SchemaDef { } }, FeatureType: { + name: "FeatureType", values: { RELEASE: "RELEASE", EXPERIMENT: "EXPERIMENT", @@ -9510,24 +9529,28 @@ export class SchemaType implements SchemaDef { } }, RRResetInterval: { + name: "RRResetInterval", values: { MONTH: "MONTH", DAY: "DAY" } }, RRTimestampBasis: { + name: "RRTimestampBasis", values: { CREATED_AT: "CREATED_AT", START_TIME: "START_TIME" } }, AccessScope: { + name: "AccessScope", values: { READ_BOOKING: "READ_BOOKING", READ_PROFILE: "READ_PROFILE" } }, RedirectType: { + name: "RedirectType", values: { UserEventType: "UserEventType", TeamEventType: "TeamEventType", @@ -9562,6 +9585,7 @@ export class SchemaType implements SchemaDef { } }, SMSLockState: { + name: "SMSLockState", values: { LOCKED: "LOCKED", UNLOCKED: "UNLOCKED", @@ -9569,6 +9593,7 @@ export class SchemaType implements SchemaDef { } }, AttributeType: { + name: "AttributeType", values: { TEXT: "TEXT", NUMBER: "NUMBER", @@ -9577,6 +9602,7 @@ export class SchemaType implements SchemaDef { } }, AssignmentReasonEnum: { + name: "AssignmentReasonEnum", values: { ROUTING_FORM_ROUTING: "ROUTING_FORM_ROUTING", ROUTING_FORM_ROUTING_FALLBACK: "ROUTING_FORM_ROUTING_FALLBACK", @@ -9587,12 +9613,14 @@ export class SchemaType implements SchemaDef { } }, EventTypeAutoTranslatedField: { + name: "EventTypeAutoTranslatedField", values: { DESCRIPTION: "DESCRIPTION", TITLE: "TITLE" } }, WatchlistType: { + name: "WatchlistType", values: { EMAIL: "EMAIL", DOMAIN: "DOMAIN", @@ -9600,6 +9628,7 @@ export class SchemaType implements SchemaDef { } }, WatchlistSeverity: { + name: "WatchlistSeverity", values: { LOW: "LOW", MEDIUM: "MEDIUM", @@ -9608,29 +9637,34 @@ export class SchemaType implements SchemaDef { } }, BillingPeriod: { + name: "BillingPeriod", values: { MONTHLY: "MONTHLY", ANNUALLY: "ANNUALLY" } }, IncompleteBookingActionType: { + name: "IncompleteBookingActionType", values: { SALESFORCE: "SALESFORCE" } }, FilterSegmentScope: { + name: "FilterSegmentScope", values: { USER: "USER", TEAM: "TEAM" } }, WorkflowContactType: { + name: "WorkflowContactType", values: { PHONE: "PHONE", EMAIL: "EMAIL" } }, RoleType: { + name: "RoleType", values: { SYSTEM: "SYSTEM", CUSTOM: "CUSTOM" diff --git a/tests/e2e/github-repos/formbricks/schema.ts b/tests/e2e/github-repos/formbricks/schema.ts index 935b8320b..f3e478760 100644 --- a/tests/e2e/github-repos/formbricks/schema.ts +++ b/tests/e2e/github-repos/formbricks/schema.ts @@ -2834,6 +2834,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { PipelineTriggers: { + name: "PipelineTriggers", values: { responseCreated: "responseCreated", responseUpdated: "responseUpdated", @@ -2841,6 +2842,7 @@ export class SchemaType implements SchemaDef { } }, WebhookSource: { + name: "WebhookSource", values: { user: "user", zapier: "zapier", @@ -2850,12 +2852,14 @@ export class SchemaType implements SchemaDef { } }, ContactAttributeType: { + name: "ContactAttributeType", values: { default: "default", custom: "custom" } }, SurveyStatus: { + name: "SurveyStatus", values: { draft: "draft", scheduled: "scheduled", @@ -2865,18 +2869,21 @@ export class SchemaType implements SchemaDef { } }, DisplayStatus: { + name: "DisplayStatus", values: { seen: "seen", responded: "responded" } }, SurveyAttributeFilterCondition: { + name: "SurveyAttributeFilterCondition", values: { equals: "equals", notEquals: "notEquals" } }, SurveyType: { + name: "SurveyType", values: { link: "link", web: "web", @@ -2885,6 +2892,7 @@ export class SchemaType implements SchemaDef { } }, displayOptions: { + name: "displayOptions", values: { displayOnce: "displayOnce", displayMultiple: "displayMultiple", @@ -2893,18 +2901,21 @@ export class SchemaType implements SchemaDef { } }, ActionType: { + name: "ActionType", values: { code: "code", noCode: "noCode" } }, EnvironmentType: { + name: "EnvironmentType", values: { production: "production", development: "development" } }, IntegrationType: { + name: "IntegrationType", values: { googleSheets: "googleSheets", notion: "notion", @@ -2913,6 +2924,7 @@ export class SchemaType implements SchemaDef { } }, DataMigrationStatus: { + name: "DataMigrationStatus", values: { pending: "pending", applied: "applied", @@ -2920,6 +2932,7 @@ export class SchemaType implements SchemaDef { } }, WidgetPlacement: { + name: "WidgetPlacement", values: { bottomLeft: "bottomLeft", bottomRight: "bottomRight", @@ -2929,6 +2942,7 @@ export class SchemaType implements SchemaDef { } }, OrganizationRole: { + name: "OrganizationRole", values: { owner: "owner", manager: "manager", @@ -2937,6 +2951,7 @@ export class SchemaType implements SchemaDef { } }, MembershipRole: { + name: "MembershipRole", values: { owner: "owner", admin: "admin", @@ -2946,6 +2961,7 @@ export class SchemaType implements SchemaDef { } }, ApiKeyPermission: { + name: "ApiKeyPermission", values: { read: "read", write: "write", @@ -2953,6 +2969,7 @@ export class SchemaType implements SchemaDef { } }, IdentityProvider: { + name: "IdentityProvider", values: { email: "email", github: "github", @@ -2963,6 +2980,7 @@ export class SchemaType implements SchemaDef { } }, Role: { + name: "Role", values: { project_manager: "project_manager", engineer: "engineer", @@ -2972,6 +2990,7 @@ export class SchemaType implements SchemaDef { } }, Objective: { + name: "Objective", values: { increase_conversion: "increase_conversion", improve_user_retention: "improve_user_retention", @@ -2982,6 +3001,7 @@ export class SchemaType implements SchemaDef { } }, Intention: { + name: "Intention", values: { survey_user_segments: "survey_user_segments", survey_at_specific_point_in_user_journey: "survey_at_specific_point_in_user_journey", @@ -2991,6 +3011,7 @@ export class SchemaType implements SchemaDef { } }, InsightCategory: { + name: "InsightCategory", values: { featureRequest: "featureRequest", complaint: "complaint", @@ -2999,6 +3020,7 @@ export class SchemaType implements SchemaDef { } }, Sentiment: { + name: "Sentiment", values: { positive: "positive", negative: "negative", @@ -3006,12 +3028,14 @@ export class SchemaType implements SchemaDef { } }, TeamUserRole: { + name: "TeamUserRole", values: { admin: "admin", contributor: "contributor" } }, ProjectTeamPermission: { + name: "ProjectTeamPermission", values: { read: "read", readWrite: "readWrite", diff --git a/tests/e2e/github-repos/trigger.dev/schema.ts b/tests/e2e/github-repos/trigger.dev/schema.ts index 9b884d078..12dab9e1b 100644 --- a/tests/e2e/github-repos/trigger.dev/schema.ts +++ b/tests/e2e/github-repos/trigger.dev/schema.ts @@ -6081,18 +6081,21 @@ export class SchemaType implements SchemaDef { } as const; enums = { AuthenticationMethod: { + name: "AuthenticationMethod", values: { GITHUB: "GITHUB", MAGIC_LINK: "MAGIC_LINK" } }, OrgMemberRole: { + name: "OrgMemberRole", values: { ADMIN: "ADMIN", MEMBER: "MEMBER" } }, RuntimeEnvironmentType: { + name: "RuntimeEnvironmentType", values: { PRODUCTION: "PRODUCTION", STAGING: "STAGING", @@ -6101,24 +6104,28 @@ export class SchemaType implements SchemaDef { } }, ProjectVersion: { + name: "ProjectVersion", values: { V2: "V2", V3: "V3" } }, SecretStoreProvider: { + name: "SecretStoreProvider", values: { DATABASE: "DATABASE", AWS_PARAM_STORE: "AWS_PARAM_STORE" } }, TaskTriggerSource: { + name: "TaskTriggerSource", values: { STANDARD: "STANDARD", SCHEDULED: "SCHEDULED" } }, TaskRunStatus: { + name: "TaskRunStatus", values: { DELAYED: "DELAYED", PENDING: "PENDING", @@ -6139,12 +6146,14 @@ export class SchemaType implements SchemaDef { } }, RunEngineVersion: { + name: "RunEngineVersion", values: { V1: "V1", V2: "V2" } }, TaskRunExecutionStatus: { + name: "TaskRunExecutionStatus", values: { RUN_CREATED: "RUN_CREATED", QUEUED: "QUEUED", @@ -6158,12 +6167,14 @@ export class SchemaType implements SchemaDef { } }, TaskRunCheckpointType: { + name: "TaskRunCheckpointType", values: { DOCKER: "DOCKER", KUBERNETES: "KUBERNETES" } }, WaitpointType: { + name: "WaitpointType", values: { RUN: "RUN", DATETIME: "DATETIME", @@ -6172,18 +6183,21 @@ export class SchemaType implements SchemaDef { } }, WaitpointStatus: { + name: "WaitpointStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED" } }, WorkerInstanceGroupType: { + name: "WorkerInstanceGroupType", values: { MANAGED: "MANAGED", UNMANAGED: "UNMANAGED" } }, TaskRunAttemptStatus: { + name: "TaskRunAttemptStatus", values: { PENDING: "PENDING", EXECUTING: "EXECUTING", @@ -6194,6 +6208,7 @@ export class SchemaType implements SchemaDef { } }, TaskEventLevel: { + name: "TaskEventLevel", values: { TRACE: "TRACE", DEBUG: "DEBUG", @@ -6204,6 +6219,7 @@ export class SchemaType implements SchemaDef { } }, TaskEventKind: { + name: "TaskEventKind", values: { UNSPECIFIED: "UNSPECIFIED", INTERNAL: "INTERNAL", @@ -6216,6 +6232,7 @@ export class SchemaType implements SchemaDef { } }, TaskEventStatus: { + name: "TaskEventStatus", values: { UNSET: "UNSET", OK: "OK", @@ -6224,18 +6241,21 @@ export class SchemaType implements SchemaDef { } }, TaskQueueType: { + name: "TaskQueueType", values: { VIRTUAL: "VIRTUAL", NAMED: "NAMED" } }, TaskQueueVersion: { + name: "TaskQueueVersion", values: { V1: "V1", V2: "V2" } }, BatchTaskRunStatus: { + name: "BatchTaskRunStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED", @@ -6243,6 +6263,7 @@ export class SchemaType implements SchemaDef { } }, BatchTaskRunItemStatus: { + name: "BatchTaskRunItemStatus", values: { PENDING: "PENDING", FAILED: "FAILED", @@ -6251,18 +6272,21 @@ export class SchemaType implements SchemaDef { } }, CheckpointType: { + name: "CheckpointType", values: { DOCKER: "DOCKER", KUBERNETES: "KUBERNETES" } }, CheckpointRestoreEventType: { + name: "CheckpointRestoreEventType", values: { CHECKPOINT: "CHECKPOINT", RESTORE: "RESTORE" } }, WorkerDeploymentType: { + name: "WorkerDeploymentType", values: { MANAGED: "MANAGED", UNMANAGED: "UNMANAGED", @@ -6270,6 +6294,7 @@ export class SchemaType implements SchemaDef { } }, WorkerDeploymentStatus: { + name: "WorkerDeploymentStatus", values: { PENDING: "PENDING", BUILDING: "BUILDING", @@ -6281,17 +6306,20 @@ export class SchemaType implements SchemaDef { } }, ScheduleType: { + name: "ScheduleType", values: { DECLARATIVE: "DECLARATIVE", IMPERATIVE: "IMPERATIVE" } }, ScheduleGeneratorType: { + name: "ScheduleGeneratorType", values: { CRON: "CRON" } }, ProjectAlertChannelType: { + name: "ProjectAlertChannelType", values: { EMAIL: "EMAIL", SLACK: "SLACK", @@ -6299,6 +6327,7 @@ export class SchemaType implements SchemaDef { } }, ProjectAlertType: { + name: "ProjectAlertType", values: { TASK_RUN: "TASK_RUN", TASK_RUN_ATTEMPT: "TASK_RUN_ATTEMPT", @@ -6307,6 +6336,7 @@ export class SchemaType implements SchemaDef { } }, ProjectAlertStatus: { + name: "ProjectAlertStatus", values: { PENDING: "PENDING", SENT: "SENT", @@ -6314,23 +6344,27 @@ export class SchemaType implements SchemaDef { } }, IntegrationService: { + name: "IntegrationService", values: { SLACK: "SLACK" } }, BulkActionType: { + name: "BulkActionType", values: { CANCEL: "CANCEL", REPLAY: "REPLAY" } }, BulkActionStatus: { + name: "BulkActionStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED" } }, BulkActionItemStatus: { + name: "BulkActionItemStatus", values: { PENDING: "PENDING", COMPLETED: "COMPLETED", diff --git a/tests/e2e/orm/schemas/basic/schema.ts b/tests/e2e/orm/schemas/basic/schema.ts index 1e559cc79..9d6cb1982 100644 --- a/tests/e2e/orm/schemas/basic/schema.ts +++ b/tests/e2e/orm/schemas/basic/schema.ts @@ -295,6 +295,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" diff --git a/tests/e2e/orm/schemas/name-mapping/schema.ts b/tests/e2e/orm/schemas/name-mapping/schema.ts index 8e610ffd0..b92cd14b4 100644 --- a/tests/e2e/orm/schemas/name-mapping/schema.ts +++ b/tests/e2e/orm/schemas/name-mapping/schema.ts @@ -90,6 +90,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { USER: "USER", ADMIN: "ADMIN", diff --git a/tests/e2e/orm/schemas/procedures/schema.ts b/tests/e2e/orm/schemas/procedures/schema.ts index f5a9044e5..bd2b10018 100644 --- a/tests/e2e/orm/schemas/procedures/schema.ts +++ b/tests/e2e/orm/schemas/procedures/schema.ts @@ -69,6 +69,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" diff --git a/tests/e2e/orm/schemas/typed-json/schema.ts b/tests/e2e/orm/schemas/typed-json/schema.ts index 11be6f901..d99f97b5d 100644 --- a/tests/e2e/orm/schemas/typed-json/schema.ts +++ b/tests/e2e/orm/schemas/typed-json/schema.ts @@ -94,6 +94,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { Gender: { + name: "Gender", values: { MALE: "MALE", FEMALE: "FEMALE" diff --git a/tests/e2e/orm/schemas/typing/schema.ts b/tests/e2e/orm/schemas/typing/schema.ts index a558c23c6..4c82da67d 100644 --- a/tests/e2e/orm/schemas/typing/schema.ts +++ b/tests/e2e/orm/schemas/typing/schema.ts @@ -328,12 +328,14 @@ export class SchemaType implements SchemaDef { } as const; enums = { Role: { + name: "Role", values: { ADMIN: "ADMIN", USER: "USER" } }, Status: { + name: "Status", values: { ACTIVE: "ACTIVE", INACTIVE: "INACTIVE", diff --git a/tests/regression/package.json b/tests/regression/package.json index d6b14dac5..12f1f3014 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -20,6 +20,7 @@ "@zenstackhq/language": "workspace:*", "@zenstackhq/orm": "workspace:*", "@zenstackhq/sdk": "workspace:*", + "@zenstackhq/plugin-policy": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", "@types/node": "catalog:" diff --git a/tests/regression/test/issue-204/schema.ts b/tests/regression/test/issue-204/schema.ts index 5b24b3ae7..3c8726b39 100644 --- a/tests/regression/test/issue-204/schema.ts +++ b/tests/regression/test/issue-204/schema.ts @@ -47,6 +47,7 @@ export class SchemaType implements SchemaDef { } as const; enums = { ShirtColor: { + name: "ShirtColor", values: { Black: "Black", White: "White", From e8717e422f62353264b5344ad51258e3264f34c8 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Tue, 27 Jan 2026 16:25:22 +0800 Subject: [PATCH 12/25] fix(cli): properly quote prisma exec path (#624) fixes #609 --- packages/cli/src/utils/exec-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/utils/exec-utils.ts b/packages/cli/src/utils/exec-utils.ts index 03629f628..99ed1be09 100644 --- a/packages/cli/src/utils/exec-utils.ts +++ b/packages/cli/src/utils/exec-utils.ts @@ -57,5 +57,5 @@ export function execPrisma(args: string, options?: Omit return; } - execSync(`node ${prismaPath} ${args}`, _options); + execSync(`node "${prismaPath}" ${args}`, _options); } From 6719d4d494e2cfc63dc6dcf577c3aedf0b69212d Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Tue, 27 Jan 2026 16:43:53 +0800 Subject: [PATCH 13/25] feat(cli): add mysql support to proxy (#625) --- packages/cli/package.json | 3 ++- packages/cli/src/actions/proxy.ts | 31 ++++++++++++++++++++----------- pnpm-lock.yaml | 3 +++ 3 files changed, 25 insertions(+), 12 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 9bfcc5f95..1149b35d8 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -56,7 +56,8 @@ "pg": "catalog:", "prisma": "catalog:", "semver": "^7.7.2", - "ts-pattern": "catalog:" + "ts-pattern": "catalog:", + "mysql2": "catalog:" }, "devDependencies": { "@types/better-sqlite3": "catalog:", diff --git a/packages/cli/src/actions/proxy.ts b/packages/cli/src/actions/proxy.ts index 45375b05d..f3f27ef7e 100644 --- a/packages/cli/src/actions/proxy.ts +++ b/packages/cli/src/actions/proxy.ts @@ -1,21 +1,23 @@ -import { isDataSource } from '@zenstackhq/language/ast'; -import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils'; -import { CliError } from '../cli-error'; import { ZModelCodeGenerator } from '@zenstackhq/language'; +import { isDataSource } from '@zenstackhq/language/ast'; import { getStringLiteral } from '@zenstackhq/language/utils'; -import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite'; -import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres'; -import SQLite from 'better-sqlite3'; -import { Pool } from 'pg'; -import path from 'node:path'; import { ZenStackClient, type ClientContract } from '@zenstackhq/orm'; +import { MysqlDialect } from '@zenstackhq/orm/dialects/mysql'; +import { PostgresDialect } from '@zenstackhq/orm/dialects/postgres'; +import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite'; import { RPCApiHandler } from '@zenstackhq/server/api'; import { ZenStackMiddleware } from '@zenstackhq/server/express'; -import express from 'express'; +import SQLite from 'better-sqlite3'; import colors from 'colors'; +import cors from 'cors'; +import express from 'express'; import { createJiti } from 'jiti'; +import { createPool as createMysqlPool } from 'mysql2'; +import path from 'node:path'; +import { Pool as PgPool } from 'pg'; +import { CliError } from '../cli-error'; import { getVersion } from '../utils/version-utils'; -import cors from 'cors'; +import { getOutputPath, getSchemaFile, loadSchemaDocument } from './action-utils'; type Options = { output?: string; @@ -125,10 +127,17 @@ function createDialect(provider: string, databaseUrl: string, outputPath: string case 'postgresql': console.log(colors.gray(`Connecting to PostgreSQL database at: ${databaseUrl}`)); return new PostgresDialect({ - pool: new Pool({ + pool: new PgPool({ connectionString: databaseUrl, }), }); + + case 'mysql': + console.log(colors.gray(`Connecting to MySQL database at: ${databaseUrl}`)); + return new MysqlDialect({ + pool: createMysqlPool(databaseUrl), + }); + default: throw new CliError(`Unsupported database provider: ${provider}`); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02aef9843..0e4080566 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -237,6 +237,9 @@ importers: mixpanel: specifier: ^0.18.1 version: 0.18.1 + mysql2: + specifier: 'catalog:' + version: 3.16.1 ora: specifier: ^5.4.1 version: 5.4.1 From 209312a27b14b6c4397e211fd4b2001437390250 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 29 Jan 2026 00:15:07 +0800 Subject: [PATCH 14/25] fix(orm): properly handle literal array values in policy evaluation for postgres (#628) --- .../src/client/crud/dialects/base-dialect.ts | 49 +- .../orm/src/client/crud/dialects/mysql.ts | 64 +- .../src/client/crud/dialects/postgresql.ts | 124 ++- .../orm/src/client/crud/dialects/sqlite.ts | 57 +- .../orm/src/client/executor/name-mapper.ts | 56 +- packages/orm/src/client/functions.ts | 12 +- .../src/client/helpers/schema-db-pusher.ts | 16 +- .../policy/src/expression-transformer.ts | 64 +- packages/plugins/policy/src/policy-handler.ts | 3 +- packages/schema/src/expression-utils.ts | 56 +- packages/schema/src/expression.ts | 1 + packages/sdk/src/ts-schema-generator.ts | 4 + samples/next.js/zenstack/schema.ts | 2 +- samples/nuxt/zenstack/schema.ts | 2 +- samples/orm/zenstack/schema.ts | 4 +- samples/sveltekit/src/zenstack/schema.ts | 2 +- tests/e2e/apps/rally/zenstack/schema.ts | 148 ++-- tests/e2e/github-repos/cal.com/schema.ts | 722 +++++++++--------- tests/e2e/github-repos/formbricks/schema.ts | 224 +++--- tests/e2e/github-repos/trigger.dev/schema.ts | 386 +++++----- tests/e2e/orm/schemas/basic/schema.ts | 6 +- tests/e2e/orm/schemas/default-auth/schema.ts | 4 +- tests/e2e/orm/schemas/delegate/schema.ts | 16 +- tests/e2e/orm/schemas/name-mapping/schema.ts | 2 +- tests/e2e/orm/schemas/omit/schema.ts | 2 +- tests/e2e/orm/schemas/petstore/schema.ts | 4 +- tests/e2e/orm/schemas/todo/schema.ts | 16 +- tests/e2e/orm/schemas/typing/schema.ts | 10 +- tests/regression/test/issue-422/schema.ts | 4 +- tests/regression/test/issue-503/schema.ts | 4 +- tests/regression/test/issue-595.test.ts | 122 +++ tests/runtimes/bun/schemas/schema.ts | 2 +- tests/runtimes/edge-runtime/schemas/schema.ts | 2 +- 33 files changed, 1136 insertions(+), 1054 deletions(-) create mode 100644 tests/regression/test/issue-595.test.ts diff --git a/packages/orm/src/client/crud/dialects/base-dialect.ts b/packages/orm/src/client/crud/dialects/base-dialect.ts index 515878d9e..91848d57a 100644 --- a/packages/orm/src/client/crud/dialects/base-dialect.ts +++ b/packages/orm/src/client/crud/dialects/base-dialect.ts @@ -489,26 +489,41 @@ export abstract class BaseCrudDialect { continue; } - const value = this.transformInput(_value, fieldType, !!fieldDef.array); + invariant(fieldDef.array, 'Field must be an array type to build array filter'); + const value = this.transformInput(_value, fieldType, true); + + let receiver = fieldRef; + if (isEnum(this.schema, fieldType)) { + // cast enum array to `text[]` for type compatibility + receiver = this.eb.cast(fieldRef, sql.raw('text[]')); + } + + const buildArray = (value: unknown) => { + invariant(Array.isArray(value), 'Array filter value must be an array'); + return this.buildArrayValue( + value.map((v) => this.eb.val(v)), + fieldType, + ); + }; switch (key) { case 'equals': { - clauses.push(this.buildLiteralFilter(fieldRef, fieldType, this.eb.val(value))); + clauses.push(this.eb(receiver, '=', buildArray(value))); break; } case 'has': { - clauses.push(this.eb(fieldRef, '@>', this.eb.val([value]))); + clauses.push(this.buildArrayContains(receiver, this.eb.val(value))); break; } case 'hasEvery': { - clauses.push(this.eb(fieldRef, '@>', this.eb.val(value))); + clauses.push(this.buildArrayHasEvery(receiver, buildArray(value))); break; } case 'hasSome': { - clauses.push(this.eb(fieldRef, '&&', this.eb.val(value))); + clauses.push(this.buildArrayHasSome(receiver, buildArray(value))); break; } @@ -1420,9 +1435,24 @@ export abstract class BaseCrudDialect { abstract buildArrayLength(array: Expression): AliasableExpression; /** - * Builds an array literal SQL string for the given values. + * Builds an array value expression. */ - abstract buildArrayLiteralSQL(values: unknown[]): AliasableExpression; + abstract buildArrayValue(values: Expression[], elemType: string): AliasableExpression; + + /** + * Builds an expression that checks if an array contains a single value. + */ + abstract buildArrayContains(field: Expression, value: Expression): AliasableExpression; + + /** + * Builds an expression that checks if an array contains all values from another array. + */ + abstract buildArrayHasEvery(field: Expression, values: Expression): AliasableExpression; + + /** + * Builds an expression that checks if an array overlaps with another array. + */ + abstract buildArrayHasSome(field: Expression, values: Expression): AliasableExpression; /** * Casts the given expression to an integer type. @@ -1439,11 +1469,6 @@ export abstract class BaseCrudDialect { */ abstract trimTextQuotes>(expression: T): T; - /** - * Gets the SQL column type for the given field definition. - */ - abstract getFieldSqlType(fieldDef: FieldDef): string; - /* * Gets the string casing behavior for the dialect. */ diff --git a/packages/orm/src/client/crud/dialects/mysql.ts b/packages/orm/src/client/crud/dialects/mysql.ts index e196323a3..1e0e3f52f 100644 --- a/packages/orm/src/client/crud/dialects/mysql.ts +++ b/packages/orm/src/client/crud/dialects/mysql.ts @@ -3,9 +3,10 @@ import Decimal from 'decimal.js'; import type { AliasableExpression, TableExpression } from 'kysely'; import { expressionBuilder, + ExpressionWrapper, sql, + ValueListNode, type Expression, - type ExpressionWrapper, type SelectQueryBuilder, type SqlBool, } from 'kysely'; @@ -13,7 +14,7 @@ import { match } from 'ts-pattern'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; import type { BuiltinType, FieldDef, SchemaDef } from '../../../schema'; import type { SortOrder } from '../../crud-types'; -import { createInternalError, createInvalidInputError, createNotSupportedError } from '../../errors'; +import { createInvalidInputError, createNotSupportedError } from '../../errors'; import type { ClientOptions } from '../../options'; import { isTypeDef } from '../../query-utils'; import { LateralJoinDialectBase } from './lateral-join-dialect-base'; @@ -223,8 +224,29 @@ export class MySqlCrudDialect extends LateralJoinDiale return this.eb.fn('JSON_LENGTH', [array]); } - override buildArrayLiteralSQL(_values: unknown[]): AliasableExpression { - throw new Error('MySQL does not support array literals'); + override buildArrayValue(values: Expression[], _elemType: string): AliasableExpression { + return new ExpressionWrapper(ValueListNode.create(values.map((v) => v.toOperationNode()))); + } + + override buildArrayContains( + _field: Expression, + _value: Expression, + ): AliasableExpression { + throw createNotSupportedError('MySQL does not support native array operations'); + } + + override buildArrayHasEvery( + _field: Expression, + _values: Expression, + ): AliasableExpression { + throw createNotSupportedError('MySQL does not support native array operations'); + } + + override buildArrayHasSome( + _field: Expression, + _values: Expression, + ): AliasableExpression { + throw createNotSupportedError('MySQL does not support native array operations'); } protected override buildJsonEqualityFilter( @@ -288,40 +310,6 @@ export class MySqlCrudDialect extends LateralJoinDiale ); } - override getFieldSqlType(fieldDef: FieldDef) { - // TODO: respect `@db.x` attributes - if (fieldDef.relation) { - throw createInternalError('Cannot get SQL type of a relation field'); - } - - let result: string; - - if (this.schema.enums?.[fieldDef.type]) { - // enums are treated as text/varchar - result = 'varchar(255)'; - } else { - result = match(fieldDef.type) - .with('String', () => 'varchar(255)') - .with('Boolean', () => 'tinyint(1)') // MySQL uses tinyint(1) for boolean - .with('Int', () => 'int') - .with('BigInt', () => 'bigint') - .with('Float', () => 'double') - .with('Decimal', () => 'decimal') - .with('DateTime', () => 'datetime') - .with('Bytes', () => 'blob') - .with('Json', () => 'json') - // fallback to text - .otherwise(() => 'text'); - } - - if (fieldDef.array) { - // MySQL stores arrays as JSON - result = 'json'; - } - - return result; - } - override getStringCasingBehavior() { // MySQL LIKE is case-insensitive by default (depends on collation), no ILIKE support return { supportsILike: false, likeCaseSensitive: false }; diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 02e9435fa..9897bcccb 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -14,9 +14,9 @@ import z from 'zod'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; import type { BuiltinType, FieldDef, SchemaDef } from '../../../schema'; import type { SortOrder } from '../../crud-types'; -import { createInternalError, createInvalidInputError } from '../../errors'; +import { createInvalidInputError } from '../../errors'; import type { ClientOptions } from '../../options'; -import { getEnum, isEnum, isTypeDef } from '../../query-utils'; +import { isEnum, isTypeDef } from '../../query-utils'; import { LateralJoinDialectBase } from './lateral-join-dialect-base'; export class PostgresCrudDialect extends LateralJoinDialectBase { @@ -95,18 +95,7 @@ export class PostgresCrudDialect extends LateralJoinDi if (type === 'Json' && !forArrayField) { // scalar `Json` fields need their input stringified return JSON.stringify(value); - } - if (isEnum(this.schema, type)) { - // cast to enum array `CAST(ARRAY[...] AS "enum_type"[])` - return this.eb.cast( - sql`ARRAY[${sql.join( - value.map((v) => this.transformInput(v, type, false)), - sql.raw(','), - )}]`, - this.createSchemaQualifiedEnumType(type, true), - ); } else { - // `Json[]` fields need their input as array (not stringified) return value.map((v) => this.transformInput(v, type, false)); } } else { @@ -136,32 +125,6 @@ export class PostgresCrudDialect extends LateralJoinDi } } - private createSchemaQualifiedEnumType(type: string, array: boolean) { - // determines the postgres schema name for the enum type, and returns the - // qualified name - - let qualified = type; - - const enumDef = getEnum(this.schema, type); - if (enumDef) { - // check if the enum has a custom "@@schema" attribute - const schemaAttr = enumDef.attributes?.find((attr) => attr.name === '@@schema'); - if (schemaAttr) { - const mapArg = schemaAttr.args?.find((arg) => arg.name === 'map'); - if (mapArg && mapArg.value.kind === 'literal') { - const schemaName = mapArg.value.value as string; - qualified = `"${schemaName}"."${type}"`; - } - } else { - // no custom schema, use default from datasource or 'public' - const defaultSchema = this.schema.provider.defaultSchema ?? 'public'; - qualified = `"${defaultSchema}"."${type}"`; - } - } - - return array ? sql.raw(`${qualified}[]`) : sql.raw(qualified); - } - override transformOutput(value: unknown, type: BuiltinType, array: boolean) { if (value === null || value === undefined) { return value; @@ -290,15 +253,25 @@ export class PostgresCrudDialect extends LateralJoinDi return this.eb.fn('array_length', [array]); } - override buildArrayLiteralSQL(values: unknown[]): AliasableExpression { - if (values.length === 0) { - return sql`{}`; - } else { - return sql`ARRAY[${sql.join( - values.map((v) => sql.val(v)), - sql.raw(','), - )}]`; - } + override buildArrayValue(values: Expression[], elemType: string): AliasableExpression { + const arr = sql`ARRAY[${sql.join(values, sql.raw(','))}]`; + const mappedType = this.getSqlType(elemType); + return this.eb.cast(arr, sql`${sql.raw(mappedType)}[]`); + } + + override buildArrayContains(field: Expression, value: Expression): AliasableExpression { + // PostgreSQL @> operator expects array on both sides, so wrap single value in array + return this.eb(field, '@>', sql`ARRAY[${value}]`); + } + + override buildArrayHasEvery(field: Expression, values: Expression): AliasableExpression { + // PostgreSQL @> operator: field contains all elements in values + return this.eb(field, '@>', values); + } + + override buildArrayHasSome(field: Expression, values: Expression): AliasableExpression { + // PostgreSQL && operator: arrays have any elements in common + return this.eb(field, '&&', values); } protected override buildJsonPathSelection(receiver: Expression, path: string | undefined) { @@ -348,37 +321,26 @@ export class PostgresCrudDialect extends LateralJoinDi ); } - override getFieldSqlType(fieldDef: FieldDef) { - // TODO: respect `@db.x` attributes - if (fieldDef.relation) { - throw createInternalError('Cannot get SQL type of a relation field'); - } - - let result: string; - - if (this.schema.enums?.[fieldDef.type]) { - // enums are treated as text - result = 'text'; + private getSqlType(zmodelType: string) { + if (isEnum(this.schema, zmodelType)) { + // reduce enum to text for type compatibility + return 'text'; } else { - result = match(fieldDef.type) - .with('String', () => 'text') - .with('Boolean', () => 'boolean') - .with('Int', () => 'integer') - .with('BigInt', () => 'bigint') - .with('Float', () => 'double precision') - .with('Decimal', () => 'decimal') - .with('DateTime', () => 'timestamp') - .with('Bytes', () => 'bytea') - .with('Json', () => 'jsonb') - // fallback to text - .otherwise(() => 'text'); - } - - if (fieldDef.array) { - result += '[]'; + return ( + match(zmodelType) + .with('String', () => 'text') + .with('Boolean', () => 'boolean') + .with('Int', () => 'integer') + .with('BigInt', () => 'bigint') + .with('Float', () => 'double precision') + .with('Decimal', () => 'decimal') + .with('DateTime', () => 'timestamp') + .with('Bytes', () => 'bytea') + .with('Json', () => 'jsonb') + // fallback to text + .otherwise(() => 'text') + ); } - - return result; } override getStringCasingBehavior() { @@ -414,9 +376,11 @@ export class PostgresCrudDialect extends LateralJoinDi )})`.as('$values'), ) .select( - fields.map((f, i) => - sql`CAST(${sql.ref(`$values.column${i + 1}`)} AS ${sql.raw(this.getFieldSqlType(f))})`.as(f.name), - ), + fields.map((f, i) => { + const mappedType = this.getSqlType(f.type); + const castType = f.array ? sql`${sql.raw(mappedType)}[]` : sql.raw(mappedType); + return this.eb.cast(sql.ref(`$values.column${i + 1}`), castType).as(f.name); + }), ); } diff --git a/packages/orm/src/client/crud/dialects/sqlite.ts b/packages/orm/src/client/crud/dialects/sqlite.ts index 4ef87a464..ee6de5da0 100644 --- a/packages/orm/src/client/crud/dialects/sqlite.ts +++ b/packages/orm/src/client/crud/dialects/sqlite.ts @@ -2,7 +2,9 @@ import { invariant } from '@zenstackhq/common-helpers'; import Decimal from 'decimal.js'; import { expressionBuilder, + ExpressionWrapper, sql, + ValueListNode, type AliasableExpression, type Expression, type ExpressionBuilder, @@ -447,8 +449,29 @@ export class SqliteCrudDialect extends BaseCrudDialect return this.eb.fn('json_array_length', [array]); } - override buildArrayLiteralSQL(_values: unknown[]): AliasableExpression { - throw new Error('SQLite does not support array literals'); + override buildArrayValue(values: Expression[], _elemType: string): AliasableExpression { + return new ExpressionWrapper(ValueListNode.create(values.map((v) => v.toOperationNode()))); + } + + override buildArrayContains( + _field: Expression, + _value: Expression, + ): AliasableExpression { + throw createNotSupportedError('SQLite does not support native array operations'); + } + + override buildArrayHasEvery( + _field: Expression, + _values: Expression, + ): AliasableExpression { + throw createNotSupportedError('SQLite does not support native array operations'); + } + + override buildArrayHasSome( + _field: Expression, + _values: Expression, + ): AliasableExpression { + throw createNotSupportedError('SQLite does not support native array operations'); } override castInt>(expression: T): T { @@ -463,36 +486,6 @@ export class SqliteCrudDialect extends BaseCrudDialect return this.eb.fn('trim', [expression, sql.lit('"')]) as unknown as T; } - override getFieldSqlType(fieldDef: FieldDef) { - // TODO: respect `@db.x` attributes - if (fieldDef.relation) { - throw createInternalError('Cannot get SQL type of a relation field'); - } - if (fieldDef.array) { - throw createInternalError('SQLite does not support scalar list type'); - } - - if (this.schema.enums?.[fieldDef.type]) { - // enums are stored as text - return 'text'; - } - - return ( - match(fieldDef.type) - .with('String', () => 'text') - .with('Boolean', () => 'integer') - .with('Int', () => 'integer') - .with('BigInt', () => 'integer') - .with('Float', () => 'real') - .with('Decimal', () => 'decimal') - .with('DateTime', () => 'numeric') - .with('Bytes', () => 'blob') - .with('Json', () => 'jsonb') - // fallback to text - .otherwise(() => 'text') - ); - } - override getStringCasingBehavior() { // SQLite `LIKE` is case-insensitive, and there is no `ILIKE` return { supportsILike: false, likeCaseSensitive: false }; diff --git a/packages/orm/src/client/executor/name-mapper.ts b/packages/orm/src/client/executor/name-mapper.ts index ad6f18326..6a7535fe0 100644 --- a/packages/orm/src/client/executor/name-mapper.ts +++ b/packages/orm/src/client/executor/name-mapper.ts @@ -14,6 +14,7 @@ import { type OperationNode, OperationNodeTransformer, PrimitiveValueListNode, + type QueryId, ReferenceNode, ReturningNode, SelectAllNode, @@ -52,6 +53,7 @@ type SelectionNodeChild = SimpleReferenceExpressionNode | AliasNode | SelectAllN export class QueryNameMapper extends OperationNodeTransformer { private readonly modelToTableMap = new Map(); private readonly fieldToColumnMap = new Map(); + private readonly enumTypeMap = new Map(); private readonly scopes: Scope[] = []; private readonly dialect: BaseCrudDialect; @@ -71,6 +73,13 @@ export class QueryNameMapper extends OperationNodeTransformer { } } } + + for (const [enumName, enumDef] of Object.entries(client.$schema.enums ?? {})) { + const mappedName = this.getMappedName(enumDef); + if (mappedName) { + this.enumTypeMap.set(enumName, mappedName); + } + } } private get schema() { @@ -79,9 +88,9 @@ export class QueryNameMapper extends OperationNodeTransformer { // #region overrides - protected override transformSelectQuery(node: SelectQueryNode) { + protected override transformSelectQuery(node: SelectQueryNode, queryId?: QueryId) { if (!node.from?.froms) { - return super.transformSelectQuery(node); + return super.transformSelectQuery(node, queryId); } // process "from" clauses @@ -109,7 +118,7 @@ export class QueryNameMapper extends OperationNodeTransformer { })) : undefined; const selections = this.processSelectQuerySelections(node); - const baseResult = super.transformSelectQuery(node); + const baseResult = super.transformSelectQuery(node, queryId); return { ...baseResult, @@ -120,16 +129,16 @@ export class QueryNameMapper extends OperationNodeTransformer { }); } - protected override transformInsertQuery(node: InsertQueryNode) { + protected override transformInsertQuery(node: InsertQueryNode, queryId?: QueryId) { if (!node.into) { - return super.transformInsertQuery(node); + return super.transformInsertQuery(node, queryId); } const model = extractModelName(node.into); invariant(model, 'InsertQueryNode must have a model name in the "into" clause'); return this.withScope({ model }, () => { - const baseResult = super.transformInsertQuery(node); + const baseResult = super.transformInsertQuery(node, queryId); let values = baseResult.values; if (node.columns && values) { // process enum values with name mapping @@ -156,9 +165,9 @@ export class QueryNameMapper extends OperationNodeTransformer { }; } - protected override transformReference(node: ReferenceNode) { + protected override transformReference(node: ReferenceNode, queryId?: QueryId) { if (!ColumnNode.is(node.column)) { - return super.transformReference(node); + return super.transformReference(node, queryId); } // resolve the reference to a field from outer scopes @@ -187,16 +196,16 @@ export class QueryNameMapper extends OperationNodeTransformer { } } - protected override transformColumn(node: ColumnNode) { + protected override transformColumn(node: ColumnNode, queryId?: QueryId) { const scope = this.resolveFieldFromScopes(node.column.name); if (!scope || scope.namesMapped || !scope.model) { - return super.transformColumn(node); + return super.transformColumn(node, queryId); } const mappedName = this.mapFieldName(scope.model, node.column.name); return ColumnNode.create(mappedName); } - protected override transformBinaryOperation(node: BinaryOperationNode) { + protected override transformBinaryOperation(node: BinaryOperationNode, queryId?: QueryId) { // transform enum name mapping for enum values used inside binary operations // 1. simple value: column = EnumValue // 2. list value: column IN [EnumValue, EnumValue2] @@ -236,19 +245,22 @@ export class QueryNameMapper extends OperationNodeTransformer { ); } - return super.transformBinaryOperation({ - ...node, - rightOperand: resultValue, - }); + return super.transformBinaryOperation( + { + ...node, + rightOperand: resultValue, + }, + queryId, + ); } } - return super.transformBinaryOperation(node); + return super.transformBinaryOperation(node, queryId); } - protected override transformUpdateQuery(node: UpdateQueryNode) { + protected override transformUpdateQuery(node: UpdateQueryNode, queryId?: QueryId) { if (!node.table) { - return super.transformUpdateQuery(node); + return super.transformUpdateQuery(node, queryId); } const { alias, node: innerTable } = stripAlias(node.table); @@ -260,7 +272,7 @@ export class QueryNameMapper extends OperationNodeTransformer { invariant(model, 'UpdateQueryNode must have a model name in the "table" clause'); return this.withScope({ model, alias }, () => { - const baseResult = super.transformUpdateQuery(node); + const baseResult = super.transformUpdateQuery(node, queryId); // process enum value mappings in update set values const updates = baseResult.updates?.map((update, i) => { @@ -285,7 +297,7 @@ export class QueryNameMapper extends OperationNodeTransformer { }); } - protected override transformDeleteQuery(node: DeleteQueryNode) { + protected override transformDeleteQuery(node: DeleteQueryNode, queryId?: QueryId) { // all "from" nodes are pushed as scopes const scopes: Scope[] = node.from.froms.map((node) => { const { alias, node: innerNode } = stripAlias(node); @@ -303,13 +315,13 @@ export class QueryNameMapper extends OperationNodeTransformer { // map table name return this.wrapAlias(this.processTableRef(innerNode), alias); } else { - return super.transformNode(from); + return super.transformNode(from, queryId); } }); return this.withScopes(scopes, () => { return { - ...super.transformDeleteQuery(node), + ...super.transformDeleteQuery(node, queryId), from: FromNode.create(froms), }; }); diff --git a/packages/orm/src/client/functions.ts b/packages/orm/src/client/functions.ts index 1f3ff6ce8..25af1e723 100644 --- a/packages/orm/src/client/functions.ts +++ b/packages/orm/src/client/functions.ts @@ -68,7 +68,7 @@ const textMatch = ( return sql`${fieldExpr} ${sql.raw(op)} ${searchExpr} escape ${sql.val('\\')}`; }; -export const has: ZModelFunction = (eb, args) => { +export const has: ZModelFunction = (_eb, args, context) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -76,10 +76,10 @@ export const has: ZModelFunction = (eb, args) => { if (!search) { throw new Error('"search" parameter is required'); } - return eb(field, '@>', [search]); + return context.dialect.buildArrayContains(field, search); }; -export const hasEvery: ZModelFunction = (eb: ExpressionBuilder, args: Expression[]) => { +export const hasEvery: ZModelFunction = (_eb, args, { dialect }: ZModelFunctionContext) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -87,10 +87,10 @@ export const hasEvery: ZModelFunction = (eb: ExpressionBuilder, a if (!search) { throw new Error('"search" parameter is required'); } - return eb(field, '@>', search); + return dialect.buildArrayHasEvery(field, search); }; -export const hasSome: ZModelFunction = (eb, args) => { +export const hasSome: ZModelFunction = (_eb, args, { dialect }: ZModelFunctionContext) => { const [field, search] = args; if (!field) { throw new Error('"field" parameter is required'); @@ -98,7 +98,7 @@ export const hasSome: ZModelFunction = (eb, args) => { if (!search) { throw new Error('"search" parameter is required'); } - return eb(field, '&&', search); + return dialect.buildArrayHasSome(field, search); }; export const isEmpty: ZModelFunction = (eb, args, { dialect }: ZModelFunctionContext) => { diff --git a/packages/orm/src/client/helpers/schema-db-pusher.ts b/packages/orm/src/client/helpers/schema-db-pusher.ts index 38df33cee..28701ca1b 100644 --- a/packages/orm/src/client/helpers/schema-db-pusher.ts +++ b/packages/orm/src/client/helpers/schema-db-pusher.ts @@ -6,6 +6,7 @@ import { ExpressionUtils, type BuiltinType, type CascadeAction, + type EnumDef, type FieldDef, type ModelDef, type SchemaDef, @@ -27,7 +28,7 @@ export class SchemaDbPusher { async push() { await this.kysely.transaction().execute(async (tx) => { if (this.schema.enums && this.providerSupportsNativeEnum) { - for (const [name, enumDef] of Object.entries(this.schema.enums)) { + for (const enumDef of Object.values(this.schema.enums)) { let enumValues: string[]; if (enumDef.fields) { enumValues = Object.values(enumDef.fields).map((f) => { @@ -47,7 +48,7 @@ export class SchemaDbPusher { enumValues = Object.values(enumDef.values); } - const createEnum = tx.schema.createType(name).asEnum(enumValues); + const createEnum = tx.schema.createType(this.getEnumName(enumDef)).asEnum(enumValues); await createEnum.execute(); } } @@ -148,6 +149,17 @@ export class SchemaDbPusher { return modelDef.name; } + private getEnumName(enumDef: EnumDef) { + const mapAttr = enumDef.attributes?.find((a) => a.name === '@@map'); + if (mapAttr && mapAttr.args?.[0]) { + const mappedName = ExpressionUtils.getLiteralValue(mapAttr.args[0].value); + if (mappedName) { + return mappedName as string; + } + } + return enumDef.name; + } + private getColumnName(fieldDef: FieldDef) { const mapAttr = fieldDef.attributes?.find((a) => a.name === '@map'); if (mapAttr && mapAttr.args?.[0]) { diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index f33de0ea3..3b31bd05c 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -32,6 +32,7 @@ import { BinaryOperationNode, ColumnNode, expressionBuilder, + ExpressionWrapper, FromNode, FunctionNode, IdentifierNode, @@ -43,7 +44,6 @@ import { ValueListNode, ValueNode, WhereNode, - type ExpressionBuilder, type OperandExpression, type OperationNode, } from 'kysely'; @@ -127,6 +127,7 @@ function expr(kind: Expression['kind']) { export class ExpressionTransformer { private readonly dialect: BaseCrudDialect; + private readonly eb = expressionBuilder(); constructor(private readonly client: ClientContract) { this.dialect = getCrudDialect(this.schema, this.clientOptions); @@ -156,7 +157,9 @@ export class ExpressionTransformer { if (!handler) { throw new Error(`Unsupported expression kind: ${expression.kind}`); } - return handler.value.call(this, expression, context); + const result = handler.value.call(this, expression, context); + invariant('kind' in result, `expression handler must return an OperationNode: transforming ${expression.kind}`); + return result; } @expr('literal') @@ -171,7 +174,12 @@ export class ExpressionTransformer { @expr('array') // @ts-expect-error private _array(expr: ArrayExpression, context: ExpressionTransformerContext) { - return ValueListNode.create(expr.items.map((item) => this.transform(item, context))); + return this.dialect + .buildArrayValue( + expr.items.map((item) => new ExpressionWrapper(this.transform(item, context))), + expr.type, + ) + .toOperationNode(); } @expr('field') @@ -539,15 +547,22 @@ export class ExpressionTransformer { } } - private transformValue(value: unknown, type: BuiltinType) { + private transformValue(value: unknown, type: BuiltinType): OperationNode { if (value === true) { return trueNode(this.dialect); } else if (value === false) { return falseNode(this.dialect); + } else if (Array.isArray(value)) { + return this.dialect + .buildArrayValue( + value.map((v) => new ExpressionWrapper(this.transformValue(v, type))), + type, + ) + .toOperationNode(); } else { const transformed = this.dialect.transformInput(value, type, false) ?? null; - if (!Array.isArray(transformed)) { - // simple primitives can be immediate values + if (typeof transformed !== 'string') { + // simple non-string primitives can be immediate values return ValueNode.createImmediate(transformed); } else { return ValueNode.create(transformed); @@ -582,10 +597,9 @@ export class ExpressionTransformer { if (!func) { throw createUnsupportedError(`Function not implemented: ${expr.function}`); } - const eb = expressionBuilder(); return func( - eb, - (expr.args ?? []).map((arg) => this.transformCallArg(eb, arg, context)), + this.eb, + (expr.args ?? []).map((arg) => this.transformCallArg(arg, context)), { client: this.client, dialect: this.dialect, @@ -611,33 +625,13 @@ export class ExpressionTransformer { return func; } - private transformCallArg( - eb: ExpressionBuilder, - arg: Expression, - context: ExpressionTransformerContext, - ): OperandExpression { - if (ExpressionUtils.isLiteral(arg)) { - return eb.val(arg.value); - } - + private transformCallArg(arg: Expression, context: ExpressionTransformerContext): OperandExpression { if (ExpressionUtils.isField(arg)) { - return eb.ref(arg.field); - } - - if (ExpressionUtils.isCall(arg)) { - return this.transformCall(arg, context); - } - - if (this.isAuthMember(arg)) { - const valNode = this.valueMemberAccess(this.auth, arg as MemberExpression, this.authType); - return valNode ? eb.val(valNode.value) : eb.val(null); + // field references are passed as-is, without translating to joins, etc. + return this.eb.ref(arg.field); + } else { + return new ExpressionWrapper(this.transform(arg, context)); } - - // TODO - // if (Expression.isMember(arg)) { - // } - - throw createUnsupportedError(`Unsupported argument expression: ${arg.kind}`); } @expr('member') @@ -792,7 +786,7 @@ export class ExpressionTransformer { return binding; } - private valueMemberAccess(receiver: any, expr: MemberExpression, receiverType: string) { + private valueMemberAccess(receiver: any, expr: MemberExpression, receiverType: string): OperationNode { if (!receiver) { return ValueNode.createImmediate(null); } diff --git a/packages/plugins/policy/src/policy-handler.ts b/packages/plugins/policy/src/policy-handler.ts index ec12d1a7f..25545cfc8 100644 --- a/packages/plugins/policy/src/policy-handler.ts +++ b/packages/plugins/policy/src/policy-handler.ts @@ -945,8 +945,9 @@ export class PolicyHandler extends OperationNodeTransf // handle the case for list column if (Array.isArray(value)) { + const fieldDef = QueryUtils.requireField(this.client.$schema, model, fields[i]!); result.push({ - node: this.dialect.buildArrayLiteralSQL(value).toOperationNode(), + node: this.dialect.buildArrayValue(value, fieldDef.type).toOperationNode(), raw: value, }); } else { diff --git a/packages/schema/src/expression-utils.ts b/packages/schema/src/expression-utils.ts index 07ee1c115..8045da716 100644 --- a/packages/schema/src/expression-utils.ts +++ b/packages/schema/src/expression-utils.ts @@ -19,77 +19,43 @@ import type { */ export const ExpressionUtils = { literal: (value: string | number | boolean): LiteralExpression => { - return { - kind: 'literal', - value, - }; + return { kind: 'literal', value }; }, - array: (items: Expression[]): ArrayExpression => { - return { - kind: 'array', - items, - }; + array: (type: string, items: Expression[]): ArrayExpression => { + return { kind: 'array', type, items }; }, call: (functionName: string, args?: Expression[]): CallExpression => { - return { - kind: 'call', - function: functionName, - args, - }; + return { kind: 'call', function: functionName, args }; }, binary: (left: Expression, op: BinaryOperator, right: Expression, binding?: string): BinaryExpression => { - return { - kind: 'binary', - op, - left, - right, - binding, - }; + return { kind: 'binary', op, left, right, binding }; }, unary: (op: UnaryOperator, operand: Expression): UnaryExpression => { - return { - kind: 'unary', - op, - operand, - }; + return { kind: 'unary', op, operand }; }, field: (field: string): FieldExpression => { - return { - kind: 'field', - field, - }; + return { kind: 'field', field }; }, member: (receiver: Expression, members: string[]): MemberExpression => { - return { - kind: 'member', - receiver: receiver, - members, - }; + return { kind: 'member', receiver: receiver, members }; }, binding: (name: string): BindingExpression => { - return { - kind: 'binding', - name, - }; + return { kind: 'binding', name }; }, _this: (): ThisExpression => { - return { - kind: 'this', - }; + return { kind: 'this' }; }, _null: (): NullExpression => { - return { - kind: 'null', - }; + return { kind: 'null' }; }, and: (expr: Expression, ...expressions: Expression[]) => { diff --git a/packages/schema/src/expression.ts b/packages/schema/src/expression.ts index 1828b9cc3..754328c18 100644 --- a/packages/schema/src/expression.ts +++ b/packages/schema/src/expression.ts @@ -17,6 +17,7 @@ export type LiteralExpression = { export type ArrayExpression = { kind: 'array'; + type: string; items: Expression[]; }; diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index 16f81bae2..b60a1b050 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -1281,7 +1281,11 @@ export class TsSchemaGenerator { } private createArrayExpression(expr: ArrayExpr): any { + const arrayResolved = expr.$resolvedType?.decl; + const arrayType = typeof arrayResolved === 'string' ? arrayResolved : arrayResolved?.name; + invariant(arrayType, 'Array type must be resolved to a string or declaration'); return this.createExpressionUtilsCall('array', [ + this.createLiteralNode(arrayType), ts.factory.createArrayLiteralExpression(expr.items.map((item) => this.createExpression(item))), ]); } diff --git a/samples/next.js/zenstack/schema.ts b/samples/next.js/zenstack/schema.ts index 695ee053a..724c9659f 100644 --- a/samples/next.js/zenstack/schema.ts +++ b/samples/next.js/zenstack/schema.ts @@ -92,7 +92,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, authorId: { diff --git a/samples/nuxt/zenstack/schema.ts b/samples/nuxt/zenstack/schema.ts index 695ee053a..724c9659f 100644 --- a/samples/nuxt/zenstack/schema.ts +++ b/samples/nuxt/zenstack/schema.ts @@ -92,7 +92,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, authorId: { diff --git a/samples/orm/zenstack/schema.ts b/samples/orm/zenstack/schema.ts index 908df1e71..25ef6342a 100644 --- a/samples/orm/zenstack/schema.ts +++ b/samples/orm/zenstack/schema.ts @@ -122,7 +122,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "profile", fields: ["userId"], references: ["id"] } }, userId: { @@ -184,7 +184,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"] } }, authorId: { diff --git a/samples/sveltekit/src/zenstack/schema.ts b/samples/sveltekit/src/zenstack/schema.ts index 695ee053a..724c9659f 100644 --- a/samples/sveltekit/src/zenstack/schema.ts +++ b/samples/sveltekit/src/zenstack/schema.ts @@ -92,7 +92,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, authorId: { diff --git a/tests/e2e/apps/rally/zenstack/schema.ts b/tests/e2e/apps/rally/zenstack/schema.ts index d9ddfdb78..dd860a2b0 100644 --- a/tests/e2e/apps/rally/zenstack/schema.ts +++ b/tests/e2e/apps/rally/zenstack/schema.ts @@ -69,7 +69,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "accounts", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, accessTokenExpiresAt: { @@ -104,7 +104,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("accounts") }] } ], idFields: ["id"], @@ -333,7 +333,7 @@ export class SchemaType implements SchemaDef { name: "defaultDestinationCalendar", type: "ProviderCalendar", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UserDefaultDestinationCalendar") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("defaultDestinationCalendarId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UserDefaultDestinationCalendar") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("defaultDestinationCalendarId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "userDefaultDestination", name: "UserDefaultDestinationCalendar", fields: ["defaultDestinationCalendarId"], references: ["id"], onDelete: "SetNull" } }, sessions: { @@ -380,7 +380,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("identifier"), ExpressionUtils.field("token")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("identifier"), ExpressionUtils.field("token")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("verification_tokens") }] } ], idFields: ["token"], @@ -451,19 +451,19 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "sessions", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, impersonatedByUser: { name: "impersonatedByUser", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ImpersonatedSessions") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("impersonatedBy")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ImpersonatedSessions") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("impersonatedBy")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "impersonatedSessions", name: "ImpersonatedSessions", fields: ["impersonatedBy"], references: ["id"] } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("token")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("token")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("sessions") }] } ], idFields: ["id"], @@ -658,14 +658,14 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "polls", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, scheduledEvent: { name: "scheduledEvent", type: "ScheduledEvent", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduledEventId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("scheduledEventId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "polls", fields: ["scheduledEventId"], references: ["id"], onDelete: "SetNull" } }, options: { @@ -708,7 +708,7 @@ export class SchemaType implements SchemaDef { name: "space", type: "Space", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "polls", fields: ["spaceId"], references: ["id"], onDelete: "SetNull" } }, spaceId: { @@ -722,9 +722,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("guestId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("guestId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("polls") }] } ], idFields: ["id"], @@ -770,18 +770,18 @@ export class SchemaType implements SchemaDef { poll: { name: "poll", type: "Poll", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "watchers", fields: ["pollId"], references: ["id"], onDelete: "Cascade" } }, user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "watcher", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("watchers") }] } ], idFields: ["id"], @@ -876,19 +876,19 @@ export class SchemaType implements SchemaDef { poll: { name: "poll", type: "Poll", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "participants", fields: ["pollId"], references: ["id"], onDelete: "Cascade" } }, user: { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "participants", fields: ["userId"], references: ["id"], onDelete: "SetNull" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("participants") }] } ], idFields: ["id"], @@ -940,12 +940,12 @@ export class SchemaType implements SchemaDef { poll: { name: "poll", type: "Poll", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "options", fields: ["pollId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("options") }] } ], idFields: ["id"], @@ -1009,26 +1009,26 @@ export class SchemaType implements SchemaDef { participant: { name: "participant", type: "Participant", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("participantId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("participantId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "votes", fields: ["participantId"], references: ["id"], onDelete: "Cascade" } }, option: { name: "option", type: "Option", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("optionId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("optionId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "votes", fields: ["optionId"], references: ["id"], onDelete: "Cascade" } }, poll: { name: "poll", type: "Poll", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "votes", fields: ["pollId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("participantId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("optionId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("participantId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("optionId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("votes") }] } ], idFields: ["id"], @@ -1094,19 +1094,19 @@ export class SchemaType implements SchemaDef { poll: { name: "poll", type: "Poll", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "comments", fields: ["pollId"], references: ["id"], onDelete: "Cascade" } }, user: { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "comments", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("comments") }] } ], idFields: ["id"], @@ -1162,21 +1162,21 @@ export class SchemaType implements SchemaDef { poll: { name: "poll", type: "Poll", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "views", fields: ["pollId"], references: ["id"], onDelete: "Cascade" } }, user: { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "pollViews", fields: ["userId"], references: ["id"], onDelete: "SetNull" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("viewedAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("pollId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("viewedAt")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("poll_views") }] } ], idFields: ["id"], @@ -1232,7 +1232,7 @@ export class SchemaType implements SchemaDef { owner: { name: "owner", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UserSpaces") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UserSpaces") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "spaces", name: "UserSpaces", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, polls: { @@ -1268,7 +1268,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("ownerId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("spaces") }] } ], idFields: ["id"], @@ -1329,20 +1329,20 @@ export class SchemaType implements SchemaDef { space: { name: "space", type: "Space", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "members", fields: ["spaceId"], references: ["id"], onDelete: "Cascade" } }, user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "memberOf", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId"), ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }, { name: "map", value: ExpressionUtils.literal("space_members_space_id_idx") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }, { name: "map", value: ExpressionUtils.literal("space_members_user_id_idx") }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId"), ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }, { name: "map", value: ExpressionUtils.literal("space_members_space_id_idx") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }, { name: "map", value: ExpressionUtils.literal("space_members_user_id_idx") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("space_members") }] } ], idFields: ["id"], @@ -1402,19 +1402,19 @@ export class SchemaType implements SchemaDef { invitedBy: { name: "invitedBy", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("inviterId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("inviterId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "spaceMemberInvites", fields: ["inviterId"], references: ["id"], onDelete: "Cascade" } }, space: { name: "space", type: "Space", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "memberInvites", fields: ["spaceId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId"), ExpressionUtils.field("email")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }, { name: "map", value: ExpressionUtils.literal("space_member_invites_space_id_idx") }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId"), ExpressionUtils.field("email")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }, { name: "map", value: ExpressionUtils.literal("space_member_invites_space_id_idx") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("space_member_invites") }] } ], idFields: ["id"], @@ -1509,19 +1509,19 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UserToSubscription") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UserToSubscription") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "subscriptions", name: "UserToSubscription", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, space: { name: "space", type: "Space", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("SpaceToSubscription") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("SpaceToSubscription") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "subscriptions", name: "SpaceToSubscription", fields: ["spaceId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("subscriptions") }] } ], idFields: ["id"], @@ -1569,7 +1569,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "paymentMethods", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, @@ -1680,13 +1680,13 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "scheduledEvents", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, space: { name: "space", type: "Space", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "scheduledEvents", fields: ["spaceId"], references: ["id"], onDelete: "Cascade" } }, rescheduledDates: { @@ -1709,8 +1709,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "type", value: ExpressionUtils.literal("Hash") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("scheduled_events") }] } ], idFields: ["id"], @@ -1762,12 +1762,12 @@ export class SchemaType implements SchemaDef { scheduledEvent: { name: "scheduledEvent", type: "ScheduledEvent", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduledEventId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("scheduledEventId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "rescheduledDates", fields: ["scheduledEventId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduledEventId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("scheduledEventId")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("rescheduled_event_dates") }] } ], idFields: ["id"], @@ -1839,21 +1839,21 @@ export class SchemaType implements SchemaDef { scheduledEvent: { name: "scheduledEvent", type: "ScheduledEvent", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduledEventId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("scheduledEventId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "invites", fields: ["scheduledEventId"], references: ["id"], onDelete: "Cascade" } }, user: { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("inviteeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("inviteeId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "scheduledEventInvites", fields: ["inviteeId"], references: ["id"], onDelete: "SetNull" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduledEventId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("inviteeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("inviteeEmail")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("scheduledEventId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("inviteeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("inviteeEmail")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("scheduled_event_invites") }] } ], idFields: ["id"], @@ -1929,13 +1929,13 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "credentials", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }, { name: "name", value: ExpressionUtils.literal("user_provider_account_unique") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("expiresAt")]) }, { name: "name", value: ExpressionUtils.literal("credential_expiry_idx") }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId"), ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }, { name: "name", value: ExpressionUtils.literal("user_provider_account_unique") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("expiresAt")]) }, { name: "name", value: ExpressionUtils.literal("credential_expiry_idx") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("credentials") }] } ], idFields: ["id"], @@ -2001,7 +2001,7 @@ export class SchemaType implements SchemaDef { credential: { name: "credential", type: "Credential", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "calendarConnections", fields: ["credentialId"], references: ["id"], onDelete: "Cascade" } }, credentialId: { @@ -2015,7 +2015,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "calendarConnections", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, providerCalendars: { @@ -2026,7 +2026,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }, { name: "name", value: ExpressionUtils.literal("user_provider_account_unique") }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId"), ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }, { name: "name", value: ExpressionUtils.literal("user_provider_account_unique") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("calendar_connections") }] } ], idFields: ["id"], @@ -2119,7 +2119,7 @@ export class SchemaType implements SchemaDef { calendarConnection: { name: "calendarConnection", type: "CalendarConnection", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("calendarConnectionId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("calendarConnectionId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "providerCalendars", fields: ["calendarConnectionId"], references: ["id"], onDelete: "Cascade" } }, userDefaultDestination: { @@ -2131,10 +2131,10 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("calendarConnectionId"), ExpressionUtils.field("providerCalendarId")]) }, { name: "name", value: ExpressionUtils.literal("connection_calendar_unique") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("calendarConnectionId"), ExpressionUtils.field("isSelected")]) }, { name: "name", value: ExpressionUtils.literal("connection_selected_idx") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("isPrimary")]) }, { name: "name", value: ExpressionUtils.literal("primary_calendar_idx") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("lastSyncedAt")]) }, { name: "name", value: ExpressionUtils.literal("sync_time_idx") }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("calendarConnectionId"), ExpressionUtils.field("providerCalendarId")]) }, { name: "name", value: ExpressionUtils.literal("connection_calendar_unique") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("calendarConnectionId"), ExpressionUtils.field("isSelected")]) }, { name: "name", value: ExpressionUtils.literal("connection_selected_idx") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("isPrimary")]) }, { name: "name", value: ExpressionUtils.literal("primary_calendar_idx") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("lastSyncedAt")]) }, { name: "name", value: ExpressionUtils.literal("sync_time_idx") }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("provider_calendars") }] } ], idFields: ["id"], @@ -2280,7 +2280,7 @@ export class SchemaType implements SchemaDef { license: { name: "license", type: "License", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("licenseId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("licenseId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "validations", fields: ["licenseId"], references: ["id"], onDelete: "Cascade" } }, ipAddress: { diff --git a/tests/e2e/github-repos/cal.com/schema.ts b/tests/e2e/github-repos/cal.com/schema.ts index 47bb801a4..e77063c76 100644 --- a/tests/e2e/github-repos/cal.com/schema.ts +++ b/tests/e2e/github-repos/cal.com/schema.ts @@ -17,7 +17,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "hosts", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -31,7 +31,7 @@ export class SchemaType implements SchemaDef { eventType: { name: "eventType", type: "EventType", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "hosts", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, eventTypeId: { @@ -67,7 +67,7 @@ export class SchemaType implements SchemaDef { name: "schedule", type: "Schedule", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("scheduleId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "Host", fields: ["scheduleId"], references: ["id"] } }, scheduleId: { @@ -86,10 +86,10 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("scheduleId")]) }] } ], idFields: ["userId", "eventTypeId"], uniqueFields: { @@ -111,7 +111,7 @@ export class SchemaType implements SchemaDef { eventType: { name: "eventType", type: "EventType", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "calVideoSettings", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, disableRecordingForOrganizer: { @@ -239,7 +239,7 @@ export class SchemaType implements SchemaDef { name: "owner", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("owner") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("owner") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "ownedEventTypes", name: "owner", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -262,14 +262,14 @@ export class SchemaType implements SchemaDef { name: "profile", type: "Profile", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("profileId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("profileId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "eventTypes", fields: ["profileId"], references: ["id"] } }, team: { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "eventTypes", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -339,7 +339,7 @@ export class SchemaType implements SchemaDef { name: "parent", type: "EventType", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("managed_eventtype") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("managed_eventtype") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "children", name: "managed_eventtype", fields: ["parentId"], references: ["id"], onDelete: "Cascade" } }, children: { @@ -516,7 +516,7 @@ export class SchemaType implements SchemaDef { name: "schedule", type: "Schedule", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("scheduleId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "eventType", fields: ["scheduleId"], references: ["id"] } }, scheduleId: { @@ -608,7 +608,7 @@ export class SchemaType implements SchemaDef { name: "instantMeetingSchedule", type: "Schedule", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("InstantMeetingSchedule") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("instantMeetingScheduleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("InstantMeetingSchedule") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("instantMeetingScheduleId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "instantMeetingEvents", name: "InstantMeetingSchedule", fields: ["instantMeetingScheduleId"], references: ["id"] } }, instantMeetingParameters: { @@ -731,7 +731,7 @@ export class SchemaType implements SchemaDef { name: "secondaryEmail", type: "SecondaryEmail", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("secondaryEmailId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("secondaryEmailId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "eventTypes", fields: ["secondaryEmailId"], references: ["id"], onDelete: "Cascade" } }, useBookerTimezone: { @@ -752,21 +752,21 @@ export class SchemaType implements SchemaDef { name: "restrictionSchedule", type: "Schedule", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("restrictionSchedule") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("restrictionScheduleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("restrictionSchedule") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("restrictionScheduleId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "restrictionSchedule", name: "restrictionSchedule", fields: ["restrictionScheduleId"], references: ["id"] } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("slug")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("slug")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("parentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("profileId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("secondaryEmailId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("restrictionScheduleId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("slug")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("slug")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("parentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("profileId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("scheduleId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("secondaryEmailId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("restrictionScheduleId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -798,7 +798,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "credentials", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -813,7 +813,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "credentials", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -828,7 +828,7 @@ export class SchemaType implements SchemaDef { name: "app", type: "App", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "credentials", fields: ["appId"], references: ["slug"], onDelete: "Cascade" } }, appId: { @@ -897,15 +897,15 @@ export class SchemaType implements SchemaDef { name: "delegationCredential", type: "DelegationCredential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "credentials", fields: ["delegationCredentialId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("appId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("subscriptionId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("invalid")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("delegationCredentialId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("appId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("subscriptionId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("invalid")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("delegationCredentialId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -939,7 +939,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "destinationCalendar", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -962,7 +962,7 @@ export class SchemaType implements SchemaDef { name: "eventType", type: "EventType", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "destinationCalendar", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, eventTypeId: { @@ -987,14 +987,14 @@ export class SchemaType implements SchemaDef { name: "credential", type: "Credential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "destinationCalendars", fields: ["credentialId"], references: ["id"], onDelete: "Cascade" } }, delegationCredential: { name: "delegationCredential", type: "DelegationCredential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "destinationCalendar", fields: ["delegationCredentialId"], references: ["id"], onDelete: "Cascade" } }, delegationCredentialId: { @@ -1009,7 +1009,7 @@ export class SchemaType implements SchemaDef { name: "domainWideDelegation", type: "DomainWideDelegation", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("domainWideDelegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("domainWideDelegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "destinationCalendar", fields: ["domainWideDelegationCredentialId"], references: ["id"], onDelete: "Cascade" } }, domainWideDelegationCredentialId: { @@ -1022,9 +1022,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1053,7 +1053,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "password", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, @@ -1082,7 +1082,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "travelSchedules", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, timeZone: { @@ -1105,8 +1105,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("startDate")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("endDate")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("startDate")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("endDate")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1486,7 +1486,7 @@ export class SchemaType implements SchemaDef { name: "organization", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("scope") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("scope") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "orgUsers", name: "scope", fields: ["organizationId"], references: ["id"], onDelete: "SetNull" } }, accessCodes: { @@ -1556,7 +1556,7 @@ export class SchemaType implements SchemaDef { name: "movedToProfile", type: "Profile", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("moved_to_profile") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("movedToProfileId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("moved_to_profile") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("movedToProfileId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "movedFromUser", name: "moved_to_profile", fields: ["movedToProfileId"], references: ["id"], onDelete: "SetNull" } }, secondaryEmails: { @@ -1699,14 +1699,14 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("email")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("email"), ExpressionUtils.field("username")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("username"), ExpressionUtils.field("organizationId")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("movedToProfileId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("username")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("emailVerified")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("identityProvider")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("identityProviderId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("email")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("email"), ExpressionUtils.field("username")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("username"), ExpressionUtils.field("organizationId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("movedToProfileId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("username")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("emailVerified")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("IdentityProvider", [ExpressionUtils.field("identityProvider")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("identityProviderId")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("users") }] } ], idFields: ["id"], @@ -1738,7 +1738,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "NotificationsSubscriptions", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, subscription: { @@ -1747,7 +1747,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("subscription")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("subscription")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1778,7 +1778,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "profiles", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, organizationId: { @@ -1791,7 +1791,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "orgProfiles", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, username: { @@ -1825,11 +1825,11 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("organizationId")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("username"), ExpressionUtils.field("organizationId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("uid")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("organizationId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("username"), ExpressionUtils.field("organizationId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("uid")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1992,7 +1992,7 @@ export class SchemaType implements SchemaDef { name: "parent", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("organization") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("organization") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "children", name: "organization", fields: ["parentId"], references: ["id"], onDelete: "Cascade" } }, children: { @@ -2114,7 +2114,7 @@ export class SchemaType implements SchemaDef { name: "createdByOAuthClient", type: "PlatformOAuthClient", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedByOAuthClient") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdByOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedByOAuthClient") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("createdByOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "teams", name: "CreatedByOAuthClient", fields: ["createdByOAuthClientId"], references: ["id"], onDelete: "Cascade" } }, createdByOAuthClientId: { @@ -2230,8 +2230,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("slug"), ExpressionUtils.field("parentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug"), ExpressionUtils.field("parentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2253,7 +2253,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "creditBalance", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -2270,7 +2270,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "creditBalance", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -2339,7 +2339,7 @@ export class SchemaType implements SchemaDef { creditBalance: { name: "creditBalance", type: "CreditBalance", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("creditBalanceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("creditBalanceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "purchaseLogs", fields: ["creditBalanceId"], references: ["id"], onDelete: "Cascade" } }, credits: { @@ -2378,7 +2378,7 @@ export class SchemaType implements SchemaDef { creditBalance: { name: "creditBalance", type: "CreditBalance", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("creditBalanceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("creditBalanceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "expenseLogs", fields: ["creditBalanceId"], references: ["id"], onDelete: "Cascade" } }, bookingUid: { @@ -2393,7 +2393,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingUid")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("uid")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("bookingUid")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("uid")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "expenseLogs", fields: ["bookingUid"], references: ["uid"], onDelete: "Cascade" } }, credits: { @@ -2438,7 +2438,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "organizationSettings", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, organizationId: { @@ -2567,19 +2567,19 @@ export class SchemaType implements SchemaDef { name: "customRole", type: "Role", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("customRoleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("customRoleId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "memberships", fields: ["customRoleId"], references: ["id"] } }, team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "members", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "teams", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, disableImpersonation: { @@ -2610,12 +2610,12 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("accepted")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("role")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("customRoleId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("accepted")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("MembershipRole", [ExpressionUtils.field("role")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("customRoleId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2676,7 +2676,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "inviteTokens", fields: ["teamId"], references: ["id"] } }, secondaryEmailId: { @@ -2691,15 +2691,15 @@ export class SchemaType implements SchemaDef { name: "secondaryEmail", type: "SecondaryEmail", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("secondaryEmailId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("secondaryEmailId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "verificationTokens", fields: ["secondaryEmailId"], references: ["id"] } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("identifier"), ExpressionUtils.field("token")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("token")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("secondaryEmailId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("identifier"), ExpressionUtils.field("token")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("token")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("secondaryEmailId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2738,7 +2738,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "instantMeetingTokens", fields: ["teamId"], references: ["id"] } }, bookingId: { @@ -2755,7 +2755,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "instantMeetingToken", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, createdAt: { @@ -2772,7 +2772,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("token")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("token")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2823,7 +2823,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "references", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, bookingId: { @@ -2848,7 +2848,7 @@ export class SchemaType implements SchemaDef { name: "credential", type: "Credential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "references", fields: ["credentialId"], references: ["id"], onDelete: "SetNull" } }, credentialId: { @@ -2863,7 +2863,7 @@ export class SchemaType implements SchemaDef { name: "delegationCredential", type: "DelegationCredential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "bookingReferences", fields: ["delegationCredentialId"], references: ["id"], onDelete: "SetNull" } }, delegationCredentialId: { @@ -2878,7 +2878,7 @@ export class SchemaType implements SchemaDef { name: "domainWideDelegation", type: "DomainWideDelegation", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("domainWideDelegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("domainWideDelegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "bookingReferences", fields: ["domainWideDelegationCredentialId"], references: ["id"], onDelete: "SetNull" } }, domainWideDelegationCredentialId: { @@ -2891,9 +2891,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("type")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("uid")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("type")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("uid")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2938,7 +2938,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attendees", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, bookingId: { @@ -2964,8 +2964,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("email")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("email")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2999,7 +2999,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "bookings", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -3025,7 +3025,7 @@ export class SchemaType implements SchemaDef { name: "eventType", type: "EventType", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "bookings", fields: ["eventTypeId"], references: ["id"] } }, eventTypeId: { @@ -3109,7 +3109,7 @@ export class SchemaType implements SchemaDef { name: "destinationCalendar", type: "DestinationCalendar", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("destinationCalendarId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("destinationCalendarId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "booking", fields: ["destinationCalendarId"], references: ["id"] } }, destinationCalendarId: { @@ -3139,7 +3139,7 @@ export class SchemaType implements SchemaDef { name: "reassignBy", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("reassignByUser") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("reassignById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("reassignByUser") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("reassignById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "reassignedBookings", name: "reassignByUser", fields: ["reassignById"], references: ["id"] } }, reassignById: { @@ -3311,13 +3311,13 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("destinationCalendarId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("recurringEventId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("uid")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("status")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("startTime"), ExpressionUtils.field("endTime"), ExpressionUtils.field("status")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("destinationCalendarId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("recurringEventId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("uid")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("BookingStatus", [ExpressionUtils.field("status")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("startTime"), ExpressionUtils.field("endTime"), ExpressionUtils.field("status")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3347,7 +3347,7 @@ export class SchemaType implements SchemaDef { booking: { name: "booking", type: "Booking", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "tracking", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, utm_source: { @@ -3377,7 +3377,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3398,7 +3398,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "schedules", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -3451,7 +3451,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3472,7 +3472,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "availability", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -3487,7 +3487,7 @@ export class SchemaType implements SchemaDef { name: "eventType", type: "EventType", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "availability", fields: ["eventTypeId"], references: ["id"] } }, eventTypeId: { @@ -3523,7 +3523,7 @@ export class SchemaType implements SchemaDef { name: "Schedule", type: "Schedule", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("scheduleId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "availability", fields: ["scheduleId"], references: ["id"] } }, scheduleId: { @@ -3536,9 +3536,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("scheduleId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3558,7 +3558,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "selectedCalendars", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -3580,7 +3580,7 @@ export class SchemaType implements SchemaDef { name: "credential", type: "Credential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "selectedCalendars", fields: ["credentialId"], references: ["id"], onDelete: "Cascade" } }, credentialId: { @@ -3620,7 +3620,7 @@ export class SchemaType implements SchemaDef { name: "delegationCredential", type: "DelegationCredential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("delegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "selectedCalendars", fields: ["delegationCredentialId"], references: ["id"], onDelete: "Cascade" } }, delegationCredentialId: { @@ -3635,7 +3635,7 @@ export class SchemaType implements SchemaDef { name: "domainWideDelegationCredential", type: "DomainWideDelegation", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("domainWideDelegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("domainWideDelegationCredentialId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "selectedCalendars", fields: ["domainWideDelegationCredentialId"], references: ["id"], onDelete: "Cascade" } }, domainWideDelegationCredentialId: { @@ -3686,19 +3686,19 @@ export class SchemaType implements SchemaDef { name: "eventType", type: "EventType", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "selectedCalendars", fields: ["eventTypeId"], references: ["id"] } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("integration"), ExpressionUtils.field("externalId"), ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("googleChannelId"), ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("externalId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("integration"), ExpressionUtils.field("googleChannelExpiration"), ExpressionUtils.field("error"), ExpressionUtils.field("watchAttempts"), ExpressionUtils.field("maxAttempts")]) }, { name: "name", value: ExpressionUtils.literal("SelectedCalendar_watch_idx") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("integration"), ExpressionUtils.field("googleChannelExpiration"), ExpressionUtils.field("error"), ExpressionUtils.field("unwatchAttempts"), ExpressionUtils.field("maxAttempts")]) }, { name: "name", value: ExpressionUtils.literal("SelectedCalendar_unwatch_idx") }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("integration"), ExpressionUtils.field("externalId"), ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("googleChannelId"), ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("externalId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("integration"), ExpressionUtils.field("googleChannelExpiration"), ExpressionUtils.field("error"), ExpressionUtils.field("watchAttempts"), ExpressionUtils.field("maxAttempts")]) }, { name: "name", value: ExpressionUtils.literal("SelectedCalendar_watch_idx") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("integration"), ExpressionUtils.field("googleChannelExpiration"), ExpressionUtils.field("error"), ExpressionUtils.field("unwatchAttempts"), ExpressionUtils.field("maxAttempts")]) }, { name: "name", value: ExpressionUtils.literal("SelectedCalendar_unwatch_idx") }] } ], idFields: ["id"], uniqueFields: { @@ -3727,7 +3727,7 @@ export class SchemaType implements SchemaDef { eventType: { name: "eventType", type: "EventType", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "customInputs", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, label: { @@ -3755,7 +3755,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3828,8 +3828,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("referenceId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("reminderType")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("referenceId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("ReminderType", [ExpressionUtils.field("reminderType")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3856,7 +3856,7 @@ export class SchemaType implements SchemaDef { name: "app", type: "App", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "payments", fields: ["appId"], references: ["slug"], onDelete: "Cascade" } }, appId: { @@ -3878,7 +3878,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "payment", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, amount: { @@ -3920,8 +3920,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("externalId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("externalId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4002,35 +4002,35 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "webhooks", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, team: { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "webhooks", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, eventType: { name: "eventType", type: "EventType", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "webhooks", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, platformOAuthClient: { name: "platformOAuthClient", type: "PlatformOAuthClient", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "webhook", fields: ["platformOAuthClientId"], references: ["id"], onDelete: "Cascade" } }, app: { name: "app", type: "App", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "Webhook", fields: ["appId"], references: ["slug"], onDelete: "Cascade" } }, appId: { @@ -4070,9 +4070,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("subscriberUrl")]) }, { name: "name", value: ExpressionUtils.literal("courseIdentifier") }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("platformOAuthClientId"), ExpressionUtils.field("subscriberUrl")]) }, { name: "name", value: ExpressionUtils.literal("oauthclientwebhook") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("active")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("subscriberUrl")]) }, { name: "name", value: ExpressionUtils.literal("courseIdentifier") }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("platformOAuthClientId"), ExpressionUtils.field("subscriberUrl")]) }, { name: "name", value: ExpressionUtils.literal("oauthclientwebhook") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("active")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4100,13 +4100,13 @@ export class SchemaType implements SchemaDef { impersonatedUser: { name: "impersonatedUser", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("impersonated_user") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("impersonatedUserId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("impersonated_user") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("impersonatedUserId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "impersonatedUsers", name: "impersonated_user", fields: ["impersonatedUserId"], references: ["id"], onDelete: "Cascade" } }, impersonatedBy: { name: "impersonatedBy", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("impersonated_by_user") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("impersonatedById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("impersonated_by_user") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("impersonatedById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "impersonatedBy", name: "impersonated_by_user", fields: ["impersonatedById"], references: ["id"], onDelete: "Cascade" } }, impersonatedUserId: { @@ -4125,8 +4125,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("impersonatedUserId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("impersonatedById")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("impersonatedUserId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("impersonatedById")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4190,21 +4190,21 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "apiKeys", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, team: { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "apiKeys", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, app: { name: "app", type: "App", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("appId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "ApiKey", fields: ["appId"], references: ["slug"], onDelete: "Cascade" } }, appId: { @@ -4223,7 +4223,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4279,12 +4279,12 @@ export class SchemaType implements SchemaDef { apiKey: { name: "apiKey", type: "ApiKey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("apiKeyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("apiKeyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "rateLimits", fields: ["apiKeyId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("apiKeyId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("apiKeyId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4310,7 +4310,7 @@ export class SchemaType implements SchemaDef { eventType: { name: "eventType", type: "EventType", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "hashedLink", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, eventTypeId: { @@ -4403,14 +4403,14 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "accounts", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("type")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("type")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4449,12 +4449,12 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "sessions", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4532,7 +4532,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("enabled")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("enabled")]) }] } ], idFields: ["slug"], uniqueFields: { @@ -4590,14 +4590,14 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("routing-form") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("routing-form") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "routingForms", name: "routing-form", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, updatedBy: { name: "updatedBy", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("updated-routing-form") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("updatedById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("updated-routing-form") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("updatedById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "updatedRoutingForms", name: "updated-routing-form", fields: ["updatedById"], references: ["id"], onDelete: "SetNull" } }, updatedById: { @@ -4619,7 +4619,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "routingForms", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -4661,8 +4661,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("disabled")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("disabled")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4688,7 +4688,7 @@ export class SchemaType implements SchemaDef { form: { name: "form", type: "App_RoutingForms_Form", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "responses", fields: ["formId"], references: ["id"], onDelete: "Cascade" } }, formId: { @@ -4729,7 +4729,7 @@ export class SchemaType implements SchemaDef { name: "routedToBooking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("routedToBookingUid")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("uid")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("routedToBookingUid")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("uid")]) }] }], relation: { opposite: "routedFromRoutingFormReponse", fields: ["routedToBookingUid"], references: ["uid"] } }, chosenRouteId: { @@ -4757,10 +4757,10 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formFillerId"), ExpressionUtils.field("formId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formFillerId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("routedToBookingUid")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formFillerId"), ExpressionUtils.field("formId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formFillerId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("routedToBookingUid")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4782,7 +4782,7 @@ export class SchemaType implements SchemaDef { form: { name: "form", type: "App_RoutingForms_Form", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "queuedResponses", fields: ["formId"], references: ["id"], onDelete: "Cascade" } }, formId: { @@ -4828,7 +4828,7 @@ export class SchemaType implements SchemaDef { name: "actualResponse", type: "App_RoutingForms_FormResponse", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("actualResponseId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("actualResponseId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "queuedFormResponse", fields: ["actualResponseId"], references: ["id"], onDelete: "Cascade" } } }, @@ -4878,21 +4878,21 @@ export class SchemaType implements SchemaDef { response: { name: "response", type: "App_RoutingForms_FormResponse", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "map", value: ExpressionUtils.literal("RoutingFormResponseField_response_fkey") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "map", value: ExpressionUtils.literal("RoutingFormResponseField_response_fkey") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "routingFormResponseFields", fields: ["responseId"], references: ["id"], onDelete: "Cascade" } }, denormalized: { name: "denormalized", type: "RoutingFormResponseDenormalized", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("DenormalizedResponseToFields") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("DenormalizedResponseToFields") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "fields", name: "DenormalizedResponseToFields", fields: ["responseId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("fieldId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("valueNumber")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("valueStringArray")]) }, { name: "type", value: ExpressionUtils.literal("Gin") }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("responseId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("fieldId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Float", [ExpressionUtils.field("valueNumber")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("valueStringArray")]) }, { name: "type", value: ExpressionUtils.literal("Gin") }] } ], idFields: ["id"], uniqueFields: { @@ -5069,7 +5069,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "routingFormResponses", fields: ["bookingId"], references: ["id"], onDelete: "SetNull" } }, bookingUid: { @@ -5186,7 +5186,7 @@ export class SchemaType implements SchemaDef { response: { name: "response", type: "App_RoutingForms_FormResponse", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "routingFormResponses", fields: ["id"], references: ["id"], onDelete: "Cascade" } }, fields: { @@ -5198,13 +5198,13 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formTeamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formUserId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formId"), ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingUserId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId"), ExpressionUtils.field("eventTypeParentId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("formTeamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("formUserId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formId"), ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingUserId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId"), ExpressionUtils.field("eventTypeParentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5237,7 +5237,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "Feedback", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, rating: { @@ -5251,8 +5251,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("rating")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("rating")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5287,7 +5287,7 @@ export class SchemaType implements SchemaDef { workflow: { name: "workflow", type: "Workflow", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "steps", fields: ["workflowId"], references: ["id"], onDelete: "Cascade" } }, sendTo: { @@ -5346,7 +5346,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5385,14 +5385,14 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workflows", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, team: { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workflows", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -5443,8 +5443,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5464,7 +5464,7 @@ export class SchemaType implements SchemaDef { eventType: { name: "eventType", type: "EventType", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "aiPhoneCallConfig", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, eventTypeId: { @@ -5531,8 +5531,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5553,7 +5553,7 @@ export class SchemaType implements SchemaDef { workflow: { name: "workflow", type: "Workflow", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "activeOn", fields: ["workflowId"], references: ["id"], onDelete: "Cascade" } }, workflowId: { @@ -5566,7 +5566,7 @@ export class SchemaType implements SchemaDef { eventType: { name: "eventType", type: "EventType", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workflows", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, eventTypeId: { @@ -5578,9 +5578,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId"), ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId"), ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5601,7 +5601,7 @@ export class SchemaType implements SchemaDef { workflow: { name: "workflow", type: "Workflow", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "activeOnTeams", fields: ["workflowId"], references: ["id"], onDelete: "Cascade" } }, workflowId: { @@ -5614,7 +5614,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "activeOrgWorkflows", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -5626,9 +5626,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId"), ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId"), ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5702,7 +5702,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingUid")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("uid")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("bookingUid")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("uid")]) }] }], relation: { opposite: "workflowReminders", fields: ["bookingUid"], references: ["uid"] } }, method: { @@ -5736,7 +5736,7 @@ export class SchemaType implements SchemaDef { name: "workflowStep", type: "WorkflowStep", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowStepId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowStepId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workflowReminders", fields: ["workflowStepId"], references: ["id"], onDelete: "Cascade" } }, cancelled: { @@ -5764,11 +5764,11 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingUid")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workflowStepId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("seatReferenceId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("method"), ExpressionUtils.field("scheduled"), ExpressionUtils.field("scheduledDate")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("cancelled"), ExpressionUtils.field("scheduledDate")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("bookingUid")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workflowStepId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("seatReferenceId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("WorkflowMethods", [ExpressionUtils.field("method"), ExpressionUtils.field("scheduled"), ExpressionUtils.field("scheduledDate")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("cancelled"), ExpressionUtils.field("scheduledDate")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5834,7 +5834,7 @@ export class SchemaType implements SchemaDef { name: "webhook", type: "Webhook", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("webhookId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("webhookId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "scheduledTriggers", fields: ["webhookId"], references: ["id"], onDelete: "Cascade" } }, bookingId: { @@ -5849,7 +5849,7 @@ export class SchemaType implements SchemaDef { name: "booking", type: "Booking", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "scheduledTriggers", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } } }, @@ -5884,7 +5884,7 @@ export class SchemaType implements SchemaDef { booking: { name: "booking", type: "Booking", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "seatsReferences", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, attendeeId: { @@ -5899,7 +5899,7 @@ export class SchemaType implements SchemaDef { attendee: { name: "attendee", type: "Attendee", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attendeeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("attendeeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "bookingSeat", fields: ["attendeeId"], references: ["id"], onDelete: "Cascade" } }, data: { @@ -5914,8 +5914,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attendeeId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("attendeeId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5946,7 +5946,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "verifiedNumbers", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -5961,7 +5961,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "verifiedNumbers", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, phoneNumber: { @@ -5970,8 +5970,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -6000,7 +6000,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "verifiedEmails", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -6015,7 +6015,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "verifiedEmails", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, email: { @@ -6024,8 +6024,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -6106,8 +6106,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("enabled")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("stale")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("enabled")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("stale")]) }] } ], idFields: ["slug"], uniqueFields: { @@ -6120,7 +6120,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "features", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -6134,7 +6134,7 @@ export class SchemaType implements SchemaDef { feature: { name: "feature", type: "Feature", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("featureId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("featureId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "users", fields: ["featureId"], references: ["slug"], onDelete: "Cascade" } }, featureId: { @@ -6163,8 +6163,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("featureId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("featureId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("featureId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("featureId")]) }] } ], idFields: ["userId", "featureId"], uniqueFields: { @@ -6177,7 +6177,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "features", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -6191,7 +6191,7 @@ export class SchemaType implements SchemaDef { feature: { name: "feature", type: "Feature", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("featureId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("featureId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "teams", fields: ["featureId"], references: ["slug"], onDelete: "Cascade" } }, featureId: { @@ -6220,8 +6220,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("featureId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("featureId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("featureId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("featureId")]) }] } ], idFields: ["teamId", "featureId"], uniqueFields: { @@ -6270,7 +6270,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("slotUtcStartDate"), ExpressionUtils.field("slotUtcEndDate"), ExpressionUtils.field("uid")]) }, { name: "name", value: ExpressionUtils.literal("selectedSlotUnique") }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("slotUtcStartDate"), ExpressionUtils.field("slotUtcEndDate"), ExpressionUtils.field("uid")]) }, { name: "name", value: ExpressionUtils.literal("selectedSlotUnique") }] } ], idFields: ["id"], uniqueFields: { @@ -6343,7 +6343,7 @@ export class SchemaType implements SchemaDef { name: "client", type: "OAuthClient", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("clientId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("clientId")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("clientId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("clientId")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "accessCodes", fields: ["clientId"], references: ["clientId"], onDelete: "Cascade" } }, expiresAt: { @@ -6367,7 +6367,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "accessCodes", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -6382,7 +6382,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "accessCodes", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } } }, @@ -6636,16 +6636,16 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventParentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("startTime")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("endTime")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("status")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("isTeamBooking")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("isTeamBooking")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventParentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("startTime")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("endTime")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("BookingStatus", [ExpressionUtils.field("status")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("isTeamBooking")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("isTeamBooking")]) }] } ], idFields: ["id"], uniqueFields: { @@ -6821,14 +6821,14 @@ export class SchemaType implements SchemaDef { name: "credential", type: "Credential", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "CalendarCache", fields: ["credentialId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId"), ExpressionUtils.field("key")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("credentialId"), ExpressionUtils.field("key")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("key")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId"), ExpressionUtils.field("key")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("credentialId"), ExpressionUtils.field("key")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("key")]) }] } ], idFields: ["key", "credentialId"], uniqueFields: { @@ -6881,7 +6881,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("from"), ExpressionUtils.field("type"), ExpressionUtils.field("fromOrgId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("from"), ExpressionUtils.field("type"), ExpressionUtils.field("fromOrgId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -6923,7 +6923,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("userId"), ExpressionUtils.field("isBanner")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("userId"), ExpressionUtils.field("isBanner")]) }] }, { name: "@@map", args: [{ name: "name", value: ExpressionUtils.literal("avatars") }] } ], idFields: ["objectKey"], @@ -6971,7 +6971,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "bookingRedirects", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, toUserId: { @@ -6986,7 +6986,7 @@ export class SchemaType implements SchemaDef { name: "toUser", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("toUser") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("toUserId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("toUser") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("toUserId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "bookingRedirectsTo", name: "toUser", fields: ["toUserId"], references: ["id"], onDelete: "Cascade" } }, reasonId: { @@ -7001,7 +7001,7 @@ export class SchemaType implements SchemaDef { name: "reason", type: "OutOfOfficeReason", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("reasonId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("reasonId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "entries", fields: ["reasonId"], references: ["id"], onDelete: "SetNull" } }, createdAt: { @@ -7018,10 +7018,10 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("uuid")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("toUserId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("start"), ExpressionUtils.field("end")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("uuid")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("toUserId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("start"), ExpressionUtils.field("end")]) }] } ], idFields: ["id"], uniqueFields: { @@ -7067,7 +7067,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "OutOfOfficeReasons", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, entries: { @@ -7131,7 +7131,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "platformOAuthClient", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, teams: { @@ -7223,13 +7223,13 @@ export class SchemaType implements SchemaDef { owner: { name: "owner", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "PlatformAuthorizationToken", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, client: { name: "client", type: "PlatformOAuthClient", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "authorizationTokens", fields: ["platformOAuthClientId"], references: ["id"], onDelete: "Cascade" } }, platformOAuthClientId: { @@ -7254,7 +7254,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("platformOAuthClientId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("platformOAuthClientId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -7291,13 +7291,13 @@ export class SchemaType implements SchemaDef { owner: { name: "owner", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "AccessToken", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, client: { name: "client", type: "PlatformOAuthClient", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "accessTokens", fields: ["platformOAuthClientId"], references: ["id"], onDelete: "Cascade" } }, platformOAuthClientId: { @@ -7350,13 +7350,13 @@ export class SchemaType implements SchemaDef { owner: { name: "owner", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "RefreshToken", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, client: { name: "client", type: "PlatformOAuthClient", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("platformOAuthClientId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "refreshToken", fields: ["platformOAuthClientId"], references: ["id"], onDelete: "Cascade" } }, platformOAuthClientId: { @@ -7414,7 +7414,7 @@ export class SchemaType implements SchemaDef { name: "org", type: "OrganizationSettings", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "dSyncData", fields: ["organizationId"], references: ["organizationId"], onDelete: "Cascade" } }, teamGroupMapping: { @@ -7469,7 +7469,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "dsyncTeamGroupMapping", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, directoryId: { @@ -7482,7 +7482,7 @@ export class SchemaType implements SchemaDef { directory: { name: "directory", type: "DSyncData", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("directoryId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("directoryId")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("directoryId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("directoryId")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "teamGroupMapping", fields: ["directoryId"], references: ["directoryId"], onDelete: "Cascade" } }, groupName: { @@ -7491,7 +7491,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("groupName")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("groupName")]) }] } ], idFields: ["id"], uniqueFields: { @@ -7512,7 +7512,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "secondaryEmails", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -7545,9 +7545,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("email")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("email")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("email")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("email")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -7647,7 +7647,7 @@ export class SchemaType implements SchemaDef { managedOrganization: { name: "managedOrganization", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ManagedOrganization") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("managedOrganizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ManagedOrganization") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("managedOrganizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "managedOrganization", name: "ManagedOrganization", fields: ["managedOrganizationId"], references: ["id"], onDelete: "Cascade" } }, managerOrganizationId: { @@ -7660,7 +7660,7 @@ export class SchemaType implements SchemaDef { managerOrganization: { name: "managerOrganization", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ManagerOrganization") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("managerOrganizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ManagerOrganization") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("managerOrganizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "managedOrganizations", name: "ManagerOrganization", fields: ["managerOrganizationId"], references: ["id"], onDelete: "Cascade" } }, createdAt: { @@ -7671,8 +7671,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("managerOrganizationId"), ExpressionUtils.field("managedOrganizationId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("managerOrganizationId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("managerOrganizationId"), ExpressionUtils.field("managedOrganizationId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("managerOrganizationId")]) }] } ], idFields: ["managedOrganizationId"], uniqueFields: { @@ -7742,7 +7742,7 @@ export class SchemaType implements SchemaDef { name: "managerBilling", type: "PlatformBilling", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("PlatformManagedBilling") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("managerBillingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("PlatformManagedBilling") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("managerBillingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "managedBillings", name: "PlatformManagedBilling", fields: ["managerBillingId"], references: ["id"] } }, managedBillings: { @@ -7755,7 +7755,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "platformBilling", fields: ["id"], references: ["id"], onDelete: "Cascade" } } }, @@ -7777,7 +7777,7 @@ export class SchemaType implements SchemaDef { attribute: { name: "attribute", type: "Attribute", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attributeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attributeId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "options", fields: ["attributeId"], references: ["id"], onDelete: "Cascade" } }, attributeId: { @@ -7831,7 +7831,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attributes", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -7899,7 +7899,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -7920,7 +7920,7 @@ export class SchemaType implements SchemaDef { member: { name: "member", type: "Membership", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("memberId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("memberId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "AttributeToUser", fields: ["memberId"], references: ["id"], onDelete: "Cascade" } }, memberId: { @@ -7933,7 +7933,7 @@ export class SchemaType implements SchemaDef { attributeOption: { name: "attributeOption", type: "AttributeOption", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attributeOptionId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attributeOptionId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "assignedUsers", fields: ["attributeOptionId"], references: ["id"], onDelete: "Cascade" } }, attributeOptionId: { @@ -7966,7 +7966,7 @@ export class SchemaType implements SchemaDef { name: "createdBy", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("createdBy") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("createdBy") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "createdAttributeToUsers", name: "createdBy", fields: ["createdById"], references: ["id"], onDelete: "SetNull" } }, createdByDSyncId: { @@ -7981,7 +7981,7 @@ export class SchemaType implements SchemaDef { name: "createdByDSync", type: "DSyncData", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("createdByDSync") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdByDSyncId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("directoryId")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("createdByDSync") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("createdByDSyncId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("directoryId")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "createdAttributeToUsers", name: "createdByDSync", fields: ["createdByDSyncId"], references: ["directoryId"], onDelete: "SetNull" } }, updatedAt: { @@ -7995,7 +7995,7 @@ export class SchemaType implements SchemaDef { name: "updatedBy", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("updatedBy") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("updatedById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("updatedBy") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("updatedById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "updatedAttributeToUsers", name: "updatedBy", fields: ["updatedById"], references: ["id"], onDelete: "SetNull" } }, updatedById: { @@ -8018,12 +8018,12 @@ export class SchemaType implements SchemaDef { name: "updatedByDSync", type: "DSyncData", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("updatedByDSync") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("updatedByDSyncId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("directoryId")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("updatedByDSync") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("updatedByDSyncId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("directoryId")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "updatedAttributeToUsers", name: "updatedByDSync", fields: ["updatedByDSyncId"], references: ["directoryId"], onDelete: "SetNull" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("memberId"), ExpressionUtils.field("attributeOptionId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("memberId"), ExpressionUtils.field("attributeOptionId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8058,7 +8058,7 @@ export class SchemaType implements SchemaDef { booking: { name: "booking", type: "Booking", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "assignmentReason", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, reasonEnum: { @@ -8071,7 +8071,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8091,7 +8091,7 @@ export class SchemaType implements SchemaDef { workspacePlatform: { name: "workspacePlatform", type: "WorkspacePlatform", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workspacePlatformId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workspacePlatformId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "delegationCredentials", fields: ["workspacePlatformId"], references: ["id"], onDelete: "Cascade" } }, workspacePlatformId: { @@ -8131,7 +8131,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "delegationCredentials", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, domain: { @@ -8176,8 +8176,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId"), ExpressionUtils.field("domain")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("enabled")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("domain")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Boolean", [ExpressionUtils.field("enabled")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8198,7 +8198,7 @@ export class SchemaType implements SchemaDef { workspacePlatform: { name: "workspacePlatform", type: "WorkspacePlatform", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workspacePlatformId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("workspacePlatformId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "domainWideDelegations", fields: ["workspacePlatformId"], references: ["id"], onDelete: "Cascade" } }, workspacePlatformId: { @@ -8228,7 +8228,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "domainWideDelegations", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, domain: { @@ -8267,7 +8267,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId"), ExpressionUtils.field("domain")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("domain")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8333,7 +8333,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("slug")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("slug")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8354,7 +8354,7 @@ export class SchemaType implements SchemaDef { eventType: { name: "eventType", type: "EventType", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "fieldTranslations", fields: ["eventTypeId"], references: ["id"], onDelete: "Cascade" } }, eventTypeId: { @@ -8411,20 +8411,20 @@ export class SchemaType implements SchemaDef { creator: { name: "creator", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedEventTypeTranslations") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdBy")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedEventTypeTranslations") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("createdBy")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "createdTranslations", name: "CreatedEventTypeTranslations", fields: ["createdBy"], references: ["id"] } }, updater: { name: "updater", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UpdatedEventTypeTranslations") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("updatedBy")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UpdatedEventTypeTranslations") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("updatedBy")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "updatedTranslations", name: "UpdatedEventTypeTranslations", fields: ["updatedBy"], references: ["id"], onDelete: "SetNull" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId"), ExpressionUtils.field("field"), ExpressionUtils.field("targetLocale")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("eventTypeId"), ExpressionUtils.field("field"), ExpressionUtils.field("targetLocale")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId"), ExpressionUtils.field("field"), ExpressionUtils.field("targetLocale")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("eventTypeId"), ExpressionUtils.field("field"), ExpressionUtils.field("targetLocale")]) }] } ], idFields: ["uid"], uniqueFields: { @@ -8465,7 +8465,7 @@ export class SchemaType implements SchemaDef { createdBy: { name: "createdBy", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedWatchlists") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedWatchlists") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "createdWatchlists", name: "CreatedWatchlists", onDelete: "Cascade", fields: ["createdById"], references: ["id"] } }, createdById: { @@ -8485,7 +8485,7 @@ export class SchemaType implements SchemaDef { name: "updatedBy", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UpdatedWatchlists") }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("updatedById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("UpdatedWatchlists") }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("updatedById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "updatedWatchlists", name: "UpdatedWatchlists", onDelete: "SetNull", fields: ["updatedById"], references: ["id"] } }, updatedById: { @@ -8504,8 +8504,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("type"), ExpressionUtils.field("value")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("type"), ExpressionUtils.field("value")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("WatchlistType", [ExpressionUtils.field("type"), ExpressionUtils.field("value")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("WatchlistType", [ExpressionUtils.field("type"), ExpressionUtils.field("value")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8526,7 +8526,7 @@ export class SchemaType implements SchemaDef { createdBy: { name: "createdBy", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedOrganizationOnboardings") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CreatedOrganizationOnboardings") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "createdOrganizationOnboardings", name: "CreatedOrganizationOnboardings", fields: ["createdById"], references: ["id"], onDelete: "Cascade" } }, createdById: { @@ -8573,7 +8573,7 @@ export class SchemaType implements SchemaDef { name: "organization", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "organizationOnboarding", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, billingPeriod: { @@ -8655,8 +8655,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("orgOwnerEmail")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("stripeCustomerId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("orgOwnerEmail")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("stripeCustomerId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8679,7 +8679,7 @@ export class SchemaType implements SchemaDef { form: { name: "form", type: "App_RoutingForms_Form", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("formId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("formId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "incompleteBookingActions", fields: ["formId"], references: ["id"], onDelete: "Cascade" } }, formId: { @@ -8736,7 +8736,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "internalNotePresets", fields: ["teamId"], references: ["id"] } }, teamId: { @@ -8760,8 +8760,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("name")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("name")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8836,7 +8836,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "filterSegments", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -8850,7 +8850,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "filterSegments", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -8869,8 +8869,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scope"), ExpressionUtils.field("userId"), ExpressionUtils.field("tableIdentifier")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scope"), ExpressionUtils.field("teamId"), ExpressionUtils.field("tableIdentifier")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("FilterSegmentScope", [ExpressionUtils.field("scope"), ExpressionUtils.field("userId"), ExpressionUtils.field("tableIdentifier")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("FilterSegmentScope", [ExpressionUtils.field("scope"), ExpressionUtils.field("teamId"), ExpressionUtils.field("tableIdentifier")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8920,20 +8920,20 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "filterSegmentPreferences", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, segment: { name: "segment", type: "FilterSegment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("segmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("segmentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "userPreferences", fields: ["segmentId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("tableIdentifier")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("segmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId"), ExpressionUtils.field("tableIdentifier")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("segmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -8955,7 +8955,7 @@ export class SchemaType implements SchemaDef { name: "notePreset", type: "InternalNotePreset", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("notePresetId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("notePresetId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "BookingInternalNote", fields: ["notePresetId"], references: ["id"], onDelete: "Cascade" } }, notePresetId: { @@ -8974,7 +8974,7 @@ export class SchemaType implements SchemaDef { booking: { name: "booking", type: "Booking", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "internalNote", fields: ["bookingId"], references: ["id"], onDelete: "Cascade" } }, bookingId: { @@ -8987,7 +8987,7 @@ export class SchemaType implements SchemaDef { createdBy: { name: "createdBy", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("createdById")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "BookingInternalNote", fields: ["createdById"], references: ["id"] } }, createdById: { @@ -9005,8 +9005,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId"), ExpressionUtils.field("notePresetId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("bookingId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId"), ExpressionUtils.field("notePresetId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("bookingId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -9050,7 +9050,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("type"), ExpressionUtils.field("value")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("WorkflowContactType", [ExpressionUtils.field("type"), ExpressionUtils.field("value")]) }] } ], idFields: ["id"], uniqueFields: { @@ -9089,7 +9089,7 @@ export class SchemaType implements SchemaDef { name: "team", type: "Team", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "roles", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, permissions: { @@ -9124,8 +9124,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("name"), ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("name"), ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -9153,7 +9153,7 @@ export class SchemaType implements SchemaDef { role: { name: "role", type: "Role", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("roleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("roleId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "permissions", fields: ["roleId"], references: ["id"], onDelete: "Cascade" } }, resource: { @@ -9172,9 +9172,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("roleId"), ExpressionUtils.field("resource"), ExpressionUtils.field("action")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("roleId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("action")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("roleId"), ExpressionUtils.field("resource"), ExpressionUtils.field("action")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("roleId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("action")]) }] } ], idFields: ["id"], uniqueFields: { diff --git a/tests/e2e/github-repos/formbricks/schema.ts b/tests/e2e/github-repos/formbricks/schema.ts index f3e478760..fe29237a3 100644 --- a/tests/e2e/github-repos/formbricks/schema.ts +++ b/tests/e2e/github-repos/formbricks/schema.ts @@ -52,7 +52,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "webhooks", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, environmentId: { @@ -74,7 +74,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -106,7 +106,7 @@ export class SchemaType implements SchemaDef { attributeKey: { name: "attributeKey", type: "ContactAttributeKey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attributeKeyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attributeKeyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attributes", fields: ["attributeKeyId"], references: ["id"], onDelete: "Cascade" } }, attributeKeyId: { @@ -119,7 +119,7 @@ export class SchemaType implements SchemaDef { contact: { name: "contact", type: "Contact", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("contactId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("contactId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attributes", fields: ["contactId"], references: ["id"], onDelete: "Cascade" } }, contactId: { @@ -135,8 +135,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("contactId"), ExpressionUtils.field("attributeKeyId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attributeKeyId"), ExpressionUtils.field("value")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("contactId"), ExpressionUtils.field("attributeKeyId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attributeKeyId"), ExpressionUtils.field("value")]) }] } ], idFields: ["id"], uniqueFields: { @@ -195,7 +195,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attributeKeys", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, environmentId: { @@ -219,8 +219,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("key"), ExpressionUtils.field("environmentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("createdAt")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("key"), ExpressionUtils.field("environmentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -258,7 +258,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "contacts", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, environmentId: { @@ -288,7 +288,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -327,7 +327,7 @@ export class SchemaType implements SchemaDef { survey: { name: "survey", type: "Survey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "responses", fields: ["surveyId"], references: ["id"], onDelete: "Cascade" } }, surveyId: { @@ -341,7 +341,7 @@ export class SchemaType implements SchemaDef { name: "contact", type: "Contact", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("contactId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("contactId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "responses", fields: ["contactId"], references: ["id"], onDelete: "Cascade" } }, contactId: { @@ -428,16 +428,16 @@ export class SchemaType implements SchemaDef { name: "display", type: "Display", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("displayId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("displayId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "response", fields: ["displayId"], references: ["id"] } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId"), ExpressionUtils.field("singleUseId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId"), ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("contactId"), ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId"), ExpressionUtils.field("singleUseId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId"), ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("contactId"), ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -471,7 +471,7 @@ export class SchemaType implements SchemaDef { response: { name: "response", type: "Response", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "notes", fields: ["responseId"], references: ["id"], onDelete: "Cascade" } }, responseId: { @@ -484,7 +484,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "responseNotes", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -512,7 +512,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("responseId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -561,13 +561,13 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "tags", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("name")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("name")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -589,7 +589,7 @@ export class SchemaType implements SchemaDef { response: { name: "response", type: "Response", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "tags", fields: ["responseId"], references: ["id"], onDelete: "Cascade" } }, tagId: { @@ -603,13 +603,13 @@ export class SchemaType implements SchemaDef { tag: { name: "tag", type: "Tag", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("tagId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("tagId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "responses", fields: ["tagId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId"), ExpressionUtils.field("tagId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("responseId"), ExpressionUtils.field("tagId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("responseId")]) }] } ], idFields: ["responseId", "tagId"], uniqueFields: { @@ -641,7 +641,7 @@ export class SchemaType implements SchemaDef { survey: { name: "survey", type: "Survey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "displays", fields: ["surveyId"], references: ["id"], onDelete: "Cascade" } }, surveyId: { @@ -655,7 +655,7 @@ export class SchemaType implements SchemaDef { name: "contact", type: "Contact", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("contactId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("contactId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "displays", fields: ["contactId"], references: ["id"], onDelete: "Cascade" } }, contactId: { @@ -686,8 +686,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("contactId"), ExpressionUtils.field("createdAt")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("contactId"), ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -720,7 +720,7 @@ export class SchemaType implements SchemaDef { survey: { name: "survey", type: "Survey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "triggers", fields: ["surveyId"], references: ["id"], onDelete: "Cascade" } }, surveyId: { @@ -733,7 +733,7 @@ export class SchemaType implements SchemaDef { actionClass: { name: "actionClass", type: "ActionClass", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("actionClassId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("actionClassId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "surveyTriggers", fields: ["actionClassId"], references: ["id"], onDelete: "Cascade" } }, actionClassId: { @@ -745,8 +745,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId"), ExpressionUtils.field("actionClassId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId"), ExpressionUtils.field("actionClassId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -779,7 +779,7 @@ export class SchemaType implements SchemaDef { attributeKey: { name: "attributeKey", type: "ContactAttributeKey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attributeKeyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attributeKeyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attributeFilters", fields: ["attributeKeyId"], references: ["id"], onDelete: "Cascade" } }, attributeKeyId: { @@ -792,7 +792,7 @@ export class SchemaType implements SchemaDef { survey: { name: "survey", type: "Survey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attributeFilters", fields: ["surveyId"], references: ["id"], onDelete: "Cascade" } }, surveyId: { @@ -812,9 +812,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId"), ExpressionUtils.field("attributeKeyId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attributeKeyId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId"), ExpressionUtils.field("attributeKeyId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attributeKeyId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -862,7 +862,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "surveys", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, environmentId: { @@ -876,7 +876,7 @@ export class SchemaType implements SchemaDef { name: "creator", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdBy")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("createdBy")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "surveys", fields: ["createdBy"], references: ["id"] } }, createdBy: { @@ -909,7 +909,7 @@ export class SchemaType implements SchemaDef { name: "endings", type: "Json", array: true, - attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array([]) }] }], + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array("Any", []) }] }], default: [] }, thankYouCard: { @@ -1017,7 +1017,7 @@ export class SchemaType implements SchemaDef { name: "segment", type: "Segment", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("segmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("segmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "surveys", fields: ["segmentId"], references: ["id"] } }, projectOverwrites: { @@ -1109,8 +1109,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("updatedAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("segmentId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("updatedAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("segmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1143,7 +1143,7 @@ export class SchemaType implements SchemaDef { survey: { name: "survey", type: "Survey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "followUps", fields: ["surveyId"], references: ["id"], onDelete: "Cascade" } }, surveyId: { @@ -1219,7 +1219,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "actionClasses", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, environmentId: { @@ -1237,9 +1237,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("key"), ExpressionUtils.field("environmentId")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("name"), ExpressionUtils.field("environmentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("createdAt")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("key"), ExpressionUtils.field("environmentId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("name"), ExpressionUtils.field("environmentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1276,13 +1276,13 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "integration", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("type"), ExpressionUtils.field("environmentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("IntegrationType", [ExpressionUtils.field("type"), ExpressionUtils.field("environmentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1358,7 +1358,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "environments", fields: ["projectId"], references: ["id"], onDelete: "Cascade" } }, projectId: { @@ -1448,7 +1448,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1484,7 +1484,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "projects", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, organizationId: { @@ -1577,8 +1577,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId"), ExpressionUtils.field("name")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("name")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1670,7 +1670,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "memberships", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, organizationId: { @@ -1684,7 +1684,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "memberships", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -1714,9 +1714,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("organizationId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId"), ExpressionUtils.field("organizationId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }] } ], idFields: ["organizationId", "userId"], uniqueFields: { @@ -1745,7 +1745,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "invites", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, organizationId: { @@ -1758,7 +1758,7 @@ export class SchemaType implements SchemaDef { creator: { name: "creator", type: "User", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("inviteCreatedBy") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("creatorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("inviteCreatedBy") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("creatorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "invitesCreated", name: "inviteCreatedBy", fields: ["creatorId"], references: ["id"] } }, creatorId: { @@ -1772,7 +1772,7 @@ export class SchemaType implements SchemaDef { name: "acceptor", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("inviteAcceptedBy") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("acceptorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("inviteAcceptedBy") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("acceptorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "invitesAccepted", name: "inviteAcceptedBy", fields: ["acceptorId"], references: ["id"], onDelete: "Cascade" } }, acceptorId: { @@ -1808,13 +1808,13 @@ export class SchemaType implements SchemaDef { name: "teamIds", type: "String", array: true, - attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array([]) }] }], + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array("Any", []) }] }], default: [] } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("email"), ExpressionUtils.field("organizationId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("email"), ExpressionUtils.field("organizationId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1867,7 +1867,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "apiKeys", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, apiKeyEnvironments: { @@ -1884,7 +1884,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1924,7 +1924,7 @@ export class SchemaType implements SchemaDef { apiKey: { name: "apiKey", type: "ApiKey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("apiKeyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("apiKeyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "apiKeyEnvironments", fields: ["apiKeyId"], references: ["id"], onDelete: "Cascade" } }, environmentId: { @@ -1937,7 +1937,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "ApiKeyEnvironment", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, permission: { @@ -1946,8 +1946,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("apiKeyId"), ExpressionUtils.field("environmentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("apiKeyId"), ExpressionUtils.field("environmentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1981,7 +1981,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "accounts", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -2048,8 +2048,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("provider"), ExpressionUtils.field("providerAccountId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2216,7 +2216,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("email")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("email")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2311,7 +2311,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "segments", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, surveys: { @@ -2322,8 +2322,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("title")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("title")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2365,7 +2365,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "languages", fields: ["projectId"], references: ["id"], onDelete: "Cascade" } }, projectId: { @@ -2383,7 +2383,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("code")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("code")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2397,7 +2397,7 @@ export class SchemaType implements SchemaDef { language: { name: "language", type: "Language", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("languageId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("languageId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "surveyLanguages", fields: ["languageId"], references: ["id"], onDelete: "Cascade" } }, languageId: { @@ -2419,7 +2419,7 @@ export class SchemaType implements SchemaDef { survey: { name: "survey", type: "Survey", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "languages", fields: ["surveyId"], references: ["id"], onDelete: "Cascade" } }, default: { @@ -2436,9 +2436,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("languageId"), ExpressionUtils.field("surveyId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("languageId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("languageId"), ExpressionUtils.field("surveyId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("languageId")]) }] } ], idFields: ["languageId", "surveyId"], uniqueFields: { @@ -2477,7 +2477,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "insights", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, category: { @@ -2523,7 +2523,7 @@ export class SchemaType implements SchemaDef { document: { name: "document", type: "Document", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("documentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("documentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "documentInsights", fields: ["documentId"], references: ["id"], onDelete: "Cascade" } }, insightId: { @@ -2537,13 +2537,13 @@ export class SchemaType implements SchemaDef { insight: { name: "insight", type: "Insight", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("insightId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("insightId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "documentInsights", fields: ["insightId"], references: ["id"], onDelete: "Cascade" } } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("documentId"), ExpressionUtils.field("insightId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("insightId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("documentId"), ExpressionUtils.field("insightId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("insightId")]) }] } ], idFields: ["documentId", "insightId"], uniqueFields: { @@ -2582,7 +2582,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "Environment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "documents", fields: ["environmentId"], references: ["id"], onDelete: "Cascade" } }, surveyId: { @@ -2597,7 +2597,7 @@ export class SchemaType implements SchemaDef { name: "survey", type: "Survey", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("surveyId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "documents", fields: ["surveyId"], references: ["id"], onDelete: "Cascade" } }, responseId: { @@ -2612,7 +2612,7 @@ export class SchemaType implements SchemaDef { name: "response", type: "Response", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("responseId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "documents", fields: ["responseId"], references: ["id"], onDelete: "Cascade" } }, questionId: { @@ -2645,8 +2645,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("responseId"), ExpressionUtils.field("questionId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdAt")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("responseId"), ExpressionUtils.field("questionId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2690,7 +2690,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "teams", fields: ["organizationId"], references: ["id"], onDelete: "Cascade" } }, teamUsers: { @@ -2707,7 +2707,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId"), ExpressionUtils.field("name")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("name")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2741,7 +2741,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "teamUsers", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -2755,7 +2755,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "teamUsers", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, role: { @@ -2764,8 +2764,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId"), ExpressionUtils.field("userId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("teamId"), ExpressionUtils.field("userId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }] } ], idFields: ["teamId", "userId"], uniqueFields: { @@ -2798,7 +2798,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "projectTeams", fields: ["projectId"], references: ["id"], onDelete: "Cascade" } }, teamId: { @@ -2812,7 +2812,7 @@ export class SchemaType implements SchemaDef { team: { name: "team", type: "Team", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "projectTeams", fields: ["teamId"], references: ["id"], onDelete: "Cascade" } }, permission: { @@ -2823,8 +2823,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("teamId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("teamId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("teamId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("teamId")]) }] } ], idFields: ["projectId", "teamId"], uniqueFields: { diff --git a/tests/e2e/github-repos/trigger.dev/schema.ts b/tests/e2e/github-repos/trigger.dev/schema.ts index 12dab9e1b..29c733500 100644 --- a/tests/e2e/github-repos/trigger.dev/schema.ts +++ b/tests/e2e/github-repos/trigger.dev/schema.ts @@ -137,7 +137,7 @@ export class SchemaType implements SchemaDef { name: "invitationCode", type: "InvitationCode", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("invitationCodeId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("invitationCodeId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "users", fields: ["invitationCodeId"], references: ["id"] } }, invitationCodeId: { @@ -223,7 +223,7 @@ export class SchemaType implements SchemaDef { name: "personalAccessToken", type: "PersonalAccessToken", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("personalAccessTokenId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("personalAccessTokenId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "authorizationCodes", fields: ["personalAccessTokenId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, personalAccessTokenId: { @@ -284,7 +284,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "personalAccessTokens", fields: ["userId"], references: ["id"] } }, userId: { @@ -512,7 +512,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "members", fields: ["organizationId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, organizationId: { @@ -525,7 +525,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "orgMemberships", fields: ["userId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, userId: { @@ -561,7 +561,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId"), ExpressionUtils.field("userId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("userId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -599,7 +599,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "invites", fields: ["organizationId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, organizationId: { @@ -612,7 +612,7 @@ export class SchemaType implements SchemaDef { inviter: { name: "inviter", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("inviterId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("inviterId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "sentInvites", fields: ["inviterId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, inviterId: { @@ -636,7 +636,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId"), ExpressionUtils.field("email")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("email")]) }] } ], idFields: ["id"], uniqueFields: { @@ -692,7 +692,7 @@ export class SchemaType implements SchemaDef { name: "parentEnvironment", type: "RuntimeEnvironment", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("parentEnvironment") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("parentEnvironment") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("parentEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "childEnvironments", name: "parentEnvironment", fields: ["parentEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, parentEnvironmentId: { @@ -745,7 +745,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "environments", fields: ["organizationId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, organizationId: { @@ -758,7 +758,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "environments", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -772,7 +772,7 @@ export class SchemaType implements SchemaDef { name: "orgMember", type: "OrgMember", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("orgMemberId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("orgMemberId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "environments", fields: ["orgMemberId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, orgMemberId: { @@ -888,7 +888,7 @@ export class SchemaType implements SchemaDef { name: "currentSession", type: "RuntimeEnvironmentSession", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("currentSession") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("currentSessionId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("currentSession") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("currentSessionId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "currentEnvironments", name: "currentSession", fields: ["currentSessionId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, currentSessionId: { @@ -937,10 +937,10 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("slug"), ExpressionUtils.field("orgMemberId")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("shortcode")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentEnvironmentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("slug"), ExpressionUtils.field("orgMemberId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("shortcode")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("parentEnvironmentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -980,7 +980,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "projects", fields: ["organizationId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, organizationId: { @@ -1040,7 +1040,7 @@ export class SchemaType implements SchemaDef { name: "defaultWorkerGroup", type: "WorkerInstanceGroup", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ProjectDefaultWorkerGroup") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("defaultWorkerGroupId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ProjectDefaultWorkerGroup") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("defaultWorkerGroupId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "defaultForProjects", name: "ProjectDefaultWorkerGroup", fields: ["defaultWorkerGroupId"], references: ["id"] } }, defaultWorkerGroupId: { @@ -1272,7 +1272,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("key")]) }, { name: "type", value: ExpressionUtils.literal("BTree") }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("key")]) }, { name: "type", value: ExpressionUtils.literal("BTree") }] } ], idFields: ["key"], uniqueFields: { @@ -1360,7 +1360,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "backgroundWorkers", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -1373,7 +1373,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "backgroundWorkers", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -1443,7 +1443,7 @@ export class SchemaType implements SchemaDef { name: "workerGroup", type: "WorkerInstanceGroup", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerGroupId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerGroupId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "backgroundWorkers", fields: ["workerGroupId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, workerGroupId: { @@ -1462,9 +1462,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("version")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("createdAt")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("version")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1504,7 +1504,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "BackgroundWorkerFile", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -1534,7 +1534,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("contentHash")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("contentHash")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1580,7 +1580,7 @@ export class SchemaType implements SchemaDef { worker: { name: "worker", type: "BackgroundWorker", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "tasks", fields: ["workerId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, workerId: { @@ -1593,7 +1593,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "backgroundWorkerTasks", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -1607,7 +1607,7 @@ export class SchemaType implements SchemaDef { name: "file", type: "BackgroundWorkerFile", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("fileId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("fileId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "tasks", fields: ["fileId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, fileId: { @@ -1621,7 +1621,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "backgroundWorkerTasks", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -1682,7 +1682,7 @@ export class SchemaType implements SchemaDef { name: "queue", type: "TaskQueue", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("queueId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("queueId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "tasks", fields: ["queueId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, maxDurationInSeconds: { @@ -1698,9 +1698,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerId"), ExpressionUtils.field("slug")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("slug")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("projectId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerId"), ExpressionUtils.field("slug")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("slug")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("projectId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -1799,7 +1799,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRuns", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -1817,7 +1817,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRuns", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -1955,7 +1955,7 @@ export class SchemaType implements SchemaDef { name: "lockedBy", type: "BackgroundWorkerTask", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("lockedById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("lockedById")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "runs", fields: ["lockedById"], references: ["id"] } }, lockedById: { @@ -1970,7 +1970,7 @@ export class SchemaType implements SchemaDef { name: "lockedToVersion", type: "BackgroundWorker", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("lockedToVersionId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("lockedToVersionId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "lockedRuns", fields: ["lockedToVersionId"], references: ["id"] } }, lockedToVersionId: { @@ -2116,7 +2116,7 @@ export class SchemaType implements SchemaDef { name: "rootTaskRun", type: "TaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskRootRun") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("rootTaskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskRootRun") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("rootTaskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], relation: { opposite: "descendantRuns", name: "TaskRootRun", fields: ["rootTaskRunId"], references: ["id"], onDelete: "SetNull", onUpdate: "NoAction" } }, rootTaskRunId: { @@ -2138,7 +2138,7 @@ export class SchemaType implements SchemaDef { name: "parentTaskRun", type: "TaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskParentRun") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentTaskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskParentRun") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("parentTaskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], relation: { opposite: "childRuns", name: "TaskParentRun", fields: ["parentTaskRunId"], references: ["id"], onDelete: "SetNull", onUpdate: "NoAction" } }, parentTaskRunId: { @@ -2160,7 +2160,7 @@ export class SchemaType implements SchemaDef { name: "parentTaskRunAttempt", type: "TaskRunAttempt", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskParentRunAttempt") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentTaskRunAttemptId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskParentRunAttempt") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("parentTaskRunAttemptId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], relation: { opposite: "childRuns", name: "TaskParentRunAttempt", fields: ["parentTaskRunAttemptId"], references: ["id"], onDelete: "SetNull", onUpdate: "NoAction" } }, parentTaskRunAttemptId: { @@ -2175,7 +2175,7 @@ export class SchemaType implements SchemaDef { name: "batch", type: "BatchTaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("batchId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("batchId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("NoAction") }] }], relation: { opposite: "runs", fields: ["batchId"], references: ["id"], onDelete: "SetNull", onUpdate: "NoAction" } }, batchId: { @@ -2259,20 +2259,20 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("oneTimeUseToken")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("taskIdentifier"), ExpressionUtils.field("idempotencyKey")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentTaskRunId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("rootTaskRunId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spanId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("parentSpanId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("scheduleId"), ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runTags")]) }, { name: "type", value: ExpressionUtils.literal("Gin") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("batchId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("id")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdAt")]) }, { name: "type", value: ExpressionUtils.literal("Brin") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("status"), ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("createdAt"), ExpressionUtils.field("id")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("oneTimeUseToken")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("taskIdentifier"), ExpressionUtils.field("idempotencyKey")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("parentTaskRunId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("rootTaskRunId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("scheduleId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spanId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("parentSpanId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("scheduleId"), ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runTags")]) }, { name: "type", value: ExpressionUtils.literal("Gin") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("batchId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("id")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("createdAt")]) }, { name: "type", value: ExpressionUtils.literal("Brin") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("TaskRunStatus", [ExpressionUtils.field("status"), ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("createdAt"), ExpressionUtils.field("id")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2332,7 +2332,7 @@ export class SchemaType implements SchemaDef { run: { name: "run", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "executionSnapshots", fields: ["runId"], references: ["id"] } }, runStatus: { @@ -2351,7 +2351,7 @@ export class SchemaType implements SchemaDef { name: "batch", type: "BatchTaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("batchId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("batchId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "executionSnapshots", fields: ["batchId"], references: ["id"] } }, attemptNumber: { @@ -2369,7 +2369,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "executionSnapshots", fields: ["environmentId"], references: ["id"] } }, environmentType: { @@ -2386,7 +2386,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "executionSnapshots", fields: ["projectId"], references: ["id"] } }, organizationId: { @@ -2399,7 +2399,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "executionSnapshots", fields: ["organizationId"], references: ["id"] } }, completedWaitpoints: { @@ -2426,7 +2426,7 @@ export class SchemaType implements SchemaDef { name: "checkpoint", type: "TaskRunCheckpoint", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("checkpointId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("checkpointId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "executionSnapshot", fields: ["checkpointId"], references: ["id"] } }, workerId: { @@ -2441,7 +2441,7 @@ export class SchemaType implements SchemaDef { name: "worker", type: "WorkerInstance", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "TaskRunExecutionSnapshot", fields: ["workerId"], references: ["id"] } }, runnerId: { @@ -2473,7 +2473,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId"), ExpressionUtils.field("isValid"), ExpressionUtils.field("createdAt")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId"), ExpressionUtils.field("isValid"), ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2522,7 +2522,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRunCheckpoints", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -2535,7 +2535,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRunCheckpoints", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -2633,7 +2633,7 @@ export class SchemaType implements SchemaDef { name: "completedByTaskRun", type: "TaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CompletingRun") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("completedByTaskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CompletingRun") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("completedByTaskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "associatedWaitpoint", name: "CompletingRun", fields: ["completedByTaskRunId"], references: ["id"], onDelete: "SetNull" } }, completedAfter: { @@ -2653,7 +2653,7 @@ export class SchemaType implements SchemaDef { name: "completedByBatch", type: "BatchTaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("completedByBatchId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("completedByBatchId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }] }], relation: { opposite: "waitpoints", fields: ["completedByBatchId"], references: ["id"], onDelete: "SetNull" } }, blockingTaskRuns: { @@ -2696,7 +2696,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "waitpoints", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -2709,7 +2709,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "waitpoints", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -2738,10 +2738,10 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("idempotencyKey")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("completedByBatchId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("type"), ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("type"), ExpressionUtils.field("status")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("idempotencyKey")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("completedByBatchId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("type"), ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("type"), ExpressionUtils.field("status")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2764,7 +2764,7 @@ export class SchemaType implements SchemaDef { taskRun: { name: "taskRun", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "blockedByWaitpoints", fields: ["taskRunId"], references: ["id"] } }, taskRunId: { @@ -2777,7 +2777,7 @@ export class SchemaType implements SchemaDef { waitpoint: { name: "waitpoint", type: "Waitpoint", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("waitpointId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("waitpointId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "blockingTaskRuns", fields: ["waitpointId"], references: ["id"] } }, waitpointId: { @@ -2790,7 +2790,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRunWaitpoints", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -2817,7 +2817,7 @@ export class SchemaType implements SchemaDef { name: "batch", type: "BatchTaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("batchId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("batchId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "runsBlocked", fields: ["batchId"], references: ["id"] } }, batchIndex: { @@ -2839,9 +2839,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId"), ExpressionUtils.field("waitpointId"), ExpressionUtils.field("batchIndex")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("waitpointId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId"), ExpressionUtils.field("waitpointId"), ExpressionUtils.field("batchIndex")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("waitpointId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2866,7 +2866,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "waitpointTags", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -2879,7 +2879,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "waitpointTags", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -2897,7 +2897,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("name")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("name")]) }] } ], idFields: ["id"], uniqueFields: { @@ -2959,7 +2959,7 @@ export class SchemaType implements SchemaDef { workerGroup: { name: "workerGroup", type: "WorkerInstanceGroup", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerGroupId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerGroupId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "workers", fields: ["workerGroupId"], references: ["id"] } }, workerGroupId: { @@ -2979,7 +2979,7 @@ export class SchemaType implements SchemaDef { name: "organization", type: "Organization", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerInstances", fields: ["organizationId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, organizationId: { @@ -2994,7 +2994,7 @@ export class SchemaType implements SchemaDef { name: "project", type: "Project", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workers", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -3009,7 +3009,7 @@ export class SchemaType implements SchemaDef { name: "environment", type: "RuntimeEnvironment", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerInstances", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -3024,7 +3024,7 @@ export class SchemaType implements SchemaDef { name: "deployment", type: "WorkerDeployment", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("deploymentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("deploymentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerInstance", fields: ["deploymentId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, deploymentId: { @@ -3059,7 +3059,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerGroupId"), ExpressionUtils.field("resourceIdentifier")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerGroupId"), ExpressionUtils.field("resourceIdentifier")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3105,7 +3105,7 @@ export class SchemaType implements SchemaDef { token: { name: "token", type: "WorkerGroupToken", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("tokenId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("tokenId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerGroup", fields: ["tokenId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, tokenId: { @@ -3140,7 +3140,7 @@ export class SchemaType implements SchemaDef { name: "organization", type: "Organization", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerGroups", fields: ["organizationId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, organizationId: { @@ -3155,7 +3155,7 @@ export class SchemaType implements SchemaDef { name: "project", type: "Project", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerGroups", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -3256,7 +3256,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "runTags", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -3274,8 +3274,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("name")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("name"), ExpressionUtils.field("id")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("name")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("name"), ExpressionUtils.field("id")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3297,7 +3297,7 @@ export class SchemaType implements SchemaDef { taskRun: { name: "taskRun", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "dependency", fields: ["taskRunId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, taskRunId: { @@ -3313,7 +3313,7 @@ export class SchemaType implements SchemaDef { name: "checkpointEvent", type: "CheckpointRestoreEvent", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("checkpointEventId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("checkpointEventId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRunDependency", fields: ["checkpointEventId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, checkpointEventId: { @@ -3330,7 +3330,7 @@ export class SchemaType implements SchemaDef { name: "dependentAttempt", type: "TaskRunAttempt", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("dependentAttemptId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("dependentAttemptId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "dependencies", fields: ["dependentAttemptId"], references: ["id"] } }, dependentAttemptId: { @@ -3345,7 +3345,7 @@ export class SchemaType implements SchemaDef { name: "dependentBatchRun", type: "BatchTaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("dependentBatchRun") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("dependentBatchRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("dependentBatchRun") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("dependentBatchRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "runDependencies", name: "dependentBatchRun", fields: ["dependentBatchRunId"], references: ["id"] } }, dependentBatchRunId: { @@ -3375,8 +3375,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("dependentAttemptId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("dependentBatchRunId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("dependentAttemptId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("dependentBatchRunId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3423,7 +3423,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRunNumberCounter", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -3441,7 +3441,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskIdentifier"), ExpressionUtils.field("environmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskIdentifier"), ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3474,7 +3474,7 @@ export class SchemaType implements SchemaDef { taskRun: { name: "taskRun", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("attempts") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("attempts") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attempts", name: "attempts", fields: ["taskRunId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, taskRunId: { @@ -3487,7 +3487,7 @@ export class SchemaType implements SchemaDef { backgroundWorker: { name: "backgroundWorker", type: "BackgroundWorker", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("backgroundWorkerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("backgroundWorkerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attempts", fields: ["backgroundWorkerId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, backgroundWorkerId: { @@ -3500,7 +3500,7 @@ export class SchemaType implements SchemaDef { backgroundWorkerTask: { name: "backgroundWorkerTask", type: "BackgroundWorkerTask", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("backgroundWorkerTaskId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("backgroundWorkerTaskId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attempts", fields: ["backgroundWorkerTaskId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, backgroundWorkerTaskId: { @@ -3513,7 +3513,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskRunAttempts", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -3526,7 +3526,7 @@ export class SchemaType implements SchemaDef { queue: { name: "queue", type: "TaskQueue", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("queueId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("queueId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "attempts", fields: ["queueId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, queueId: { @@ -3631,8 +3631,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId"), ExpressionUtils.field("number")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId"), ExpressionUtils.field("number")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3901,9 +3901,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("traceId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spanId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("traceId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spanId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -3950,7 +3950,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskQueues", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -3963,7 +3963,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskQueues", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -4027,7 +4027,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("name")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("name")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4065,7 +4065,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "batchTaskRuns", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, status: { @@ -4103,7 +4103,7 @@ export class SchemaType implements SchemaDef { name: "runIds", type: "String", array: true, - attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array([]) }] }], + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array("Any", []) }] }], default: [] }, runCount: { @@ -4217,7 +4217,7 @@ export class SchemaType implements SchemaDef { name: "checkpointEvent", type: "CheckpointRestoreEvent", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("checkpointEventId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("checkpointEventId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "batchTaskRunDependency", fields: ["checkpointEventId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, checkpointEventId: { @@ -4234,7 +4234,7 @@ export class SchemaType implements SchemaDef { name: "dependentTaskAttempt", type: "TaskRunAttempt", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("dependentTaskAttemptId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("dependentTaskAttemptId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "batchDependencies", fields: ["dependentTaskAttemptId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, dependentTaskAttemptId: { @@ -4254,9 +4254,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("oneTimeUseToken")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("idempotencyKey")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("dependentTaskAttemptId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("oneTimeUseToken")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId"), ExpressionUtils.field("idempotencyKey")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("dependentTaskAttemptId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4286,7 +4286,7 @@ export class SchemaType implements SchemaDef { batchTaskRun: { name: "batchTaskRun", type: "BatchTaskRun", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("batchTaskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("batchTaskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "items", fields: ["batchTaskRunId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, batchTaskRunId: { @@ -4299,7 +4299,7 @@ export class SchemaType implements SchemaDef { taskRun: { name: "taskRun", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "batchItems", fields: ["taskRunId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, taskRunId: { @@ -4313,7 +4313,7 @@ export class SchemaType implements SchemaDef { name: "taskRunAttempt", type: "TaskRunAttempt", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunAttemptId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunAttemptId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "batchTaskRunItems", fields: ["taskRunAttemptId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, taskRunAttemptId: { @@ -4343,9 +4343,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("batchTaskRunId"), ExpressionUtils.field("taskRunId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunAttemptId")]) }, { name: "map", value: ExpressionUtils.literal("idx_batchtaskrunitem_taskrunattempt") }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }, { name: "map", value: ExpressionUtils.literal("idx_batchtaskrunitem_taskrun") }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("batchTaskRunId"), ExpressionUtils.field("taskRunId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunAttemptId")]) }, { name: "map", value: ExpressionUtils.literal("idx_batchtaskrunitem_taskrunattempt") }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }, { name: "map", value: ExpressionUtils.literal("idx_batchtaskrunitem_taskrun") }] } ], idFields: ["id"], uniqueFields: { @@ -4376,7 +4376,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "environmentVariables", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -4406,7 +4406,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("key")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("key")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4429,7 +4429,7 @@ export class SchemaType implements SchemaDef { name: "valueReference", type: "SecretReference", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("valueReferenceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("valueReferenceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "environmentVariableValues", fields: ["valueReferenceId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, valueReferenceId: { @@ -4443,7 +4443,7 @@ export class SchemaType implements SchemaDef { variable: { name: "variable", type: "EnvironmentVariable", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("variableId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("variableId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "values", fields: ["variableId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, variableId: { @@ -4456,7 +4456,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "environmentVariableValues", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -4486,7 +4486,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("variableId"), ExpressionUtils.field("environmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("variableId"), ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4541,7 +4541,7 @@ export class SchemaType implements SchemaDef { run: { name: "run", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "checkpoints", fields: ["runId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runId: { @@ -4554,7 +4554,7 @@ export class SchemaType implements SchemaDef { attempt: { name: "attempt", type: "TaskRunAttempt", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attemptId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attemptId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "checkpoints", fields: ["attemptId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, attemptId: { @@ -4572,7 +4572,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "checkpoints", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -4585,7 +4585,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "checkpoints", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -4609,8 +4609,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attemptId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attemptId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4645,7 +4645,7 @@ export class SchemaType implements SchemaDef { checkpoint: { name: "checkpoint", type: "Checkpoint", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("checkpointId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("checkpointId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "events", fields: ["checkpointId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, checkpointId: { @@ -4658,7 +4658,7 @@ export class SchemaType implements SchemaDef { run: { name: "run", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "CheckpointRestoreEvent", fields: ["runId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runId: { @@ -4671,7 +4671,7 @@ export class SchemaType implements SchemaDef { attempt: { name: "attempt", type: "TaskRunAttempt", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("attemptId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("attemptId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "CheckpointRestoreEvent", fields: ["attemptId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, attemptId: { @@ -4684,7 +4684,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "CheckpointRestoreEvent", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -4697,7 +4697,7 @@ export class SchemaType implements SchemaDef { runtimeEnvironment: { name: "runtimeEnvironment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runtimeEnvironmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "CheckpointRestoreEvent", fields: ["runtimeEnvironmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, runtimeEnvironmentId: { @@ -4733,8 +4733,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("checkpointId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("checkpointId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4800,7 +4800,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "WorkerDeployment", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -4813,7 +4813,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerDeployments", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -4827,7 +4827,7 @@ export class SchemaType implements SchemaDef { name: "worker", type: "BackgroundWorker", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "deployment", fields: ["workerId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, workerId: { @@ -4844,7 +4844,7 @@ export class SchemaType implements SchemaDef { name: "triggeredBy", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("triggeredById")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("triggeredById")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "deployments", fields: ["triggeredById"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, triggeredById: { @@ -4912,8 +4912,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("shortCode")]) }] }, - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("version")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("shortCode")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("version")]) }] } ], idFields: ["id"], uniqueFields: { @@ -4941,7 +4941,7 @@ export class SchemaType implements SchemaDef { deployment: { name: "deployment", type: "WorkerDeployment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("deploymentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("deploymentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "promotions", fields: ["deploymentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, deploymentId: { @@ -4954,7 +4954,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "workerDeploymentPromotions", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -4966,7 +4966,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId"), ExpressionUtils.field("label")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId"), ExpressionUtils.field("label")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5053,7 +5053,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskSchedules", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -5083,9 +5083,9 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("deduplicationKey")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("createdAt")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("deduplicationKey")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5107,7 +5107,7 @@ export class SchemaType implements SchemaDef { taskSchedule: { name: "taskSchedule", type: "TaskSchedule", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskScheduleId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskScheduleId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "instances", fields: ["taskScheduleId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, taskScheduleId: { @@ -5120,7 +5120,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "taskScheduleInstances", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -5160,7 +5160,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskScheduleId"), ExpressionUtils.field("environmentId")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskScheduleId"), ExpressionUtils.field("environmentId")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5185,7 +5185,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "sessions", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -5257,7 +5257,7 @@ export class SchemaType implements SchemaDef { name: "integration", type: "OrganizationIntegration", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("integrationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("integrationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("SetNull") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alertChannels", fields: ["integrationId"], references: ["id"], onDelete: "SetNull", onUpdate: "Cascade" } }, integrationId: { @@ -5295,13 +5295,13 @@ export class SchemaType implements SchemaDef { name: "environmentTypes", type: "RuntimeEnvironmentType", array: true, - attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array([ExpressionUtils.literal("STAGING"), ExpressionUtils.literal("PRODUCTION")]) }] }], + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.array("RuntimeEnvironmentType", [ExpressionUtils.literal("STAGING"), ExpressionUtils.literal("PRODUCTION")]) }] }], default: ["STAGING", "PRODUCTION"] }, project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alertChannels", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -5337,7 +5337,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId"), ExpressionUtils.field("deduplicationKey")]) }] } + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId"), ExpressionUtils.field("deduplicationKey")]) }] } ], idFields: ["id"], uniqueFields: { @@ -5365,7 +5365,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alerts", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -5378,7 +5378,7 @@ export class SchemaType implements SchemaDef { environment: { name: "environment", type: "RuntimeEnvironment", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("environmentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alerts", fields: ["environmentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, environmentId: { @@ -5391,7 +5391,7 @@ export class SchemaType implements SchemaDef { channel: { name: "channel", type: "ProjectAlertChannel", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("channelId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("channelId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alerts", fields: ["channelId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, channelId: { @@ -5415,7 +5415,7 @@ export class SchemaType implements SchemaDef { name: "taskRunAttempt", type: "TaskRunAttempt", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunAttemptId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunAttemptId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alerts", fields: ["taskRunAttemptId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, taskRunAttemptId: { @@ -5430,7 +5430,7 @@ export class SchemaType implements SchemaDef { name: "taskRun", type: "TaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("taskRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alerts", fields: ["taskRunId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, taskRunId: { @@ -5445,7 +5445,7 @@ export class SchemaType implements SchemaDef { name: "workerDeployment", type: "WorkerDeployment", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("workerDeploymentId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("workerDeploymentId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alerts", fields: ["workerDeploymentId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, workerDeploymentId: { @@ -5488,7 +5488,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alertStorages", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -5501,7 +5501,7 @@ export class SchemaType implements SchemaDef { alertChannel: { name: "alertChannel", type: "ProjectAlertChannel", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("alertChannelId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("alertChannelId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "alertStorages", fields: ["alertChannelId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, alertChannelId: { @@ -5568,7 +5568,7 @@ export class SchemaType implements SchemaDef { tokenReference: { name: "tokenReference", type: "SecretReference", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("tokenReferenceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("tokenReferenceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "OrganizationIntegration", fields: ["tokenReferenceId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, tokenReferenceId: { @@ -5581,7 +5581,7 @@ export class SchemaType implements SchemaDef { organization: { name: "organization", type: "Organization", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "organizationIntegrations", fields: ["organizationId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, organizationId: { @@ -5635,7 +5635,7 @@ export class SchemaType implements SchemaDef { project: { name: "project", type: "Project", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "bulkActionGroups", fields: ["projectId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, projectId: { @@ -5699,7 +5699,7 @@ export class SchemaType implements SchemaDef { group: { name: "group", type: "BulkActionGroup", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("groupId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("groupId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "items", fields: ["groupId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, groupId: { @@ -5722,7 +5722,7 @@ export class SchemaType implements SchemaDef { sourceRun: { name: "sourceRun", type: "TaskRun", - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("SourceActionItemRun") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("sourceRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("SourceActionItemRun") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("sourceRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "sourceBulkActionItems", name: "SourceActionItemRun", fields: ["sourceRunId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, sourceRunId: { @@ -5736,7 +5736,7 @@ export class SchemaType implements SchemaDef { name: "destinationRun", type: "TaskRun", optional: true, - attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("DestinationActionItemRun") }, { name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("destinationRunId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("DestinationActionItemRun") }, { name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("destinationRunId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "destinationBulkActionItems", name: "DestinationActionItemRun", fields: ["destinationRunId"], references: ["id"], onDelete: "Cascade", onUpdate: "Cascade" } }, destinationRunId: { @@ -5805,8 +5805,8 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("createdAt")]) }] } + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("DateTime", [ExpressionUtils.field("createdAt")]) }] } ], idFields: ["id"], uniqueFields: { @@ -6068,10 +6068,10 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("id"), ExpressionUtils.field("createdAt")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("traceId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spanId")]) }] }, - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("runId")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("id"), ExpressionUtils.field("createdAt")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("traceId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spanId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("runId")]) }] } ], idFields: ["id", "createdAt"], uniqueFields: { diff --git a/tests/e2e/orm/schemas/basic/schema.ts b/tests/e2e/orm/schemas/basic/schema.ts index 9d6cb1982..ce123db2d 100644 --- a/tests/e2e/orm/schemas/basic/schema.ts +++ b/tests/e2e/orm/schemas/basic/schema.ts @@ -118,7 +118,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, authorId: { @@ -175,7 +175,7 @@ export class SchemaType implements SchemaDef { name: "post", type: "Post", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("postId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("postId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "comments", fields: ["postId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, postId: { @@ -227,7 +227,7 @@ export class SchemaType implements SchemaDef { name: "user", type: "User", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "profile", fields: ["userId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, userId: { diff --git a/tests/e2e/orm/schemas/default-auth/schema.ts b/tests/e2e/orm/schemas/default-auth/schema.ts index 986173e6a..cdc982776 100644 --- a/tests/e2e/orm/schemas/default-auth/schema.ts +++ b/tests/e2e/orm/schemas/default-auth/schema.ts @@ -61,14 +61,14 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "profile", fields: ["userId"], references: ["id"], hasDefault: true } }, address: { name: "address", type: "Address", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("addressId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("addressId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "profile", fields: ["addressId"], references: ["id"] } }, addressId: { diff --git a/tests/e2e/orm/schemas/delegate/schema.ts b/tests/e2e/orm/schemas/delegate/schema.ts index d24bd749e..b3fabd6ea 100644 --- a/tests/e2e/orm/schemas/delegate/schema.ts +++ b/tests/e2e/orm/schemas/delegate/schema.ts @@ -72,7 +72,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("assetId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "comments", fields: ["assetId"], references: ["id"], onDelete: "Cascade" } }, assetId: { @@ -121,7 +121,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, ownerId: { @@ -145,7 +145,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("ownerId")]) }] }, + { name: "@@index", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("ownerId")]) }] }, { name: "@@delegate", args: [{ name: "discriminator", value: ExpressionUtils.field("assetType") }] } ], idFields: ["id"], @@ -192,7 +192,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, ownerId: { @@ -281,7 +281,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, ownerId: { @@ -332,7 +332,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("direct") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "ratedVideos", name: "direct", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -387,7 +387,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "assets", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, ownerId: { @@ -420,7 +420,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("galleryId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "images", fields: ["galleryId"], references: ["id"], onDelete: "Cascade" } }, galleryId: { diff --git a/tests/e2e/orm/schemas/name-mapping/schema.ts b/tests/e2e/orm/schemas/name-mapping/schema.ts index b92cd14b4..e39aeb951 100644 --- a/tests/e2e/orm/schemas/name-mapping/schema.ts +++ b/tests/e2e/orm/schemas/name-mapping/schema.ts @@ -67,7 +67,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"] } }, authorId: { diff --git a/tests/e2e/orm/schemas/omit/schema.ts b/tests/e2e/orm/schemas/omit/schema.ts index 60d51ad5c..29391a36e 100644 --- a/tests/e2e/orm/schemas/omit/schema.ts +++ b/tests/e2e/orm/schemas/omit/schema.ts @@ -58,7 +58,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onDelete: "Cascade" } }, authorId: { diff --git a/tests/e2e/orm/schemas/petstore/schema.ts b/tests/e2e/orm/schemas/petstore/schema.ts index 795946d59..feebb2972 100644 --- a/tests/e2e/orm/schemas/petstore/schema.ts +++ b/tests/e2e/orm/schemas/petstore/schema.ts @@ -78,7 +78,7 @@ export class SchemaType implements SchemaDef { name: "order", type: "Order", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("orderId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("orderId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "pets", fields: ["orderId"], references: ["id"] } }, orderId: { @@ -131,7 +131,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "orders", fields: ["userId"], references: ["id"] } }, userId: { diff --git a/tests/e2e/orm/schemas/todo/schema.ts b/tests/e2e/orm/schemas/todo/schema.ts index fff7ad070..2df7b731b 100644 --- a/tests/e2e/orm/schemas/todo/schema.ts +++ b/tests/e2e/orm/schemas/todo/schema.ts @@ -48,7 +48,7 @@ export class SchemaType implements SchemaDef { 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "ownedSpaces", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, ownerId: { @@ -109,7 +109,7 @@ export class SchemaType implements SchemaDef { space: { name: "space", type: "Space", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "members", fields: ["spaceId"], references: ["id"], onDelete: "Cascade" } }, spaceId: { @@ -122,7 +122,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "spaces", fields: ["userId"], references: ["id"], onDelete: "Cascade" } }, userId: { @@ -138,7 +138,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId"), ExpressionUtils.field("spaceId")]) }] }, + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId"), ExpressionUtils.field("spaceId")]) }] }, { name: "@@deny", args: [{ name: "operation", value: ExpressionUtils.literal("all") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.call("auth"), "==", ExpressionUtils._null()) }] }, { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("create,update,delete") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("space"), ["ownerId"]), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])), "||", ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("space"), ["members"]), "?", ExpressionUtils.binary(ExpressionUtils.binary(ExpressionUtils.field("userId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"])), "&&", ExpressionUtils.binary(ExpressionUtils.field("role"), "==", ExpressionUtils.literal("ADMIN"))))) }] }, { name: "@@allow", args: [{ name: "operation", value: ExpressionUtils.literal("read") }, { name: "condition", value: ExpressionUtils.binary(ExpressionUtils.member(ExpressionUtils.field("space"), ["members"]), "?", ExpressionUtils.binary(ExpressionUtils.field("userId"), "==", ExpressionUtils.member(ExpressionUtils.call("auth"), ["id"]))) }] } @@ -259,7 +259,7 @@ export class SchemaType implements SchemaDef { space: { name: "space", type: "Space", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("spaceId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "lists", fields: ["spaceId"], references: ["id"], onDelete: "Cascade" } }, spaceId: { @@ -272,7 +272,7 @@ export class SchemaType implements SchemaDef { owner: { name: "owner", type: "User", - 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "lists", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, ownerId: { @@ -344,7 +344,7 @@ export class SchemaType implements SchemaDef { owner: { name: "owner", type: "User", - 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") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "todos", fields: ["ownerId"], references: ["id"], onDelete: "Cascade" } }, ownerId: { @@ -357,7 +357,7 @@ export class SchemaType implements SchemaDef { list: { name: "list", type: "List", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("listId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("listId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "todos", fields: ["listId"], references: ["id"], onDelete: "Cascade" } }, listId: { diff --git a/tests/e2e/orm/schemas/typing/schema.ts b/tests/e2e/orm/schemas/typing/schema.ts index 4c82da67d..58325be74 100644 --- a/tests/e2e/orm/schemas/typing/schema.ts +++ b/tests/e2e/orm/schemas/typing/schema.ts @@ -113,7 +113,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"] } }, authorId: { @@ -159,7 +159,7 @@ export class SchemaType implements SchemaDef { name: "region", type: "Region", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("regionCountry"), ExpressionUtils.field("regionCity")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("regionCountry"), ExpressionUtils.field("regionCity")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] }], relation: { opposite: "profiles", fields: ["regionCountry", "regionCity"], references: ["country", "city"] } }, regionCountry: { @@ -181,7 +181,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "profile", fields: ["userId"], references: ["id"] } }, userId: { @@ -252,7 +252,7 @@ export class SchemaType implements SchemaDef { } }, attributes: [ - { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] } + { name: "@@id", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("country"), ExpressionUtils.field("city")]) }] } ], idFields: ["country", "city"], uniqueFields: { @@ -280,7 +280,7 @@ export class SchemaType implements SchemaDef { post: { name: "post", type: "Post", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("postId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("postId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "meta", fields: ["postId"], references: ["id"] } }, postId: { diff --git a/tests/regression/test/issue-422/schema.ts b/tests/regression/test/issue-422/schema.ts index 212279b05..32450381f 100644 --- a/tests/regression/test/issue-422/schema.ts +++ b/tests/regression/test/issue-422/schema.ts @@ -34,7 +34,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "sessions", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, @@ -101,7 +101,7 @@ export class SchemaType implements SchemaDef { user: { name: "user", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "profile", fields: ["userId"], references: ["id"], onDelete: "Cascade" } } }, diff --git a/tests/regression/test/issue-503/schema.ts b/tests/regression/test/issue-503/schema.ts index 0c8573144..702a67e37 100644 --- a/tests/regression/test/issue-503/schema.ts +++ b/tests/regression/test/issue-503/schema.ts @@ -46,7 +46,7 @@ export class SchemaType implements SchemaDef { chat: { name: "chat", type: "InternalChat", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("chatId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("chatId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "messages", fields: ["chatId"], references: ["id"] } }, chatId: { @@ -60,7 +60,7 @@ export class SchemaType implements SchemaDef { name: "media", type: "Media", optional: true, - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("mediaId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("mediaId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }], relation: { opposite: "messages", fields: ["mediaId"], references: ["id"] } }, mediaId: { diff --git a/tests/regression/test/issue-595.test.ts b/tests/regression/test/issue-595.test.ts new file mode 100644 index 000000000..8211aad69 --- /dev/null +++ b/tests/regression/test/issue-595.test.ts @@ -0,0 +1,122 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 595', () => { + it('verifies the issue', async () => { + const db = await createPolicyTestClient( + ` +enum Role { + admin @map('administrator') + editor + viewer + + @@map('roles') +} + +type AuthUser { + id String + roles Role[] + + @@auth +} + +model User { + id String @id @default(cuid()) + name String + email String? @unique + posts Post[] + profile Profile? + + @@allow('read', auth() != null) + @@allow('update', auth().id == this.id) + @@allow('all', has(auth().roles, admin)) +} + +model Profile { + id String @id @default(cuid()) + userId String @unique + bio String? + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@allow('read', auth() != null) + @@allow('create', userId == auth().id) + @@allow('update', userId == auth().id) + @@allow('all', has(auth().roles, admin)) +} + +model Post { + id String @id @default(cuid()) + title String + authorId String + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + + @@allow('read', auth() != null) + @@allow('create', authorId == auth().id) + @@allow('update', hasSome(auth().roles, [admin, editor])) +} + `, + { usePrismaPush: true, provider: 'postgresql' }, + ); + + const adminDb = db.$setAuth({ roles: ['admin'] }); + const userId = 'user-123'; + const userDb = db.$setAuth({ id: userId, roles: ['editor'] }); + const otherRoleDb = db.$setAuth({ roles: ['viewer'] }); + + await expect( + db.user.create({ + data: { id: 'user-123', name: 'User 123' }, + }), + ).toBeRejectedByPolicy(); + + await expect( + adminDb.user.create({ + data: { id: 'user-123', name: 'User 123' }, + }), + ).toResolveTruthy(); + + await userDb.user.update({ + data: { + profile: { + upsert: { + create: { bio: 'Hello' }, + update: { bio: 'Hello' }, + where: { userId }, + }, + }, + }, + where: { id: userId }, + }); + + const postId = 'post-123'; + + await expect( + userDb.post.create({ + data: { + id: postId, + title: 'First Post', + authorId: userId, + }, + }), + ).toResolveTruthy(); + + await expect( + otherRoleDb.post.update({ + where: { id: postId }, + data: { title: 'Updated Title' }, + }), + ).toBeRejectedNotFound(); + + await expect( + userDb.post.update({ + where: { id: postId }, + data: { title: 'Updated Title' }, + }), + ).toResolveTruthy(); + + await expect(userDb.user.delete({ where: { id: userId } })).toBeRejectedNotFound(); + + await expect(adminDb.user.delete({ where: { id: userId } })).toResolveTruthy(); + }); +}); diff --git a/tests/runtimes/bun/schemas/schema.ts b/tests/runtimes/bun/schemas/schema.ts index 199c89967..5dfb25346 100644 --- a/tests/runtimes/bun/schemas/schema.ts +++ b/tests/runtimes/bun/schemas/schema.ts @@ -72,7 +72,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, authorId: { diff --git a/tests/runtimes/edge-runtime/schemas/schema.ts b/tests/runtimes/edge-runtime/schemas/schema.ts index 13425189b..367e5f123 100644 --- a/tests/runtimes/edge-runtime/schemas/schema.ts +++ b/tests/runtimes/edge-runtime/schemas/schema.ts @@ -72,7 +72,7 @@ export class SchemaType implements SchemaDef { author: { name: "author", type: "User", - attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array([ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array([ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("String", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("String", [ExpressionUtils.field("id")]) }, { name: "onUpdate", value: ExpressionUtils.literal("Cascade") }, { name: "onDelete", value: ExpressionUtils.literal("Cascade") }] }], relation: { opposite: "posts", fields: ["authorId"], references: ["id"], onUpdate: "Cascade", onDelete: "Cascade" } }, authorId: { From 1d47af918f3a351fdafe3b1ebab80f017af68d71 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Thu, 29 Jan 2026 16:56:03 +0800 Subject: [PATCH 15/25] fix(orm): deal with node-pg timezone issue more reliably (#630) * fix(orm): deal with node-pg timezone issue more reliably fixes #606 * addressing PR comments --- .../src/client/crud/dialects/postgresql.ts | 61 +- .../client-api/timezone/pg-timezone.test.ts | 546 ++++++++++++++++++ 2 files changed, 589 insertions(+), 18 deletions(-) create mode 100644 tests/e2e/orm/client-api/timezone/pg-timezone.test.ts diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 9897bcccb..9ff53c2e6 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -10,7 +10,6 @@ import { } from 'kysely'; import { parse as parsePostgresArray } from 'postgres-array'; import { match } from 'ts-pattern'; -import z from 'zod'; import { AnyNullClass, DbNullClass, JsonNullClass } from '../../../common-types'; import type { BuiltinType, FieldDef, SchemaDef } from '../../../schema'; import type { SortOrder } from '../../crud-types'; @@ -20,16 +19,46 @@ import { isEnum, isTypeDef } from '../../query-utils'; import { LateralJoinDialectBase } from './lateral-join-dialect-base'; export class PostgresCrudDialect extends LateralJoinDialectBase { - private isoDateSchema = z.iso.datetime({ local: true, offset: true }); + private static typeParserOverrideApplied = false; constructor(schema: Schema, options: ClientOptions) { super(schema, options); + this.overrideTypeParsers(); } override get provider() { return 'postgresql' as const; } + private overrideTypeParsers() { + if (this.options.fixPostgresTimezone !== false && !PostgresCrudDialect.typeParserOverrideApplied) { + PostgresCrudDialect.typeParserOverrideApplied = true; + + // override node-pg's default type parser to resolve the timezone handling issue + // with "TIMESTAMP WITHOUT TIME ZONE" fields + // https://github.com/brianc/node-postgres/issues/429 + import('pg') + .then((pg) => { + pg.types.setTypeParser(pg.types.builtins.TIMESTAMP, (value) => { + if (typeof value !== 'string') { + return value; + } + if (!this.hasTimezoneOffset(value)) { + // force UTC if no offset + value += 'Z'; + } + const result = new Date(value); + return isNaN(result.getTime()) + ? value // fallback to original value if parsing fails + : result; + }); + }) + .catch(() => { + // ignore + }); + } + } + // #region capabilities override get supportsUpdateWithLimit(): boolean { @@ -165,27 +194,23 @@ export class PostgresCrudDialect extends LateralJoinDi private transformOutputDate(value: unknown) { if (typeof value === 'string') { - // PostgreSQL's jsonb_build_object serializes timestamp as ISO 8601 strings - // without timezone, (e.g., "2023-01-01T12:00:00.123456"). Since Date is always - // stored as UTC `timestamp` type, we add 'Z' to explicitly mark them as UTC for - // correct Date object creation. - if (this.isoDateSchema.safeParse(value).success) { - const hasOffset = value.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(value); - return new Date(hasOffset ? value : `${value}Z`); - } else { - return value; - } - } else if (value instanceof Date && this.options.fixPostgresTimezone !== false) { - // SPECIAL NOTES: - // node-pg has a terrible quirk that it returns the date value in local timezone - // as a `Date` object although for `DateTime` field the data in DB is stored in UTC - // see: https://github.com/brianc/node-postgres/issues/429 - return new Date(value.getTime() - value.getTimezoneOffset() * 60 * 1000); + // PostgreSQL's jsonb_build_object serializes timestamp as ISO 8601 strings, + // we force interpret them as UTC dates here if the value does not carry timezone + // offset (this happens with "TIMESTAMP WITHOUT TIME ZONE" field type) + const normalized = this.hasTimezoneOffset(value) ? value : `${value}Z`; + const parsed = new Date(normalized); + return Number.isNaN(parsed.getTime()) + ? value // fallback to original value if parsing fails + : parsed; } else { return value; } } + private hasTimezoneOffset(value: string) { + return value.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(value); + } + private transformOutputBytes(value: unknown) { return Buffer.isBuffer(value) ? Uint8Array.from(value) diff --git a/tests/e2e/orm/client-api/timezone/pg-timezone.test.ts b/tests/e2e/orm/client-api/timezone/pg-timezone.test.ts new file mode 100644 index 000000000..935ca818d --- /dev/null +++ b/tests/e2e/orm/client-api/timezone/pg-timezone.test.ts @@ -0,0 +1,546 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +describe('Timezone handling tests for postgres', () => { + describe.each([ + { + name: 'DateTime without timezone', + withTimezone: false, + }, + { + name: 'DateTime with timezone', + withTimezone: true, + }, + ])('$name', ({ withTimezone }) => { + const schema = ` +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + createdAt DateTime @default(now()) ${withTimezone ? '@db.Timestamptz' : ''} + posts Post[] +} + +model Post { + id Int @id @default(autoincrement()) + title String + publishedAt DateTime? ${withTimezone ? '@db.Timestamptz' : ''} + createdAt DateTime @default(now()) ${withTimezone ? '@db.Timestamptz' : ''} + authorId Int + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) +} + `; + + let client: any; + + beforeEach(async () => { + client = await createTestClient(schema, { + usePrismaPush: true, + provider: 'postgresql', + }); + }); + + afterEach(async () => { + await client?.$disconnect(); + }); + + it('returns DateTime fields as JS Date objects', async () => { + const testDate = new Date('2024-06-15T14:30:00.000Z'); + + const user = await client.user.create({ + data: { + email: 'user@test.com', + name: 'Test User', + }, + }); + + const post = await client.post.create({ + data: { + title: 'Test Post', + publishedAt: testDate, + authorId: user.id, + }, + }); + + expect(post.publishedAt).toBeInstanceOf(Date); + expect(post.createdAt).toBeInstanceOf(Date); + expect(user.createdAt).toBeInstanceOf(Date); + }); + + it('preserves exact datetime value on create and query', async () => { + const testDate = new Date('2024-06-15T14:30:00.000Z'); + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + const created = await client.post.create({ + data: { + title: 'Test Post', + publishedAt: testDate, + authorId: user.id, + }, + }); + + expect(created.publishedAt).toBeInstanceOf(Date); + expect(created.publishedAt.toISOString()).toBe(testDate.toISOString()); + + const queried = await client.post.findUnique({ + where: { id: created.id }, + }); + + expect(queried.publishedAt).toBeInstanceOf(Date); + expect(queried.publishedAt.toISOString()).toBe(testDate.toISOString()); + }); + + it('preserves exact datetime value with different timezone offsets', async () => { + // Test with a date that has a non-UTC timezone offset + // When created with '2024-06-15T10:30:00-04:00', it should be stored as '2024-06-15T14:30:00Z' + const testDate = new Date('2024-06-15T10:30:00-04:00'); + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + const created = await client.post.create({ + data: { + title: 'Test Post', + publishedAt: testDate, + authorId: user.id, + }, + }); + + expect(created.publishedAt).toBeInstanceOf(Date); + expect(created.publishedAt.getTime()).toBe(testDate.getTime()); + expect(created.publishedAt.toISOString()).toBe('2024-06-15T14:30:00.000Z'); + + const queried = await client.post.findUnique({ + where: { id: created.id }, + }); + + expect(queried.publishedAt).toBeInstanceOf(Date); + expect(queried.publishedAt.getTime()).toBe(testDate.getTime()); + expect(queried.publishedAt.toISOString()).toBe('2024-06-15T14:30:00.000Z'); + }); + + it('stores datetime as UTC in database', async () => { + const testDate = new Date('2024-06-15T14:30:00.000Z'); + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + const created = await client.post.create({ + data: { + title: 'Test Post', + publishedAt: testDate, + authorId: user.id, + }, + }); + + // Query the database directly using Kysely to verify UTC storage + const rawResult = await client.$qb + .selectFrom('Post') + .select(['publishedAt']) + .where('id', '=', created.id) + .executeTakeFirst(); + + // The raw value from database should be a Date object with UTC time + expect(rawResult.publishedAt).toBeInstanceOf(Date); + expect(rawResult.publishedAt.toISOString()).toBe('2024-06-15T14:30:00.000Z'); + }); + + it('handles multiple posts with different timezones correctly', async () => { + const dates = [ + new Date('2024-01-15T09:00:00Z'), // UTC + new Date('2024-02-15T09:00:00-05:00'), // EST + new Date('2024-03-15T09:00:00+09:00'), // JST + new Date('2024-04-15T09:00:00+02:00'), // CEST + ]; + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + const createdPosts: any[] = []; + for (let i = 0; i < dates.length; i++) { + const post = await client.post.create({ + data: { + title: `Post ${i + 1}`, + publishedAt: dates[i], + authorId: user.id, + }, + }); + createdPosts.push(post); + } + + // Verify each post + for (let i = 0; i < dates.length; i++) { + const queried = await client.post.findUnique({ + where: { id: createdPosts[i].id }, + }); + + expect(queried).toBeTruthy(); + expect(queried.publishedAt).toBeInstanceOf(Date); + expect(queried.publishedAt.getTime()).toBe(dates[i]?.getTime()); + } + }); + + it('handles date filtering correctly across timezones', async () => { + const baseDate = new Date('2024-06-15T12:00:00Z'); + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + await client.post.create({ + data: { + title: 'Before Post', + publishedAt: new Date(baseDate.getTime() - 3600000), // 1 hour before + authorId: user.id, + }, + }); + + const targetPost = await client.post.create({ + data: { + title: 'Target Post', + publishedAt: baseDate, + authorId: user.id, + }, + }); + + await client.post.create({ + data: { + title: 'After Post', + publishedAt: new Date(baseDate.getTime() + 3600000), // 1 hour after + authorId: user.id, + }, + }); + + // Query for posts published at exactly the base date + const found = await client.post.findMany({ + where: { + publishedAt: baseDate, + }, + }); + + expect(found).toHaveLength(1); + expect(found[0].id).toBe(targetPost.id); + }); + + it('handles update operations with timezone-aware dates', async () => { + const initialDate = new Date('2024-06-15T10:00:00Z'); + const updatedDate = new Date('2024-06-15T15:00:00-05:00'); // 20:00 UTC + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + const created = await client.post.create({ + data: { + title: 'Test Post', + publishedAt: initialDate, + authorId: user.id, + }, + }); + + const updated = await client.post.update({ + where: { id: created.id }, + data: { publishedAt: updatedDate }, + }); + + expect(updated.publishedAt).toBeInstanceOf(Date); + expect(updated.publishedAt.getTime()).toBe(updatedDate.getTime()); + expect(updated.publishedAt.toISOString()).toBe('2024-06-15T20:00:00.000Z'); + + const queried = await client.post.findUnique({ + where: { id: created.id }, + }); + + expect(queried.publishedAt.getTime()).toBe(updatedDate.getTime()); + }); + + it('returns DateTime fields correctly in nested relations (include)', async () => { + const userCreatedAt = new Date('2024-01-01T10:00:00Z'); + const postPublishedAt = new Date('2024-06-15T14:30:00-05:00'); + + // Create user with a specific createdAt using raw Kysely + const userResult = await client.$qb + .insertInto('User') + .values({ + email: 'user@test.com', + name: 'Test User', + // with query builder, when dealing with no-timezone timestamp, we need to + // pass ISO string to avoid any timezone conversion by pg + createdAt: withTimezone ? userCreatedAt : userCreatedAt.toISOString(), + }) + .returning(['id']) + .executeTakeFirstOrThrow(); + + const post = await client.post.create({ + data: { + title: 'Test Post', + publishedAt: postPublishedAt, + authorId: userResult.id, + }, + }); + + // Query post with author included + const postWithAuthor = await client.post.findUnique({ + where: { id: post.id }, + include: { author: true }, + }); + + expect(postWithAuthor.publishedAt).toBeInstanceOf(Date); + expect(postWithAuthor.publishedAt.getTime()).toBe(postPublishedAt.getTime()); + expect(postWithAuthor.createdAt).toBeInstanceOf(Date); + + expect(postWithAuthor.author.createdAt).toBeInstanceOf(Date); + expect(postWithAuthor.author.createdAt.toISOString()).toBe(userCreatedAt.toISOString()); + + // Query user with posts included + const userWithPosts = await client.user.findUnique({ + where: { id: userResult.id }, + include: { posts: true }, + }); + + expect(userWithPosts.createdAt).toBeInstanceOf(Date); + expect(userWithPosts.createdAt.toISOString()).toBe(userCreatedAt.toISOString()); + + expect(userWithPosts.posts).toHaveLength(1); + expect(userWithPosts.posts[0].publishedAt).toBeInstanceOf(Date); + expect(userWithPosts.posts[0].publishedAt.getTime()).toBe(postPublishedAt.getTime()); + expect(userWithPosts.posts[0].createdAt).toBeInstanceOf(Date); + }); + + it('returns DateTime fields correctly in nested create operations', async () => { + const publishedAt = new Date('2024-06-15T10:30:00-04:00'); + + const user = await client.user.create({ + data: { + email: 'user@test.com', + name: 'Test User', + posts: { + create: [ + { + title: 'Post 1', + publishedAt: publishedAt, + }, + { + title: 'Post 2', + publishedAt: new Date('2024-06-16T10:30:00+02:00'), + }, + ], + }, + }, + include: { posts: true }, + }); + + expect(user.createdAt).toBeInstanceOf(Date); + expect(user.posts).toHaveLength(2); + + expect(user.posts[0].publishedAt).toBeInstanceOf(Date); + expect(user.posts[0].publishedAt.getTime()).toBe(publishedAt.getTime()); + expect(user.posts[0].createdAt).toBeInstanceOf(Date); + + expect(user.posts[1].publishedAt).toBeInstanceOf(Date); + expect(user.posts[1].createdAt).toBeInstanceOf(Date); + }); + + it('handles findMany with nested includes and mixed timezones', async () => { + const user1Date = new Date('2024-01-01T08:00:00-05:00'); + const user2Date = new Date('2024-01-02T08:00:00+09:00'); + + // Create users with raw Kysely to control createdAt + const user1Result = await client.$qb + .insertInto('User') + .values({ + email: 'user1@test.com', + name: 'User 1', + createdAt: withTimezone ? user1Date : user1Date.toISOString(), + }) + .returning(['id']) + .executeTakeFirstOrThrow(); + + const user2Result = await client.$qb + .insertInto('User') + .values({ + email: 'user2@test.com', + name: 'User 2', + createdAt: withTimezone ? user2Date : user2Date.toISOString(), + }) + .returning(['id']) + .executeTakeFirstOrThrow(); + + await client.post.create({ + data: { + title: 'Post 1', + publishedAt: new Date('2024-06-15T10:00:00-07:00'), + authorId: user1Result.id, + }, + }); + + await client.post.create({ + data: { + title: 'Post 2', + publishedAt: new Date('2024-06-16T15:00:00+01:00'), + authorId: user2Result.id, + }, + }); + + const usersWithPosts = await client.user.findMany({ + include: { posts: true }, + orderBy: { id: 'asc' }, + }); + + expect(usersWithPosts).toHaveLength(2); + + expect(usersWithPosts[0].createdAt).toBeInstanceOf(Date); + expect(usersWithPosts[0].createdAt.getTime()).toBe(user1Date.getTime()); + expect(usersWithPosts[0].posts[0].publishedAt).toBeInstanceOf(Date); + expect(usersWithPosts[0].posts[0].createdAt).toBeInstanceOf(Date); + + expect(usersWithPosts[1].createdAt).toBeInstanceOf(Date); + expect(usersWithPosts[1].createdAt.getTime()).toBe(user2Date.getTime()); + expect(usersWithPosts[1].posts[0].publishedAt).toBeInstanceOf(Date); + expect(usersWithPosts[1].posts[0].createdAt).toBeInstanceOf(Date); + }); + + it('query builder: handles DateTime insert with different timezones', async () => { + const userCreatedAt = new Date('2024-01-15T08:00:00-05:00'); + const postPublishedAt = new Date('2024-06-15T15:30:00+09:00'); + + // Insert using query builder + const userResult = await client.$qb + .insertInto('User') + .values({ + email: 'qb-user@test.com', + name: 'QB User', + createdAt: withTimezone ? userCreatedAt : userCreatedAt.toISOString(), + }) + .returning(['id', 'createdAt']) + .executeTakeFirstOrThrow(); + + expect(userResult.createdAt).toBeInstanceOf(Date); + expect(userResult.createdAt.getTime()).toBe(userCreatedAt.getTime()); + + const postResult = await client.$qb + .insertInto('Post') + .values({ + title: 'QB Post', + publishedAt: withTimezone ? postPublishedAt : postPublishedAt.toISOString(), + authorId: userResult.id, + createdAt: withTimezone ? new Date() : new Date().toISOString(), + }) + .returning(['id', 'publishedAt', 'createdAt']) + .executeTakeFirstOrThrow(); + + expect(postResult.publishedAt).toBeInstanceOf(Date); + expect(postResult.publishedAt.getTime()).toBe(postPublishedAt.getTime()); + expect(postResult.publishedAt.toISOString()).toBe('2024-06-15T06:30:00.000Z'); + expect(postResult.createdAt).toBeInstanceOf(Date); + }); + + it('query builder: handles DateTime select and preserves timezone', async () => { + const testDate = new Date('2024-06-15T10:30:00-07:00'); + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + // Insert using query builder + const insertResult = await client.$qb + .insertInto('Post') + .values({ + title: 'QB Select Test', + publishedAt: withTimezone ? testDate : testDate.toISOString(), + authorId: user.id, + createdAt: withTimezone ? new Date() : new Date().toISOString(), + }) + .returning(['id']) + .executeTakeFirstOrThrow(); + + // Select using query builder + const selectResult = await client.$qb + .selectFrom('Post') + .select(['id', 'title', 'publishedAt', 'createdAt']) + .where('id', '=', insertResult.id) + .executeTakeFirstOrThrow(); + + expect(selectResult.publishedAt).toBeInstanceOf(Date); + expect(selectResult.publishedAt.getTime()).toBe(testDate.getTime()); + expect(selectResult.publishedAt.toISOString()).toBe('2024-06-15T17:30:00.000Z'); + expect(selectResult.createdAt).toBeInstanceOf(Date); + }); + + it('query builder: handles multiple DateTime inserts with mixed timezones', async () => { + const dates = [ + { createdAt: new Date('2024-01-15T09:00:00Z'), publishedAt: new Date('2024-06-15T10:00:00Z') }, + { + createdAt: new Date('2024-02-15T09:00:00-05:00'), + publishedAt: new Date('2024-06-16T10:00:00-05:00'), + }, + { + createdAt: new Date('2024-03-15T09:00:00+09:00'), + publishedAt: new Date('2024-06-17T10:00:00+09:00'), + }, + ]; + + const user = await client.user.create({ + data: { + email: 'user@test.com', + }, + }); + + const insertedIds: any[] = []; + for (const dateSet of dates) { + const result = await client.$qb + .insertInto('Post') + .values({ + title: 'Multi Insert Test', + publishedAt: withTimezone ? dateSet.publishedAt : dateSet.publishedAt.toISOString(), + authorId: user.id, + createdAt: withTimezone ? dateSet.createdAt : dateSet.createdAt.toISOString(), + }) + .returning(['id']) + .executeTakeFirstOrThrow(); + insertedIds.push(result.id); + } + + // Verify all records using query builder select + const results = await client.$qb + .selectFrom('Post') + .select(['id', 'publishedAt', 'createdAt']) + .where('id', 'in', insertedIds) + .orderBy('id', 'asc') + .execute(); + + expect(results).toHaveLength(3); + + for (let i = 0; i < results.length; i++) { + const row = results[i]; + const expectedDates = dates[i]; + expect(row.publishedAt).toBeTruthy(); + expect(row.publishedAt).toBeInstanceOf(Date); + expect((row.publishedAt as Date).getTime()).toBe(expectedDates!.publishedAt.getTime()); + expect(row.createdAt).toBeTruthy(); + expect(row.createdAt).toBeInstanceOf(Date); + expect((row.createdAt as Date).getTime()).toBe(expectedDates!.createdAt.getTime()); + } + }); + }); +}); From ab9535ea90890ef5f395645bf9543fbab4251ce4 Mon Sep 17 00:00:00 2001 From: Yiming Cao Date: Fri, 30 Jan 2026 13:38:10 +0800 Subject: [PATCH 16/25] fix(language): resolve mixin fields from imported files in scope (#598) (#632) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed issue where access policy rules couldn't reference fields inherited from mixins defined in separate imported files. The language service now correctly resolves these fields during scope computation. ## Root Cause The `getRecursiveBases()` function only searched for mixin declarations in the current document (`decl.$container.declarations`), which failed for imported mixins. ## Solution - Modified `getRecursiveBases()` to accept optional `LangiumDocuments` parameter - Implemented two-strategy approach: 1. Use resolved reference if available (post-linking) 2. Search by name across all documents including imports (pre-linking) - Updated `ZModelScopeComputation.processNode()` to pass `LangiumDocuments` - Leverages existing `getAllDeclarationsIncludingImports()` helper ## Changes - **packages/language/src/utils.ts**: Fixed `getRecursiveBases()` to search imported documents - **packages/language/src/zmodel-scope.ts**: Pass LangiumDocuments to scope computation - **packages/language/test/mixin.test.ts**: Added tests for imported mixin field resolution - **packages/testtools**: Added `extraZModelFiles` option for multi-file test schemas - **tests/regression/test/issue-598.test.ts**: Regression test for the issue ## Test Results ✅ All language package tests pass (65 tests) ✅ Regression test validates policy rules can access imported mixin fields ✅ Handles edge cases: cyclic imports, nested mixins, transitive imports Co-authored-by: Claude Sonnet 4.5 --- packages/language/src/utils.ts | 27 +++++-- packages/language/src/zmodel-scope.ts | 2 +- packages/language/test/mixin.test.ts | 99 ++++++++++++++++++++++++- packages/testtools/src/client.ts | 16 +++- packages/testtools/src/schema.ts | 14 ++++ tests/regression/test/issue-598.test.ts | 79 ++++++++++++++++++++ 6 files changed, 225 insertions(+), 12 deletions(-) create mode 100644 tests/regression/test/issue-598.test.ts diff --git a/packages/language/src/utils.ts b/packages/language/src/utils.ts index a99f160cb..bdd8baa6a 100644 --- a/packages/language/src/utils.ts +++ b/packages/language/src/utils.ts @@ -104,9 +104,7 @@ export function typeAssignable(destType: ExpressionType, sourceType: ExpressionT /** * Maps a ZModel builtin type to expression type */ -export function mapBuiltinTypeToExpressionType( - type: BuiltinType | ExpressionType, -): ExpressionType { +export function mapBuiltinTypeToExpressionType(type: BuiltinType | ExpressionType): ExpressionType { switch (type) { case 'Any': case 'Boolean': @@ -170,6 +168,7 @@ export function resolved(ref: Reference): T { export function getRecursiveBases( decl: DataModel | TypeDef, includeDelegate = true, + documents?: LangiumDocuments, seen = new Set(), ): (TypeDef | DataModel)[] { const result: (TypeDef | DataModel)[] = []; @@ -179,16 +178,28 @@ export function getRecursiveBases( seen.add(decl); const bases = [...decl.mixins, ...(isDataModel(decl) && decl.baseModel ? [decl.baseModel] : [])]; bases.forEach((base) => { - // avoid using .ref since this function can be called before linking - const baseDecl = decl.$container.declarations.find( - (d): d is TypeDef | DataModel => (isTypeDef(d) || isDataModel(d)) && d.name === base.$refText, - ); + let baseDecl: TypeDef | DataModel | undefined; + + if (base.ref && (isTypeDef(base.ref) || isDataModel(base.ref))) { + // base is already resolved + baseDecl = base.ref; + } else { + // otherwise, search by name, in all imported documents if provided + const declarations = documents + ? getAllDeclarationsIncludingImports(documents, decl.$container) + : decl.$container.declarations; + + baseDecl = declarations.find( + (d): d is TypeDef | DataModel => (isTypeDef(d) || isDataModel(d)) && d.name === base.$refText, + ); + } + if (baseDecl) { if (!includeDelegate && isDelegateModel(baseDecl)) { return; } result.push(baseDecl); - result.push(...getRecursiveBases(baseDecl, includeDelegate, seen)); + result.push(...getRecursiveBases(baseDecl, includeDelegate, documents, seen)); } }); return result; diff --git a/packages/language/src/zmodel-scope.ts b/packages/language/src/zmodel-scope.ts index 30b77e293..d05a30cfb 100644 --- a/packages/language/src/zmodel-scope.ts +++ b/packages/language/src/zmodel-scope.ts @@ -80,7 +80,7 @@ export class ZModelScopeComputation extends DefaultScopeComputation { super.processNode(node, document, scopes); if (isDataModel(node) || isTypeDef(node)) { // add base fields to the scope recursively - const bases = getRecursiveBases(node); + const bases = getRecursiveBases(node, true, this.services.shared.workspace.LangiumDocuments); for (const base of bases) { for (const field of base.fields) { scopes.add(node, this.descriptions.createDescription(field, this.nameProvider.getName(field))); diff --git a/packages/language/test/mixin.test.ts b/packages/language/test/mixin.test.ts index 8fa9e9335..1c4356d8b 100644 --- a/packages/language/test/mixin.test.ts +++ b/packages/language/test/mixin.test.ts @@ -1,6 +1,12 @@ +import { invariant } from '@zenstackhq/common-helpers'; +import fs from 'node:fs'; +import path from 'node:path'; +import tmp from 'tmp'; import { describe, expect, it } from 'vitest'; -import { loadSchema, loadSchemaWithError } from './utils'; +import { loadDocument } from '../src'; import { DataModel, TypeDef } from '../src/ast'; +import { getAllFields } from '../src/utils'; +import { loadSchema, loadSchemaWithError } from './utils'; describe('Mixin Tests', () => { it('supports model mixing types to Model', async () => { @@ -136,4 +142,95 @@ describe('Mixin Tests', () => { 'Type field cannot be a relation', ); }); + + it('supports mixin fields from imported file', async () => { + const { name } = tmp.dirSync(); + + fs.writeFileSync( + path.join(name, 'base.zmodel'), + ` +type Timestamped { + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + `, + ); + + fs.writeFileSync( + path.join(name, 'main.zmodel'), + ` +import './base' + +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} + +model Post with Timestamped { + id String @id + title String +} + `, + ); + + const model = await expectLoaded(path.join(name, 'main.zmodel')); + const post = model.declarations.find((d) => d.name === 'Post') as DataModel; + expect(post.mixins[0].ref?.name).toBe('Timestamped'); + + // Verify fields from imported mixin are accessible + const allFields = getAllFields(post); + expect(allFields.map((f) => f.name)).toContain('createdAt'); + expect(allFields.map((f) => f.name)).toContain('updatedAt'); + }); + + it('can reference imported mixin fields in policy rules', async () => { + const { name } = tmp.dirSync(); + + fs.writeFileSync( + path.join(name, 'base.zmodel'), + ` +type Owned { + ownerId String +} + `, + ); + + fs.writeFileSync( + path.join(name, 'main.zmodel'), + ` +import './base' + +datasource db { + provider = 'sqlite' + url = 'file:./dev.db' +} + +model User { + id String @id + @@auth() +} + +model Post with Owned { + id String @id + + @@allow('update', auth().id == ownerId) +} + `, + ); + + // If this loads without "Could not resolve reference" error, the fix works + const model = await expectLoaded(path.join(name, 'main.zmodel')); + expect(model).toBeDefined(); + }); + + async function expectLoaded(file: string) { + const pluginDocs = [path.resolve(__dirname, '../../plugins/policy/plugin.zmodel')]; + const result = await loadDocument(file, pluginDocs); + if (!result.success) { + console.error('Errors:', result.errors); + throw new Error(`Failed to load document from ${file}`); + } + invariant(result.success); + return result.model; + } }); diff --git a/packages/testtools/src/client.ts b/packages/testtools/src/client.ts index c82a6b780..89148c405 100644 --- a/packages/testtools/src/client.ts +++ b/packages/testtools/src/client.ts @@ -68,7 +68,12 @@ type ExtraTestClientOptions = { usePrismaPush?: boolean; /** - * Extra source files to create and compile. + * Extra ZModel files to be created in the working directory. + */ + extraZModelFiles?: Record; + + /** + * Extra TypeScript source files to create and compile. */ extraSourceFiles?: Record; @@ -126,7 +131,14 @@ export async function createTestClient( let model: Model | undefined; if (typeof schema === 'string') { - const generated = await generateTsSchema(schema, provider, dbUrl, options?.extraSourceFiles, undefined); + const generated = await generateTsSchema( + schema, + provider, + dbUrl, + options?.extraSourceFiles, + undefined, + options?.extraZModelFiles, + ); workDir = generated.workDir; model = generated.model; // replace schema's provider diff --git a/packages/testtools/src/schema.ts b/packages/testtools/src/schema.ts index 44f4483c9..ffd016b11 100644 --- a/packages/testtools/src/schema.ts +++ b/packages/testtools/src/schema.ts @@ -61,6 +61,7 @@ export async function generateTsSchema( dbUrl?: string, extraSourceFiles?: Record, withLiteSchema?: boolean, + extraZModelFiles?: Record, ) { const workDir = createTestProject(); @@ -71,6 +72,19 @@ export async function generateTsSchema( `${noPrelude ? '' : makePrelude(provider, dbUrl)}\n\n${replacePlaceholders(schemaText, provider, dbUrl)}`, ); + // write extra ZModel files before loading the schema + if (extraZModelFiles) { + for (const [fileName, content] of Object.entries(extraZModelFiles)) { + let name = fileName; + if (!name.endsWith('.zmodel')) { + name += '.zmodel'; + } + const filePath = path.join(workDir, name); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, content); + } + } + const result = await loadDocumentWithPlugins(zmodelPath); if (!result.success) { throw new Error(`Failed to load schema from ${zmodelPath}: ${result.errors}`); diff --git a/tests/regression/test/issue-598.test.ts b/tests/regression/test/issue-598.test.ts new file mode 100644 index 000000000..c7e680dc2 --- /dev/null +++ b/tests/regression/test/issue-598.test.ts @@ -0,0 +1,79 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue 598', () => { + it('access policy can reference mixin fields from imported files', async () => { + const db = await createPolicyTestClient( + ` +import './mixins' + +datasource db { + provider = 'postgresql' + url = '$DB_URL' +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + documents Document[] + + @@allow('all', true) +} + +model Document with AuditMixin { + id String @id @default(cuid()) + title String + ownerId Int + owner User @relation(fields: [ownerId], references: [id]) + + @@allow('create,read', auth() != null) + @@allow('update', auth().id == createdById) // createdById from mixin +} + `, + { + extraZModelFiles: { + mixins: ` +type AuditMixin { + createdById Int + createdAt DateTime @default(now()) +} + `, + }, + }, + ); + + // Test that the policy rule using mixin field works correctly + const userDb = db.$setAuth({ id: 1 }); + const otherUserDb = db.$setAuth({ id: 2 }); + + // Create users first + await db.user.create({ data: { id: 1, email: 'user1@test.com' } }); + await db.user.create({ data: { id: 2, email: 'user2@test.com' } }); + + // Create document as user 1 + await userDb.document.create({ + data: { + id: 'doc-1', + title: 'Test Document', + ownerId: 1, + createdById: 1, // From mixin + }, + }); + + // User 1 should be able to update (matches createdById) + await expect( + userDb.document.update({ + where: { id: 'doc-1' }, + data: { title: 'Updated' }, + }), + ).toResolveTruthy(); + + // User 2 should NOT be able to update (different createdById) + await expect( + otherUserDb.document.update({ + where: { id: 'doc-1' }, + data: { title: 'Hacked' }, + }), + ).toBeRejectedNotFound(); + }); +}); From c5da34924cc1d6c70151c0944b0818ed0691fb93 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 30 Jan 2026 13:39:45 +0800 Subject: [PATCH 17/25] feat(cli): add dotenv support (#629) * feat(cli): add dotenv support * [WIP] Update dotenv support implementation in CLI based on review (#631) * Initial plan * fix(cli): move dotenv import to top of file Co-authored-by: jiashengguo <16688722+jiashengguo@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jiashengguo <16688722+jiashengguo@users.noreply.github.com> --------- Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> --- packages/cli/package.json | 5 +++-- packages/cli/src/index.ts | 1 + pnpm-lock.yaml | 9 ++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 1149b35d8..9d6ea65a5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -46,18 +46,19 @@ "colors": "1.4.0", "commander": "^8.3.0", "cors": "^2.8.5", + "dotenv": "^17.2.3", "execa": "^9.6.0", "express": "^5.0.0", "jiti": "^2.6.1", "langium": "catalog:", "mixpanel": "^0.18.1", + "mysql2": "catalog:", "ora": "^5.4.1", "package-manager-detector": "^1.3.0", "pg": "catalog:", "prisma": "catalog:", "semver": "^7.7.2", - "ts-pattern": "catalog:", - "mysql2": "catalog:" + "ts-pattern": "catalog:" }, "devDependencies": { "@types/better-sqlite3": "catalog:", diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 836823f06..82d0e736c 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,3 +1,4 @@ +import 'dotenv/config'; import { ZModelLanguageMetaData } from '@zenstackhq/language'; import colors from 'colors'; import { Command, CommanderError, Option } from 'commander'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0e4080566..3f519e5c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -222,6 +222,9 @@ importers: cors: specifier: ^2.8.5 version: 2.8.5 + dotenv: + specifier: ^17.2.3 + version: 17.2.3 execa: specifier: ^9.6.0 version: 9.6.0 @@ -12977,7 +12980,7 @@ snapshots: eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-jsx-a11y: 6.10.2(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.29.0(jiti@2.6.1)) eslint-plugin-react-hooks: 7.0.1(eslint@9.29.0(jiti@2.6.1)) @@ -13010,7 +13013,7 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -13025,7 +13028,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.46.2(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 From a42efc178eba0cd031c0fb6d0ea7f785b39e739a Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 30 Jan 2026 16:02:41 +0800 Subject: [PATCH 18/25] fix(cli): handle error for proxy server (#634) * fix(cli): handle error for proxy server * fix(cli): update default port for ZenStack proxy server from 8008 to 2311 --- packages/cli/src/actions/proxy.ts | 11 +++++++++++ packages/cli/src/index.ts | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/actions/proxy.ts b/packages/cli/src/actions/proxy.ts index f3f27ef7e..27c809518 100644 --- a/packages/cli/src/actions/proxy.ts +++ b/packages/cli/src/actions/proxy.ts @@ -166,6 +166,17 @@ function startServer(client: ClientContract, schema: any, options: Opt console.log(`You can visit ZenStack Studio at: ${colors.blue('https://studio.zenstack.dev')}`); }); + server.on('error', (err: NodeJS.ErrnoException) => { + if (err.code === 'EADDRINUSE') { + console.error( + colors.red(`Port ${options.port} is already in use. Please choose a different port using -p option.`), + ); + } else { + throw new CliError(`Failed to start the server: ${err.message}`); + } + process.exit(1); + }); + // Graceful shutdown process.on('SIGTERM', async () => { server.close(() => { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 82d0e736c..e38966064 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -196,7 +196,7 @@ Arguments following -- are passed to the seed script. E.g.: "zen db seed -- --us .alias('studio') .description('Start the ZenStack proxy server') .addOption(schemaOption) - .addOption(new Option('-p, --port ', 'port to run the proxy server on').default(8008)) + .addOption(new Option('-p, --port ', 'port to run the proxy server on').default(2311)) .addOption(new Option('-o, --output ', 'output directory for `zen generate` command')) .addOption(new Option('-d, --databaseUrl ', 'database connection URL')) .addOption(new Option('-l, --logLevel ', 'Query log levels (e.g., query, error)')) From 2b3d7d6f61ccf89aba509597bbfdd252a2958232 Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:16:16 -0800 Subject: [PATCH 19/25] feat: `ignore` argument for `@updatedAt` (#572) * feat: `ignore` argument for `@updatedAt` * chore: add tests * Trigger Build * Check test. * Use `getTime` * Retry. * Retry. * Retry. * Retry. * Retry. * Retry. * Retry. * Clean up. * Document param. * Extract to function. * Relocate function. * Adjust formatting. * Use `getAttributeArg` * Use `$resolvedParam` * Null check. * fix: resolve a merge error * fix: delay data clone to right before changing it --------- Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- packages/cli/test/ts-schema-gen.test.ts | 282 ++++++++++++++++++ packages/language/res/stdlib.zmodel | 6 +- packages/orm/src/client/crud-types.ts | 3 +- .../orm/src/client/crud/operations/base.ts | 17 +- packages/schema/src/schema.ts | 8 +- packages/sdk/src/ts-schema-generator.ts | 18 +- tests/e2e/orm/client-api/update.test.ts | 62 ++++ tests/e2e/orm/schemas/basic/schema.ts | 20 +- tests/e2e/orm/schemas/basic/schema.zmodel | 2 +- 9 files changed, 397 insertions(+), 21 deletions(-) diff --git a/packages/cli/test/ts-schema-gen.test.ts b/packages/cli/test/ts-schema-gen.test.ts index 1056478ff..29aa7b522 100644 --- a/packages/cli/test/ts-schema-gen.test.ts +++ b/packages/cli/test/ts-schema-gen.test.ts @@ -466,4 +466,286 @@ model User { expect(schemaLite!.models.User.fields.id.attributes).toBeUndefined(); expect(schemaLite!.models.User.fields.email.attributes).toBeUndefined(); }); + + it('supports ignorable fields for @updatedAt', async () => { + const { schema } = await generateTsSchema(` +model User { + id String @id @default(uuid()) + name String + email String @unique + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt(ignore: [email]) + posts Post[] + + @@map('users') +} + +model Post { + id String @id @default(cuid()) + title String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id], onDelete: Cascade) + authorId String +} + `); + + expect(schema).toMatchObject({ + provider: { + type: 'sqlite' + }, + models: { + User: { + name: 'User', + fields: { + id: { + name: 'id', + type: 'String', + id: true, + attributes: [ + { + name: '@id' + }, + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'uuid' + } + } + ] + } + ], + default: { + kind: 'call', + function: 'uuid' + } + }, + name: { + name: 'name', + type: 'String' + }, + email: { + name: 'email', + type: 'String', + unique: true, + attributes: [ + { + name: '@unique' + } + ] + }, + createdAt: { + name: 'createdAt', + type: 'DateTime', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'now' + } + } + ] + } + ], + default: { + kind: 'call', + function: 'now' + } + }, + updatedAt: { + name: 'updatedAt', + type: 'DateTime', + updatedAt: { + ignore: [ + 'email' + ] + }, + attributes: [ + { + name: '@updatedAt', + args: [ + { + name: 'ignore', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'email' + } + ] + } + } + ] + } + ] + }, + posts: { + name: 'posts', + type: 'Post', + array: true, + relation: { + opposite: 'author' + } + } + }, + attributes: [ + { + name: '@@map', + args: [ + { + name: 'name', + value: { + kind: 'literal', + value: 'users' + } + } + ] + } + ], + idFields: [ + 'id' + ], + uniqueFields: { + id: { + type: 'String' + }, + email: { + type: 'String' + } + } + }, + Post: { + name: 'Post', + fields: { + id: { + name: 'id', + type: 'String', + id: true, + attributes: [ + { + name: '@id' + }, + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'call', + function: 'cuid' + } + } + ] + } + ], + default: { + kind: 'call', + function: 'cuid' + } + }, + title: { + name: 'title', + type: 'String' + }, + published: { + name: 'published', + type: 'Boolean', + attributes: [ + { + name: '@default', + args: [ + { + name: 'value', + value: { + kind: 'literal', + value: false + } + } + ] + } + ], + default: false + }, + author: { + name: 'author', + type: 'User', + attributes: [ + { + name: '@relation', + args: [ + { + name: 'fields', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'authorId' + } + ] + } + }, + { + name: 'references', + value: { + kind: 'array', + items: [ + { + kind: 'field', + field: 'id' + } + ] + } + }, + { + name: 'onDelete', + value: { + kind: 'literal', + value: 'Cascade' + } + } + ] + } + ], + relation: { + opposite: 'posts', + fields: [ + 'authorId' + ], + references: [ + 'id' + ], + onDelete: 'Cascade' + } + }, + authorId: { + name: 'authorId', + type: 'String', + foreignKeyFor: [ + 'author' + ] + } + }, + idFields: [ + 'id' + ], + uniqueFields: { + id: { + type: 'String' + } + } + } + }, + authType: 'User', + plugins: {} + }); + }) }); diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index 81d52dc95..f3f568820 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -402,8 +402,12 @@ attribute @omit() /** * Automatically stores the time when a record was last updated. + * + * @param ignore: A list of field names that are not considered when the ORM client is determining whether any + * updates have been made to a record. An update that only contains ignored fields does not change the + * timestamp. */ -attribute @updatedAt() @@@targetField([DateTimeField]) @@@prisma +attribute @updatedAt(_ ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma /** * Add full text index (MySQL only). diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index bfee7ff14..62be0c199 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -31,6 +31,7 @@ import type { SchemaDef, TypeDefFieldIsArray, TypeDefFieldIsOptional, + UpdatedAtInfo, } from '../schema'; import type { AtLeast, @@ -994,7 +995,7 @@ type OptionalFieldsForCreate extends true ? Key - : GetModelField['updatedAt'] extends true + : GetModelField['updatedAt'] extends (true | UpdatedAtInfo) ? Key : never]: GetModelField; }; diff --git a/packages/orm/src/client/crud/operations/base.ts b/packages/orm/src/client/crud/operations/base.ts index 33cc1a641..fc75cac9d 100644 --- a/packages/orm/src/client/crud/operations/base.ts +++ b/packages/orm/src/client/crud/operations/base.ts @@ -1149,11 +1149,20 @@ export abstract class BaseOperationHandler { const autoUpdatedFields: string[] = []; for (const [fieldName, fieldDef] of Object.entries(modelDef.fields)) { if (fieldDef.updatedAt && finalData[fieldName] === undefined) { - if (finalData === data) { - finalData = clone(data); + const ignoredFields = new Set(typeof fieldDef.updatedAt === 'boolean' ? [] : fieldDef.updatedAt.ignore); + const hasNonIgnoredFields = Object.keys(data).some( + (field) => + (isScalarField(this.schema, modelDef.name, field) || + isForeignKeyField(this.schema, modelDef.name, field)) && + !ignoredFields.has(field), + ); + if (hasNonIgnoredFields) { + if (finalData === data) { + finalData = clone(data); + } + finalData[fieldName] = this.dialect.transformInput(new Date(), 'DateTime', false); + autoUpdatedFields.push(fieldName); } - finalData[fieldName] = this.dialect.transformInput(new Date(), 'DateTime', false); - autoUpdatedFields.push(fieldName); } } diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index 08b0726f6..d98b86f01 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -59,6 +59,10 @@ export type RelationInfo = { onUpdate?: CascadeAction; }; +export type UpdatedAtInfo = { + ignore?: readonly string[]; +}; + export type FieldDef = { name: string; type: string; @@ -66,7 +70,7 @@ export type FieldDef = { array?: boolean; optional?: boolean; unique?: boolean; - updatedAt?: boolean; + updatedAt?: boolean | UpdatedAtInfo; attributes?: readonly AttributeApplication[]; default?: MappedBuiltinType | Expression | readonly unknown[]; omit?: boolean; @@ -283,7 +287,7 @@ export type FieldHasDefault< Field extends GetModelFields, > = GetModelField['default'] extends object | number | string | boolean ? true - : GetModelField['updatedAt'] extends true + : GetModelField['updatedAt'] extends (true | UpdatedAtInfo) ? true : GetModelField['relation'] extends { hasDefault: true } ? true diff --git a/packages/sdk/src/ts-schema-generator.ts b/packages/sdk/src/ts-schema-generator.ts index b60a1b050..413131269 100644 --- a/packages/sdk/src/ts-schema-generator.ts +++ b/packages/sdk/src/ts-schema-generator.ts @@ -522,6 +522,14 @@ export class TsSchemaGenerator { ); } + private createUpdatedAtObject(ignoreArg: AttributeArg) { + return ts.factory.createObjectLiteralExpression([ + ts.factory.createPropertyAssignment('ignore', ts.factory.createArrayLiteralExpression( + (ignoreArg.value as ArrayExpr).items.map((item) => ts.factory.createStringLiteral((item as ReferenceExpr).target.$refText)) + )) + ]); + } + private mapFieldTypeToTSType(type: DataFieldType) { let result = match(type.type) .with('String', () => 'string') @@ -564,8 +572,14 @@ export class TsSchemaGenerator { objectFields.push(ts.factory.createPropertyAssignment('array', ts.factory.createTrue())); } - if (hasAttribute(field, '@updatedAt')) { - objectFields.push(ts.factory.createPropertyAssignment('updatedAt', ts.factory.createTrue())); + const updatedAtAttrib = getAttribute(field, '@updatedAt') as DataFieldAttribute | undefined; + if (updatedAtAttrib) { + const ignoreArg = updatedAtAttrib.args.find(arg => arg.$resolvedParam?.name === 'ignore'); + objectFields.push(ts.factory.createPropertyAssignment('updatedAt', + ignoreArg + ? this.createUpdatedAtObject(ignoreArg) + : ts.factory.createTrue() + )); } if (hasAttribute(field, '@omit')) { diff --git a/tests/e2e/orm/client-api/update.test.ts b/tests/e2e/orm/client-api/update.test.ts index 08377e518..e14432050 100644 --- a/tests/e2e/orm/client-api/update.test.ts +++ b/tests/e2e/orm/client-api/update.test.ts @@ -154,6 +154,68 @@ describe('Client update tests', () => { expect(updatedUser?.updatedAt).toEqual(originalUpdatedAt); }); + it('does not update updatedAt if only ignored fields are present', async () => { + const user = await createUser(client, 'u1@test.com'); + const originalUpdatedAt = user.updatedAt; + + await client.user.update({ + where: { + id: user.id, + }, + + data: { + createdAt: new Date(), + }, + }) + + let updatedUser = await client.user.findUnique({ where: { id: user.id } }); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + + await client.user.update({ + where: { + id: user.id, + }, + + data: { + id: 'User2', + }, + }) + + updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + + // multiple ignored fields + await client.user.update({ + where: { + id: 'User2', + }, + + data: { + id: 'User3', + createdAt: new Date(), + }, + }) + + updatedUser = await client.user.findUnique({ where: { id: 'User3' } }); + expect(updatedUser?.updatedAt.getTime()).toEqual(originalUpdatedAt.getTime()); + }); + + it('updates updatedAt if any non-ignored fields are present', async () => { + const user = await createUser(client, 'u1@test.com'); + const originalUpdatedAt = user.updatedAt; + + await client.user.update({ + where: { id: user.id }, + data: { + id: 'User2', + name: 'User2', + }, + }); + + const updatedUser = await client.user.findUnique({ where: { id: 'User2' } }); + expect(updatedUser?.updatedAt.getTime()).toBeGreaterThan(originalUpdatedAt.getTime()); + }); + it('works with numeric incremental update', async () => { await createUser(client, 'u1@test.com', { profile: { create: { id: '1', bio: 'bio' } }, diff --git a/tests/e2e/orm/schemas/basic/schema.ts b/tests/e2e/orm/schemas/basic/schema.ts index ce123db2d..ea345b9ed 100644 --- a/tests/e2e/orm/schemas/basic/schema.ts +++ b/tests/e2e/orm/schemas/basic/schema.ts @@ -30,8 +30,8 @@ export class SchemaType implements SchemaDef { updatedAt: { name: "updatedAt", type: "DateTime", - updatedAt: true, - attributes: [{ name: "@updatedAt" }] + updatedAt: { ignore: ["id", "createdAt"] }, + attributes: [{ name: "@updatedAt", args: [{ name: "ignore", value: ExpressionUtils.array("String", [ExpressionUtils.field("id"), ExpressionUtils.field("createdAt")]) }] }] }, email: { name: "email", @@ -97,8 +97,8 @@ export class SchemaType implements SchemaDef { updatedAt: { name: "updatedAt", type: "DateTime", - updatedAt: true, - attributes: [{ name: "@updatedAt" }] + updatedAt: { ignore: ["id", "createdAt"] }, + attributes: [{ name: "@updatedAt", args: [{ name: "ignore", value: ExpressionUtils.array("String", [ExpressionUtils.field("id"), ExpressionUtils.field("createdAt")]) }] }] }, title: { name: "title", @@ -164,8 +164,8 @@ export class SchemaType implements SchemaDef { updatedAt: { name: "updatedAt", type: "DateTime", - updatedAt: true, - attributes: [{ name: "@updatedAt" }] + updatedAt: { ignore: ["id", "createdAt"] }, + attributes: [{ name: "@updatedAt", args: [{ name: "ignore", value: ExpressionUtils.array("String", [ExpressionUtils.field("id"), ExpressionUtils.field("createdAt")]) }] }] }, content: { name: "content", @@ -211,8 +211,8 @@ export class SchemaType implements SchemaDef { updatedAt: { name: "updatedAt", type: "DateTime", - updatedAt: true, - attributes: [{ name: "@updatedAt" }] + updatedAt: { ignore: ["id", "createdAt"] }, + attributes: [{ name: "@updatedAt", args: [{ name: "ignore", value: ExpressionUtils.array("String", [ExpressionUtils.field("id"), ExpressionUtils.field("createdAt")]) }] }] }, bio: { name: "bio", @@ -287,8 +287,8 @@ export class SchemaType implements SchemaDef { updatedAt: { name: "updatedAt", type: "DateTime", - updatedAt: true, - attributes: [{ name: "@updatedAt" }] + updatedAt: { ignore: ["id", "createdAt"] }, + attributes: [{ name: "@updatedAt", args: [{ name: "ignore", value: ExpressionUtils.array("String", [ExpressionUtils.field("id"), ExpressionUtils.field("createdAt")]) }] }] } } } diff --git a/tests/e2e/orm/schemas/basic/schema.zmodel b/tests/e2e/orm/schemas/basic/schema.zmodel index 9d3a7d91d..8fd48872c 100644 --- a/tests/e2e/orm/schemas/basic/schema.zmodel +++ b/tests/e2e/orm/schemas/basic/schema.zmodel @@ -15,7 +15,7 @@ enum Role { type CommonFields { id String @id @default(cuid()) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @updatedAt(ignore: [id, createdAt]) } model User with CommonFields { From f9475e66e724e4aa10a5861734c0ec31ff50d96c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:17:50 +0800 Subject: [PATCH 20/25] fix(cli): redact credentials from database URL console logs (#626) * feat(cli): add mysql support to proxy * Initial plan * fix: redact credentials from database URL logs Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * fix: also redact SQLite URLs for consistency Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * Merge dev branch and remove SQLite URL redaction Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * docs: clean up merge conflicts by rebasing on correct base * Push new branch to create fresh PR Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> * clean up agent mess --------- Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- packages/cli/src/actions/proxy.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/actions/proxy.ts b/packages/cli/src/actions/proxy.ts index 27c809518..532edbb88 100644 --- a/packages/cli/src/actions/proxy.ts +++ b/packages/cli/src/actions/proxy.ts @@ -109,6 +109,22 @@ function evaluateUrl(value: string): string { } } +function redactDatabaseUrl(url: string): string { + try { + const parsedUrl = new URL(url); + if (parsedUrl.password) { + parsedUrl.password = '***'; + } + if (parsedUrl.username) { + parsedUrl.username = '***'; + } + return parsedUrl.toString(); + } catch { + // If URL parsing fails, return the original + return url; + } +} + function createDialect(provider: string, databaseUrl: string, outputPath: string) { switch (provider) { case 'sqlite': { @@ -125,7 +141,7 @@ function createDialect(provider: string, databaseUrl: string, outputPath: string }); } case 'postgresql': - console.log(colors.gray(`Connecting to PostgreSQL database at: ${databaseUrl}`)); + console.log(colors.gray(`Connecting to PostgreSQL database at: ${redactDatabaseUrl(databaseUrl)}`)); return new PostgresDialect({ pool: new PgPool({ connectionString: databaseUrl, @@ -133,7 +149,7 @@ function createDialect(provider: string, databaseUrl: string, outputPath: string }); case 'mysql': - console.log(colors.gray(`Connecting to MySQL database at: ${databaseUrl}`)); + console.log(colors.gray(`Connecting to MySQL database at: ${redactDatabaseUrl(databaseUrl)}`)); return new MysqlDialect({ pool: createMysqlPool(databaseUrl), }); From 3392104d047b8197653ce068155cd960d5fe890e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 16:22:01 +0800 Subject: [PATCH 21/25] chore: bump version 3.3.0 (#635) Co-authored-by: ymc9 <104139426+ymc9@users.noreply.github.com> --- package.json | 2 +- packages/auth-adapters/better-auth/package.json | 2 +- packages/cli/package.json | 2 +- packages/clients/client-helpers/package.json | 2 +- packages/clients/tanstack-query/package.json | 2 +- packages/common-helpers/package.json | 2 +- packages/config/eslint-config/package.json | 2 +- packages/config/typescript-config/package.json | 2 +- packages/config/vitest-config/package.json | 2 +- packages/create-zenstack/package.json | 2 +- packages/ide/vscode/package.json | 2 +- packages/language/package.json | 2 +- packages/orm/package.json | 2 +- packages/plugins/policy/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- packages/zod/package.json | 2 +- samples/orm/package.json | 2 +- tests/e2e/package.json | 2 +- tests/regression/package.json | 2 +- tests/runtimes/bun/package.json | 2 +- tests/runtimes/edge-runtime/package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index ba1c74796..483728798 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-v3", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack", "packageManager": "pnpm@10.23.0", "type": "module", diff --git a/packages/auth-adapters/better-auth/package.json b/packages/auth-adapters/better-auth/package.json index 8fc58ae84..9e870515a 100644 --- a/packages/auth-adapters/better-auth/package.json +++ b/packages/auth-adapters/better-auth/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/better-auth", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack Better Auth Adapter. This adapter is modified from better-auth's Prisma adapter.", "type": "module", "scripts": { diff --git a/packages/cli/package.json b/packages/cli/package.json index 9d6ea65a5..2196fa74d 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.2.1", + "version": "3.3.0", "type": "module", "author": { "name": "ZenStack Team" diff --git a/packages/clients/client-helpers/package.json b/packages/clients/client-helpers/package.json index f31fe5374..d98a534cd 100644 --- a/packages/clients/client-helpers/package.json +++ b/packages/clients/client-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/client-helpers", - "version": "3.2.1", + "version": "3.3.0", "description": "Helpers for implementing clients that consume ZenStack's CRUD service", "type": "module", "scripts": { diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index e7a527681..f7038c56a 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/tanstack-query", - "version": "3.2.1", + "version": "3.3.0", "description": "TanStack Query Client for consuming ZenStack v3's CRUD service", "type": "module", "scripts": { diff --git a/packages/common-helpers/package.json b/packages/common-helpers/package.json index e4a575396..7bd2d2836 100644 --- a/packages/common-helpers/package.json +++ b/packages/common-helpers/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/common-helpers", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack Common Helpers", "type": "module", "scripts": { diff --git a/packages/config/eslint-config/package.json b/packages/config/eslint-config/package.json index 79d9933e4..0a165bb09 100644 --- a/packages/config/eslint-config/package.json +++ b/packages/config/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/eslint-config", - "version": "3.2.1", + "version": "3.3.0", "type": "module", "private": true, "license": "MIT" diff --git a/packages/config/typescript-config/package.json b/packages/config/typescript-config/package.json index ba55bbcae..5f2123433 100644 --- a/packages/config/typescript-config/package.json +++ b/packages/config/typescript-config/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/typescript-config", - "version": "3.2.1", + "version": "3.3.0", "private": true, "license": "MIT" } diff --git a/packages/config/vitest-config/package.json b/packages/config/vitest-config/package.json index 96405ecdf..d15f57401 100644 --- a/packages/config/vitest-config/package.json +++ b/packages/config/vitest-config/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/vitest-config", "type": "module", - "version": "3.2.1", + "version": "3.3.0", "private": true, "license": "MIT", "exports": { diff --git a/packages/create-zenstack/package.json b/packages/create-zenstack/package.json index 45b0def5d..c62e80c52 100644 --- a/packages/create-zenstack/package.json +++ b/packages/create-zenstack/package.json @@ -1,6 +1,6 @@ { "name": "create-zenstack", - "version": "3.2.1", + "version": "3.3.0", "description": "Create a new ZenStack project", "type": "module", "scripts": { diff --git a/packages/ide/vscode/package.json b/packages/ide/vscode/package.json index 0976e9692..e5b8933b4 100644 --- a/packages/ide/vscode/package.json +++ b/packages/ide/vscode/package.json @@ -1,7 +1,7 @@ { "name": "zenstack-v3", "publisher": "zenstack", - "version": "3.2.1", + "version": "3.3.0", "displayName": "ZenStack V3 Language Tools", "description": "VSCode extension for ZenStack (v3) ZModel language", "private": true, diff --git a/packages/language/package.json b/packages/language/package.json index 3f4cca8c3..c5f60b107 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.2.1", + "version": "3.3.0", "license": "MIT", "author": "ZenStack Team", "files": [ diff --git a/packages/orm/package.json b/packages/orm/package.json index eb5ee6684..67d7da753 100644 --- a/packages/orm/package.json +++ b/packages/orm/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/orm", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack ORM", "type": "module", "scripts": { diff --git a/packages/plugins/policy/package.json b/packages/plugins/policy/package.json index e686d56c3..eb5b49705 100644 --- a/packages/plugins/policy/package.json +++ b/packages/plugins/policy/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/plugin-policy", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack Policy Plugin", "type": "module", "scripts": { diff --git a/packages/schema/package.json b/packages/schema/package.json index bc25a5bf1..a1d1dd76a 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/schema", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack Runtime Schema", "type": "module", "scripts": { diff --git a/packages/sdk/package.json b/packages/sdk/package.json index a2fe1f0d9..79cc9883e 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack SDK", "type": "module", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index d1ca34d46..499ac858c 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack automatic CRUD API handlers and server adapters", "type": "module", "scripts": { diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 7ec0135d1..648e63ff4 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "3.2.1", + "version": "3.3.0", "description": "ZenStack Test Tools", "type": "module", "scripts": { diff --git a/packages/zod/package.json b/packages/zod/package.json index 3176a7c0c..4b282a25f 100644 --- a/packages/zod/package.json +++ b/packages/zod/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/zod", - "version": "3.2.1", + "version": "3.3.0", "description": "", "type": "module", "main": "index.js", diff --git a/samples/orm/package.json b/samples/orm/package.json index b052aa9ea..3b9ba4acc 100644 --- a/samples/orm/package.json +++ b/samples/orm/package.json @@ -1,6 +1,6 @@ { "name": "sample-blog", - "version": "3.2.1", + "version": "3.3.0", "description": "", "main": "index.js", "private": true, diff --git a/tests/e2e/package.json b/tests/e2e/package.json index f37d8b29d..d7b3f4b55 100644 --- a/tests/e2e/package.json +++ b/tests/e2e/package.json @@ -1,6 +1,6 @@ { "name": "e2e", - "version": "3.2.1", + "version": "3.3.0", "private": true, "type": "module", "scripts": { diff --git a/tests/regression/package.json b/tests/regression/package.json index 12f1f3014..2f9fd6330 100644 --- a/tests/regression/package.json +++ b/tests/regression/package.json @@ -1,6 +1,6 @@ { "name": "regression", - "version": "3.2.1", + "version": "3.3.0", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/bun/package.json b/tests/runtimes/bun/package.json index 9c0f9eef8..8ed96ebc2 100644 --- a/tests/runtimes/bun/package.json +++ b/tests/runtimes/bun/package.json @@ -1,6 +1,6 @@ { "name": "bun-e2e", - "version": "3.2.1", + "version": "3.3.0", "private": true, "type": "module", "scripts": { diff --git a/tests/runtimes/edge-runtime/package.json b/tests/runtimes/edge-runtime/package.json index 2036296e4..0999f28c5 100644 --- a/tests/runtimes/edge-runtime/package.json +++ b/tests/runtimes/edge-runtime/package.json @@ -1,6 +1,6 @@ { "name": "edge-runtime-e2e", - "version": "3.2.1", + "version": "3.3.0", "private": true, "type": "module", "scripts": { From 895b8f1a8bfaf8db7dd55d97bf776d3d69cae5cd Mon Sep 17 00:00:00 2001 From: sanny-io <3054653+sanny-io@users.noreply.github.com> Date: Fri, 30 Jan 2026 02:58:44 -0800 Subject: [PATCH 22/25] Require explicit `ignore` argument for `@updatedAt` (#638) --- packages/language/res/stdlib.zmodel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/language/res/stdlib.zmodel b/packages/language/res/stdlib.zmodel index f3f568820..d0c3c0003 100644 --- a/packages/language/res/stdlib.zmodel +++ b/packages/language/res/stdlib.zmodel @@ -407,7 +407,7 @@ attribute @omit() * updates have been made to a record. An update that only contains ignored fields does not change the * timestamp. */ -attribute @updatedAt(_ ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma +attribute @updatedAt(ignore: FieldReference[]?) @@@targetField([DateTimeField]) @@@prisma /** * Add full text index (MySQL only). From f612723b8a5a071f9caa63e2a363158289e008da Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 30 Jan 2026 20:49:27 +0800 Subject: [PATCH 23/25] refactor(cli): replace dynamic function call eval with regex match for datasource url (#637) * refactor(cli): replace dynamic function call eval with regex match for datasource url * fix(proxy): improve regex for env() function call matching --- packages/cli/src/actions/proxy.ts | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/actions/proxy.ts b/packages/cli/src/actions/proxy.ts index 532edbb88..4da3ca431 100644 --- a/packages/cli/src/actions/proxy.ts +++ b/packages/cli/src/actions/proxy.ts @@ -87,25 +87,17 @@ export async function run(options: Options) { } function evaluateUrl(value: string): string { - // Create env helper function - const env = (varName: string) => { - const envValue = process.env[varName]; + // Check if it's an env() function call + const envMatch = value.trim().match(/^env\s*\(\s*['"]([^'"]+)['"]\s*\)$/); + if (envMatch) { + const varName = envMatch[1]; + const envValue = process.env[varName!]; if (!envValue) { throw new CliError(`Environment variable ${varName} is not set`); } return envValue; - }; - - try { - // Use Function constructor to evaluate the url value - const urlFn = new Function('env', `return ${value}`); - const url = urlFn(env); - return url; - } catch (err) { - if (err instanceof CliError) { - throw err; - } - throw new CliError('Could not evaluate datasource url from schema, you could provide it via -d option.'); + } else { + return value; } } From 14a0a035b9f8a2bfd30c33abac8bd48bd5f1028e Mon Sep 17 00:00:00 2001 From: Lukas Kahwe Smith Date: Fri, 30 Jan 2026 14:07:44 +0100 Subject: [PATCH 24/25] fix handling of a single column unique index with externalIdMapping (#612) --- packages/server/src/api/rest/index.ts | 2 +- packages/server/test/api/rest.test.ts | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/server/src/api/rest/index.ts b/packages/server/src/api/rest/index.ts index 980af6ba7..c2723f061 100644 --- a/packages/server/src/api/rest/index.ts +++ b/packages/server/src/api/rest/index.ts @@ -1344,7 +1344,7 @@ export class RestApiHandler implements Api if (name === externalIdName) { if (typeof info.type === 'string') { // single unique field - return [this.requireField(model, info.type)]; + return [this.requireField(model, name)]; } else { // compound unique fields return Object.keys(info).map((f) => this.requireField(model, f)); diff --git a/packages/server/test/api/rest.test.ts b/packages/server/test/api/rest.test.ts index 96b469ba7..ec0a6a8a9 100644 --- a/packages/server/test/api/rest.test.ts +++ b/packages/server/test/api/rest.test.ts @@ -3183,6 +3183,7 @@ describe('REST server tests', () => { model Post { id Int @id @default(autoincrement()) title String + short_title String @unique() author User? @relation(fields: [authorId], references: [id]) authorId Int? } @@ -3195,6 +3196,7 @@ describe('REST server tests', () => { endpoint: 'http://localhost/api', externalIdMapping: { User: 'name_source', + Post: 'short_title', }, }); handler = (args) => _handler.handleRequest({ ...args, url: new URL(`http://localhost/${args.path}`) }); @@ -3229,13 +3231,13 @@ describe('REST server tests', () => { expect(r.body.data.attributes.name).toBe('User1'); await client.post.create({ - data: { id: 1, title: 'Title1', authorId: 1 }, + data: { id: 1, title: 'Title1', short_title: 'post-title-1', authorId: 1 }, }); // post is exposed using the `id` field r = await handler({ method: 'get', - path: '/post/1', + path: '/post/post-title-1', query: { include: 'author' }, client, }); From b3aff66943427ebbf1e73e45b647a45678066bb2 Mon Sep 17 00:00:00 2001 From: Jiasheng Date: Fri, 30 Jan 2026 21:09:34 +0800 Subject: [PATCH 25/25] fix(cli): update logLevel option to accept multiple values (#639) --- packages/cli/src/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index e38966064..4efc86fd9 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -199,7 +199,7 @@ Arguments following -- are passed to the seed script. E.g.: "zen db seed -- --us .addOption(new Option('-p, --port ', 'port to run the proxy server on').default(2311)) .addOption(new Option('-o, --output ', 'output directory for `zen generate` command')) .addOption(new Option('-d, --databaseUrl ', 'database connection URL')) - .addOption(new Option('-l, --logLevel ', 'Query log levels (e.g., query, error)')) + .addOption(new Option('-l, --logLevel ', 'Query log levels (e.g., query, error)')) .addOption(noVersionCheckOption) .action(proxyAction);