Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(client): fix tsc crashes with client extensions #16856

Merged
merged 2 commits into from
Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/client/scripts/default-index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export declare type PrismaClientExtends<ExtArgs extends runtime.Types.Extensions
Args extends runtime.Types.Extensions.Args = { result: R; model: M; query: Q; client: C }
>(args: ((client: PrismaClientExtends<ExtArgs>) => { $extends: { extArgs: Args } }) | {
result?: R; model?: M; query?: Q; client?: C
}) => PrismaClientExtends<runtime.Types.Extensions.MergeArgs<Args, ExtArgs, string, false>>)
}) => PrismaClientExtends<runtime.Types.Utils.PatchDeep<Args, ExtArgs>>)
}

export declare const dmmf: any
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,7 @@ export namespace Prisma {
db?: Datasource
}

export type DefaultPrismaClient = PrismaClient
export type RejectOnNotFound = boolean | ((error: Error) => Error)
export type RejectPerModel = { [P in ModelName]?: RejectOnNotFound }
export type RejectPerOperation = { [P in "findUnique" | "findFirst"]?: RejectPerModel | RejectOnNotFound }
Expand Down Expand Up @@ -1487,7 +1488,7 @@ export namespace Prisma {
/**
* \`PrismaClient\` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>
export type TransactionClient = Omit<Prisma.DefaultPrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>

export type Datasource = {
url?: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1346,6 +1346,7 @@ export namespace Prisma {
db?: Datasource
}

export type DefaultPrismaClient = PrismaClient
export type RejectOnNotFound = boolean | ((error: Error) => Error)
export type RejectPerModel = { [P in ModelName]?: RejectOnNotFound }
export type RejectPerOperation = { [P in "findUnique" | "findFirst"]?: RejectPerModel | RejectOnNotFound }
Expand Down Expand Up @@ -1489,7 +1490,7 @@ export namespace Prisma {
/**
* \`PrismaClient\` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>
export type TransactionClient = Omit<Prisma.DefaultPrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>

export type Datasource = {
url?: string
Expand Down
3 changes: 1 addition & 2 deletions packages/client/src/generation/TSClient/Model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,6 @@ export type ${getSelectName(model.name)}${ifExtensions(
)} = ${ifExtensions(() => `runtime.Types.Extensions.GetResultSelect<`, '')}{
${indent(
outputType.fields
.filter((field) => ifExtensions(field.outputType.location === 'outputObjectTypes', true))
.map((f) => {
const fieldTypeName = (f.outputType.type as DMMF.OutputType).name
return (
Expand All @@ -335,7 +334,7 @@ ${indent(
.join('\n'),
TAB_SIZE,
)}
}${ifExtensions(() => ` & ${getSelectName(model.name)}Scalar, ExtArgs['result']['${lowerCase(model.name)}']>`, '')}
}${ifExtensions(() => `, ExtArgs['result']['${lowerCase(model.name)}']>`, '')}
${ifExtensions(() => {
return `
export type ${getSelectName(model.name)}Scalar = {
Expand Down
88 changes: 53 additions & 35 deletions packages/client/src/generation/TSClient/PrismaClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Datasources } from './Datasources'
import type { Generatable } from './Generatable'
import { getModelActions } from './utils/getModelActions'
import { ifExtensions } from './utils/ifExtensions'
import { Patch, Patch3 } from './utils/Patch'

function clientExtensionsResultDefinition(this: PrismaClientClass) {
const modelNames = Object.keys(this.dmmf.getModelMap())
Expand Down Expand Up @@ -72,45 +73,46 @@ function clientExtensionsModelDefinition(this: PrismaClientClass) {
function clientExtensionsQueryDefinition(this: PrismaClientClass) {
const modelNames = Object.keys(this.dmmf.getModelMap())

const prismaNamespaceDefinitions = `export type TypeMap<ExtArgs extends runtime.Types.Extensions.Args = runtime.Types.Extensions.DefaultArgs> = {${modelNames.reduce(
(acc, modelName) => {
const prismaNamespaceDefinitions = `export type TypeMap<ExtArgs extends runtime.Types.Extensions.Args = runtime.Types.Extensions.DefaultArgs> = {
model: {${modelNames.reduce((acc, modelName) => {
const actions = getModelActions(this.dmmf, modelName)

return `${acc}
${modelName}: {${actions.reduce((acc, action) => {
${modelName}: {${actions.reduce((acc, action) => {
return `${acc}
${action}: {
args: Prisma.${getModelArgName(modelName, action)}<ExtArgs>,
result: runtime.Types.Utils.OptionalFlat<${modelName}>
}`
${action}: {
args: Prisma.${getModelArgName(modelName, action)}<ExtArgs>,
result: runtime.Types.Utils.OptionalFlat<${modelName}>
}`
}, '')}
}`
},
'',
)}
}`
}, '')}
}
}`

const queryCbDefinition = (modelName: string, operationName: string) => {
const queryArgs = `runtime.Types.Extensions.ReadonlySelector<Prisma.TypeMap<ExtArgs>[${modelName}][${operationName}]['args']>`
const queryResult = `Prisma.TypeMap<ExtArgs>[${modelName}][${operationName}]['result']`
const inputQuery = `{ model: ${modelName}, operation: ${operationName}, args: ${queryArgs} }`
const inputQueryCb = `${inputQuery} & { query: (args: ${queryArgs}) => PrismaPromise<${queryResult}> }`
const queryArgs = `runtime.Types.Extensions.ReadonlySelector<Prisma.TypeMap<ExtArgs>['model'][${modelName}][${operationName}]['args']>`
const queryResult = `Prisma.TypeMap<ExtArgs>['model'][${modelName}][${operationName}]['result']`
const inputQueryBase = `model: ${modelName}, operation: ${operationName}, args: ${queryArgs}`
const inputQueryCbBase = `query: (args: ${queryArgs}) => PrismaPromise<${queryResult}>`
const inputQuery = `{ ${inputQueryBase}, ${inputQueryCbBase} }`

return `(args: ${inputQueryCb}) => Promise<${queryResult}>`
return `(args: ${inputQuery}) => Promise<${queryResult}>`
}

const allOperationsParam = (modelNames: string[], indent: string) => {
const key = modelNames.length > 1 ? `keyof Prisma.TypeMap<ExtArgs>` : `'${modelNames[0]}'`
const modelName = modelNames.map((mn) => `'${mn}'`).join(' | ')

return `{
${indent}$allOperations?: ${queryCbDefinition(key, `keyof Prisma.TypeMap<ExtArgs>[${key}]`)}
${indent}$allOperations?: ${queryCbDefinition(modelName, `keyof Prisma.TypeMap['model'][${modelName}]`)}
${indent}}`
}

const modelParam = (propName: string, modelNames: string[]) => {
const key = modelNames.length > 1 ? `keyof Prisma.TypeMap<ExtArgs>` : `'${modelNames[0]}'`
const key = modelNames.map((mn) => `'${mn}'`).join(' | ')

return `${propName}?: {
[K in keyof Prisma.TypeMap<ExtArgs>[${key}]]?: ${queryCbDefinition(key, `K`)}
[K in keyof Prisma.TypeMap['model'][${key}]]?: ${queryCbDefinition(key, `K`)}
} & ${allOperationsParam(modelNames, ' ')}`
}

Expand Down Expand Up @@ -145,33 +147,48 @@ function clientExtensionsHookDefinition(this: PrismaClientClass, name: '$extends
const query = clientExtensionsQueryDefinition.call(this)
const lcModelNames = Object.keys(this.dmmf.getModelMap()).map(lowerCase)
const modelNameUnion = lcModelNames.map((m) => `'${m}'`).join(' | ')
const genericParams = [result.genericParams, model.genericParams, query.genericParams, client.genericParams]
const genericVars = genericParams.map((gp) => gp.replace(/ extends .*/g, ','))

return {
signature: `${name === 'defineExtension' ? name : `${name}: { extArgs: ExtArgs } & (`}<
${result.genericParams},
${model.genericParams},
${query.genericParams},
${client.genericParams},
${genericParams.join(',\n ')},
Args extends runtime.Types.Extensions.Args = { result: R, model: M, query: Q, client: C },${
name === 'defineExtension'
? `
ExtArgs extends runtime.Types.Extensions.Args = runtime.Types.Extensions.DefaultArgs,`
: ''
}
>(extension: ((client: ${
name === 'defineExtension' ? 'PrismaClient' : 'this'
}) => { $extends: { extArgs: Args } }) | {
name?: string,
result?: R & ${result.params}
model?: M & ${model.params}
query?: ${query.params}
client?: C & ${client.params}
})${name === 'defineExtension' ? ':' : '=>'} ${
name === 'defineExtension' ? 'Prisma.DefaultPrismaClient' : 'this'
}) => { $extends: { extArgs: Args } }) | Prisma.ExtensionArgs<
ExtArgs,
${genericVars.join('\n ').slice(0, -1)}
>) ${name === 'defineExtension' ? ':' : '=>'} ${
name === 'defineExtension'
? '(client: any) => PrismaClient<any, any, any, Args>'
: `runtime.Types.Extensions.GetClient<PrismaClient<T, U, GlobalReject, runtime.Types.Extensions.MergeArgs<Args, ExtArgs, ${modelNameUnion}>>, Args['client'], ExtArgs['client']>`
: `runtime.Types.Extensions.GetClient<PrismaClient<T, U, GlobalReject, {
result: '$allModels' extends keyof Args['result']
? { [K in ${modelNameUnion}]: ${Patch3(`Args['result'][K]`, `Args['result']['$allModels']`, `ExtArgs['result'][K]`)} }
: { [K in (keyof Args['result'] | keyof ExtArgs['result'])]: ${Patch(`Args['result'][K]`, `ExtArgs['result'][K]`)} }
model: '$allModels' extends keyof Args['model']
? { [K in ${modelNameUnion}]: ${Patch3(`Args['model'][K]`, `Args['model']['$allModels']`, `ExtArgs['model'][K]`)} }
: { [K in (keyof Args['model'] | keyof ExtArgs['model'])]: ${Patch(`Args['model'][K]`, `ExtArgs['model'][K]`)} }
client: ${Patch(`Args['client']`, `ExtArgs['client']`)}
query: {}
}>, Args['client'], ExtArgs['client']>`
}${name === 'defineExtension' ? '' : ')'};`,
prismaNamespaceDefinitions: `${query.prismaNamespaceDefinitions}`,
prismaNamespaceDefinitions: `${query.prismaNamespaceDefinitions}
export type ExtensionArgs<
ExtArgs extends runtime.Types.Extensions.Args,
${genericParams.join(',\n ')}
> = {
name?: string,
result?: R & ${result.params}
model?: M & ${model.params}
query?: ${query.params}
client?: C & ${client.params}
}`,
}
}

Expand Down Expand Up @@ -444,6 +461,7 @@ get ${methodName}(): ${ifExtensions(
public toTS(): string {
return `${new Datasources(this.internalDatasources).toTS()}
${this.clientExtensionsDefinitions.prismaNamespaceDefinitions}
export type DefaultPrismaClient = PrismaClient
export type RejectOnNotFound = boolean | ((error: Error) => Error)
export type RejectPerModel = { [P in ModelName]?: RejectOnNotFound }
export type RejectPerOperation = { [P in "findUnique" | "findFirst"]?: RejectPerModel | RejectOnNotFound }
Expand Down Expand Up @@ -587,7 +605,7 @@ export function getLogLevel(log: Array<LogLevel | LogDefinition>): LogLevel | un
/**
* \`PrismaClient\` proxy available in interactive transactions.
*/
export type TransactionClient = Omit<PrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>
export type TransactionClient = Omit<Prisma.DefaultPrismaClient, '$connect' | '$disconnect' | '$on' | '$transaction' | '$use'>
`
}
}
4 changes: 4 additions & 0 deletions packages/client/src/generation/TSClient/utils/Patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ import { Omit } from './Omit'
export function Patch(O1: string, O2: string, KName = 'P') {
return `(${O1}) & ${Omit(O2, `keyof (${O1})`, KName)}`
}

export function Patch3(O1: string, O2: string, O3: string, KName = 'P') {
return `(${O1}) & ${Omit(O2, `keyof (${O1})`, KName)} & ${Omit(O3, `keyof (${O1}) | keyof (${O2})`, KName)}`
}
34 changes: 8 additions & 26 deletions packages/client/src/runtime/core/types/Extensions.ts
Original file line number Diff line number Diff line change
@@ -1,43 +1,25 @@
/* eslint-disable prettier/prettier */
import { RequiredArgs as Args } from '../extensions/$extends'
import { Omit, PatchFlat3,Pick, } from './Utils'
import { Omit, ReadonlyDeep } from './Utils'

export type DefaultArgs = { result: {}; model: {}; query: {}; client: {} }

export type GetResultPayload<Base extends object, R extends Args['result'][string]> =
PatchFlat3<{}, { [K in keyof R]: ReturnType<R[K]['compute']> }, Base>
{} extends R ? Base : { [K in keyof R]: ReturnType<R[K]['compute']> } & { [K in Exclude<keyof Base, keyof R>]: Base[K] }

export type GetResultSelect<Base extends object, R extends Args['result'][string]> =
Base & { [K in keyof R]?: true }
R extends unknown ? Base & { [K in keyof R]?: boolean } : never

export type GetModel<Base extends object, M extends Args['model'][string]> =
PatchFlat3<M, Base, {}>
M & Omit<Base, keyof M>

export type GetClient<Base extends object, C extends Args['client'], CP extends Args['client']> =
PatchFlat3<C, CP, Omit<Base, '$use'>>
C & Omit<CP, keyof C> & Omit<Base, '$use' | keyof C | keyof CP>

export type ReadonlySelector<T> = {
readonly [K in keyof T as K extends 'include' | 'select' ? K : never]: ReadonlySelector<T[K]>
export type ReadonlySelector<T> = T extends unknown ? {
readonly [K in keyof T as K extends 'include' | 'select' ? K : never]: ReadonlyDeep<T[K]>
} & {
[K in keyof T as K extends 'include' | 'select' ? never : K]: T[K]
}

export type MergeArgs<
ExtArgs extends Args,
PrevExtArgs extends Args,
ModelNames extends string,
ApplyAllModels extends boolean = true,
> = {
result: '$allModels' extends keyof ExtArgs['result'] ? ApplyAllModels extends true
? { [K in ModelNames]: PatchFlat3<ExtArgs['result'][K], ExtArgs['result']['$allModels'], PrevExtArgs['result'][K]> }
: { [K in keyof ExtArgs['result'] | keyof PrevExtArgs['result']]: PatchFlat3<ExtArgs['result'][K], PrevExtArgs['result'][K], {}> }
: { [K in keyof ExtArgs['result'] | keyof PrevExtArgs['result']]: PatchFlat3<ExtArgs['result'][K], PrevExtArgs['result'][K], {}> }
model: '$allModels' extends keyof ExtArgs['model'] ? ApplyAllModels extends true
? { [K in ModelNames]: PatchFlat3<ExtArgs['model'][K], ExtArgs['model']['$allModels'], PrevExtArgs['model'][K]> }
: { [K in keyof ExtArgs['model'] | keyof PrevExtArgs['model']]: PatchFlat3<ExtArgs['model'][K], PrevExtArgs['model'][K], {}> }
: { [K in keyof ExtArgs['model'] | keyof PrevExtArgs['model']]: PatchFlat3<ExtArgs['model'][K], PrevExtArgs['model'][K], {}> }
client: Pick<PatchFlat3<ExtArgs['client'], PrevExtArgs['client'], {}>, `$${string}`>
query: {}
}
} : never

export type { Args }
4 changes: 4 additions & 0 deletions packages/client/src/runtime/core/types/Utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,7 @@ export type Compute<T> = T extends Function
export type OptionalFlat<T> = {
[K in keyof T]?: T[K]
}

export type ReadonlyDeep<T> = {
readonly [K in keyof T]: ReadonlyDeep<T[K]>
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,38 @@
import { foreignKeyForProvider, idForProvider } from '../../../_utils/idForProvider'
import testMatrix from '../_matrix'

function createModelToManyField(times: number) {
const models: string[] = []
for (let i = 0; i < times; i++) {
models.push(`myModel${i} MyModel${i}[]`)
}

return models.join('\n')
}

function createModel(times: number, provider: string) {
const models: string[] = []
for (let i = 0; i < times; i++) {
models.push(`model MyModel${i} {
id ${idForProvider(provider)}
${createModelField(10)}
user User @relation(fields: [userId], references: [id])
userId String
}`)
}

return models.join('\n')
}

function createModelField(times: number) {
const fields: string[] = []
for (let i = 0; i < times; i++) {
fields.push(`myField${i} String`)
}

return fields.join('\n')
}

export default testMatrix.setupSchema(({ provider }) => {
return /* Prisma */ `
generator client {
Expand All @@ -19,12 +51,16 @@ export default testMatrix.setupSchema(({ provider }) => {
firstName String
lastName String
posts Post[]
${createModelToManyField(20)}
}

model Post {
id ${idForProvider(provider)}
user User @relation(fields: [userId], references: [id])
userId ${foreignKeyForProvider(provider)}
}

// the following models aren't used in the tests, but are here to test the performance of the extension
${createModel(20, provider)}
`
})
11 changes: 9 additions & 2 deletions packages/client/tests/functional/extensions/enabled/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ testMatrix.setupTestSuite(
query: {
user: {
findFirst({ args, query, operation, model }) {
if (args.select != undefined) {
// @ts-expect-error
args.select.email = undefined
}
// @ts-expect-error
args.include = undefined
// @ts-expect-error
Expand Down Expand Up @@ -675,7 +679,8 @@ testMatrix.setupTestSuite(
expectTypeOf(args).not.toBeAny
expectTypeOf(query).toBeFunction()
expectTypeOf(operation).toEqualTypeOf<'findFirst'>()
expectTypeOf(model).toEqualTypeOf<'Post' | 'User'>()
type Model = typeof model & ('Post' | 'User')
expectTypeOf<Model>().toEqualTypeOf<'Post' | 'User'>()

fnModel({ args, operation, model })

Expand Down Expand Up @@ -735,7 +740,9 @@ testMatrix.setupTestSuite(
| 'groupBy'
| 'count'
>()
expectTypeOf(model).toEqualTypeOf<'Post' | 'User'>()

type Model = typeof model & ('Post' | 'User')
expectTypeOf<Model>().toEqualTypeOf<'Post' | 'User'>()

fnModel({ args, operation, model })

Expand Down