Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Expand Down
122 changes: 75 additions & 47 deletions packages/client-generator-ts/src/TSClient/PrismaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')) {
Expand Down Expand Up @@ -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<ClientOptions extends Prisma.PrismaClientOptions> =
Expand All @@ -277,14 +319,35 @@ export interface PrismaClientConstructor {
>(options: Prisma.PrismaClientConstructorArgs<Options>): PrismaClient<LogOpts, OmitOpts, ExtArgs>
}

${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<ExtArgs>['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<LogOpts, OmitOpts, ExtArgs> {
$on<V extends LogOpts>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;

/**
Expand All @@ -297,42 +360,7 @@ export interface PrismaClient<
*/
$disconnect(): runtime.Types.Utils.JsPromise<void>;

${[
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}
}`
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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')}

Expand Down Expand Up @@ -140,7 +139,7 @@ export type PrismaAction =
/**
* \`PrismaClient\` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<DefaultPrismaClient, ${transactionClientDenyList}>
export type TransactionClient = PrismaClientBase

`
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { PrismaClient } from '@prisma/client'
export { PrismaClient, Prisma } from '@prisma/client'
20 changes: 18 additions & 2 deletions packages/client/src/__tests__/types/$transaction/index.test-d.ts
Original file line number Diff line number Diff line change
@@ -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()

Expand All @@ -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>(prisma)

// TransactionClient is not assignable to PrismaClient (it is missing the
// members denied inside interactive transactions).
expectNotAssignable<PrismaClient>(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)
25 changes: 24 additions & 1 deletion packages/client/src/__tests__/types/$transaction/test.ts
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/migrate/src/commands/DbPull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/migrate/src/utils/captureStdout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class CaptureStdout {
* @param string
* @private
*/
_writeCapture(string) {
_writeCapture(string: string) {
this._capturedText.push(string)
}

Expand Down
2 changes: 1 addition & 1 deletion packages/migrate/src/utils/printDatasources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ ${indent(printDatamodelObject(obj), tab)}
}
}

export function printDatamodelObject(obj: any): string {
export function printDatamodelObject(obj: Record<string, unknown>): string {
const maxLength = Object.keys(obj).reduce((max, curr) => Math.max(max, curr.length), 0)
return Object.entries(obj)
.map(
Expand Down
2 changes: 1 addition & 1 deletion packages/migrate/src/utils/promptForMigrationName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,6 @@ export async function getMigrationName(name?: string): Promise<getMigrationNameO
}

return {
name: slugify(response.name, { separator: '_' }).substring(0, maxMigrationNameLength) || '',
name: slugify(response.name as string, { separator: '_' }).substring(0, maxMigrationNameLength) || '',
}
}
2 changes: 1 addition & 1 deletion packages/migrate/src/utils/removeSchemaFiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ import fs from 'node:fs/promises'
import { MultipleSchemas } from '@prisma/internals'

export async function removeSchemaFiles(files: MultipleSchemas) {
await Promise.all(files.map(([path]) => fs.rm(path)))
await Promise.all(files.map(([path]) => fs.rm(path as string)))
}
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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'])
Loading