diff --git a/packages/client/src/__tests__/integration/happy/interactive-transactions/test.ts b/packages/client/src/__tests__/integration/happy/interactive-transactions/test.ts index 363bfcb16736..078549001104 100644 --- a/packages/client/src/__tests__/integration/happy/interactive-transactions/test.ts +++ b/packages/client/src/__tests__/integration/happy/interactive-transactions/test.ts @@ -303,6 +303,42 @@ Invalid \`prisma.user.create()\` invocation: expect(users.length).toBe(0) }) + /** + * A bad batch should rollback using the interactive transaction logic + */ + test('batching raw rollback', async () => { + await prisma.user.create({ + data: { + email: 'user_1@website.com', + }, + }) + + const result = prisma.$transaction([ + prisma.$executeRaw( + 'INSERT INTO User (id, email) VALUES ("2", "user_2@website.com")', + ), + prisma.$queryRaw('DELETE FROM User'), + prisma.$executeRaw( + 'INSERT INTO User (id, email) VALUES ("1", "user_1@website.com")', + ), + prisma.$executeRaw( + 'INSERT INTO User (id, email) VALUES ("1", "user_1@website.com")', + ), + ]) + + await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(` + +Invalid \`prisma.executeRaw()\` invocation: + + + Raw query failed. Code: \`2067\`. Message: \`UNIQUE constraint failed: User.email\` +`) + + const users = await prisma.user.findMany() + + expect(users.length).toBe(1) + }) + /** * Middlewares should work normally on batches */ diff --git a/packages/client/src/__tests__/integration/happy/prisma-promises/.gitignore b/packages/client/src/__tests__/integration/happy/prisma-promises/.gitignore new file mode 100644 index 000000000000..c8d334f884aa --- /dev/null +++ b/packages/client/src/__tests__/integration/happy/prisma-promises/.gitignore @@ -0,0 +1 @@ +!dev.db diff --git a/packages/client/src/__tests__/integration/happy/prisma-promises/dev.db b/packages/client/src/__tests__/integration/happy/prisma-promises/dev.db new file mode 100644 index 000000000000..4be62b3edd4e Binary files /dev/null and b/packages/client/src/__tests__/integration/happy/prisma-promises/dev.db differ diff --git a/packages/client/src/__tests__/integration/happy/prisma-promises/schema.prisma b/packages/client/src/__tests__/integration/happy/prisma-promises/schema.prisma new file mode 100644 index 000000000000..f82593508957 --- /dev/null +++ b/packages/client/src/__tests__/integration/happy/prisma-promises/schema.prisma @@ -0,0 +1,29 @@ +datasource db { + provider = "sqlite" + url = "file:dev.db" +} + +generator client { + provider = "prisma-client-js" +} + +// / User model comment +model User { + id String @default(uuid()) @id + email String @unique + age Int + // / name comment + name String? + posts Post[] +} + +model Post { + id String @default(cuid()) @id + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + published Boolean + title String + content String? + authorId String? + author User? @relation(fields: [authorId], references: [id]) +} diff --git a/packages/client/src/__tests__/integration/happy/prisma-promises/test.ts b/packages/client/src/__tests__/integration/happy/prisma-promises/test.ts new file mode 100644 index 000000000000..f606d6a2df98 --- /dev/null +++ b/packages/client/src/__tests__/integration/happy/prisma-promises/test.ts @@ -0,0 +1,68 @@ +import { getTestClient } from '../../../../utils/getTestClient' +describe('prisma promises', () => { + /** + * Requests must get sent if we call `.catch` + */ + test('catch', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient() + const handler = (e) => Promise.reject(e) + + const remove = await prisma.user.deleteMany().catch(handler) + const queryRaw = await prisma.$queryRaw('SELECT 1').catch(handler) + const executeRaw = await prisma + .$executeRaw('DELETE FROM User') + .catch(handler) + const findMany = await prisma.user.findMany().catch(handler) + + expect(remove).toMatchInlineSnapshot(` + Object { + count: 0, + } + `) + expect(queryRaw).toMatchInlineSnapshot(` + Array [ + Object { + 1: 1, + }, + ] + `) + expect(executeRaw).toMatchInlineSnapshot(`0`) + expect(findMany).toMatchInlineSnapshot(`Array []`) + + await prisma.$disconnect() + }) + + /** + * Requests must get sent if we call `.finally` + */ + test('finally', async () => { + const PrismaClient = await getTestClient() + const prisma = new PrismaClient() + const handler = () => {} + + const remove = await prisma.user.deleteMany().finally(handler) + const queryRaw = await prisma.$queryRaw('SELECT 1').finally(handler) + const executeRaw = await prisma + .$executeRaw('DELETE FROM User') + .finally(handler) + const findMany = await prisma.user.findMany().finally(handler) + + expect(remove).toMatchInlineSnapshot(` + Object { + count: 0, + } + `) + expect(queryRaw).toMatchInlineSnapshot(` + Array [ + Object { + 1: 1, + }, + ] + `) + expect(executeRaw).toMatchInlineSnapshot(`0`) + expect(findMany).toMatchInlineSnapshot(`Array []`) + + await prisma.$disconnect() + }) +}) diff --git a/packages/client/src/runtime/getPrismaClient.ts b/packages/client/src/runtime/getPrismaClient.ts index 48ef1ad76708..294710150edf 100644 --- a/packages/client/src/runtime/getPrismaClient.ts +++ b/packages/client/src/runtime/getPrismaClient.ts @@ -548,7 +548,7 @@ export function getPrismaClient(config: GetPrismaClientOptions) { */ private $executeRawInternal( runInTransaction: boolean, - transactionId: number | null, + transactionId: number | undefined, stringOrTemplateStringsArray: | ReadonlyArray | string @@ -649,7 +649,7 @@ export function getPrismaClient(config: GetPrismaClientOptions) { action: 'executeRaw', callsite: this._getCallsite(), runInTransaction, - transactionId: transactionId ?? undefined, + transactionId: transactionId, }) } @@ -663,11 +663,11 @@ export function getPrismaClient(config: GetPrismaClientOptions) { | sqlTemplateTag.Sql, ...values: sqlTemplateTag.RawValue[] ) { - const doRequest = (runInTransaction = false, transactionId?: number) => { + const request = (transactionId?: number, runInTransaction?: boolean) => { try { const promise = this.$executeRawInternal( - runInTransaction, - transactionId ?? null, + runInTransaction ?? false, + transactionId, stringOrTemplateStringsArray, ...values, ) @@ -679,17 +679,17 @@ export function getPrismaClient(config: GetPrismaClientOptions) { } } return { - then(onfulfilled, onrejected) { - return doRequest().then(onfulfilled, onrejected) + then(onFulfilled, onRejected, transactionId?: number) { + return request(transactionId).then(onFulfilled, onRejected) }, requestTransaction(transactionId: number) { - return doRequest(true, transactionId) + return request(transactionId, true) }, - catch(onrejected) { - return doRequest().catch(onrejected) + catch(onRejected) { + return request().catch(onRejected) }, - finally(onfinally) { - return doRequest().finally(onfinally) + finally(onFinally) { + return request().finally(onFinally) }, } } @@ -706,7 +706,7 @@ export function getPrismaClient(config: GetPrismaClientOptions) { */ private $queryRawInternal( runInTransaction: boolean, - transactionId: number | null, + transactionId: number | undefined, stringOrTemplateStringsArray: | ReadonlyArray | TemplateStringsArray @@ -808,7 +808,7 @@ export function getPrismaClient(config: GetPrismaClientOptions) { action: 'queryRaw', callsite: this._getCallsite(), runInTransaction, - transactionId: transactionId ?? undefined, + transactionId: transactionId, }) } @@ -819,11 +819,11 @@ export function getPrismaClient(config: GetPrismaClientOptions) { stringOrTemplateStringsArray, ...values: sqlTemplateTag.RawValue[] ) { - const doRequest = (runInTransaction = false, transactionId?: number) => { + const request = (transactionId?: number, runInTransaction?: boolean) => { try { const promise = this.$queryRawInternal( - runInTransaction, - transactionId ?? null, + runInTransaction ?? false, + transactionId, stringOrTemplateStringsArray, ...values, ) @@ -835,17 +835,17 @@ export function getPrismaClient(config: GetPrismaClientOptions) { } } return { - then(onfulfilled, onrejected) { - return doRequest().then(onfulfilled, onrejected) + then(onFulfilled, onRejected, transactionId?: number) { + return request(transactionId).then(onFulfilled, onRejected) }, requestTransaction(transactionId: number) { - return doRequest(true, transactionId) + return request(transactionId, true) }, - catch(onrejected) { - return doRequest().catch(onrejected) + catch(onRejected) { + return request().catch(onRejected) }, - finally(onfinally) { - return doRequest().finally(onfinally) + finally(onFinally) { + return request().finally(onFinally) }, } } @@ -1194,19 +1194,20 @@ new PrismaClient({ } private _bootstrapClient() { - const clients = this._dmmf.mappings.modelOperations.reduce( - (acc, mapping) => { - const lowerCaseModel = lowerCase(mapping.model) - const model = this._dmmf.modelMap[mapping.model] + const modelClientBuilders = this._dmmf.mappings.modelOperations.reduce( + (modelClientBuilders, modelMapping) => { + const lowerCaseModel = lowerCase(modelMapping.model) + const model = this._dmmf.modelMap[modelMapping.model] if (!model) { throw new Error( - `Invalid mapping ${mapping.model}, can't find model`, + `Invalid mapping ${modelMapping.model}, can't find model`, ) } - // TODO: add types - const prismaClient = ({ + // creates a builder for `prisma...` in the runtime so that + // all models will get their own sub-"client" for query execution + const ModelClientBuilder = ({ operation, actionName, args, @@ -1221,59 +1222,54 @@ new PrismaClient({ modelName: string unpacker?: Unpacker }) => { - dataPath = dataPath ?? [] + let requestPromise: Promise | undefined - const clientMethod = `${lowerCaseModel}.${actionName}` - - let requestPromise: Promise + // prepare a request with current context & prevent multi-calls we + // save it into `requestPromise` to allow one request per promise const callsite = this._getCallsite() + const request = ( + transactionId?: number, + runInTransaction?: boolean, + ) => { + requestPromise = + requestPromise ?? + this._request({ + args, + model: modelName ?? model.name, + action: actionName, + clientMethod: `${lowerCaseModel}.${actionName}`, + dataPath: dataPath, + callsite: callsite, + runInTransaction: runInTransaction ?? false, + transactionId: transactionId, + unpacker, + }) - const requestModelName = modelName ?? model.name - - const clientImplementation = { - then: (onfulfilled, onrejected, transactionId?: number) => { - if (!requestPromise) { - requestPromise = this._request({ - args, - dataPath, - action: actionName, - model: requestModelName, - clientMethod, - callsite, - runInTransaction: false, - transactionId: transactionId, - unpacker, - }) - } + return requestPromise + } - return requestPromise.then(onfulfilled, onrejected) + // `modelClient` implements promises to have deferred actions that + // will be called later on through model delegated functions + const modelClient = { + then: (onFulfilled, onRejected, transactionId?: number) => { + return request(transactionId).then(onFulfilled, onRejected) }, requestTransaction: (transactionId: number) => { - if (!requestPromise) { - requestPromise = this._request({ - args, - dataPath, - action: actionName, - model: requestModelName, - clientMethod, - callsite, - runInTransaction: true, - transactionId, - unpacker, - }) - } - - return requestPromise + return request(transactionId, true) + }, + catch: (onRejected) => { + return request().catch(onRejected) + }, + finally: (onFinally) => { + return request().finally(onFinally) }, - catch: (onrejected) => requestPromise?.catch(onrejected), - finally: (onfinally) => requestPromise?.finally(onfinally), } // add relation fields for (const field of model.fields.filter( (f) => f.kind === 'object', )) { - clientImplementation[field.name] = (fieldArgs) => { + modelClient[field.name] = (fieldArgs) => { const prefix = dataPath.includes('select') ? 'select' : dataPath.includes('include') @@ -1282,7 +1278,7 @@ new PrismaClient({ const newDataPath = [...dataPath, prefix, field.name] const newArgs = deepSet(args, newDataPath, fieldArgs || true) - return clients[field.type]({ + return modelClientBuilders[field.type]({ operation, actionName, args: newArgs, @@ -1296,12 +1292,12 @@ new PrismaClient({ } } - return clientImplementation + return modelClient } - acc[model.name] = prismaClient + modelClientBuilders[model.name] = ModelClientBuilder - return acc + return modelClientBuilders }, {}, ) @@ -1316,13 +1312,16 @@ new PrismaClient({ groupBy: true, } + // here we call the `modelClientBuilder` inside of each delegate function + // once triggered, the function will return the `modelClient` from above const delegate: any = Object.keys(mapping).reduce((acc, actionName) => { if (!filteredActionsList[actionName]) { const operation = getOperation(actionName as any) acc[actionName] = (args) => - clients[mapping.model]({ + modelClientBuilders[mapping.model]({ operation, actionName, + dataPath: [], args, }) } @@ -1343,7 +1342,7 @@ new PrismaClient({ } } - return clients[mapping.model]({ + return modelClientBuilders[mapping.model]({ operation: 'query', actionName: `aggregate`, args: { @@ -1391,7 +1390,7 @@ new PrismaClient({ return acc }, {} as any) - return clients[mapping.model]({ + return modelClientBuilders[mapping.model]({ operation: 'query', actionName: 'aggregate', // actionName is just cosmetics 💅🏽 rootField: mapping.aggregate, @@ -1453,7 +1452,7 @@ new PrismaClient({ return acc }, {} as any) - return clients[mapping.model]({ + return modelClientBuilders[mapping.model]({ operation: 'query', actionName: 'groupBy', // actionName is just cosmetics 💅🏽 rootField: mapping.groupBy, diff --git a/packages/debug/package.json b/packages/debug/package.json index eef2afacf026..1259e92dd55c 100644 --- a/packages/debug/package.json +++ b/packages/debug/package.json @@ -15,11 +15,10 @@ ], "bugs": "https://github.com/prisma/prisma/issues", "devDependencies": { - "@types/debug": "4.1.7", - "@types/jest": "26.0.24", - "@types/node": "12.20.18", - "@typescript-eslint/eslint-plugin": "4.29.0", - "@typescript-eslint/parser": "4.29.0", + "@types/jest": "27.0.0", + "@types/node": "12.20.19", + "@typescript-eslint/eslint-plugin": "4.29.1", + "@typescript-eslint/parser": "4.29.1", "esbuild": "0.12.16", "eslint": "7.32.0", "eslint-config-prettier": "8.3.0", @@ -50,6 +49,7 @@ "dist" ], "dependencies": { + "@types/debug": "4.1.7", "debug": "4.3.2", "ms": "2.1.3" },