@teamkeel/functions-runtime 0.412.0-next.2 → 0.412.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/.env.test +2 -0
  2. package/compose.yaml +10 -0
  3. package/package.json +5 -23
  4. package/src/Duration.js +40 -0
  5. package/src/Duration.test.js +34 -0
  6. package/src/File.js +295 -0
  7. package/src/ModelAPI.js +377 -0
  8. package/src/ModelAPI.test.js +1428 -0
  9. package/src/QueryBuilder.js +184 -0
  10. package/src/QueryContext.js +90 -0
  11. package/src/RequestHeaders.js +21 -0
  12. package/src/TimePeriod.js +89 -0
  13. package/src/TimePeriod.test.js +148 -0
  14. package/src/applyAdditionalQueryConstraints.js +22 -0
  15. package/src/applyJoins.js +67 -0
  16. package/src/applyWhereConditions.js +124 -0
  17. package/src/auditing.js +110 -0
  18. package/src/auditing.test.js +330 -0
  19. package/src/camelCasePlugin.js +52 -0
  20. package/src/casing.js +54 -0
  21. package/src/casing.test.js +56 -0
  22. package/src/consts.js +14 -0
  23. package/src/database.js +244 -0
  24. package/src/errors.js +160 -0
  25. package/src/handleJob.js +110 -0
  26. package/src/handleJob.test.js +270 -0
  27. package/src/handleRequest.js +153 -0
  28. package/src/handleRequest.test.js +463 -0
  29. package/src/handleRoute.js +112 -0
  30. package/src/handleSubscriber.js +105 -0
  31. package/src/index.d.ts +317 -0
  32. package/src/index.js +38 -0
  33. package/src/parsing.js +113 -0
  34. package/src/parsing.test.js +140 -0
  35. package/src/permissions.js +77 -0
  36. package/src/permissions.test.js +118 -0
  37. package/src/tracing.js +184 -0
  38. package/src/tracing.test.js +147 -0
  39. package/src/tryExecuteFunction.js +91 -0
  40. package/src/tryExecuteJob.js +29 -0
  41. package/src/tryExecuteSubscriber.js +17 -0
  42. package/src/type-utils.js +18 -0
  43. package/vite.config.js +7 -0
  44. package/dist/index.d.mts +0 -340
  45. package/dist/index.d.ts +0 -340
  46. package/dist/index.js +0 -3093
  47. package/dist/index.js.map +0 -1
  48. package/dist/index.mjs +0 -3097
  49. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,56 @@
1
+ import { test, expect } from "vitest";
2
+ import { camelCaseObject, snakeCaseObject } from "./casing";
3
+
4
+ const cases = {
5
+ FROM_SNAKE: {
6
+ input: {
7
+ id: "123",
8
+ slack_id: "xxx_2929",
9
+ api_key: "1234",
10
+ test_11: "1234",
11
+ test_1_test: "1234",
12
+ test12: "1234",
13
+ abc123_test: "1234",
14
+ },
15
+ expected: {
16
+ id: "123",
17
+ slackId: "xxx_2929",
18
+ apiKey: "1234",
19
+ test11: "1234",
20
+ test1Test: "1234",
21
+ test12: "1234",
22
+ abc123Test: "1234",
23
+ },
24
+ },
25
+ FROM_CAMEL: {
26
+ input: {
27
+ id: "123",
28
+ slackId: "xxx_2929",
29
+ apiKey: "1234",
30
+ test1: "test",
31
+ testA1: "test",
32
+ test1Test: "test",
33
+ test20: "test",
34
+ testURL: "test",
35
+ },
36
+ expected: {
37
+ id: "123",
38
+ slack_id: "xxx_2929",
39
+ api_key: "1234",
40
+ test_1: "test",
41
+ test_a_1: "test",
42
+ test_1_test: "test",
43
+ test_20: "test",
44
+ test_url: "test",
45
+ },
46
+ },
47
+ };
48
+
49
+ Object.entries(cases).map(([key, { input, expected }]) => {
50
+ test(key, () => {
51
+ const result =
52
+ key === "FROM_SNAKE" ? camelCaseObject(input) : snakeCaseObject(input);
53
+
54
+ expect(result).toEqual(expected);
55
+ });
56
+ });
package/src/consts.js ADDED
@@ -0,0 +1,14 @@
1
+ const PROTO_ACTION_TYPES = {
2
+ UNKNOWN: "ACTION_TYPE_UNKNOWN",
3
+ CREATE: "ACTION_TYPE_CREATE",
4
+ GET: "ACTION_TYPE_GET",
5
+ LIST: "ACTION_TYPE_LIST",
6
+ UPDATE: "ACTION_TYPE_UPDATE",
7
+ DELETE: "ACTION_TYPE_DELETE",
8
+ READ: "ACTION_TYPE_READ",
9
+ WRITE: "ACTION_TYPE_WRITE",
10
+ JOB: "JOB_TYPE",
11
+ SUBSCRIBER: "SUBSCRIBER_TYPE",
12
+ };
13
+
14
+ module.exports.PROTO_ACTION_TYPES = PROTO_ACTION_TYPES;
@@ -0,0 +1,244 @@
1
+ const { Kysely, PostgresDialect } = require("kysely");
2
+ const neonserverless = require("@neondatabase/serverless");
3
+ const { AsyncLocalStorage } = require("async_hooks");
4
+ const { AuditContextPlugin } = require("./auditing");
5
+ const { KeelCamelCasePlugin } = require("./camelCasePlugin");
6
+ const pg = require("pg");
7
+ const { withSpan } = require("./tracing");
8
+ const ws = require("ws");
9
+ const fs = require("node:fs");
10
+ const { Duration } = require("./Duration");
11
+
12
+ // withDatabase is responsible for setting the correct database client in our AsyncLocalStorage
13
+ // so that the the code in a custom function uses the correct client.
14
+ // For GET and LIST action types, no transaction is used, but for
15
+ // actions that mutate data such as CREATE, DELETE & UPDATE, all of the code inside
16
+ // the user's custom function is wrapped in a transaction so we can rollback
17
+ // the transaction if something goes wrong.
18
+ // withDatabase shouldn't be exposed in the public api of the sdk
19
+ async function withDatabase(db, requiresTransaction, cb) {
20
+ // db.transaction() provides a kysely instance bound to a transaction.
21
+ if (requiresTransaction) {
22
+ return db.transaction().execute(async (transaction) => {
23
+ return dbInstance.run(transaction, async () => {
24
+ return cb({ transaction });
25
+ });
26
+ });
27
+ }
28
+
29
+ // db.connection() provides a kysely instance bound to a single database connection.
30
+ return db.connection().execute(async (sDb) => {
31
+ return dbInstance.run(sDb, async () => {
32
+ return cb({ sDb });
33
+ });
34
+ });
35
+ }
36
+
37
+ const dbInstance = new AsyncLocalStorage();
38
+
39
+ // used to establish a singleton for our vitest environment
40
+ let vitestDb = null;
41
+
42
+ // useDatabase will retrieve the database client set by withDatabase from the local storage
43
+ function useDatabase() {
44
+ // retrieve the instance of the database client from the store which is aware of
45
+ // which context the current connection to the db is running in - e.g does the context
46
+ // require a transaction or not?
47
+ let fromStore = dbInstance.getStore();
48
+ if (fromStore) {
49
+ return fromStore;
50
+ }
51
+
52
+ // if the NODE_ENV is 'test' then we know we are inside of the vitest environment
53
+ // which covers any test files ending in *.test.ts. Custom function code runs in a different node process which will not have this environment variable. Tests written using our testing
54
+ // framework call actions (and in turn custom function code) over http using the ActionExecutor class
55
+ if ("NODE_ENV" in process.env && process.env.NODE_ENV == "test") {
56
+ if (!vitestDb) {
57
+ vitestDb = createDatabaseClient();
58
+ }
59
+ return vitestDb;
60
+ }
61
+
62
+ // If we've gotten to this point, then we know that we are in a custom function runtime server
63
+ // context and we haven't been able to retrieve the in-context instance of Kysely, which means we should throw an error.
64
+ console.trace();
65
+ throw new Error("useDatabase must be called within a function");
66
+ }
67
+
68
+ // createDatabaseClient will return a brand new instance of Kysely. Every instance of Kysely
69
+ // represents an individual connection to the database.
70
+ // not to be exported externally from our sdk - consumers should use useDatabase
71
+ function createDatabaseClient({ connString } = {}) {
72
+ const db = new Kysely({
73
+ dialect: getDialect(connString),
74
+ plugins: [
75
+ // ensures that the audit context data is written to Postgres configuration parameters
76
+ new AuditContextPlugin(),
77
+ // allows users to query using camelCased versions of the database column names, which
78
+ // should match the names we use in our schema.
79
+ // We're using an extended version of Kysely's CamelCasePlugin which avoids changing keys of objects that represent
80
+ // rich data formats, specific to Keel (e.g. Duration)
81
+ new KeelCamelCasePlugin(),
82
+ ],
83
+ log(event) {
84
+ if ("DEBUG" in process.env) {
85
+ if (event.level === "query") {
86
+ console.log(event.query.sql);
87
+ console.log(event.query.parameters);
88
+ }
89
+ }
90
+ },
91
+ });
92
+
93
+ return db;
94
+ }
95
+
96
+ class InstrumentedPool extends pg.Pool {
97
+ async connect(...args) {
98
+ const _super = super.connect.bind(this);
99
+ return withSpan("Database Connect", function (span) {
100
+ span.setAttribute("dialect", process.env["KEEL_DB_CONN_TYPE"]);
101
+ return _super(...args);
102
+ });
103
+ }
104
+ }
105
+
106
+ class InstrumentedNeonServerlessPool extends neonserverless.Pool {
107
+ async connect(...args) {
108
+ const _super = super.connect.bind(this);
109
+ return withSpan("Database Connect", function (span) {
110
+ span.setAttribute("dialect", process.env["KEEL_DB_CONN_TYPE"]);
111
+ return _super(...args);
112
+ });
113
+ }
114
+ }
115
+
116
+ const txStatements = {
117
+ begin: "Transaction Begin",
118
+ commit: "Transaction Commit",
119
+ rollback: "Transaction Rollback",
120
+ };
121
+
122
+ class InstrumentedClient extends pg.Client {
123
+ async query(...args) {
124
+ const _super = super.query.bind(this);
125
+ const sql = args[0];
126
+
127
+ let sqlAttribute = false;
128
+ let spanName = txStatements[sql.toLowerCase()];
129
+ if (!spanName) {
130
+ spanName = "Database Query";
131
+ sqlAttribute = true;
132
+ }
133
+
134
+ return withSpan(spanName, function (span) {
135
+ if (sqlAttribute) {
136
+ span.setAttribute("sql", args[0]);
137
+ span.setAttribute("dialect", process.env["KEEL_DB_CONN_TYPE"]);
138
+ }
139
+ return _super(...args);
140
+ });
141
+ }
142
+ }
143
+
144
+ function getDialect(connString) {
145
+ const dbConnType = process.env.KEEL_DB_CONN_TYPE;
146
+ switch (dbConnType) {
147
+ case "pg":
148
+ // Adding a custom type parser for numeric fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
149
+ // 1700 = type for NUMERIC
150
+ pg.types.setTypeParser(pg.types.builtins.NUMERIC, function (val) {
151
+ return parseFloat(val);
152
+ });
153
+ // Adding a custom type parser for interval fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
154
+ // 1186 = type for INTERVAL
155
+ pg.types.setTypeParser(pg.types.builtins.INTERVAL, function (val) {
156
+ return new Duration(val);
157
+ });
158
+
159
+ return new PostgresDialect({
160
+ pool: new InstrumentedPool({
161
+ Client: InstrumentedClient,
162
+ // Increased idle time before closing a connection in the local pool (from 10s default).
163
+ // Establising a new connection on (almost) every functions query can be expensive, so this
164
+ // will reduce having to open connections as regularly. https://node-postgres.com/apis/pool
165
+ //
166
+ // NOTE: We should consider setting this to 0 (i.e. never pool locally) and open and close
167
+ // connections with each invocation. This is because the freeze/thaw nature of lambdas can cause problems
168
+ // with long-lived connections - see https://github.com/brianc/node-postgres/issues/2718
169
+ // Once we're "fully regional" this should not be a performance problem anymore.
170
+ //
171
+ // Although I doubt we will run into these freeze/thaw issues if idleTimeoutMillis is always shorter than the
172
+ // time is takes for a lambda to freeze (which is not a constant, but could be as short as several minutes,
173
+ // https://www.pluralsight.com/resources/blog/cloud/how-long-does-aws-lambda-keep-your-idle-functions-around-before-a-cold-start)
174
+ idleTimeoutMillis: 50000,
175
+
176
+ // If connString is not passed fall back to reading from env var
177
+ connectionString: connString || process.env.KEEL_DB_CONN,
178
+
179
+ // Allow the setting of a cert (.pem) file. RDS requires this to enforce SSL.
180
+ ...(process.env.KEEL_DB_CERT
181
+ ? { ssl: { ca: fs.readFileSync(process.env.KEEL_DB_CERT) } }
182
+ : undefined),
183
+ }),
184
+ });
185
+ case "neon":
186
+ // Adding a custom type parser for numeric fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
187
+ // 1700 = type for NUMERIC
188
+ neonserverless.types.setTypeParser(
189
+ pg.types.builtins.NUMERIC,
190
+ function (val) {
191
+ return parseFloat(val);
192
+ }
193
+ );
194
+ // Adding a custom type parser for interval fields: see https://kysely.dev/docs/recipes/data-types#configuring-runtime-javascript-types
195
+ // 1186 = type for INTERVAL
196
+ neonserverless.types.setTypeParser(
197
+ pg.types.builtins.INTERVAL,
198
+ function (val) {
199
+ return new Duration(val);
200
+ }
201
+ );
202
+
203
+ neonserverless.neonConfig.webSocketConstructor = ws;
204
+
205
+ const pool = new InstrumentedNeonServerlessPool({
206
+ // If connString is not passed fall back to reading from env var
207
+ connectionString: connString || process.env.KEEL_DB_CONN,
208
+ });
209
+
210
+ pool.on("connect", (client) => {
211
+ const originalQuery = client.query;
212
+ client.query = function (...args) {
213
+ const sql = args[0];
214
+
215
+ let sqlAttribute = false;
216
+ let spanName = txStatements[sql.toLowerCase()];
217
+ if (!spanName) {
218
+ spanName = "Database Query";
219
+ sqlAttribute = true;
220
+ }
221
+
222
+ return withSpan(spanName, function (span) {
223
+ if (sqlAttribute) {
224
+ span.setAttribute("sql", args[0]);
225
+ span.setAttribute("dialect", dbConnType);
226
+ }
227
+ return originalQuery.apply(client, args);
228
+ });
229
+ };
230
+ });
231
+
232
+ return new PostgresDialect({
233
+ pool: pool,
234
+ });
235
+ default:
236
+ throw Error("unexpected KEEL_DB_CONN_TYPE: " + dbConnType);
237
+ }
238
+ }
239
+
240
+ module.exports = {
241
+ createDatabaseClient,
242
+ useDatabase,
243
+ withDatabase,
244
+ };
package/src/errors.js ADDED
@@ -0,0 +1,160 @@
1
+ const { createJSONRPCErrorResponse } = require("json-rpc-2.0");
2
+
3
+ const RuntimeErrors = {
4
+ // Catchall error type for unhandled execution errors during custom function
5
+ UnknownError: -32001,
6
+ // DatabaseError represents any error at pg level that isn't handled explicitly below
7
+ DatabaseError: -32002,
8
+ // No result returned from custom function by user
9
+ NoResultError: -32003,
10
+ // When trying to delete/update a non existent record in the db
11
+ RecordNotFoundError: -32004,
12
+ ForeignKeyConstraintError: -32005,
13
+ NotNullConstraintError: -32006,
14
+ UniqueConstraintError: -32007,
15
+ PermissionError: -32008,
16
+ BadRequestError: -32009,
17
+ };
18
+
19
+ // Error presets
20
+ class PermissionError extends Error {}
21
+
22
+ class DatabaseError extends Error {
23
+ constructor(error) {
24
+ super(error.message);
25
+ this.error = error;
26
+ }
27
+ }
28
+
29
+ class NotFoundError extends Error {
30
+ errorCode = RuntimeErrors.RecordNotFoundError;
31
+ constructor(message) {
32
+ super(message); // Default message is handled by the runtime for consistency with built in actions
33
+ }
34
+ }
35
+
36
+ class BadRequestError extends Error {
37
+ errorCode = RuntimeErrors.BadRequestError;
38
+ constructor(message = "bad request") {
39
+ super(message);
40
+ }
41
+ }
42
+
43
+ class UnknownError extends Error {
44
+ errorCode = RuntimeErrors.UnknownError;
45
+ constructor(message = "unknown error") {
46
+ super(message);
47
+ }
48
+ }
49
+
50
+ const ErrorPresets = {
51
+ NotFound: NotFoundError,
52
+ BadRequest: BadRequestError,
53
+ Unknown: UnknownError,
54
+ };
55
+
56
+ // errorToJSONRPCResponse transforms a JavaScript Error instance (or derivative) into a valid JSONRPC response object to pass back to the Keel runtime.
57
+ function errorToJSONRPCResponse(request, e) {
58
+ switch (e.constructor.name) {
59
+ case "PermissionError":
60
+ return createJSONRPCErrorResponse(
61
+ request.id,
62
+ RuntimeErrors.PermissionError,
63
+ e.message
64
+ );
65
+ // Any error thrown in the ModelAPI class is
66
+ // wrapped in a DatabaseError in order to differentiate 'our code' vs the user's own code.
67
+ case "NoResultError":
68
+ return createJSONRPCErrorResponse(
69
+ request.id,
70
+
71
+ // to be matched to https://github.com/teamkeel/keel/blob/e3115ffe381bfc371d4f45bbf96a15072a994ce5/runtime/actions/update.go#L54-L54
72
+ RuntimeErrors.RecordNotFoundError,
73
+ "" // Don't pass on the message as we want to normalise these at the runtime layer but still support custom messages in other NotFound errors
74
+ );
75
+ case "DatabaseError":
76
+ let err = e;
77
+
78
+ // If wrapped error then unwrap
79
+ if (e instanceof DatabaseError) {
80
+ err = e.error;
81
+ }
82
+
83
+ if (err.constructor.name == "NoResultError") {
84
+ return createJSONRPCErrorResponse(
85
+ request.id,
86
+
87
+ // to be matched to https://github.com/teamkeel/keel/blob/e3115ffe381bfc371d4f45bbf96a15072a994ce5/runtime/actions/update.go#L54-L54
88
+ RuntimeErrors.RecordNotFoundError,
89
+ "" // Don't pass on the message as we want to normalise these at the runtime layer but still support custom messages in other NotFound errors
90
+ );
91
+ }
92
+
93
+ // if the error contains 'code' then assume it has other pg error message keys
94
+ // todo: make this more ironclad.
95
+ // when using lib-pq, should match https://github.com/brianc/node-postgres/blob/master/packages/pg-protocol/src/parser.ts#L371-L386
96
+ if ("code" in err) {
97
+ const { code, detail, table } = err;
98
+
99
+ let rpcErrorCode, column, value;
100
+ const [col, val] = parseKeyMessage(err.detail);
101
+ column = col;
102
+ value = val;
103
+
104
+ switch (code) {
105
+ case "23502":
106
+ rpcErrorCode = RuntimeErrors.NotNullConstraintError;
107
+ column = err.column;
108
+ break;
109
+ case "23503":
110
+ rpcErrorCode = RuntimeErrors.ForeignKeyConstraintError;
111
+ break;
112
+ case "23505":
113
+ rpcErrorCode = RuntimeErrors.UniqueConstraintError;
114
+ break;
115
+ default:
116
+ rpcErrorCode = RuntimeErrors.DatabaseError;
117
+ break;
118
+ }
119
+
120
+ return createJSONRPCErrorResponse(request.id, rpcErrorCode, e.message, {
121
+ table,
122
+ column,
123
+ code,
124
+ detail,
125
+ value,
126
+ });
127
+ }
128
+
129
+ // we don't know what it is, but it's something else
130
+ return createJSONRPCErrorResponse(
131
+ request.id,
132
+ RuntimeErrors.DatabaseError,
133
+ e.message
134
+ );
135
+ default:
136
+ // Use the errorCode in the error if we have some from a preset
137
+ return createJSONRPCErrorResponse(
138
+ request.id,
139
+ e.errorCode ?? RuntimeErrors.UnknownError,
140
+ e.message
141
+ );
142
+ }
143
+ }
144
+
145
+ // example data:
146
+ // Key (author_id)=(fake) is not present in table "author".
147
+ const keyMessagePattern = /\Key\s[(](.*)[)][=][(](.*)[)]/;
148
+ const parseKeyMessage = (msg) => {
149
+ const [, col, value] = keyMessagePattern.exec(msg) || [];
150
+
151
+ return [col, value];
152
+ };
153
+
154
+ module.exports = {
155
+ errorToJSONRPCResponse,
156
+ RuntimeErrors,
157
+ DatabaseError,
158
+ PermissionError,
159
+ ErrorPresets,
160
+ };
@@ -0,0 +1,110 @@
1
+ const {
2
+ createJSONRPCErrorResponse,
3
+ createJSONRPCSuccessResponse,
4
+ JSONRPCErrorCode,
5
+ } = require("json-rpc-2.0");
6
+ const { createDatabaseClient } = require("./database");
7
+ const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
8
+ const opentelemetry = require("@opentelemetry/api");
9
+ const { withSpan } = require("./tracing");
10
+ const { tryExecuteJob } = require("./tryExecuteJob");
11
+ const { parseInputs } = require("./parsing");
12
+ const { PROTO_ACTION_TYPES } = require("./consts");
13
+
14
+ // Generic handler function that is agnostic to runtime environment (local or lambda)
15
+ // to execute a job function based on the contents of a jsonrpc-2.0 payload object.
16
+ // To read more about jsonrpc request and response shapes, please read https://www.jsonrpc.org/specification
17
+ async function handleJob(request, config) {
18
+ // Try to extract trace context from caller
19
+ const activeContext = opentelemetry.propagation.extract(
20
+ opentelemetry.context.active(),
21
+ request.meta?.tracing
22
+ );
23
+
24
+ // Run the whole request with the extracted context
25
+ return opentelemetry.context.with(activeContext, () => {
26
+ // Wrapping span for the whole request
27
+ return withSpan(request.method, async (span) => {
28
+ let db = null;
29
+
30
+ try {
31
+ const { createJobContextAPI, jobs } = config;
32
+
33
+ if (!(request.method in jobs)) {
34
+ const message = `no corresponding job found for '${request.method}'`;
35
+ span.setStatus({
36
+ code: opentelemetry.SpanStatusCode.ERROR,
37
+ message: message,
38
+ });
39
+ return createJSONRPCErrorResponse(
40
+ request.id,
41
+ JSONRPCErrorCode.MethodNotFound,
42
+ message
43
+ );
44
+ }
45
+
46
+ // The ctx argument passed into the job function.
47
+ const ctx = createJobContextAPI({
48
+ meta: request.meta,
49
+ });
50
+
51
+ const permitted =
52
+ request.meta && request.meta.permissionState.status === "granted"
53
+ ? true
54
+ : null;
55
+
56
+ db = createDatabaseClient({
57
+ connString: request.meta?.secrets?.KEEL_DB_CONN,
58
+ });
59
+ const jobFunction = jobs[request.method];
60
+ const actionType = PROTO_ACTION_TYPES.JOB;
61
+
62
+ const functionConfig = jobFunction?.config ?? {};
63
+
64
+ await tryExecuteJob(
65
+ { request, permitted, db, actionType, functionConfig },
66
+ async () => {
67
+ // parse request params to convert objects into rich field types (e.g. InlineFile)
68
+ const inputs = parseInputs(request.params);
69
+
70
+ // Return the job function to the containing tryExecuteJob block
71
+ return jobFunction(ctx, inputs);
72
+ }
73
+ );
74
+
75
+ return createJSONRPCSuccessResponse(request.id, null);
76
+ } catch (e) {
77
+ if (e instanceof Error) {
78
+ span.recordException(e);
79
+ span.setStatus({
80
+ code: opentelemetry.SpanStatusCode.ERROR,
81
+ message: e.message,
82
+ });
83
+ return errorToJSONRPCResponse(request, e);
84
+ }
85
+
86
+ const message = JSON.stringify(e);
87
+
88
+ span.setStatus({
89
+ code: opentelemetry.SpanStatusCode.ERROR,
90
+ message: message,
91
+ });
92
+
93
+ return createJSONRPCErrorResponse(
94
+ request.id,
95
+ RuntimeErrors.UnknownError,
96
+ message
97
+ );
98
+ } finally {
99
+ if (db) {
100
+ await db.destroy();
101
+ }
102
+ }
103
+ });
104
+ });
105
+ }
106
+
107
+ module.exports = {
108
+ handleJob,
109
+ RuntimeErrors,
110
+ };