diff --git a/packages/clients/client-helpers/src/types.ts b/packages/clients/client-helpers/src/types.ts index 5f30ac613..a4f4f68cc 100644 --- a/packages/clients/client-helpers/src/types.ts +++ b/packages/clients/client-helpers/src/types.ts @@ -1,4 +1,4 @@ -import type { ClientContract, QueryOptions } from '@zenstackhq/orm'; +import { ExtQueryArgsMarker, ExtResultMarker, type QueryOptions } from '@zenstackhq/orm'; import type { SchemaDef } from '@zenstackhq/schema'; /** @@ -11,10 +11,15 @@ export type MaybePromise = T | Promise | PromiseLike; */ export type InferSchema = T extends { $schema: infer S extends SchemaDef } ? S : T extends SchemaDef ? T : never; +/** + * Extracts the ExtQueryArgs type from a client contract, or defaults to `{}`. + */ +export type InferExtQueryArgs = T extends { [ExtQueryArgsMarker]?: infer E } ? (unknown extends E ? {} : E) : {}; + /** * Extracts the ExtResult type from a client contract, or defaults to `{}`. */ -export type InferExtResult = T extends ClientContract ? E : {}; +export type InferExtResult = T extends { [ExtResultMarker]?: infer E } ? (unknown extends E ? {} : E) : {}; /** * Infers query options from a client contract type, or defaults to `QueryOptions`. diff --git a/packages/clients/tanstack-query/package.json b/packages/clients/tanstack-query/package.json index eaddd4aa4..9d07c7956 100644 --- a/packages/clients/tanstack-query/package.json +++ b/packages/clients/tanstack-query/package.json @@ -57,6 +57,7 @@ "@zenstackhq/client-helpers": "workspace:*", "@zenstackhq/common-helpers": "workspace:*", "@zenstackhq/schema": "workspace:*", + "@zenstackhq/orm": "workspace:*", "decimal.js": "catalog:" }, "devDependencies": { @@ -71,7 +72,6 @@ "@zenstackhq/cli": "workspace:*", "@zenstackhq/eslint-config": "workspace:*", "@zenstackhq/language": "workspace:*", - "@zenstackhq/orm": "workspace:*", "@zenstackhq/sdk": "workspace:*", "@zenstackhq/typescript-config": "workspace:*", "@zenstackhq/vitest-config": "workspace:*", diff --git a/packages/clients/tanstack-query/src/common/types.ts b/packages/clients/tanstack-query/src/common/types.ts index 564e48934..867234397 100644 --- a/packages/clients/tanstack-query/src/common/types.ts +++ b/packages/clients/tanstack-query/src/common/types.ts @@ -1,28 +1,17 @@ import type { Logger, OptimisticDataProvider } from '@zenstackhq/client-helpers'; import type { FetchFn } from '@zenstackhq/client-helpers/fetch'; import type { - AggregateArgs, - CountArgs, - CreateArgs, - CreateManyAndReturnArgs, - CreateManyArgs, - DeleteArgs, - DeleteManyArgs, - ExistsArgs, - FindFirstArgs, - FindManyArgs, - FindUniqueArgs, + CoreCrudOperations, + CrudArgsMap, + CrudReturnMap, + ExtQueryArgsBase, + ExtResultBase, GetProcedureNames, GetSlicedOperations, - GroupByArgs, ModelAllowsCreate, OperationsRequiringCreate, ProcedureFunc, QueryOptions, - UpdateArgs, - UpdateManyAndReturnArgs, - UpdateManyArgs, - UpsertArgs, } from '@zenstackhq/orm'; import type { GetModels, SchemaDef } from '@zenstackhq/schema'; @@ -118,46 +107,58 @@ export type ProcedureReturn; /** - * Maps each core CRUD operation to its argument type for a given model. + * Operations available in a sequential transaction. */ -type CrudArgsMap> = { - findMany: FindManyArgs; - findUnique: FindUniqueArgs; - findFirst: FindFirstArgs; - create: CreateArgs; - createMany: CreateManyArgs; - createManyAndReturn: CreateManyAndReturnArgs; - update: UpdateArgs; - updateMany: UpdateManyArgs; - updateManyAndReturn: UpdateManyAndReturnArgs; - upsert: UpsertArgs; - delete: DeleteArgs; - deleteMany: DeleteManyArgs; - count: CountArgs; - aggregate: AggregateArgs; - groupBy: GroupByArgs; - exists: ExistsArgs; -}; - -/** - * Operations available for a given model, omitting create-style operations - * for models that don't allow them (e.g. delegate models). - */ -type AllowedTransactionOps> = +type AllowedTransactionOps< + Schema extends SchemaDef, + Model extends GetModels, + Options extends QueryOptions = QueryOptions, +> = ModelAllowsCreate extends true - ? keyof CrudArgsMap - : Exclude, OperationsRequiringCreate>; + ? GetSlicedOperations & CoreCrudOperations + : Exclude & CoreCrudOperations, OperationsRequiringCreate>; /** * Represents a single operation to execute within a sequential transaction. * * The `model`, `op`, and `args` fields are correlated: `op` is constrained to - * the CRUD operations available on `model`, and `args` is typed accordingly. + * the CRUD operations available on `model` (respecting `Options['slicing']`), and + * `args` is typed accordingly. */ -export type TransactionOperation = { +export type TransactionOperation< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = { [Model in GetModels]: { - [Op in AllowedTransactionOps]: {} extends CrudArgsMap[Op] - ? { model: Model; op: Op; args?: CrudArgsMap[Op] } - : { model: Model; op: Op; args: CrudArgsMap[Op] }; - }[AllowedTransactionOps]; + [Op in AllowedTransactionOps]: {} extends CrudArgsMap< + Schema, + Model, + Options, + ExtQueryArgs, + ExtResult + >[Op] + ? { model: Model; op: Op; args?: CrudArgsMap[Op] } + : { model: Model; op: Op; args: CrudArgsMap[Op] }; + }[AllowedTransactionOps]; }[GetModels]; + +/** + * Maps each operation in a transaction tuple to its precise result type, preserving + * per-position typing. + */ +export type TransactionResults< + Schema extends SchemaDef, + Ops extends readonly TransactionOperation[], + Options extends QueryOptions = QueryOptions, + ExtResult extends ExtResultBase = {}, +> = { + [K in keyof Ops]: Ops[K] extends { model: infer M; op: infer O; args?: infer A } + ? M extends GetModels + ? O extends keyof CrudReturnMap + ? CrudReturnMap[O] + : never + : never + : never; +}; diff --git a/packages/clients/tanstack-query/src/react.ts b/packages/clients/tanstack-query/src/react.ts index 731f3fda0..2f8413dc7 100644 --- a/packages/clients/tanstack-query/src/react.ts +++ b/packages/clients/tanstack-query/src/react.ts @@ -23,6 +23,7 @@ import { createInvalidator, createOptimisticUpdater, DEFAULT_QUERY_ENDPOINT, + type InferExtQueryArgs, type InferExtResult, type InferOptions, type InferSchema, @@ -42,6 +43,7 @@ import type { DeleteArgs, DeleteManyArgs, ExistsArgs, + ExtQueryArgsBase, ExtResultBase, FindFirstArgs, FindManyArgs, @@ -75,6 +77,7 @@ import type { ProcedureReturn, QueryContext, TransactionOperation, + TransactionResults, TrimSlicedOperations, WithOptimistic, } from './common/types.js'; @@ -167,28 +170,69 @@ export type ModelMutationModelResult< ): Promise>; }; -export type TransactionMutationOptions = Omit< - UseMutationOptions[]>, +/** + * Options accepted by `$transaction.useSequential()`. + */ +export type TransactionMutationOptions< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = Omit< + UseMutationOptions[]>, 'mutationFn' > & Omit; +/** + * The return type of `$transaction.useSequential()`. Overrides `mutateAsync` so the + * resolved value is a tuple of per-operation results, narrowed to each operation's + * model + op + args (mirrors the typing of the corresponding ORM CRUD method). + */ +export type TransactionMutationResult< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = Omit< + UseMutationResult[]>, + 'mutateAsync' +> & { + mutateAsync[]>( + operations: T, + options?: Omit< + UseMutationOptions, DefaultError, T>, + 'mutationFn' + >, + ): Promise>; +}; + +/** + * The full set of TanStack Query hooks returned by {@link useClientQueries}. Includes: + * + * - One entry per (sliced) model under its uncapitalized name, providing the per-model + * {@link ModelQueryHooks} (e.g. `client.user.useFindMany`, `client.user.useCreate`). + * - A `$procs` namespace with hooks for any custom procedures declared in the schema. + * - A `$transaction.useSequential` hook for executing a sequential transaction. + */ export type ClientHooks< Schema extends SchemaDef, Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, > = { [Model in GetSlicedModels as `${Uncapitalize}`]: ModelQueryHooks< Schema, Model, Options, + ExtQueryArgs, ExtResult >; } & ProcedureHooks & { $transaction: { useSequential( - options?: TransactionMutationOptions, - ): UseMutationResult[]>; + options?: TransactionMutationOptions, + ): TransactionMutationResult; }; }; @@ -252,49 +296,53 @@ export type ModelQueryHooks< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, > = TrimSlicedOperations< Schema, Model, Options, { - useFindUnique>( - args: SelectSubset>, + useFindUnique>( + args: SelectSubset>, options?: ModelQueryOptions | null>, ): ModelQueryResult | null>; - useSuspenseFindUnique>( - args: SelectSubset>, + useSuspenseFindUnique>( + args: SelectSubset>, options?: ModelSuspenseQueryOptions | null>, ): ModelSuspenseQueryResult | null>; - useFindFirst>( - args?: SelectSubset>, + useFindFirst>( + args?: SelectSubset>, options?: ModelQueryOptions | null>, ): ModelQueryResult | null>; - useSuspenseFindFirst>( - args?: SelectSubset>, + useSuspenseFindFirst>( + args?: SelectSubset>, options?: ModelSuspenseQueryOptions | null>, ): ModelSuspenseQueryResult | null>; - useExists>( - args?: Subset>, + useExists>( + args?: Subset>, options?: ModelQueryOptions, ): ModelQueryResult; - useFindMany>( - args?: SelectSubset>, + useFindMany>( + args?: SelectSubset>, options?: ModelQueryOptions[]>, ): ModelQueryResult[]>; - useSuspenseFindMany>( - args?: SelectSubset>, + useSuspenseFindMany>( + args?: SelectSubset>, options?: ModelSuspenseQueryOptions[]>, ): ModelSuspenseQueryResult[]>; - useInfiniteFindMany, TPageParam = unknown>( - args?: SelectSubset>, + useInfiniteFindMany< + T extends FindManyArgs, + TPageParam = unknown, + >( + args?: SelectSubset>, options?: ModelInfiniteQueryOptions< SimplifiedPlainResult[], TPageParam @@ -304,10 +352,10 @@ export type ModelQueryHooks< >; useSuspenseInfiniteFindMany< - T extends FindManyArgs, + T extends FindManyArgs, TPageParam = unknown, >( - args?: SelectSubset>, + args?: SelectSubset>, options?: ModelSuspenseInfiniteQueryOptions< SimplifiedPlainResult[], TPageParam @@ -316,69 +364,69 @@ export type ModelQueryHooks< InfiniteData[], TPageParam> >; - useCreate>( + useCreate>( options?: ModelMutationOptions, T>, ): ModelMutationModelResult; - useCreateMany>( + useCreateMany>( options?: ModelMutationOptions, ): ModelMutationResult; - useCreateManyAndReturn>( + useCreateManyAndReturn>( options?: ModelMutationOptions[], T>, ): ModelMutationModelResult; - useUpdate>( + useUpdate>( options?: ModelMutationOptions, T>, ): ModelMutationModelResult; - useUpdateMany>( + useUpdateMany>( options?: ModelMutationOptions, ): ModelMutationResult; - useUpdateManyAndReturn>( + useUpdateManyAndReturn>( options?: ModelMutationOptions[], T>, ): ModelMutationModelResult; - useUpsert>( + useUpsert>( options?: ModelMutationOptions, T>, ): ModelMutationModelResult; - useDelete>( + useDelete>( options?: ModelMutationOptions, T>, ): ModelMutationModelResult; - useDeleteMany>( + useDeleteMany>( options?: ModelMutationOptions, ): ModelMutationResult; - useCount>( - args?: Subset>, + useCount>( + args?: Subset>, options?: ModelQueryOptions>, ): ModelQueryResult>; - useSuspenseCount>( - args?: Subset>, + useSuspenseCount>( + args?: Subset>, options?: ModelSuspenseQueryOptions>, ): ModelSuspenseQueryResult>; - useAggregate>( - args: Subset>, + useAggregate>( + args: Subset>, options?: ModelQueryOptions>, ): ModelQueryResult>; - useSuspenseAggregate>( - args: Subset>, + useSuspenseAggregate>( + args: Subset>, options?: ModelSuspenseQueryOptions>, ): ModelSuspenseQueryResult>; - useGroupBy>( - args: Subset>, + useGroupBy>( + args: Subset>, options?: ModelQueryOptions>, ): ModelQueryResult>; - useSuspenseGroupBy>( - args: Subset>, + useSuspenseGroupBy>( + args: Subset>, options?: ModelSuspenseQueryOptions>, ): ModelSuspenseQueryResult>; } @@ -409,6 +457,7 @@ export function useClientQueries, InferOptions>, + InferExtQueryArgs extends ExtQueryArgsBase ? InferExtQueryArgs : {}, InferExtResult extends ExtResultBase> ? InferExtResult : {} @@ -476,8 +525,13 @@ export function useModelQueries< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, ->(schema: Schema, model: Model, rootOptions?: QueryContext): ModelQueryHooks { +>( + schema: Schema, + model: Model, + rootOptions?: QueryContext, +): ModelQueryHooks { const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); if (!modelDef) { throw new Error(`Model "${model}" not found in schema`); @@ -587,7 +641,7 @@ export function useModelQueries< useSuspenseGroupBy: (args: any, options?: any) => { return useInternalSuspenseQuery(schema, modelName, 'groupBy', args, { ...rootOptions, ...options }); }, - } as ModelQueryHooks; + } as ModelQueryHooks; } export function useInternalQuery( @@ -807,10 +861,15 @@ export function useInternalMutation( return useMutation(finalOptions); } -export function useInternalTransactionMutation( +export function useInternalTransactionMutation< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +>( schema: Schema, - options?: TransactionMutationOptions, -) { + options?: TransactionMutationOptions, +): TransactionMutationResult { const { endpoint, fetch, logging } = useFetchOptions(options); const queryClient = useQueryClient(); @@ -828,7 +887,12 @@ export function useInternalTransactionMutation( ); } - return useMutation(finalOptions); + return useMutation(finalOptions as any) as unknown as TransactionMutationResult< + Schema, + Options, + ExtQueryArgs, + ExtResult + >; } function useFetchOptions(options: QueryContext | undefined) { diff --git a/packages/clients/tanstack-query/src/svelte/index.svelte.ts b/packages/clients/tanstack-query/src/svelte/index.svelte.ts index b8bf2c9be..5e0467d28 100644 --- a/packages/clients/tanstack-query/src/svelte/index.svelte.ts +++ b/packages/clients/tanstack-query/src/svelte/index.svelte.ts @@ -19,6 +19,7 @@ import { createInvalidator, createOptimisticUpdater, DEFAULT_QUERY_ENDPOINT, + type InferExtQueryArgs, type InferExtResult, type InferOptions, type InferSchema, @@ -39,6 +40,7 @@ import type { DeleteArgs, DeleteManyArgs, ExistsArgs, + ExtQueryArgsBase, ExtResultBase, FindFirstArgs, FindManyArgs, @@ -72,11 +74,12 @@ import type { ProcedureReturn, QueryContext, TransactionOperation, + TransactionResults, TrimSlicedOperations, WithOptimistic, } from '../common/types.js'; export { AnyNull, DbNull, JsonNull } from '@zenstackhq/client-helpers'; -export type { InferExtResult, InferOptions, InferSchema } from '@zenstackhq/client-helpers'; +export type { InferExtQueryArgs, InferExtResult, InferOptions, InferSchema } from '@zenstackhq/client-helpers'; export type { FetchFn } from '@zenstackhq/client-helpers/fetch'; export type { SchemaDef } from '@zenstackhq/schema'; @@ -160,28 +163,64 @@ export type ModelMutationModelResult< ): Promise>; }; -export type TransactionMutationOptions = Omit< - CreateMutationOptions[]>, +/** + * Options accepted by `$transaction.useSequential()`. + */ +export type TransactionMutationOptions< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = Omit< + CreateMutationOptions[]>, 'mutationFn' > & Omit; +/** + * The return type of `$transaction.useSequential()`. Overrides `mutateAsync` so the + * resolved value is a tuple of per-operation results, narrowed to each operation's + * model + op + args (mirrors the typing of the corresponding ORM CRUD method). + */ +export type TransactionMutationResult< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = Omit< + CreateMutationResult[]>, + 'mutateAsync' +> & { + mutateAsync[]>( + operations: T, + options?: Omit< + CreateMutationOptions, DefaultError, T>, + 'mutationFn' + >, + ): Promise>; +}; + +/** + * The full set of TanStack Query hooks returned by {@link useClientQueries}. + */ export type ClientHooks< Schema extends SchemaDef, Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, > = { [Model in GetSlicedModels as `${Uncapitalize}`]: ModelQueryHooks< Schema, Model, Options, + ExtQueryArgs, ExtResult >; } & ProcedureHooks & { $transaction: { useSequential( - options?: TransactionMutationOptions, - ): CreateMutationResult[]>; + options?: TransactionMutationOptions, + ): TransactionMutationResult; }; }; @@ -235,34 +274,38 @@ export type ModelQueryHooks< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, > = TrimSlicedOperations< Schema, Model, Options, { - useFindUnique>( - args: Accessor>>, + useFindUnique>( + args: Accessor>>, options?: Accessor | null>>, ): ModelQueryResult | null>; - useFindFirst>( - args?: Accessor>>, + useFindFirst>( + args?: Accessor>>, options?: Accessor | null>>, ): ModelQueryResult | null>; - useExists>( - args?: Accessor>>, + useExists>( + args?: Accessor>>, options?: Accessor>, ): ModelQueryResult; - useFindMany>( - args?: Accessor>>, + useFindMany>( + args?: Accessor>>, options?: Accessor[]>>, ): ModelQueryResult[]>; - useInfiniteFindMany, TPageParam = unknown>( - args?: Accessor>>, + useInfiniteFindMany< + T extends FindManyArgs, + TPageParam = unknown, + >( + args?: Accessor>>, options?: Accessor< ModelInfiniteQueryOptions[], TPageParam> >, @@ -270,52 +313,52 @@ export type ModelQueryHooks< InfiniteData[], TPageParam> >; - useCreate>( + useCreate>( options?: Accessor, T>>, ): ModelMutationModelResult; - useCreateMany>( + useCreateMany>( options?: Accessor>, ): ModelMutationResult; - useCreateManyAndReturn>( + useCreateManyAndReturn>( options?: Accessor[], T>>, ): ModelMutationModelResult; - useUpdate>( + useUpdate>( options?: Accessor, T>>, ): ModelMutationModelResult; - useUpdateMany>( + useUpdateMany>( options?: Accessor>, ): ModelMutationResult; - useUpdateManyAndReturn>( + useUpdateManyAndReturn>( options?: Accessor[], T>>, ): ModelMutationModelResult; - useUpsert>( + useUpsert>( options?: Accessor, T>>, ): ModelMutationModelResult; - useDelete>( + useDelete>( options?: Accessor, T>>, ): ModelMutationModelResult; - useDeleteMany>( + useDeleteMany>( options?: Accessor>, ): ModelMutationResult; - useCount>( - args?: Accessor>>, + useCount>( + args?: Accessor>>, options?: Accessor>>, ): ModelQueryResult>; - useAggregate>( - args: Accessor>>, + useAggregate>( + args: Accessor>>, options?: Accessor>>, ): ModelQueryResult>; - useGroupBy>( - args: Accessor>>, + useGroupBy>( + args: Accessor>>, options?: Accessor>>, ): ModelQueryResult>; } @@ -343,6 +386,7 @@ export function useClientQueries, InferOptions>, + InferExtQueryArgs extends ExtQueryArgsBase ? InferExtQueryArgs : {}, InferExtResult extends ExtResultBase> ? InferExtResult : {} @@ -403,12 +447,13 @@ export function useModelQueries< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, >( schema: Schema, model: Model, rootOptions?: Accessor, -): ModelQueryHooks { +): ModelQueryHooks { const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); if (!modelDef) { throw new Error(`Model "${model}" not found in schema`); @@ -484,7 +529,7 @@ export function useModelQueries< useGroupBy: (args: any, options?: any) => { return useInternalQuery(schema, modelName, 'groupBy', args, options); }, - } as unknown as ModelQueryHooks; + } as unknown as ModelQueryHooks; } export function useInternalQuery( @@ -709,10 +754,15 @@ export function useInternalMutation( return createMutation(finalOptions); } -export function useInternalTransactionMutation( +export function useInternalTransactionMutation< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +>( schema: Schema, - options?: Accessor>, -) { + options?: Accessor>, +): TransactionMutationResult { const { endpoint, fetch, logging } = useFetchOptions(options); const queryClient = useQueryClient(); @@ -736,7 +786,12 @@ export function useInternalTransactionMutation( return result; }; - return createMutation(finalOptions); + return createMutation(finalOptions) as unknown as TransactionMutationResult< + Schema, + Options, + ExtQueryArgs, + ExtResult + >; } function useFetchOptions(options: Accessor | undefined) { diff --git a/packages/clients/tanstack-query/src/vue.ts b/packages/clients/tanstack-query/src/vue.ts index 76669b1a8..bfffa2481 100644 --- a/packages/clients/tanstack-query/src/vue.ts +++ b/packages/clients/tanstack-query/src/vue.ts @@ -17,6 +17,7 @@ import { createInvalidator, createOptimisticUpdater, DEFAULT_QUERY_ENDPOINT, + type InferExtQueryArgs, type InferExtResult, type InferOptions, type InferSchema, @@ -37,6 +38,7 @@ import type { DeleteArgs, DeleteManyArgs, ExistsArgs, + ExtQueryArgsBase, ExtResultBase, FindFirstArgs, FindManyArgs, @@ -70,11 +72,12 @@ import type { ProcedureReturn, QueryContext, TransactionOperation, + TransactionResults, TrimSlicedOperations, WithOptimistic, } from './common/types.js'; export { AnyNull, DbNull, JsonNull } from '@zenstackhq/client-helpers'; -export type { InferExtResult, InferOptions, InferSchema } from '@zenstackhq/client-helpers'; +export type { InferExtQueryArgs, InferExtResult, InferOptions, InferSchema } from '@zenstackhq/client-helpers'; export type { FetchFn } from '@zenstackhq/client-helpers/fetch'; export type { SchemaDef } from '@zenstackhq/schema'; @@ -154,27 +157,77 @@ export type ModelMutationModelResult< ): Promise>; }; -export type TransactionMutationOptions = MaybeRefOrGetter< - Omit[]>>, 'mutationFn'> & +/** + * Options accepted by `$transaction.useSequential()`. + */ +export type TransactionMutationOptions< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = MaybeRefOrGetter< + Omit< + UnwrapRef< + UseMutationOptions< + unknown[], + DefaultError, + TransactionOperation[] + > + >, + 'mutationFn' + > & Omit >; +/** + * The return type of `$transaction.useSequential()`. Overrides `mutateAsync` so the + * resolved value is a tuple of per-operation results, narrowed to each operation's + * model + op + args (mirrors the typing of the corresponding ORM CRUD method). + */ +export type TransactionMutationResult< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = Omit< + UseMutationReturnType< + unknown[], + DefaultError, + TransactionOperation[], + unknown + >, + 'mutateAsync' +> & { + mutateAsync[]>( + operations: T, + options?: Omit< + UseMutationOptions, DefaultError, T>, + 'mutationFn' + >, + ): Promise>; +}; + +/** + * The full set of TanStack Query hooks returned by {@link useClientQueries}. + */ export type ClientHooks< Schema extends SchemaDef, Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, > = { [Model in GetSlicedModels as `${Uncapitalize}`]: ModelQueryHooks< Schema, Model, Options, + ExtQueryArgs, ExtResult >; } & ProcedureHooks & { $transaction: { useSequential( - options?: TransactionMutationOptions, - ): UseMutationReturnType[], unknown>; + options?: TransactionMutationOptions, + ): TransactionMutationResult; }; }; @@ -237,40 +290,41 @@ export type ModelQueryHooks< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, > = TrimSlicedOperations< Schema, Model, Options, { - useFindUnique>( - args: MaybeRefOrGetter>>, + useFindUnique>( + args: MaybeRefOrGetter>>, options?: MaybeRefOrGetter< ModelQueryOptions | null> >, ): ModelQueryResult | null>; - useFindFirst>( - args?: MaybeRefOrGetter>>, + useFindFirst>( + args?: MaybeRefOrGetter>>, options?: MaybeRefOrGetter< ModelQueryOptions | null> >, ): ModelQueryResult | null>; - useExists>( - args?: MaybeRefOrGetter>>, + useExists>( + args?: MaybeRefOrGetter>>, options?: MaybeRefOrGetter>, ): ModelQueryResult; - useFindMany>( - args?: MaybeRefOrGetter>>, + useFindMany>( + args?: MaybeRefOrGetter>>, options?: MaybeRefOrGetter< ModelQueryOptions[]> >, ): ModelQueryResult[]>; - useInfiniteFindMany, TPageParam = unknown>( - args?: MaybeRefOrGetter>>, + useInfiniteFindMany, TPageParam = unknown>( + args?: MaybeRefOrGetter>>, options?: MaybeRefOrGetter< ModelInfiniteQueryOptions[], TPageParam> >, @@ -278,66 +332,66 @@ export type ModelQueryHooks< InfiniteData[], TPageParam> >; - useCreate>( + useCreate>( options?: MaybeRefOrGetter< ModelMutationOptions, T> >, ): ModelMutationModelResult; - useCreateMany>( + useCreateMany>( options?: MaybeRefOrGetter>, ): ModelMutationResult; - useCreateManyAndReturn>( + useCreateManyAndReturn>( options?: MaybeRefOrGetter< ModelMutationOptions[], T> >, ): ModelMutationModelResult; - useUpdate>( + useUpdate>( options?: MaybeRefOrGetter< ModelMutationOptions, T> >, ): ModelMutationModelResult; - useUpdateMany>( + useUpdateMany>( options?: MaybeRefOrGetter>, ): ModelMutationResult; - useUpdateManyAndReturn>( + useUpdateManyAndReturn>( options?: MaybeRefOrGetter< ModelMutationOptions[], T> >, ): ModelMutationModelResult; - useUpsert>( + useUpsert>( options?: MaybeRefOrGetter< ModelMutationOptions, T> >, ): ModelMutationModelResult; - useDelete>( + useDelete>( options?: MaybeRefOrGetter< ModelMutationOptions, T> >, ): ModelMutationModelResult; - useDeleteMany>( + useDeleteMany>( options?: MaybeRefOrGetter>, ): ModelMutationResult; - useCount>( - args?: MaybeRefOrGetter>>, + useCount>( + args?: MaybeRefOrGetter>>, options?: MaybeRefOrGetter>>, ): ModelQueryResult>; - useAggregate>( - args: MaybeRefOrGetter>>, + useAggregate>( + args: MaybeRefOrGetter>>, options?: MaybeRefOrGetter>>, ): ModelQueryResult>; - useGroupBy>( - args: MaybeRefOrGetter>>, + useGroupBy>( + args: MaybeRefOrGetter>>, options?: MaybeRefOrGetter>>, ): ModelQueryResult>; } @@ -365,6 +419,7 @@ export function useClientQueries, InferOptions>, + InferExtQueryArgs extends ExtQueryArgsBase ? InferExtQueryArgs : {}, InferExtResult extends ExtResultBase> ? InferExtResult : {} @@ -433,12 +488,13 @@ export function useModelQueries< Schema extends SchemaDef, Model extends GetModels, Options extends QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, ExtResult extends ExtResultBase = {}, >( schema: Schema, model: Model, rootOptions?: MaybeRefOrGetter, -): ModelQueryHooks { +): ModelQueryHooks { const modelDef = Object.values(schema.models).find((m) => m.name.toLowerCase() === model.toLowerCase()); if (!modelDef) { throw new Error(`Model "${model}" not found in schema`); @@ -520,7 +576,7 @@ export function useModelQueries< useGroupBy: (args: any, options?: any) => { return useInternalQuery(schema, modelName, 'groupBy', args, merge(rootOptions, options)); }, - } as ModelQueryHooks; + } as ModelQueryHooks; } export function useInternalQuery( @@ -726,10 +782,15 @@ export function useInternalMutation( return useMutation(finalOptions); } -export function useInternalTransactionMutation( +export function useInternalTransactionMutation< + Schema extends SchemaDef, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +>( schema: Schema, - options?: TransactionMutationOptions, -) { + options?: TransactionMutationOptions, +): TransactionMutationResult { const queryClient = useQueryClient(); const { endpoint, fetch, logging } = useFetchOptions(options); @@ -751,7 +812,12 @@ export function useInternalTransactionMutation( return result; }); - return useMutation(finalOptions); + return useMutation(finalOptions as any) as unknown as TransactionMutationResult< + Schema, + Options, + ExtQueryArgs, + ExtResult + >; } function useFetchOptions(options: MaybeRefOrGetter) { diff --git a/packages/clients/tanstack-query/test/react/react-sliced-client.test-d.ts b/packages/clients/tanstack-query/test/react/react-sliced-client.test-d.ts index cc41fb731..73ded7efb 100644 --- a/packages/clients/tanstack-query/test/react/react-sliced-client.test-d.ts +++ b/packages/clients/tanstack-query/test/react/react-sliced-client.test-d.ts @@ -73,6 +73,41 @@ describe('React client sliced client test', () => { client.user.useFindMany({ where: { name: { contains: 'test' } } }); }); + it('respects slicing in sequential transaction op union', () => { + const _slicedTx = new ZenStackClient(schema, { + dialect: {} as any, + slicing: { + models: { + user: { + // user can only do reads — no writes in transactions + includedOperations: ['findUnique', 'findMany', 'count'], + }, + }, + }, + }); + const client = useClientQueries(schema); + const tx = client.$transaction.useSequential(); + + void async function () { + // included read ops are allowed + await tx.mutateAsync([ + { model: 'User', op: 'findMany' }, + { model: 'User', op: 'findUnique', args: { where: { id: '1' } } }, + { model: 'User', op: 'count' }, + ] as const); + + await tx.mutateAsync([ + // @ts-expect-error 'create' was sliced away by `includedOperations` + { model: 'User', op: 'create', args: { data: { email: 'a@b.com' } } }, + ] as const); + + await tx.mutateAsync([ + // @ts-expect-error 'delete' was sliced away by `includedOperations` + { model: 'User', op: 'delete', args: { where: { id: '1' } } }, + ] as const); + }; + }); + it('works with sliced procedures', () => { const _slicedProcs = new ZenStackClient(procSchema, { dialect: {} as any, diff --git a/packages/clients/tanstack-query/test/react/react-typing.test-d.ts b/packages/clients/tanstack-query/test/react/react-typing.test-d.ts index 10a565fbf..6008ad878 100644 --- a/packages/clients/tanstack-query/test/react/react-typing.test-d.ts +++ b/packages/clients/tanstack-query/test/react/react-typing.test-d.ts @@ -1,3 +1,4 @@ +import type { ClientContract, ClientOptions } from '@zenstackhq/orm'; import { describe, it } from 'vitest'; import { useClientQueries } from '../../src/react'; import { schema } from '../schemas/basic/schema-lite'; @@ -128,6 +129,74 @@ describe('React client typing test', () => { client.bar.useCreate(); }); + it('reflects ExtQueryArgs and ExtResult inferred from a ClientContract type', () => { + type DbType = ClientContract< + typeof schema, + ClientOptions, + // ExtQueryArgs: $read adds a `cache` filter to all read ops; $create adds a `bust` flag + { + $read: { cache?: { ttl?: number } }; + $create: { cache?: { bust?: boolean } }; + }, + // ExtClientMembers (unused here) + {}, + // ExtResult: User gains a computed `displayName` field + { + user: { + displayName: { + needs: { email: true }; + compute: (data: { email: string }) => string; + }; + }; + } + >; + + const client = useClientQueries(schema); + + // ExtQueryArgs $read flows into read ops + check(client.user.useFindMany({ cache: { ttl: 1000 } }).data?.[0]?.email); + check(client.user.useFindUnique({ where: { id: '1' }, cache: { ttl: 1000 } }).data?.email); + check(client.user.useCount({ cache: { ttl: 1000 } }).data); + + // @ts-expect-error: $read's cache shape doesn't accept `bust` + client.user.useFindMany({ cache: { bust: true } }); + + // ExtQueryArgs $create flows into useCreate + client.user.useCreate().mutate({ data: { email: 'a@b.com' }, cache: { bust: true } }); + + // @ts-expect-error: $create's cache shape doesn't accept `ttl` + client.user.useCreate().mutate({ data: { email: 'a@b.com' }, cache: { ttl: 1000 } }); + + // ExtResult: `displayName` is added to User read results + const findUniqueData = client.user.useFindUnique({ where: { id: '1' } }).data; + check(findUniqueData?.displayName); + + const findManyData = client.user.useFindMany().data; + check(findManyData?.[0]?.displayName); + + // ExtResult: `displayName` is also present on mutation return + client.user + .useCreate() + .mutateAsync({ data: { email: 'a@b.com' } }) + .then((d) => check(d.displayName)); + + // Transaction: ExtQueryArgs flows through to operation args + const tx = client.$transaction.useSequential(); + void async function () { + const r = await tx.mutateAsync([ + { model: 'User', op: 'findMany', args: { cache: { ttl: 500 } } }, + { model: 'User', op: 'create', args: { data: { email: 'a@b.com' }, cache: { bust: true } } }, + ] as const); + + // ExtResult flows through to per-op transaction return + check(r[0][0]?.displayName); + check(r[1].displayName); + }; + + // @ts-expect-error: transaction args must respect ExtQueryArgs shape ($create has no ttl) + tx.mutateAsync([{ model: 'User', op: 'create', args: { data: { email: 'a' }, cache: { ttl: 1 } } }] as const); + }); + it('types procedure queries correctly', () => { const proceduresClient = useClientQueries(proceduresSchema); @@ -158,6 +227,6 @@ describe('React client typing test', () => { }); }); -function check(_value: unknown) { - // noop +function check(_value: T): T { + return _value; } diff --git a/packages/clients/tanstack-query/test/react/sequential-transaction.test.tsx b/packages/clients/tanstack-query/test/react/sequential-transaction.test.tsx index 78136865a..752f78a2b 100644 --- a/packages/clients/tanstack-query/test/react/sequential-transaction.test.tsx +++ b/packages/clients/tanstack-query/test/react/sequential-transaction.test.tsx @@ -3,6 +3,7 @@ */ import { act, renderHook, waitFor } from '@testing-library/react'; +import type { ClientContract, ClientOptions } from '@zenstackhq/orm'; import nock from 'nock'; import { describe, expect, it } from 'vitest'; import { getQueryKey } from '../../src/common/query-key'; @@ -14,54 +15,96 @@ import { BASE_URL, createWrapper, makeUrl, registerCleanup } from './helpers'; registerCleanup(); describe('Sequential transaction', () => { - it('works with sequential transaction and invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); + describe('Runtime behavior', () => { + it('works with sequential transaction and invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); - const users: any[] = []; - const posts: any[] = []; + const users: any[] = []; + const posts: any[] = []; - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data: users })) - .persist(); + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data: users })) + .persist(); - nock(makeUrl('Post', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data: posts })) - .persist(); + nock(makeUrl('Post', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data: posts })) + .persist(); - const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); - const { result: postResult } = renderHook(() => useClientQueries(schema).post.useFindMany(), { wrapper }); + const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); + const { result: postResult } = renderHook(() => useClientQueries(schema).post.useFindMany(), { wrapper }); - await waitFor(() => { - expect(userResult.current.data).toHaveLength(0); - expect(postResult.current.data).toHaveLength(0); - }); + await waitFor(() => { + expect(userResult.current.data).toHaveLength(0); + expect(postResult.current.data).toHaveLength(0); + }); - nock(`${BASE_URL}/api/model/$transaction/sequential`) - .post(/.*/) - .reply(200, () => { - users.push({ id: '1', email: 'foo@bar.com' }); - posts.push({ id: 'p1', title: 'Hello' }); - return { data: [users[0], posts[0]] }; + nock(`${BASE_URL}/api/model/$transaction/sequential`) + .post(/.*/) + .reply(200, () => { + users.push({ id: '1', email: 'foo@bar.com' }); + posts.push({ id: 'p1', title: 'Hello' }); + return { data: [users[0], posts[0]] }; + }); + + const { result: txResult } = renderHook(() => useClientQueries(schema).$transaction.useSequential(), { + wrapper, }); - const { result: txResult } = renderHook(() => useClientQueries(schema).$transaction.useSequential(), { - wrapper, + act(() => + txResult.current.mutate([ + { model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }, + { model: 'Post', op: 'create', args: { data: { title: 'Hello' } } }, + ]), + ); + + await waitFor(() => { + const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + const cachedPosts = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); + expect(cachedUsers).toHaveLength(1); + expect(cachedPosts).toHaveLength(1); + }); }); - act(() => - txResult.current.mutate([ - { model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }, - { model: 'Post', op: 'create', args: { data: { title: 'Hello' } } }, - ]), - ); - - await waitFor(() => { - const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - const cachedPosts = queryClient.getQueryData(getQueryKey('Post', 'findMany', undefined)); - expect(cachedUsers).toHaveLength(1); - expect(cachedPosts).toHaveLength(1); + it('works with sequential transaction and no invalidation', async () => { + const { queryClient, wrapper } = createWrapper(); + + const users: any[] = []; + + nock(makeUrl('User', 'findMany')) + .get(/.*/) + .reply(200, () => ({ data: users })) + .persist(); + + const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); + + await waitFor(() => { + expect(userResult.current.data).toHaveLength(0); + }); + + nock(`${BASE_URL}/api/model/$transaction/sequential`) + .post(/.*/) + .reply(200, () => { + users.push({ id: '1', email: 'foo@bar.com' }); + return { data: [users[0]] }; + }); + + const { result: txResult } = renderHook( + () => useClientQueries(schema).$transaction.useSequential({ invalidateQueries: false }), + { wrapper }, + ); + + act(() => + txResult.current.mutate([{ model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }]), + ); + + await waitFor(() => { + expect(txResult.current.isSuccess).toBe(true); + // cache not refreshed because invalidation was disabled + const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); + expect(cachedUsers).toHaveLength(0); + }); }); }); @@ -114,6 +157,51 @@ describe('Sequential transaction', () => { expect([badCreate, badUpdate, badDelete, badFindUnique, badUpsert, badGroupBy]).toHaveLength(6); }); + it('infers per-op result types on mutateAsync', () => { + const { wrapper } = createWrapper(); + const { result: txResult } = renderHook(() => useClientQueries(schema).$transaction.useSequential(), { + wrapper, + }); + + // Inline tuple — TS should infer each result element's shape. + void async function () { + const results = await txResult.current.mutateAsync([ + { model: 'User', op: 'create', args: { data: { email: 'a@b.com' } } }, + { model: 'Post', op: 'findFirst', args: { where: { id: '1' } } }, + { model: 'User', op: 'findMany' }, + { model: 'User', op: 'count' }, + { model: 'User', op: 'deleteMany' }, + { model: 'User', op: 'exists' }, + ] as const); + + // create → User + check(results[0].id); + check(results[0].email); + + // findFirst → Post | null + check(results[1]?.id); + check(results[1]?.title); + // null is allowed + const _maybeNull: (typeof results)[1] = null; + void _maybeNull; + + // findMany → User[] + check(results[2][0]?.email); + + // count → number (no select arg) + check(results[3]); + + // deleteMany → BatchResult + check(results[4].count); + + // exists → boolean + check(results[5]); + + // @ts-expect-error wrong field on User + check(results[0].nonExistent); + }; + }); + it('rejects create-style ops on delegate models that disallow create', () => { // 'Foo' is a delegate model — create-style ops are filtered out of the union @@ -134,41 +222,178 @@ describe('Sequential transaction', () => { }); }); - it('works with sequential transaction and no invalidation', async () => { - const { queryClient, wrapper } = createWrapper(); - - const users: any[] = []; - - nock(makeUrl('User', 'findMany')) - .get(/.*/) - .reply(200, () => ({ data: users })) - .persist(); + describe('generic parameter influence (Options / ExtQueryArgs / ExtResult)', () => { + // A typed `ClientContract` standing in for what `useClientQueries(schema)` + // would receive when a real client (with plugins applied) is passed. Forwarded + // generics flow into the transaction operation args and per-op result shapes. + type DbType = ClientContract< + typeof schema, + ClientOptions, + // ExtQueryArgs: per-bucket extension keys + { + $read: { cache?: { ttl?: number } }; + $create: { audit?: { user?: string } }; + $update: { audit?: { user?: string } }; + $delete: { audit?: { user?: string } }; + }, + // ExtClientMembers (unused here) + {}, + // ExtResult: User gains a computed `displayName` field + { + user: { + displayName: { + needs: { email: true }; + compute: (data: { email: string }) => string; + }; + }; + } + >; + + // The negative @ts-expect-error checks below assign each operation to a + // concrete `TxOp` annotation, which forces TS to apply excess-property + // checking on the literal. (Inline `mutateAsync([...])` calls capture the + // tuple via a `const T extends ...[]` generic, where structural subtype + // assignability allows extra properties — so negative cases need the + // explicit annotation to fire.) + type TxOp = TransactionOperation< + typeof schema, + ClientOptions, + // mirror DbType's ExtQueryArgs + { + $read: { cache?: { ttl?: number } }; + $create: { audit?: { user?: string } }; + $update: { audit?: { user?: string } }; + $delete: { audit?: { user?: string } }; + }, + // ExtResult is irrelevant for arg-shape tests + {} + >; + + it('threads ExtQueryArgs `$read` into read ops only', () => { + const { wrapper } = createWrapper(); + const { result: txResult } = renderHook( + () => useClientQueries(schema).$transaction.useSequential(), + { wrapper }, + ); + + void async function () { + // positive: `$read`'s `cache` flows into every read op + await txResult.current.mutateAsync([ + { model: 'User', op: 'findMany', args: { cache: { ttl: 1000 } } }, + { model: 'User', op: 'findUnique', args: { where: { id: '1' }, cache: { ttl: 1000 } } }, + { model: 'User', op: 'findFirst', args: { cache: { ttl: 500 } } }, + { model: 'User', op: 'count', args: { cache: { ttl: 1000 } } }, + { model: 'User', op: 'exists', args: { cache: { ttl: 1000 } } }, + ] as const); + }; - const { result: userResult } = renderHook(() => useClientQueries(schema).user.useFindMany(), { wrapper }); + // negative: `$read.cache` doesn't apply to `create` + const badCreate: TxOp = { + model: 'User', + op: 'create', + // @ts-expect-error excess `cache` on a write op + args: { data: { email: 'a@b.com' }, cache: { ttl: 1000 } }, + }; + // negative: `$create`'s `audit` doesn't apply to read ops + // @ts-expect-error excess `audit` on a read op + const badFindMany: TxOp = { model: 'User', op: 'findMany', args: { audit: { user: 'admin' } } }; - await waitFor(() => { - expect(userResult.current.data).toHaveLength(0); + expect([badCreate, badFindMany]).toHaveLength(2); }); - nock(`${BASE_URL}/api/model/$transaction/sequential`) - .post(/.*/) - .reply(200, () => { - users.push({ id: '1', email: 'foo@bar.com' }); - return { data: [users[0]] }; - }); + it('threads ExtQueryArgs `$create` / `$update` / `$delete` into the matching write ops', () => { + const { wrapper } = createWrapper(); + const { result: txResult } = renderHook( + () => useClientQueries(schema).$transaction.useSequential(), + { wrapper }, + ); + + void async function () { + // positive: each write bucket's extension flows into its own ops + await txResult.current.mutateAsync([ + { + model: 'User', + op: 'create', + args: { data: { email: 'a@b.com' }, audit: { user: 'admin' } }, + }, + { + model: 'User', + op: 'update', + args: { where: { id: '1' }, data: { email: 'b@c.com' }, audit: { user: 'admin' } }, + }, + { + model: 'User', + op: 'delete', + args: { where: { id: '1' }, audit: { user: 'admin' } }, + }, + ] as const); + }; - const { result: txResult } = renderHook( - () => useClientQueries(schema).$transaction.useSequential({ invalidateQueries: false }), - { wrapper }, - ); + // negative: `$update`'s `audit` doesn't apply to read ops + // @ts-expect-error excess `audit` on a read op + const badRead: TxOp = { model: 'User', op: 'count', args: { audit: { user: 'admin' } } }; - act(() => txResult.current.mutate([{ model: 'User', op: 'create', args: { data: { email: 'foo@bar.com' } } }])); + expect(badRead).toBeDefined(); + }); - await waitFor(() => { - expect(txResult.current.isSuccess).toBe(true); - // cache not refreshed because invalidation was disabled - const cachedUsers = queryClient.getQueryData(getQueryKey('User', 'findMany', undefined)); - expect(cachedUsers).toHaveLength(0); + it('threads ExtResult into transaction per-op return types', () => { + const { wrapper } = createWrapper(); + const { result: txResult } = renderHook( + () => useClientQueries(schema).$transaction.useSequential(), + { wrapper }, + ); + + void async function () { + const r = await txResult.current.mutateAsync([ + { model: 'User', op: 'create', args: { data: { email: 'a@b.com' } } }, + { model: 'User', op: 'findFirst' }, + { model: 'User', op: 'findMany' }, + { model: 'User', op: 'update', args: { where: { id: '1' }, data: {} } }, + { model: 'User', op: 'upsert', args: { where: { id: '1' }, create: { email: 'a' }, update: {} } }, + { model: 'User', op: 'delete', args: { where: { id: '1' } } }, + ] as const); + + // `displayName` (from ExtResult) is present on every entity-returning op + check(r[0].displayName); + check(r[1]?.displayName); + check(r[2][0]?.displayName); + check(r[3].displayName); + check(r[4].displayName); + check(r[5].displayName); + }; + }); + + it('respects ExtQueryArgs across non-`User` models too', () => { + const { wrapper } = createWrapper(); + const { result: txResult } = renderHook( + () => useClientQueries(schema).$transaction.useSequential(), + { wrapper }, + ); + + void async function () { + // `$read` extension also applies to Post's reads + await txResult.current.mutateAsync([ + { model: 'Post', op: 'findMany', args: { cache: { ttl: 1000 } } }, + { model: 'Post', op: 'count', args: { cache: { ttl: 1000 } } }, + ] as const); + + // `$create` extension also applies to Post's writes + await txResult.current.mutateAsync([ + { + model: 'Post', + op: 'create', + args: { + data: { title: 'hello', author: { connect: { id: '1' } } }, + audit: { user: 'admin' }, + }, + }, + ] as const); + }; }); }); }); + +// Type-only assertion: forces `value` to be assignable to `T` at compile time. +function check(value: T): T { + return value; +} diff --git a/packages/orm/src/client/contract.ts b/packages/orm/src/client/contract.ts index 29e2456e0..3f9fb493a 100644 --- a/packages/orm/src/client/contract.ts +++ b/packages/orm/src/client/contract.ts @@ -11,32 +11,13 @@ import type { AnyKysely } from '../utils/kysely-utils'; import type { Simplify, UnwrapTuplePromises } from '../utils/type-utils'; import type { TRANSACTION_UNSUPPORTED_METHODS } from './constants'; import type { - AggregateArgs, - AggregateResult, - BatchResult, - CountArgs, - CountResult, - CreateArgs, - CreateManyAndReturnArgs, - CreateManyArgs, + CrudArgsType, + CrudReturnType, DefaultModelResult, - DeleteArgs, - DeleteManyArgs, - ExistsArgs, - FindFirstArgs, - FindManyArgs, - FindUniqueArgs, - GroupByArgs, - GroupByResult, ProcedureFunc, SelectSubset, - SimplifiedPlainResult, Subset, TypeDefResult, - UpdateArgs, - UpdateManyAndReturnArgs, - UpdateManyArgs, - UpsertArgs, } from './crud-types'; import type { Diagnostics } from './diagnostics'; import type { ClientOptions, QueryOptions } from './options'; @@ -65,6 +46,21 @@ export enum TransactionIsolationLevel { Snapshot = 'snapshot', } +/** + * Symbol used as a type-only key on `ClientContract` to brand the `ExtQueryArgs` + * generic slot. Hidden from member-access autocomplete since symbol keys are + * not surfaced. Consumed by `InferExtQueryArgs` to recover the slot. + * @internal + */ +export const ExtQueryArgsMarker: unique symbol = Symbol('zenstack.client.extQueryArgs'); + +/** + * Symbol used as a type-only key on `ClientContract` to brand the `ExtResult` + * generic slot. Consumed by `InferExtResult` to recover the slot. + * @internal + */ +export const ExtResultMarker: unique symbol = Symbol('zenstack.client.extResult'); + /** * ZenStack client interface. */ @@ -85,6 +81,12 @@ export type ClientContract< */ readonly $options: Options; + /** @internal type-only brand carrying the `ExtQueryArgs` slot for inference. */ + readonly [ExtQueryArgsMarker]?: ExtQueryArgs; + + /** @internal type-only brand carrying the `ExtResult` slot for inference. */ + readonly [ExtResultMarker]?: ExtResult; + /** * Executes a prepared raw query and returns the number of affected rows. * @example @@ -379,9 +381,17 @@ export type AllModelOperations< * }); * ``` */ - createManyAndReturn>( - args?: SelectSubset>, - ): ZenStackPromise[]>; + createManyAndReturn< + T extends CrudArgsType, + >( + args?: SelectSubset< + T, + CrudArgsType + >, + ): ZenStackPromise< + Schema, + CrudReturnType + >; /** * Updates multiple entities and returns them. @@ -405,9 +415,17 @@ export type AllModelOperations< * }); * ``` */ - updateManyAndReturn>( - args: Subset>, - ): ZenStackPromise[]>; + updateManyAndReturn< + T extends CrudArgsType, + >( + args: Subset< + T, + CrudArgsType + >, + ): ZenStackPromise< + Schema, + CrudReturnType + >; }); type CommonModelOperations< @@ -498,9 +516,9 @@ type CommonModelOperations< * }); // result: `{ _count: { posts: number } }` * ``` */ - findMany>( - args?: SelectSubset>, - ): ZenStackPromise[]>; + findMany>( + args?: SelectSubset>, + ): ZenStackPromise>; /** * Returns a uniquely identified entity. @@ -508,9 +526,9 @@ type CommonModelOperations< * @returns a single entity or null if not found * @see {@link findMany} */ - findUnique>( - args: SelectSubset>, - ): ZenStackPromise | null>; + findUnique>( + args: SelectSubset>, + ): ZenStackPromise>; /** * Returns a uniquely identified entity or throws `NotFoundError` if not found. @@ -518,9 +536,9 @@ type CommonModelOperations< * @returns a single entity * @see {@link findMany} */ - findUniqueOrThrow>( - args: SelectSubset>, - ): ZenStackPromise>; + findUniqueOrThrow>( + args: SelectSubset>, + ): ZenStackPromise>; /** * Returns the first entity. @@ -528,9 +546,9 @@ type CommonModelOperations< * @returns a single entity or null if not found * @see {@link findMany} */ - findFirst>( - args?: SelectSubset>, - ): ZenStackPromise | null>; + findFirst>( + args?: SelectSubset>, + ): ZenStackPromise>; /** * Returns the first entity or throws `NotFoundError` if not found. @@ -538,9 +556,9 @@ type CommonModelOperations< * @returns a single entity * @see {@link findMany} */ - findFirstOrThrow>( - args?: SelectSubset>, - ): ZenStackPromise>; + findFirstOrThrow>( + args?: SelectSubset>, + ): ZenStackPromise>; /** * Creates a new entity. @@ -594,9 +612,9 @@ type CommonModelOperations< * }); * ``` */ - create>( - args: SelectSubset>, - ): ZenStackPromise>; + create>( + args: SelectSubset>, + ): ZenStackPromise>; /** * Creates multiple entities. Only scalar fields are allowed. @@ -623,9 +641,9 @@ type CommonModelOperations< * }); * ``` */ - createMany>( - args?: SelectSubset>, - ): ZenStackPromise; + createMany>( + args?: SelectSubset>, + ): ZenStackPromise>; /** * Updates a uniquely identified entity. @@ -744,9 +762,9 @@ type CommonModelOperations< * }); * ``` */ - update>( - args: SelectSubset>, - ): ZenStackPromise>; + update>( + args: SelectSubset>, + ): ZenStackPromise>; /** * Updates multiple entities. @@ -768,9 +786,9 @@ type CommonModelOperations< * limit: 10 * }); */ - updateMany>( - args: Subset>, - ): ZenStackPromise; + updateMany>( + args: Subset>, + ): ZenStackPromise>; /** * Creates or updates an entity. @@ -792,9 +810,9 @@ type CommonModelOperations< * }); * ``` */ - upsert>( - args: SelectSubset>, - ): ZenStackPromise>; + upsert>( + args: SelectSubset>, + ): ZenStackPromise>; /** * Deletes a uniquely identifiable entity. @@ -815,9 +833,9 @@ type CommonModelOperations< * }); // result: `{ id: string; email: string }` * ``` */ - delete>( - args: SelectSubset>, - ): ZenStackPromise>; + delete>( + args: SelectSubset>, + ): ZenStackPromise>; /** * Deletes multiple entities. @@ -838,9 +856,9 @@ type CommonModelOperations< * }); * ``` */ - deleteMany>( - args?: Subset>, - ): ZenStackPromise; + deleteMany>( + args?: Subset>, + ): ZenStackPromise>; /** * Counts rows or field values. @@ -860,9 +878,9 @@ type CommonModelOperations< * select: { _all: true, email: true } * }); // result: `{ _all: number, email: number }` */ - count>( - args?: Subset>, - ): ZenStackPromise>>; + count>( + args?: Subset>, + ): ZenStackPromise>>; /** * Aggregates rows. @@ -881,9 +899,9 @@ type CommonModelOperations< * _max: { age: true } * }); // result: `{ _count: number, _avg: { age: number }, ... }` */ - aggregate>( - args: Subset>, - ): ZenStackPromise>>; + aggregate>( + args: Subset>, + ): ZenStackPromise>>; /** * Groups rows by columns. @@ -918,9 +936,9 @@ type CommonModelOperations< * having: { country: 'US', age: { _avg: { gte: 18 } } } * }); */ - groupBy>( - args: Subset>, - ): ZenStackPromise>>; + groupBy>( + args: Subset>, + ): ZenStackPromise>>; /** * Checks if an entity exists. @@ -939,9 +957,9 @@ type CommonModelOperations< * where: { posts: { some: { published: true } } }, * }); // result: `boolean` */ - exists>( - args?: Subset>, - ): ZenStackPromise; + exists>( + args?: Subset>, + ): ZenStackPromise>; }; export type OperationsRequiringCreate = 'create' | 'createMany' | 'createManyAndReturn' | 'upsert'; diff --git a/packages/orm/src/client/crud-types.ts b/packages/orm/src/client/crud-types.ts index b5981c797..db5b3c21d 100644 --- a/packages/orm/src/client/crud-types.ts +++ b/packages/orm/src/client/crud-types.ts @@ -1,5 +1,3 @@ -import type { ExpressionBuilder, OperandExpression, SqlBool } from 'kysely'; -import type { DbNull, JsonNull, JsonNullValues, JsonValue } from '../common-types'; import type { BuiltinType, FieldDef, @@ -35,6 +33,8 @@ import type { TypeDefFieldIsOptional, UpdatedAtInfo, } from '@zenstackhq/schema'; +import type { ExpressionBuilder, OperandExpression, SqlBool } from 'kysely'; +import type { DbNull, JsonNull, JsonNullValues, JsonValue } from '../common-types'; import type { AtLeast, MapBaseType, @@ -53,6 +53,7 @@ import type { } from '../utils/type-utils'; import type { ClientContract } from './contract'; import type { + AllCrudOperations, CoreCreateOperations, CoreCrudOperations, CoreDeleteOperations, @@ -2359,6 +2360,94 @@ export type GroupByResult< // #endregion +// #region Op maps + +/** + * Maps each CRUD operation name to its argument type for a given model. + */ +export type CrudArgsMap< + Schema extends SchemaDef, + Model extends GetModels, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = { + findMany: FindManyArgs; + findUnique: FindUniqueArgs; + findUniqueOrThrow: FindUniqueArgs; + findFirst: FindFirstArgs; + findFirstOrThrow: FindFirstArgs; + create: CreateArgs; + createMany: CreateManyArgs; + createManyAndReturn: CreateManyAndReturnArgs; + update: UpdateArgs; + updateMany: UpdateManyArgs; + updateManyAndReturn: UpdateManyAndReturnArgs; + upsert: UpsertArgs; + delete: DeleteArgs; + deleteMany: DeleteManyArgs; + count: CountArgs; + aggregate: AggregateArgs; + groupBy: GroupByArgs; + exists: ExistsArgs; +}; + +/** + * Maps a CRUD operation name to its argument type for a given model. + */ +export type CrudArgsType< + Schema extends SchemaDef, + Model extends GetModels, + Op extends AllCrudOperations, + Options extends QueryOptions = QueryOptions, + ExtQueryArgs extends ExtQueryArgsBase = {}, + ExtResult extends ExtResultBase = {}, +> = CrudArgsMap[Op]; + +/** + * Maps each CRUD operation name to its return type for a given model + args. + */ +export type CrudReturnMap< + Schema extends SchemaDef, + Model extends GetModels, + Args, + Options extends QueryOptions = QueryOptions, + ExtResult extends ExtResultBase = {}, +> = { + findMany: SimplifiedPlainResult[]; + findUnique: SimplifiedPlainResult | null; + findUniqueOrThrow: SimplifiedPlainResult; + findFirst: SimplifiedPlainResult | null; + findFirstOrThrow: SimplifiedPlainResult; + create: SimplifiedPlainResult; + createMany: BatchResult; + createManyAndReturn: SimplifiedPlainResult[]; + update: SimplifiedPlainResult; + updateMany: BatchResult; + updateManyAndReturn: SimplifiedPlainResult[]; + upsert: SimplifiedPlainResult; + delete: SimplifiedPlainResult; + deleteMany: BatchResult; + count: CountResult; + aggregate: AggregateResult; + groupBy: Args extends { by: unknown } ? GroupByResult : never; + exists: boolean; +}; + +/** + * Maps a CRUD operation name to its return type for a given model + args. + */ +export type CrudReturnType< + Schema extends SchemaDef, + Model extends GetModels, + Op extends AllCrudOperations, + Args, + Options extends QueryOptions = QueryOptions, + ExtResult extends ExtResultBase = {}, +> = CrudReturnMap[Op]; + +// #endregion + // #region Relation manipulation type ConnectInput< diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 360915de0..b88ed8ff1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -361,6 +361,9 @@ importers: '@zenstackhq/common-helpers': specifier: workspace:* version: link:../../common-helpers + '@zenstackhq/orm': + specifier: workspace:* + version: link:../../orm '@zenstackhq/schema': specifier: workspace:* version: link:../../schema @@ -401,9 +404,6 @@ importers: '@zenstackhq/language': specifier: workspace:* version: link:../../language - '@zenstackhq/orm': - specifier: workspace:* - version: link:../../orm '@zenstackhq/sdk': specifier: workspace:* version: link:../../sdk diff --git a/tests/e2e/performance/tsc-torture/main.ts b/tests/e2e/performance/tsc-torture/main.ts new file mode 100644 index 000000000..8461a6dec --- /dev/null +++ b/tests/e2e/performance/tsc-torture/main.ts @@ -0,0 +1,647 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ + +// Type-checking-only torture file. NOT a vitest test (intentionally `.ts`, +// not `.test.ts`) — exists solely so `pnpm test:typecheck` exercises a +// large, deeply-nested ORM workload against `tsc` to surface compiler +// performance regressions. Never executed at runtime. + +import { ZenStackClient } from '@zenstackhq/orm'; +import { SqliteDialect } from '@zenstackhq/orm/dialects/sqlite'; +import Database from 'better-sqlite3'; +import { schema } from './zenstack/schema'; + +function createClient() { + return new ZenStackClient(schema, { + dialect: new SqliteDialect({ database: new Database(':memory:') }), + }); +} + +type Client = ReturnType; + +// ─── Cleanup: delete all rows in FK-safe leaf-to-root order ───────────────── + +async function cleanupAll(db: Client) { + // Concrete delegate subtypes first (they hold the extra columns) + await db.approvalNotification.deleteMany(); + await db.reviewRequestNotification.deleteMany(); + await db.commentNotification.deleteMany(); + await db.statusChangeNotification.deleteMany(); + await db.assignmentNotification.deleteMany(); + await db.mentionNotification.deleteMany(); + await db.attachmentComment.deleteMany(); + await db.codeSnippetComment.deleteMany(); + await db.textComment.deleteMany(); + // Base delegate models (rows remaining after concrete removal) + await db.notification.deleteMany(); + await db.comment.deleteMany(); + // Leaf / junction tables + await db.commentReaction.deleteMany(); + await db.reviewComment.deleteMany(); + await db.review.deleteMany(); + await db.timeEntry.deleteMany(); + await db.activityLogEntry.deleteMany(); + await db.auditLog.deleteMany(); + await db.attachment.deleteMany(); + await db.taskLabel.deleteMany(); + await db.customFieldValue.deleteMany(); + await db.task.deleteMany(); + await db.label.deleteMany(); + await db.integrationLink.deleteMany(); + await db.integration.deleteMany(); + await db.documentSection.deleteMany(); + await db.document.deleteMany(); + await db.sprint.deleteMany(); + await db.milestone.deleteMany(); + await db.projectTeamAssignment.deleteMany(); + await db.project.deleteMany(); + await db.invoiceLineItem.deleteMany(); + await db.invoice.deleteMany(); + await db.billingInfo.deleteMany(); + await db.customFieldDefinition.deleteMany(); + await db.teamMember.deleteMany(); + await db.team.deleteMany(); + await db.apiToken.deleteMany(); + await db.userPreferences.deleteMany(); + await db.organizationMember.deleteMany(); + await db.user.deleteMany(); + await db.organization.deleteMany(); +} + +// ─── Seed: deep nested creates across multiple calls ───────────────────────── + +async function seedDeep(db: Client) { + // Step 1: create users + const alice = await db.user.create({ + data: { + email: 'alice@acme.com', + username: 'alice', + displayName: 'Alice', + userPreferences: { + create: { emailNotifications: true, theme: 'dark' }, + }, + apiTokens: { + create: [ + { name: 'CI token', tokenHash: 'hash-alice-ci' }, + { name: 'Dev token', tokenHash: 'hash-alice-dev' }, + ], + }, + }, + include: { userPreferences: true, apiTokens: true }, + }); + + const bob = await db.user.create({ + data: { + email: 'bob@acme.com', + username: 'bob', + displayName: 'Bob', + userPreferences: { create: { emailNotifications: false, theme: 'light' } }, + }, + include: { userPreferences: true }, + }); + + // Step 2: organization with billing, teams, integrations, custom fields + const org = await db.organization.create({ + data: { + name: 'Acme Corp', + slug: 'acme', + members: { + create: [ + { role: 'ADMIN', user: { connect: { id: alice.id } } }, + { role: 'MEMBER', user: { connect: { id: bob.id } } }, + ], + }, + teams: { + create: [{ name: 'Engineering', color: '#0066cc' }], + }, + billingInfo: { + create: { + planName: 'Pro', + billingEmail: 'billing@acme.com', + paymentMethod: 'CREDIT_CARD', + invoices: { + create: [ + { + number: 'INV-001', + amountCents: 9900, + dueDate: new Date('2026-05-01'), + status: 'SENT', + lineItems: { + create: [ + { description: 'Pro plan — April 2026', quantity: 1, unitCents: 9900 }, + ], + }, + }, + ], + }, + }, + }, + customFields: { + create: [ + { name: 'Business Unit', fieldType: 'text' }, + { name: 'Risk Score', fieldType: 'number', required: false }, + ], + }, + integrations: { + create: [ + { provider: 'github', config: JSON.stringify({ repo: 'acme/monorepo' }) }, + { provider: 'slack', config: JSON.stringify({ channel: '#eng' }) }, + ], + }, + }, + }); + + // Step 3: project with labels, milestones, sprints, documents + const project = await db.project.create({ + data: { + name: 'Titan Platform', + slug: 'titan', + description: 'Next-gen platform rebuild', + status: 'ACTIVE', + organization: { connect: { id: org.id } }, + owner: { connect: { id: alice.id } }, + labels: { + create: [ + { name: 'bug', color: '#e11d48' }, + { name: 'feature', color: '#16a34a' }, + { name: 'chore', color: '#ca8a04' }, + ], + }, + milestones: { + create: [ + { name: 'Alpha', dueDate: new Date('2026-06-01') }, + { name: 'Beta', dueDate: new Date('2026-08-01') }, + ], + }, + sprints: { + create: [ + { + name: 'Sprint 1', + goal: 'Set up CI/CD', + startDate: new Date('2026-04-21'), + endDate: new Date('2026-05-04'), + }, + ], + }, + documents: { + create: [ + { + title: 'Architecture Decision Records', + published: true, + sections: { + create: [ + { order: 1, heading: 'ADR-001: Database choice', content: 'We chose SQLite.' }, + { order: 2, heading: 'ADR-002: Auth strategy', content: 'JWT with refresh tokens.' }, + ], + }, + }, + ], + }, + }, + include: { + labels: true, + milestones: true, + sprints: true, + }, + }); + + const alphaMilestone = project.milestones.find((m) => m.name === 'Alpha')!; + const sprint = project.sprints[0]!; + const featureLabel = project.labels.find((l) => l.name === 'feature')!; + + // Step 4: tasks + const bootstrapTask = await db.task.create({ + data: { + title: 'Bootstrap repo', + status: 'DONE', + priority: 'HIGH', + project: { connect: { id: project.id } }, + milestone: { connect: { id: alphaMilestone.id } }, + sprint: { connect: { id: sprint.id } }, + reporter: { connect: { id: alice.id } }, + assignee: { connect: { id: alice.id } }, + attachments: { + create: [ + { + filename: 'screenshot.png', + mimeType: 'image/png', + sizeBytes: 204800, + storageKey: 's3://bucket/screenshot.png', + }, + ], + }, + labels: { + create: [{ label: { connect: { id: featureLabel.id } } }], + }, + }, + }); + + // Step 5: delegate comment types — TextComment, CodeSnippetComment, AttachmentComment + + // TextComment with reactions and a reply + const mainComment = await db.textComment.create({ + data: { + body: 'Bootstrap is complete! Repo is live.', + task: { connect: { id: bootstrapTask.id } }, + author: { connect: { id: alice.id } }, + reactions: { + create: [{ emoji: '🎉' }, { emoji: '👍' }], + }, + }, + include: { reactions: true }, + }); + + // Reply (TextComment) to mainComment + await db.textComment.create({ + data: { + body: 'Great work, Alice!', + task: { connect: { id: bootstrapTask.id } }, + author: { connect: { id: bob.id } }, + parent: { connect: { id: mainComment.id } }, + }, + }); + + // CodeSnippetComment — shows CI config snippet + await db.codeSnippetComment.create({ + data: { + body: 'name: CI\non: [push]\njobs:\n test:\n runs-on: ubuntu-latest', + language: 'yaml', + task: { connect: { id: bootstrapTask.id } }, + author: { connect: { id: alice.id } }, + }, + }); + + // AttachmentComment — inline file metadata + await db.attachmentComment.create({ + data: { + body: 'Attaching the architecture diagram for reference.', + task: { connect: { id: bootstrapTask.id } }, + author: { connect: { id: alice.id } }, + attachFilename: 'arch-diagram.png', + attachMimeType: 'image/png', + attachSizeBytes: 512000, + attachStorageKey: 's3://bucket/arch-diagram.png', + }, + }); + + // Step 6: subtask with review + const testTask = await db.task.create({ + data: { + title: 'Write unit tests', + status: 'IN_PROGRESS', + priority: 'MEDIUM', + project: { connect: { id: project.id } }, + milestone: { connect: { id: alphaMilestone.id } }, + sprint: { connect: { id: sprint.id } }, + reporter: { connect: { id: bob.id } }, + assignee: { connect: { id: bob.id } }, + parent: { connect: { id: bootstrapTask.id } }, + }, + }); + + const review = await db.review.create({ + data: { + task: { connect: { id: testTask.id } }, + reviewer: { connect: { id: alice.id } }, + decision: 'CHANGES_REQUESTED', + summary: 'Need more edge-case coverage', + comments: { + create: [ + { body: 'Add a test for the null path', lineRef: 'src/index.ts:42' }, + { body: 'Mock the DB layer here', lineRef: 'src/db.ts:17' }, + ], + }, + }, + include: { comments: true }, + }); + + await db.timeEntry.create({ + data: { + task: { connect: { id: testTask.id } }, + user: { connect: { id: bob.id } }, + startedAt: new Date('2026-04-22T09:00:00Z'), + stoppedAt: new Date('2026-04-22T11:30:00Z'), + durationMin: 150, + }, + }); + + // Step 7: typed notifications via delegate concrete models + await db.mentionNotification.create({ + data: { + user: { connect: { id: bob.id } }, + mentionedByUserId: alice.id, + taskId: bootstrapTask.id, + }, + }); + + await db.assignmentNotification.create({ + data: { + user: { connect: { id: bob.id } }, + taskId: testTask.id, + assignedByUserId: alice.id, + }, + }); + + await db.statusChangeNotification.create({ + data: { + user: { connect: { id: alice.id } }, + taskId: bootstrapTask.id, + fromStatus: 'IN_PROGRESS', + toStatus: 'DONE', + }, + }); + + await db.commentNotification.create({ + data: { + user: { connect: { id: alice.id } }, + commentId: mainComment.id, + }, + }); + + await db.reviewRequestNotification.create({ + data: { + user: { connect: { id: alice.id } }, + reviewId: review.id, + requestedByUserId: bob.id, + }, + }); + + await db.approvalNotification.create({ + data: { + user: { connect: { id: alice.id } }, + targetType: 'Task', + targetId: testTask.id, + }, + }); + + return { org, project, alice, bob, bootstrapTask, testTask, sprint, alphaMilestone }; +} + +// ─── Complex nested reads ───────────────────────────────────────────────────── + +async function runComplexReads(db: Client, orgId: number) { + // Read 1: org → billing → invoices → line-items (4 levels) + const orgWithBilling = await db.organization.findUnique({ + where: { id: orgId }, + include: { + billingInfo: { + include: { + invoices: { + include: { lineItems: true }, + where: { status: { in: ['SENT', 'OVERDUE'] } }, + }, + }, + }, + members: { + include: { + user: { + include: { + userPreferences: true, + apiTokens: { where: { expiresAt: null } }, + }, + }, + }, + }, + }, + }); + + // Read 2: project → milestones → tasks → subtasks → reviews (5 levels) + const projectDeep = await db.project.findFirst({ + where: { organizationId: orgId }, + include: { + milestones: { + include: { + tasks: { + include: { + subtasks: { + include: { + reviews: { + include: { comments: true, reviewer: true }, + }, + timeEntries: true, + assignee: true, + }, + }, + // Query all comment subtypes through the base relation + comments: { + include: { + reactions: true, + replies: { include: { author: true } }, + author: true, + }, + }, + labels: { include: { label: true } }, + attachments: true, + }, + orderBy: { createdAt: 'desc' }, + }, + }, + }, + sprints: { + include: { + tasks: { + include: { assignee: true, reporter: true }, + }, + }, + }, + documents: { include: { sections: { orderBy: { order: 'asc' } } } }, + labels: true, + }, + }); + + // Read 3: org → members → user → reportedTasks → comments → reactions (5 levels) + const orgActivity = await db.organization.findUnique({ + where: { id: orgId }, + include: { + members: { + include: { + user: { + include: { + reportedTasks: { + include: { + comments: { + include: { reactions: true, author: true }, + take: 5, + }, + milestone: true, + sprint: true, + labels: { include: { label: true } }, + }, + where: { status: { notIn: ['DONE', 'CANCELLED'] } }, + }, + assignedTasks: { + include: { + reviews: { + include: { + comments: true, + reviewer: { include: { userPreferences: true } }, + }, + }, + timeEntries: true, + subtasks: { include: { assignee: true } }, + }, + take: 10, + }, + }, + }, + }, + }, + teams: { + include: { + members: { include: { user: true } }, + projects: { + include: { + project: { include: { milestones: true, sprints: true } }, + }, + }, + }, + }, + }, + }); + + // Read 4: query base Notification — returns all subtypes with discriminated fields + const allNotifications = await db.notification.findMany({ + where: { userId: { gt: 0 } }, + include: { user: true }, + orderBy: { createdAt: 'desc' }, + }); + + // Read 5: query concrete comment subtypes separately + const codeComments = await db.codeSnippetComment.findMany({ + include: { + author: true, + task: { include: { project: true } }, + reactions: true, + }, + }); + + const attachmentComments = await db.attachmentComment.findMany({ + include: { + author: true, + task: { include: { project: true, milestone: true } }, + }, + }); + + // Read 6: custom fields → task → project (4 levels) + const customFieldValues = await db.customFieldValue.findMany({ + where: { field: { organizationId: orgId } }, + include: { + field: { include: { organization: true } }, + project: { include: { owner: true } }, + task: { + include: { + project: { include: { milestones: true } }, + assignee: true, + }, + }, + }, + }); + + return { + orgWithBilling, + projectDeep, + orgActivity, + allNotifications, + codeComments, + attachmentComments, + customFieldValues, + }; +} + +// ─── Mutations ──────────────────────────────────────────────────────────────── + +async function runMutations(db: Client, orgId: number, projectId: number, aliceId: number, bobId: number) { + const integration = await db.integration.findFirst({ + where: { organizationId: orgId, provider: 'github' }, + }); + + if (integration) { + await db.integrationLink.upsert({ + where: { + integrationId_externalId: { + integrationId: integration.id, + externalId: 'pr-42', + }, + }, + create: { + externalId: 'pr-42', + url: 'https://github.com/acme/monorepo/pull/42', + integration: { connect: { id: integration.id } }, + project: { connect: { id: projectId } }, + }, + update: { url: 'https://github.com/acme/monorepo/pull/42' }, + }); + } + + await db.task.createMany({ + data: [ + { + title: 'Set up CI pipeline', + status: 'TODO', + priority: 'HIGH', + projectId, + reporterId: aliceId, + assigneeId: bobId, + }, + { title: 'Deploy to staging', status: 'BACKLOG', priority: 'MEDIUM', projectId, reporterId: aliceId }, + { + title: 'Load testing', + status: 'BACKLOG', + priority: 'LOW', + projectId, + reporterId: bobId, + assigneeId: bobId, + }, + ], + }); + + const taskCounts = await db.task.groupBy({ + by: ['status'], + where: { projectId }, + _count: { id: true }, + }); + + const storyPointSum = await db.task.aggregate({ + where: { projectId }, + _sum: { storyPoints: true }, + _count: { id: true }, + }); + + // Fan-out: typed notifications to all org members + const members = await db.organizationMember.findMany({ + where: { organizationId: orgId }, + }); + + for (const m of members) { + await db.statusChangeNotification.create({ + data: { + user: { connect: { id: m.userId } }, + taskId: 1, + fromStatus: 'BACKLOG', + toStatus: 'IN_PROGRESS', + }, + }); + } + + return { taskCounts, storyPointSum }; +} + +// ─── Entry point (never invoked — this file is type-check-only) ────────────── + +async function main() { + const db = createClient(); + await cleanupAll(db); + + const { org, project } = await seedDeep(db); + const reads = await runComplexReads(db, org.id); + const mutations = await runMutations( + db, + org.id, + project.id, + reads.orgWithBilling!.members[0]!.userId, + reads.orgWithBilling!.members[1]!.userId, + ); + + void mutations; +} + +void main; diff --git a/tests/e2e/performance/tsc-torture/tsconfig.test.json b/tests/e2e/performance/tsc-torture/tsconfig.test.json new file mode 100644 index 000000000..6b9056178 --- /dev/null +++ b/tests/e2e/performance/tsc-torture/tsconfig.test.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/tests/e2e/performance/tsc-torture/zenstack/schema.ts b/tests/e2e/performance/tsc-torture/zenstack/schema.ts new file mode 100644 index 000000000..37e56a71f --- /dev/null +++ b/tests/e2e/performance/tsc-torture/zenstack/schema.ts @@ -0,0 +1,3110 @@ +////////////////////////////////////////////////////////////////////////////////////////////// +// DO NOT MODIFY THIS FILE // +// This file is automatically generated by ZenStack CLI and should not be manually updated. // +////////////////////////////////////////////////////////////////////////////////////////////// + +/* eslint-disable */ + +import { type SchemaDef, type AttributeApplication, type FieldDefault, ExpressionUtils } from "@zenstackhq/schema"; +export class SchemaType implements SchemaDef { + provider = { + type: "sqlite" + } as const; + models = { + Organization: { + name: "Organization", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + name: { + name: "name", + type: "String" + }, + slug: { + name: "slug", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[] + }, + logoUrl: { + name: "logoUrl", + type: "String", + optional: true + }, + website: { + name: "website", + type: "String", + optional: true + }, + teams: { + name: "teams", + type: "Team", + array: true, + relation: { opposite: "organization" } + }, + members: { + name: "members", + type: "OrganizationMember", + array: true, + relation: { opposite: "organization" } + }, + projects: { + name: "projects", + type: "Project", + array: true, + relation: { opposite: "organization" } + }, + billingInfo: { + name: "billingInfo", + type: "BillingInfo", + optional: true, + relation: { opposite: "organization" } + }, + auditLogs: { + name: "auditLogs", + type: "AuditLog", + array: true, + relation: { opposite: "organization" } + }, + activityLog: { + name: "activityLog", + type: "ActivityLogEntry", + array: true, + relation: { opposite: "organization" } + }, + integrations: { + name: "integrations", + type: "Integration", + array: true, + relation: { opposite: "organization" } + }, + customFields: { + name: "customFields", + type: "CustomFieldDefinition", + array: true, + relation: { opposite: "organization" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + slug: { type: "String" } + } + }, + BillingInfo: { + name: "BillingInfo", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + planName: { + name: "planName", + type: "String" + }, + billingEmail: { + name: "billingEmail", + type: "String" + }, + paymentMethod: { + name: "paymentMethod", + type: "PaymentMethod" + }, + stripeCustomerId: { + name: "stripeCustomerId", + type: "String", + optional: true + }, + organization: { + name: "organization", + type: "Organization", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "billingInfo", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[], + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + invoices: { + name: "invoices", + type: "Invoice", + array: true, + relation: { opposite: "billingInfo" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + organizationId: { type: "Int" } + } + }, + Invoice: { + name: "Invoice", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + number: { + name: "number", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[] + }, + amountCents: { + name: "amountCents", + type: "Int" + }, + currency: { + name: "currency", + type: "String", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("USD") }] }] as readonly AttributeApplication[], + default: "USD" as FieldDefault + }, + status: { + name: "status", + type: "InvoiceStatus", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("DRAFT") }] }] as readonly AttributeApplication[], + default: "DRAFT" as FieldDefault + }, + dueDate: { + name: "dueDate", + type: "DateTime" + }, + paidAt: { + name: "paidAt", + type: "DateTime", + optional: true + }, + billingInfo: { + name: "billingInfo", + type: "BillingInfo", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("billingInfoId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "invoices", fields: ["billingInfoId"], references: ["id"] } + }, + billingInfoId: { + name: "billingInfoId", + type: "Int", + foreignKeyFor: [ + "billingInfo" + ] as readonly string[] + }, + lineItems: { + name: "lineItems", + type: "InvoiceLineItem", + array: true, + relation: { opposite: "invoice" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + number: { type: "String" } + } + }, + InvoiceLineItem: { + name: "InvoiceLineItem", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + description: { + name: "description", + type: "String" + }, + quantity: { + name: "quantity", + type: "Int", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(1) }] }] as readonly AttributeApplication[], + default: 1 as FieldDefault + }, + unitCents: { + name: "unitCents", + type: "Int" + }, + invoice: { + name: "invoice", + type: "Invoice", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("invoiceId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "lineItems", fields: ["invoiceId"], references: ["id"] } + }, + invoiceId: { + name: "invoiceId", + type: "Int", + foreignKeyFor: [ + "invoice" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + User: { + name: "User", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + email: { + name: "email", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[] + }, + username: { + name: "username", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[] + }, + displayName: { + name: "displayName", + type: "String" + }, + avatarUrl: { + name: "avatarUrl", + type: "String", + optional: true + }, + timezone: { + name: "timezone", + type: "String", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("UTC") }] }] as readonly AttributeApplication[], + default: "UTC" as FieldDefault + }, + locale: { + name: "locale", + type: "String", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("en") }] }] as readonly AttributeApplication[], + default: "en" as FieldDefault + }, + orgMemberships: { + name: "orgMemberships", + type: "OrganizationMember", + array: true, + relation: { opposite: "user" } + }, + teamMemberships: { + name: "teamMemberships", + type: "TeamMember", + array: true, + relation: { opposite: "user" } + }, + ownedProjects: { + name: "ownedProjects", + type: "Project", + array: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ProjectOwner") }] }] as readonly AttributeApplication[], + relation: { opposite: "owner", name: "ProjectOwner" } + }, + assignedTasks: { + name: "assignedTasks", + type: "Task", + array: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskAssignee") }] }] as readonly AttributeApplication[], + relation: { opposite: "assignee", name: "TaskAssignee" } + }, + reportedTasks: { + name: "reportedTasks", + type: "Task", + array: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskReporter") }] }] as readonly AttributeApplication[], + relation: { opposite: "reporter", name: "TaskReporter" } + }, + comments: { + name: "comments", + type: "Comment", + array: true, + relation: { opposite: "author" } + }, + notifications: { + name: "notifications", + type: "Notification", + array: true, + relation: { opposite: "user" } + }, + reviews: { + name: "reviews", + type: "Review", + array: true, + relation: { opposite: "reviewer" } + }, + auditLogs: { + name: "auditLogs", + type: "AuditLog", + array: true, + relation: { opposite: "actor" } + }, + userPreferences: { + name: "userPreferences", + type: "UserPreferences", + optional: true, + relation: { opposite: "user" } + }, + activityLog: { + name: "activityLog", + type: "ActivityLogEntry", + array: true, + relation: { opposite: "user" } + }, + timeEntries: { + name: "timeEntries", + type: "TimeEntry", + array: true, + relation: { opposite: "user" } + }, + apiTokens: { + name: "apiTokens", + type: "ApiToken", + array: true, + relation: { opposite: "user" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + email: { type: "String" }, + username: { type: "String" } + } + }, + UserPreferences: { + name: "UserPreferences", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + emailNotifications: { + name: "emailNotifications", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(true) }] }] as readonly AttributeApplication[], + default: true as FieldDefault + }, + slackNotifications: { + name: "slackNotifications", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + theme: { + name: "theme", + type: "String", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("light") }] }] as readonly AttributeApplication[], + default: "light" as FieldDefault + }, + defaultProjectId: { + name: "defaultProjectId", + type: "Int", + optional: true + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "userPreferences", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[], + foreignKeyFor: [ + "user" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + userId: { type: "Int" } + } + }, + ApiToken: { + name: "ApiToken", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + lastUsedAt: { + name: "lastUsedAt", + type: "DateTime", + optional: true + }, + name: { + name: "name", + type: "String" + }, + tokenHash: { + name: "tokenHash", + type: "String", + unique: true, + attributes: [{ name: "@unique" }] as readonly AttributeApplication[] + }, + expiresAt: { + name: "expiresAt", + type: "DateTime", + optional: true + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "apiTokens", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + foreignKeyFor: [ + "user" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + tokenHash: { type: "String" } + } + }, + OrganizationMember: { + name: "OrganizationMember", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + joinedAt: { + name: "joinedAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + role: { + name: "role", + type: "UserRole", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("MEMBER") }] }] as readonly AttributeApplication[], + default: "MEMBER" as FieldDefault + }, + organization: { + name: "organization", + type: "Organization", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "members", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "orgMemberships", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + foreignKeyFor: [ + "user" + ] as readonly string[] + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("userId")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + organizationId_userId: { organizationId: { type: "Int" }, userId: { type: "Int" } } + } + }, + Team: { + name: "Team", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + name: { + name: "name", + type: "String" + }, + description: { + name: "description", + type: "String", + optional: true + }, + color: { + name: "color", + type: "String", + optional: true + }, + organization: { + name: "organization", + type: "Organization", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "teams", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + members: { + name: "members", + type: "TeamMember", + array: true, + relation: { opposite: "team" } + }, + projects: { + name: "projects", + type: "ProjectTeamAssignment", + array: true, + relation: { opposite: "team" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + TeamMember: { + name: "TeamMember", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + joinedAt: { + name: "joinedAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + role: { + name: "role", + type: "UserRole", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("MEMBER") }] }] as readonly AttributeApplication[], + default: "MEMBER" as FieldDefault + }, + team: { + name: "team", + type: "Team", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "members", fields: ["teamId"], references: ["id"] } + }, + teamId: { + name: "teamId", + type: "Int", + foreignKeyFor: [ + "team" + ] as readonly string[] + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "teamMemberships", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + foreignKeyFor: [ + "user" + ] as readonly string[] + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId"), ExpressionUtils.field("userId")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + teamId_userId: { teamId: { type: "Int" }, userId: { type: "Int" } } + } + }, + Project: { + name: "Project", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + name: { + name: "name", + type: "String" + }, + slug: { + name: "slug", + type: "String" + }, + description: { + name: "description", + type: "String", + optional: true + }, + status: { + name: "status", + type: "ProjectStatus", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("ACTIVE") }] }] as readonly AttributeApplication[], + default: "ACTIVE" as FieldDefault + }, + startDate: { + name: "startDate", + type: "DateTime", + optional: true + }, + endDate: { + name: "endDate", + type: "DateTime", + optional: true + }, + budget: { + name: "budget", + type: "Int", + optional: true + }, + organization: { + name: "organization", + type: "Organization", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "projects", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + owner: { + name: "owner", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("ProjectOwner") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("ownerId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "ownedProjects", name: "ProjectOwner", fields: ["ownerId"], references: ["id"] } + }, + ownerId: { + name: "ownerId", + type: "Int", + foreignKeyFor: [ + "owner" + ] as readonly string[] + }, + teamAssignments: { + name: "teamAssignments", + type: "ProjectTeamAssignment", + array: true, + relation: { opposite: "project" } + }, + milestones: { + name: "milestones", + type: "Milestone", + array: true, + relation: { opposite: "project" } + }, + tasks: { + name: "tasks", + type: "Task", + array: true, + relation: { opposite: "project" } + }, + labels: { + name: "labels", + type: "Label", + array: true, + relation: { opposite: "project" } + }, + sprints: { + name: "sprints", + type: "Sprint", + array: true, + relation: { opposite: "project" } + }, + documents: { + name: "documents", + type: "Document", + array: true, + relation: { opposite: "project" } + }, + customFieldValues: { + name: "customFieldValues", + type: "CustomFieldValue", + array: true, + relation: { opposite: "project" } + }, + integrationLinks: { + name: "integrationLinks", + type: "IntegrationLink", + array: true, + relation: { opposite: "project" } + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("slug")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + organizationId_slug: { organizationId: { type: "Int" }, slug: { type: "String" } } + } + }, + ProjectTeamAssignment: { + name: "ProjectTeamAssignment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + assignedAt: { + name: "assignedAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + project: { + name: "project", + type: "Project", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "teamAssignments", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + foreignKeyFor: [ + "project" + ] as readonly string[] + }, + team: { + name: "team", + type: "Team", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("teamId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "projects", fields: ["teamId"], references: ["id"] } + }, + teamId: { + name: "teamId", + type: "Int", + foreignKeyFor: [ + "team" + ] as readonly string[] + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId"), ExpressionUtils.field("teamId")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + projectId_teamId: { projectId: { type: "Int" }, teamId: { type: "Int" } } + } + }, + Milestone: { + name: "Milestone", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + name: { + name: "name", + type: "String" + }, + description: { + name: "description", + type: "String", + optional: true + }, + dueDate: { + name: "dueDate", + type: "DateTime", + optional: true + }, + completedAt: { + name: "completedAt", + type: "DateTime", + optional: true + }, + project: { + name: "project", + type: "Project", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "milestones", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + foreignKeyFor: [ + "project" + ] as readonly string[] + }, + tasks: { + name: "tasks", + type: "Task", + array: true, + relation: { opposite: "milestone" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Sprint: { + name: "Sprint", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + name: { + name: "name", + type: "String" + }, + goal: { + name: "goal", + type: "String", + optional: true + }, + startDate: { + name: "startDate", + type: "DateTime" + }, + endDate: { + name: "endDate", + type: "DateTime" + }, + closedAt: { + name: "closedAt", + type: "DateTime", + optional: true + }, + project: { + name: "project", + type: "Project", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "sprints", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + foreignKeyFor: [ + "project" + ] as readonly string[] + }, + tasks: { + name: "tasks", + type: "Task", + array: true, + relation: { opposite: "sprint" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Task: { + name: "Task", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + title: { + name: "title", + type: "String" + }, + description: { + name: "description", + type: "String", + optional: true + }, + status: { + name: "status", + type: "TaskStatus", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("BACKLOG") }] }] as readonly AttributeApplication[], + default: "BACKLOG" as FieldDefault + }, + priority: { + name: "priority", + type: "TaskPriority", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("MEDIUM") }] }] as readonly AttributeApplication[], + default: "MEDIUM" as FieldDefault + }, + storyPoints: { + name: "storyPoints", + type: "Int", + optional: true + }, + dueDate: { + name: "dueDate", + type: "DateTime", + optional: true + }, + completedAt: { + name: "completedAt", + type: "DateTime", + optional: true + }, + position: { + name: "position", + type: "Int", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(0) }] }] as readonly AttributeApplication[], + default: 0 as FieldDefault + }, + project: { + name: "project", + type: "Project", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "tasks", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + foreignKeyFor: [ + "project" + ] as readonly string[] + }, + assignee: { + name: "assignee", + type: "User", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskAssignee") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("assigneeId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "assignedTasks", name: "TaskAssignee", fields: ["assigneeId"], references: ["id"] } + }, + assigneeId: { + name: "assigneeId", + type: "Int", + optional: true, + foreignKeyFor: [ + "assignee" + ] as readonly string[] + }, + reporter: { + name: "reporter", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskReporter") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("reporterId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "reportedTasks", name: "TaskReporter", fields: ["reporterId"], references: ["id"] } + }, + reporterId: { + name: "reporterId", + type: "Int", + foreignKeyFor: [ + "reporter" + ] as readonly string[] + }, + milestone: { + name: "milestone", + type: "Milestone", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("milestoneId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "tasks", fields: ["milestoneId"], references: ["id"] } + }, + milestoneId: { + name: "milestoneId", + type: "Int", + optional: true, + foreignKeyFor: [ + "milestone" + ] as readonly string[] + }, + sprint: { + name: "sprint", + type: "Sprint", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("sprintId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "tasks", fields: ["sprintId"], references: ["id"] } + }, + sprintId: { + name: "sprintId", + type: "Int", + optional: true, + foreignKeyFor: [ + "sprint" + ] as readonly string[] + }, + parent: { + name: "parent", + type: "Task", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskSubtasks") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "subtasks", name: "TaskSubtasks", fields: ["parentId"], references: ["id"] } + }, + parentId: { + name: "parentId", + type: "Int", + optional: true, + foreignKeyFor: [ + "parent" + ] as readonly string[] + }, + subtasks: { + name: "subtasks", + type: "Task", + array: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("TaskSubtasks") }] }] as readonly AttributeApplication[], + relation: { opposite: "parent", name: "TaskSubtasks" } + }, + labels: { + name: "labels", + type: "TaskLabel", + array: true, + relation: { opposite: "task" } + }, + comments: { + name: "comments", + type: "Comment", + array: true, + relation: { opposite: "task" } + }, + attachments: { + name: "attachments", + type: "Attachment", + array: true, + relation: { opposite: "task" } + }, + reviews: { + name: "reviews", + type: "Review", + array: true, + relation: { opposite: "task" } + }, + timeEntries: { + name: "timeEntries", + type: "TimeEntry", + array: true, + relation: { opposite: "task" } + }, + customFieldValues: { + name: "customFieldValues", + type: "CustomFieldValue", + array: true, + relation: { opposite: "task" } + }, + activityLog: { + name: "activityLog", + type: "ActivityLogEntry", + array: true, + relation: { opposite: "task" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Label: { + name: "Label", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + name: { + name: "name", + type: "String" + }, + color: { + name: "color", + type: "String", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("#888888") }] }] as readonly AttributeApplication[], + default: "#888888" as FieldDefault + }, + description: { + name: "description", + type: "String", + optional: true + }, + project: { + name: "project", + type: "Project", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "labels", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + foreignKeyFor: [ + "project" + ] as readonly string[] + }, + tasks: { + name: "tasks", + type: "TaskLabel", + array: true, + relation: { opposite: "label" } + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId"), ExpressionUtils.field("name")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + projectId_name: { projectId: { type: "Int" }, name: { type: "String" } } + } + }, + TaskLabel: { + name: "TaskLabel", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + appliedAt: { + name: "appliedAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + task: { + name: "task", + type: "Task", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "labels", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + foreignKeyFor: [ + "task" + ] as readonly string[] + }, + label: { + name: "label", + type: "Label", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("labelId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "tasks", fields: ["labelId"], references: ["id"] } + }, + labelId: { + name: "labelId", + type: "Int", + foreignKeyFor: [ + "label" + ] as readonly string[] + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId"), ExpressionUtils.field("labelId")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + taskId_labelId: { taskId: { type: "Int" }, labelId: { type: "Int" } } + } + }, + Comment: { + name: "Comment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + body: { + name: "body", + type: "String" + }, + kind: { + name: "kind", + type: "CommentKind", + isDiscriminator: true + }, + edited: { + name: "edited", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + resolved: { + name: "resolved", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + task: { + name: "task", + type: "Task", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + foreignKeyFor: [ + "task" + ] as readonly string[] + }, + author: { + name: "author", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["authorId"], references: ["id"] } + }, + authorId: { + name: "authorId", + type: "Int", + foreignKeyFor: [ + "author" + ] as readonly string[] + }, + parent: { + name: "parent", + type: "Comment", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "replies", name: "CommentReplies", fields: ["parentId"], references: ["id"] } + }, + parentId: { + name: "parentId", + type: "Int", + optional: true, + foreignKeyFor: [ + "parent" + ] as readonly string[] + }, + replies: { + name: "replies", + type: "Comment", + array: true, + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }] }] as readonly AttributeApplication[], + relation: { opposite: "parent", name: "CommentReplies" } + }, + reactions: { + name: "reactions", + type: "CommentReaction", + array: true, + relation: { opposite: "comment" } + } + }, + attributes: [ + { name: "@@delegate", args: [{ name: "discriminator", value: ExpressionUtils.field("kind") }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + }, + isDelegate: true, + subModels: ["TextComment", "CodeSnippetComment", "AttachmentComment"] + }, + TextComment: { + name: "TextComment", + baseModel: "Comment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + originModel: "Comment", + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + body: { + name: "body", + type: "String", + originModel: "Comment" + }, + kind: { + name: "kind", + type: "CommentKind", + originModel: "Comment", + isDiscriminator: true + }, + edited: { + name: "edited", + type: "Boolean", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + resolved: { + name: "resolved", + type: "Boolean", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + task: { + name: "task", + type: "Task", + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + originModel: "Comment", + foreignKeyFor: [ + "task" + ] as readonly string[] + }, + author: { + name: "author", + type: "User", + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["authorId"], references: ["id"] } + }, + authorId: { + name: "authorId", + type: "Int", + originModel: "Comment", + foreignKeyFor: [ + "author" + ] as readonly string[] + }, + parent: { + name: "parent", + type: "Comment", + optional: true, + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "replies", name: "CommentReplies", fields: ["parentId"], references: ["id"] } + }, + parentId: { + name: "parentId", + type: "Int", + optional: true, + originModel: "Comment", + foreignKeyFor: [ + "parent" + ] as readonly string[] + }, + replies: { + name: "replies", + type: "Comment", + array: true, + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }] }] as readonly AttributeApplication[], + relation: { opposite: "parent", name: "CommentReplies" } + }, + reactions: { + name: "reactions", + type: "CommentReaction", + array: true, + originModel: "Comment", + relation: { opposite: "comment" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + CodeSnippetComment: { + name: "CodeSnippetComment", + baseModel: "Comment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + originModel: "Comment", + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + body: { + name: "body", + type: "String", + originModel: "Comment" + }, + kind: { + name: "kind", + type: "CommentKind", + originModel: "Comment", + isDiscriminator: true + }, + edited: { + name: "edited", + type: "Boolean", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + resolved: { + name: "resolved", + type: "Boolean", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + task: { + name: "task", + type: "Task", + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + originModel: "Comment", + foreignKeyFor: [ + "task" + ] as readonly string[] + }, + author: { + name: "author", + type: "User", + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["authorId"], references: ["id"] } + }, + authorId: { + name: "authorId", + type: "Int", + originModel: "Comment", + foreignKeyFor: [ + "author" + ] as readonly string[] + }, + parent: { + name: "parent", + type: "Comment", + optional: true, + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "replies", name: "CommentReplies", fields: ["parentId"], references: ["id"] } + }, + parentId: { + name: "parentId", + type: "Int", + optional: true, + originModel: "Comment", + foreignKeyFor: [ + "parent" + ] as readonly string[] + }, + replies: { + name: "replies", + type: "Comment", + array: true, + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }] }] as readonly AttributeApplication[], + relation: { opposite: "parent", name: "CommentReplies" } + }, + reactions: { + name: "reactions", + type: "CommentReaction", + array: true, + originModel: "Comment", + relation: { opposite: "comment" } + }, + language: { + name: "language", + type: "String", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("plaintext") }] }] as readonly AttributeApplication[], + default: "plaintext" as FieldDefault + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + AttachmentComment: { + name: "AttachmentComment", + baseModel: "Comment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + originModel: "Comment", + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + body: { + name: "body", + type: "String", + originModel: "Comment" + }, + kind: { + name: "kind", + type: "CommentKind", + originModel: "Comment", + isDiscriminator: true + }, + edited: { + name: "edited", + type: "Boolean", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + resolved: { + name: "resolved", + type: "Boolean", + originModel: "Comment", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + task: { + name: "task", + type: "Task", + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + originModel: "Comment", + foreignKeyFor: [ + "task" + ] as readonly string[] + }, + author: { + name: "author", + type: "User", + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("authorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["authorId"], references: ["id"] } + }, + authorId: { + name: "authorId", + type: "Int", + originModel: "Comment", + foreignKeyFor: [ + "author" + ] as readonly string[] + }, + parent: { + name: "parent", + type: "Comment", + optional: true, + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }, { name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("parentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "replies", name: "CommentReplies", fields: ["parentId"], references: ["id"] } + }, + parentId: { + name: "parentId", + type: "Int", + optional: true, + originModel: "Comment", + foreignKeyFor: [ + "parent" + ] as readonly string[] + }, + replies: { + name: "replies", + type: "Comment", + array: true, + originModel: "Comment", + attributes: [{ name: "@relation", args: [{ name: "name", value: ExpressionUtils.literal("CommentReplies") }] }] as readonly AttributeApplication[], + relation: { opposite: "parent", name: "CommentReplies" } + }, + reactions: { + name: "reactions", + type: "CommentReaction", + array: true, + originModel: "Comment", + relation: { opposite: "comment" } + }, + attachFilename: { + name: "attachFilename", + type: "String" + }, + attachMimeType: { + name: "attachMimeType", + type: "String" + }, + attachSizeBytes: { + name: "attachSizeBytes", + type: "Int" + }, + attachStorageKey: { + name: "attachStorageKey", + type: "String" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + CommentReaction: { + name: "CommentReaction", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + emoji: { + name: "emoji", + type: "String" + }, + comment: { + name: "comment", + type: "Comment", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("commentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "reactions", fields: ["commentId"], references: ["id"] } + }, + commentId: { + name: "commentId", + type: "Int", + foreignKeyFor: [ + "comment" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Review: { + name: "Review", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + decision: { + name: "decision", + type: "ReviewDecision", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal("PENDING") }] }] as readonly AttributeApplication[], + default: "PENDING" as FieldDefault + }, + summary: { + name: "summary", + type: "String", + optional: true + }, + task: { + name: "task", + type: "Task", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "reviews", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + foreignKeyFor: [ + "task" + ] as readonly string[] + }, + reviewer: { + name: "reviewer", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("reviewerId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "reviews", fields: ["reviewerId"], references: ["id"] } + }, + reviewerId: { + name: "reviewerId", + type: "Int", + foreignKeyFor: [ + "reviewer" + ] as readonly string[] + }, + comments: { + name: "comments", + type: "ReviewComment", + array: true, + relation: { opposite: "review" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + ReviewComment: { + name: "ReviewComment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + body: { + name: "body", + type: "String" + }, + lineRef: { + name: "lineRef", + type: "String", + optional: true + }, + review: { + name: "review", + type: "Review", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("reviewId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "comments", fields: ["reviewId"], references: ["id"] } + }, + reviewId: { + name: "reviewId", + type: "Int", + foreignKeyFor: [ + "review" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Attachment: { + name: "Attachment", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + filename: { + name: "filename", + type: "String" + }, + mimeType: { + name: "mimeType", + type: "String" + }, + sizeBytes: { + name: "sizeBytes", + type: "Int" + }, + storageKey: { + name: "storageKey", + type: "String" + }, + task: { + name: "task", + type: "Task", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "attachments", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + optional: true, + foreignKeyFor: [ + "task" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Document: { + name: "Document", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + title: { + name: "title", + type: "String" + }, + content: { + name: "content", + type: "String", + optional: true + }, + published: { + name: "published", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + project: { + name: "project", + type: "Project", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "documents", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + foreignKeyFor: [ + "project" + ] as readonly string[] + }, + sections: { + name: "sections", + type: "DocumentSection", + array: true, + relation: { opposite: "document" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + DocumentSection: { + name: "DocumentSection", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + order: { + name: "order", + type: "Int" + }, + heading: { + name: "heading", + type: "String" + }, + content: { + name: "content", + type: "String", + optional: true + }, + document: { + name: "document", + type: "Document", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("documentId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "sections", fields: ["documentId"], references: ["id"] } + }, + documentId: { + name: "documentId", + type: "Int", + foreignKeyFor: [ + "document" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + TimeEntry: { + name: "TimeEntry", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + startedAt: { + name: "startedAt", + type: "DateTime" + }, + stoppedAt: { + name: "stoppedAt", + type: "DateTime", + optional: true + }, + durationMin: { + name: "durationMin", + type: "Int", + optional: true + }, + note: { + name: "note", + type: "String", + optional: true + }, + task: { + name: "task", + type: "Task", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "timeEntries", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + foreignKeyFor: [ + "task" + ] as readonly string[] + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "timeEntries", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + foreignKeyFor: [ + "user" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Notification: { + name: "Notification", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + readAt: { + name: "readAt", + type: "DateTime", + optional: true + }, + kind: { + name: "kind", + type: "NotificationType", + isDiscriminator: true + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "notifications", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + foreignKeyFor: [ + "user" + ] as readonly string[] + } + }, + attributes: [ + { name: "@@delegate", args: [{ name: "discriminator", value: ExpressionUtils.field("kind") }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + }, + isDelegate: true, + subModels: ["MentionNotification", "AssignmentNotification", "StatusChangeNotification", "CommentNotification", "ReviewRequestNotification", "ApprovalNotification"] + }, + MentionNotification: { + name: "MentionNotification", + baseModel: "Notification", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Notification", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + readAt: { + name: "readAt", + type: "DateTime", + optional: true, + originModel: "Notification" + }, + kind: { + name: "kind", + type: "NotificationType", + originModel: "Notification", + isDiscriminator: true + }, + user: { + name: "user", + type: "User", + originModel: "Notification", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "notifications", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + originModel: "Notification", + foreignKeyFor: [ + "user" + ] as readonly string[] + }, + mentionedByUserId: { + name: "mentionedByUserId", + type: "Int" + }, + taskId: { + name: "taskId", + type: "Int" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + AssignmentNotification: { + name: "AssignmentNotification", + baseModel: "Notification", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Notification", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + readAt: { + name: "readAt", + type: "DateTime", + optional: true, + originModel: "Notification" + }, + kind: { + name: "kind", + type: "NotificationType", + originModel: "Notification", + isDiscriminator: true + }, + user: { + name: "user", + type: "User", + originModel: "Notification", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "notifications", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + originModel: "Notification", + foreignKeyFor: [ + "user" + ] as readonly string[] + }, + taskId: { + name: "taskId", + type: "Int" + }, + assignedByUserId: { + name: "assignedByUserId", + type: "Int" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + StatusChangeNotification: { + name: "StatusChangeNotification", + baseModel: "Notification", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Notification", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + readAt: { + name: "readAt", + type: "DateTime", + optional: true, + originModel: "Notification" + }, + kind: { + name: "kind", + type: "NotificationType", + originModel: "Notification", + isDiscriminator: true + }, + user: { + name: "user", + type: "User", + originModel: "Notification", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "notifications", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + originModel: "Notification", + foreignKeyFor: [ + "user" + ] as readonly string[] + }, + taskId: { + name: "taskId", + type: "Int" + }, + fromStatus: { + name: "fromStatus", + type: "String" + }, + toStatus: { + name: "toStatus", + type: "String" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + CommentNotification: { + name: "CommentNotification", + baseModel: "Notification", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Notification", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + readAt: { + name: "readAt", + type: "DateTime", + optional: true, + originModel: "Notification" + }, + kind: { + name: "kind", + type: "NotificationType", + originModel: "Notification", + isDiscriminator: true + }, + user: { + name: "user", + type: "User", + originModel: "Notification", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "notifications", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + originModel: "Notification", + foreignKeyFor: [ + "user" + ] as readonly string[] + }, + commentId: { + name: "commentId", + type: "Int" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + ReviewRequestNotification: { + name: "ReviewRequestNotification", + baseModel: "Notification", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Notification", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + readAt: { + name: "readAt", + type: "DateTime", + optional: true, + originModel: "Notification" + }, + kind: { + name: "kind", + type: "NotificationType", + originModel: "Notification", + isDiscriminator: true + }, + user: { + name: "user", + type: "User", + originModel: "Notification", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "notifications", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + originModel: "Notification", + foreignKeyFor: [ + "user" + ] as readonly string[] + }, + reviewId: { + name: "reviewId", + type: "Int" + }, + requestedByUserId: { + name: "requestedByUserId", + type: "Int" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + ApprovalNotification: { + name: "ApprovalNotification", + baseModel: "Notification", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + originModel: "Notification", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + readAt: { + name: "readAt", + type: "DateTime", + optional: true, + originModel: "Notification" + }, + kind: { + name: "kind", + type: "NotificationType", + originModel: "Notification", + isDiscriminator: true + }, + user: { + name: "user", + type: "User", + originModel: "Notification", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "notifications", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + originModel: "Notification", + foreignKeyFor: [ + "user" + ] as readonly string[] + }, + targetType: { + name: "targetType", + type: "String" + }, + targetId: { + name: "targetId", + type: "Int" + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + ActivityLogEntry: { + name: "ActivityLogEntry", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + action: { + name: "action", + type: "String" + }, + meta: { + name: "meta", + type: "String", + optional: true + }, + organization: { + name: "organization", + type: "Organization", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "activityLog", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + optional: true, + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + user: { + name: "user", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("userId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "activityLog", fields: ["userId"], references: ["id"] } + }, + userId: { + name: "userId", + type: "Int", + foreignKeyFor: [ + "user" + ] as readonly string[] + }, + task: { + name: "task", + type: "Task", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "activityLog", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + optional: true, + foreignKeyFor: [ + "task" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + AuditLog: { + name: "AuditLog", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + entityType: { + name: "entityType", + type: "String" + }, + entityId: { + name: "entityId", + type: "Int" + }, + action: { + name: "action", + type: "String" + }, + diff: { + name: "diff", + type: "String", + optional: true + }, + organization: { + name: "organization", + type: "Organization", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "auditLogs", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + actor: { + name: "actor", + type: "User", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("actorId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "auditLogs", fields: ["actorId"], references: ["id"] } + }, + actorId: { + name: "actorId", + type: "Int", + foreignKeyFor: [ + "actor" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + CustomFieldDefinition: { + name: "CustomFieldDefinition", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + name: { + name: "name", + type: "String" + }, + fieldType: { + name: "fieldType", + type: "String" + }, + required: { + name: "required", + type: "Boolean", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.literal(false) }] }] as readonly AttributeApplication[], + default: false as FieldDefault + }, + defaultValue: { + name: "defaultValue", + type: "String", + optional: true + }, + organization: { + name: "organization", + type: "Organization", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "customFields", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + values: { + name: "values", + type: "CustomFieldValue", + array: true, + relation: { opposite: "field" } + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + CustomFieldValue: { + name: "CustomFieldValue", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + value: { + name: "value", + type: "String" + }, + field: { + name: "field", + type: "CustomFieldDefinition", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("fieldId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "values", fields: ["fieldId"], references: ["id"] } + }, + fieldId: { + name: "fieldId", + type: "Int", + foreignKeyFor: [ + "field" + ] as readonly string[] + }, + project: { + name: "project", + type: "Project", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "customFieldValues", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + optional: true, + foreignKeyFor: [ + "project" + ] as readonly string[] + }, + task: { + name: "task", + type: "Task", + optional: true, + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("taskId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "customFieldValues", fields: ["taskId"], references: ["id"] } + }, + taskId: { + name: "taskId", + type: "Int", + optional: true, + foreignKeyFor: [ + "task" + ] as readonly string[] + } + }, + idFields: ["id"], + uniqueFields: { + id: { type: "Int" } + } + }, + Integration: { + name: "Integration", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + updatedAt: { + name: "updatedAt", + type: "DateTime", + updatedAt: true, + attributes: [{ name: "@updatedAt" }] as readonly AttributeApplication[] + }, + provider: { + name: "provider", + type: "String" + }, + accessToken: { + name: "accessToken", + type: "String", + optional: true + }, + config: { + name: "config", + type: "String", + optional: true + }, + organization: { + name: "organization", + type: "Organization", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "integrations", fields: ["organizationId"], references: ["id"] } + }, + organizationId: { + name: "organizationId", + type: "Int", + foreignKeyFor: [ + "organization" + ] as readonly string[] + }, + links: { + name: "links", + type: "IntegrationLink", + array: true, + relation: { opposite: "integration" } + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("organizationId"), ExpressionUtils.field("provider")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + organizationId_provider: { organizationId: { type: "Int" }, provider: { type: "String" } } + } + }, + IntegrationLink: { + name: "IntegrationLink", + fields: { + id: { + name: "id", + type: "Int", + id: true, + attributes: [{ name: "@id" }, { name: "@default", args: [{ name: "value", value: ExpressionUtils.call("autoincrement") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("autoincrement") as FieldDefault + }, + createdAt: { + name: "createdAt", + type: "DateTime", + attributes: [{ name: "@default", args: [{ name: "value", value: ExpressionUtils.call("now") }] }] as readonly AttributeApplication[], + default: ExpressionUtils.call("now") as FieldDefault + }, + externalId: { + name: "externalId", + type: "String" + }, + url: { + name: "url", + type: "String", + optional: true + }, + integration: { + name: "integration", + type: "Integration", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("integrationId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "links", fields: ["integrationId"], references: ["id"] } + }, + integrationId: { + name: "integrationId", + type: "Int", + foreignKeyFor: [ + "integration" + ] as readonly string[] + }, + project: { + name: "project", + type: "Project", + attributes: [{ name: "@relation", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("projectId")]) }, { name: "references", value: ExpressionUtils.array("Int", [ExpressionUtils.field("id")]) }] }] as readonly AttributeApplication[], + relation: { opposite: "integrationLinks", fields: ["projectId"], references: ["id"] } + }, + projectId: { + name: "projectId", + type: "Int", + foreignKeyFor: [ + "project" + ] as readonly string[] + } + }, + attributes: [ + { name: "@@unique", args: [{ name: "fields", value: ExpressionUtils.array("Int", [ExpressionUtils.field("integrationId"), ExpressionUtils.field("externalId")]) }] } + ] as readonly AttributeApplication[], + idFields: ["id"], + uniqueFields: { + id: { type: "Int" }, + integrationId_externalId: { integrationId: { type: "Int" }, externalId: { type: "String" } } + } + } + } as const; + enums = { + UserRole: { + name: "UserRole", + values: { + SUPER_ADMIN: "SUPER_ADMIN", + ADMIN: "ADMIN", + MANAGER: "MANAGER", + MEMBER: "MEMBER", + GUEST: "GUEST" + } + }, + ProjectStatus: { + name: "ProjectStatus", + values: { + DRAFT: "DRAFT", + ACTIVE: "ACTIVE", + ARCHIVED: "ARCHIVED", + DELETED: "DELETED" + } + }, + TaskStatus: { + name: "TaskStatus", + values: { + BACKLOG: "BACKLOG", + TODO: "TODO", + IN_PROGRESS: "IN_PROGRESS", + IN_REVIEW: "IN_REVIEW", + DONE: "DONE", + CANCELLED: "CANCELLED" + } + }, + TaskPriority: { + name: "TaskPriority", + values: { + CRITICAL: "CRITICAL", + HIGH: "HIGH", + MEDIUM: "MEDIUM", + LOW: "LOW" + } + }, + CommentKind: { + name: "CommentKind", + values: { + TEXT: "TEXT", + CODE_SNIPPET: "CODE_SNIPPET", + ATTACHMENT: "ATTACHMENT" + } + }, + NotificationType: { + name: "NotificationType", + values: { + MENTION: "MENTION", + ASSIGNMENT: "ASSIGNMENT", + STATUS_CHANGE: "STATUS_CHANGE", + COMMENT: "COMMENT", + REVIEW_REQUEST: "REVIEW_REQUEST", + APPROVAL_NEEDED: "APPROVAL_NEEDED" + } + }, + ReviewDecision: { + name: "ReviewDecision", + values: { + PENDING: "PENDING", + APPROVED: "APPROVED", + REJECTED: "REJECTED", + CHANGES_REQUESTED: "CHANGES_REQUESTED" + } + }, + InvoiceStatus: { + name: "InvoiceStatus", + values: { + DRAFT: "DRAFT", + SENT: "SENT", + PAID: "PAID", + OVERDUE: "OVERDUE", + CANCELLED: "CANCELLED" + } + }, + PaymentMethod: { + name: "PaymentMethod", + values: { + CREDIT_CARD: "CREDIT_CARD", + BANK_TRANSFER: "BANK_TRANSFER", + PAYPAL: "PAYPAL", + CRYPTO: "CRYPTO" + } + } + } as const; + authType = "User" as const; + plugins = {}; +} +export const schema = new SchemaType(); diff --git a/tests/e2e/performance/tsc-torture/zenstack/schema.zmodel b/tests/e2e/performance/tsc-torture/zenstack/schema.zmodel new file mode 100644 index 000000000..220d90ae5 --- /dev/null +++ b/tests/e2e/performance/tsc-torture/zenstack/schema.zmodel @@ -0,0 +1,614 @@ +// ZenStack v3 torture test schema — 30+ models with complex relations + +datasource db { + provider = "sqlite" + url = "file:./dev.db" +} + +// ─── Enums ─────────────────────────────────────────────────────────────────── + +enum UserRole { + SUPER_ADMIN + ADMIN + MANAGER + MEMBER + GUEST +} + +enum ProjectStatus { + DRAFT + ACTIVE + ARCHIVED + DELETED +} + +enum TaskStatus { + BACKLOG + TODO + IN_PROGRESS + IN_REVIEW + DONE + CANCELLED +} + +enum TaskPriority { + CRITICAL + HIGH + MEDIUM + LOW +} + +enum CommentKind { + TEXT + CODE_SNIPPET + ATTACHMENT +} + +enum NotificationType { + MENTION + ASSIGNMENT + STATUS_CHANGE + COMMENT + REVIEW_REQUEST + APPROVAL_NEEDED +} + +enum ReviewDecision { + PENDING + APPROVED + REJECTED + CHANGES_REQUESTED +} + +enum InvoiceStatus { + DRAFT + SENT + PAID + OVERDUE + CANCELLED +} + +enum PaymentMethod { + CREDIT_CARD + BANK_TRANSFER + PAYPAL + CRYPTO +} + +// ─── Core identity & org models ────────────────────────────────────────────── + +model Organization { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + slug String @unique + logoUrl String? + website String? + + teams Team[] + members OrganizationMember[] + projects Project[] + billingInfo BillingInfo? + auditLogs AuditLog[] + activityLog ActivityLogEntry[] + integrations Integration[] + customFields CustomFieldDefinition[] +} + +model BillingInfo { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + planName String + billingEmail String + paymentMethod PaymentMethod + stripeCustomerId String? + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId Int @unique + invoices Invoice[] +} + +model Invoice { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + number String @unique + amountCents Int + currency String @default("USD") + status InvoiceStatus @default(DRAFT) + dueDate DateTime + paidAt DateTime? + + billingInfo BillingInfo @relation(fields: [billingInfoId], references: [id]) + billingInfoId Int + lineItems InvoiceLineItem[] +} + +model InvoiceLineItem { + id Int @id @default(autoincrement()) + description String + quantity Int @default(1) + unitCents Int + + invoice Invoice @relation(fields: [invoiceId], references: [id]) + invoiceId Int +} + +model User { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + email String @unique + username String @unique + displayName String + avatarUrl String? + timezone String @default("UTC") + locale String @default("en") + + orgMemberships OrganizationMember[] + teamMemberships TeamMember[] + ownedProjects Project[] @relation("ProjectOwner") + assignedTasks Task[] @relation("TaskAssignee") + reportedTasks Task[] @relation("TaskReporter") + comments Comment[] + notifications Notification[] + reviews Review[] + auditLogs AuditLog[] + userPreferences UserPreferences? + activityLog ActivityLogEntry[] + timeEntries TimeEntry[] + apiTokens ApiToken[] +} + +model UserPreferences { + id Int @id @default(autoincrement()) + emailNotifications Boolean @default(true) + slackNotifications Boolean @default(false) + theme String @default("light") + defaultProjectId Int? + + user User @relation(fields: [userId], references: [id]) + userId Int @unique +} + +model ApiToken { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + lastUsedAt DateTime? + name String + tokenHash String @unique + expiresAt DateTime? + + user User @relation(fields: [userId], references: [id]) + userId Int +} + +model OrganizationMember { + id Int @id @default(autoincrement()) + joinedAt DateTime @default(now()) + role UserRole @default(MEMBER) + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId Int + user User @relation(fields: [userId], references: [id]) + userId Int + + @@unique([organizationId, userId]) +} + +// ─── Teams ─────────────────────────────────────────────────────────────────── + +model Team { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + description String? + color String? + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId Int + members TeamMember[] + projects ProjectTeamAssignment[] +} + +model TeamMember { + id Int @id @default(autoincrement()) + joinedAt DateTime @default(now()) + role UserRole @default(MEMBER) + + team Team @relation(fields: [teamId], references: [id]) + teamId Int + user User @relation(fields: [userId], references: [id]) + userId Int + + @@unique([teamId, userId]) +} + +// ─── Projects ──────────────────────────────────────────────────────────────── + +model Project { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + slug String + description String? + status ProjectStatus @default(ACTIVE) + startDate DateTime? + endDate DateTime? + budget Int? + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId Int + owner User @relation("ProjectOwner", fields: [ownerId], references: [id]) + ownerId Int + teamAssignments ProjectTeamAssignment[] + milestones Milestone[] + tasks Task[] + labels Label[] + sprints Sprint[] + documents Document[] + customFieldValues CustomFieldValue[] + integrationLinks IntegrationLink[] + + @@unique([organizationId, slug]) +} + +model ProjectTeamAssignment { + id Int @id @default(autoincrement()) + assignedAt DateTime @default(now()) + + project Project @relation(fields: [projectId], references: [id]) + projectId Int + team Team @relation(fields: [teamId], references: [id]) + teamId Int + + @@unique([projectId, teamId]) +} + +model Milestone { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + description String? + dueDate DateTime? + completedAt DateTime? + + project Project @relation(fields: [projectId], references: [id]) + projectId Int + tasks Task[] +} + +model Sprint { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + name String + goal String? + startDate DateTime + endDate DateTime + closedAt DateTime? + + project Project @relation(fields: [projectId], references: [id]) + projectId Int + tasks Task[] +} + +// ─── Tasks ─────────────────────────────────────────────────────────────────── + +model Task { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + description String? + status TaskStatus @default(BACKLOG) + priority TaskPriority @default(MEDIUM) + storyPoints Int? + dueDate DateTime? + completedAt DateTime? + position Int @default(0) + + project Project @relation(fields: [projectId], references: [id]) + projectId Int + assignee User? @relation("TaskAssignee", fields: [assigneeId], references: [id]) + assigneeId Int? + reporter User @relation("TaskReporter", fields: [reporterId], references: [id]) + reporterId Int + milestone Milestone? @relation(fields: [milestoneId], references: [id]) + milestoneId Int? + sprint Sprint? @relation(fields: [sprintId], references: [id]) + sprintId Int? + parent Task? @relation("TaskSubtasks", fields: [parentId], references: [id]) + parentId Int? + subtasks Task[] @relation("TaskSubtasks") + labels TaskLabel[] + comments Comment[] + attachments Attachment[] + reviews Review[] + timeEntries TimeEntry[] + customFieldValues CustomFieldValue[] + activityLog ActivityLogEntry[] +} + +model Label { + id Int @id @default(autoincrement()) + name String + color String @default("#888888") + description String? + + project Project @relation(fields: [projectId], references: [id]) + projectId Int + tasks TaskLabel[] + + @@unique([projectId, name]) +} + +model TaskLabel { + id Int @id @default(autoincrement()) + appliedAt DateTime @default(now()) + + task Task @relation(fields: [taskId], references: [id]) + taskId Int + label Label @relation(fields: [labelId], references: [id]) + labelId Int + + @@unique([taskId, labelId]) +} + +// ─── Comments & Reviews ────────────────────────────────────────────────────── + +model Comment { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + body String + kind CommentKind + edited Boolean @default(false) + resolved Boolean @default(false) + + task Task @relation(fields: [taskId], references: [id]) + taskId Int + author User @relation(fields: [authorId], references: [id]) + authorId Int + parent Comment? @relation("CommentReplies", fields: [parentId], references: [id]) + parentId Int? + replies Comment[] @relation("CommentReplies") + reactions CommentReaction[] + + @@delegate(kind) +} + +model TextComment extends Comment { + // plain-text comment — no extra fields beyond base +} + +model CodeSnippetComment extends Comment { + language String @default("plaintext") +} + +// AttachmentComment stores file metadata inline rather than via a separate Attachment join +model AttachmentComment extends Comment { + attachFilename String + attachMimeType String + attachSizeBytes Int + attachStorageKey String +} + +model CommentReaction { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + emoji String + + comment Comment @relation(fields: [commentId], references: [id]) + commentId Int +} + +model Review { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + decision ReviewDecision @default(PENDING) + summary String? + + task Task @relation(fields: [taskId], references: [id]) + taskId Int + reviewer User @relation(fields: [reviewerId], references: [id]) + reviewerId Int + comments ReviewComment[] +} + +model ReviewComment { + id Int @id @default(autoincrement()) + body String + lineRef String? + + review Review @relation(fields: [reviewId], references: [id]) + reviewId Int +} + +// ─── Files & Documents ─────────────────────────────────────────────────────── + +model Attachment { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + filename String + mimeType String + sizeBytes Int + storageKey String + + task Task? @relation(fields: [taskId], references: [id]) + taskId Int? +} + +model Document { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + title String + content String? + published Boolean @default(false) + + project Project @relation(fields: [projectId], references: [id]) + projectId Int + sections DocumentSection[] +} + +model DocumentSection { + id Int @id @default(autoincrement()) + order Int + heading String + content String? + + document Document @relation(fields: [documentId], references: [id]) + documentId Int +} + +// ─── Time tracking ─────────────────────────────────────────────────────────── + +model TimeEntry { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + startedAt DateTime + stoppedAt DateTime? + durationMin Int? + note String? + + task Task @relation(fields: [taskId], references: [id]) + taskId Int + user User @relation(fields: [userId], references: [id]) + userId Int +} + +// ─── Notifications & Activity ──────────────────────────────────────────────── + +model Notification { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + readAt DateTime? + kind NotificationType + + user User @relation(fields: [userId], references: [id]) + userId Int + + @@delegate(kind) +} + +model MentionNotification extends Notification { + mentionedByUserId Int + taskId Int +} + +model AssignmentNotification extends Notification { + taskId Int + assignedByUserId Int +} + +model StatusChangeNotification extends Notification { + taskId Int + fromStatus String + toStatus String +} + +model CommentNotification extends Notification { + commentId Int +} + +model ReviewRequestNotification extends Notification { + reviewId Int + requestedByUserId Int +} + +model ApprovalNotification extends Notification { + targetType String + targetId Int +} + +model ActivityLogEntry { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + action String + meta String? // JSON blob + + organization Organization? @relation(fields: [organizationId], references: [id]) + organizationId Int? + user User @relation(fields: [userId], references: [id]) + userId Int + task Task? @relation(fields: [taskId], references: [id]) + taskId Int? +} + +model AuditLog { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + entityType String + entityId Int + action String + diff String? // JSON blob + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId Int + actor User @relation(fields: [actorId], references: [id]) + actorId Int +} + +// ─── Custom fields ─────────────────────────────────────────────────────────── + +model CustomFieldDefinition { + id Int @id @default(autoincrement()) + name String + fieldType String // "text"|"number"|"date"|"select" + required Boolean @default(false) + defaultValue String? + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId Int + values CustomFieldValue[] +} + +model CustomFieldValue { + id Int @id @default(autoincrement()) + value String + + field CustomFieldDefinition @relation(fields: [fieldId], references: [id]) + fieldId Int + project Project? @relation(fields: [projectId], references: [id]) + projectId Int? + task Task? @relation(fields: [taskId], references: [id]) + taskId Int? +} + +// ─── Integrations ──────────────────────────────────────────────────────────── + +model Integration { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + provider String // "github"|"slack"|"jira" + accessToken String? + config String? // JSON blob + + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId Int + links IntegrationLink[] + + @@unique([organizationId, provider]) +} + +model IntegrationLink { + id Int @id @default(autoincrement()) + createdAt DateTime @default(now()) + externalId String + url String? + + integration Integration @relation(fields: [integrationId], references: [id]) + integrationId Int + project Project @relation(fields: [projectId], references: [id]) + projectId Int + + @@unique([integrationId, externalId]) +}