@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 +3 -2
- package/src/ModelAPI.js +69 -34
- package/src/ModelAPI.test.js +180 -71
- package/src/QueryBuilder.js +129 -1
- package/src/auditing.js +35 -0
- package/src/consts.js +8 -8
- package/src/database.js +7 -2
- package/src/errors.js +11 -1
- package/src/handleSubscriber.js +1 -1
- package/src/permissions.js +1 -2
- package/src/permissions.test.js +1 -3
- package/src/tracing.js +1 -1
- package/src/tryExecuteFunction.js +8 -4
- package/src/tryExecuteJob.js +8 -7
- package/src/tryExecuteSubscriber.js +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teamkeel/functions-runtime",
|
|
3
|
-
"version": "0.365.
|
|
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
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
200
|
+
return await this.#execute(async (db) => {
|
|
201
|
+
let builder = db.updateTable(this._tableName).returningAll();
|
|
168
202
|
|
|
169
|
-
|
|
203
|
+
builder = builder.set(snakeCaseObject(values));
|
|
170
204
|
|
|
171
|
-
|
|
205
|
+
const context = new QueryContext(
|
|
206
|
+
[this._tableName],
|
|
207
|
+
this._tableConfigMap
|
|
208
|
+
);
|
|
172
209
|
|
|
173
|
-
|
|
174
|
-
|
|
210
|
+
// TODO: support joins for update
|
|
211
|
+
builder = applyWhereConditions(context, builder, where);
|
|
175
212
|
|
|
176
|
-
|
|
213
|
+
span.setAttribute("sql", builder.compile().sql);
|
|
177
214
|
|
|
178
|
-
try {
|
|
179
215
|
const row = await builder.executeTakeFirstOrThrow();
|
|
180
216
|
return camelCaseObject(row);
|
|
181
|
-
}
|
|
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
|
-
|
|
225
|
+
return await this.#execute(async (db) => {
|
|
226
|
+
let builder = db.deleteFrom(this._tableName).returning(["id"]);
|
|
193
227
|
|
|
194
|
-
|
|
228
|
+
const context = new QueryContext(
|
|
229
|
+
[this._tableName],
|
|
230
|
+
this._tableConfigMap
|
|
231
|
+
);
|
|
195
232
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
}
|
|
204
|
-
throw new DatabaseError(e);
|
|
205
|
-
}
|
|
240
|
+
});
|
|
206
241
|
});
|
|
207
242
|
}
|
|
208
243
|
|
package/src/ModelAPI.test.js
CHANGED
|
@@ -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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
892
|
-
|
|
893
|
-
|
|
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
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
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
|
-
|
|
918
|
-
|
|
919
|
-
|
|
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
|
});
|
package/src/QueryBuilder.js
CHANGED
|
@@ -5,10 +5,15 @@ const {
|
|
|
5
5
|
applyOrderBy,
|
|
6
6
|
} = require("./applyAdditionalQueryConstraints");
|
|
7
7
|
const { applyJoins } = require("./applyJoins");
|
|
8
|
-
const {
|
|
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();
|
package/src/auditing.js
ADDED
|
@@ -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: "
|
|
3
|
-
CREATE: "
|
|
4
|
-
GET: "
|
|
5
|
-
LIST: "
|
|
6
|
-
UPDATE: "
|
|
7
|
-
DELETE: "
|
|
8
|
-
READ: "
|
|
9
|
-
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
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
|
};
|
package/src/handleSubscriber.js
CHANGED
|
@@ -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
|
});
|
package/src/permissions.js
CHANGED
package/src/permissions.test.js
CHANGED
|
@@ -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,
|
|
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
|
|
19
|
-
|
|
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()) {
|
package/src/tryExecuteJob.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
const { withDatabase } = require("./database");
|
|
2
|
-
const {
|
|
3
|
-
|
|
4
|
-
|
|
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 (
|
|
13
|
-
await
|
|
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
|
|
7
|
+
await withAuditContext(request, async () => {
|
|
8
|
+
return cb();
|
|
9
|
+
});
|
|
7
10
|
});
|
|
8
11
|
}
|
|
9
12
|
|