PrismaClient.ts•25 kB
import { capitalize, NonModelOperation, Operation, uncapitalize } from '@prisma/client-common'
import type * as DMMF from '@prisma/dmmf'
import type { DataSource } from '@prisma/generator'
import { assertNever } from '@prisma/internals'
import * as ts from '@prisma/ts-builders'
import indent from 'indent-string'
import {
extArgsParam,
getAggregateName,
getCountAggregateOutputName,
getFieldRefsTypeName,
getGroupByName,
getModelArgName,
getPayloadName,
} from '../utils'
import { runtimeImport, runtimeImportedType } from '../utils/runtimeImport'
import { TAB_SIZE } from './constants'
import { Datasources } from './Datasources'
import type { Generable } from './Generable'
import { GenerateContext } from './GenerateContext'
import { globalOmitConfig } from './globalOmit'
import { TSClientOptions } from './TSClient'
import { getModelActions } from './utils/getModelActions'
import * as tsx from './utils/type-builders'
function clientTypeMapModelsDefinition(context: GenerateContext) {
const meta = ts.objectType()
const modelNames = context.dmmf.datamodel.models.map((m) => m.name)
// `modelNames` can be empty if `generate --allow-no-models` is used.
if (modelNames.length === 0) {
meta.add(ts.property('modelProps', ts.neverType))
} else {
meta.add(ts.property('modelProps', ts.unionType(modelNames.map((name) => ts.stringLiteral(uncapitalize(name))))))
}
const isolationLevel = context.dmmf.hasEnumInNamespace('TransactionIsolationLevel', 'prisma')
? ts.namedType('Prisma.TransactionIsolationLevel')
: ts.neverType
meta.add(ts.property('txIsolationLevel', isolationLevel))
const model = ts.objectType()
model.addMultiple(
modelNames.map((modelName) => {
const entry = ts.objectType()
entry.add(
ts.property('payload', ts.namedType(getPayloadName(modelName)).addGenericArgument(extArgsParam.toArgument())),
)
entry.add(ts.property('fields', ts.namedType(`Prisma.${getFieldRefsTypeName(modelName)}`)))
const actions = getModelActions(context.dmmf, modelName)
const operations = ts.objectType()
operations.addMultiple(
actions.map((action) => {
const operationType = ts.objectType()
const argsType = `Prisma.${getModelArgName(modelName, action)}`
operationType.add(ts.property('args', ts.namedType(argsType).addGenericArgument(extArgsParam.toArgument())))
operationType.add(ts.property('result', clientTypeMapModelsResultDefinition(modelName, action)))
return ts.property(action, operationType)
}),
)
entry.add(ts.property('operations', operations))
return ts.property(modelName, entry)
}),
)
return ts
.objectType()
.add(ts.property('globalOmitOptions', ts.objectType().add(ts.property('omit', ts.namedType('GlobalOmitOptions')))))
.add(ts.property('meta', meta))
.add(ts.property('model', model))
}
function clientTypeMapModelsResultDefinition(
modelName: string,
action: Exclude<Operation, `$${string}`>,
): ts.TypeBuilder {
if (action === 'count')
return ts.unionType([tsx.optional(ts.namedType(getCountAggregateOutputName(modelName))), ts.numberType])
if (action === 'groupBy') return ts.array(tsx.optional(ts.namedType(getGroupByName(modelName))))
if (action === 'aggregate') return tsx.optional(ts.namedType(getAggregateName(modelName)))
if (action === 'findRaw') return ts.namedType('JsonObject')
if (action === 'aggregateRaw') return ts.namedType('JsonObject')
if (action === 'deleteMany') return ts.namedType('BatchPayload')
if (action === 'createMany') return ts.namedType('BatchPayload')
if (action === 'createManyAndReturn') return ts.array(payloadToResult(modelName))
if (action === 'updateMany') return ts.namedType('BatchPayload')
if (action === 'updateManyAndReturn') return ts.array(payloadToResult(modelName))
if (action === 'findMany') return ts.array(payloadToResult(modelName))
if (action === 'findFirst') return ts.unionType([payloadToResult(modelName), ts.nullType])
if (action === 'findUnique') return ts.unionType([payloadToResult(modelName), ts.nullType])
if (action === 'findFirstOrThrow') return payloadToResult(modelName)
if (action === 'findUniqueOrThrow') return payloadToResult(modelName)
if (action === 'create') return payloadToResult(modelName)
if (action === 'update') return payloadToResult(modelName)
if (action === 'upsert') return payloadToResult(modelName)
if (action === 'delete') return payloadToResult(modelName)
assertNever(action, `Unknown action: ${action}`)
}
function payloadToResult(modelName: string) {
return ts.namedType('$Utils.PayloadToResult').addGenericArgument(ts.namedType(getPayloadName(modelName)))
}
function clientTypeMapOthersDefinition(context: GenerateContext) {
const otherOperationsNames = context.dmmf.getOtherOperationNames().flatMap((name) => {
const results = [`$${name}`]
if (name === 'executeRaw' || name === 'queryRaw') {
results.push(`$${name}Unsafe`)
}
if (name === 'queryRaw' && context.isPreviewFeatureOn('typedSql')) {
results.push(`$queryRawTyped`)
}
return results
})
const argsResultMap = {
$executeRaw: { args: '[query: TemplateStringsArray | Prisma.Sql, ...values: any[]]', result: 'any' },
$queryRaw: { args: '[query: TemplateStringsArray | Prisma.Sql, ...values: any[]]', result: 'any' },
$executeRawUnsafe: { args: '[query: string, ...values: any[]]', result: 'any' },
$queryRawUnsafe: { args: '[query: string, ...values: any[]]', result: 'any' },
$runCommandRaw: { args: 'Prisma.InputJsonObject', result: 'Prisma.JsonObject' },
$queryRawTyped: { args: 'runtime.UnknownTypedSql', result: 'Prisma.JsonObject' },
} satisfies Record<NonModelOperation, { args: string; result: string }>
return `{
other: {
payload: any
operations: {${otherOperationsNames.reduce((acc, action) => {
return `${acc}
${action}: {
args: ${argsResultMap[action].args},
result: ${argsResultMap[action].result}
}`
}, '')}
}
}
}`
}
function clientTypeMapDefinition(context: GenerateContext) {
const typeMap = `${ts.stringify(clientTypeMapModelsDefinition(context))} & ${clientTypeMapOthersDefinition(context)}`
return `
interface TypeMapCb<ClientOptions = {}> extends $Utils.Fn<{extArgs: $Extensions.InternalArgs }, $Utils.Record<string, any>> {
returns: Prisma.TypeMap<this['params']['extArgs'], ClientOptions extends { omit: infer OmitOptions } ? OmitOptions : {}>
}
export type TypeMap<ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs, GlobalOmitOptions = {}> = ${typeMap}`
}
function clientExtensionsDefinitions(context: GenerateContext) {
const typeMap = clientTypeMapDefinition(context)
const define = ts.moduleExport(
ts.constDeclaration(
'defineExtension',
ts
.namedType('$Extensions.ExtendsHook')
.addGenericArgument(ts.stringLiteral('define'))
.addGenericArgument(ts.namedType('Prisma.TypeMapCb'))
.addGenericArgument(ts.namedType('$Extensions.DefaultArgs')),
),
)
return [typeMap, ts.stringify(define)].join('\n')
}
function extendsPropertyDefinition() {
const extendsDefinition = ts
.namedType('$Extensions.ExtendsHook')
.addGenericArgument(ts.stringLiteral('extends'))
.addGenericArgument(ts.namedType('Prisma.TypeMapCb').addGenericArgument(ts.namedType('ClientOptions')))
.addGenericArgument(ts.namedType('ExtArgs'))
.addGenericArgument(
ts
.namedType('$Utils.Call')
.addGenericArgument(ts.namedType('Prisma.TypeMapCb').addGenericArgument(ts.namedType('ClientOptions')))
.addGenericArgument(ts.objectType().add(ts.property('extArgs', ts.namedType('ExtArgs')))),
)
return ts.stringify(ts.property('$extends', extendsDefinition), { indentLevel: 1 })
}
function batchingTransactionDefinition(context: GenerateContext) {
const method = ts
.method('$transaction')
.setDocComment(
ts.docComment`
Allows the running of a sequence of read/write operations that are guaranteed to either succeed or fail as a whole.
@example
\`\`\`
const [george, bob, alice] = await prisma.$transaction([
prisma.user.create({ data: { name: 'George' } }),
prisma.user.create({ data: { name: 'Bob' } }),
prisma.user.create({ data: { name: 'Alice' } }),
])
\`\`\`
Read more in our [docs](https://www.prisma.io/docs/concepts/components/prisma-client/transactions).
`,
)
.addGenericParameter(ts.genericParameter('P').extends(ts.array(tsx.prismaPromise(ts.anyType))))
.addParameter(ts.parameter('arg', ts.arraySpread(ts.namedType('P'))))
.setReturnType(tsx.promise(ts.namedType('runtime.Types.Utils.UnwrapTuple').addGenericArgument(ts.namedType('P'))))
if (context.dmmf.hasEnumInNamespace('TransactionIsolationLevel', 'prisma')) {
const options = ts
.objectType()
.formatInline()
.add(ts.property('isolationLevel', ts.namedType('Prisma.TransactionIsolationLevel')).optional())
method.addParameter(ts.parameter('options', options).optional())
}
return ts.stringify(method, { indentLevel: 1, newLine: 'leading' })
}
function interactiveTransactionDefinition(context: GenerateContext) {
const options = ts
.objectType()
.formatInline()
.add(ts.property('maxWait', ts.numberType).optional())
.add(ts.property('timeout', ts.numberType).optional())
if (context.dmmf.hasEnumInNamespace('TransactionIsolationLevel', 'prisma')) {
const isolationLevel = ts.property('isolationLevel', ts.namedType('Prisma.TransactionIsolationLevel')).optional()
options.add(isolationLevel)
}
const returnType = tsx.promise(ts.namedType('R'))
const callbackType = ts
.functionType()
.addParameter(
ts.parameter('prisma', ts.omit(ts.namedType('PrismaClient'), ts.namedType('runtime.ITXClientDenyList'))),
)
.setReturnType(returnType)
const method = ts
.method('$transaction')
.addGenericParameter(ts.genericParameter('R'))
.addParameter(ts.parameter('fn', callbackType))
.addParameter(ts.parameter('options', options).optional())
.setReturnType(returnType)
return ts.stringify(method, { indentLevel: 1, newLine: 'leading' })
}
function queryRawDefinition(context: GenerateContext) {
// we do not generate `$queryRaw...` definitions if not supported
if (!context.dmmf.mappings.otherOperations.write.includes('queryRaw')) {
return '' // https://github.com/prisma/prisma/issues/8189
}
return `
/**
* Performs a prepared raw query and returns the \`SELECT\` data.
* @example
* \`\`\`
* const result = await prisma.$queryRaw\`SELECT * FROM User WHERE id = \${1} OR email = \${'user@email.com'};\`
* \`\`\`
*
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
*/
$queryRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<T>;
/**
* Performs a raw query and returns the \`SELECT\` data.
* Susceptible to SQL injections, see documentation.
* @example
* \`\`\`
* const result = await prisma.$queryRawUnsafe('SELECT * FROM User WHERE id = $1 OR email = $2;', 1, 'user@email.com')
* \`\`\`
*
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
*/
$queryRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<T>;`
}
function executeRawDefinition(context: GenerateContext) {
// we do not generate `$executeRaw...` definitions if not supported
if (!context.dmmf.mappings.otherOperations.write.includes('executeRaw')) {
return '' // https://github.com/prisma/prisma/issues/8189
}
return `
/**
* Executes a prepared raw query and returns the number of affected rows.
* @example
* \`\`\`
* const result = await prisma.$executeRaw\`UPDATE User SET cool = \${true} WHERE email = \${'user@email.com'};\`
* \`\`\`
*
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
*/
$executeRaw<T = unknown>(query: TemplateStringsArray | Prisma.Sql, ...values: any[]): Prisma.PrismaPromise<number>;
/**
* Executes a raw query and returns the number of affected rows.
* Susceptible to SQL injections, see documentation.
* @example
* \`\`\`
* const result = await prisma.$executeRawUnsafe('UPDATE User SET cool = $1 WHERE email = $2 ;', true, 'user@email.com')
* \`\`\`
*
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
*/
$executeRawUnsafe<T = unknown>(query: string, ...values: any[]): Prisma.PrismaPromise<number>;`
}
function queryRawTypedDefinition(context: GenerateContext) {
if (!context.isPreviewFeatureOn('typedSql')) {
return ''
}
if (!context.dmmf.mappings.otherOperations.write.includes('queryRaw')) {
return ''
}
const param = ts.genericParameter('T')
const method = ts
.method('$queryRawTyped')
.setDocComment(
ts.docComment`
Executes a typed SQL query and returns a typed result
@example
\`\`\`
import { myQuery } from '@prisma/client/sql'
const result = await prisma.$queryRawTyped(myQuery())
\`\`\`
`,
)
.addGenericParameter(param)
.addParameter(
ts.parameter(
'typedSql',
runtimeImportedType('TypedSql')
.addGenericArgument(ts.array(ts.unknownType))
.addGenericArgument(param.toArgument()),
),
)
.setReturnType(tsx.prismaPromise(ts.array(param.toArgument())))
return ts.stringify(method, { indentLevel: 1, newLine: 'leading' })
}
function metricDefinition(context: GenerateContext) {
if (!context.isPreviewFeatureOn('metrics')) {
return ''
}
const property = ts
.property('$metrics', ts.namedType(`runtime.${runtimeImport('MetricsClient')}`))
.setDocComment(
ts.docComment`
Gives access to the client metrics in json or prometheus format.
@example
\`\`\`
const metrics = await prisma.$metrics.json()
// or
const metrics = await prisma.$metrics.prometheus()
\`\`\`
`,
)
.readonly()
return ts.stringify(property, { indentLevel: 1, newLine: 'leading' })
}
function runCommandRawDefinition(context: GenerateContext) {
// we do not generate `$runCommandRaw` definitions if not supported
if (!context.dmmf.mappings.otherOperations.write.includes('runCommandRaw')) {
return '' // https://github.com/prisma/prisma/issues/8189
}
const method = ts
.method('$runCommandRaw')
.addParameter(ts.parameter('command', ts.namedType('Prisma.InputJsonObject')))
.setReturnType(tsx.prismaPromise(ts.namedType('Prisma.JsonObject'))).setDocComment(ts.docComment`
Executes a raw MongoDB command and returns the result of it.
@example
\`\`\`
const user = await prisma.$runCommandRaw({
aggregate: 'User',
pipeline: [{ $match: { name: 'Bob' } }, { $project: { email: true, _id: false } }],
explain: false,
})
\`\`\`
Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/raw-database-access).
`)
return ts.stringify(method, { indentLevel: 1, newLine: 'leading' })
}
function applyPendingMigrationsDefinition(this: PrismaClientClass) {
if (this.runtimeNameTs !== 'react-native') {
return null
}
const method = ts
.method('$applyPendingMigrations')
.setReturnType(tsx.promise(ts.voidType))
.setDocComment(
ts.docComment`Tries to apply pending migrations one by one. If a migration fails to apply, the function will stop and throw an error. You are responsible for informing the user and possibly blocking the app as we cannot guarantee the state of the database.`,
)
return ts.stringify(method, { indentLevel: 1, newLine: 'leading' })
}
function eventRegistrationMethodDeclaration(runtimeNameTs: TSClientOptions['runtimeNameTs']) {
if (runtimeNameTs === 'binary.js') {
return `$on<V extends (U | 'beforeExit')>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : V extends 'beforeExit' ? () => $Utils.JsPromise<void> : Prisma.LogEvent) => void): PrismaClient;`
} else {
return `$on<V extends U>(eventType: V, callback: (event: V extends 'query' ? Prisma.QueryEvent : Prisma.LogEvent) => void): PrismaClient;`
}
}
export class PrismaClientClass implements Generable {
constructor(
protected readonly context: GenerateContext,
protected readonly internalDatasources: DataSource[],
protected readonly outputDir: string,
protected readonly runtimeNameTs: TSClientOptions['runtimeNameTs'],
protected readonly browser?: boolean,
) {}
private get jsDoc(): string {
const { dmmf } = this.context
let example: DMMF.ModelMapping
if (dmmf.mappings.modelOperations.length) {
example = dmmf.mappings.modelOperations[0]
} else {
// because generator models is empty we need to create a fake example
example = {
model: 'User',
plural: 'users',
}
}
return `/**
* ## Prisma Client ʲˢ
*
* Type-safe database client for TypeScript & Node.js
* @example
* \`\`\`
* const prisma = new PrismaClient()
* // Fetch zero or more ${capitalize(example.plural)}
* const ${uncapitalize(example.plural)} = await prisma.${uncapitalize(example.model)}.findMany()
* \`\`\`
*
*
* Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client).
*/`
}
public toTSWithoutNamespace(): string {
const { dmmf } = this.context
return `${this.jsDoc}
export class PrismaClient<
ClientOptions extends Prisma.PrismaClientOptions = Prisma.PrismaClientOptions,
const U = 'log' extends keyof ClientOptions ? ClientOptions['log'] extends Array<Prisma.LogLevel | Prisma.LogDefinition> ? Prisma.GetEvents<ClientOptions['log']> : never : never,
ExtArgs extends $Extensions.InternalArgs = $Extensions.DefaultArgs
> {
[K: symbol]: { types: Prisma.TypeMap<ExtArgs>['other'] }
${indent(this.jsDoc, TAB_SIZE)}
constructor(optionsArg ?: Prisma.Subset<ClientOptions, Prisma.PrismaClientOptions>);
${eventRegistrationMethodDeclaration(this.runtimeNameTs)}
/**
* Connect with the database
*/
$connect(): $Utils.JsPromise<void>;
/**
* Disconnect from the database
*/
$disconnect(): $Utils.JsPromise<void>;
${[
executeRawDefinition(this.context),
queryRawDefinition(this.context),
queryRawTypedDefinition(this.context),
batchingTransactionDefinition(this.context),
interactiveTransactionDefinition(this.context),
runCommandRawDefinition(this.context),
metricDefinition(this.context),
applyPendingMigrationsDefinition.bind(this)(),
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', 'ClientOptions']
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,
)}
}`
}
public toTS(): string {
const clientOptions = this.buildClientOptions()
return `${new Datasources(this.internalDatasources).toTS()}
${clientExtensionsDefinitions(this.context)}
export type DefaultPrismaClient = PrismaClient
export type ErrorFormat = 'pretty' | 'colorless' | 'minimal'
${ts.stringify(ts.moduleExport(clientOptions))}
${ts.stringify(globalOmitConfig(this.context.dmmf))}
/* Types for Logging */
export type LogLevel = 'info' | 'query' | 'warn' | 'error'
export type LogDefinition = {
level: LogLevel
emit: 'stdout' | 'event'
}
export type CheckIsLogLevel<T> = T extends LogLevel ? T : never;
export type GetLogType<T> = CheckIsLogLevel<
T extends LogDefinition ? T['level'] : T
>;
export type GetEvents<T extends any[]> = T extends Array<LogLevel | LogDefinition>
? GetLogType<T[number]>
: never;
export type QueryEvent = {
timestamp: Date
query: string
params: string
duration: number
target: string
}
export type LogEvent = {
timestamp: Date
message: string
target: string
}
/* End Types for Logging */
export type PrismaAction =
| 'findUnique'
| 'findUniqueOrThrow'
| 'findMany'
| 'findFirst'
| 'findFirstOrThrow'
| 'create'
| 'createMany'
| 'createManyAndReturn'
| 'update'
| 'updateMany'
| 'updateManyAndReturn'
| 'upsert'
| 'delete'
| 'deleteMany'
| 'executeRaw'
| 'queryRaw'
| 'aggregate'
| 'count'
| 'runCommandRaw'
| 'findRaw'
| 'groupBy'
// tested in getLogLevel.test.ts
export function getLogLevel(log: Array<LogLevel | LogDefinition>): LogLevel | undefined;
/**
* \`PrismaClient\` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<Prisma.DefaultPrismaClient, runtime.ITXClientDenyList>
`
}
private buildClientOptions() {
const clientOptions = ts
.interfaceDeclaration('PrismaClientOptions')
.add(
ts
.property('datasources', ts.namedType('Datasources'))
.optional()
.setDocComment(ts.docComment('Overwrites the datasource url from your schema.prisma file')),
)
.add(
ts
.property('datasourceUrl', ts.stringType)
.optional()
.setDocComment(ts.docComment('Overwrites the datasource url from your schema.prisma file')),
)
.add(
ts
.property('errorFormat', ts.namedType('ErrorFormat'))
.optional()
.setDocComment(ts.docComment('@default "colorless"')),
)
.add(
ts.property('log', ts.array(ts.unionType([ts.namedType('LogLevel'), ts.namedType('LogDefinition')]))).optional()
.setDocComment(ts.docComment`
@example
\`\`\`
// Shorthand for \`emit: 'stdout'\`
log: ['query', 'info', 'warn', 'error']
// Emit as events only
log: [
{ emit: 'event', level: 'query' },
{ emit: 'event', level: 'info' },
{ emit: 'event', level: 'warn' }
{ emit: 'event', level: 'error' }
]
// Emit as events and log to stdout
log: [
{ emit: 'stdout', level: 'query' },
{ emit: 'stdout', level: 'info' },
{ emit: 'stdout', level: 'warn' }
{ emit: 'stdout', level: 'error' }
]
\`\`\`
Read more in our [docs](https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/logging#the-log-option).
`),
)
const transactionOptions = ts
.objectType()
.add(ts.property('maxWait', ts.numberType).optional())
.add(ts.property('timeout', ts.numberType).optional())
if (this.context.dmmf.hasEnumInNamespace('TransactionIsolationLevel', 'prisma')) {
transactionOptions.add(ts.property('isolationLevel', ts.namedType('Prisma.TransactionIsolationLevel')).optional())
}
clientOptions.add(
ts.property('transactionOptions', transactionOptions).optional().setDocComment(ts.docComment`
The default values for transactionOptions
maxWait ?= 2000
timeout ?= 5000
`),
)
if (
['library.js', 'client.js'].includes(this.runtimeNameTs) &&
// We don't support a custom adapter with MongoDB for now.
this.internalDatasources.some((d) => d.provider !== 'mongodb')
) {
clientOptions.add(
ts
.property('adapter', ts.unionType([ts.namedType('runtime.SqlDriverAdapterFactory'), ts.namedType('null')]))
.optional()
.setDocComment(
ts.docComment('Instance of a Driver Adapter, e.g., like one provided by `@prisma/adapter-planetscale`'),
),
)
}
clientOptions.add(
ts.property('omit', ts.namedType('Prisma.GlobalOmitConfig')).optional().setDocComment(ts.docComment`
Global configuration for omitting model fields by default.
@example
\`\`\`
const prisma = new PrismaClient({
omit: {
user: {
password: true
}
}
})
\`\`\`
`),
)
return clientOptions
}
}