From 2785eaa9fec151541247a152a237cd0ec46eb9a3 Mon Sep 17 00:00:00 2001 From: Alexey Orlenko Date: Tue, 8 Nov 2022 15:09:34 +0100 Subject: [PATCH] feat(client): enable ITX support for DataProxy (#16005) Closes prisma/client-planning#92 Closes #16177 Co-authored-by: Daniel Starns Co-authored-by: Sergey Tatarintsev --- packages/client/package.json | 2 +- packages/client/src/runtime/RequestHandler.ts | 9 +- .../src/runtime/core/request/PrismaPromise.ts | 1 + .../client/src/runtime/getPrismaClient.ts | 2 +- .../__snapshots__/tests.ts.snap | 24 +- .../interactive-transactions/tests.ts | 1167 ++++++++--------- .../issues/13405-mongo-raw-itx/tests.ts | 4 - .../tests/functional/issues/15044/tests.ts | 86 +- .../engine-core/src/__tests__/errors.test.ts | 46 +- .../engine-core/src/binary/BinaryEngine.ts | 47 +- packages/engine-core/src/common/Engine.ts | 13 +- .../src/common/types/Transaction.ts | 12 +- .../utils/runtimeHeadersToHttpHeaders.ts | 15 + .../src/data-proxy/DataProxyEngine.ts | 222 +++- .../src/data-proxy/errors/BadRequestError.ts | 2 +- .../errors/EngineHealthcheckTimeoutError.ts | 19 + .../data-proxy/errors/EngineStartupError.ts | 19 + .../errors/EngineVersionNotSupportedError.ts | 17 + .../data-proxy/errors/GatewayTimeoutError.ts | 6 +- .../errors/InteractiveTransactionError.ts | 19 + .../data-proxy/errors/InvalidRequestError.ts | 24 + .../src/data-proxy/errors/NotFoundError.ts | 6 +- .../src/data-proxy/errors/ServerError.ts | 2 +- .../data-proxy/errors/UnauthorizedError.ts | 6 +- .../data-proxy/errors/UsageExceededError.ts | 6 +- .../errors/utils/responseToError.ts | 185 ++- .../src/data-proxy/utils/getClientVersion.ts | 9 + .../engine-core/src/library/LibraryEngine.ts | 14 +- .../getGenerators/getGenerators.test.ts | 33 - ...d-interactiveTransactions-client-js.prisma | 16 - .../check-feature-flags/checkFeatureFlags.ts | 10 +- ...ddenPreviewFeatureWithProxyFlagMessage.ts} | 0 pnpm-lock.yaml | 8 +- 33 files changed, 1168 insertions(+), 883 deletions(-) create mode 100644 packages/engine-core/src/common/utils/runtimeHeadersToHttpHeaders.ts create mode 100644 packages/engine-core/src/data-proxy/errors/EngineHealthcheckTimeoutError.ts create mode 100644 packages/engine-core/src/data-proxy/errors/EngineStartupError.ts create mode 100644 packages/engine-core/src/data-proxy/errors/EngineVersionNotSupportedError.ts create mode 100644 packages/engine-core/src/data-proxy/errors/InteractiveTransactionError.ts create mode 100644 packages/engine-core/src/data-proxy/errors/InvalidRequestError.ts delete mode 100644 packages/internals/src/__tests__/getGenerators/proxy-and-interactiveTransactions-client-js.prisma rename packages/internals/src/get-generators/utils/check-feature-flags/{forbiddenItxWithProxyFlagMessage.ts => forbiddenPreviewFeatureWithProxyFlagMessage.ts} (100%) diff --git a/packages/client/package.json b/packages/client/package.json index e251a51bfca7..65cc8f24ef85 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -79,7 +79,7 @@ "@prisma/instrumentation": "workspace:*", "@prisma/internals": "workspace:*", "@prisma/migrate": "workspace:*", - "@prisma/mini-proxy": "0.2.0", + "@prisma/mini-proxy": "0.3.0", "@swc-node/register": "1.5.4", "@swc/core": "1.3.14", "@swc/jest": "0.2.23", diff --git a/packages/client/src/runtime/RequestHandler.ts b/packages/client/src/runtime/RequestHandler.ts index 2897625e506f..f3dbe2181195 100644 --- a/packages/client/src/runtime/RequestHandler.ts +++ b/packages/client/src/runtime/RequestHandler.ts @@ -69,7 +69,7 @@ function getRequestInfo(request: Request) { } return { - batchTransaction: transaction?.kind === 'batch' ? transaction : undefined, + transaction, headers, } } @@ -92,13 +92,16 @@ export class RequestHandler { // TODO: pass the child information to QE for it to issue links to queries // const links = requests.map((r) => trace.getSpanContext(r.otelChildCtx!)) - return this.client._engine.requestBatch(queries, info.headers, info.batchTransaction) + const batchTransaction = info.transaction?.kind === 'batch' ? info.transaction : undefined + + return this.client._engine.requestBatch(queries, info.headers, batchTransaction) }, singleLoader: (request) => { const info = getRequestInfo(request) const query = String(request.document) + const interactiveTransaction = info.transaction?.kind === 'itx' ? info.transaction : undefined - return this.client._engine.request(query, info.headers) + return this.client._engine.request(query, info.headers, interactiveTransaction) }, batchBy: (request) => { if (request.transaction?.id) { diff --git a/packages/client/src/runtime/core/request/PrismaPromise.ts b/packages/client/src/runtime/core/request/PrismaPromise.ts index fa62d2b53775..f63cbcf52637 100644 --- a/packages/client/src/runtime/core/request/PrismaPromise.ts +++ b/packages/client/src/runtime/core/request/PrismaPromise.ts @@ -9,6 +9,7 @@ export type PrismaPromiseBatchTransaction = { export type PrismaPromiseInteractiveTransaction = { kind: 'itx' id: string + payload: unknown } export type PrismaPromiseTransaction = PrismaPromiseBatchTransaction | PrismaPromiseInteractiveTransaction diff --git a/packages/client/src/runtime/getPrismaClient.ts b/packages/client/src/runtime/getPrismaClient.ts index c4c0498db707..3c14b481924c 100644 --- a/packages/client/src/runtime/getPrismaClient.ts +++ b/packages/client/src/runtime/getPrismaClient.ts @@ -974,7 +974,7 @@ new PrismaClient({ let result: unknown try { // execute user logic with a proxied the client - result = await callback(transactionProxy(this, { id: info.id })) + result = await callback(transactionProxy(this, { id: info.id, payload: info.payload })) // it went well, then we commit the transaction await this._engine.transaction('commit', headers, info) diff --git a/packages/client/tests/functional/interactive-transactions/__snapshots__/tests.ts.snap b/packages/client/tests/functional/interactive-transactions/__snapshots__/tests.ts.snap index 80b035978171..14991637807a 100644 --- a/packages/client/tests/functional/interactive-transactions/__snapshots__/tests.ts.snap +++ b/packages/client/tests/functional/interactive-transactions/__snapshots__/tests.ts.snap @@ -13,8 +13,8 @@ exports[`interactive-transactions (provider=cockroachdb) batching rollback 1`] = Invalid \`prisma.user.create()\` invocation in /client/tests/functional/interactive-transactions/tests.ts:0:0 - XX */ - XX testIf(getClientEngineType() === ClientEngineType.Library)('batching rollback', async () => { + XX 'batching rollback', + XX async () => { XX const result = prisma.$transaction([ → XX prisma.user.create( Unique constraint failed on the fields: (\`email\`) @@ -37,8 +37,8 @@ exports[`interactive-transactions (provider=mongodb) batching rollback 1`] = ` Invalid \`prisma.user.create()\` invocation in /client/tests/functional/interactive-transactions/tests.ts:0:0 - XX */ - XX testIf(getClientEngineType() === ClientEngineType.Library)('batching rollback', async () => { + XX 'batching rollback', + XX async () => { XX const result = prisma.$transaction([ → XX prisma.user.create( Unique constraint failed on the constraint: \`User_email_key\` @@ -69,8 +69,8 @@ exports[`interactive-transactions (provider=mysql) batching rollback 1`] = ` Invalid \`prisma.user.create()\` invocation in /client/tests/functional/interactive-transactions/tests.ts:0:0 - XX */ - XX testIf(getClientEngineType() === ClientEngineType.Library)('batching rollback', async () => { + XX 'batching rollback', + XX async () => { XX const result = prisma.$transaction([ → XX prisma.user.create( Unique constraint failed on the constraint: \`User_email_key\` @@ -101,8 +101,8 @@ exports[`interactive-transactions (provider=postgresql) batching rollback 1`] = Invalid \`prisma.user.create()\` invocation in /client/tests/functional/interactive-transactions/tests.ts:0:0 - XX */ - XX testIf(getClientEngineType() === ClientEngineType.Library)('batching rollback', async () => { + XX 'batching rollback', + XX async () => { XX const result = prisma.$transaction([ → XX prisma.user.create( Unique constraint failed on the fields: (\`email\`) @@ -133,8 +133,8 @@ exports[`interactive-transactions (provider=sqlite) batching rollback 1`] = ` Invalid \`prisma.user.create()\` invocation in /client/tests/functional/interactive-transactions/tests.ts:0:0 - XX */ - XX testIf(getClientEngineType() === ClientEngineType.Library)('batching rollback', async () => { + XX 'batching rollback', + XX async () => { XX const result = prisma.$transaction([ → XX prisma.user.create( Unique constraint failed on the fields: (\`email\`) @@ -165,8 +165,8 @@ exports[`interactive-transactions (provider=sqlserver) batching rollback 1`] = ` Invalid \`prisma.user.create()\` invocation in /client/tests/functional/interactive-transactions/tests.ts:0:0 - XX */ - XX testIf(getClientEngineType() === ClientEngineType.Library)('batching rollback', async () => { + XX 'batching rollback', + XX async () => { XX const result = prisma.$transaction([ → XX prisma.user.create( Unique constraint failed on the constraint: \`dbo.User\` diff --git a/packages/client/tests/functional/interactive-transactions/tests.ts b/packages/client/tests/functional/interactive-transactions/tests.ts index 56c0c43a654e..23085d9f2ad3 100644 --- a/packages/client/tests/functional/interactive-transactions/tests.ts +++ b/packages/client/tests/functional/interactive-transactions/tests.ts @@ -11,221 +11,221 @@ declare let newPrismaClient: NewPrismaClient const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) -testMatrix.setupTestSuite( - ({ provider }) => { - // TODO: Technically, only "high concurrency" test requires larger timeout - // but `jest.setTimeout` does not work inside of the test at the moment - // https://github.com/facebook/jest/issues/11543 - jest.setTimeout(60_000) +testMatrix.setupTestSuite(({ provider }, _suiteMeta, clientMeta) => { + // TODO: Technically, only "high concurrency" test requires larger timeout + // but `jest.setTimeout` does not work inside of the test at the moment + // https://github.com/facebook/jest/issues/11543 + jest.setTimeout(60_000) + + beforeEach(async () => { + await prisma.user.deleteMany() + }) + + /** + * Minimal example of an interactive transaction + */ + test('basic', async () => { + const result = await prisma.$transaction(async (prisma) => { + await prisma.user.create({ + data: { + email: 'user_1@website.com', + }, + }) - beforeEach(async () => { - await prisma.user.deleteMany() - }) + await prisma.user.create({ + data: { + email: 'user_2@website.com', + }, + }) - /** - * Minimal example of an interactive transaction - */ - test('basic', async () => { - const result = await prisma.$transaction(async (prisma) => { - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) + return prisma.user.findMany() + }) - await prisma.user.create({ - data: { - email: 'user_2@website.com', - }, - }) + expect(result.length).toBe(2) + }) - return prisma.user.findMany() + /** + * Transactions should fail after the default timeout + */ + test('timeout default', async () => { + const result = prisma.$transaction(async (prisma) => { + await prisma.user.create({ + data: { + email: 'user_1@website.com', + }, }) - expect(result.length).toBe(2) + await delay(6000) }) - /** - * Transactions should fail after the default timeout - */ - test('timeout default', async () => { - const result = prisma.$transaction(async (prisma) => { + await expect(result).rejects.toMatchObject({ + message: expect.stringContaining('Transaction API error: Transaction already closed'), + code: 'P2028', + clientVersion: '0.0.0', + }) + + expect(await prisma.user.findMany()).toHaveLength(0) + }) + + /** + * Transactions should fail if they time out on `timeout` + */ + test('timeout override', async () => { + const result = prisma.$transaction( + async (prisma) => { await prisma.user.create({ data: { email: 'user_1@website.com', }, }) - await delay(6000) - }) - - await expect(result).rejects.toMatchObject({ - message: expect.stringContaining('Transaction API error: Transaction already closed'), - code: 'P2028', - clientVersion: '0.0.0', - }) + await delay(600) + }, + { + maxWait: 200, + timeout: 500, + }, + ) - expect(await prisma.user.findMany()).toHaveLength(0) + await expect(result).rejects.toMatchObject({ + message: expect.stringContaining('Transaction API error: Transaction already closed'), }) - /** - * Transactions should fail if they time out on `timeout` - */ - test('timeout override', async () => { - const result = prisma.$transaction( - async (prisma) => { - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) + expect(await prisma.user.findMany()).toHaveLength(0) + }) - await new Promise((res) => setTimeout(res, 600)) - }, - { - maxWait: 200, - timeout: 500, + /** + * Transactions should fail and rollback if thrown within + */ + test('rollback throw', async () => { + const result = prisma.$transaction(async (prisma) => { + await prisma.user.create({ + data: { + email: 'user_1@website.com', }, - ) - - await expect(result).rejects.toMatchObject({ - message: expect.stringContaining('Transaction API error: Transaction already closed'), }) - expect(await prisma.user.findMany()).toHaveLength(0) + throw new Error('you better rollback now') }) - /** - * Transactions should fail and rollback if thrown within - */ - test('rollback throw', async () => { - const result = prisma.$transaction(async (prisma) => { - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) + await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`you better rollback now`) - throw new Error('you better rollback now') - }) + const users = await prisma.user.findMany() - await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`you better rollback now`) + expect(users.length).toBe(0) + }) - const users = await prisma.user.findMany() + /** + * Transactions should fail and rollback if a value is thrown within + */ + test('rollback throw value', async () => { + const result = prisma.$transaction(async (prisma) => { + await prisma.user.create({ + data: { + email: 'user_1@website.com', + }, + }) - expect(users.length).toBe(0) + throw 'you better rollback now' }) - /** - * Transactions should fail and rollback if a value is thrown within - */ - test('rollback throw value', async () => { - const result = prisma.$transaction(async (prisma) => { - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) - - throw 'you better rollback now' - }) + await expect(result).rejects.toBe(`you better rollback now`) - await expect(result).rejects.toBe(`you better rollback now`) + const users = await prisma.user.findMany() - const users = await prisma.user.findMany() + expect(users.length).toBe(0) + }) - expect(users.length).toBe(0) - }) + /** + * A transaction might fail if it's called inside another transaction + * //! this works only for postgresql + */ + testIf(provider === 'postgresql')('postgresql: nested create', async () => { + const result = prisma.$transaction(async (tx) => { + await tx.user.create({ + data: { + email: 'user_1@website.com', + }, + }) - /** - * A transaction might fail if it's called inside another transaction - * //! this works only for postgresql - */ - testIf(provider === 'postgresql')('postgresql: nested create', async () => { - const result = prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx) => { await tx.user.create({ data: { - email: 'user_1@website.com', + email: 'user_2@website.com', }, }) - - await prisma.$transaction(async (tx) => { - await tx.user.create({ - data: { - email: 'user_2@website.com', - }, - }) - }) - - return tx.user.findMany() }) - await expect(result).resolves.toHaveLength(2) + return tx.user.findMany() }) - /** - * We don't allow certain methods to be called in a transaction - */ - test('forbidden', async () => { - const forbidden = ['$connect', '$disconnect', '$on', '$transaction', '$use'] - expect.assertions(forbidden.length + 1) + await expect(result).resolves.toHaveLength(2) + }) - const result = prisma.$transaction((prisma) => { - for (const method of forbidden) { - expect(prisma).not.toHaveProperty(method) - } - return Promise.resolve() - }) + /** + * We don't allow certain methods to be called in a transaction + */ + test('forbidden', async () => { + const forbidden = ['$connect', '$disconnect', '$on', '$transaction', '$use'] + expect.assertions(forbidden.length + 1) - await expect(result).resolves.toBe(undefined) + const result = prisma.$transaction((prisma) => { + for (const method of forbidden) { + expect(prisma).not.toHaveProperty(method) + } + return Promise.resolve() }) - /** - * If one of the query fails, all queries should cancel - */ - test('rollback query', async () => { - const result = prisma.$transaction(async (prisma) => { - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) + await expect(result).resolves.toBe(undefined) + }) - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) + /** + * If one of the query fails, all queries should cancel + */ + testIf(clientMeta.runtime !== 'edge')('rollback query', async () => { + const result = prisma.$transaction(async (prisma) => { + await prisma.user.create({ + data: { + email: 'user_1@website.com', + }, }) - await expect(result).rejects.toMatchPrismaErrorSnapshot() + await prisma.user.create({ + data: { + email: 'user_1@website.com', + }, + }) + }) - const users = await prisma.user.findMany() + await expect(result).rejects.toMatchPrismaErrorSnapshot() - expect(users.length).toBe(0) - }) + const users = await prisma.user.findMany() - test('already committed', async () => { - let transactionBoundPrisma - await prisma.$transaction((prisma) => { - transactionBoundPrisma = prisma - return Promise.resolve() - }) + expect(users.length).toBe(0) + }) - const result = prisma.$transaction(async () => { - await transactionBoundPrisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) - }) + test('already committed', async () => { + let transactionBoundPrisma + await prisma.$transaction((prisma) => { + transactionBoundPrisma = prisma + return Promise.resolve() + }) - await expect(result).rejects.toMatchObject({ - message: expect.stringContaining('Transaction API error: Transaction already closed'), - code: 'P2028', - clientVersion: '0.0.0', + const result = prisma.$transaction(async () => { + await transactionBoundPrisma.user.create({ + data: { + email: 'user_1@website.com', + }, }) + }) + + await expect(result).rejects.toMatchObject({ + message: expect.stringContaining('Transaction API error: Transaction already closed'), + code: 'P2028', + clientVersion: '0.0.0', + }) + if (clientMeta.runtime !== 'edge') { await expect(result).rejects.toMatchPrismaErrorInlineSnapshot(` Invalid \`transactionBoundPrisma.user.create()\` invocation in @@ -237,39 +237,42 @@ testMatrix.setupTestSuite( → XX await transactionBoundPrisma.user.create( Transaction API error: Transaction already closed: A query cannot be executed on a closed transaction.. `) + } - const users = await prisma.user.findMany() + const users = await prisma.user.findMany() - expect(users.length).toBe(0) - }) + expect(users.length).toBe(0) + }) - /** - * Batching should work with using the interactive transaction logic - */ - test('batching', async () => { - await prisma.$transaction([ - prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }), - prisma.user.create({ - data: { - email: 'user_2@website.com', - }, - }), - ]) + /** + * Batching should work with using the interactive transaction logic + */ + test('batching', async () => { + await prisma.$transaction([ + prisma.user.create({ + data: { + email: 'user_1@website.com', + }, + }), + prisma.user.create({ + data: { + email: 'user_2@website.com', + }, + }), + ]) - const users = await prisma.user.findMany() + const users = await prisma.user.findMany() - expect(users.length).toBe(2) - }) + expect(users.length).toBe(2) + }) - /** - * A bad batch should rollback using the interactive transaction logic - * // TODO: skipped because output differs from binary to library - */ - testIf(getClientEngineType() === ClientEngineType.Library)('batching rollback', async () => { + /** + * A bad batch should rollback using the interactive transaction logic + * // TODO: skipped because output differs from binary to library + */ + testIf(getClientEngineType() === ClientEngineType.Library && clientMeta.runtime !== 'edge')( + 'batching rollback', + async () => { const result = prisma.$transaction([ prisma.user.create({ data: { @@ -288,160 +291,107 @@ testMatrix.setupTestSuite( const users = await prisma.user.findMany() expect(users.length).toBe(0) - }) + }, + ) + + /** + * A bad batch should rollback using the interactive transaction logic + * // TODO: skipped because output differs from binary to library + */ + testIf(getClientEngineType() === ClientEngineType.Library && provider !== 'mongodb' && clientMeta.runtime !== 'edge')( + 'batching raw rollback', + async () => { + await prisma.user.create({ + data: { + id: '1', + email: 'user_1@website.com', + }, + }) - /** - * A bad batch should rollback using the interactive transaction logic - * // TODO: skipped because output differs from binary to library - */ - testIf(getClientEngineType() === ClientEngineType.Library && provider !== 'mongodb')( - 'batching raw rollback', - async () => { - await prisma.user.create({ - data: { - id: '1', - email: 'user_1@website.com', - }, - }) + const result = + provider === 'mysql' + ? prisma.$transaction([ + // @ts-test-if: provider !== 'mongodb' + prisma.$executeRaw`INSERT INTO User (id, email) VALUES (${'2'}, ${'user_2@website.com'})`, + // @ts-test-if: provider !== 'mongodb' + prisma.$queryRaw`DELETE FROM User`, + // @ts-test-if: provider !== 'mongodb' + prisma.$executeRaw`INSERT INTO User (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, + // @ts-test-if: provider !== 'mongodb' + prisma.$executeRaw`INSERT INTO User (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, + ]) + : prisma.$transaction([ + // @ts-test-if: provider !== 'mongodb' + prisma.$executeRaw`INSERT INTO "User" (id, email) VALUES (${'2'}, ${'user_2@website.com'})`, + // @ts-test-if: provider !== 'mongodb' + prisma.$queryRaw`DELETE FROM "User"`, + // @ts-test-if: provider !== 'mongodb' + prisma.$executeRaw`INSERT INTO "User" (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, + // @ts-test-if: provider !== 'mongodb' + prisma.$executeRaw`INSERT INTO "User" (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, + ]) - const result = - provider === 'mysql' - ? prisma.$transaction([ - // @ts-test-if: provider !== 'mongodb' - prisma.$executeRaw`INSERT INTO User (id, email) VALUES (${'2'}, ${'user_2@website.com'})`, - // @ts-test-if: provider !== 'mongodb' - prisma.$queryRaw`DELETE FROM User`, - // @ts-test-if: provider !== 'mongodb' - prisma.$executeRaw`INSERT INTO User (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, - // @ts-test-if: provider !== 'mongodb' - prisma.$executeRaw`INSERT INTO User (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, - ]) - : prisma.$transaction([ - // @ts-test-if: provider !== 'mongodb' - prisma.$executeRaw`INSERT INTO "User" (id, email) VALUES (${'2'}, ${'user_2@website.com'})`, - // @ts-test-if: provider !== 'mongodb' - prisma.$queryRaw`DELETE FROM "User"`, - // @ts-test-if: provider !== 'mongodb' - prisma.$executeRaw`INSERT INTO "User" (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, - // @ts-test-if: provider !== 'mongodb' - prisma.$executeRaw`INSERT INTO "User" (id, email) VALUES (${'1'}, ${'user_1@website.com'})`, - ]) - - await expect(result).rejects.toMatchPrismaErrorSnapshot() - - const users = await prisma.user.findMany() - - expect(users.length).toBe(1) - }, - ) + await expect(result).rejects.toMatchPrismaErrorSnapshot() - // running this test on isolated prisma instance since - // middleware change the return values of model methods - // and this would affect subsequent tests if run on a main instance - describe('middlewares', () => { - /** - * Minimal example of a interactive transaction & middleware - */ - test('middleware basic', async () => { - const isolatedPrisma = newPrismaClient() - let runInTransaction = false + const users = await prisma.user.findMany() - isolatedPrisma.$use(async (params, next) => { - await next(params) + expect(users.length).toBe(1) + }, + ) - runInTransaction = params.runInTransaction + // running this test on isolated prisma instance since + // middleware change the return values of model methods + // and this would affect subsequent tests if run on a main instance + describe('middlewares', () => { + /** + * Minimal example of a interactive transaction & middleware + */ + test('middleware basic', async () => { + const isolatedPrisma = newPrismaClient() + let runInTransaction = false - return 'result' - }) + isolatedPrisma.$use(async (params, next) => { + await next(params) - const result = await isolatedPrisma.$transaction((prisma) => { - return prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) - }) + runInTransaction = params.runInTransaction - expect(result).toBe('result') - expect(runInTransaction).toBe(true) + return 'result' }) - /** - * Middlewares should work normally on batches - */ - test('middlewares batching', async () => { - const isolatedPrisma = newPrismaClient() - isolatedPrisma.$use(async (params, next) => { - const result = await next(params) - - return result + const result = await isolatedPrisma.$transaction((prisma) => { + return prisma.user.create({ + data: { + email: 'user_1@website.com', + }, }) - - await isolatedPrisma.$transaction([ - prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }), - prisma.user.create({ - data: { - email: 'user_2@website.com', - }, - }), - ]) - - const users = await prisma.user.findMany() - - expect(users.length).toBe(2) }) - test('middleware exclude from transaction', async () => { - const isolatedPrisma = newPrismaClient() - - isolatedPrisma.$use((params, next) => { - return next({ ...params, runInTransaction: false }) - }) - - await isolatedPrisma - .$transaction(async (prisma) => { - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) - - await prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }) - }) - .catch((e) => {}) - - const users = await isolatedPrisma.user.findMany() - expect(users).toHaveLength(1) - }) + expect(result).toBe('result') + expect(runInTransaction).toBe(true) }) /** - * Two concurrent transactions should work + * Middlewares should work normally on batches */ - test('concurrent', async () => { - await Promise.all([ - prisma.$transaction([ - prisma.user.create({ - data: { - email: 'user_1@website.com', - }, - }), - ]), - prisma.$transaction([ - prisma.user.create({ - data: { - email: 'user_2@website.com', - }, - }), - ]), + test('middlewares batching', async () => { + const isolatedPrisma = newPrismaClient() + isolatedPrisma.$use(async (params, next) => { + const result = await next(params) + + return result + }) + + await isolatedPrisma.$transaction([ + prisma.user.create({ + data: { + email: 'user_1@website.com', + }, + }), + prisma.user.create({ + data: { + email: 'user_2@website.com', + }, + }), ]) const users = await prisma.user.findMany() @@ -449,304 +399,334 @@ testMatrix.setupTestSuite( expect(users.length).toBe(2) }) - /** - * Makes sure that the engine does not deadlock - * For sqlite, it sometimes causes DB lock up and all subsequent - * tests fail. We might want to re-enable it either after we implemented - * WAL mode (https://github.com/prisma/prisma/issues/3303) or identified the - * issue on our side - */ - testIf(provider !== 'sqlite')('high concurrency', async () => { - jest.setTimeout(30_000) + test('middleware exclude from transaction', async () => { + const isolatedPrisma = newPrismaClient() - await prisma.user.create({ - data: { - email: 'x', - name: 'y', - }, + isolatedPrisma.$use((params, next) => { + return next({ ...params, runInTransaction: false }) }) - for (let i = 0; i < 5; i++) { - await Promise.allSettled([ - prisma.$transaction((tx) => tx.user.update({ data: { name: 'a' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'b' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'c' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'd' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'e' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'f' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'g' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'h' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'i' }, where: { email: 'x' } }), { timeout: 25 }), - prisma.$transaction((tx) => tx.user.update({ data: { name: 'j' }, where: { email: 'x' } }), { timeout: 25 }), - ]).catch(() => {}) // we don't care for errors, there will be - } - }) - - /** - * Rollback should happen even with `then` calls - */ - test('rollback with then calls', async () => { - const result = prisma.$transaction(async (prisma) => { - await prisma.user - .create({ + await isolatedPrisma + .$transaction(async (prisma) => { + await prisma.user.create({ data: { email: 'user_1@website.com', }, }) - .then() - await prisma.user - .create({ + await prisma.user.create({ data: { - email: 'user_2@website.com', + email: 'user_1@website.com', }, }) - .then() - .then() + }) + .catch((e) => {}) - throw new Error('rollback') - }) + const users = await isolatedPrisma.user.findMany() + expect(users).toHaveLength(1) + }) + }) + + /** + * Two concurrent transactions should work + */ + test('concurrent', async () => { + await Promise.all([ + prisma.$transaction([ + prisma.user.create({ + data: { + email: 'user_1@website.com', + }, + }), + ]), + prisma.$transaction([ + prisma.user.create({ + data: { + email: 'user_2@website.com', + }, + }), + ]), + ]) + + const users = await prisma.user.findMany() + + expect(users.length).toBe(2) + }) + + /** + * Makes sure that the engine does not deadlock + * For sqlite, it sometimes causes DB lock up and all subsequent + * tests fail. We might want to re-enable it either after we implemented + * WAL mode (https://github.com/prisma/prisma/issues/3303) or identified the + * issue on our side + */ + testIf(provider !== 'sqlite')('high concurrency', async () => { + jest.setTimeout(30_000) + + await prisma.user.create({ + data: { + email: 'x', + name: 'y', + }, + }) - await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`rollback`) + for (let i = 0; i < 5; i++) { + await Promise.allSettled([ + prisma.$transaction((tx) => tx.user.update({ data: { name: 'a' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'b' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'c' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'd' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'e' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'f' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'g' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'h' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'i' }, where: { email: 'x' } }), { timeout: 25 }), + prisma.$transaction((tx) => tx.user.update({ data: { name: 'j' }, where: { email: 'x' } }), { timeout: 25 }), + ]).catch(() => {}) // we don't care for errors, there will be + } + }) + + /** + * Rollback should happen even with `then` calls + */ + test('rollback with then calls', async () => { + const result = prisma.$transaction(async (prisma) => { + await prisma.user + .create({ + data: { + email: 'user_1@website.com', + }, + }) + .then() - const users = await prisma.user.findMany() + await prisma.user + .create({ + data: { + email: 'user_2@website.com', + }, + }) + .then() + .then() - expect(users.length).toBe(0) + throw new Error('rollback') }) - /** - * Rollback should happen even with `catch` calls - */ - test('rollback with catch calls', async () => { - const result = prisma.$transaction(async (prisma) => { - await prisma.user - .create({ - data: { - email: 'user_1@website.com', - }, - }) - .catch() - await prisma.user - .create({ - data: { - email: 'user_2@website.com', - }, - }) - .catch() - .then() + await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`rollback`) - throw new Error('rollback') - }) + const users = await prisma.user.findMany() - await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`rollback`) + expect(users.length).toBe(0) + }) - const users = await prisma.user.findMany() + /** + * Rollback should happen even with `catch` calls + */ + test('rollback with catch calls', async () => { + const result = prisma.$transaction(async (prisma) => { + await prisma.user + .create({ + data: { + email: 'user_1@website.com', + }, + }) + .catch() + await prisma.user + .create({ + data: { + email: 'user_2@website.com', + }, + }) + .catch() + .then() - expect(users.length).toBe(0) + throw new Error('rollback') }) - /** - * Rollback should happen even with `finally` calls - */ - test('rollback with finally calls', async () => { - const result = prisma.$transaction(async (prisma) => { - await prisma.user - .create({ - data: { - email: 'user_1@website.com', - }, - }) - .finally() + await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`rollback`) - await prisma.user - .create({ - data: { - email: 'user_2@website.com', - }, - }) - .then() - .catch() - .finally() + const users = await prisma.user.findMany() - throw new Error('rollback') - }) + expect(users.length).toBe(0) + }) - await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`rollback`) + /** + * Rollback should happen even with `finally` calls + */ + test('rollback with finally calls', async () => { + const result = prisma.$transaction(async (prisma) => { + await prisma.user + .create({ + data: { + email: 'user_1@website.com', + }, + }) + .finally() - const users = await prisma.user.findMany() + await prisma.user + .create({ + data: { + email: 'user_2@website.com', + }, + }) + .then() + .catch() + .finally() - expect(users.length).toBe(0) + throw new Error('rollback') }) - /** - * Makes sure that the engine can process when the transaction has locks inside - * Engine PR - https://github.com/prisma/prisma-engines/pull/2811 - * Issue - https://github.com/prisma/prisma/issues/11750 - */ - testIf(provider === 'postgresql')('high concurrency with SET FOR UPDATE', async () => { - jest.setTimeout(60_000) - const CONCURRENCY = 12 - - await prisma.user.create({ - data: { - email: 'x', - name: 'y', - val: 1, - }, - }) + await expect(result).rejects.toThrowErrorMatchingInlineSnapshot(`rollback`) - const promises = [...Array(CONCURRENCY)].map(() => - prisma.$transaction( - async (transactionPrisma) => { - // @ts-test-if: provider !== 'mongodb' - await transactionPrisma.$queryRaw`SELECT id from "User" where email = 'x' FOR UPDATE` - - const user = await transactionPrisma.user.findUnique({ - rejectOnNotFound: true, - where: { - email: 'x', - }, - }) - - // Add a delay here to force the transaction to be open for longer - // this will increase the chance of deadlock in the itx transactions - // if deadlock is a possibility. - await delay(100) - - const updatedUser = await transactionPrisma.user.update({ - where: { - email: 'x', - }, - data: { - val: user.val! + 1, - }, - }) - - return updatedUser - }, - { timeout: 60000, maxWait: 60000 }, - ), - ) + const users = await prisma.user.findMany() - await Promise.allSettled(promises) + expect(users.length).toBe(0) + }) - const finalUser = await prisma.user.findUnique({ - rejectOnNotFound: true, - where: { - email: 'x', - }, - }) + /** + * Makes sure that the engine can process when the transaction has locks inside + * Engine PR - https://github.com/prisma/prisma-engines/pull/2811 + * Issue - https://github.com/prisma/prisma/issues/11750 + */ + testIf(provider === 'postgresql')('high concurrency with SET FOR UPDATE', async () => { + jest.setTimeout(60_000) + const CONCURRENCY = 12 - expect(finalUser.val).toEqual(CONCURRENCY + 1) + await prisma.user.create({ + data: { + email: 'x', + name: 'y', + val: 1, + }, }) - describeIf(provider !== 'mongodb')('isolation levels', () => { - /* eslint-disable jest/no-standalone-expect */ - function testIsolationLevel(title: string, supported: boolean, fn: () => Promise) { - test(title, async () => { - if (supported) { - await fn() - } else { - await expect(fn()).rejects.toThrowError('Invalid enum value') - } - }) - } + const promises = [...Array(CONCURRENCY)].map(() => + prisma.$transaction( + async (transactionPrisma) => { + // @ts-test-if: provider !== 'mongodb' + await transactionPrisma.$queryRaw`SELECT id from "User" where email = 'x' FOR UPDATE` - testIsolationLevel('read committed', provider !== 'sqlite' && provider !== 'cockroachdb', async () => { - await prisma.$transaction( - async (tx) => { - await tx.user.create({ data: { email: 'user@example.com' } }) - }, - { - // @ts-test-if: !['mongodb', 'sqlite', 'cockroachdb'].includes(provider) - isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted, - }, - ) - await expect(prisma.user.findMany()).resolves.toHaveLength(1) - }) + const user = await transactionPrisma.user.findUnique({ + rejectOnNotFound: true, + where: { + email: 'x', + }, + }) - testIsolationLevel('read uncommitted', provider !== 'sqlite' && provider !== 'cockroachdb', async () => { - await prisma.$transaction( - async (tx) => { - await tx.user.create({ data: { email: 'user@example.com' } }) - }, - { - // @ts-test-if: !['mongodb', 'sqlite', 'cockroachdb'].includes(provider) - isolationLevel: Prisma.TransactionIsolationLevel.ReadUncommitted, - }, - ) - await expect(prisma.user.findMany()).resolves.toHaveLength(1) - }) + // Add a delay here to force the transaction to be open for longer + // this will increase the chance of deadlock in the itx transactions + // if deadlock is a possibility. + await delay(100) - testIsolationLevel('repeatable read', provider !== 'sqlite' && provider !== 'cockroachdb', async () => { - await prisma.$transaction( - async (tx) => { - await tx.user.create({ data: { email: 'user@example.com' } }) - }, - { - // @ts-test-if: !['mongodb', 'sqlite', 'cockroachdb'].includes(provider) - isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead, - }, - ) - await expect(prisma.user.findMany()).resolves.toHaveLength(1) - }) + const updatedUser = await transactionPrisma.user.update({ + where: { + email: 'x', + }, + data: { + val: user.val! + 1, + }, + }) - testIsolationLevel('serializable', true, async () => { - await prisma.$transaction( - async (tx) => { - await tx.user.create({ data: { email: 'user@example.com' } }) - }, - { - // @ts-test-if: provider !== 'mongodb' - isolationLevel: Prisma.TransactionIsolationLevel.Serializable, - }, - ) + return updatedUser + }, + { timeout: 60000, maxWait: 60000 }, + ), + ) + + await Promise.allSettled(promises) - await expect(prisma.user.findMany()).resolves.toHaveLength(1) + const finalUser = await prisma.user.findUnique({ + rejectOnNotFound: true, + where: { + email: 'x', + }, + }) + + expect(finalUser.val).toEqual(CONCURRENCY + 1) + }) + + describeIf(provider !== 'mongodb')('isolation levels', () => { + /* eslint-disable jest/no-standalone-expect */ + function testIsolationLevel(title: string, supported: boolean, fn: () => Promise) { + test(title, async () => { + if (supported) { + await fn() + } else { + await expect(fn()).rejects.toThrowError('Invalid enum value') + } }) + } - // TODO: there is also Snapshot level for sqlserver - // it needs to be explicitly enabled on DB level and test setup can't do it at the moment - // ref: https://docs.microsoft.com/en-us/troubleshoot/sql/analysis-services/enable-snapshot-transaction-isolation-level - // testIsolationLevel('snapshot', provider === 'sqlserver', async () => { - // await prisma.$transaction( - // async (tx) => { - // await tx.user.create({ data: { email: 'user@example.com' } }) - // }, - // { - // // @ts-test-if: provider === 'sqlserver' - // isolationLevel: Prisma.TransactionIsolationLevel.Snapshot, - // }, - // ) - - // await expect(prisma.user.findMany()).resolves.toHaveLength(1) - // }) - - test('invalid value', async () => { - // @ts-test-if: provider === 'mongodb' - const result = prisma.$transaction( - async (tx) => { - await tx.user.create({ data: { email: 'user@example.com' } }) - }, - { - // @ts-test-if: provider !== 'mongodb' - isolationLevel: 'NotAValidLevel', - }, - ) + testIsolationLevel('read committed', provider !== 'sqlite' && provider !== 'cockroachdb', async () => { + await prisma.$transaction( + async (tx) => { + await tx.user.create({ data: { email: 'user@example.com' } }) + }, + { + // @ts-test-if: !['mongodb', 'sqlite', 'cockroachdb'].includes(provider) + isolationLevel: Prisma.TransactionIsolationLevel.ReadCommitted, + }, + ) + await expect(prisma.user.findMany()).resolves.toHaveLength(1) + }) - await expect(result).rejects.toMatchObject({ - code: 'P2023', - clientVersion: '0.0.0', - }) + testIsolationLevel('read uncommitted', provider !== 'sqlite' && provider !== 'cockroachdb', async () => { + await prisma.$transaction( + async (tx) => { + await tx.user.create({ data: { email: 'user@example.com' } }) + }, + { + // @ts-test-if: !['mongodb', 'sqlite', 'cockroachdb'].includes(provider) + isolationLevel: Prisma.TransactionIsolationLevel.ReadUncommitted, + }, + ) + await expect(prisma.user.findMany()).resolves.toHaveLength(1) + }) - await expect(result).rejects.toThrowErrorMatchingInlineSnapshot( - `Inconsistent column data: Conversion failed: Invalid isolation level \`NotAValidLevel\``, - ) - }) - /* eslint-enable jest/no-standalone-expect */ + testIsolationLevel('repeatable read', provider !== 'sqlite' && provider !== 'cockroachdb', async () => { + await prisma.$transaction( + async (tx) => { + await tx.user.create({ data: { email: 'user@example.com' } }) + }, + { + // @ts-test-if: !['mongodb', 'sqlite', 'cockroachdb'].includes(provider) + isolationLevel: Prisma.TransactionIsolationLevel.RepeatableRead, + }, + ) + await expect(prisma.user.findMany()).resolves.toHaveLength(1) + }) + + testIsolationLevel('serializable', true, async () => { + await prisma.$transaction( + async (tx) => { + await tx.user.create({ data: { email: 'user@example.com' } }) + }, + { + // @ts-test-if: provider !== 'mongodb' + isolationLevel: Prisma.TransactionIsolationLevel.Serializable, + }, + ) + + await expect(prisma.user.findMany()).resolves.toHaveLength(1) }) - testIf(provider === 'mongodb')('attempt to set isolation level on mongo', async () => { + // TODO: there is also Snapshot level for sqlserver + // it needs to be explicitly enabled on DB level and test setup can't do it at the moment + // ref: https://docs.microsoft.com/en-us/troubleshoot/sql/analysis-services/enable-snapshot-transaction-isolation-level + // testIsolationLevel('snapshot', provider === 'sqlserver', async () => { + // await prisma.$transaction( + // async (tx) => { + // await tx.user.create({ data: { email: 'user@example.com' } }) + // }, + // { + // // @ts-test-if: provider === 'sqlserver' + // isolationLevel: Prisma.TransactionIsolationLevel.Snapshot, + // }, + // ) + + // await expect(prisma.user.findMany()).resolves.toHaveLength(1) + // }) + + test('invalid value', async () => { // @ts-test-if: provider === 'mongodb' const result = prisma.$transaction( async (tx) => { @@ -754,19 +734,36 @@ testMatrix.setupTestSuite( }, { // @ts-test-if: provider !== 'mongodb' - isolationLevel: 'CanBeAnything', + isolationLevel: 'NotAValidLevel', }, ) + await expect(result).rejects.toMatchObject({ + code: 'P2023', + clientVersion: '0.0.0', + }) + await expect(result).rejects.toThrowErrorMatchingInlineSnapshot( - `The current database provider doesn't support a feature that the query used: Mongo does not support setting transaction isolation levels.`, + `Inconsistent column data: Conversion failed: Invalid isolation level \`NotAValidLevel\``, ) }) - }, - { - skipDataProxy: { - runtimes: ['node', 'edge'], - reason: 'Interactive transactions are not supported with Data Proxy yet', - }, - }, -) + /* eslint-enable jest/no-standalone-expect */ + }) + + testIf(provider === 'mongodb')('attempt to set isolation level on mongo', async () => { + // @ts-test-if: provider === 'mongodb' + const result = prisma.$transaction( + async (tx) => { + await tx.user.create({ data: { email: 'user@example.com' } }) + }, + { + // @ts-test-if: provider !== 'mongodb' + isolationLevel: 'CanBeAnything', + }, + ) + + await expect(result).rejects.toThrowErrorMatchingInlineSnapshot( + `The current database provider doesn't support a feature that the query used: Mongo does not support setting transaction isolation levels.`, + ) + }) +}) diff --git a/packages/client/tests/functional/issues/13405-mongo-raw-itx/tests.ts b/packages/client/tests/functional/issues/13405-mongo-raw-itx/tests.ts index 7ca6fa794894..f7d8835991f2 100644 --- a/packages/client/tests/functional/issues/13405-mongo-raw-itx/tests.ts +++ b/packages/client/tests/functional/issues/13405-mongo-raw-itx/tests.ts @@ -126,9 +126,5 @@ testMatrix.setupTestSuite( from: ['sqlite', 'mysql', 'postgresql', 'sqlserver', 'cockroachdb'], reason: 'findRaw, runCommandRaw and aggregateRaw are MongoDB-only APIs', }, - skipDataProxy: { - runtimes: ['node', 'edge'], - reason: 'Interactive transactions are not supported in Data Proxy yet', - }, }, ) diff --git a/packages/client/tests/functional/issues/15044/tests.ts b/packages/client/tests/functional/issues/15044/tests.ts index c00f365c8160..39d6dedb3e51 100644 --- a/packages/client/tests/functional/issues/15044/tests.ts +++ b/packages/client/tests/functional/issues/15044/tests.ts @@ -7,58 +7,50 @@ import type { PrismaClient } from './node_modules/@prisma/client' declare let prisma: PrismaClient // https://github.com/prisma/prisma/issues/15044 -testMatrix.setupTestSuite( - () => { - test('should not throw error when using connect inside transaction', async () => { - const userName = faker.name.firstName() - const walletName = faker.name.firstName() - - const result = await prisma.$transaction(async (tx) => { - const user = await tx.user.create({ - data: { - name: userName, - }, - }) +testMatrix.setupTestSuite(() => { + test('should not throw error when using connect inside transaction', async () => { + const userName = faker.name.firstName() + const walletName = faker.name.firstName() + + const result = await prisma.$transaction(async (tx) => { + const user = await tx.user.create({ + data: { + name: userName, + }, + }) - const wallet = await tx.wallet.create({ - data: { - name: walletName, - }, - }) + const wallet = await tx.wallet.create({ + data: { + name: walletName, + }, + }) - const walletLink = await tx.walletLink.create({ - data: { - name: `${userName}-${walletName}`, - wallet: { - connect: { - id: wallet.id, - }, - }, - user: { - connect: { - id: user.id, - }, + const walletLink = await tx.walletLink.create({ + data: { + name: `${userName}-${walletName}`, + wallet: { + connect: { + id: wallet.id, }, }, - select: { - id: true, - name: true, - wallet: true, - user: true, + user: { + connect: { + id: user.id, + }, }, - }) - - return walletLink + }, + select: { + id: true, + name: true, + wallet: true, + user: true, + }, }) - expect(result.wallet.name).toEqual(walletName) - expect(result.user.name).toEqual(userName) + return walletLink }) - }, - { - skipDataProxy: { - runtimes: ['node', 'edge'], - reason: 'Interactive transactions are not supported with Data Proxy yet', - }, - }, -) + + expect(result.wallet.name).toEqual(walletName) + expect(result.user.name).toEqual(userName) + }) +}) diff --git a/packages/engine-core/src/__tests__/errors.test.ts b/packages/engine-core/src/__tests__/errors.test.ts index ff551de0da97..c290e9b9f61c 100644 --- a/packages/engine-core/src/__tests__/errors.test.ts +++ b/packages/engine-core/src/__tests__/errors.test.ts @@ -5,8 +5,9 @@ import { getErrorMessageWithLink } from '../common/errors/utils/getErrorMessageW import { responseToError } from '../data-proxy/errors/utils/responseToError' import type { RequestResponse } from '../data-proxy/utils/request' -const response = (body: Promise, code?: number, requestId?: string): RequestResponse => ({ - json: () => body, +const response = (body: string, code?: number, requestId?: string): RequestResponse => ({ + json: () => Promise.resolve(body), + text: () => body, url: '', ok: false, status: code || 400, @@ -20,7 +21,7 @@ describe('responseToError', () => { expect.assertions(2) try { - await responseToError(response(Promise.reject(), 500), '') + await responseToError(response('', 500), '') } catch (error) { expect(error.message).toEqual('Unknown server error') expect(error.logs).toBe(undefined) @@ -32,18 +33,36 @@ describe('responseToError', () => { const errorJSON = { EngineNotStarted: { - reason: 'VersionNotSupported', + reason: 'EngineVersionNotSupported', }, } try { - await responseToError(response(Promise.resolve(errorJSON), 500), '') + await responseToError(response(JSON.stringify(errorJSON), 500), '') } catch (error) { - expect(error.message).toEqual('VersionNotSupported') + expect(error.message).toEqual('Engine version is not supported') expect(error.logs).toBe(undefined) } }) + test('serialization of 500 with wrong shape', async () => { + expect.assertions(1) + + const errorJSON = { + EngineNotStarted: { + reason: 'ILikeButterflies', + }, + } + + try { + await responseToError(response(JSON.stringify(errorJSON), 500), '') + } catch (error) { + expect(error.message).toEqual( + 'Unknown server error: {"type":"UnknownJsonError","body":{"EngineNotStarted":{"reason":"ILikeButterflies"}}}', + ) + } + }) + test('serialization of 500 with engine logs', async () => { expect.assertions(2) @@ -61,9 +80,9 @@ describe('responseToError', () => { } try { - await responseToError(response(Promise.resolve(errorJSON), 500), '') + await responseToError(response(JSON.stringify(errorJSON), 500), '') } catch (error) { - expect(error.message).toEqual('HealthcheckTimeout') + expect(error.message).toEqual('Engine not started: healthcheck timeout') expect(error.logs).toEqual([ '{"timestamp":"2022-04-14T12:01:00.487760Z","level":"INFO","fields":{"message":"Encountered error during initialization:"},"target":"query_engine"}\r\n', '{"is_panic":false,"message":"Database error. error code: unknown, error message: Server selection timeout: No available servers. Topology: { Type: ReplicaSetNoPrimary, Servers: [ { Address: test-shard-00-00.abc.mongodb.net:27017, Type: Unknown, Error: Connection reset by peer (os error 104) }, { Address: test-shard-00-01.abc.mongodb.net:27017, Type: Unknown, Error: Connection reset by peer (os error 104) }, { Address: test-shard-00-02.abc.mongodb.net:27017, Type: Unknown, Error: Connection reset by peer (os error 104) }, ] }","backtrace":" 0: user_facing_errors::Error::new_non_panic_with_current_backtrace\\n 1: query_engine::error:: for user_facing_errors::Error>::from\\n 2: query_engine::error::PrismaError::render_as_json\\n 3: query_engine::main::main::{{closure}}::{{closure}}\\n 4: as core::future::future::Future>::poll\\n 5: std::thread::local::LocalKey::with\\n 6: as core::future::future::Future>::poll\\n 7: async_io::driver::block_on\\n 8: std::thread::local::LocalKey::with\\n 9: std::thread::local::LocalKey::with\\n 10: async_std::task::builder::Builder::blocking\\n 11: query_engine::main\\n 12: std::sys_common::backtrace::__rust_begin_short_backtrace\\n 13: std::rt::lang_start::{{closure}}\\n 14: core::ops::function::impls:: for &F>::call_once\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/core/src/ops/function.rs:259:13\\n std::panicking::try::do_call\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:403:40\\n std::panicking::try\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:367:19\\n std::panic::catch_unwind\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panic.rs:133:14\\n std::rt::lang_start_internal::{{closure}}\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/rt.rs:128:48\\n std::panicking::try::do_call\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:403:40\\n std::panicking::try\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panicking.rs:367:19\\n std::panic::catch_unwind\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/panic.rs:133:14\\n std::rt::lang_start_internal\\n at /rustc/f1edd0429582dd29cccacaf50fd134b05593bd9c/library/std/src/rt.rs:128:20\\n 15: main\\n 16: __libc_start_main\\n 17: \\n"}\r\n', @@ -75,7 +94,7 @@ describe('responseToError', () => { expect.assertions(2) try { - await responseToError(response(Promise.reject()), '') + await responseToError(response(''), '') } catch (error) { expect(error.message).toEqual('This request could not be understood by the server') expect(error.code).toEqual('P5000') @@ -83,7 +102,7 @@ describe('responseToError', () => { }) test('serialization of 400 includes original cause from data proxy if it is a known error', async () => { - expect.assertions(2) + expect.assertions(3) const errorJSON = { EngineNotStarted: { @@ -97,12 +116,13 @@ describe('responseToError', () => { } try { - await responseToError(response(Promise.resolve(errorJSON)), '') + await responseToError(response(JSON.stringify(errorJSON)), '') } catch (error) { + expect(error.constructor.name).toEqual('PrismaClientInitializationError') expect(error.message).toEqual( 'Authentication failed against database server at `my-database.random-id.eu-west-1.rds.amazonaws.com`, the provided database credentials for `username` are not valid.\n\nPlease make sure to provide valid database credentials for the database server at `my-database.random-id.eu-west-1.rds.amazonaws.com`.', ) - expect(error.code).toEqual('P1000') + expect(error.errorCode).toEqual('P1000') } }) @@ -115,7 +135,7 @@ describe('responseToError', () => { }, } - const error = await responseToError(response(Promise.resolve(errorJSON), 404, 'some-request-id'), '') + const error = await responseToError(response(JSON.stringify(errorJSON), 404, 'some-request-id'), '') if (error) { expect(error.message).toEqual('Schema needs to be uploaded (The request id was: some-request-id)') } diff --git a/packages/engine-core/src/binary/BinaryEngine.ts b/packages/engine-core/src/binary/BinaryEngine.ts index 11c9dfe300b9..d81b94c34be0 100644 --- a/packages/engine-core/src/binary/BinaryEngine.ts +++ b/packages/engine-core/src/binary/BinaryEngine.ts @@ -9,7 +9,6 @@ import { spawn } from 'child_process' import EventEmitter from 'events' import execa from 'execa' import fs from 'fs' -import type { IncomingHttpHeaders } from 'http' import net from 'net' import pRetry from 'p-retry' import path from 'path' @@ -23,6 +22,7 @@ import type { EngineConfig, EngineEventType, GetConfigResult, + InteractiveTransactionOptions, } from '../common/Engine' import { Engine } from '../common/Engine' import { PrismaClientInitializationError } from '../common/errors/PrismaClientInitializationError' @@ -43,10 +43,11 @@ import type { } from '../common/types/QueryEngine' import type * as Tx from '../common/types/Transaction' import { printGeneratorConfig } from '../common/utils/printGeneratorConfig' +import { runtimeHeadersToHttpHeaders } from '../common/utils/runtimeHeadersToHttpHeaders' import { fixBinaryTargets, plusX } from '../common/utils/util' import byline from '../tools/byline' import { omit } from '../tools/omit' -import { createSpan, getTraceParent, runInChildSpan } from '../tracing' +import { createSpan, runInChildSpan } from '../tracing' import { TracingConfig } from '../tracing/getTracingConfig' import type { Result } from './Connection' import { Connection } from './Connection' @@ -931,9 +932,15 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. return this.lastVersion } - async request(query: string, headers: QueryEngineRequestHeaders = {}, numTry = 1): Promise> { + async request( + query: string, + headers: QueryEngineRequestHeaders = {}, + _transaction?: InteractiveTransactionOptions, + numTry = 1, + ): Promise> { await this.start() + // TODO: we don't need the transactionId "runtime header" anymore, we can use the txInfo object here this.currentRequestPromise = this.connection.post('/', stringifyQuery(query), runtimeHeadersToHttpHeaders(headers)) this.lastQuery = query @@ -964,7 +971,7 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. // retry if (numTry <= MAX_REQUEST_RETRIES) { logger('trying a retry now') - return this.request(query, headers, numTry + 1) + return this.request(query, headers, _transaction, numTry + 1) } } @@ -1027,9 +1034,9 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. * @param options to change the default timeouts * @param info transaction information for the QE */ - async transaction(action: 'start', headers: Tx.TransactionHeaders, options?: Tx.Options): Promise - async transaction(action: 'commit', headers: Tx.TransactionHeaders, info: Tx.Info): Promise - async transaction(action: 'rollback', headers: Tx.TransactionHeaders, info: Tx.Info): Promise + async transaction(action: 'start', headers: Tx.TransactionHeaders, options?: Tx.Options): Promise> + async transaction(action: 'commit', headers: Tx.TransactionHeaders, info: Tx.Info): Promise + async transaction(action: 'rollback', headers: Tx.TransactionHeaders, info: Tx.Info): Promise async transaction(action: any, headers: Tx.TransactionHeaders, arg?: any) { await this.start() @@ -1041,7 +1048,11 @@ You very likely have the wrong "binaryTarget" defined in the schema.prisma file. }) const result = await Connection.onHttpError( - this.connection.post('/transaction/start', jsonOptions, runtimeHeadersToHttpHeaders(headers)), + this.connection.post>( + '/transaction/start', + jsonOptions, + runtimeHeadersToHttpHeaders(headers), + ), (result) => this.transactionHttpErrorHandler(result), ) @@ -1244,26 +1255,6 @@ function initHooks() { } } -/** - * Takes runtime data headers and turns it into QE HTTP headers - * @param headers to transform - * @returns - */ -function runtimeHeadersToHttpHeaders(headers: QueryEngineRequestHeaders): IncomingHttpHeaders { - return Object.keys(headers).reduce((acc, runtimeHeaderKey) => { - let httpHeaderKey = runtimeHeaderKey - - if (runtimeHeaderKey === 'transactionId') { - httpHeaderKey = 'X-transaction-id' - } - - // if header key isn't changed, a copy happens - acc[httpHeaderKey] = headers[runtimeHeaderKey] - - return acc - }, {} as IncomingHttpHeaders) -} - function killProcessAndWait(childProcess: ChildProcess): Promise { return new Promise((resolve) => { childProcess.once('exit', resolve) diff --git a/packages/engine-core/src/common/Engine.ts b/packages/engine-core/src/common/Engine.ts index c6dca9462ff1..a9f7110fd9b4 100644 --- a/packages/engine-core/src/common/Engine.ts +++ b/packages/engine-core/src/common/Engine.ts @@ -22,6 +22,8 @@ export type BatchTransactionOptions = { isolationLevel?: Transaction.IsolationLevel } +export type InteractiveTransactionOptions = Transaction.Info + // TODO Move shared logic in here export abstract class Engine { abstract on(event: EngineEventType, listener: (args?: any) => any): void @@ -33,6 +35,7 @@ export abstract class Engine { abstract request( query: string, headers?: QueryEngineRequestHeaders, + transaction?: InteractiveTransactionOptions, numTry?: number, ): Promise> abstract requestBatch( @@ -45,12 +48,16 @@ export abstract class Engine { action: 'start', headers: Transaction.TransactionHeaders, options?: Transaction.Options, - ): Promise - abstract transaction(action: 'commit', headers: Transaction.TransactionHeaders, info: Transaction.Info): Promise + ): Promise> + abstract transaction( + action: 'commit', + headers: Transaction.TransactionHeaders, + info: Transaction.Info, + ): Promise abstract transaction( action: 'rollback', headers: Transaction.TransactionHeaders, - info: Transaction.Info, + info: Transaction.Info, ): Promise abstract metrics(options: MetricsOptionsJson): Promise diff --git a/packages/engine-core/src/common/types/Transaction.ts b/packages/engine-core/src/common/types/Transaction.ts index a2bfd6705f3c..ea84055d6d16 100644 --- a/packages/engine-core/src/common/types/Transaction.ts +++ b/packages/engine-core/src/common/types/Transaction.ts @@ -16,8 +16,18 @@ export type Options = { isolationLevel?: IsolationLevel } -export type Info = { +export type Info = { + /** + * Transaction ID returned by the query engine. + */ id: string + + /** + * Arbitrary payload the meaning of which depends on the `Engine` implementation. + * For example, `DataProxyEngine` needs to associate different API endpoints with transactions. + * In `LibraryEngine` and `BinaryEngine` it is currently not used. + */ + payload: Payload } export type TransactionHeaders = { diff --git a/packages/engine-core/src/common/utils/runtimeHeadersToHttpHeaders.ts b/packages/engine-core/src/common/utils/runtimeHeadersToHttpHeaders.ts new file mode 100644 index 000000000000..a71dd40dc921 --- /dev/null +++ b/packages/engine-core/src/common/utils/runtimeHeadersToHttpHeaders.ts @@ -0,0 +1,15 @@ +import { QueryEngineRequestHeaders } from '../types/QueryEngine' + +/** + * Takes runtime data headers and turns it into QE HTTP headers + * @param headers to transform + * @returns + */ +export function runtimeHeadersToHttpHeaders(headers: QueryEngineRequestHeaders): Record { + if (headers.transactionId) { + const { transactionId, ...httpHeaders } = headers + httpHeaders['X-transaction-id'] = transactionId + return httpHeaders + } + return headers +} diff --git a/packages/engine-core/src/data-proxy/DataProxyEngine.ts b/packages/engine-core/src/data-proxy/DataProxyEngine.ts index 67a241788b6a..93f40f2d0ec8 100644 --- a/packages/engine-core/src/data-proxy/DataProxyEngine.ts +++ b/packages/engine-core/src/data-proxy/DataProxyEngine.ts @@ -8,11 +8,15 @@ import type { EngineEventType, GetConfigResult, InlineDatasource, + InteractiveTransactionOptions, } from '../common/Engine' import { Engine } from '../common/Engine' +import { PrismaClientUnknownRequestError } from '../common/errors/PrismaClientUnknownRequestError' import { prismaGraphQLToJSError } from '../common/errors/utils/prismaGraphQLToJSError' import { EngineMetricsOptions, Metrics, MetricsOptionsJson, MetricsOptionsPrometheus } from '../common/types/Metrics' -import { QueryEngineBatchRequest } from '../common/types/QueryEngine' +import { QueryEngineBatchRequest, QueryEngineRequestHeaders, QueryEngineResult } from '../common/types/QueryEngine' +import type * as Tx from '../common/types/Transaction' +import { runtimeHeadersToHttpHeaders } from '../common/utils/runtimeHeadersToHttpHeaders' import { DataProxyError } from './errors/DataProxyError' import { ForcedRetryError } from './errors/ForcedRetryError' import { InvalidDatasourceError } from './errors/InvalidDatasourceError' @@ -30,6 +34,12 @@ const P = Promise.resolve() const debug = Debug('prisma:client:dataproxyEngine') +type DataProxyTxInfoPayload = { + endpoint: string +} + +type DataProxyTxInfo = Tx.Info + export class DataProxyEngine extends Engine { private inlineSchema: string readonly inlineSchemaHash: string @@ -130,18 +140,22 @@ export class DataProxyEngine extends Engine { } } - request(query: string, headers: Record, attempt = 0) { + request( + query: string, + headers: QueryEngineRequestHeaders = {}, + transaction?: InteractiveTransactionOptions, + ): Promise> { this.logEmitter.emit('query', { query }) - return this.requestInternal({ query, variables: {} }, headers, attempt) + // TODO: `elapsed`? + return this.requestInternal({ query, variables: {} }, headers, transaction) } async requestBatch( queries: string[], - headers: Record, + headers: QueryEngineRequestHeaders = {}, transaction?: BatchTransactionOptions, - attempt = 0, - ) { + ): Promise[]> { const isTransaction = Boolean(transaction) this.logEmitter.emit('query', { query: `Batch${isTransaction ? ' in transaction' : ''} (${queries.length}):\n${queries.join('\n')}`, @@ -153,76 +167,119 @@ export class DataProxyEngine extends Engine { isolationLevel: transaction?.isolationLevel, } - const { batchResult } = await this.requestInternal(body, headers, attempt) + const { batchResult } = await this.requestInternal(body, headers) + // TODO: add elapsed to each result similar to BinaryEngine + // also check that the error handling is correct for batch return batchResult } - private async requestInternal(body: Record, headers: Record, attempt: number) { - try { - this.logEmitter.emit('info', { - message: `Calling ${await this.url('graphql')} (n=${attempt})`, - }) + private requestInternal( + body: Record, + headers: QueryEngineRequestHeaders, + itx?: InteractiveTransactionOptions, + ): Promise[] } : QueryEngineResult> { + return this.withRetry({ + actionGerund: 'querying', + callback: async ({ logHttpCall }) => { + const url = itx ? `${itx.payload.endpoint}/graphql` : await this.url('graphql') + + logHttpCall(url) + + const response = await request(url, { + method: 'POST', + headers: { ...runtimeHeadersToHttpHeaders(headers), ...this.headers }, + body: JSON.stringify(body), + clientVersion: this.clientVersion, + }) - const response = await request(await this.url('graphql'), { - method: 'POST', - headers: { ...headers, ...this.headers }, - body: JSON.stringify(body), - clientVersion: this.clientVersion, - }) + if (!response.ok) { + debug('graphql response status', response.status) + } - if (!response.ok) { - debug('graphql response status', response.status) - } + const e = await responseToError(response, this.clientVersion) + await this.handleError(e) - const e = await responseToError(response, this.clientVersion) + const data = await response.json() - if (e instanceof SchemaMissingError) { - await this.uploadSchema() - throw new ForcedRetryError({ - clientVersion: this.clientVersion, - cause: e, - }) - } + // TODO: headers contain `x-elapsed` and it needs to be returned - if (e) throw e + if (data.errors) { + if (data.errors.length === 1) { + throw prismaGraphQLToJSError(data.errors[0], this.config.clientVersion!) + } else { + throw new PrismaClientUnknownRequestError(data.errors, this.config.clientVersion!) + } + } - const data = await response.json() + return data + }, + }) + } - if (data.errors) { - if (data.errors.length === 1) { - throw prismaGraphQLToJSError(data.errors[0], this.config.clientVersion!) - } - } + /** + * Send START, COMMIT, or ROLLBACK to the Query Engine + * @param action START, COMMIT, or ROLLBACK + * @param headers headers for tracing + * @param options to change the default timeouts + * @param info transaction information for the QE + */ + transaction(action: 'start', headers: Tx.TransactionHeaders, options?: Tx.Options): Promise + transaction(action: 'commit', headers: Tx.TransactionHeaders, info: DataProxyTxInfo): Promise + transaction(action: 'rollback', headers: Tx.TransactionHeaders, info: DataProxyTxInfo): Promise + async transaction(action: any, headers: Tx.TransactionHeaders, arg?: any) { + const actionToGerund = { + start: 'starting', + commit: 'committing', + rollback: 'rolling back', + } - return data - } catch (e) { - this.logEmitter.emit('error', { - message: `Error while querying: ${e.message ?? '(unknown)'}`, - }) + return this.withRetry({ + actionGerund: `${actionToGerund[action]} transaction`, + callback: async ({ logHttpCall }) => { + if (action === 'start') { + const body = JSON.stringify({ + max_wait: arg?.maxWait ?? 2000, // default + timeout: arg?.timeout ?? 5000, // default + isolation_level: arg?.isolationLevel, + }) + + const url = await this.url('transaction/start') + + logHttpCall(url) + + const response = await request(url, { + method: 'POST', + headers: { ...runtimeHeadersToHttpHeaders(headers), ...this.headers }, + body, + clientVersion: this.clientVersion, + }) - if (!(e instanceof DataProxyError)) throw e - if (!e.isRetryable) throw e - if (attempt >= MAX_RETRIES) { - if (e instanceof ForcedRetryError) { - throw e.cause + const err = await responseToError(response, this.clientVersion) + await this.handleError(err) + + const json = await response.json() + const id = json.id as string + const endpoint = json['data-proxy'].endpoint as string + + return { id, payload: { endpoint } } } else { - throw e - } - } + const url = `${arg.payload.endpoint}/${action}` - this.logEmitter.emit('warn', { message: 'This request can be retried' }) - const delay = await backOff(attempt) - this.logEmitter.emit('warn', { message: `Retrying after ${delay}ms` }) + logHttpCall(url) - return this.requestInternal(body, headers, attempt + 1) - } - } + const response = await request(url, { + method: 'POST', + headers: { ...runtimeHeadersToHttpHeaders(headers), ...this.headers }, + clientVersion: this.clientVersion, + }) - // TODO: figure out how to support transactions - transaction(): Promise { - throw new NotImplementedYetError('Interactive transactions are not yet supported', { - clientVersion: this.clientVersion, + const err = await responseToError(response, this.clientVersion) + await this.handleError(err) + + return undefined + } + }, }) } @@ -318,4 +375,51 @@ export class DataProxyEngine extends Engine { clientVersion: this.clientVersion, }) } + + private async withRetry(args: { + callback: (api: { logHttpCall: (url: string) => void }) => Promise + actionGerund: string + }): Promise { + for (let attempt = 0; ; attempt++) { + const logHttpCall = (url: string) => { + this.logEmitter.emit('info', { + message: `Calling ${url} (n=${attempt})`, + }) + } + + try { + return await args.callback({ logHttpCall }) + } catch (e) { + this.logEmitter.emit('error', { + message: `Error while ${args.actionGerund}: ${e.message ?? '(unknown)'}`, + }) + + if (!(e instanceof DataProxyError)) throw e + if (!e.isRetryable) throw e + if (attempt >= MAX_RETRIES) { + if (e instanceof ForcedRetryError) { + throw e.cause + } else { + throw e + } + } + + this.logEmitter.emit('warn', { message: 'This request can be retried' }) + const delay = await backOff(attempt) + this.logEmitter.emit('warn', { message: `Retrying after ${delay}ms` }) + } + } + } + + private async handleError(error: DataProxyError | undefined): Promise { + if (error instanceof SchemaMissingError) { + await this.uploadSchema() + throw new ForcedRetryError({ + clientVersion: this.clientVersion, + cause: error, + }) + } else if (error) { + throw error + } + } } diff --git a/packages/engine-core/src/data-proxy/errors/BadRequestError.ts b/packages/engine-core/src/data-proxy/errors/BadRequestError.ts index f4f67f4ed946..8b22e4c3b755 100644 --- a/packages/engine-core/src/data-proxy/errors/BadRequestError.ts +++ b/packages/engine-core/src/data-proxy/errors/BadRequestError.ts @@ -4,7 +4,7 @@ import { setRetryable } from './utils/setRetryable' export interface BadRequestErrorInfo extends DataProxyAPIErrorInfo {} -const BAD_REQUEST_DEFAULT_MESSAGE = 'This request could not be understood by the server' +export const BAD_REQUEST_DEFAULT_MESSAGE = 'This request could not be understood by the server' export class BadRequestError extends DataProxyAPIError { public name = 'BadRequestError' diff --git a/packages/engine-core/src/data-proxy/errors/EngineHealthcheckTimeoutError.ts b/packages/engine-core/src/data-proxy/errors/EngineHealthcheckTimeoutError.ts new file mode 100644 index 000000000000..057244d44f7e --- /dev/null +++ b/packages/engine-core/src/data-proxy/errors/EngineHealthcheckTimeoutError.ts @@ -0,0 +1,19 @@ +import type { RequestResponse } from '../utils/request' +import type { DataProxyAPIErrorInfo } from './DataProxyAPIError' +import { DataProxyAPIError } from './DataProxyAPIError' +import { setRetryable } from './utils/setRetryable' + +export interface HealthcheckTimeoutErrorInfo extends DataProxyAPIErrorInfo { + response: RequestResponse +} + +export class HealthcheckTimeoutError extends DataProxyAPIError { + public name = 'HealthcheckTimeoutError' + public code = 'P5013' + public logs: string[] + + constructor(info: HealthcheckTimeoutErrorInfo, logs: string[]) { + super('Engine not started: healthcheck timeout', setRetryable(info, true)) + this.logs = logs + } +} diff --git a/packages/engine-core/src/data-proxy/errors/EngineStartupError.ts b/packages/engine-core/src/data-proxy/errors/EngineStartupError.ts new file mode 100644 index 000000000000..e6f459874e65 --- /dev/null +++ b/packages/engine-core/src/data-proxy/errors/EngineStartupError.ts @@ -0,0 +1,19 @@ +import type { RequestResponse } from '../utils/request' +import type { DataProxyAPIErrorInfo } from './DataProxyAPIError' +import { DataProxyAPIError } from './DataProxyAPIError' +import { setRetryable } from './utils/setRetryable' + +export interface EngineStartupErrorInfo extends DataProxyAPIErrorInfo { + response: RequestResponse +} + +export class EngineStartupError extends DataProxyAPIError { + public name = 'EngineStartupError' + public code = 'P5014' + public logs: string[] + + constructor(info: EngineStartupErrorInfo, message: string, logs: string[]) { + super(message, setRetryable(info, true)) + this.logs = logs + } +} diff --git a/packages/engine-core/src/data-proxy/errors/EngineVersionNotSupportedError.ts b/packages/engine-core/src/data-proxy/errors/EngineVersionNotSupportedError.ts new file mode 100644 index 000000000000..6f89f86fd1d9 --- /dev/null +++ b/packages/engine-core/src/data-proxy/errors/EngineVersionNotSupportedError.ts @@ -0,0 +1,17 @@ +import type { RequestResponse } from '../utils/request' +import type { DataProxyAPIErrorInfo } from './DataProxyAPIError' +import { DataProxyAPIError } from './DataProxyAPIError' +import { setRetryable } from './utils/setRetryable' + +export interface EngineVersionNotSupportedErrorInfo extends DataProxyAPIErrorInfo { + response: RequestResponse +} + +export class EngineVersionNotSupportedError extends DataProxyAPIError { + public name = 'EngineVersionNotSupportedError' + public code = 'P5012' + + constructor(info: EngineVersionNotSupportedErrorInfo) { + super('Engine version is not supported', setRetryable(info, false)) + } +} diff --git a/packages/engine-core/src/data-proxy/errors/GatewayTimeoutError.ts b/packages/engine-core/src/data-proxy/errors/GatewayTimeoutError.ts index 6ce6f7b792de..773b2bcda45c 100644 --- a/packages/engine-core/src/data-proxy/errors/GatewayTimeoutError.ts +++ b/packages/engine-core/src/data-proxy/errors/GatewayTimeoutError.ts @@ -4,11 +4,13 @@ import { setRetryable } from './utils/setRetryable' export interface GatewayTimeoutErrorInfo extends DataProxyAPIErrorInfo {} +export const GATEWAY_TIMEOUT_DEFAULT_MESSAGE = 'Request timed out' + export class GatewayTimeoutError extends DataProxyAPIError { public name = 'GatewayTimeoutError' public code = 'P5009' - constructor(info: GatewayTimeoutErrorInfo) { - super('Request timed out', setRetryable(info, false)) + constructor(info: GatewayTimeoutErrorInfo, message = GATEWAY_TIMEOUT_DEFAULT_MESSAGE) { + super(message, setRetryable(info, false)) } } diff --git a/packages/engine-core/src/data-proxy/errors/InteractiveTransactionError.ts b/packages/engine-core/src/data-proxy/errors/InteractiveTransactionError.ts new file mode 100644 index 000000000000..b389066a3e68 --- /dev/null +++ b/packages/engine-core/src/data-proxy/errors/InteractiveTransactionError.ts @@ -0,0 +1,19 @@ +import type { RequestResponse } from '../utils/request' +import type { DataProxyAPIErrorInfo } from './DataProxyAPIError' +import { DataProxyAPIError } from './DataProxyAPIError' +import { setRetryable } from './utils/setRetryable' + +export interface InteractiveTransactionErrorInfo extends DataProxyAPIErrorInfo { + response: RequestResponse +} + +export const INTERACTIVE_TRANSACTION_ERROR_DEFAULT_MESSAGE = 'Interactive transaction error' + +export class InteractiveTransactionError extends DataProxyAPIError { + public name = 'InteractiveTransactionError' + public code = 'P5015' + + constructor(info: InteractiveTransactionErrorInfo, message = INTERACTIVE_TRANSACTION_ERROR_DEFAULT_MESSAGE) { + super(message, setRetryable(info, false)) + } +} diff --git a/packages/engine-core/src/data-proxy/errors/InvalidRequestError.ts b/packages/engine-core/src/data-proxy/errors/InvalidRequestError.ts new file mode 100644 index 000000000000..da48abe97f48 --- /dev/null +++ b/packages/engine-core/src/data-proxy/errors/InvalidRequestError.ts @@ -0,0 +1,24 @@ +import type { RequestResponse } from '../utils/request' +import type { DataProxyAPIErrorInfo } from './DataProxyAPIError' +import { DataProxyAPIError } from './DataProxyAPIError' +import { setRetryable } from './utils/setRetryable' + +export interface InvalidRequestErrorInfo extends DataProxyAPIErrorInfo { + response: RequestResponse +} + +export const INVALID_REQUEST_DEFAULT_MESSAGE = 'Request parameters are invalid' + +/** + * Used when the request validation failed. + * The difference from `BadRequestError` is the latter is used when the server couldn't understand the request, + * while this error means the server could understand it but rejected due to some parameters being invalid. + */ +export class InvalidRequestError extends DataProxyAPIError { + public name = 'InvalidRequestError' + public code = 'P5011' + + constructor(info: InvalidRequestErrorInfo, message = INVALID_REQUEST_DEFAULT_MESSAGE) { + super(message, setRetryable(info, false)) + } +} diff --git a/packages/engine-core/src/data-proxy/errors/NotFoundError.ts b/packages/engine-core/src/data-proxy/errors/NotFoundError.ts index 6b30b80fded4..8d2cfe402c97 100644 --- a/packages/engine-core/src/data-proxy/errors/NotFoundError.ts +++ b/packages/engine-core/src/data-proxy/errors/NotFoundError.ts @@ -7,11 +7,13 @@ export interface NotFoundErrorInfo extends DataProxyAPIErrorInfo { response: RequestResponse } +export const NOT_FOUND_DEFAULT_MESSAGE = 'Requested resource does not exist' + export class NotFoundError extends DataProxyAPIError { public name = 'NotFoundError' public code = 'P5003' - constructor(info: NotFoundErrorInfo) { - super('Requested resource does not exist', setRetryable(info, false)) + constructor(info: NotFoundErrorInfo, message = NOT_FOUND_DEFAULT_MESSAGE) { + super(message, setRetryable(info, false)) } } diff --git a/packages/engine-core/src/data-proxy/errors/ServerError.ts b/packages/engine-core/src/data-proxy/errors/ServerError.ts index 079d90aef005..048b2fa6fb21 100644 --- a/packages/engine-core/src/data-proxy/errors/ServerError.ts +++ b/packages/engine-core/src/data-proxy/errors/ServerError.ts @@ -4,7 +4,7 @@ import { setRetryable } from './utils/setRetryable' export interface ServerErrorInfo extends DataProxyAPIErrorInfo {} -const SERVER_ERROR_DEFAULT_MESSAGE = 'Unknown server error' +export const SERVER_ERROR_DEFAULT_MESSAGE = 'Unknown server error' export class ServerError extends DataProxyAPIError { public name = 'ServerError' diff --git a/packages/engine-core/src/data-proxy/errors/UnauthorizedError.ts b/packages/engine-core/src/data-proxy/errors/UnauthorizedError.ts index e190e57dba5f..3fc3aaedde3e 100644 --- a/packages/engine-core/src/data-proxy/errors/UnauthorizedError.ts +++ b/packages/engine-core/src/data-proxy/errors/UnauthorizedError.ts @@ -4,11 +4,13 @@ import { setRetryable } from './utils/setRetryable' export interface UnauthorizedErrorInfo extends DataProxyAPIErrorInfo {} +export const UNAUTHORIZED_DEFAULT_MESSAGE = 'Unauthorized, check your connection string' + export class UnauthorizedError extends DataProxyAPIError { public name = 'UnauthorizedError' public code = 'P5007' - constructor(info: UnauthorizedErrorInfo) { - super('Unauthorized, check your connection string', setRetryable(info, false)) + constructor(info: UnauthorizedErrorInfo, message = UNAUTHORIZED_DEFAULT_MESSAGE) { + super(message, setRetryable(info, false)) } } diff --git a/packages/engine-core/src/data-proxy/errors/UsageExceededError.ts b/packages/engine-core/src/data-proxy/errors/UsageExceededError.ts index d033b3159d58..e063947ddbdb 100644 --- a/packages/engine-core/src/data-proxy/errors/UsageExceededError.ts +++ b/packages/engine-core/src/data-proxy/errors/UsageExceededError.ts @@ -4,11 +4,13 @@ import { setRetryable } from './utils/setRetryable' export interface UsageExceededErrorInfo extends DataProxyAPIErrorInfo {} +export const USAGE_EXCEEDED_DEFAULT_MESSAGE = 'Usage exceeded, retry again later' + export class UsageExceededError extends DataProxyAPIError { public name = 'UsageExceededError' public code = 'P5008' - constructor(info: UsageExceededErrorInfo) { - super('Usage exceeded, retry again later', setRetryable(info, true)) + constructor(info: UsageExceededErrorInfo, message = USAGE_EXCEEDED_DEFAULT_MESSAGE) { + super(message, setRetryable(info, true)) } } diff --git a/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts b/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts index 33ba3dee85cf..7123092a0c9f 100644 --- a/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts +++ b/packages/engine-core/src/data-proxy/errors/utils/responseToError.ts @@ -1,12 +1,89 @@ +import { PrismaClientInitializationError } from '../../../common/errors/PrismaClientInitializationError' +import { PrismaClientKnownRequestError } from '../../../common/errors/PrismaClientKnownRequestError' import type { RequestResponse } from '../../utils/request' -import { BadRequestError } from '../BadRequestError' +import { BAD_REQUEST_DEFAULT_MESSAGE, BadRequestError } from '../BadRequestError' import type { DataProxyError } from '../DataProxyError' -import { GatewayTimeoutError } from '../GatewayTimeoutError' -import { NotFoundError } from '../NotFoundError' +import { HealthcheckTimeoutError } from '../EngineHealthcheckTimeoutError' +import { EngineStartupError } from '../EngineStartupError' +import { EngineVersionNotSupportedError } from '../EngineVersionNotSupportedError' +import { GATEWAY_TIMEOUT_DEFAULT_MESSAGE, GatewayTimeoutError } from '../GatewayTimeoutError' +import { InteractiveTransactionError } from '../InteractiveTransactionError' +import { InvalidRequestError } from '../InvalidRequestError' +import { NOT_FOUND_DEFAULT_MESSAGE, NotFoundError } from '../NotFoundError' import { SchemaMissingError } from '../SchemaMissingError' -import { ServerError } from '../ServerError' -import { UnauthorizedError } from '../UnauthorizedError' -import { UsageExceededError } from '../UsageExceededError' +import { SERVER_ERROR_DEFAULT_MESSAGE, ServerError } from '../ServerError' +import { UNAUTHORIZED_DEFAULT_MESSAGE, UnauthorizedError } from '../UnauthorizedError' +import { USAGE_EXCEEDED_DEFAULT_MESSAGE, UsageExceededError } from '../UsageExceededError' + +type DataProxyHttpError = + | 'InternalDataProxyError' + | { EngineNotStarted: { reason: EngineNotStartedReason } } + | { InteractiveTransactionMisrouted: { reason: InteractiveTransactionMisroutedReason } } + | { InvalidRequestError: { reason: string } } + +type EngineNotStartedReason = + | 'SchemaMissing' + | 'EngineVersionNotSupported' + | { EngineStartupError: { msg: string; logs: string[] } } + | { KnownEngineStartupError: { msg: string; error_code: string } } + | { HealthcheckTimeout: { logs: string[] } } + +type InteractiveTransactionMisroutedReason = 'IDParseError' | 'NoQueryEngineFoundError' | 'TransactionStartError' + +type QueryEngineError = { + is_panic: boolean + message: string + error_code: string +} + +type ResponseErrorBody = + | { type: 'DataProxyError'; body: DataProxyHttpError } + | { type: 'QueryEngineError'; body: QueryEngineError } + | { type: 'UnknownJsonError'; body: unknown } + | { type: 'UnknownTextError'; body: string } + | { type: 'EmptyError' } + +async function getResponseErrorBody(response: RequestResponse): Promise { + let text: string + + try { + // eslint-disable-next-line @typescript-eslint/await-thenable + text = await response.text() + } catch { + return { type: 'EmptyError' } + } + + try { + const error = JSON.parse(text) + + if (typeof error === 'string') { + switch (error) { + case 'InternalDataProxyError': + return { type: 'DataProxyError', body: error } + default: + return { type: 'UnknownTextError', body: error } + } + } + + if (typeof error === 'object' && error !== null) { + if ('is_panic' in error && 'message' in error && 'error_code' in error) { + return { type: 'QueryEngineError', body: error } + } + + if ('EngineNotStarted' in error || 'InteractiveTransactionMisrouted' in error || 'InvalidRequestError' in error) { + const reason = (Object.values(error as object)[0] as any).reason + if (typeof reason === 'string' && !['SchemaMissing', 'EngineVersionNotSupported'].includes(reason)) { + return { type: 'UnknownJsonError', body: error } + } + return { type: 'DataProxyError', body: error } + } + } + + return { type: 'UnknownJsonError', body: error } + } catch { + return text === '' ? { type: 'EmptyError' } : { type: 'UnknownTextError', body: text } + } +} export async function responseToError( response: RequestResponse, @@ -15,68 +92,82 @@ export async function responseToError( if (response.ok) return undefined const info = { clientVersion, response } + const error = await getResponseErrorBody(response) + + if (error.type === 'QueryEngineError') { + throw new PrismaClientKnownRequestError(error.body.message, error.body.error_code, clientVersion) + } + + if (error.type === 'DataProxyError') { + if (error.body === 'InternalDataProxyError') { + throw new ServerError(info, 'Internal Data Proxy error') + } - // Explicitly handle 400 errors which contain known errors - if (response.status === 400) { - let knownError - try { - const body = await response.json() - knownError = body?.EngineNotStarted?.reason?.KnownEngineStartupError - } catch (_) {} + if ('EngineNotStarted' in error.body) { + if (error.body.EngineNotStarted.reason === 'SchemaMissing') { + return new SchemaMissingError(info) + } + if (error.body.EngineNotStarted.reason === 'EngineVersionNotSupported') { + throw new EngineVersionNotSupportedError(info) + } + if ('EngineStartupError' in error.body.EngineNotStarted.reason) { + const { msg, logs } = error.body.EngineNotStarted.reason.EngineStartupError + throw new EngineStartupError(info, msg, logs) + } + if ('KnownEngineStartupError' in error.body.EngineNotStarted.reason) { + const { msg, error_code } = error.body.EngineNotStarted.reason.KnownEngineStartupError + throw new PrismaClientInitializationError(msg, clientVersion, error_code) + } + if ('HealthcheckTimeout' in error.body.EngineNotStarted.reason) { + const { logs } = error.body.EngineNotStarted.reason.HealthcheckTimeout + throw new HealthcheckTimeoutError(info, logs) + } + } + + if ('InteractiveTransactionMisrouted' in error.body) { + const messageByReason: Record = { + IDParseError: 'Could not parse interactive transaction ID', + NoQueryEngineFoundError: 'Could not find Query Engine for the specified host and transaction ID', + TransactionStartError: 'Could not start interactive transaction', + } + throw new InteractiveTransactionError(info, messageByReason[error.body.InteractiveTransactionMisrouted.reason]) + } - if (knownError) { - throw new BadRequestError(info, knownError.msg, knownError.error_code) + if ('InvalidRequestError' in error.body) { + throw new InvalidRequestError(info, error.body.InvalidRequestError.reason) } } - if (response.status === 401) { - throw new UnauthorizedError(info) + if (response.status === 401 || response.status === 403) { + throw new UnauthorizedError(info, buildErrorMessage(UNAUTHORIZED_DEFAULT_MESSAGE, error)) } if (response.status === 404) { - try { - const body = await response.json() - const isSchemaMissing = body?.EngineNotStarted?.reason === 'SchemaMissing' - - return isSchemaMissing ? new SchemaMissingError(info) : new NotFoundError(info) - } catch (err) { - return new NotFoundError(info) - } + return new NotFoundError(info, buildErrorMessage(NOT_FOUND_DEFAULT_MESSAGE, error)) } if (response.status === 429) { - throw new UsageExceededError(info) + throw new UsageExceededError(info, buildErrorMessage(USAGE_EXCEEDED_DEFAULT_MESSAGE, error)) } if (response.status === 504) { - throw new GatewayTimeoutError(info) + throw new GatewayTimeoutError(info, buildErrorMessage(GATEWAY_TIMEOUT_DEFAULT_MESSAGE, error)) } if (response.status >= 500) { - let body - try { - body = await response.json() - } catch (err) { - throw new ServerError(info) - } - - if (typeof body?.EngineNotStarted?.reason === 'string') { - throw new ServerError(info, body.EngineNotStarted.reason) - } else if (typeof body?.EngineNotStarted?.reason === 'object') { - const keys = Object.keys(body.EngineNotStarted.reason) - if (keys.length > 0) { - const reason = body.EngineNotStarted.reason - const content = reason[keys[0]] - throw new ServerError(info, keys[0], content.logs) - } - } - - throw new ServerError(info) + throw new ServerError(info, buildErrorMessage(SERVER_ERROR_DEFAULT_MESSAGE, error)) } if (response.status >= 400) { - throw new BadRequestError(info) + throw new BadRequestError(info, buildErrorMessage(BAD_REQUEST_DEFAULT_MESSAGE, error)) } return undefined } + +function buildErrorMessage(defaultMessage: string, errorBody: ResponseErrorBody): string { + if (errorBody.type === 'EmptyError') { + return defaultMessage + } + return `${defaultMessage}: ${JSON.stringify(errorBody)}` +} diff --git a/packages/engine-core/src/data-proxy/utils/getClientVersion.ts b/packages/engine-core/src/data-proxy/utils/getClientVersion.ts index d30b50179f51..168ba31d8983 100644 --- a/packages/engine-core/src/data-proxy/utils/getClientVersion.ts +++ b/packages/engine-core/src/data-proxy/utils/getClientVersion.ts @@ -36,6 +36,15 @@ async function _getClientVersion(config: EngineConfig) { const pkgURL = prismaPkgURL(`<=${major}.${minor}.${patch}`) const res = await request(pkgURL, { clientVersion }) + if (!res.ok) { + throw new Error( + `Failed to fetch stable Prisma version, unpkg.com status ${res.status} ${ + res.statusText + // eslint-disable-next-line @typescript-eslint/await-thenable + }, response body: ${(await res.text()) || ''}`, + ) + } + // we need to await for edge workers // because it's using the global "fetch" // eslint-disable-next-line @typescript-eslint/await-thenable diff --git a/packages/engine-core/src/library/LibraryEngine.ts b/packages/engine-core/src/library/LibraryEngine.ts index 0c82ede916fa..6f1ea2856a5f 100644 --- a/packages/engine-core/src/library/LibraryEngine.ts +++ b/packages/engine-core/src/library/LibraryEngine.ts @@ -120,9 +120,9 @@ export class LibraryEngine extends Engine { } } - async transaction(action: 'start', headers: Tx.TransactionHeaders, options?: Tx.Options): Promise - async transaction(action: 'commit', headers: Tx.TransactionHeaders, info: Tx.Info): Promise - async transaction(action: 'rollback', headers: Tx.TransactionHeaders, info: Tx.Info): Promise + async transaction(action: 'start', headers: Tx.TransactionHeaders, options?: Tx.Options): Promise> + async transaction(action: 'commit', headers: Tx.TransactionHeaders, info: Tx.Info): Promise + async transaction(action: 'rollback', headers: Tx.TransactionHeaders, info: Tx.Info): Promise async transaction(action: any, headers: Tx.TransactionHeaders, arg?: any) { await this.start() @@ -154,7 +154,7 @@ export class LibraryEngine extends Engine { ) } - return response as Tx.Info | undefined + return response as Tx.Info | undefined } private async instantiateLibrary(): Promise { @@ -442,11 +442,7 @@ You may have to run ${chalk.greenBright('prisma generate')} for your changes to return this.library?.debugPanic(message) as Promise } - async request( - query: string, - headers: QueryEngineRequestHeaders = {}, - numTry = 1, - ): Promise<{ data: T; elapsed: number }> { + async request(query: string, headers: QueryEngineRequestHeaders = {}): Promise<{ data: T; elapsed: number }> { debug(`sending request, this.libraryStarted: ${this.libraryStarted}`) const request: QueryEngineRequest = { query, variables: {} } const headerStr = JSON.stringify(headers) // object equivalent to http headers for the library diff --git a/packages/internals/src/__tests__/getGenerators/getGenerators.test.ts b/packages/internals/src/__tests__/getGenerators/getGenerators.test.ts index 7edcb1c16e1b..9282016e1e57 100644 --- a/packages/internals/src/__tests__/getGenerators/getGenerators.test.ts +++ b/packages/internals/src/__tests__/getGenerators/getGenerators.test.ts @@ -759,39 +759,6 @@ describe('getGenerators', () => { expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(`""`) }) - test('fail if dataProxy and interactiveTransactions are used together - prisma-client-js - postgres', async () => { - expect.assertions(5) - const aliases = { - 'predefined-generator': { - generatorPath: generatorPath, - outputPath: __dirname, - }, - } - - try { - await getGenerators({ - schemaPath: path.join(__dirname, 'proxy-and-interactiveTransactions-client-js.prisma'), - providerAliases: aliases, - skipDownload: true, - dataProxy: true, - }) - } catch (e) { - expect(stripAnsi(e.message)).toMatchInlineSnapshot(` - " - interactiveTransactions preview feature is not yet available with --data-proxy. - Please remove interactiveTransactions from the previewFeatures in your schema. - - More information about Data Proxy: https://pris.ly/d/data-proxy - " - `) - } - - expect(ctx.mocked['console.log'].mock.calls.join('\n')).toMatchInlineSnapshot(`""`) - expect(ctx.mocked['console.info'].mock.calls.join('\n')).toMatchInlineSnapshot(`""`) - expect(ctx.mocked['console.warn'].mock.calls.join('\n')).toMatchInlineSnapshot(`""`) - expect(ctx.mocked['console.error'].mock.calls.join('\n')).toMatchInlineSnapshot(`""`) - }) - test('fail if dataProxy and tracing are used together - prisma-client-js - postgres', async () => { expect.assertions(5) const aliases = { diff --git a/packages/internals/src/__tests__/getGenerators/proxy-and-interactiveTransactions-client-js.prisma b/packages/internals/src/__tests__/getGenerators/proxy-and-interactiveTransactions-client-js.prisma deleted file mode 100644 index 5c24ccfe3d42..000000000000 --- a/packages/internals/src/__tests__/getGenerators/proxy-and-interactiveTransactions-client-js.prisma +++ /dev/null @@ -1,16 +0,0 @@ -generator client { - provider = "prisma-client-js" - previewFeatures = ["interactiveTransactions"] -} - -datasource db { - provider = "postgres" - url = env("DATABASE_URL") -} - -model User { - id String @id - createdAt DateTime @default(now()) - email String @unique - name String? -} diff --git a/packages/internals/src/get-generators/utils/check-feature-flags/checkFeatureFlags.ts b/packages/internals/src/get-generators/utils/check-feature-flags/checkFeatureFlags.ts index 7832c4a2f7d0..415b1c116c0e 100644 --- a/packages/internals/src/get-generators/utils/check-feature-flags/checkFeatureFlags.ts +++ b/packages/internals/src/get-generators/utils/check-feature-flags/checkFeatureFlags.ts @@ -1,6 +1,6 @@ import type { ConfigMetaFormat } from '../../../engine-commands' import { GetGeneratorOptions } from '../../getGenerators' -import { forbiddenPreviewFeatureWithDataProxyFlagMessage } from './forbiddenItxWithProxyFlagMessage' +import { forbiddenPreviewFeatureWithDataProxyFlagMessage } from './forbiddenPreviewFeatureWithProxyFlagMessage' /** * Check feature flags and preview features @@ -8,10 +8,10 @@ import { forbiddenPreviewFeatureWithDataProxyFlagMessage } from './forbiddenItxW * @param options */ export function checkFeatureFlags(config: ConfigMetaFormat, options: GetGeneratorOptions) { - checkForbiddenItxWithDataProxyFlag(config, options) + checkForbiddenFeaturesWithDataProxyFlag(config, options) } -function checkForbiddenItxWithDataProxyFlag(config: ConfigMetaFormat, options: GetGeneratorOptions) { +function checkForbiddenFeaturesWithDataProxyFlag(config: ConfigMetaFormat, options: GetGeneratorOptions) { options.dataProxy === true && config.generators.some((generatorConfig) => { return generatorConfig.previewFeatures.some((feature) => { @@ -22,10 +22,6 @@ function checkForbiddenItxWithDataProxyFlag(config: ConfigMetaFormat, options: G if (feature.toLocaleLowerCase() === 'tracing'.toLocaleLowerCase()) { throw new Error(forbiddenPreviewFeatureWithDataProxyFlagMessage('tracing')) } - - if (feature.toLocaleLowerCase() === 'interactiveTransactions'.toLocaleLowerCase()) { - throw new Error(forbiddenPreviewFeatureWithDataProxyFlagMessage('interactiveTransactions')) - } }) }) } diff --git a/packages/internals/src/get-generators/utils/check-feature-flags/forbiddenItxWithProxyFlagMessage.ts b/packages/internals/src/get-generators/utils/check-feature-flags/forbiddenPreviewFeatureWithProxyFlagMessage.ts similarity index 100% rename from packages/internals/src/get-generators/utils/check-feature-flags/forbiddenItxWithProxyFlagMessage.ts rename to packages/internals/src/get-generators/utils/check-feature-flags/forbiddenPreviewFeatureWithProxyFlagMessage.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 166b372c8455..e276ac156d85 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -240,7 +240,7 @@ importers: '@prisma/instrumentation': workspace:* '@prisma/internals': workspace:* '@prisma/migrate': workspace:* - '@prisma/mini-proxy': 0.2.0 + '@prisma/mini-proxy': 0.3.0 '@swc-node/register': 1.5.4 '@swc/core': 1.3.14 '@swc/jest': 0.2.23 @@ -322,7 +322,7 @@ importers: '@prisma/instrumentation': link:../instrumentation '@prisma/internals': link:../internals '@prisma/migrate': link:../migrate - '@prisma/mini-proxy': 0.2.0 + '@prisma/mini-proxy': 0.3.0 '@swc-node/register': 1.5.4_ldaqumno46ke76prz5kcgkjhhy '@swc/core': 1.3.14 '@swc/jest': 0.2.23_@swc+core@1.3.14 @@ -3209,8 +3209,8 @@ packages: /@prisma/engines-version/4.6.0-53.2e719efb80b56a3f32d18a62489de95bb9c130e3: resolution: {integrity: sha512-0CTnfEuUbLlO6n1fM89ERDbSwI4LoyZn+1OKVSwG+aVqohj34+mXRfwOWIM0ONtYtLGGBpddvQAnAZkg+cgS6g==} - /@prisma/mini-proxy/0.2.0: - resolution: {integrity: sha512-jwaPkbGftRKg6EZBkDNTkeIdYH//v7ra3MpFoKndnuNaUgkbdCLl39m0rHi9IiyMZqWdw9112SgUsiBmcbrWeg==} + /@prisma/mini-proxy/0.3.0: + resolution: {integrity: sha512-Vcp8L5S66qM9aUdolqzwF7FBZUSWSb+PzzOE8ikgCB58Sw8DVS1TZG2KbWNbmMre1e/naxwOIFdovJpO/Jg+Ww==} engines: {node: '>=14.17'} hasBin: true dev: true