diff --git a/AGENTS.md b/AGENTS.md index 7b0f8761857f..6019601e0c5a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -123,7 +123,7 @@ - `@prisma/client-common` provides shared client utilities used by both generators and runtime. - `@prisma/client-runtime-utils` provides utility types and singletons for Prisma Client. - NPS survey infrastructure lives in `packages/cli/src/utils/nps/` (`survey.ts` orchestrates: TTY check via `isInteractive` from `@prisma/internals`, 30s prompt timeout, gating on `isCi`/`maybeInGitHook`/`isInNpmLifecycleHook`/`isInContainer`, once-per-timeframe persistence in `env-paths('prisma').config` as `nps.json`). Triggered only from `prisma generate` (suppressed by `--no-hints` and watch mode). Reuse this machinery for any new one-time interactive CLI prompt. - - `prisma mcp` (`packages/cli/src/mcp/MCP.ts`) is a stdio MCP server exposing `migrate-status`, `migrate-dev`, `migrate-reset`, and `Prisma-Studio` tools by shelling back into the CLI binary. + - `prisma mcp` (`packages/cli/src/mcp/MCP.ts`) is a stdio MCP server exposing `migrate-status`, `migrate-dev`, and `Prisma-Studio` tools by shelling back into the CLI binary. - **Coding conventions**: - Use **kebab-case** for new file names (e.g., `query-utils.ts`, `filter-operators.test.ts`). diff --git a/packages/client-generator-ts/src/TSClient/PrismaClient.ts b/packages/client-generator-ts/src/TSClient/PrismaClient.ts index 3317fbc7f40b..465791ff02d0 100644 --- a/packages/client-generator-ts/src/TSClient/PrismaClient.ts +++ b/packages/client-generator-ts/src/TSClient/PrismaClient.ts @@ -75,7 +75,16 @@ function interactiveTransactionDefinition(context: GenerateContext) { const callbackType = ts .functionType() - .addParameter(ts.parameter('prisma', tsx.omit(ts.namedType('PrismaClient'), itxTransactionClientDenyList(context)))) + .addParameter( + ts.parameter( + 'prisma', + ts + .namedType('PrismaClientBase') + .addGenericArgument(ts.namedType('LogOpts')) + .addGenericArgument(ts.namedType('OmitOpts')) + .addGenericArgument(ts.namedType('ExtArgs')), + ), + ) .setReturnType(returnType) const method = ts @@ -88,14 +97,6 @@ function interactiveTransactionDefinition(context: GenerateContext) { return ts.stringify(method, { indentLevel: 1, newLine: 'leading' }) } -function itxTransactionClientDenyList(context: GenerateContext) { - if (!context.isSqlProvider()) { - return ts.unionType([ts.namedType('runtime.ITXClientDenyList'), ts.stringLiteral('$transaction')]) - } - - return ts.namedType('runtime.ITXClientDenyList') -} - function queryRawDefinition(context: GenerateContext) { // we do not generate `$queryRaw...` definitions if not supported if (!context.dmmf.mappings.otherOperations.write.includes('queryRaw')) { @@ -262,6 +263,47 @@ export class PrismaClientClass { public toTS(): string { const { dmmf } = this.context + const isSql = this.context.isSqlProvider() + + const sharedMethods = [ + executeRawDefinition(this.context), + queryRawDefinition(this.context), + queryRawTypedDefinition(this.context), + ...(isSql ? [batchingTransactionDefinition(this.context), interactiveTransactionDefinition(this.context)] : []), + runCommandRawDefinition(this.context), + ] + .filter((d) => d !== null) + .join('\n') + .trim() + + const exclusiveMethods = [ + ...(!isSql ? [batchingTransactionDefinition(this.context), interactiveTransactionDefinition(this.context)] : []), + extendsPropertyDefinition(), + ] + .filter((d) => d !== null) + .join('\n') + .trim() + + const modelOperations = dmmf.mappings.modelOperations + .filter((m) => m.findMany) + .map((m) => { + let methodName = uncapitalize(m.model) + if (methodName === 'constructor') { + methodName = '["constructor"]' + } + const generics = ['ExtArgs', '{ omit: OmitOpts }'] + return `\ +/** + * \`prisma.${methodName}\`: Exposes CRUD operations for the **${m.model}** model. + * Example usage: + * \`\`\`ts + * // Fetch zero or more ${capitalize(m.plural)} + * const ${uncapitalize(m.plural)} = await prisma.${methodName}.findMany() + * \`\`\` + */ +get ${methodName}(): Prisma.${m.model}Delegate<${generics.join(', ')}>;` + }) + .join('\n\n') return `\ export type LogOptions = @@ -277,14 +319,35 @@ export interface PrismaClientConstructor { >(options: Prisma.PrismaClientConstructorArgs): PrismaClient } -${this.jsDoc} -export interface PrismaClient< +/** + * \`PrismaClient\` members shared between \`PrismaClient\` and \`TransactionClient\`. + * + * This interface exists so that \`TransactionClient\` does not have to be defined + * as a separate structural copy of \`PrismaClient\` (e.g. via \`Omit\`). Sharing the + * same member surface keeps property resolution on \`TransactionClient\` and on + * unions of \`PrismaClient\` and \`TransactionClient\` cheap, since TypeScript can + * resolve each member as a direct reference instead of instantiating a mapped type. + * + * This is an internal type and is not part of the public API. + */ +export interface PrismaClientBase< in LogOpts extends Prisma.LogLevel = never, in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'], in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs > { [K: symbol]: { types: Prisma.TypeMap['other'] } +${sharedMethods} + + ${indent(modelOperations, 2)} +} + +${this.jsDoc} +export interface PrismaClient< + in LogOpts extends Prisma.LogLevel = never, + in out OmitOpts extends Prisma.PrismaClientOptions['omit'] = Prisma.PrismaClientOptions['omit'], + in out ExtArgs extends runtime.Types.Extensions.InternalArgs = runtime.Types.Extensions.DefaultArgs +> extends PrismaClientBase { $on(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient; /** @@ -297,42 +360,7 @@ export interface PrismaClient< */ $disconnect(): runtime.Types.Utils.JsPromise; -${[ - executeRawDefinition(this.context), - queryRawDefinition(this.context), - queryRawTypedDefinition(this.context), - batchingTransactionDefinition(this.context), - interactiveTransactionDefinition(this.context), - runCommandRawDefinition(this.context), - extendsPropertyDefinition(), -] - .filter((d) => d !== null) - .join('\n') - .trim()} - - ${indent( - dmmf.mappings.modelOperations - .filter((m) => m.findMany) - .map((m) => { - let methodName = uncapitalize(m.model) - if (methodName === 'constructor') { - methodName = '["constructor"]' - } - const generics = ['ExtArgs', '{ omit: OmitOpts }'] - return `\ -/** - * \`prisma.${methodName}\`: Exposes CRUD operations for the **${m.model}** model. - * Example usage: - * \`\`\`ts - * // Fetch zero or more ${capitalize(m.plural)} - * const ${uncapitalize(m.plural)} = await prisma.${methodName}.findMany() - * \`\`\` - */ -get ${methodName}(): Prisma.${m.model}Delegate<${generics.join(', ')}>;` - }) - .join('\n\n'), - 2, - )} +${exclusiveMethods} }` } } diff --git a/packages/client-generator-ts/src/TSClient/file-generators/PrismaNamespaceFile.ts b/packages/client-generator-ts/src/TSClient/file-generators/PrismaNamespaceFile.ts index f33fd90409fc..31a2b4fa8ce6 100644 --- a/packages/client-generator-ts/src/TSClient/file-generators/PrismaNamespaceFile.ts +++ b/packages/client-generator-ts/src/TSClient/file-generators/PrismaNamespaceFile.ts @@ -24,17 +24,16 @@ export function createPrismaNamespaceFile(context: GenerateContext, options: TSC const imports = [ ts.moduleImport(context.runtimeImport).asNamespace('runtime'), ts.moduleImport(context.importFileName(`../models`)).asNamespace('Prisma').typeOnly(), - ts.moduleImport(context.importFileName(`./class`)).named(ts.namedImport('PrismaClient').typeOnly()), + ts + .moduleImport(context.importFileName(`./class`)) + .named(ts.namedImport('PrismaClient').typeOnly()) + .named(ts.namedImport('PrismaClientBase').typeOnly()), ].map((i) => ts.stringify(i)) const prismaEnums = context.dmmf.schema.enumTypes.prisma?.map((type) => new Enum(type, true).toTS()) const fieldRefs = context.dmmf.schema.fieldRefTypes.prisma?.map((type) => new FieldRefInput(type).toTS()) ?? [] - const transactionClientDenyList = context.isSqlProvider() - ? 'runtime.ITXClientDenyList' - : "runtime.ITXClientDenyList | '$transaction'" - return `${jsDocHeader} ${imports.join('\n')} @@ -140,7 +139,7 @@ export type PrismaAction = /** * \`PrismaClient\` proxy available in interactive transactions. */ -export type TransactionClient = Omit +export type TransactionClient = PrismaClientBase ` } diff --git a/packages/client/src/__tests__/types/$transaction/index.d.ts b/packages/client/src/__tests__/types/$transaction/index.d.ts index d2d6d3b181f9..2fe0f910f828 100644 --- a/packages/client/src/__tests__/types/$transaction/index.d.ts +++ b/packages/client/src/__tests__/types/$transaction/index.d.ts @@ -1 +1 @@ -export { PrismaClient } from '@prisma/client' +export { PrismaClient, Prisma } from '@prisma/client' diff --git a/packages/client/src/__tests__/types/$transaction/index.test-d.ts b/packages/client/src/__tests__/types/$transaction/index.test-d.ts index 36eab9cb5ef5..dc189815c420 100644 --- a/packages/client/src/__tests__/types/$transaction/index.test-d.ts +++ b/packages/client/src/__tests__/types/$transaction/index.test-d.ts @@ -1,6 +1,6 @@ -import { expectError } from 'tsd' +import { expectAssignable, expectError, expectNotAssignable } from 'tsd' -import { PrismaClient } from '.' +import { PrismaClient, Prisma } from '.' const prisma = new PrismaClient() @@ -13,3 +13,19 @@ const prisma = new PrismaClient() expectError(await prisma.$transaction(['str'])) expectError(await prisma.$transaction([{}])) })() + +declare const tx: Prisma.TransactionClient + +// PrismaClient is assignable to TransactionClient. +expectAssignable(prisma) + +// TransactionClient is not assignable to PrismaClient (it is missing the +// members denied inside interactive transactions). +expectNotAssignable(tx) + +// Members denied on TransactionClient are absent from it. +expectNotAssignable<{ $connect: () => void }>(tx) +expectNotAssignable<{ $disconnect: () => void }>(tx) +expectNotAssignable<{ $on: () => void }>(tx) +expectNotAssignable<{ $use: () => void }>(tx) +expectNotAssignable<{ $extends: () => void }>(tx) diff --git a/packages/client/src/__tests__/types/$transaction/test.ts b/packages/client/src/__tests__/types/$transaction/test.ts index 8134a982d4ac..2459dce09ae6 100644 --- a/packages/client/src/__tests__/types/$transaction/test.ts +++ b/packages/client/src/__tests__/types/$transaction/test.ts @@ -1,5 +1,5 @@ import type { User } from '@prisma/client' -import { PrismaClient } from '@prisma/client' +import { PrismaClient, Prisma } from '@prisma/client' // This file will not be executed, just compiled to check if the typings are valid async function main() { @@ -23,6 +23,29 @@ async function main() { // Test Type Fallback const txs = [prisma.user.findMany(), prisma.user.findFirst()] const res: (User | User[] | null)[] = await prisma.$transaction(txs) + + // Interactive transaction: the callback argument supports model operations + // and raw queries. + const count = await prisma.$transaction(async (tx) => { + const users = await tx.user.findMany() + await tx.$queryRaw`SELECT 1` + await tx.$executeRaw`DELETE FROM "User" WHERE 1 = 1` + return users.length + }) + void count + + // Interactive transaction callback argument is a TransactionClient. + const tx2: Prisma.TransactionClient = await prisma.$transaction(async (tx) => tx) + await tx2.user.findMany() + + // PrismaClient is assignable to TransactionClient. + const tx3: Prisma.TransactionClient = prisma + await tx3.user.findMany() + + // `tx ?? client` is usable as a TransactionClient. + const db = (await prisma.$transaction(async (tx) => tx)) ?? prisma + await db.user.findMany() + await db.$queryRaw`SELECT 1` } main().catch((e) => { diff --git a/packages/migrate/src/commands/DbPull.ts b/packages/migrate/src/commands/DbPull.ts index fb47a43f87a5..c25ba7048fc6 100644 --- a/packages/migrate/src/commands/DbPull.ts +++ b/packages/migrate/src/commands/DbPull.ts @@ -249,7 +249,7 @@ Then you can run ${green(getCommandWithExecutor('prisma db pull'))} again. /* P1012: Schema parsing error */ process.stdout.write('\n') // empty line - const message = relativizePathInPSLError(e.message) + const message = relativizePathInPSLError(e.message as string) throw new Error(`${red(message)} Introspection failed as your current Prisma schema file is invalid diff --git a/packages/migrate/src/utils/captureStdout.ts b/packages/migrate/src/utils/captureStdout.ts index a4761cb39c94..5630ef12261d 100644 --- a/packages/migrate/src/utils/captureStdout.ts +++ b/packages/migrate/src/utils/captureStdout.ts @@ -59,7 +59,7 @@ export class CaptureStdout { * @param string * @private */ - _writeCapture(string) { + _writeCapture(string: string) { this._capturedText.push(string) } diff --git a/packages/migrate/src/utils/printDatasources.ts b/packages/migrate/src/utils/printDatasources.ts index 48bcde84fa9c..174258a16158 100644 --- a/packages/migrate/src/utils/printDatasources.ts +++ b/packages/migrate/src/utils/printDatasources.ts @@ -49,7 +49,7 @@ ${indent(printDatamodelObject(obj), tab)} } } -export function printDatamodelObject(obj: any): string { +export function printDatamodelObject(obj: Record): string { const maxLength = Object.keys(obj).reduce((max, curr) => Math.max(max, curr.length), 0) return Object.entries(obj) .map( diff --git a/packages/migrate/src/utils/promptForMigrationName.ts b/packages/migrate/src/utils/promptForMigrationName.ts index e57cb593e9d5..72f9f79b21e3 100644 --- a/packages/migrate/src/utils/promptForMigrationName.ts +++ b/packages/migrate/src/utils/promptForMigrationName.ts @@ -44,6 +44,6 @@ export async function getMigrationName(name?: string): Promise fs.rm(path))) + await Promise.all(files.map(([path]) => fs.rm(path as string))) } diff --git a/packages/type-benchmark-tests/huge-schema/transaction-client-types.ts b/packages/type-benchmark-tests/huge-schema/transaction-client-types.ts new file mode 100644 index 000000000000..88fa613e04d8 --- /dev/null +++ b/packages/type-benchmark-tests/huge-schema/transaction-client-types.ts @@ -0,0 +1,32 @@ +import type { Prisma, PrismaClient } from './generated/client' + +declare const client: PrismaClient +declare const tx: Prisma.TransactionClient + +// PrismaClient is assignable to TransactionClient (positive contract). +const _positive: Prisma.TransactionClient = client + +// TransactionClient is not assignable to PrismaClient: it is missing the +// members denied inside interactive transactions. +// @ts-expect-error TransactionClient is missing the members denied inside interactive transactions +const _negative: PrismaClient = tx + +// The members denied inside interactive transactions are absent from +// TransactionClient. Each assertion below intentionally errors today: if any +// denied member leaks back onto TransactionClient, the corresponding +// directive becomes unused and tsc fails with TS2578. +// @ts-expect-error $connect is denied on TransactionClient +const _connect: { $connect: () => void } = tx +// @ts-expect-error $disconnect is denied on TransactionClient +const _disconnect: { $disconnect: () => void } = tx +// @ts-expect-error $on is denied on TransactionClient +const _on: { $on: () => void } = tx +// @ts-expect-error $extends is denied on TransactionClient +const _extends: { $extends: () => void } = tx + +void _negative +void _connect +void _disconnect +void _on +void _extends +void _positive diff --git a/packages/type-benchmark-tests/huge-schema/transaction-client.bench.ts b/packages/type-benchmark-tests/huge-schema/transaction-client.bench.ts new file mode 100644 index 000000000000..878db857e0a4 --- /dev/null +++ b/packages/type-benchmark-tests/huge-schema/transaction-client.bench.ts @@ -0,0 +1,33 @@ +/* eslint-disable @typescript-eslint/no-floating-promises */ +// (more convenient benches since we only care about types) + +import { bench } from '@ark/attest' + +// @ts-ignore +import type { Prisma, PrismaClient } from './generated/client' + +declare const client: PrismaClient + +bench.baseline(() => { + client.model1.findUnique({ + where: { id: 1 }, + }) +}) + +bench('transaction client: tx ?? client', () => { + async function run() { + const tx = await client.$transaction(async (tx) => { + await tx.model1.findFirst() + return tx + }) + const db = tx ?? client + await db.model1.findMany() + await db.$queryRaw`SELECT 1` + } + void run +}).types([638, 'instantiations']) + +bench('transaction client: explicit union', () => { + const db: Prisma.TransactionClient | PrismaClient = client + db.model1.findMany() +}).types([522, 'instantiations']) diff --git a/packages/type-benchmark-tests/test.ts b/packages/type-benchmark-tests/test.ts index 1499a6f2a24b..6f57921131ff 100644 --- a/packages/type-benchmark-tests/test.ts +++ b/packages/type-benchmark-tests/test.ts @@ -33,6 +33,8 @@ async function main() { } if (shouldOnlyGenerate) continue + results.push(await runTypecheck(dir, cwd)) + const benchFiles = getBenchmarkFiles(dir) for (const benchFile of benchFiles) { if (testFilter && !`${dir}/${benchFile}`.includes(testFilter)) { @@ -109,6 +111,22 @@ function getBenchmarkFiles(dir: string) { }) } +async function runTypecheck(dir: string, cwd: string) { + console.log(`Running tsc typecheck in ${dir}...`) + try { + await execa('tsc', ['--noEmit', '-p', 'tsconfig.json'], { cwd, stdio: 'inherit' }) + return { + directory: `${dir}/typecheck`, + success: true, + } + } catch { + return { + directory: `${dir}/typecheck`, + success: false, + } + } +} + async function runGenerate(dir: string, cwd: string) { console.log(`Running generate command in ${dir}...`) // tsx sometimes crashes with stack overflow with the default stack size when diff --git a/scripts/bench.ts b/scripts/bench.ts index 11e8f2d8d2c3..7d1b08b70e46 100644 --- a/scripts/bench.ts +++ b/scripts/bench.ts @@ -1,10 +1,11 @@ -import { execaCommand } from 'execa' +import { execa } from 'execa' import globby from 'globby' async function main() { - let benchmarks = await globby(['./packages/**/*.bench.ts', '!./packages/type-benchmark-tests/**'], { + let benchmarks = await globby(['./packages/**/*.bench.ts', '!./packages/type-benchmark-tests/**', '!**/node_modules/**', '!**/.git/**', '!**/dist/**', '!**/build/**'], { gitignore: true, }) + console.log('After globby, benchmarks count:', benchmarks.length) if (process.argv.length > 2) { const filterRegex = new RegExp(process.argv[2]) @@ -23,7 +24,7 @@ async function run(benchmarks: string[]) { for (const location of benchmarks) { try { - await execaCommand(`node -r esbuild-register ${location}`, { + await execa('node', ['-r', 'esbuild-register', location], { stdio: 'inherit', }) } catch (e) { @@ -41,4 +42,4 @@ async function run(benchmarks: string[]) { main().catch((e) => { console.error(e) process.exit(1) -}) +}) \ No newline at end of file