From 9ae1105ed50ae9b933f40fbd1b7f7ea41fb56f12 Mon Sep 17 00:00:00 2001 From: tomfrew Date: Wed, 23 Oct 2024 03:32:41 -0400 Subject: [PATCH] feat: function configuration for auto transactions (#1615) Co-authored-by: Dave New --- integration/testdata/audit_logs/tests.test.ts | 25 +-- .../testdata/events_basic/tests.test.ts | 7 +- .../testdata/jobs_permissions/tests.test.ts | 12 +- .../testdata/subscribers_basic/tests.test.ts | 6 +- node/codegen.go | 14 +- node/codegen_test.go | 14 +- .../functions-runtime/src/auditing.test.js | 184 ++++++++---------- packages/functions-runtime/src/database.js | 13 +- packages/functions-runtime/src/handleJob.js | 4 +- .../functions-runtime/src/handleRequest.js | 12 +- .../functions-runtime/src/handleSubscriber.js | 17 +- packages/functions-runtime/src/index.d.ts | 12 ++ .../src/tryExecuteFunction.js | 17 +- .../functions-runtime/src/tryExecuteJob.js | 8 +- .../src/tryExecuteSubscriber.js | 8 +- 15 files changed, 169 insertions(+), 184 deletions(-) diff --git a/integration/testdata/audit_logs/tests.test.ts b/integration/testdata/audit_logs/tests.test.ts index 96011f754..4b871693e 100644 --- a/integration/testdata/audit_logs/tests.test.ts +++ b/integration/testdata/audit_logs/tests.test.ts @@ -558,7 +558,7 @@ test("job function with identity - audit table populated", async () => { expect(weddingUpdateAudit.data.headcount).toEqual(1); }); -test("job function with error and no rollback - audit table is not rolled back", async () => { +test("job function with error and default rollback - audit table is also rolled back", async () => { const identity = await models.identity.create({ email: "keelson@keel.xyz", issuer: "https://keel.so", @@ -594,7 +594,7 @@ test("job function with error and no rollback - audit table is not rolled back", >`SELECT * FROM keel_audit where table_name = 'wedding_invitee'`.execute( useDatabase() ); - expect(inviteesAudits.rows.length).toEqual(4); + expect(inviteesAudits.rows.length).toEqual(3); const keelsonAudit = inviteesAudits.rows.at(0)!; expect(keelsonAudit.id).toHaveLength(27); @@ -617,22 +617,12 @@ test("job function with error and no rollback - audit table is not rolled back", expect(weavetonAudit.identityId).toBeNull(); expect(weavetonAudit.data.id).toEqual(prisma.id); - const keelerDeleteAudit = inviteesAudits.rows.at(3)!; - expect(keelerDeleteAudit.id).toHaveLength(27); - expect(keelerDeleteAudit.tableName).toEqual("wedding_invitee"); - expect(keelerDeleteAudit.op).toEqual("delete"); - expect(keelerDeleteAudit.identityId).toEqual(identity.id); - expect(keelerDeleteAudit.data.id).toEqual(keeler.id); - expect(keelerDeleteAudit.data.firstName).toEqual(keeler.firstName); - expect(keelerDeleteAudit.data.status).toEqual(keeler.status); - expect(keelerDeleteAudit.data.isFamily).toEqual(keeler.isFamily); - const weddingAudits = await sql< Audit >`SELECT * FROM keel_audit where table_name = 'wedding'`.execute( useDatabase() ); - expect(weddingAudits.rows.length).toEqual(2); + expect(weddingAudits.rows.length).toEqual(1); const weddingAudit = weddingAudits.rows.at(0)!; expect(weddingAudit.id).toHaveLength(27); @@ -642,15 +632,6 @@ test("job function with error and no rollback - audit table is not rolled back", expect(weddingAudit.data.id).toEqual(wedding.id); expect(weddingAudit.data.name).toEqual(wedding.name); expect(weddingAudit.data.headcount).toEqual(0); - - const weddingUpdateAudit = weddingAudits.rows.at(1)!; - expect(weddingUpdateAudit.id).toHaveLength(27); - expect(weddingUpdateAudit.tableName).toEqual("wedding"); - expect(weddingUpdateAudit.op).toEqual("update"); - expect(weddingUpdateAudit.identityId).toEqual(identity.id); - expect(weddingUpdateAudit.data.id).toEqual(wedding.id); - expect(weddingUpdateAudit.data.name).toEqual(wedding.name); - expect(weddingUpdateAudit.data.headcount).toEqual(1); }); test("job function using kysely with identity - audit table populated", async () => { diff --git a/integration/testdata/events_basic/tests.test.ts b/integration/testdata/events_basic/tests.test.ts index b1a315851..c5eda48a1 100644 --- a/integration/testdata/events_basic/tests.test.ts +++ b/integration/testdata/events_basic/tests.test.ts @@ -72,11 +72,8 @@ test("events from failed job", async () => { const persons = await models.person.findMany(); - expect(persons).toHaveLength(2); - expect(persons[0].verifiedEmail).toBeTruthy(); - expect(persons[1].verifiedEmail).toBeTruthy(); - expect(persons[0].verifiedUpdate).toBeTruthy(); - expect(persons[1].verifiedUpdate).toBeTruthy(); + // Due to default rollback + expect(persons).toHaveLength(0); }); test("events from failed custom function with rollback", async () => { diff --git a/integration/testdata/jobs_permissions/tests.test.ts b/integration/testdata/jobs_permissions/tests.test.ts index 05587cfc4..119faf60f 100644 --- a/integration/testdata/jobs_permissions/tests.test.ts +++ b/integration/testdata/jobs_permissions/tests.test.ts @@ -211,7 +211,7 @@ test("job - allowed in job code - permitted", async () => { expect(await jobRan(id)).toBeTruthy(); }); -test("job - denied in job code - not permitted without rollback transaction", async () => { +test("job - denied in job code - not permitted with default rollback transaction", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); const identity = await models.identity.create({ email: "keel@keel.so", @@ -222,11 +222,11 @@ test("job - denied in job code - not permitted without rollback transaction", as jobs.withIdentity(identity).manualJobDeniedInCode({ id, denyIt: true }) ).toHaveAuthorizationError(); - // This would be false if a transaction rolled back. - expect(await jobRan(id)).toBeTruthy(); + // This would be true if the transaction didn't roll back. + expect(await jobRan(id)).toBeFalsy(); }); -test("job - exception - internal error without rollback transaction", async () => { +test("job - exception - internal error with default rollback transaction", async () => { const { id } = await models.trackJob.create({ didJobRun: false }); const identity = await models.identity.create({ email: "keel@keel.so", @@ -240,8 +240,8 @@ test("job - exception - internal error without rollback transaction", async () = message: "something bad has happened!", }); - // This would be false if a transaction rolled back. - expect(await jobRan(id)).toBeTruthy(); + // This would be true if the transaction didn't roll back. + expect(await jobRan(id)).toBeFalsy(); }); test("scheduled job - without identity - permitted", async () => { diff --git a/integration/testdata/subscribers_basic/tests.test.ts b/integration/testdata/subscribers_basic/tests.test.ts index 1c6c80649..a6a7dba1a 100644 --- a/integration/testdata/subscribers_basic/tests.test.ts +++ b/integration/testdata/subscribers_basic/tests.test.ts @@ -33,7 +33,7 @@ test("subscriber - mutating field", async () => { expect(updatedMary?.verified).toBeTruthy(); }); -test("subscriber - exception - internal error without rollback transaction", async () => { +test("subscriber - exception - internal error with default rollback transaction", async () => { await models.trackSubscriber.create({ didSubscriberRun: false }); const mary = await models.member.create({ @@ -56,8 +56,8 @@ test("subscriber - exception - internal error without rollback transaction", asy message: "something bad has happened!", }); - // This would be false if a transaction rolled back. - expect(await subscriberRan()).toBeTruthy(); + // This would be true if the transaction didn't roll back. + expect(await subscriberRan()).toBeFalsy(); }); test("subscriber - with env vars - successful", async () => { diff --git a/node/codegen.go b/node/codegen.go index 3ecb38caa..a136c252e 100644 --- a/node/codegen.go +++ b/node/codegen.go @@ -1227,7 +1227,7 @@ func writeFunctionWrapperType(w *codegen.Writer, schema *proto.Schema, model *pr // we use the 'declare' keyword to indicate to the typescript compiler that the function // has already been declared in the underlying vanilla javascript and therefore we are just // decorating existing js code with types. - w.Writef("export declare function %s", casing.ToCamel(action.Name)) + w.Writef("export declare const %s: runtime.FuncWithConfig<{", casing.ToCamel(action.Name)) if action.IsArbitraryFunction() { inputType := action.InputMessageName @@ -1239,14 +1239,14 @@ func writeFunctionWrapperType(w *codegen.Writer, schema *proto.Schema, model *pr w.Write(toCustomFunctionReturnType(model, action, false)) w.Write("): ") w.Write(toCustomFunctionReturnType(model, action, false)) - w.Writeln(";") + w.Writeln("}>;") return } hooksType := fmt.Sprintf("%sHooks", casing.ToCamel(action.Name)) // TODO: void return type here is wrong. It should be the type of the function e.g. (ctx, inputs) => ReturnType - w.Writef("(hooks?: %s): void\n", hooksType) + w.Writef("(hooks?: %s): void}>\n", hooksType) w.Writef("export type %s = ", hooksType) @@ -1321,7 +1321,7 @@ func toCustomFunctionReturnType(model *proto.Model, op *proto.Action, isTestingP } func writeJobFunctionWrapperType(w *codegen.Writer, job *proto.Job) { - w.Writef("export declare function %s", casing.ToCamel(job.Name)) + w.Writef("export declare const %s: runtime.FuncWithConfig<{", casing.ToCamel(job.Name)) inputType := job.InputMessageName @@ -1331,13 +1331,13 @@ func writeJobFunctionWrapperType(w *codegen.Writer, job *proto.Job) { w.Writef("(fn: (ctx: JobContextAPI, inputs: %s) => Promise): Promise", inputType) } - w.Writeln(";") + w.Writeln("}>;") } func writeSubscriberFunctionWrapperType(w *codegen.Writer, subscriber *proto.Subscriber) { - w.Writef("export declare function %s", casing.ToCamel(subscriber.Name)) + w.Writef("export declare const %s: runtime.FuncWithConfig<{", casing.ToCamel(subscriber.Name)) w.Writef("(fn: (ctx: SubscriberContextAPI, event: %s) => Promise): Promise", subscriber.InputMessageName) - w.Writeln(";") + w.Writeln("}>;") } func toActionReturnType(model *proto.Model, op *proto.Action) string { diff --git a/node/codegen_test.go b/node/codegen_test.go index 67a366bcf..5fd6be203 100644 --- a/node/codegen_test.go +++ b/node/codegen_test.go @@ -1789,8 +1789,8 @@ model Member { }` expected := ` -export declare function VerifyEmail(fn: (ctx: SubscriberContextAPI, event: VerifyEmailEvent) => Promise): Promise; -export declare function SendWelcomeEmail(fn: (ctx: SubscriberContextAPI, event: SendWelcomeEmailEvent) => Promise): Promise;` +export declare const VerifyEmail: runtime.FuncWithConfig<{(fn: (ctx: SubscriberContextAPI, event: VerifyEmailEvent) => Promise): Promise}>; +export declare const SendWelcomeEmail: runtime.FuncWithConfig<{(fn: (ctx: SubscriberContextAPI, event: SendWelcomeEmailEvent) => Promise): Promise}>;` runWriterTest(t, schema, expected, func(s *proto.Schema, w *codegen.Writer) { for _, s := range s.Subscribers { @@ -1815,12 +1815,12 @@ job AdHocJobWithInputs { job AdHocJobWithoutInputs { @permission(roles: [Admin]) } -role Admin {} - ` +role Admin {}` + expected := ` -export declare function JobWithoutInputs(fn: (ctx: JobContextAPI) => Promise): Promise; -export declare function AdHocJobWithInputs(fn: (ctx: JobContextAPI, inputs: AdHocJobWithInputsMessage) => Promise): Promise; -export declare function AdHocJobWithoutInputs(fn: (ctx: JobContextAPI) => Promise): Promise;` +export declare const JobWithoutInputs: runtime.FuncWithConfig<{(fn: (ctx: JobContextAPI) => Promise): Promise}>; +export declare const AdHocJobWithInputs: runtime.FuncWithConfig<{(fn: (ctx: JobContextAPI, inputs: AdHocJobWithInputsMessage) => Promise): Promise}>; +export declare const AdHocJobWithoutInputs: runtime.FuncWithConfig<{(fn: (ctx: JobContextAPI) => Promise): Promise}>;` runWriterTest(t, schema, expected, func(s *proto.Schema, w *codegen.Writer) { for _, j := range s.Jobs { diff --git a/packages/functions-runtime/src/auditing.test.js b/packages/functions-runtime/src/auditing.test.js index 7b47252e6..6cbd30aa2 100644 --- a/packages/functions-runtime/src/auditing.test.js +++ b/packages/functions-runtime/src/auditing.test.js @@ -65,23 +65,19 @@ test("auditing - capturing identity id in transaction", async () => { const identityId = request.meta.identity.id; - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.CREATE, // CREATE will ensure a transaction is opened - async ({ transaction }) => { - const row = withAuditContext(request, async () => { - return await personAPI.create({ - id: KSUID.randomSync().string, - name: "James", - }); + const row = await withDatabase(db, true, async ({ transaction }) => { + const row = withAuditContext(request, async () => { + return await personAPI.create({ + id: KSUID.randomSync().string, + name: "James", }); + }); - expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); - expect(await identityIdFromConfigParam(db)).toBeNull(); + expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); + expect(await identityIdFromConfigParam(db)).toBeNull(); - return row; - } - ); + return row; + }); expect(row.name).toEqual("James"); expect(await identityIdFromConfigParam(db)).toBeNull(); @@ -101,23 +97,19 @@ test("auditing - capturing tracing in transaction", async () => { request.meta.tracing.traceparent ).traceId; - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.CREATE, // CREATE will ensure a transaction is opened - async ({ transaction }) => { - const row = withAuditContext(request, async () => { - return await personAPI.create({ - id: KSUID.randomSync().string, - name: "Jim", - }); + const row = await withDatabase(db, true, async ({ transaction }) => { + const row = withAuditContext(request, async () => { + return await personAPI.create({ + id: KSUID.randomSync().string, + name: "Jim", }); + }); - expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); - expect(await traceIdFromConfigParam(db)).toBeNull(); + expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); + expect(await traceIdFromConfigParam(db)).toBeNull(); - return row; - } - ); + return row; + }); expect(row.name).toEqual("Jim"); expect(await traceIdFromConfigParam(db)).toBeNull(); @@ -131,23 +123,19 @@ test("auditing - capturing identity id without transaction", async () => { }, }; - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.GET, // GET will _not_ open a transaction - async ({ sDb }) => { - const row = withAuditContext(request, async () => { - return await personAPI.create({ - id: KSUID.randomSync().string, - name: "James", - }); + const row = await withDatabase(db, false, async ({ sDb }) => { + const row = withAuditContext(request, async () => { + return await personAPI.create({ + id: KSUID.randomSync().string, + name: "James", }); + }); - expect(await identityIdFromConfigParam(sDb)).toBeNull(); - expect(await identityIdFromConfigParam(db)).toBeNull(); + expect(await identityIdFromConfigParam(sDb)).toBeNull(); + expect(await identityIdFromConfigParam(db)).toBeNull(); - return row; - } - ); + return row; + }); expect(row.name).toEqual("James"); expect(await identityIdFromConfigParam(db)).toBeNull(); @@ -163,23 +151,19 @@ test("auditing - capturing tracing without transaction", async () => { }, }; - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.GET, // GET will _not_ open a transaction - async ({ sDb }) => { - const row = withAuditContext(request, async () => { - return await personAPI.create({ - id: KSUID.randomSync().string, - name: "Jim", - }); + const row = await withDatabase(db, false, async ({ sDb }) => { + const row = withAuditContext(request, async () => { + return await personAPI.create({ + id: KSUID.randomSync().string, + name: "Jim", }); + }); - expect(await traceIdFromConfigParam(sDb)).toBeNull(); - expect(await traceIdFromConfigParam(db)).toBeNull(); + expect(await traceIdFromConfigParam(sDb)).toBeNull(); + expect(await traceIdFromConfigParam(db)).toBeNull(); - return row; - } - ); + return row; + }); expect(row.name).toEqual("Jim"); expect(await traceIdFromConfigParam(db)).toBeNull(); @@ -225,23 +209,19 @@ test("auditing - ModelAPI.create", async () => { request.meta.tracing.traceparent ).traceId; - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.CREATE, - async ({ transaction }) => { - const row = withAuditContext(request, async () => { - return await personAPI.create({ - id: KSUID.randomSync().string, - name: "Jake", - }); + const row = await withDatabase(db, true, async ({ transaction }) => { + const row = withAuditContext(request, async () => { + return await personAPI.create({ + id: KSUID.randomSync().string, + name: "Jake", }); + }); - expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); - expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); + expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); + expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); - return row; - } - ); + return row; + }); expect(row.name).toEqual("Jake"); }); @@ -266,20 +246,16 @@ test("auditing - ModelAPI.update", async () => { name: "Jake", }); - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.CREATE, - async ({ transaction }) => { - const row = withAuditContext(request, async () => { - return await personAPI.update({ id: created.id }, { name: "Jim" }); - }); + const row = await withDatabase(db, true, async ({ transaction }) => { + const row = withAuditContext(request, async () => { + return await personAPI.update({ id: created.id }, { name: "Jim" }); + }); - expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); - expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); + expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); + expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); - return row; - } - ); + return row; + }); expect(row.name).toEqual("Jim"); }); @@ -304,20 +280,16 @@ test("auditing - ModelAPI.delete", async () => { name: "Jake", }); - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.CREATE, - async ({ transaction }) => { - const row = withAuditContext(request, async () => { - return await personAPI.delete({ id: created.id }); - }); + const row = await withDatabase(db, true, async ({ transaction }) => { + const row = withAuditContext(request, async () => { + return await personAPI.delete({ id: created.id }); + }); - expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); - expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); + expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); + expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); - return row; - } - ); + return row; + }); expect(row).toEqual(created.id); }); @@ -337,23 +309,19 @@ test("auditing - identity id and trace id fields dropped from result", async () request.meta.tracing.traceparent ).traceId; - const row = await withDatabase( - db, - PROTO_ACTION_TYPES.CREATE, - async ({ transaction }) => { - const row = withAuditContext(request, async () => { - return await personAPI.create({ - id: KSUID.randomSync().string, - name: "Jake", - }); + const row = await withDatabase(db, true, async ({ transaction }) => { + const row = withAuditContext(request, async () => { + return await personAPI.create({ + id: KSUID.randomSync().string, + name: "Jake", }); + }); - expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); - expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); + expect(await identityIdFromConfigParam(transaction)).toEqual(identityId); + expect(await traceIdFromConfigParam(transaction)).toEqual(traceId); - return row; - } - ); + return row; + }); expect(row.name).toEqual("Jake"); expect(row.keelIdentityId).toBeUndefined(); diff --git a/packages/functions-runtime/src/database.js b/packages/functions-runtime/src/database.js index 96f069748..c2524f16d 100644 --- a/packages/functions-runtime/src/database.js +++ b/packages/functions-runtime/src/database.js @@ -14,18 +14,7 @@ const ws = require("ws"); // the user's custom function is wrapped in a transaction so we can rollback // the transaction if something goes wrong. // withDatabase shouldn't be exposed in the public api of the sdk -async function withDatabase(db, actionType, cb) { - let requiresTransaction = true; - - switch (actionType) { - case PROTO_ACTION_TYPES.SUBSCRIBER: - case PROTO_ACTION_TYPES.JOB: - case PROTO_ACTION_TYPES.GET: - case PROTO_ACTION_TYPES.LIST: - requiresTransaction = false; - break; - } - +async function withDatabase(db, requiresTransaction, cb) { // db.transaction() provides a kysely instance bound to a transaction. if (requiresTransaction) { return db.transaction().execute(async (transaction) => { diff --git a/packages/functions-runtime/src/handleJob.js b/packages/functions-runtime/src/handleJob.js index b9daf6306..4ec8f6b92 100644 --- a/packages/functions-runtime/src/handleJob.js +++ b/packages/functions-runtime/src/handleJob.js @@ -57,8 +57,10 @@ async function handleJob(request, config) { const jobFunction = jobs[request.method]; const actionType = PROTO_ACTION_TYPES.JOB; + const functionConfig = jobFunction?.config ?? {}; + await tryExecuteJob( - { request, permitted, db, actionType }, + { request, permitted, db, actionType, functionConfig }, async () => { // parse request params to convert objects into rich field types (e.g. InlineFile) const inputs = parseInputs(request.params); diff --git a/packages/functions-runtime/src/handleRequest.js b/packages/functions-runtime/src/handleRequest.js index 2ce7705f2..45e47af10 100644 --- a/packages/functions-runtime/src/handleRequest.js +++ b/packages/functions-runtime/src/handleRequest.js @@ -68,8 +68,18 @@ async function handleRequest(request, config) { const customFunction = functions[request.method]; const actionType = actionTypes[request.method]; + const functionConfig = customFunction?.config ?? {}; + const result = await tryExecuteFunction( - { request, ctx, permitted, db, permissionFns, actionType }, + { + request, + ctx, + permitted, + db, + permissionFns, + actionType, + functionConfig, + }, async () => { // parse request params to convert objects into rich field types (e.g. InlineFile) const inputs = parseInputs(request.params); diff --git a/packages/functions-runtime/src/handleSubscriber.js b/packages/functions-runtime/src/handleSubscriber.js index 9a8659331..00ef44b37 100644 --- a/packages/functions-runtime/src/handleSubscriber.js +++ b/packages/functions-runtime/src/handleSubscriber.js @@ -52,13 +52,18 @@ async function handleSubscriber(request, config) { const subscriberFunction = subscribers[request.method]; const actionType = PROTO_ACTION_TYPES.SUBSCRIBER; - await tryExecuteSubscriber({ request, db, actionType }, async () => { - // parse request params to convert objects into rich field types (e.g. InlineFile) - const inputs = parseInputs(request.params); + const functionConfig = subscriberFunction?.config ?? {}; - // Return the subscriber function to the containing tryExecuteSubscriber block - return subscriberFunction(ctx, inputs); - }); + await tryExecuteSubscriber( + { request, db, actionType, functionConfig }, + async () => { + // parse request params to convert objects into rich field types (e.g. InlineFile) + const inputs = parseInputs(request.params); + + // Return the subscriber function to the containing tryExecuteSubscriber block + return subscriberFunction(ctx, inputs); + } + ); return createJSONRPCSuccessResponse(request.id, null); } catch (e) { diff --git a/packages/functions-runtime/src/index.d.ts b/packages/functions-runtime/src/index.d.ts index bb2fe14a8..ebaa18dcb 100644 --- a/packages/functions-runtime/src/index.d.ts +++ b/packages/functions-runtime/src/index.d.ts @@ -227,3 +227,15 @@ export type Errors = { */ Unknown: typeof UnknownError; }; + +export type FunctionConfig = { + /** + * All DB calls within the function will be executed within a transaction. + * The transaction is rolled back if the function throws an error. + */ + dbTransaction?: boolean; +}; + +export type FuncWithConfig = T & { + config: FunctionConfig; +}; diff --git a/packages/functions-runtime/src/tryExecuteFunction.js b/packages/functions-runtime/src/tryExecuteFunction.js index dd9ab1cd5..9db6699ff 100644 --- a/packages/functions-runtime/src/tryExecuteFunction.js +++ b/packages/functions-runtime/src/tryExecuteFunction.js @@ -11,11 +11,24 @@ const { PROTO_ACTION_TYPES } = require("./consts"); // tryExecuteFunction will create a new database transaction around a function call // and handle any permissions checks. If a permission check fails, then an Error will be thrown and the catch block will be hit. function tryExecuteFunction( - { request, db, permitted, permissionFns, actionType, ctx }, + { request, db, permitted, permissionFns, actionType, ctx, functionConfig }, cb ) { return withPermissions(permitted, async ({ getPermissionState }) => { - return withDatabase(db, actionType, async ({ transaction }) => { + let requiresTransaction = true; + switch (actionType) { + case PROTO_ACTION_TYPES.GET: + case PROTO_ACTION_TYPES.LIST: + case PROTO_ACTION_TYPES.READ: + requiresTransaction = false; + break; + } + + if (functionConfig?.dbTransaction !== undefined) { + requiresTransaction = functionConfig.dbTransaction; + } + + return withDatabase(db, requiresTransaction, async ({ transaction }) => { const fnResult = await withAuditContext(request, async () => { return cb(); }); diff --git a/packages/functions-runtime/src/tryExecuteJob.js b/packages/functions-runtime/src/tryExecuteJob.js index d1a470761..e6c785588 100644 --- a/packages/functions-runtime/src/tryExecuteJob.js +++ b/packages/functions-runtime/src/tryExecuteJob.js @@ -5,9 +5,13 @@ const { PermissionError } = require("./errors"); // tryExecuteJob will create a new database transaction around a function call // and handle any permissions checks. If a permission check fails, then an Error will be thrown and the catch block will be hit. -function tryExecuteJob({ db, permitted, actionType, request }, cb) { +function tryExecuteJob({ db, permitted, request, functionConfig }, cb) { return withPermissions(permitted, async ({ getPermissionState }) => { - return withDatabase(db, actionType, async () => { + let requiresTransaction = true; + if (functionConfig?.dbTransaction !== undefined) { + requiresTransaction = functionConfig.dbTransaction; + } + return withDatabase(db, requiresTransaction, async () => { await withAuditContext(request, async () => { return cb(); }); diff --git a/packages/functions-runtime/src/tryExecuteSubscriber.js b/packages/functions-runtime/src/tryExecuteSubscriber.js index 74eaa08ac..c77874aef 100644 --- a/packages/functions-runtime/src/tryExecuteSubscriber.js +++ b/packages/functions-runtime/src/tryExecuteSubscriber.js @@ -2,8 +2,12 @@ const { withDatabase } = require("./database"); const { withAuditContext } = require("./auditing"); // tryExecuteSubscriber will create a new database connection and execute the function call. -function tryExecuteSubscriber({ request, db, actionType }, cb) { - return withDatabase(db, actionType, async () => { +function tryExecuteSubscriber({ request, db, functionConfig }, cb) { + let requiresTransaction = true; + if (functionConfig?.dbTransaction !== undefined) { + requiresTransaction = functionConfig.dbTransaction; + } + return withDatabase(db, requiresTransaction, async () => { await withAuditContext(request, async () => { return cb(); });