@teamkeel/functions-runtime 0.365.14 → 0.365.15-dev32
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 +1 -1
- package/src/ModelAPI.js +1 -7
- package/src/ModelAPI.test.js +180 -71
- package/src/QueryBuilder.js +129 -1
- package/src/consts.js +9 -8
- package/src/database.js +6 -2
- package/src/errors.js +11 -1
- package/src/handleJob.js +2 -7
- package/src/handleJob.test.js +0 -1
- package/src/handleSubscriber.js +88 -0
- package/src/index.js +2 -0
- package/src/permissions.js +1 -2
- package/src/permissions.test.js +1 -3
- package/src/tracing.js +1 -1
- package/src/tryExecuteFunction.js +2 -2
- package/src/tryExecuteJob.js +6 -7
- package/src/tryExecuteSubscriber.js +10 -0
- package/pnpm-lock.yaml +0 -1524
package/package.json
CHANGED
package/src/ModelAPI.js
CHANGED
|
@@ -14,6 +14,7 @@ const {
|
|
|
14
14
|
upperCamelCase,
|
|
15
15
|
} = require("./casing");
|
|
16
16
|
const tracing = require("./tracing");
|
|
17
|
+
const { DatabaseError } = require("./errors");
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* RelationshipConfig is a simple representation of a model field that
|
|
@@ -34,13 +35,6 @@ const tracing = require("./tracing");
|
|
|
34
35
|
* @typedef {Object.<string, TableConfig>} TableConfigMap
|
|
35
36
|
*/
|
|
36
37
|
|
|
37
|
-
class DatabaseError extends Error {
|
|
38
|
-
constructor(error) {
|
|
39
|
-
super(error.message);
|
|
40
|
-
this.error = error;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
38
|
class ModelAPI {
|
|
45
39
|
/**
|
|
46
40
|
* @param {string} tableName The name of the table this API is for
|
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/consts.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
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
|
+
SUBSCRIBER: "SUBSCRIBER_TYPE",
|
|
11
12
|
};
|
|
12
13
|
|
|
13
14
|
module.exports.PROTO_ACTION_TYPES = PROTO_ACTION_TYPES;
|
package/src/database.js
CHANGED
|
@@ -22,6 +22,7 @@ async function withDatabase(db, actionType, cb) {
|
|
|
22
22
|
break;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
// db.transaction() provides a kysely instance bound to a transaction.
|
|
25
26
|
if (requiresTransaction) {
|
|
26
27
|
return db.transaction().execute(async (transaction) => {
|
|
27
28
|
return dbInstance.run(transaction, async () => {
|
|
@@ -30,8 +31,11 @@ async function withDatabase(db, actionType, cb) {
|
|
|
30
31
|
});
|
|
31
32
|
}
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
34
|
+
// db.connection() provides a kysely instance bound to a single database connection.
|
|
35
|
+
return db.connection().execute(async (sDb) => {
|
|
36
|
+
return dbInstance.run(sDb, async () => {
|
|
37
|
+
return cb({ sDb });
|
|
38
|
+
});
|
|
35
39
|
});
|
|
36
40
|
}
|
|
37
41
|
|
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/handleJob.js
CHANGED
|
@@ -4,7 +4,6 @@ const {
|
|
|
4
4
|
JSONRPCErrorCode,
|
|
5
5
|
} = require("json-rpc-2.0");
|
|
6
6
|
const { getDatabaseClient } = require("./database");
|
|
7
|
-
const { tryExecuteFunction } = require("./tryExecuteFunction");
|
|
8
7
|
const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
|
|
9
8
|
const opentelemetry = require("@opentelemetry/api");
|
|
10
9
|
const { withSpan } = require("./tracing");
|
|
@@ -54,15 +53,11 @@ async function handleJob(request, config) {
|
|
|
54
53
|
const db = getDatabaseClient();
|
|
55
54
|
const jobFunction = jobs[request.method];
|
|
56
55
|
const actionType = PROTO_ACTION_TYPES.JOB;
|
|
57
|
-
const permissionFns = new Object();
|
|
58
|
-
|
|
59
|
-
// Jobs will have no permission functions yet.
|
|
60
|
-
permissionFns[request.method] = [];
|
|
61
56
|
|
|
62
57
|
await tryExecuteJob(
|
|
63
|
-
{ request,
|
|
58
|
+
{ request, permitted, db, actionType },
|
|
64
59
|
async () => {
|
|
65
|
-
// Return the job function to the containing
|
|
60
|
+
// Return the job function to the containing tryExecuteJob block
|
|
66
61
|
return jobFunction(ctx, request.params);
|
|
67
62
|
}
|
|
68
63
|
);
|
package/src/handleJob.test.js
CHANGED
|
@@ -4,7 +4,6 @@ import { handleJob, RuntimeErrors } from "./handleJob";
|
|
|
4
4
|
import { test, expect, beforeEach, describe } from "vitest";
|
|
5
5
|
import { ModelAPI } from "./ModelAPI";
|
|
6
6
|
import { useDatabase } from "./database";
|
|
7
|
-
const { Permissions } = require("./permissions");
|
|
8
7
|
import KSUID from "ksuid";
|
|
9
8
|
|
|
10
9
|
test("when the job returns nothing as expected", async () => {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const {
|
|
2
|
+
createJSONRPCErrorResponse,
|
|
3
|
+
createJSONRPCSuccessResponse,
|
|
4
|
+
JSONRPCErrorCode,
|
|
5
|
+
} = require("json-rpc-2.0");
|
|
6
|
+
const { getDatabaseClient } = require("./database");
|
|
7
|
+
const { errorToJSONRPCResponse, RuntimeErrors } = require("./errors");
|
|
8
|
+
const opentelemetry = require("@opentelemetry/api");
|
|
9
|
+
const { withSpan } = require("./tracing");
|
|
10
|
+
const { PROTO_ACTION_TYPES } = require("./consts");
|
|
11
|
+
const { tryExecuteSubscriber } = require("./tryExecuteSubscriber");
|
|
12
|
+
|
|
13
|
+
// Generic handler function that is agnostic to runtime environment (local or lambda)
|
|
14
|
+
// to execute a subscriber function based on the contents of a jsonrpc-2.0 payload object.
|
|
15
|
+
// To read more about jsonrpc request and response shapes, please read https://www.jsonrpc.org/specification
|
|
16
|
+
async function handleSubscriber(request, config) {
|
|
17
|
+
// Try to extract trace context from caller
|
|
18
|
+
const activeContext = opentelemetry.propagation.extract(
|
|
19
|
+
opentelemetry.context.active(),
|
|
20
|
+
request.meta?.tracing
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
// Run the whole request with the extracted context
|
|
24
|
+
return opentelemetry.context.with(activeContext, () => {
|
|
25
|
+
// Wrapping span for the whole request
|
|
26
|
+
return withSpan(request.method, async (span) => {
|
|
27
|
+
try {
|
|
28
|
+
const { createSubscriberContextAPI, subscribers } = config;
|
|
29
|
+
|
|
30
|
+
if (!(request.method in subscribers)) {
|
|
31
|
+
const message = `no corresponding subscriber found for '${request.method}'`;
|
|
32
|
+
span.setStatus({
|
|
33
|
+
code: opentelemetry.SpanStatusCode.ERROR,
|
|
34
|
+
message: message,
|
|
35
|
+
});
|
|
36
|
+
return createJSONRPCErrorResponse(
|
|
37
|
+
request.id,
|
|
38
|
+
JSONRPCErrorCode.MethodNotFound,
|
|
39
|
+
message
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The ctx argument passed into the subscriber function.
|
|
44
|
+
const ctx = createSubscriberContextAPI({
|
|
45
|
+
meta: request.meta,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const db = getDatabaseClient();
|
|
49
|
+
const subscriberFunction = subscribers[request.method];
|
|
50
|
+
const actionType = PROTO_ACTION_TYPES.SUBSCRIBER;
|
|
51
|
+
|
|
52
|
+
await tryExecuteSubscriber({ db, actionType }, async () => {
|
|
53
|
+
// Return the subscriber function to the containing tryExecuteSubscriber block
|
|
54
|
+
return subscriberFunction(ctx, request.params);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return createJSONRPCSuccessResponse(request.id, null);
|
|
58
|
+
} catch (e) {
|
|
59
|
+
if (e instanceof Error) {
|
|
60
|
+
span.recordException(e);
|
|
61
|
+
span.setStatus({
|
|
62
|
+
code: opentelemetry.SpanStatusCode.ERROR,
|
|
63
|
+
message: e.message,
|
|
64
|
+
});
|
|
65
|
+
return errorToJSONRPCResponse(request, e);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const message = JSON.stringify(e);
|
|
69
|
+
|
|
70
|
+
span.setStatus({
|
|
71
|
+
code: opentelemetry.SpanStatusCode.ERROR,
|
|
72
|
+
message: message,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
return createJSONRPCErrorResponse(
|
|
76
|
+
request.id,
|
|
77
|
+
RuntimeErrors.UnknownError,
|
|
78
|
+
message
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = {
|
|
86
|
+
handleSubscriber,
|
|
87
|
+
RuntimeErrors,
|
|
88
|
+
};
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const { ModelAPI } = require("./ModelAPI");
|
|
|
2
2
|
const { RequestHeaders } = require("./RequestHeaders");
|
|
3
3
|
const { handleRequest } = require("./handleRequest");
|
|
4
4
|
const { handleJob } = require("./handleJob");
|
|
5
|
+
const { handleSubscriber } = require("./handleSubscriber");
|
|
5
6
|
const KSUID = require("ksuid");
|
|
6
7
|
const { useDatabase } = require("./database");
|
|
7
8
|
const {
|
|
@@ -16,6 +17,7 @@ module.exports = {
|
|
|
16
17
|
RequestHeaders,
|
|
17
18
|
handleRequest,
|
|
18
19
|
handleJob,
|
|
20
|
+
handleSubscriber,
|
|
19
21
|
useDatabase,
|
|
20
22
|
Permissions,
|
|
21
23
|
PERMISSION_STATE,
|
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 = {};
|