diff --git a/.changeset/fix-virtual-fields-never-select.md b/.changeset/fix-virtual-fields-never-select.md new file mode 100644 index 00000000..03e95e28 --- /dev/null +++ b/.changeset/fix-virtual-fields-never-select.md @@ -0,0 +1,5 @@ +--- +'@opensaas/stack-cli': patch +--- + +Fix virtual fields typed as `never` when mixed with relation fields in `select` or when using `include` diff --git a/examples/auth-demo/prisma/schema.prisma b/examples/auth-demo/prisma/schema.prisma index 41192eb0..7ded77a2 100644 --- a/examples/auth-demo/prisma/schema.prisma +++ b/examples/auth-demo/prisma/schema.prisma @@ -18,7 +18,7 @@ model Post { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([authorId]) } @@ -33,7 +33,7 @@ model User { accounts Account[] posts Post[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Session { @@ -45,7 +45,7 @@ model Session { userId String? @map("user") user User? @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([userId]) } @@ -64,7 +64,7 @@ model Account { userId String? @map("user") user User? @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([userId]) } @@ -75,5 +75,5 @@ model Verification { value String expiresAt DateTime? createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } diff --git a/examples/blog/prisma/schema.prisma b/examples/blog/prisma/schema.prisma index 4b167e65..4018257e 100644 --- a/examples/blog/prisma/schema.prisma +++ b/examples/blog/prisma/schema.prisma @@ -13,7 +13,7 @@ model Settings { maintenanceMode Boolean @default(false) maxUploadSize Int? createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model User { @@ -23,7 +23,7 @@ model User { password String posts Post[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Post { @@ -38,7 +38,7 @@ model Post { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([authorId]) } diff --git a/examples/composable-dashboard/prisma/schema.prisma b/examples/composable-dashboard/prisma/schema.prisma index 0f171f85..cb3ba992 100644 --- a/examples/composable-dashboard/prisma/schema.prisma +++ b/examples/composable-dashboard/prisma/schema.prisma @@ -14,7 +14,7 @@ model User { password String posts Post[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Post { @@ -28,7 +28,7 @@ model Post { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([authorId]) } diff --git a/examples/custom-field/prisma/schema.prisma b/examples/custom-field/prisma/schema.prisma index 47591168..3c656a71 100644 --- a/examples/custom-field/prisma/schema.prisma +++ b/examples/custom-field/prisma/schema.prisma @@ -15,7 +15,7 @@ model User { favoriteColor String? posts Post[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Post { @@ -29,7 +29,7 @@ model Post { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([authorId]) } diff --git a/examples/file-upload-demo/prisma/schema.prisma b/examples/file-upload-demo/prisma/schema.prisma index 65c8352b..57459171 100644 --- a/examples/file-upload-demo/prisma/schema.prisma +++ b/examples/file-upload-demo/prisma/schema.prisma @@ -14,7 +14,7 @@ model User { avatar Json? posts Post[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Post { @@ -26,5 +26,7 @@ model Post { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt + + @@index([authorId]) } diff --git a/examples/json-demo/prisma/schema.prisma b/examples/json-demo/prisma/schema.prisma index 0e120666..1825bc1a 100644 --- a/examples/json-demo/prisma/schema.prisma +++ b/examples/json-demo/prisma/schema.prisma @@ -14,7 +14,7 @@ model Product { settings Json? configuration Json createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Article { @@ -23,5 +23,5 @@ model Article { content Json? taxonomy Json? createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } diff --git a/examples/rag-openai-chatbot/prisma/schema.prisma b/examples/rag-openai-chatbot/prisma/schema.prisma index 5483875b..e87827bc 100644 --- a/examples/rag-openai-chatbot/prisma/schema.prisma +++ b/examples/rag-openai-chatbot/prisma/schema.prisma @@ -15,5 +15,5 @@ model KnowledgeBase { published Boolean @default(true) contentEmbedding Json? createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } diff --git a/examples/starter-auth/prisma/schema.prisma b/examples/starter-auth/prisma/schema.prisma index 41192eb0..7ded77a2 100644 --- a/examples/starter-auth/prisma/schema.prisma +++ b/examples/starter-auth/prisma/schema.prisma @@ -18,7 +18,7 @@ model Post { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([authorId]) } @@ -33,7 +33,7 @@ model User { accounts Account[] posts Post[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Session { @@ -45,7 +45,7 @@ model Session { userId String? @map("user") user User? @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([userId]) } @@ -64,7 +64,7 @@ model Account { userId String? @map("user") user User? @relation(fields: [userId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([userId]) } @@ -75,5 +75,5 @@ model Verification { value String expiresAt DateTime? createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } diff --git a/examples/starter/prisma/schema.prisma b/examples/starter/prisma/schema.prisma index 0f171f85..cb3ba992 100644 --- a/examples/starter/prisma/schema.prisma +++ b/examples/starter/prisma/schema.prisma @@ -14,7 +14,7 @@ model User { password String posts Post[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Post { @@ -28,7 +28,7 @@ model Post { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt @@index([authorId]) } diff --git a/examples/tiptap-demo/prisma/schema.prisma b/examples/tiptap-demo/prisma/schema.prisma index 7b7a5beb..1523a515 100644 --- a/examples/tiptap-demo/prisma/schema.prisma +++ b/examples/tiptap-demo/prisma/schema.prisma @@ -13,7 +13,7 @@ model User { email String @unique articles Article[] createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt } model Article { @@ -26,5 +26,7 @@ model Article { authorId String? @map("author") author User? @relation(fields: [authorId], references: [id]) createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + updatedAt DateTime @default(now()) @updatedAt + + @@index([authorId]) } diff --git a/packages/cli/src/commands/__snapshots__/generate.test.ts.snap b/packages/cli/src/commands/__snapshots__/generate.test.ts.snap index 2e2bb278..7e0b8eb1 100644 --- a/packages/cli/src/commands/__snapshots__/generate.test.ts.snap +++ b/packages/cli/src/commands/__snapshots__/generate.test.ts.snap @@ -150,6 +150,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -443,6 +451,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Custom DB type that uses Prisma's conditional types with virtual and transformed field support * Types change based on select/include - relationships only present when explicitly included diff --git a/packages/cli/src/generator/__snapshots__/types.test.ts.snap b/packages/cli/src/generator/__snapshots__/types.test.ts.snap index 93b7c114..8bcafdc6 100644 --- a/packages/cli/src/generator/__snapshots__/types.test.ts.snap +++ b/packages/cli/src/generator/__snapshots__/types.test.ts.snap @@ -10,6 +10,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -233,6 +241,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for Post - computed fields not in database * These are added to query results via resolveOutput hooks @@ -459,6 +475,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -586,7 +610,7 @@ export type UserInclude = Prisma.UserInclude & { * type Result = UserGetPayload<{ select: typeof select }> */ export type UserGetPayload = - Omit, 'posts'> & + Omit>, 'posts'> & { posts?: T extends { select: any } @@ -1017,6 +1041,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -1126,7 +1158,7 @@ export type UserSelect = Prisma.UserSelect & { * type Result = UserGetPayload<{ select: typeof select }> */ export type UserGetPayload = - Prisma.UserGetPayload & + Prisma.UserGetPayload> & ( T extends { select: any } ? T['select'] extends true @@ -1276,6 +1308,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for Post - computed fields not in database * These are added to query results via resolveOutput hooks @@ -1502,6 +1542,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -1725,6 +1773,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -1951,6 +2007,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -2538,6 +2602,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for Post - computed fields not in database * These are added to query results via resolveOutput hooks @@ -3008,6 +3080,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for Post - computed fields not in database * These are added to query results via resolveOutput hooks @@ -3478,6 +3558,14 @@ import type { Session as OpensaasSession, StorageUtils, ServerActionProps, Acces import type { PrismaClient, Prisma } from './prisma-client/client' import type { PluginServices } from './plugin-types' +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + /** * Virtual fields for User - computed fields not in database * These are added to query results via resolveOutput hooks @@ -4001,3 +4089,569 @@ export type Context = BaseCo sudo: () => Context }" `; + +exports[`Types Generator > generateTypes > should use StripVirtualFromArgs in GetPayload for lists with virtual + relation fields (regression #383) 1`] = ` +"/** + * Generated types from OpenSaas configuration + * DO NOT EDIT - This file is automatically generated + */ + +import type { Session as OpensaasSession, StorageUtils, ServerActionProps, AccessControlledDB, AccessContext } from '@opensaas/stack-core' +import type { PrismaClient, Prisma } from './prisma-client/client' +import type { PluginServices } from './plugin-types' + +// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them +type StripVirtualFromArgs = + T extends { select: infer S extends object } + ? Omit & { select: Omit } + : T extends { include: infer I extends object } + ? Omit & { include: Omit } + : T + +/** + * Virtual fields for Account - computed fields not in database + * These are added to query results via resolveOutput hooks + */ +export type AccountVirtualFields = { + // No virtual fields defined +} + +/** + * Transformed fields for Account - fields with resultExtension transformations + * These override Prisma's base types with transformed types via result extensions + */ +export type AccountTransformedFields = { + // No transformed fields defined +} + +export type AccountOutput = { + id: string + name: string | null + bills?: BillOutput[] + createdAt: Date + updatedAt: Date +} & AccountVirtualFields + +export type Account = AccountOutput + +export type AccountCreateInput = { + name?: string + bills?: { connect: Array<{ id: string }> } +} + +export type AccountUpdateInput = { + name?: string + bills?: { connect: Array<{ id: string }>, disconnect: Array<{ id: string }> } +} + +export type AccountWhereInput = Prisma.AccountWhereInput + +/** + * Hook types for Account list + * Properly typed to use Prisma's generated input types + */ +export type AccountHooks = { + resolveInput?: (args: + | { + operation: 'create' + resolvedData: Prisma.AccountCreateInput + item: undefined + context: BaseContext + } + | { + operation: 'update' + resolvedData: Prisma.AccountUpdateInput + item: Account + context: BaseContext + } + ) => Promise + validateInput?: (args: { + operation: 'create' | 'update' + resolvedData: Prisma.AccountCreateInput | Prisma.AccountUpdateInput + item?: Account + context: BaseContext + addValidationError: (msg: string) => void + }) => Promise + beforeOperation?: (args: { + operation: 'create' | 'update' | 'delete' + resolvedData?: Prisma.AccountCreateInput | Prisma.AccountUpdateInput + item?: Account + context: BaseContext + }) => Promise + afterOperation?: (args: { + operation: 'create' | 'update' | 'delete' + resolvedData?: Prisma.AccountCreateInput | Prisma.AccountUpdateInput + item?: Account + context: BaseContext + }) => Promise +} + +/** + * Select type for Account with virtual field support + * Extends Prisma's Select type to include virtual fields + * and supports custom Select types in nested relationships + * Use this type when selecting fields to enable virtual field selection + * + * @example + * const select = { + * id: true, + * name: true, + + * } satisfies AccountSelect + */ +export type AccountSelect = Prisma.AccountSelect & { + bills?: boolean | BillDefaultArgs +} + +/** + * Include type for Account with virtual field support + * Extends Prisma's Include type to include virtual fields + * and supports custom Include types in nested relationships + * Use this type when including relationships to enable virtual field selection + * + * @example + * const include = { + * author: true, + + * } satisfies AccountInclude + */ +export type AccountInclude = Prisma.AccountInclude & { + bills?: boolean | BillDefaultArgs +} + +/** + * GetPayload type for Account + * Wraps Prisma's GetPayload to ensure nested relations support virtual fields + * Use this type to get properly typed results with virtual fields + * + * @example + * const select = { + * id: true, + * // Relations can include virtual fields from related lists + * } satisfies AccountSelect + * + * type Result = AccountGetPayload<{ select: typeof select }> + */ +export type AccountGetPayload = + Omit, 'bills'> & + { + bills?: + T extends { select: any } + ? 'bills' extends keyof T['select'] + ? T['select']['bills'] extends true + ? Bill[] + : T['select']['bills'] extends { select: any } + ? BillGetPayload<{ select: T['select']['bills']['select'] }>[] + : T['select']['bills'] extends { include: any } + ? BillGetPayload<{ include: T['select']['bills']['include'] }>[] + : Bill[] + : never + : T extends { include: any } + ? T['include'] extends true + ? Bill[] + : 'bills' extends keyof T['include'] + ? T['include']['bills'] extends true + ? Bill[] + : T['include']['bills'] extends { select: any } + ? BillGetPayload<{ select: T['include']['bills']['select'] }>[] + : T['include']['bills'] extends { include: any } + ? BillGetPayload<{ include: T['include']['bills']['include'] }>[] + : Bill[] + : never + : Bill[] + } & + {} + +/** + * Default args type for Account with custom Select/Include support + * Used in nested relationship selections to support virtual fields + */ +export type AccountDefaultArgs = { + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Custom FindUniqueArgs for Account with virtual field support in nested relationships + */ +export type AccountFindUniqueArgs = Omit & { + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Custom FindManyArgs for Account with virtual field support in nested relationships + */ +export type AccountFindManyArgs = Omit & { + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Custom CreateArgs for Account with virtual field support in nested relationships + */ +export type AccountCreateArgs = Omit & { + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Custom UpdateArgs for Account with virtual field support in nested relationships + */ +export type AccountUpdateArgs = Omit & { + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Custom DeleteArgs for Account with virtual field support in nested relationships + */ +export type AccountDeleteArgs = Omit & { + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Custom CreateManyArgs for Account with virtual field support in nested relationships + */ +export type AccountCreateManyArgs = { + data: Prisma.AccountCreateInput[] + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Custom UpdateManyArgs for Account with virtual field support in nested relationships + */ +export type AccountUpdateManyArgs = { + where?: AccountWhereInput + data: Prisma.AccountUpdateInput + select?: AccountSelect | null + include?: AccountInclude | null +} + +/** + * Virtual fields for Bill - computed fields not in database + * These are added to query results via resolveOutput hooks + */ +export type BillVirtualFields = { + total: string +} + +/** + * Transformed fields for Bill - fields with resultExtension transformations + * These override Prisma's base types with transformed types via result extensions + */ +export type BillTransformedFields = { + // No transformed fields defined +} + +export type BillOutput = { + id: string + name: string | null + accountId: string | null + account?: AccountOutput | null + createdAt: Date + updatedAt: Date +} & BillVirtualFields + +export type Bill = BillOutput + +export type BillCreateInput = { + name?: string + account?: { connect: { id: string } } +} + +export type BillUpdateInput = { + name?: string + account?: { connect: { id: string } } | { disconnect: true } +} + +export type BillWhereInput = Prisma.BillWhereInput + +/** + * Hook types for Bill list + * Properly typed to use Prisma's generated input types + */ +export type BillHooks = { + resolveInput?: (args: + | { + operation: 'create' + resolvedData: Prisma.BillCreateInput + item: undefined + context: BaseContext + } + | { + operation: 'update' + resolvedData: Prisma.BillUpdateInput + item: Bill + context: BaseContext + } + ) => Promise + validateInput?: (args: { + operation: 'create' | 'update' + resolvedData: Prisma.BillCreateInput | Prisma.BillUpdateInput + item?: Bill + context: BaseContext + addValidationError: (msg: string) => void + }) => Promise + beforeOperation?: (args: { + operation: 'create' | 'update' | 'delete' + resolvedData?: Prisma.BillCreateInput | Prisma.BillUpdateInput + item?: Bill + context: BaseContext + }) => Promise + afterOperation?: (args: { + operation: 'create' | 'update' | 'delete' + resolvedData?: Prisma.BillCreateInput | Prisma.BillUpdateInput + item?: Bill + context: BaseContext + }) => Promise +} + +/** + * Select type for Bill with virtual field support + * Extends Prisma's Select type to include virtual fields + * and supports custom Select types in nested relationships + * Use this type when selecting fields to enable virtual field selection + * + * @example + * const select = { + * id: true, + * name: true, + * total: true, // Virtual field + * } satisfies BillSelect + */ +export type BillSelect = Prisma.BillSelect & { + total?: boolean + account?: boolean | AccountDefaultArgs +} + +/** + * Include type for Bill with virtual field support + * Extends Prisma's Include type to include virtual fields + * and supports custom Include types in nested relationships + * Use this type when including relationships to enable virtual field selection + * + * @example + * const include = { + * author: true, + * total: true, // Virtual field + * } satisfies BillInclude + */ +export type BillInclude = Prisma.BillInclude & { + total?: boolean + account?: boolean | AccountDefaultArgs +} + +/** + * GetPayload type for Bill with virtual and transformed field support + * Extends Prisma's GetPayload to include virtual and transformed fields + * Use this type to get properly typed results with virtual fields + * + * @example + * const select = { + * id: true, + * total: true, // Virtual field + * } satisfies BillSelect + * + * type Result = BillGetPayload<{ select: typeof select }> + */ +export type BillGetPayload = + Omit>, 'account'> & + { + account?: + T extends { select: any } + ? 'account' extends keyof T['select'] + ? T['select']['account'] extends true + ? Account + : T['select']['account'] extends { select: any } + ? AccountGetPayload<{ select: T['select']['account']['select'] }> + : T['select']['account'] extends { include: any } + ? AccountGetPayload<{ include: T['select']['account']['include'] }> + : Account + : never + : T extends { include: any } + ? T['include'] extends true + ? Account + : 'account' extends keyof T['include'] + ? T['include']['account'] extends true + ? Account + : T['include']['account'] extends { select: any } + ? AccountGetPayload<{ select: T['include']['account']['select'] }> + : T['include']['account'] extends { include: any } + ? AccountGetPayload<{ include: T['include']['account']['include'] }> + : Account + : never + : Account + } & + ( + T extends { select: any } + ? T['select'] extends true + ? BillVirtualFields + : { + [K in keyof BillVirtualFields as K extends keyof T['select'] + ? T['select'][K] extends true + ? K + : never + : never]: BillVirtualFields[K] + } + : T extends { include: any } + ? T['include'] extends true + ? BillVirtualFields + : { + [K in keyof BillVirtualFields as K extends keyof T['include'] + ? T['include'][K] extends true + ? K + : never + : never]: BillVirtualFields[K] + } + : BillVirtualFields + ) + +/** + * Default args type for Bill with custom Select/Include support + * Used in nested relationship selections to support virtual fields + */ +export type BillDefaultArgs = { + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom FindUniqueArgs for Bill with virtual field support in nested relationships + */ +export type BillFindUniqueArgs = Omit & { + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom FindManyArgs for Bill with virtual field support in nested relationships + */ +export type BillFindManyArgs = Omit & { + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom CreateArgs for Bill with virtual field support in nested relationships + */ +export type BillCreateArgs = Omit & { + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom UpdateArgs for Bill with virtual field support in nested relationships + */ +export type BillUpdateArgs = Omit & { + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom DeleteArgs for Bill with virtual field support in nested relationships + */ +export type BillDeleteArgs = Omit & { + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom CreateManyArgs for Bill with virtual field support in nested relationships + */ +export type BillCreateManyArgs = { + data: Prisma.BillCreateInput[] + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom UpdateManyArgs for Bill with virtual field support in nested relationships + */ +export type BillUpdateManyArgs = { + where?: BillWhereInput + data: Prisma.BillUpdateInput + select?: BillSelect | null + include?: BillInclude | null +} + +/** + * Custom DB type that uses Prisma's conditional types with virtual and transformed field support + * Types change based on select/include - relationships only present when explicitly included + * Virtual fields and transformed fields are added to the base model type + */ +export type CustomDB = Omit, + 'account' | 'bill' +> & { + account: { + findUnique: ( + args: Prisma.SelectSubset + ) => Promise | null> + findMany: ( + args?: Prisma.SelectSubset + ) => Promise>> + create: ( + args: Prisma.SelectSubset + ) => Promise> + update: ( + args: Prisma.SelectSubset + ) => Promise | null> + delete: ( + args: Prisma.SelectSubset + ) => Promise | null> + count: (args?: Prisma.AccountCountArgs) => Promise + createMany: ( + args: Prisma.SelectSubset + ) => Promise>> + updateMany: ( + args: Prisma.SelectSubset + ) => Promise>> + } + bill: { + findUnique: ( + args: Prisma.SelectSubset + ) => Promise | null> + findMany: ( + args?: Prisma.SelectSubset + ) => Promise>> + create: ( + args: Prisma.SelectSubset + ) => Promise> + update: ( + args: Prisma.SelectSubset + ) => Promise | null> + delete: ( + args: Prisma.SelectSubset + ) => Promise | null> + count: (args?: Prisma.BillCountArgs) => Promise + createMany: ( + args: Prisma.SelectSubset + ) => Promise>> + updateMany: ( + args: Prisma.SelectSubset + ) => Promise>> + } +} + +/** + * Base context type for services that only need database and session access + * Compatible with both AccessContext (from hooks) and Context (from server actions) + * Use this type for services that should work in both contexts + */ +export type BaseContext = Omit, 'db' | 'session'> & { + db: CustomDB + session: TSession +} + +/** + * Full context type with server action capabilities and virtual field typing + * Extends BaseContext and adds serverAction and sudo methods + * Use this type in server actions and components that need full context capabilities + */ +export type Context = BaseContext & { + serverAction: (props: ServerActionProps) => Promise + sudo: () => Context +}" +`; diff --git a/packages/cli/src/generator/types.test.ts b/packages/cli/src/generator/types.test.ts index abfbb202..92ac03f1 100644 --- a/packages/cli/src/generator/types.test.ts +++ b/packages/cli/src/generator/types.test.ts @@ -297,11 +297,56 @@ describe('Types Generator', () => { expect(types).toContain('export type UserSelect = Prisma.UserSelect & {') expect(types).toContain('fullName?: boolean') - // Should generate UserGetPayload helper type + // Should generate UserGetPayload helper type using StripVirtualFromArgs expect(types).toContain( 'export type UserGetPayload =', ) - expect(types).toContain('Prisma.UserGetPayload &') + expect(types).toContain( + 'Prisma.UserGetPayload>', + ) + + expect(types).toMatchSnapshot() + }) + + it('should use StripVirtualFromArgs in GetPayload for lists with virtual + relation fields (regression #383)', () => { + const config: OpenSaasConfig = { + db: { + provider: 'sqlite', + }, + lists: { + Account: { + fields: { + name: text(), + bills: relationship({ ref: 'Bill.account', many: true }), + }, + }, + Bill: { + fields: { + name: text(), + total: virtual({ + type: 'string', + hooks: { + resolveOutput: async () => '100.00', + }, + }), + account: relationship({ ref: 'Account.bills' }), + }, + }, + }, + } + + const types = generateTypes(config) + + // The shared utility type must be emitted once + expect(types).toContain('type StripVirtualFromArgs =') + + // Bill has virtual fields so its GetPayload must use StripVirtualFromArgs + expect(types).toContain( + 'Prisma.BillGetPayload>', + ) + + // Account has no virtual fields so its GetPayload must NOT use StripVirtualFromArgs + expect(types).not.toContain('Prisma.AccountGetPayload =`) // Build the base type (Prisma's GetPayload minus relationship and transformed fields) + // When virtual fields exist, strip them from T before passing to Prisma so Prisma never + // tries to resolve a virtual field name (which would produce `never` in its payload type). const fieldsToOmit = [...transformedFieldNames, ...relationshipFields.map((r) => r.name)] + const prismaT = + virtualFields.length > 0 ? `StripVirtualFromArgs` : 'T' if (fieldsToOmit.length > 0) { lines.push( - ` Omit, ${fieldsToOmit.map((n) => `'${n}'`).join(' | ')}> &`, + ` Omit, ${fieldsToOmit.map((n) => `'${n}'`).join(' | ')}> &`, ) } else { - lines.push(` Prisma.${listName}GetPayload &`) + lines.push(` Prisma.${listName}GetPayload<${prismaT}> &`) } // Add transformed fields back @@ -990,6 +994,19 @@ export function generateTypes(config: OpenSaasConfig): string { lines.push('') + // Emit shared utility type used by GetPayload types to strip virtual field keys + // from select/include before passing to Prisma's GetPayload (prevents `never` intersection) + lines.push( + '// Utility: strips virtual field keys from select/include so Prisma GetPayload never sees them', + ) + lines.push('type StripVirtualFromArgs =') + lines.push(' T extends { select: infer S extends object }') + lines.push(" ? Omit & { select: Omit }") + lines.push(' : T extends { include: infer I extends object }') + lines.push(" ? Omit & { include: Omit }") + lines.push(' : T') + lines.push('') + // Generate types for each list for (const [listName, listConfig] of Object.entries(config.lists)) { // Generate VirtualFields type first (needed by Output type and CustomDB)