@teamkeel/functions-runtime 0.236.1 → 0.236.2
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/compose.yaml +10 -0
- package/package.json +9 -24
- package/pnpm-lock.yaml +1455 -0
- package/src/ModelAPI.js +86 -0
- package/src/ModelAPI.test.js +441 -0
- package/src/QueryBuilder.js +29 -0
- package/src/applyWhereConditions.js +48 -0
- package/src/casing.js +24 -0
- package/src/database.js +56 -0
- package/src/handleRequest.js +52 -0
- package/src/handleRequest.test.js +112 -0
- package/src/index.d.ts +35 -0
- package/src/index.js +9 -0
- package/dist/index.d.ts +0 -296
- package/dist/index.js +0 -26405
package/src/ModelAPI.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const { applyWhereConditions } = require("./applyWhereConditions");
|
|
2
|
+
const { camelCaseObject, snakeCaseObject } = require("./casing");
|
|
3
|
+
const { QueryBuilder } = require("./QueryBuilder");
|
|
4
|
+
const { getDatabase } = require("./database");
|
|
5
|
+
|
|
6
|
+
class ModelAPI {
|
|
7
|
+
constructor(tableName, defaultValues, db) {
|
|
8
|
+
this._tableName = tableName;
|
|
9
|
+
this._defaultValues = defaultValues;
|
|
10
|
+
this._db = db || getDatabase();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async create(values) {
|
|
14
|
+
const row = await this._db
|
|
15
|
+
.insertInto(this._tableName)
|
|
16
|
+
.values(
|
|
17
|
+
snakeCaseObject({
|
|
18
|
+
...this._defaultValues(),
|
|
19
|
+
...values,
|
|
20
|
+
})
|
|
21
|
+
)
|
|
22
|
+
.returningAll()
|
|
23
|
+
.executeTakeFirst();
|
|
24
|
+
return camelCaseObject(row);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async findOne(where) {
|
|
28
|
+
const row = await this._db
|
|
29
|
+
.selectFrom(this._tableName)
|
|
30
|
+
.selectAll()
|
|
31
|
+
.where((qb) => {
|
|
32
|
+
return applyWhereConditions(qb, where);
|
|
33
|
+
})
|
|
34
|
+
.executeTakeFirst();
|
|
35
|
+
if (!row) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
return camelCaseObject(row);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async findMany(where) {
|
|
42
|
+
const rows = await this._db
|
|
43
|
+
.selectFrom(this._tableName)
|
|
44
|
+
.selectAll()
|
|
45
|
+
.where((qb) => {
|
|
46
|
+
return applyWhereConditions(qb, where);
|
|
47
|
+
})
|
|
48
|
+
.execute();
|
|
49
|
+
return rows.map((x) => camelCaseObject(x));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async update(where, values) {
|
|
53
|
+
const row = await this._db
|
|
54
|
+
.updateTable(this._tableName)
|
|
55
|
+
.returningAll()
|
|
56
|
+
.set(snakeCaseObject(values))
|
|
57
|
+
.where((qb) => {
|
|
58
|
+
return applyWhereConditions(qb, where);
|
|
59
|
+
})
|
|
60
|
+
.executeTakeFirstOrThrow();
|
|
61
|
+
return camelCaseObject(row);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async delete(where) {
|
|
65
|
+
const row = await this._db
|
|
66
|
+
.deleteFrom(this._tableName)
|
|
67
|
+
.returning(["id"])
|
|
68
|
+
.where((qb) => {
|
|
69
|
+
return applyWhereConditions(qb, where);
|
|
70
|
+
})
|
|
71
|
+
.executeTakeFirstOrThrow();
|
|
72
|
+
return row.id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
where(conditions) {
|
|
76
|
+
const q = this._db
|
|
77
|
+
.selectFrom(this._tableName)
|
|
78
|
+
.selectAll()
|
|
79
|
+
.where((qb) => {
|
|
80
|
+
return applyWhereConditions(qb, conditions);
|
|
81
|
+
});
|
|
82
|
+
return new QueryBuilder(q);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports.ModelAPI = ModelAPI;
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { test, expect, beforeEach } from "vitest";
|
|
2
|
+
const { ModelAPI } = require("./ModelAPI");
|
|
3
|
+
const { sql } = require("kysely");
|
|
4
|
+
const { getDatabase } = require("./database");
|
|
5
|
+
const KSUID = require("ksuid");
|
|
6
|
+
|
|
7
|
+
process.env.DB_CONN_TYPE = "pg";
|
|
8
|
+
process.env.DB_CONN = `postgresql://postgres:postgres@localhost:5432/functions-runtime`;
|
|
9
|
+
|
|
10
|
+
let api;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
const db = getDatabase();
|
|
14
|
+
|
|
15
|
+
await sql`
|
|
16
|
+
DROP TABLE IF EXISTS model_api_test;
|
|
17
|
+
CREATE TABLE model_api_test(
|
|
18
|
+
id text PRIMARY KEY,
|
|
19
|
+
name text UNIQUE,
|
|
20
|
+
married boolean,
|
|
21
|
+
favourite_number integer,
|
|
22
|
+
date timestamp
|
|
23
|
+
);
|
|
24
|
+
`.execute(db);
|
|
25
|
+
|
|
26
|
+
api = new ModelAPI(
|
|
27
|
+
"model_api_test",
|
|
28
|
+
() => {
|
|
29
|
+
return {
|
|
30
|
+
id: KSUID.randomSync().string,
|
|
31
|
+
date: new Date("2022-01-01"),
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
db
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("ModelAPI.create", async () => {
|
|
39
|
+
const row = await api.create({
|
|
40
|
+
name: "Jim",
|
|
41
|
+
married: false,
|
|
42
|
+
favouriteNumber: 10,
|
|
43
|
+
});
|
|
44
|
+
expect(row.name).toEqual("Jim");
|
|
45
|
+
expect(row.married).toEqual(false);
|
|
46
|
+
expect(row.date).toEqual(new Date("2022-01-01"));
|
|
47
|
+
expect(row.favouriteNumber).toEqual(10);
|
|
48
|
+
expect(KSUID.parse(row.id).string).toEqual(row.id);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("ModelAPI.create - throws if database constraint fails", async () => {
|
|
52
|
+
const row = await api.create({
|
|
53
|
+
name: "Jim",
|
|
54
|
+
married: false,
|
|
55
|
+
favouriteNumber: 10,
|
|
56
|
+
});
|
|
57
|
+
const promise = api.create({
|
|
58
|
+
id: row.id,
|
|
59
|
+
name: "Jim",
|
|
60
|
+
married: false,
|
|
61
|
+
favouriteNumber: 10,
|
|
62
|
+
});
|
|
63
|
+
await expect(promise).rejects.toThrow(
|
|
64
|
+
`duplicate key value violates unique constraint "model_api_test_pkey"`
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("ModelAPI.findOne", async () => {
|
|
69
|
+
const created = await api.create({
|
|
70
|
+
name: "Jim",
|
|
71
|
+
married: false,
|
|
72
|
+
favouriteNumber: 10,
|
|
73
|
+
});
|
|
74
|
+
const row = await api.findOne({
|
|
75
|
+
id: created.id,
|
|
76
|
+
});
|
|
77
|
+
expect(row).toEqual(created);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("ModelAPI.findOne - return null if not found", async () => {
|
|
81
|
+
const row = await api.findOne({
|
|
82
|
+
id: "doesntexist",
|
|
83
|
+
});
|
|
84
|
+
expect(row).toEqual(null);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("ModelAPI.findMany", async () => {
|
|
88
|
+
const jim = await api.create({
|
|
89
|
+
name: "Jim",
|
|
90
|
+
married: false,
|
|
91
|
+
favouriteNumber: 10,
|
|
92
|
+
});
|
|
93
|
+
const bob = await api.create({
|
|
94
|
+
name: "Bob",
|
|
95
|
+
married: true,
|
|
96
|
+
favouriteNumber: 11,
|
|
97
|
+
});
|
|
98
|
+
const sally = await api.create({
|
|
99
|
+
name: "Sally",
|
|
100
|
+
married: true,
|
|
101
|
+
favouriteNumber: 12,
|
|
102
|
+
});
|
|
103
|
+
const rows = await api.findMany({
|
|
104
|
+
married: true,
|
|
105
|
+
});
|
|
106
|
+
expect(rows.length).toEqual(2);
|
|
107
|
+
expect(rows.map((x) => x.id).sort()).toEqual([bob.id, sally.id].sort());
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("ModelAPI.findMany - startsWith", async () => {
|
|
111
|
+
const jim = await api.create({
|
|
112
|
+
name: "Jim",
|
|
113
|
+
});
|
|
114
|
+
await api.create({
|
|
115
|
+
name: "Bob",
|
|
116
|
+
});
|
|
117
|
+
const rows = await api.findMany({
|
|
118
|
+
name: {
|
|
119
|
+
startsWith: "Ji",
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
expect(rows.length).toEqual(1);
|
|
123
|
+
expect(rows[0].id).toEqual(jim.id);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("ModelAPI.findMany - endsWith", async () => {
|
|
127
|
+
const jim = await api.create({
|
|
128
|
+
name: "Jim",
|
|
129
|
+
});
|
|
130
|
+
await api.create({
|
|
131
|
+
name: "Bob",
|
|
132
|
+
});
|
|
133
|
+
const rows = await api.findMany({
|
|
134
|
+
name: {
|
|
135
|
+
endsWith: "im",
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
expect(rows.length).toEqual(1);
|
|
139
|
+
expect(rows[0].id).toEqual(jim.id);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("ModelAPI.findMany - contains", async () => {
|
|
143
|
+
const billy = await api.create({
|
|
144
|
+
name: "Billy",
|
|
145
|
+
});
|
|
146
|
+
const sally = await api.create({
|
|
147
|
+
name: "Sally",
|
|
148
|
+
});
|
|
149
|
+
await api.create({
|
|
150
|
+
name: "Jim",
|
|
151
|
+
});
|
|
152
|
+
const rows = await api.findMany({
|
|
153
|
+
name: {
|
|
154
|
+
contains: "ll",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
expect(rows.length).toEqual(2);
|
|
158
|
+
expect(rows.map((x) => x.id).sort()).toEqual([billy.id, sally.id].sort());
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("ModelAPI.findMany - oneOf", async () => {
|
|
162
|
+
const billy = await api.create({
|
|
163
|
+
name: "Billy",
|
|
164
|
+
});
|
|
165
|
+
const sally = await api.create({
|
|
166
|
+
name: "Sally",
|
|
167
|
+
});
|
|
168
|
+
await api.create({
|
|
169
|
+
name: "Jim",
|
|
170
|
+
});
|
|
171
|
+
const rows = await api.findMany({
|
|
172
|
+
name: {
|
|
173
|
+
oneOf: ["Billy", "Sally"],
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
expect(rows.length).toEqual(2);
|
|
177
|
+
expect(rows.map((x) => x.id).sort()).toEqual([billy.id, sally.id].sort());
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("ModelAPI.findMany - greaterThan", async () => {
|
|
181
|
+
await api.create({
|
|
182
|
+
favouriteNumber: 1,
|
|
183
|
+
});
|
|
184
|
+
const p = await api.create({
|
|
185
|
+
favouriteNumber: 2,
|
|
186
|
+
});
|
|
187
|
+
const rows = await api.findMany({
|
|
188
|
+
favouriteNumber: {
|
|
189
|
+
greaterThan: 1,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
expect(rows.length).toEqual(1);
|
|
193
|
+
expect(rows[0].id).toEqual(p.id);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("ModelAPI.findMany - greaterThanOrEquals", async () => {
|
|
197
|
+
await api.create({
|
|
198
|
+
favouriteNumber: 1,
|
|
199
|
+
});
|
|
200
|
+
const p = await api.create({
|
|
201
|
+
favouriteNumber: 2,
|
|
202
|
+
});
|
|
203
|
+
const p2 = await api.create({
|
|
204
|
+
favouriteNumber: 3,
|
|
205
|
+
});
|
|
206
|
+
const rows = await api.findMany({
|
|
207
|
+
favouriteNumber: {
|
|
208
|
+
greaterThanOrEquals: 2,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
expect(rows.length).toEqual(2);
|
|
212
|
+
expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("ModelAPI.findMany - lessThan", async () => {
|
|
216
|
+
const p = await api.create({
|
|
217
|
+
favouriteNumber: 1,
|
|
218
|
+
});
|
|
219
|
+
await api.create({
|
|
220
|
+
favouriteNumber: 2,
|
|
221
|
+
});
|
|
222
|
+
const rows = await api.findMany({
|
|
223
|
+
favouriteNumber: {
|
|
224
|
+
lessThan: 2,
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
expect(rows.length).toEqual(1);
|
|
228
|
+
expect(rows[0].id).toEqual(p.id);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("ModelAPI.findMany - lessThanOrEquals", async () => {
|
|
232
|
+
const p = await api.create({
|
|
233
|
+
favouriteNumber: 1,
|
|
234
|
+
});
|
|
235
|
+
const p2 = await api.create({
|
|
236
|
+
favouriteNumber: 2,
|
|
237
|
+
});
|
|
238
|
+
await api.create({
|
|
239
|
+
favouriteNumber: 3,
|
|
240
|
+
});
|
|
241
|
+
const rows = await api.findMany({
|
|
242
|
+
favouriteNumber: {
|
|
243
|
+
lessThanOrEquals: 2,
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
expect(rows.length).toEqual(2);
|
|
247
|
+
expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("ModelAPI.findMany - before", async () => {
|
|
251
|
+
const p = await api.create({
|
|
252
|
+
date: new Date("2022-01-01"),
|
|
253
|
+
});
|
|
254
|
+
await api.create({
|
|
255
|
+
date: new Date("2022-01-02"),
|
|
256
|
+
});
|
|
257
|
+
const rows = await api.findMany({
|
|
258
|
+
date: {
|
|
259
|
+
before: new Date("2022-01-02"),
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
expect(rows.length).toEqual(1);
|
|
263
|
+
expect(rows[0].id).toEqual(p.id);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("ModelAPI.findMany - onOrBefore", async () => {
|
|
267
|
+
const p = await api.create({
|
|
268
|
+
date: new Date("2022-01-01"),
|
|
269
|
+
});
|
|
270
|
+
const p2 = await api.create({
|
|
271
|
+
date: new Date("2022-01-02"),
|
|
272
|
+
});
|
|
273
|
+
await api.create({
|
|
274
|
+
date: new Date("2022-01-03"),
|
|
275
|
+
});
|
|
276
|
+
const rows = await api.findMany({
|
|
277
|
+
date: {
|
|
278
|
+
onOrBefore: new Date("2022-01-02"),
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
expect(rows.length).toEqual(2);
|
|
282
|
+
expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("ModelAPI.findMany - after", async () => {
|
|
286
|
+
await api.create({
|
|
287
|
+
date: new Date("2022-01-01"),
|
|
288
|
+
});
|
|
289
|
+
const p = await api.create({
|
|
290
|
+
date: new Date("2022-01-02"),
|
|
291
|
+
});
|
|
292
|
+
const rows = await api.findMany({
|
|
293
|
+
date: {
|
|
294
|
+
after: new Date("2022-01-01"),
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
expect(rows.length).toEqual(1);
|
|
298
|
+
expect(rows[0].id).toEqual(p.id);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("ModelAPI.findMany - onOrAfter", async () => {
|
|
302
|
+
await api.create({
|
|
303
|
+
date: new Date("2022-01-01"),
|
|
304
|
+
});
|
|
305
|
+
const p = await api.create({
|
|
306
|
+
date: new Date("2022-01-02"),
|
|
307
|
+
});
|
|
308
|
+
const p2 = await api.create({
|
|
309
|
+
date: new Date("2022-01-03"),
|
|
310
|
+
});
|
|
311
|
+
const rows = await api.findMany({
|
|
312
|
+
date: {
|
|
313
|
+
onOrAfter: new Date("2022-01-02"),
|
|
314
|
+
},
|
|
315
|
+
});
|
|
316
|
+
expect(rows.length).toEqual(2);
|
|
317
|
+
expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("ModelAPI.findMany - equals", async () => {
|
|
321
|
+
const p = await api.create({
|
|
322
|
+
name: "Jim",
|
|
323
|
+
});
|
|
324
|
+
await api.create({
|
|
325
|
+
name: "Sally",
|
|
326
|
+
});
|
|
327
|
+
const rows = await api.findMany({
|
|
328
|
+
name: {
|
|
329
|
+
equals: "Jim",
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
expect(rows.length).toEqual(1);
|
|
333
|
+
expect(rows[0].id).toEqual(p.id);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("ModelAPI.findMany - notEquals", async () => {
|
|
337
|
+
const p = await api.create({
|
|
338
|
+
name: "Jim",
|
|
339
|
+
});
|
|
340
|
+
await api.create({
|
|
341
|
+
name: "Sally",
|
|
342
|
+
});
|
|
343
|
+
const rows = await api.findMany({
|
|
344
|
+
name: {
|
|
345
|
+
notEquals: "Sally",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
expect(rows.length).toEqual(1);
|
|
349
|
+
expect(rows[0].id).toEqual(p.id);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("ModelAPI.findMany - complex query", async () => {
|
|
353
|
+
const p = await api.create({
|
|
354
|
+
name: "Jake",
|
|
355
|
+
favouriteNumber: 8,
|
|
356
|
+
date: new Date("2021-12-31"),
|
|
357
|
+
});
|
|
358
|
+
await api.create({
|
|
359
|
+
name: "Jane",
|
|
360
|
+
favouriteNumber: 12,
|
|
361
|
+
date: new Date("2022-01-11"),
|
|
362
|
+
});
|
|
363
|
+
const p2 = await api.create({
|
|
364
|
+
name: "Billy",
|
|
365
|
+
favouriteNumber: 16,
|
|
366
|
+
date: new Date("2022-01-05"),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
const rows = await api
|
|
370
|
+
// Will match Jake
|
|
371
|
+
.where({
|
|
372
|
+
name: {
|
|
373
|
+
startsWith: "J",
|
|
374
|
+
endsWith: "e",
|
|
375
|
+
},
|
|
376
|
+
favouriteNumber: {
|
|
377
|
+
lessThan: 10,
|
|
378
|
+
},
|
|
379
|
+
})
|
|
380
|
+
// Will match Billy
|
|
381
|
+
.orWhere({
|
|
382
|
+
date: {
|
|
383
|
+
after: new Date("2022-01-01"),
|
|
384
|
+
before: new Date("2022-01-10"),
|
|
385
|
+
},
|
|
386
|
+
})
|
|
387
|
+
.findMany();
|
|
388
|
+
expect(rows.length).toEqual(2);
|
|
389
|
+
expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test("ModelAPI.update", async () => {
|
|
393
|
+
let jim = await api.create({
|
|
394
|
+
name: "Jim",
|
|
395
|
+
married: false,
|
|
396
|
+
favouriteNumber: 10,
|
|
397
|
+
});
|
|
398
|
+
let bob = await api.create({
|
|
399
|
+
name: "Bob",
|
|
400
|
+
married: false,
|
|
401
|
+
favouriteNumber: 11,
|
|
402
|
+
});
|
|
403
|
+
jim = await api.update(
|
|
404
|
+
{
|
|
405
|
+
id: jim.id,
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
married: true,
|
|
409
|
+
}
|
|
410
|
+
);
|
|
411
|
+
expect(jim.married).toEqual(true);
|
|
412
|
+
expect(jim.name).toEqual("Jim");
|
|
413
|
+
|
|
414
|
+
bob = await api.findOne({ id: bob.id });
|
|
415
|
+
expect(bob.married).toEqual(false);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("ModelAPI.update - throws if not found", async () => {
|
|
419
|
+
const result = api.update(
|
|
420
|
+
{
|
|
421
|
+
id: "doesntexist",
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
married: true,
|
|
425
|
+
}
|
|
426
|
+
);
|
|
427
|
+
await expect(result).rejects.toThrow("no result");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("ModelAPI.delete", async () => {
|
|
431
|
+
const jim = await api.create({
|
|
432
|
+
name: "Jim",
|
|
433
|
+
});
|
|
434
|
+
const id = jim.id;
|
|
435
|
+
const deletedId = await api.delete({
|
|
436
|
+
name: "Jim",
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
expect(deletedId).toEqual(id);
|
|
440
|
+
await expect(api.findOne({ id })).resolves.toEqual(null);
|
|
441
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const { applyWhereConditions } = require("./applyWhereConditions");
|
|
2
|
+
const { camelCaseObject } = require("./casing");
|
|
3
|
+
|
|
4
|
+
class QueryBuilder {
|
|
5
|
+
constructor(db) {
|
|
6
|
+
this._db = db;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
where(conditions) {
|
|
10
|
+
const q = this._db.where((qb) => {
|
|
11
|
+
return applyWhereConditions(qb, conditions);
|
|
12
|
+
});
|
|
13
|
+
return new QueryBuilder(q);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
orWhere(conditions) {
|
|
17
|
+
const q = this._db.orWhere((qb) => {
|
|
18
|
+
return applyWhereConditions(qb, conditions);
|
|
19
|
+
});
|
|
20
|
+
return new QueryBuilder(q);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async findMany() {
|
|
24
|
+
const rows = await this._db.execute();
|
|
25
|
+
return rows.map((x) => camelCaseObject(x));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
module.exports.QueryBuilder = QueryBuilder;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { snakeCase } = require("./casing");
|
|
2
|
+
const { sql } = require("kysely");
|
|
3
|
+
|
|
4
|
+
const opMapping = {
|
|
5
|
+
startsWith: { op: "like", value: (v) => `${v}%` },
|
|
6
|
+
endsWith: { op: "like", value: (v) => `%${v}` },
|
|
7
|
+
contains: { op: "like", value: (v) => `%${v}%` },
|
|
8
|
+
oneOf: { op: "in" },
|
|
9
|
+
greaterThan: { op: ">" },
|
|
10
|
+
greaterThanOrEquals: { op: ">=" },
|
|
11
|
+
lessThan: { op: "<" },
|
|
12
|
+
lessThanOrEquals: { op: "<=" },
|
|
13
|
+
before: { op: "<" },
|
|
14
|
+
onOrBefore: { op: "<=" },
|
|
15
|
+
after: { op: ">" },
|
|
16
|
+
onOrAfter: { op: ">=" },
|
|
17
|
+
equals: { op: sql`is not distinct from` },
|
|
18
|
+
notEquals: { op: sql`is distinct from` },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function applyWhereConditions(qb, where) {
|
|
22
|
+
for (const key of Object.keys(where)) {
|
|
23
|
+
const v = where[key];
|
|
24
|
+
const fieldName = snakeCase(key);
|
|
25
|
+
|
|
26
|
+
if (Object.prototype.toString.call(v) !== "[object Object]") {
|
|
27
|
+
qb = qb.where(fieldName, "=", v);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
for (const op of Object.keys(v)) {
|
|
32
|
+
const mapping = opMapping[op];
|
|
33
|
+
if (!mapping) {
|
|
34
|
+
throw new Error(`invalid where condition: ${op}`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
qb = qb.where(
|
|
38
|
+
fieldName,
|
|
39
|
+
mapping.op,
|
|
40
|
+
mapping.value ? mapping.value(v[op]) : v[op]
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return qb;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports.applyWhereConditions = applyWhereConditions;
|
package/src/casing.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const { snakeCase, camelCase } = require("change-case");
|
|
2
|
+
|
|
3
|
+
function camelCaseObject(obj) {
|
|
4
|
+
const r = {};
|
|
5
|
+
for (const key of Object.keys(obj)) {
|
|
6
|
+
r[camelCase(key)] = obj[key];
|
|
7
|
+
}
|
|
8
|
+
return r;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function snakeCaseObject(obj) {
|
|
12
|
+
const r = {};
|
|
13
|
+
for (const key of Object.keys(obj)) {
|
|
14
|
+
r[snakeCase(key)] = obj[key];
|
|
15
|
+
}
|
|
16
|
+
return r;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = {
|
|
20
|
+
camelCaseObject,
|
|
21
|
+
snakeCaseObject,
|
|
22
|
+
snakeCase,
|
|
23
|
+
camelCase,
|
|
24
|
+
};
|
package/src/database.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const { Kysely, PostgresDialect } = require("kysely");
|
|
2
|
+
const pg = require("pg");
|
|
3
|
+
const { DataApiDialect } = require("kysely-data-api");
|
|
4
|
+
const RDSDataService = require("aws-sdk/clients/rdsdataservice");
|
|
5
|
+
|
|
6
|
+
function mustEnv(key) {
|
|
7
|
+
const v = process.env[key];
|
|
8
|
+
if (!v) {
|
|
9
|
+
throw new Error(`expected environment variable ${key} to be set`);
|
|
10
|
+
}
|
|
11
|
+
return v;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getDialect() {
|
|
15
|
+
const dbConnType = process.env["DB_CONN_TYPE"];
|
|
16
|
+
switch (dbConnType) {
|
|
17
|
+
case "pg":
|
|
18
|
+
return new PostgresDialect({
|
|
19
|
+
pool: new pg.Pool({
|
|
20
|
+
connectionString: mustEnv("DB_CONN"),
|
|
21
|
+
}),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
case "dataapi":
|
|
25
|
+
return new DataApiDialect({
|
|
26
|
+
mode: "postgres",
|
|
27
|
+
driver: {
|
|
28
|
+
client: new RDSDataService({
|
|
29
|
+
region: mustEnv("DB_REGION"),
|
|
30
|
+
}),
|
|
31
|
+
database: mustEnv("DB_NAME"),
|
|
32
|
+
secretArn: mustEnv("DB_SECRET_ARN"),
|
|
33
|
+
resourceArn: mustEnv("DB_RESOURCE_ARN"),
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
default:
|
|
38
|
+
throw Error("unexpected DB_CONN_TYPE: " + dbConnType);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let db = null;
|
|
43
|
+
|
|
44
|
+
function getDatabase() {
|
|
45
|
+
if (db) {
|
|
46
|
+
return db;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
db = new Kysely({
|
|
50
|
+
dialect: getDialect(),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return db;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports.getDatabase = getDatabase;
|