@teamkeel/functions-runtime 0.365.15-prerelease9 → 0.365.16-12

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@teamkeel/functions-runtime",
3
- "version": "0.365.15-prerelease9",
3
+ "version": "0.365.16-12",
4
4
  "description": "Internal package used by @teamkeel/sdk",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -28,6 +28,7 @@
28
28
  "json-rpc-2.0": "^1.4.1",
29
29
  "ksuid": "^3.0.0",
30
30
  "kysely": "^0.23.4",
31
- "pg": "^8.8.0"
31
+ "pg": "^8.8.0",
32
+ "traceparent": "^1.0.0"
32
33
  }
33
34
  }
package/src/ModelAPI.js CHANGED
@@ -1,8 +1,11 @@
1
1
  const { useDatabase } = require("./database");
2
+ const { getAuditContext } = require("./auditing");
2
3
  const { QueryBuilder } = require("./QueryBuilder");
3
4
  const { QueryContext } = require("./QueryContext");
4
5
  const { applyWhereConditions } = require("./applyWhereConditions");
5
6
  const { applyJoins } = require("./applyJoins");
7
+ const { sql } = require("kysely");
8
+
6
9
  const {
7
10
  applyLimit,
8
11
  applyOffset,
@@ -14,6 +17,7 @@ const {
14
17
  upperCamelCase,
15
18
  } = require("./casing");
16
19
  const tracing = require("./tracing");
20
+ const { DatabaseError } = require("./errors");
17
21
 
18
22
  /**
19
23
  * RelationshipConfig is a simple representation of a model field that
@@ -34,13 +38,6 @@ const tracing = require("./tracing");
34
38
  * @typedef {Object.<string, TableConfig>} TableConfigMap
35
39
  */
36
40
 
37
- class DatabaseError extends Error {
38
- constructor(error) {
39
- super(error.message);
40
- this.error = error;
41
- }
42
- }
43
-
44
41
  class ModelAPI {
45
42
  /**
46
43
  * @param {string} tableName The name of the table this API is for
@@ -53,12 +50,52 @@ class ModelAPI {
53
50
  this._modelName = upperCamelCase(this._tableName);
54
51
  }
55
52
 
53
+ // execute sets the audit context in the database and then runs individual
54
+ // database statements within a transaction if one hasn't been opened, which
55
+ // is necessary because the audit parameters will only be available within transactions.
56
+ async #execute(fn) {
57
+ const db = useDatabase();
58
+
59
+ try {
60
+ if (db.isTransaction) {
61
+ await this.#setAuditParameters(db);
62
+ return await fn(db);
63
+ } else {
64
+ return await db.transaction().execute(async (transaction) => {
65
+ await this.#setAuditParameters(transaction);
66
+ return fn(transaction);
67
+ });
68
+ }
69
+ } catch (e) {
70
+ throw new DatabaseError(e);
71
+ }
72
+ }
73
+
74
+ // setAuditParameters sets audit context data to configuration parameters
75
+ // in the database so that they can be read by the triggered auditing function.
76
+ async #setAuditParameters(transaction) {
77
+ const audit = getAuditContext();
78
+ const statements = [];
79
+
80
+ if (audit.identityId) {
81
+ statements.push(`CALL set_identity_id('${audit.identityId}');`);
82
+ }
83
+
84
+ if (audit.traceId) {
85
+ statements.push(`CALL set_trace_id('${audit.traceId}');`);
86
+ }
87
+
88
+ if (statements.length > 0) {
89
+ const stmt = statements.join("");
90
+ await sql.raw(stmt).execute(transaction);
91
+ }
92
+ }
93
+
56
94
  async create(values) {
57
95
  const name = tracing.spanNameForModelAPI(this._modelName, "create");
58
- const db = useDatabase();
59
96
 
60
97
  return tracing.withSpan(name, async (span) => {
61
- try {
98
+ return await this.#execute(async (db) => {
62
99
  const query = db
63
100
  .insertInto(this._tableName)
64
101
  .values(
@@ -70,11 +107,8 @@ class ModelAPI {
70
107
 
71
108
  span.setAttribute("sql", query.compile().sql);
72
109
  const row = await query.executeTakeFirstOrThrow();
73
-
74
110
  return camelCaseObject(row);
75
- } catch (e) {
76
- throw new DatabaseError(e);
77
- }
111
+ });
78
112
  });
79
113
  }
80
114
 
@@ -161,48 +195,49 @@ class ModelAPI {
161
195
 
162
196
  async update(where, values) {
163
197
  const name = tracing.spanNameForModelAPI(this._modelName, "update");
164
- const db = useDatabase();
165
198
 
166
199
  return tracing.withSpan(name, async (span) => {
167
- let builder = db.updateTable(this._tableName).returningAll();
200
+ return await this.#execute(async (db) => {
201
+ let builder = db.updateTable(this._tableName).returningAll();
168
202
 
169
- builder = builder.set(snakeCaseObject(values));
203
+ builder = builder.set(snakeCaseObject(values));
170
204
 
171
- const context = new QueryContext([this._tableName], this._tableConfigMap);
205
+ const context = new QueryContext(
206
+ [this._tableName],
207
+ this._tableConfigMap
208
+ );
172
209
 
173
- // TODO: support joins for update
174
- builder = applyWhereConditions(context, builder, where);
210
+ // TODO: support joins for update
211
+ builder = applyWhereConditions(context, builder, where);
175
212
 
176
- span.setAttribute("sql", builder.compile().sql);
213
+ span.setAttribute("sql", builder.compile().sql);
177
214
 
178
- try {
179
215
  const row = await builder.executeTakeFirstOrThrow();
180
216
  return camelCaseObject(row);
181
- } catch (e) {
182
- throw new DatabaseError(e);
183
- }
217
+ });
184
218
  });
185
219
  }
186
220
 
187
221
  async delete(where) {
188
222
  const name = tracing.spanNameForModelAPI(this._modelName, "delete");
189
- const db = useDatabase();
190
223
 
191
224
  return tracing.withSpan(name, async (span) => {
192
- let builder = db.deleteFrom(this._tableName).returning(["id"]);
225
+ return await this.#execute(async (db) => {
226
+ let builder = db.deleteFrom(this._tableName).returning(["id"]);
193
227
 
194
- const context = new QueryContext([this._tableName], this._tableConfigMap);
228
+ const context = new QueryContext(
229
+ [this._tableName],
230
+ this._tableConfigMap
231
+ );
195
232
 
196
- // TODO: support joins for delete
197
- builder = applyWhereConditions(context, builder, where);
233
+ // TODO: support joins for delete
234
+ builder = applyWhereConditions(context, builder, where);
235
+
236
+ span.setAttribute("sql", builder.compile().sql);
198
237
 
199
- span.setAttribute("sql", builder.compile().sql);
200
- try {
201
238
  const row = await builder.executeTakeFirstOrThrow();
202
239
  return row.id;
203
- } catch (e) {
204
- throw new DatabaseError(e);
205
- }
240
+ });
206
241
  });
207
242
  }
208
243
 
@@ -1,4 +1,4 @@
1
- import { test, expect, beforeEach } from "vitest";
1
+ import { test, expect, beforeEach, describe } from "vitest";
2
2
  const { ModelAPI } = require("./ModelAPI");
3
3
  const { sql } = require("kysely");
4
4
  const { useDatabase } = require("./database");
@@ -654,49 +654,6 @@ test("ModelAPI.findMany - notEquals", async () => {
654
654
  expect(rows[0].id).toEqual(p.id);
655
655
  });
656
656
 
657
- test("ModelAPI.findMany - complex query", async () => {
658
- const p = await personAPI.create({
659
- id: KSUID.randomSync().string,
660
- name: "Jake",
661
- favouriteNumber: 8,
662
- date: new Date("2021-12-31"),
663
- });
664
- await personAPI.create({
665
- id: KSUID.randomSync().string,
666
- name: "Jane",
667
- favouriteNumber: 12,
668
- date: new Date("2022-01-11"),
669
- });
670
- const p2 = await personAPI.create({
671
- id: KSUID.randomSync().string,
672
- name: "Billy",
673
- favouriteNumber: 16,
674
- date: new Date("2022-01-05"),
675
- });
676
-
677
- const rows = await personAPI
678
- // Will match Jake
679
- .where({
680
- name: {
681
- startsWith: "J",
682
- endsWith: "e",
683
- },
684
- favouriteNumber: {
685
- lessThan: 10,
686
- },
687
- })
688
- // Will match Billy
689
- .orWhere({
690
- date: {
691
- after: new Date("2022-01-01"),
692
- before: new Date("2022-01-10"),
693
- },
694
- })
695
- .findMany();
696
- expect(rows.length).toEqual(2);
697
- expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
698
- });
699
-
700
657
  test("ModelAPI.findMany - relationships - one to many", async () => {
701
658
  const person = await personAPI.create({
702
659
  id: KSUID.randomSync().string,
@@ -883,38 +840,190 @@ test("ModelAPI.delete", async () => {
883
840
  await expect(personAPI.findOne({ id })).resolves.toEqual(null);
884
841
  });
885
842
 
886
- test("ModelAPI chained findMany with offset/limit/order by", async () => {
887
- await postAPI.create({
888
- id: KSUID.randomSync().string,
889
- title: "adam",
843
+ describe("QueryBuilder", () => {
844
+ test("ModelAPI chained findMany with offset/limit/order by", async () => {
845
+ await postAPI.create({
846
+ id: KSUID.randomSync().string,
847
+ title: "adam",
848
+ });
849
+ await postAPI.create({
850
+ id: KSUID.randomSync().string,
851
+ title: "dave",
852
+ });
853
+ const three = await postAPI.create({
854
+ id: KSUID.randomSync().string,
855
+ title: "jon",
856
+ });
857
+ const four = await postAPI.create({
858
+ id: KSUID.randomSync().string,
859
+ title: "jon bretman",
860
+ });
861
+
862
+ const results = await postAPI
863
+ .where({ title: { equals: "adam" } })
864
+ .orWhere({
865
+ title: { startsWith: "jon" },
866
+ })
867
+ .findMany({
868
+ limit: 3,
869
+ offset: 1,
870
+ orderBy: {
871
+ title: "asc",
872
+ },
873
+ });
874
+
875
+ // because we've offset by 1, adam should not appear in the results even though
876
+ // the query constraints match adam
877
+ expect(results).toEqual([three, four]);
890
878
  });
891
- await postAPI.create({
892
- id: KSUID.randomSync().string,
893
- title: "dave",
879
+
880
+ test("ModelAPI.findMany - complex query", async () => {
881
+ const p = await personAPI.create({
882
+ id: KSUID.randomSync().string,
883
+ name: "Jake",
884
+ favouriteNumber: 8,
885
+ date: new Date("2021-12-31"),
886
+ });
887
+ await personAPI.create({
888
+ id: KSUID.randomSync().string,
889
+ name: "Jane",
890
+ favouriteNumber: 12,
891
+ date: new Date("2022-01-11"),
892
+ });
893
+ const p2 = await personAPI.create({
894
+ id: KSUID.randomSync().string,
895
+ name: "Billy",
896
+ favouriteNumber: 16,
897
+ date: new Date("2022-01-05"),
898
+ });
899
+
900
+ const rows = await personAPI
901
+ // Will match Jake
902
+ .where({
903
+ name: {
904
+ startsWith: "J",
905
+ endsWith: "e",
906
+ },
907
+ favouriteNumber: {
908
+ lessThan: 10,
909
+ },
910
+ })
911
+ // Will match Billy
912
+ .orWhere({
913
+ date: {
914
+ after: new Date("2022-01-01"),
915
+ before: new Date("2022-01-10"),
916
+ },
917
+ })
918
+ .findMany();
919
+ expect(rows.length).toEqual(2);
920
+ expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
894
921
  });
895
- const three = await postAPI.create({
896
- id: KSUID.randomSync().string,
897
- title: "jon",
922
+
923
+ test("ModelAPI chained delete", async () => {
924
+ const p = await personAPI.create({
925
+ id: KSUID.randomSync().string,
926
+ name: "Jake",
927
+ favouriteNumber: 8,
928
+ date: new Date("2021-12-31"),
929
+ });
930
+
931
+ const deletedId = await personAPI.where({ id: p.id }).delete();
932
+
933
+ expect(deletedId).toEqual(p.id);
898
934
  });
899
- const four = await postAPI.create({
900
- id: KSUID.randomSync().string,
901
- title: "jon bretman",
935
+
936
+ test("Model API chained delete - non existent id", async () => {
937
+ const fakeId = "xxx";
938
+
939
+ // the error message returned from the runtime will actually be 'record not found'
940
+ // but this is handled at handleRequest level
941
+ // no result is the error msg returned by kysely.
942
+ await expect(personAPI.where({ id: fakeId }).delete()).rejects.toThrow(
943
+ "no result"
944
+ );
902
945
  });
903
946
 
904
- const results = await postAPI
905
- .where({ title: { equals: "adam" } })
906
- .orWhere({
907
- title: { startsWith: "jon" },
908
- })
909
- .findMany({
910
- limit: 3,
911
- offset: 1,
912
- orderBy: {
913
- title: "asc",
914
- },
947
+ test("Model API chained findOne", async () => {
948
+ const p = await personAPI.create({
949
+ id: KSUID.randomSync().string,
950
+ name: "Jake",
951
+ favouriteNumber: 8,
952
+ date: new Date("2021-12-31"),
915
953
  });
916
954
 
917
- // because we've offset by 1, adam should not appear in the results even though
918
- // the query constraints match adam
919
- expect(results).toEqual([three, four]);
955
+ const jake = await personAPI.where({ id: p.id }).findOne();
956
+
957
+ expect(jake).toEqual(p);
958
+ });
959
+
960
+ // test("Model API chained order by", async () => {
961
+ // const p1 = await postAPI.create({
962
+ // id: KSUID.randomSync().string,
963
+ // title: "adam",
964
+ // });
965
+ // const p2 = await postAPI.create({
966
+ // id: KSUID.randomSync().string,
967
+ // title: "dave",
968
+ // });
969
+ // const p3 = await postAPI.create({
970
+ // id: KSUID.randomSync().string,
971
+ // title: "jon",
972
+ // });
973
+ // const p4 = await postAPI.create({
974
+ // id: KSUID.randomSync().string,
975
+ // title: "jon bretman",
976
+ // });
977
+
978
+ // const query = postAPI
979
+ // .where({ title: "adam" })
980
+ // .orWhere({ title: "dave" })
981
+ // .orderBy({ title: "desc" });
982
+
983
+ // const results = await query.findMany();
984
+
985
+ // expect(results[0].id).toEqual(p2);
986
+ // });
987
+
988
+ test("Model API chained update", async () => {
989
+ const p1 = await postAPI.create({
990
+ id: KSUID.randomSync().string,
991
+ title: "adam",
992
+ });
993
+ const p2 = await postAPI.create({
994
+ id: KSUID.randomSync().string,
995
+ title: "adam",
996
+ });
997
+ const p3 = await postAPI.create({
998
+ id: KSUID.randomSync().string,
999
+ title: "adam",
1000
+ });
1001
+
1002
+ const updatedRow = await postAPI
1003
+ .where({ id: p2.id })
1004
+ .update({ title: "adam 2" });
1005
+
1006
+ expect(updatedRow.title).toEqual("adam 2");
1007
+ expect(updatedRow.id).toEqual(p2.id);
1008
+
1009
+ // will fail because there is more than 1 row matching the constraints (p1 and p3)
1010
+ await expect(
1011
+ postAPI
1012
+ .where({
1013
+ title: "adam",
1014
+ })
1015
+ .update({ title: "bob" })
1016
+ ).rejects.toThrowError(
1017
+ "more than one row matched update constraints - only unique fields should be used when updating."
1018
+ );
1019
+
1020
+ // will fail because there are no rows to update
1021
+ await expect(
1022
+ postAPI
1023
+ .where({
1024
+ title: "no match",
1025
+ })
1026
+ .update({ title: "bob" })
1027
+ ).resolves.toEqual(null);
1028
+ });
920
1029
  });
@@ -5,10 +5,15 @@ const {
5
5
  applyOrderBy,
6
6
  } = require("./applyAdditionalQueryConstraints");
7
7
  const { applyJoins } = require("./applyJoins");
8
- const { camelCaseObject } = require("./casing");
8
+ const {
9
+ camelCaseObject,
10
+ snakeCaseObject,
11
+ upperCamelCase,
12
+ } = require("./casing");
9
13
  const { useDatabase } = require("./database");
10
14
  const { QueryContext } = require("./QueryContext");
11
15
  const tracing = require("./tracing");
16
+ const { DatabaseError } = require("./errors");
12
17
 
13
18
  class QueryBuilder {
14
19
  /**
@@ -20,6 +25,7 @@ class QueryBuilder {
20
25
  this._tableName = tableName;
21
26
  this._context = context;
22
27
  this._db = db;
28
+ this._modelName = upperCamelCase(this._tableName);
23
29
  }
24
30
 
25
31
  where(where) {
@@ -43,6 +49,128 @@ class QueryBuilder {
43
49
  return new QueryBuilder(this._tableName, context, builder);
44
50
  }
45
51
 
52
+ sql() {
53
+ return this._db.compile().sql;
54
+ }
55
+
56
+ async update(values) {
57
+ const name = tracing.spanNameForModelAPI(this._modelName, "update");
58
+ const db = useDatabase();
59
+
60
+ return tracing.withSpan(name, async (span) => {
61
+ // we build a sub-query to add to the WHERE id IN (XXX) containing all of the
62
+ // wheres added in previous .where() chains.
63
+ const sub = this._db.clearSelect().select("id");
64
+
65
+ const query = db
66
+ .updateTable(this._tableName)
67
+ .set(snakeCaseObject(values))
68
+ .returningAll()
69
+ .where("id", "in", sub);
70
+
71
+ try {
72
+ const result = await query.execute();
73
+ const numUpdatedRows = result.length;
74
+
75
+ // the double (==) is important because we are comparing bigint to int
76
+ if (numUpdatedRows == 0) {
77
+ return null;
78
+ }
79
+
80
+ if (numUpdatedRows > 1) {
81
+ throw new DatabaseError(
82
+ new Error(
83
+ "more than one row matched update constraints - only unique fields should be used when updating."
84
+ )
85
+ );
86
+ }
87
+
88
+ return camelCaseObject(result[0]);
89
+ } catch (e) {
90
+ throw new DatabaseError(e);
91
+ }
92
+ });
93
+ }
94
+
95
+ async delete() {
96
+ const name = tracing.spanNameForModelAPI(this._modelName, "delete");
97
+ const db = useDatabase();
98
+
99
+ return tracing.withSpan(name, async (span) => {
100
+ // the original query selects the distinct id + the model.* so we need to clear
101
+ const sub = this._db.clearSelect().select("id");
102
+ let builder = db.deleteFrom(this._tableName).where("id", "in", sub);
103
+
104
+ const query = builder.returning(["id"]);
105
+
106
+ // final query looks something like:
107
+ // delete from "person" where "id" in (select distinct on ("person"."id") "id" from "person" where "person"."id" = $1) returning "id"
108
+
109
+ span.setAttribute("sql", query.compile().sql);
110
+
111
+ try {
112
+ const row = await query.executeTakeFirstOrThrow();
113
+ return row.id;
114
+ } catch (e) {
115
+ throw new DatabaseError(e);
116
+ }
117
+ });
118
+ }
119
+
120
+ async findOne() {
121
+ const name = tracing.spanNameForModelAPI(this._modelName, "findOne");
122
+ const db = useDatabase();
123
+
124
+ return tracing.withSpan(name, async (span) => {
125
+ let builder = db
126
+ .selectFrom((qb) => {
127
+ // this._db contains all of the where constraints and joins
128
+ // we want to include that in the sub query in the same way we
129
+ // add all of this information into the sub query in the ModelAPI's
130
+ // implementation of findOne
131
+ return this._db.as(this._tableName);
132
+ })
133
+ .selectAll();
134
+
135
+ span.setAttribute("sql", builder.compile().sql);
136
+
137
+ const row = await builder.executeTakeFirstOrThrow();
138
+
139
+ if (!row) {
140
+ return null;
141
+ }
142
+
143
+ return camelCaseObject(row);
144
+ });
145
+ }
146
+
147
+ // orderBy(conditions) {
148
+ // const context = this._context.clone();
149
+
150
+ // const builder = applyOrderBy(
151
+ // context,
152
+ // this._db,
153
+ // this._tableName,
154
+ // conditions
155
+ // );
156
+
157
+ // return new QueryBuilder(this._tableName, context, builder);
158
+ // }
159
+
160
+ // limit(limit) {
161
+ // const context = this._context.clone();
162
+ // const builder = applyLimit(context, this._db, limit);
163
+
164
+ // return new QueryBuilder(this._tableName, context, builder);
165
+ // }
166
+
167
+ // offset(offset) {
168
+ // const context = this._context.clone();
169
+ // const builder = applyOffset(context, builder, offset);
170
+
171
+ // return new QueryBuilder(this._tableName, context, builder);
172
+ // }
173
+
46
174
  async findMany(params) {
47
175
  const name = tracing.spanNameForModelAPI(this._modelName, "findMany");
48
176
  const db = useDatabase();
@@ -0,0 +1,35 @@
1
+ const { AsyncLocalStorage } = require("async_hooks");
2
+ const TraceParent = require("traceparent");
3
+
4
+ const auditContextStorage = new AsyncLocalStorage();
5
+
6
+ // withAuditContext creates the audit context from the runtime request body
7
+ // and sets it to in AsyncLocalStorage so that this data is available to the
8
+ // ModelAPI during the execution of actions, jobs and subscribers.
9
+ async function withAuditContext(request, cb) {
10
+ let audit = {};
11
+ if (request.meta?.identity) {
12
+ audit.identityId = request.meta.identity.id;
13
+ }
14
+ if (request.meta?.tracing?.traceparent) {
15
+ audit.traceId = TraceParent.fromString(
16
+ request.meta.tracing.traceparent
17
+ )?.traceId;
18
+ }
19
+
20
+ return await auditContextStorage.run(audit, () => {
21
+ return cb();
22
+ });
23
+ }
24
+
25
+ // getAuditContext retrieves the audit context from AsyncLocalStorage.
26
+ function getAuditContext() {
27
+ let auditStore = auditContextStorage.getStore();
28
+ return {
29
+ identityId: auditStore?.identityId,
30
+ traceId: auditStore?.traceId,
31
+ };
32
+ }
33
+
34
+ module.exports.withAuditContext = withAuditContext;
35
+ module.exports.getAuditContext = getAuditContext;
package/src/consts.js CHANGED
@@ -1,12 +1,12 @@
1
1
  const PROTO_ACTION_TYPES = {
2
- UNKNOWN: "OPERATION_TYPE_UNKNOWN",
3
- CREATE: "OPERATION_TYPE_CREATE",
4
- GET: "OPERATION_TYPE_GET",
5
- LIST: "OPERATION_TYPE_LIST",
6
- UPDATE: "OPERATION_TYPE_UPDATE",
7
- DELETE: "OPERATION_TYPE_DELETE",
8
- READ: "OPERATION_TYPE_READ",
9
- WRITE: "OPERATION_TYPE_WRITE",
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
10
  JOB: "JOB_TYPE",
11
11
  SUBSCRIBER: "SUBSCRIBER_TYPE",
12
12
  };
package/src/database.js CHANGED
@@ -15,6 +15,7 @@ async function withDatabase(db, actionType, cb) {
15
15
  let requiresTransaction = true;
16
16
 
17
17
  switch (actionType) {
18
+ case PROTO_ACTION_TYPES.SUBSCRIBER:
18
19
  case PROTO_ACTION_TYPES.JOB:
19
20
  case PROTO_ACTION_TYPES.GET:
20
21
  case PROTO_ACTION_TYPES.LIST:
@@ -22,6 +23,7 @@ async function withDatabase(db, actionType, cb) {
22
23
  break;
23
24
  }
24
25
 
26
+ // db.transaction() provides a kysely instance bound to a transaction.
25
27
  if (requiresTransaction) {
26
28
  return db.transaction().execute(async (transaction) => {
27
29
  return dbInstance.run(transaction, async () => {
@@ -30,8 +32,11 @@ async function withDatabase(db, actionType, cb) {
30
32
  });
31
33
  }
32
34
 
33
- return dbInstance.run(db, async () => {
34
- return cb({ transaction: db });
35
+ // db.connection() provides a kysely instance bound to a single database connection.
36
+ return db.connection().execute(async (sDb) => {
37
+ return dbInstance.run(sDb, async () => {
38
+ return cb({ sDb });
39
+ });
35
40
  });
36
41
  }
37
42
 
package/src/errors.js CHANGED
@@ -1,5 +1,13 @@
1
1
  const { createJSONRPCErrorResponse } = require("json-rpc-2.0");
2
- const { PermissionError } = require("./permissions");
2
+
3
+ class PermissionError extends Error {}
4
+
5
+ class DatabaseError extends Error {
6
+ constructor(error) {
7
+ super(error.message);
8
+ this.error = error;
9
+ }
10
+ }
3
11
 
4
12
  const RuntimeErrors = {
5
13
  // Catchall error type for unhandled execution errors during custom function
@@ -113,4 +121,6 @@ const parseKeyMessage = (msg) => {
113
121
  module.exports = {
114
122
  errorToJSONRPCResponse,
115
123
  RuntimeErrors,
124
+ DatabaseError,
125
+ PermissionError,
116
126
  };
@@ -49,7 +49,7 @@ async function handleSubscriber(request, config) {
49
49
  const subscriberFunction = subscribers[request.method];
50
50
  const actionType = PROTO_ACTION_TYPES.SUBSCRIBER;
51
51
 
52
- await tryExecuteSubscriber({ db, actionType }, async () => {
52
+ await tryExecuteSubscriber({ request, db, actionType }, async () => {
53
53
  // Return the subscriber function to the containing tryExecuteSubscriber block
54
54
  return subscriberFunction(ctx, request.params);
55
55
  });
@@ -1,6 +1,5 @@
1
1
  const { AsyncLocalStorage } = require("async_hooks");
2
-
3
- class PermissionError extends Error {}
2
+ const { PermissionError } = require("./errors");
4
3
 
5
4
  const PERMISSION_STATE = {
6
5
  UNKNOWN: "unknown",
@@ -2,13 +2,11 @@ const {
2
2
  permissionsApiInstance,
3
3
  Permissions,
4
4
  PERMISSION_STATE,
5
- PermissionError,
6
5
  checkBuiltInPermissions,
7
6
  } = require("./permissions");
8
-
9
7
  import { useDatabase } from "./database";
10
-
11
8
  import { beforeEach, describe, expect, test } from "vitest";
9
+ const { PermissionError } = require("./errors");
12
10
 
13
11
  let permissions;
14
12
  let ctx = {};
package/src/tracing.js CHANGED
@@ -6,7 +6,7 @@ const {
6
6
  const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
7
7
  const { envDetectorSync } = require("@opentelemetry/resources");
8
8
 
9
- function withSpan(name, fn) {
9
+ async function withSpan(name, fn) {
10
10
  return getTracer().startActiveSpan(name, async (span) => {
11
11
  try {
12
12
  // await the thing (this means we can use try/catch)
@@ -1,22 +1,26 @@
1
1
  const { withDatabase } = require("./database");
2
+ const { withAuditContext } = require("./auditing");
2
3
  const {
3
4
  withPermissions,
4
5
  PERMISSION_STATE,
5
- PermissionError,
6
6
  checkBuiltInPermissions,
7
7
  } = require("./permissions");
8
+ const { PermissionError } = require("./errors");
8
9
  const { PROTO_ACTION_TYPES } = require("./consts");
9
10
 
10
11
  // tryExecuteFunction will create a new database transaction around a function call
11
12
  // and handle any permissions checks. If a permission check fails, then an Error will be thrown and the catch block will be hit.
12
13
  function tryExecuteFunction(
13
- { db, permitted, permissionFns, actionType, request, ctx },
14
+ { request, db, permitted, permissionFns, actionType, ctx },
14
15
  cb
15
16
  ) {
16
17
  return withPermissions(permitted, async ({ getPermissionState }) => {
17
18
  return withDatabase(db, actionType, async ({ transaction }) => {
18
- const fnResult = await cb();
19
- // api.permissions maintains an internal state of whether the current operation has been *explicitly* permitted/denied by the user in the course of their custom function, or if execution has already been permitted by a role based permission (evaluated in the main runtime).
19
+ const fnResult = await withAuditContext(request, async () => {
20
+ return cb();
21
+ });
22
+
23
+ // api.permissions maintains an internal state of whether the current function has been *explicitly* permitted/denied by the user in the course of their custom function, or if execution has already been permitted by a role based permission (evaluated in the main runtime).
20
24
  // we need to check that the final state is permitted or unpermitted. if it's not, then it means that the user has taken no explicit action to permit/deny
21
25
  // and therefore we default to checking the permissions defined in the schema automatically.
22
26
  switch (getPermissionState()) {
@@ -1,16 +1,17 @@
1
1
  const { withDatabase } = require("./database");
2
- const {
3
- withPermissions,
4
- PERMISSION_STATE,
5
- PermissionError,
6
- } = require("./permissions");
2
+ const { withAuditContext } = require("./auditing");
3
+ const { withPermissions, PERMISSION_STATE } = require("./permissions");
4
+ const { PermissionError } = require("./errors");
7
5
 
8
6
  // tryExecuteJob will create a new database transaction around a function call
9
7
  // and handle any permissions checks. If a permission check fails, then an Error will be thrown and the catch block will be hit.
10
8
  function tryExecuteJob({ db, permitted, actionType, request }, cb) {
11
9
  return withPermissions(permitted, async ({ getPermissionState }) => {
12
- return withDatabase(db, actionType, async ({ transaction }) => {
13
- await cb();
10
+ return withDatabase(db, actionType, async () => {
11
+ await withAuditContext(request, async () => {
12
+ return cb();
13
+ });
14
+
14
15
  // api.permissions maintains an internal state of whether the current operation has been *explicitly* permitted/denied by the user in the course of their custom function, or if execution has already been permitted by a role based permission (evaluated in the main runtime).
15
16
  // we need to check that the final state is permitted or unpermitted. if it's not, then it means that the user has taken no explicit action to permit/deny
16
17
  // and therefore we default to checking the permissions defined in the schema automatically.
@@ -1,9 +1,12 @@
1
1
  const { withDatabase } = require("./database");
2
+ const { withAuditContext } = require("./auditing");
2
3
 
3
4
  // tryExecuteSubscriber will create a new database connection and execute the function call.
4
- function tryExecuteSubscriber({ db, actionType }, cb) {
5
+ function tryExecuteSubscriber({ request, db, actionType }, cb) {
5
6
  return withDatabase(db, actionType, async () => {
6
- await cb();
7
+ await withAuditContext(request, async () => {
8
+ return cb();
9
+ });
7
10
  });
8
11
  }
9
12