diff --git a/src/packages/cli/src/__tests__/__snapshots__/studio.test.ts.snap b/src/packages/cli/src/__tests__/__snapshots__/studio.test.ts.snap index 9ecafea476b9..dba454e68b39 100644 --- a/src/packages/cli/src/__tests__/__snapshots__/studio.test.ts.snap +++ b/src/packages/cli/src/__tests__/__snapshots__/studio.test.ts.snap @@ -36,7 +36,6 @@ Object { string: true, }, }, - clientMethod: with_all_field_types.create, dataPath: Array [], model: with_all_field_types, runInTransaction: false, @@ -89,7 +88,6 @@ Object { id: 2, }, }, - clientMethod: with_all_field_types.delete, dataPath: Array [], model: with_all_field_types, runInTransaction: false, @@ -133,7 +131,6 @@ Object { string: true, }, }, - clientMethod: with_all_field_types.findMany, dataPath: Array [], model: with_all_field_types, runInTransaction: false, @@ -211,7 +208,6 @@ Object { id: 1, }, }, - clientMethod: with_all_field_types.update, dataPath: Array [], model: with_all_field_types, runInTransaction: false, diff --git a/src/packages/client/src/__tests__/batching.test.ts b/src/packages/client/src/__tests__/batching.test.ts index 7dcd4a23d93f..8a3a88e24104 100644 --- a/src/packages/client/src/__tests__/batching.test.ts +++ b/src/packages/client/src/__tests__/batching.test.ts @@ -1,4 +1,4 @@ -import { PrismaClientFetcher } from '../runtime/getPrismaClient' +import { PrismaClientFetcher } from '../runtime/PrismaClientFetcher' import { blog } from '../fixtures/blog' import { getDMMF } from '../generation/getDMMF' import { DMMFClass, makeDocument } from '../runtime' diff --git a/src/packages/client/src/__tests__/integration/happy/async-hooks/test.ts b/src/packages/client/src/__tests__/integration/happy/async-hooks/test.ts index bd8010ccfb38..4a98acfe6544 100644 --- a/src/packages/client/src/__tests__/integration/happy/async-hooks/test.ts +++ b/src/packages/client/src/__tests__/integration/happy/async-hooks/test.ts @@ -5,9 +5,9 @@ test('async-hooks', async () => { const PrismaClient = await getTestClient() const prisma = new PrismaClient() let asyncId - prisma.$use((params, fetch) => { + prisma.$use((params, next) => { asyncId = executionAsyncId() - return fetch(params) + return next(params) }) await prisma.user.findMany() diff --git a/src/packages/client/src/__tests__/integration/happy/middlewares-manipulation/test.ts b/src/packages/client/src/__tests__/integration/happy/middlewares-manipulation/test.ts index 17598c893f7f..57e99c0c0d31 100644 --- a/src/packages/client/src/__tests__/integration/happy/middlewares-manipulation/test.ts +++ b/src/packages/client/src/__tests__/integration/happy/middlewares-manipulation/test.ts @@ -6,7 +6,7 @@ test('middlewares-manipulation', async () => { let theParams let firstCall = true - prisma.$use(async (params, fetch) => { + prisma.$use(async (params, next) => { theParams = JSON.parse(JSON.stringify(params)) // clone if (firstCall) { params.args = { @@ -16,12 +16,12 @@ test('middlewares-manipulation', async () => { } firstCall = false } - const result = await fetch(params) + const result = await next(params) return result }) - prisma.$use(async (params, fetch) => { - const result = await fetch(params) + prisma.$use(async (params, next) => { + const result = await next(params) if (result.length > 0) { result[0].name += '2' // make sure that we can change the result } diff --git a/src/packages/client/src/__tests__/integration/happy/middlewares/dev.db b/src/packages/client/src/__tests__/integration/happy/middlewares/dev.db index c281cfd3121d..0aa8e85f3acb 100644 Binary files a/src/packages/client/src/__tests__/integration/happy/middlewares/dev.db and b/src/packages/client/src/__tests__/integration/happy/middlewares/dev.db differ diff --git a/src/packages/client/src/__tests__/integration/happy/middlewares/test.ts b/src/packages/client/src/__tests__/integration/happy/middlewares/test.ts index 60aaee825ede..738b8081350a 100644 --- a/src/packages/client/src/__tests__/integration/happy/middlewares/test.ts +++ b/src/packages/client/src/__tests__/integration/happy/middlewares/test.ts @@ -1,64 +1,121 @@ import { getTestClient } from '../../../../utils/getTestClient' -test('middlewares', async () => { - const PrismaClient = await getTestClient() - const db = new PrismaClient() +describe('middleware', () => { + test('basic', async () => { + const PrismaClient = await getTestClient() - const allResults: any[] = [] - const engineResults: any[] = [] + const db = new PrismaClient() - const order: number[] = [] + const allResults: any[] = [] - db.$use(async (params, fetch) => { - order.push(1) - const result = await fetch(params) - order.push(4) - return result - }) + db.$use(async (params, next) => { + const result = await next(params) + allResults.push(result) + return result + }) + + await db.user.findMany() + await db.post.findMany() + + expect(allResults).toEqual([[], []]) - db.$use(async (params, fetch) => { - order.push(2) - const result = await fetch(params) - order.push(3) - allResults.push(result) - return result + db.$disconnect() }) + test('order', async () => { + const PrismaClient = await getTestClient() + const db = new PrismaClient() + const order: number[] = [] + + db.$use(async (params, next) => { + order.push(1) + const result = await next(params) + order.push(4) + return result + }) + + db.$use(async (params, next) => { + order.push(2) + const result = await next(params) + order.push(3) + return result + }) - db.$use('engine', async (params, fetch) => { - const result = await fetch(params) - engineResults.push(result) - return result + await db.user.findMany() + await db.post.findMany() + + expect(order).toEqual([1, 2, 3, 4, 1, 2, 3, 4]) + + db.$disconnect() }) + test('engine middleware', async () => { + const PrismaClient = await getTestClient() + const db = new PrismaClient() - await db.user.findMany() - await db.post.findMany() + const engineResults: any[] = [] - expect(order).toEqual([1, 2, 3, 4, 1, 2, 3, 4]) - expect(allResults).toEqual([[], []]) - expect(engineResults.map((r) => r.data)).toEqual([ - { - data: { - findManyUser: [], + db.$use('engine', async (params, next) => { + const result = await next(params) + engineResults.push(result) + return result + }) + + await db.user.findMany() + await db.post.findMany() + expect(engineResults.map((r) => r.data)).toEqual([ + { + data: { + findManyUser: [], + }, }, - }, - { + { + data: { + findManyPost: [], + }, + }, + ]) + expect(typeof engineResults[0].elapsed).toEqual('number') + expect(typeof engineResults[1].elapsed).toEqual('number') + + db.$disconnect() + }) + test('modify params', async () => { + const PrismaClient = await getTestClient() + const db = new PrismaClient() + + const user = await db.user.create({ data: { - findManyPost: [], + email: 'test@test.com', + name: 'test', }, - }, - ]) - expect(typeof engineResults[0].elapsed).toEqual('number') - expect(typeof engineResults[1].elapsed).toEqual('number') + }) + db.$use(async (params, next) => { + if (params.action === 'findFirst' && params.model === 'User') { + params.args = { ...params.args, where: { name: 'test' } } + } + const result = await next(params) + return result + }) - db.$disconnect() -}) + const users = await db.user.findMany() + console.warn(users) + // The name should be overwritten by the middleware + const u = await db.user.findFirst({ + where: { + name: 'fake', + }, + }) + expect(u.id).toBe(user.id) + await db.user.deleteMany() -test('middlewares unpack', async () => { - const PrismaClient = await getTestClient() - const db = new PrismaClient() - db.$use((params, next) => next(params)) - const result = await db.user.count() - expect(typeof result).toBe('number') + db.$disconnect() + }) + test('count unpack', async () => { + const PrismaClient = await getTestClient() + const db = new PrismaClient() + db.$use((params, next) => next(params)) + const result = await db.user.count() + expect(typeof result).toBe('number') - db.$disconnect() + db.$disconnect() + }) }) diff --git a/src/packages/client/src/generation/TSClient/helpers.ts b/src/packages/client/src/generation/TSClient/helpers.ts index fb7544c026b1..2eb4fd033e50 100644 --- a/src/packages/client/src/generation/TSClient/helpers.ts +++ b/src/packages/client/src/generation/TSClient/helpers.ts @@ -87,6 +87,8 @@ export function getArgFieldJSDoc( const comment = JSDocs[action]?.fields[fieldName](singular, plural) return comment as string } + + return undefined } export function escapeJson(str: string): string { diff --git a/src/packages/client/src/runtime/MiddlewareHandler.ts b/src/packages/client/src/runtime/MiddlewareHandler.ts new file mode 100644 index 000000000000..8424a89cc2f8 --- /dev/null +++ b/src/packages/client/src/runtime/MiddlewareHandler.ts @@ -0,0 +1,59 @@ +import { Action } from './getPrismaClient' +import { Document } from './query' + +export type QueryMiddleware = ( + params: QueryMiddlewareParams, + next: (params: QueryMiddlewareParams) => Promise, +) => Promise + +export type QueryMiddlewareParams = { + /** The model this is executed on */ + model?: string + /** The action that is being handled */ + action: Action + /** TODO what is this */ + dataPath: string[] + /** TODO what is this */ + runInTransaction: boolean + /** TODO what is this */ + args: any // TODO remove any, does this make sense, what is args? +} + +export type EngineMiddleware = ( + params: EngineMiddlewareParams, + next: ( + params: EngineMiddlewareParams, + ) => Promise<{ data: T; elapsed: number }>, +) => Promise<{ data: T; elapsed: number }> + +export type EngineMiddlewareParams = { + document: Document + runInTransaction?: boolean +} + +export type Namespace = 'all' | 'engine' + +class MiddlewareHandler { + private _middlewares: M[] = [] + + use(middleware: M) { + this._middlewares.push(middleware) + } + + get(id: number): M | undefined { + return this._middlewares[id] + } + + has(id: number) { + return !!this._middlewares[id] + } + + length() { + return this._middlewares.length + } +} + +export class Middlewares { + query = new MiddlewareHandler() + engine = new MiddlewareHandler() +} diff --git a/src/packages/client/src/runtime/PrismaClientFetcher.ts b/src/packages/client/src/runtime/PrismaClientFetcher.ts new file mode 100644 index 000000000000..c1058c4b3069 --- /dev/null +++ b/src/packages/client/src/runtime/PrismaClientFetcher.ts @@ -0,0 +1,220 @@ +import stripAnsi from 'strip-ansi' +import { + PrismaClientInitializationError, + PrismaClientKnownRequestError, + PrismaClientRustPanicError, + PrismaClientUnknownRequestError, +} from '.' +import { Dataloader } from './Dataloader' +import { debug, RequestParams, Unpacker } from './getPrismaClient' +import { Args, Document, unpack } from './query' +import { printStack } from './utils/printStack' +import { throwIfNotFound } from './utils/rejectOnNotFound' + +export class PrismaClientFetcher { + prisma: any + debug: boolean + hooks: any + dataloader: Dataloader<{ + document: Document + runInTransaction?: boolean + transactionId?: number + headers?: Record + }> + + constructor(prisma, enableDebug = false, hooks?: any) { + this.prisma = prisma + this.debug = enableDebug + this.hooks = hooks + this.dataloader = new Dataloader({ + batchLoader: (requests) => { + const queries = requests.map((r) => String(r.document)) + const runTransaction = requests[0].runInTransaction + return this.prisma._engine.requestBatch(queries, runTransaction) + }, + singleLoader: (request) => { + const query = String(request.document) + return this.prisma._engine.request(query, request.headers) + }, + batchBy: (request) => { + if (request.runInTransaction) { + if (request.transactionId) { + return `transaction-batch-${request.transactionId}` + } + return 'transaction-batch' + } + + if (!request.document.children[0].name.startsWith('findUnique')) { + return null + } + + const selectionSet = request.document.children[0].children!.join(',') + + const args = request.document.children[0].args?.args + .map((a) => { + if (a.value instanceof Args) { + return `${a.key}-${a.value.args.map((a) => a.key).join(',')}` + } + return a.key + }) + .join(',') + + return `${request.document.children[0].name}|${args}|${selectionSet}` + }, + }) + } + get [Symbol.toStringTag]() { + return 'PrismaClientFetcher' + } + + async request({ + document, + dataPath = [], + rootField, + typeName, + isList, + callsite, + rejectOnNotFound, + clientMethod, + runInTransaction, + showColors, + engineHook, + args, + headers, + transactionId, + unpacker, + }: RequestParams) { + const cb = async () => { + if (this.hooks && this.hooks.beforeRequest) { + const query = String(document) + this.hooks.beforeRequest({ + query, + path: dataPath, + rootField, + typeName, + document, + isList, + clientMethod, + args, + }) + } + try { + /** + * If there's an engine hook, use it here + */ + let data, elapsed + if (engineHook) { + const result = await engineHook( + { + document, + runInTransaction, + }, + (params) => this.dataloader.request(params), + ) + data = result.data + elapsed = result.elapsed + } else { + const result = await this.dataloader.request({ + document, + runInTransaction, + headers, + transactionId, + }) + data = result?.data + elapsed = result?.elapsed + } + + /** + * Unpack + */ + const unpackResult = this.unpack( + document, + data, + dataPath, + rootField, + unpacker, + ) + throwIfNotFound(unpackResult, clientMethod, typeName, rejectOnNotFound) + if (process.env.PRISMA_CLIENT_GET_TIME) { + return { data: unpackResult, elapsed } + } + return unpackResult + } catch (e) { + debug(e) + let message = e.message + if (callsite) { + const { stack } = printStack({ + callsite, + originalMethod: clientMethod, + onUs: e.isPanic, + showColors, + }) + message = `${stack}\n ${e.message}` + } + + message = this.sanitizeMessage(message) + // TODO: Do request with callsite instead, so we don't need to rethrow + if (e.code) { + throw new PrismaClientKnownRequestError( + message, + e.code, + this.prisma._clientVersion, + e.meta, + ) + } else if (e.isPanic) { + throw new PrismaClientRustPanicError( + message, + this.prisma._clientVersion, + ) + } else if (e instanceof PrismaClientUnknownRequestError) { + throw new PrismaClientUnknownRequestError( + message, + this.prisma._clientVersion, + ) + } else if (e instanceof PrismaClientInitializationError) { + throw new PrismaClientInitializationError( + message, + this.prisma._clientVersion, + ) + } else if (e instanceof PrismaClientRustPanicError) { + throw new PrismaClientRustPanicError( + message, + this.prisma._clientVersion, + ) + } + + e.clientVersion = this.prisma._clientVersion + + throw e + } + } + if (transactionId) { + return cb + } else { + return cb() + } + } + + sanitizeMessage(message) { + if (this.prisma._errorFormat && this.prisma._errorFormat !== 'pretty') { + return stripAnsi(message) + } + return message + } + unpack(document, data, path, rootField, unpacker?: Unpacker) { + if (data?.data) { + data = data.data + } + // to lift up _all in count + if (unpacker) { + data[rootField] = unpacker(data[rootField]) + } + + const getPath: any[] = [] + if (rootField) { + getPath.push(rootField) + } + getPath.push(...path.filter((p) => p !== 'select' && p !== 'include')) + return unpack({ document, data, path: getPath }) + } +} diff --git a/src/packages/client/src/runtime/getPrismaClient.ts b/src/packages/client/src/runtime/getPrismaClient.ts index e0c679378471..2053511c07bf 100644 --- a/src/packages/client/src/runtime/getPrismaClient.ts +++ b/src/packages/client/src/runtime/getPrismaClient.ts @@ -18,40 +18,32 @@ import { AsyncResource } from 'async_hooks' import fs from 'fs' import path from 'path' import * as sqlTemplateTag from 'sql-template-tag' -import stripAnsi from 'strip-ansi' -import { - PrismaClientInitializationError, - PrismaClientKnownRequestError, - PrismaClientRustPanicError, - PrismaClientUnknownRequestError, -} from '.' -import { Dataloader } from './Dataloader' import { DMMFClass } from './dmmf' import { DMMF } from './dmmf-types' import { getLogLevel } from './getLogLevel' import { mergeBy } from './mergeBy' import { - Args, - Document, - makeDocument, - transformDocument, - unpack, -} from './query' + EngineMiddleware, + Middlewares, + Namespace, + QueryMiddleware, + QueryMiddlewareParams, +} from './MiddlewareHandler' +import { PrismaClientFetcher } from './PrismaClientFetcher' +import { Document, makeDocument, transformDocument } from './query' import { clientVersion } from './utils/clientVersion' import { getOutputTypeName, lowerCase } from './utils/common' import { deepSet } from './utils/deep-set' import { mssqlPreparedStatement } from './utils/mssqlPreparedStatement' import { printJsonWithErrors } from './utils/printJsonErrors' -import { printStack } from './utils/printStack' import { getRejectOnNotFound, InstanceRejectOnNotFound, RejectOnNotFound, - throwIfNotFound, } from './utils/rejectOnNotFound' import { serializeRawParameters } from './utils/serializeRawParameters' import { validatePrismaClientOptions } from './utils/validatePrismaClientOptions' -const debug = Debug('prisma:client') +export const debug = Debug('prisma:client') const ALTER_RE = /^(\s*alter\s)/i function isReadonlyArray(arg: any): arg is ReadonlyArray { @@ -134,7 +126,7 @@ export interface PrismaClientOptions { } } -type Unpacker = (data: any) => any +export type Unpacker = (data: any) => any export type HookParams = { query: string @@ -146,59 +138,42 @@ export type HookParams = { args: any } -/** - * These options are being passed in to the middleware as "params" - */ -export type MiddlewareParams = { - model?: string - action: Action - args: any - dataPath: string[] - runInTransaction: boolean -} - -/** - * The `T` type makes sure, that the `return proceed` is not forgotten in the middleware implementation - */ -export type Middleware = ( - params: MiddlewareParams, - next: (params: MiddlewareParams) => Promise, -) => Promise +export type Action = + | 'findUnique' + | 'findFirst' + | 'findMany' + | 'create' + | 'createMany' + | 'update' + | 'updateMany' + | 'upsert' + | 'delete' + | 'deleteMany' + | 'executeRaw' + | 'queryRaw' + | 'aggregate' -export interface InternalRequestParams extends MiddlewareParams { +export type InternalRequestParams = { /** * The original client method being called. * Even though the rootField / operation can be changed, * this method stays as it is, as it's what the user's * code looks like */ - clientMethod: string - callsite?: string - headers?: Record - transactionId?: number - unpacker?: Unpacker -} - -export type HookPoint = 'all' | 'engine' - -export type EngineMiddlewareParams = { - document: Document - runInTransaction?: boolean -} + clientMethod: string // TODO what is this + callsite?: string // TODO what is this + headers?: Record // TODO what is this + transactionId?: number // TODO what is this + unpacker?: Unpacker // TODO what is this +} & QueryMiddlewareParams // only used by the .use() hooks export type AllHookArgs = { params: HookParams fetch: (params: HookParams) => Promise } -/** - * The `T` type makes sure, that the `return proceed` is not forgotten in the middleware implementation - */ -export type EngineMiddleware = ( - params: EngineMiddlewareParams, - next: (params: EngineMiddlewareParams) => Promise, -) => Promise +// TODO: drop hooks 💣 export type Hooks = { beforeRequest?: (options: HookParams) => any } @@ -252,21 +227,6 @@ export interface GetPrismaClientOptions { activeProvider: string } -export type Action = - | 'findUnique' - | 'findFirst' - | 'findMany' - | 'create' - | 'createMany' - | 'update' - | 'updateMany' - | 'upsert' - | 'delete' - | 'deleteMany' - | 'executeRaw' - | 'queryRaw' - | 'aggregate' - const actionOperationMap = { findUnique: 'query', findFirst: 'query', @@ -311,13 +271,12 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { _disconnectionPromise?: Promise _engineConfig: EngineConfig private _errorFormat: ErrorFormat - private _hooks?: Hooks + private _hooks?: Hooks // private _getConfigPromise?: Promise<{ datasources: DataSource[] generators: GeneratorConfig[] }> - private _middlewares: Middleware[] = [] - private _engineMiddlewares: EngineMiddleware[] = [] + private _middlewares: Middlewares = new Middlewares() private _clientVersion: string private _previewFeatures: string[] private _activeProvider: string @@ -471,25 +430,27 @@ export function getPrismaClient(config: GetPrismaClientOptions): any { return new NodeEngine(this._engineConfig) } } - $use(cb: Middleware) - $use(namespace: 'all', cb: Middleware) + + /** + * Hook a middleware into the client + * @param middleware to hook + */ + $use(middleware: QueryMiddleware) + $use(namespace: 'all', cb: QueryMiddleware) $use(namespace: 'engine', cb: EngineMiddleware) $use( - namespace: HookPoint | Middleware, - cb?: Middleware | EngineMiddleware, + arg0: Namespace | QueryMiddleware, + arg1?: QueryMiddleware | EngineMiddleware, ) { - if (typeof namespace === 'function') { - this._middlewares.push(namespace) - } else if (typeof namespace === 'string') { - if (namespace === 'all') { - this._middlewares.push(cb! as Middleware) - } else if (namespace === 'engine') { - this._engineMiddlewares.push(cb! as EngineMiddleware) - } else { - throw new Error(`Unknown middleware hook ${namespace}`) - } + // TODO use a mixin and move this into MiddlewareHandler + if (typeof arg0 === 'function') { + this._middlewares.query.use(arg0) + } else if (arg0 === 'all') { + this._middlewares.query.use(arg1 as QueryMiddleware) + } else if (arg0 === 'engine') { + this._middlewares.engine.use(arg1 as EngineMiddleware) } else { - throw new Error(`Invalid middleware ${namespace}`) + throw new Error(`Invalid middleware ${arg0}`) } } @@ -948,69 +909,56 @@ new PrismaClient({ } } - private _request(internalParams: InternalRequestParams) { + /** + * Runs the middlewares over params before executing a request + * @param internalParams + * @param middlewareIndex + * @returns + */ + private _request( + internalParams: InternalRequestParams, + middlewareIndex = 0, + ): Promise { try { + // in this recursion, we check for our terminating condition + const middleware = this._middlewares.query.get(middlewareIndex) + // async scope https://github.com/prisma/prisma/issues/3148 const resource = new AsyncResource('prisma-client-request') - if (this._middlewares.length > 0) { - // https://perf.link/#eyJpZCI6Img4bmd0anp5eGxrIiwidGl0bGUiOiJGaW5kaW5nIG51bWJlcnMgaW4gYW4gYXJyYXkgb2YgMTAwMCIsImJlZm9yZSI6ImNvbnN0IGRhdGEgPSB7XG4gIG9wZXJhdGlvbjogXCJxdWVyeVwiLFxuICByb290RmllbGQ6IFwiZmluZE1hbnlVc2VyXCIsXG4gIGFyZ3M6IHtcbiAgICB3aGVyZTogeyBpZDogeyBndDogNSB9IH1cbiAgfSxcbiAgZGF0YVBhdGg6IFtdLFxuICBjbGllbnRNZXRob2Q6ICd1c2VyLmZpbmRNYW55J1xufSIsInRlc3RzIjpbeyJuYW1lIjoiZm9yIGluIiwiY29kZSI6ImNvbnN0IG5ld0RhdGEgPSB7fVxuZm9yIChjb25zdCBrZXkgaW4gZGF0YSkge1xuICBpZiAoa2V5ICE9PSAnY2xpZW50TWV0aG9kJykge1xuICAgIG5ld0RhdGFba2V5XSA9IGRhdGFba2V5XVxuICB9XG59IiwicnVucyI6WzU1MzAwMCw0OTAwMDAsMzQ0MDAwLDYyNDAwMCwxMzkxMDAwLDEyMjQwMDAsMTA2NDAwMCwxMjE3MDAwLDc0MDAwLDM3MzAwMCw5MDUwMDAsNTM3MDAwLDE3MDYwMDAsOTAzMDAwLDE0MjUwMDAsMTMxMjAwMCw3NjkwMDAsMTM0NTAwMCwxOTQ4MDAwLDk5MDAwMCw5MDAwMDAsMTM0ODAwMCwxMDk2MDAwLDM4NjAwMCwxNTE3MDAwLDE5MzYwMDAsMTAwMCwyMTM0MDAwLDEzMjgwMDAsODI5MDAwLDE1ODYwMDAsMTc2MzAwMCw1MDgwMDAsOTg2MDAwLDE5NDkwMDAsMjEwODAwMCwxNjA4MDAwLDIyNDAwMCwxOTAyMDAwLDEyNjgwMDAsMjEzNDAwMCwxNzEwMDAwLDEzNzIwMDAsMjExMDAwMCwxNzgwMDAwLDc3NzAwMCw1NzgwMDAsNDAwMCw4OTAwMDAsMTEwMTAwMCwxNTk0MDAwLDE3ODAwMDAsMzU0MDAwLDU0NDAwMCw4MjQwMDAsNzEwMDAwLDg0OTAwMCwxNjQwMDAwLDE5ODQwMDAsNzAzMDAwLDg4MjAwMCw4NTAwMDAsMTA2MDAwLDMwMzAwMCwxMzMwMDAsNjA4MDAwLDIxMzQwMDAsNTUxMDAwLDc0MjAwMCwyMDcwMDAsMTU3NTAwMCwxMzQwMDAsNDAwMCwxMDAwLDQ5NDAwMCwyNTAwMDAsMTQwMjAwMCw2OTgwMDAsNTgxMDAwLDQ4MDAwMCwyMDMwMDAsMTY4MzAwMCwxNjcxMDAwLDEyNDAwMDAsMTk1NjAwMCwzMDUwMDAsODkwMDAsNjUzMDAwLDE3MDgwMDAsMTYwMTAwMCwxOTg0MDAwLDg4ODAwMCwyMTAwMDAwLDE5NzUwMDAsNTM2MDAwLDU3NTAwMCwyMTM0MDAwLDEwMTcwMDAsMTI5NzAwMCw3NTYwMDBdLCJvcHMiOjEwNDUxNTB9LHsibmFtZSI6IkRlY29uc3RydWN0b3IiLCJjb2RlIjoiY29uc3QgeyBjbGllbnRNZXRob2QsIC4uLnJlc3QgfSA9IGRhdGEiLCJydW5zIjpbMjE0MDAwLDUxMDAwLDg2NDAwMCw3MjcwMDAsNDMxMDAwLDIyMDAwMCwzOTAwMDAsODQxMDAwLDIyOTAwMCw3MjIwMDAsNDEzMDAwLDYwODAwMCwyOTgwMDAsMzY4MDAwLDg2NDAwMCw5MjQwMDAsMTI4MDAwLDU1MzAwMCw4ODAwMDAsNTQ1MDAwLDc3NTAwMCw0MzAwMDAsMjM3MDAwLDc4NjAwMCw1NTUwMDAsNTI2MDAwLDMyNzAwMCw2MzAwMCw5MTIwMDAsMTgxMDAwLDMzMTAwMCw0MzAwMCwyMjUwMDAsNTQ3MDAwLDgyMjAwMCw3OTMwMDAsMTA1NzAwMCw1NjAwMCwyNzUwMDAsMzkzMDAwLDgwNTAwMCw5MzAwMCw3NjYwMDAsODM0MDAwLDUwMzAwMCw4MDAwMCwyMzgwMDAsNDY0MDAwLDU2NDAwMCw3MzAwMDAsOTU1MDAwLDgwOTAwMCwyMDMwMDAsNDEzMDAwLDM0NDAwMCw1MDIwMDAsNjEzMDAwLDEwMDAwMCw0MzIwMDAsNjcwMDAwLDQ1MzAwMCw4OTEwMDAsNTUwMDAsMjMwMDAwLDM5MTAwMCw3NTQwMDAsMTEyMjAwMCw3NjIwMDAsMzU3MDAwLDQ3MDAwLDc5MjAwMCwzNTQwMDAsMTA4MDAwMCwxNjAwMCwxODgwMDAsMTQxMDAwLDIxMDAwMCw2MDcwMDAsOTAyMDAwLDgyNTAwMCwxOTAwMDAsMjMzMDAwLDI4MzAwMCwyMzgwMDAsNjk2MDAwLDc2ODAwMCw3NTgwMDAsMTk0MDAwLDI3OTAwMCwyMjMwMDAsMjM4MDAwLDkzNDAwMCw2MDUwMDAsMTcwMDAsMjEwMDAwLDMyMjAwMCwxMDM0MDAwLDgxMjAwMCw0NDYwMDAsNjMxMDAwXSwib3BzIjo0OTAxMDB9LHsibmFtZSI6ImRlbGV0ZSIsImNvZGUiOiJjb25zdCB7IGNsaWVudE1ldGhvZCB9ID0gZGF0YVxuZGVsZXRlIGRhdGEuY2xpZW50TWV0aG9kIiwicnVucyI6WzI3NjIwMDAsNjIyMDAwLDEwNTcwMDAsMzIzMTAwMCwzNDQ2MDAwLDIwNzMwMDAsMzM4MjAwMCwyNzA0MDAwLDM4ODEwMDAsMTIwMTAwMCwzNzk3MDAwLDI1OTAwMCwxMDI4MDAwLDI1MTgwMDAsMjEwMjAwMCwxOTczMDAwLDM0MTIwMDAsMzU4MDAwLDExNDcwMDAsMTA3NDAwMCwzMTk1MDAwLDM2NzUwMDAsNTQ3MDAwLDIwNzkwMDAsMjc0NTAwMCwyNDE1MDAwLDIxOTAwMCwzNzM3MDAwLDM2OTIwMDAsMTY0MDAwLDI0MzMwMDAsNjQzMDAwLDcxODAwMCw0Mzg2MDAwLDE3MDIwMDAsMTAyNDAwMCw1NjUwMDAsNDIxOTAwMCwxMTk3MDAwLDE4MzkwMDAsMzgyMTAwMCwxMTUyMDAwLDg1MzAwMCwxMzczMDAwLDI5NTAwMCwxNDg5MDAwLDE0MjEwMDAsMjcyNDAwMCw1MDYxMDAwLDI2NTcwMDAsMjYzNzAwMCwyOTkwMDAsMjE1NzAwMCwxNTAxMDAwLDM2OTAwMDAsMzU3OTAwMCw0MjE5MDAwLDI4NTgwMDAsNTI0MzAwMCwxNTA0MDAwLDEyMTMwMDAsMjM4NDAwMCw3NzgwMDAsMjgyNjAwMCwxNzQ5MDAwLDM2MjAwMCwyNzEzMDAwLDMzODYwMDAsMzE2NjAwMCwxNTMwMDAsNzk0MDAwLDMyMTcwMDAsMjA4MjAwMCw0MTUwMDAsMzMyMDAwMCwyMTA1MDAwLDE1NzYwMDAsMjUxMDAwLDIzMjkwMDAsOTI1MDAwLDM3MTUwMDAsNjkyMDAwLDE5MDIwMDAsMjA0NzAwMCwyNTM5MDAwLDIwMjkwMDAsMzE3OTAwMCwyMTA2MDAwLDg5NTAwMCwxNTUwMDAwLDYwNzAwMCw0MTA1MDAwLDM0ODMwMDAsMzcxNTAwMCw0OTQwMDAwLDIyODAwMCw0MDI2MDAwLDE2MTYwMDAsMzMxNDAwMCwyNDIyMDAwXSwib3BzIjoyMTY2MDgwfSx7Im5hbWUiOiJDcmVhdGUgbmV3IG9iamVjdCIsImNvZGUiOiJjb25zdCBuZXdEYXRhID0ge1xuICBvcGVyYXRpb246IGRhdGEub3BlcmF0aW9uLFxuICByb290RmllbGQ6IGRhdGEucm9vdEZpZWxkLFxuICBhcmdzOiBkYXRhLmFyZ3MsXG4gIGRhdGFQYXRoOiBkYXRhLmRhdGFQYXRoXG59IiwicnVucyI6WzcwNTAwMCwxMTAwMDAsMzI3NTAwMCwxOTgwMDAsMjE5OTAwMCw0MzYwMDAsODI4MDAwLDI5MjcwMDAsNzI0MDAwLDI1NDAwMCwyOTgzMDAwLDI2NzIwMDAsMjUzMDAwLDI4MjcwMDAsMzA0ODAwMCwyOTA3MDAwLDM0OTkwMDAsMjY1OTAwMCwzODIyMDAwLDI3NzcwMDAsMzc5NzAwMCw4MDAwMDAsNDM1MDAwLDExOTMwMDAsMTAwMDAsMTQ0MDAwMCw3NTcwMDAsMTMyMDAwMCwzMjIwMDAsMjA3MDAwLDM2ODAwMDAsMzkxMTAwMCwzMjQxMDAwLDExMDcwMDAsNDM4MDAwLDMwNDQwMDAsMTA3NjAwMCwyMTAwMDAsNDIxOTAwMCwzNzQ4MDAwLDQwNjcwMDAsNzc0MDAwLDYzMDAwLDMyMTAwMCwzMDQ4MDAwLDMxMjgwMDAsMTg3MTAwMCwzNTkxMDAwLDI0MzcwMDAsNjcxMDAwLDc5OTAwMCwxMTUzMDAwLDIxMTMwMDAsOTUwMDAsNTg3MDAwLDYyMzAwMCwxMzEzMDAwLDMxNTgwMDAsMzMyNzAwMCwxNTkwMDAsNDg4MDAwLDIxMTAwMCwxMjk0MDAwLDExNTcwMDAsNDA0MDAwLDM2MjMwMDAsMjY4NDAwMCw4NzkwMDAsMjE4NTAwMCwxNTkyMDAwLDM2ODcwMDAsMjI0ODAwMCwyMjE4MDAwLDE3NDMwMDAsNzg4MDAwLDQwODYwMDAsMjExNTAwMCwzOTE0MDAwLDM5MjgwMDAsNDM3MjAwMCwxOTkwMDAsMzc1MzAwMCwzNjQ3MDAwLDE2MjcwMDAsMTQ5OTAwMCwxODQyMDAwLDIxMjkwMDAsNDAwMCwxMjIzMDAwLDI4NjMwMDAsMzgzNDAwMCwzNjk0MDAwLDYzNjAwMCw0MjQ3MDAwLDQwMjIwMDAsMTAwMDAsMTcxNDAwMCwxNzUwMDAwLDI5MDEwMDAsMTM0NjAwMF0sIm9wcyI6MTkzOTEyMH1dLCJ1cGRhdGVkIjoiMjAyMC0wNy0xNVQxMTowMDo1Ny45MzhaIn0%3D - const params: MiddlewareParams = { + + if (middleware) { + // make sure that we don't leak extra properties to users + const params: QueryMiddlewareParams = { args: internalParams.args, dataPath: internalParams.dataPath, runInTransaction: internalParams.runInTransaction, action: internalParams.action, model: internalParams.model, } - return resource.runInAsyncScope(() => - this._requestWithMiddlewares( - params, - this._middlewares.slice(), - internalParams.clientMethod, - internalParams.callsite, - internalParams.headers, - internalParams.unpacker, - ), - ) + + return resource.runInAsyncScope(() => { + // call the middleware of the user & get their changes + return middleware(params, (changedParams) => { + // this middleware returns the value of the next one 🐛 + return this._request( + { + ...internalParams, + ...changedParams, + }, + ++middlewareIndex, + ) // recursion happens over here + }) + }) } - return resource.runInAsyncScope(() => - this._executeRequest(internalParams), - ) + // they're finished, or there's none, then execute request + return resource.runInAsyncScope(() => { + return this._executeRequest(internalParams) + }) } catch (e) { e.clientVersion = this._clientVersion - throw e - } - } - private _requestWithMiddlewares( - params: MiddlewareParams, - middlewares: Middleware[], - clientMethod: string, - callsite?: string, - headers?: Record, - unpacker?: Unpacker, - ) { - const middleware = middlewares.shift() - if (middleware) { - return middleware(params, (params2) => - this._requestWithMiddlewares( - params2, - middlewares, - clientMethod, - callsite, - headers, - unpacker, - ), - ) + throw e } - - // No, we won't copy the whole object here just to make it easier to do TypeScript - // as it would be much slower - ;(params as InternalRequestParams).clientMethod = clientMethod - ;(params as InternalRequestParams).callsite = callsite - ;(params as InternalRequestParams).headers = headers - ;(params as InternalRequestParams).unpacker = unpacker - - return this._executeRequest(params as InternalRequestParams) } private _executeRequest({ @@ -1112,7 +1060,7 @@ new PrismaClient({ callsite, showColors: this._errorFormat === 'pretty', args, - engineHook: this._engineMiddlewares[0], + engineHook: this._middlewares.engine.get(0), runInTransaction, headers, transactionId, @@ -1427,227 +1375,22 @@ new PrismaClient({ return NewPrismaClient } -export class PrismaClientFetcher { - prisma: any - debug: boolean - hooks: any - dataloader: Dataloader<{ - document: Document - runInTransaction?: boolean - transactionId?: number - headers?: Record - }> - - constructor(prisma, enableDebug = false, hooks?: any) { - this.prisma = prisma - this.debug = enableDebug - this.hooks = hooks - this.dataloader = new Dataloader({ - batchLoader: (requests) => { - const queries = requests.map((r) => String(r.document)) - const runTransaction = requests[0].runInTransaction - return this.prisma._engine.requestBatch(queries, runTransaction) - }, - singleLoader: (request) => { - const query = String(request.document) - return this.prisma._engine.request(query, request.headers) - }, - batchBy: (request) => { - if (request.runInTransaction) { - if (request.transactionId) { - return `transaction-batch-${request.transactionId}` - } - return 'transaction-batch' - } - - if (!request.document.children[0].name.startsWith('findUnique')) { - return null - } - - const selectionSet = request.document.children[0].children!.join(',') - - const args = request.document.children[0].args?.args - .map((a) => { - if (a.value instanceof Args) { - return `${a.key}-${a.value.args.map((a) => a.key).join(',')}` - } - return a.key - }) - .join(',') - - return `${request.document.children[0].name}|${args}|${selectionSet}` - }, - }) - } - get [Symbol.toStringTag]() { - return 'PrismaClientFetcher' - } - async request({ - document, - dataPath = [], - rootField, - typeName, - isList, - callsite, - rejectOnNotFound, - clientMethod, - runInTransaction, - showColors, - engineHook, - args, - headers, - transactionId, - unpacker, - }: { - document: Document - dataPath: string[] - rootField: string - typeName: string - isList: boolean - clientMethod: string - callsite?: string - rejectOnNotFound?: RejectOnNotFound - runInTransaction?: boolean - showColors?: boolean - engineHook?: EngineMiddleware - args: any - headers?: Record - transactionId?: number - unpacker?: Unpacker - }) { - const cb = async () => { - if (this.hooks && this.hooks.beforeRequest) { - const query = String(document) - this.hooks.beforeRequest({ - query, - path: dataPath, - rootField, - typeName, - document, - isList, - clientMethod, - args, - }) - } - try { - /** - * If there's an engine hook, use it here - */ - let data, elapsed - if (engineHook) { - const result = await engineHook( - { - document, - runInTransaction, - }, - (params) => this.dataloader.request(params), - ) - data = result.data - elapsed = result.elapsed - } else { - const result = await this.dataloader.request({ - document, - runInTransaction, - headers, - transactionId, - }) - data = result?.data - elapsed = result?.elapsed - } - - /** - * Unpack - */ - const unpackResult = this.unpack( - document, - data, - dataPath, - rootField, - unpacker, - ) - throwIfNotFound(unpackResult, clientMethod, typeName, rejectOnNotFound) - if (process.env.PRISMA_CLIENT_GET_TIME) { - return { data: unpackResult, elapsed } - } - return unpackResult - } catch (e) { - debug(e) - let message = e.message - if (callsite) { - const { stack } = printStack({ - callsite, - originalMethod: clientMethod, - onUs: e.isPanic, - showColors, - }) - message = `${stack}\n ${e.message}` - } - - message = this.sanitizeMessage(message) - // TODO: Do request with callsite instead, so we don't need to rethrow - if (e.code) { - throw new PrismaClientKnownRequestError( - message, - e.code, - this.prisma._clientVersion, - e.meta, - ) - } else if (e.isPanic) { - throw new PrismaClientRustPanicError( - message, - this.prisma._clientVersion, - ) - } else if (e instanceof PrismaClientUnknownRequestError) { - throw new PrismaClientUnknownRequestError( - message, - this.prisma._clientVersion, - ) - } else if (e instanceof PrismaClientInitializationError) { - throw new PrismaClientInitializationError( - message, - this.prisma._clientVersion, - ) - } else if (e instanceof PrismaClientRustPanicError) { - throw new PrismaClientRustPanicError( - message, - this.prisma._clientVersion, - ) - } - - e.clientVersion = this.prisma._clientVersion - - throw e - } - } - if (transactionId) { - return cb - } else { - return cb() - } - } - - sanitizeMessage(message) { - if (this.prisma._errorFormat && this.prisma._errorFormat !== 'pretty') { - return stripAnsi(message) - } - return message - } - unpack(document, data, path, rootField, unpacker?: Unpacker) { - if (data?.data) { - data = data.data - } - // to lift up _all in _count - if (unpacker) { - data[rootField] = unpacker(data[rootField]) - } - - const getPath: any[] = [] - if (rootField) { - getPath.push(rootField) - } - getPath.push(...path.filter((p) => p !== 'select' && p !== 'include')) - return unpack({ document, data, path: getPath }) - } +export type RequestParams = { + document: Document + dataPath: string[] + rootField: string + typeName: string + isList: boolean + clientMethod: string + callsite?: string + rejectOnNotFound?: RejectOnNotFound + runInTransaction?: boolean + showColors?: boolean + engineHook?: EngineMiddleware + args: any + headers?: Record + transactionId?: number + unpacker?: Unpacker } export function getOperation(action: DMMF.ModelAction): 'query' | 'mutation' { diff --git a/src/packages/client/src/runtime/query.ts b/src/packages/client/src/runtime/query.ts index a433fe4d30ed..34fb9016f7f4 100644 --- a/src/packages/client/src/runtime/query.ts +++ b/src/packages/client/src/runtime/query.ts @@ -393,7 +393,10 @@ ${errorMessages}${missingArgsLegend}\n` return str } + + return undefined } + protected printArgError = ( { error, path, id }: ArgError, hasMissingItems: boolean, @@ -552,6 +555,8 @@ ${errorMessages}${missingArgsLegend}\n` .map((key) => chalk.redBright(key)) .join(' and ')}.${additional}` } + + return undefined } /** * As we're allowing both single objects and array of objects for list inputs, we need to remove incorrect diff --git a/src/packages/client/tsconfig.json b/src/packages/client/tsconfig.json index 5524ba538e24..3d3b80db1f4e 100644 --- a/src/packages/client/tsconfig.json +++ b/src/packages/client/tsconfig.json @@ -1,16 +1,21 @@ { "compilerOptions": { - "lib": ["esnext"], + "lib": [ + "esnext" + ], "module": "commonjs", "target": "es2018", "strict": true, "esModuleInterop": true, "sourceMap": true, - "noImplicitAny": false, + "noImplicitAny": false, // TODO Can't let TS shoot itself on the foot "outDir": "dist", "rootDir": "src", "declaration": true, - "incremental": true + "incremental": true, + "noUncheckedIndexedAccess": false, // TODO Can't let TS shoot itself on the foot + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true }, "exclude": [ "dist", @@ -29,4 +34,4 @@ "index.d.ts", "scripts/backup-index.d.ts" ] -} +} \ No newline at end of file