@teamkeel/functions-runtime 0.243.0 → 0.244.1
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 +85 -42
- package/src/ModelAPI.test.js +230 -80
- package/src/QueryBuilder.js +24 -11
- package/src/QueryContext.js +90 -0
- package/src/applyJoins.js +65 -0
- package/src/applyWhereConditions.js +26 -4
package/package.json
CHANGED
package/src/ModelAPI.js
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
|
+
const { getDatabase } = require("./database");
|
|
2
|
+
const { QueryBuilder } = require("./QueryBuilder");
|
|
3
|
+
const { QueryContext } = require("./QueryContext");
|
|
1
4
|
const { applyWhereConditions } = require("./applyWhereConditions");
|
|
5
|
+
const { applyJoins } = require("./applyJoins");
|
|
2
6
|
const { camelCaseObject, snakeCaseObject } = require("./casing");
|
|
3
|
-
|
|
4
|
-
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* RelationshipConfig is a simple representation of a model field that
|
|
10
|
+
* is a relationship. It is used by applyJoins and applyWhereConditions
|
|
11
|
+
* to build the correct query.
|
|
12
|
+
* @typedef {{
|
|
13
|
+
* relationshipType: "belongsTo" | "hasMany",
|
|
14
|
+
* foreignKey: string,
|
|
15
|
+
* referencesTable: string,
|
|
16
|
+
* }} RelationshipConfig
|
|
17
|
+
*
|
|
18
|
+
* TableConfig is an object where the keys are relationship field names
|
|
19
|
+
* (which don't exist in the database) and the values are RelationshipConfig
|
|
20
|
+
* objects describing that relationship.
|
|
21
|
+
* @typedef {Object.<string, RelationshipConfig} TableConfig
|
|
22
|
+
*
|
|
23
|
+
* TableConfigMap is mapping of database table names to TableConfig objects
|
|
24
|
+
* @typedef {Object.<string, TableConfig>} TableConfigMap
|
|
25
|
+
*/
|
|
5
26
|
|
|
6
27
|
class ModelAPI {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
28
|
+
/**
|
|
29
|
+
* @param {string} tableName The name of the table this API is for
|
|
30
|
+
* @param {Function} defaultValues A function that returns the default values for a row in this table
|
|
31
|
+
* @param {import("kysely").Kysely} db
|
|
32
|
+
* @param {TableConfigMap} tableConfigMap
|
|
33
|
+
*/
|
|
34
|
+
constructor(tableName, defaultValues, db, tableConfigMap = {}) {
|
|
10
35
|
this._db = db || getDatabase();
|
|
36
|
+
this._defaultValues = defaultValues;
|
|
37
|
+
this._tableName = tableName;
|
|
38
|
+
this._tableConfigMap = tableConfigMap;
|
|
11
39
|
}
|
|
12
40
|
|
|
13
41
|
async create(values) {
|
|
@@ -21,70 +49,85 @@ class ModelAPI {
|
|
|
21
49
|
)
|
|
22
50
|
.returningAll()
|
|
23
51
|
.executeTakeFirst();
|
|
52
|
+
|
|
24
53
|
return camelCaseObject(row);
|
|
25
54
|
}
|
|
26
55
|
|
|
27
56
|
async findOne(where) {
|
|
28
|
-
|
|
57
|
+
let builder = this._db
|
|
29
58
|
.selectFrom(this._tableName)
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
59
|
+
.distinctOn(`${this._tableName}.id`)
|
|
60
|
+
.selectAll(this._tableName);
|
|
61
|
+
|
|
62
|
+
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
63
|
+
|
|
64
|
+
builder = applyJoins(context, builder, where);
|
|
65
|
+
builder = applyWhereConditions(context, builder, where);
|
|
66
|
+
|
|
67
|
+
const row = await builder.executeTakeFirst();
|
|
35
68
|
if (!row) {
|
|
36
69
|
return null;
|
|
37
70
|
}
|
|
71
|
+
|
|
38
72
|
return camelCaseObject(row);
|
|
39
73
|
}
|
|
40
74
|
|
|
41
75
|
async findMany(where) {
|
|
42
|
-
let
|
|
76
|
+
let builder = this._db
|
|
77
|
+
.selectFrom(this._tableName)
|
|
78
|
+
.distinctOn(`${this._tableName}.id`)
|
|
79
|
+
.selectAll(this._tableName);
|
|
43
80
|
|
|
44
|
-
|
|
45
|
-
if (Object.keys(where).length > 0) {
|
|
46
|
-
query = query.where((qb) => {
|
|
47
|
-
return applyWhereConditions(qb, where);
|
|
48
|
-
});
|
|
49
|
-
}
|
|
81
|
+
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
50
82
|
|
|
51
|
-
|
|
83
|
+
builder = applyJoins(context, builder, where);
|
|
84
|
+
builder = applyWhereConditions(context, builder, where);
|
|
52
85
|
|
|
86
|
+
const rows = await builder.orderBy("id").execute();
|
|
53
87
|
return rows.map((x) => camelCaseObject(x));
|
|
54
88
|
}
|
|
55
89
|
|
|
56
90
|
async update(where, values) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
91
|
+
let builder = this._db.updateTable(this._tableName).returningAll();
|
|
92
|
+
|
|
93
|
+
builder = builder.set(snakeCaseObject(values));
|
|
94
|
+
|
|
95
|
+
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
96
|
+
|
|
97
|
+
// TODO: support joins for update
|
|
98
|
+
builder = applyWhereConditions(context, builder, where);
|
|
99
|
+
|
|
100
|
+
const row = await builder.executeTakeFirstOrThrow();
|
|
65
101
|
return camelCaseObject(row);
|
|
66
102
|
}
|
|
67
103
|
|
|
68
104
|
async delete(where) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
let builder = this._db.deleteFrom(this._tableName).returning(["id"]);
|
|
106
|
+
|
|
107
|
+
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
108
|
+
|
|
109
|
+
// TODO: support joins for delete
|
|
110
|
+
builder = applyWhereConditions(context, builder, where);
|
|
111
|
+
|
|
112
|
+
const row = await builder.executeTakeFirstOrThrow();
|
|
76
113
|
return row.id;
|
|
77
114
|
}
|
|
78
115
|
|
|
79
|
-
where(
|
|
80
|
-
|
|
116
|
+
where(where) {
|
|
117
|
+
let builder = this._db
|
|
81
118
|
.selectFrom(this._tableName)
|
|
82
|
-
.
|
|
83
|
-
.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
119
|
+
.distinctOn(`${this._tableName}.id`)
|
|
120
|
+
.selectAll(this._tableName);
|
|
121
|
+
|
|
122
|
+
const context = new QueryContext([this._tableName], this._tableConfigMap);
|
|
123
|
+
|
|
124
|
+
builder = applyJoins(context, builder, where);
|
|
125
|
+
builder = applyWhereConditions(context, builder, where);
|
|
126
|
+
|
|
127
|
+
return new QueryBuilder(context, builder);
|
|
87
128
|
}
|
|
88
129
|
}
|
|
89
130
|
|
|
90
|
-
module.exports
|
|
131
|
+
module.exports = {
|
|
132
|
+
ModelAPI,
|
|
133
|
+
};
|
package/src/ModelAPI.test.js
CHANGED
|
@@ -7,36 +7,72 @@ const KSUID = require("ksuid");
|
|
|
7
7
|
process.env.DB_CONN_TYPE = "pg";
|
|
8
8
|
process.env.DB_CONN = `postgresql://postgres:postgres@localhost:5432/functions-runtime`;
|
|
9
9
|
|
|
10
|
-
let
|
|
10
|
+
let personAPI;
|
|
11
|
+
let postAPI;
|
|
11
12
|
|
|
12
13
|
beforeEach(async () => {
|
|
13
14
|
const db = getDatabase();
|
|
14
15
|
|
|
15
16
|
await sql`
|
|
16
|
-
DROP TABLE IF EXISTS
|
|
17
|
-
|
|
17
|
+
DROP TABLE IF EXISTS post;
|
|
18
|
+
DROP TABLE IF EXISTS person;
|
|
19
|
+
CREATE TABLE person(
|
|
18
20
|
id text PRIMARY KEY,
|
|
19
21
|
name text UNIQUE,
|
|
20
22
|
married boolean,
|
|
21
23
|
favourite_number integer,
|
|
22
24
|
date timestamp
|
|
23
25
|
);
|
|
26
|
+
CREATE TABLE post(
|
|
27
|
+
id text PRIMARY KEY,
|
|
28
|
+
title text,
|
|
29
|
+
author_id text references person(id)
|
|
30
|
+
);
|
|
24
31
|
`.execute(db);
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
const tableConfigMap = {
|
|
34
|
+
person: {
|
|
35
|
+
posts: {
|
|
36
|
+
relationshipType: "hasMany",
|
|
37
|
+
foreignKey: "author_id",
|
|
38
|
+
referencesTable: "post",
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
post: {
|
|
42
|
+
author: {
|
|
43
|
+
relationshipType: "belongsTo",
|
|
44
|
+
foreignKey: "author_id",
|
|
45
|
+
referencesTable: "person",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
personAPI = new ModelAPI(
|
|
51
|
+
"person",
|
|
28
52
|
() => {
|
|
29
53
|
return {
|
|
30
54
|
id: KSUID.randomSync().string,
|
|
31
55
|
date: new Date("2022-01-01"),
|
|
32
56
|
};
|
|
33
57
|
},
|
|
34
|
-
db
|
|
58
|
+
db,
|
|
59
|
+
tableConfigMap
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
postAPI = new ModelAPI(
|
|
63
|
+
"post",
|
|
64
|
+
() => {
|
|
65
|
+
return {
|
|
66
|
+
id: KSUID.randomSync().string,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
db,
|
|
70
|
+
tableConfigMap
|
|
35
71
|
);
|
|
36
72
|
});
|
|
37
73
|
|
|
38
74
|
test("ModelAPI.create", async () => {
|
|
39
|
-
const row = await
|
|
75
|
+
const row = await personAPI.create({
|
|
40
76
|
name: "Jim",
|
|
41
77
|
married: false,
|
|
42
78
|
favouriteNumber: 10,
|
|
@@ -49,58 +85,77 @@ test("ModelAPI.create", async () => {
|
|
|
49
85
|
});
|
|
50
86
|
|
|
51
87
|
test("ModelAPI.create - throws if database constraint fails", async () => {
|
|
52
|
-
const row = await
|
|
88
|
+
const row = await personAPI.create({
|
|
53
89
|
name: "Jim",
|
|
54
90
|
married: false,
|
|
55
91
|
favouriteNumber: 10,
|
|
56
92
|
});
|
|
57
|
-
const promise =
|
|
93
|
+
const promise = personAPI.create({
|
|
58
94
|
id: row.id,
|
|
59
95
|
name: "Jim",
|
|
60
96
|
married: false,
|
|
61
97
|
favouriteNumber: 10,
|
|
62
98
|
});
|
|
63
99
|
await expect(promise).rejects.toThrow(
|
|
64
|
-
`duplicate key value violates unique constraint "
|
|
100
|
+
`duplicate key value violates unique constraint "person_pkey"`
|
|
65
101
|
);
|
|
66
102
|
});
|
|
67
103
|
|
|
68
104
|
test("ModelAPI.findOne", async () => {
|
|
69
|
-
const created = await
|
|
105
|
+
const created = await personAPI.create({
|
|
70
106
|
name: "Jim",
|
|
71
107
|
married: false,
|
|
72
108
|
favouriteNumber: 10,
|
|
73
109
|
});
|
|
74
|
-
const row = await
|
|
110
|
+
const row = await personAPI.findOne({
|
|
75
111
|
id: created.id,
|
|
76
112
|
});
|
|
77
113
|
expect(row).toEqual(created);
|
|
78
114
|
});
|
|
79
115
|
|
|
116
|
+
test("ModelAPI.findOne - relationships - one to many", async () => {
|
|
117
|
+
const person = await personAPI.create({
|
|
118
|
+
name: "Jim",
|
|
119
|
+
married: false,
|
|
120
|
+
favouriteNumber: 10,
|
|
121
|
+
});
|
|
122
|
+
const post = await postAPI.create({
|
|
123
|
+
title: "My Post",
|
|
124
|
+
authorId: person.id,
|
|
125
|
+
});
|
|
126
|
+
const row = await personAPI.findOne({
|
|
127
|
+
posts: {
|
|
128
|
+
id: post.id,
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
expect(row.name).toEqual("Jim");
|
|
132
|
+
expect(row.id).toEqual(person.id);
|
|
133
|
+
});
|
|
134
|
+
|
|
80
135
|
test("ModelAPI.findOne - return null if not found", async () => {
|
|
81
|
-
const row = await
|
|
136
|
+
const row = await personAPI.findOne({
|
|
82
137
|
id: "doesntexist",
|
|
83
138
|
});
|
|
84
139
|
expect(row).toEqual(null);
|
|
85
140
|
});
|
|
86
141
|
|
|
87
142
|
test("ModelAPI.findMany", async () => {
|
|
88
|
-
const jim = await
|
|
143
|
+
const jim = await personAPI.create({
|
|
89
144
|
name: "Jim",
|
|
90
145
|
married: false,
|
|
91
146
|
favouriteNumber: 10,
|
|
92
147
|
});
|
|
93
|
-
const bob = await
|
|
148
|
+
const bob = await personAPI.create({
|
|
94
149
|
name: "Bob",
|
|
95
150
|
married: true,
|
|
96
151
|
favouriteNumber: 11,
|
|
97
152
|
});
|
|
98
|
-
const sally = await
|
|
153
|
+
const sally = await personAPI.create({
|
|
99
154
|
name: "Sally",
|
|
100
155
|
married: true,
|
|
101
156
|
favouriteNumber: 12,
|
|
102
157
|
});
|
|
103
|
-
const rows = await
|
|
158
|
+
const rows = await personAPI.findMany({
|
|
104
159
|
married: true,
|
|
105
160
|
});
|
|
106
161
|
expect(rows.length).toEqual(2);
|
|
@@ -108,26 +163,26 @@ test("ModelAPI.findMany", async () => {
|
|
|
108
163
|
});
|
|
109
164
|
|
|
110
165
|
test("ModelAPI.findMany - no where conditions", async () => {
|
|
111
|
-
const jim = await
|
|
166
|
+
const jim = await personAPI.create({
|
|
112
167
|
name: "Jim",
|
|
113
168
|
});
|
|
114
|
-
await
|
|
169
|
+
await personAPI.create({
|
|
115
170
|
name: "Bob",
|
|
116
171
|
});
|
|
117
172
|
|
|
118
|
-
const rows = await
|
|
173
|
+
const rows = await personAPI.findMany({});
|
|
119
174
|
|
|
120
175
|
expect(rows.length).toEqual(2);
|
|
121
176
|
});
|
|
122
177
|
|
|
123
178
|
test("ModelAPI.findMany - startsWith", async () => {
|
|
124
|
-
const jim = await
|
|
179
|
+
const jim = await personAPI.create({
|
|
125
180
|
name: "Jim",
|
|
126
181
|
});
|
|
127
|
-
await
|
|
182
|
+
await personAPI.create({
|
|
128
183
|
name: "Bob",
|
|
129
184
|
});
|
|
130
|
-
const rows = await
|
|
185
|
+
const rows = await personAPI.findMany({
|
|
131
186
|
name: {
|
|
132
187
|
startsWith: "Ji",
|
|
133
188
|
},
|
|
@@ -137,13 +192,13 @@ test("ModelAPI.findMany - startsWith", async () => {
|
|
|
137
192
|
});
|
|
138
193
|
|
|
139
194
|
test("ModelAPI.findMany - endsWith", async () => {
|
|
140
|
-
const jim = await
|
|
195
|
+
const jim = await personAPI.create({
|
|
141
196
|
name: "Jim",
|
|
142
197
|
});
|
|
143
|
-
await
|
|
198
|
+
await personAPI.create({
|
|
144
199
|
name: "Bob",
|
|
145
200
|
});
|
|
146
|
-
const rows = await
|
|
201
|
+
const rows = await personAPI.findMany({
|
|
147
202
|
name: {
|
|
148
203
|
endsWith: "im",
|
|
149
204
|
},
|
|
@@ -153,16 +208,16 @@ test("ModelAPI.findMany - endsWith", async () => {
|
|
|
153
208
|
});
|
|
154
209
|
|
|
155
210
|
test("ModelAPI.findMany - contains", async () => {
|
|
156
|
-
const billy = await
|
|
211
|
+
const billy = await personAPI.create({
|
|
157
212
|
name: "Billy",
|
|
158
213
|
});
|
|
159
|
-
const sally = await
|
|
214
|
+
const sally = await personAPI.create({
|
|
160
215
|
name: "Sally",
|
|
161
216
|
});
|
|
162
|
-
await
|
|
217
|
+
await personAPI.create({
|
|
163
218
|
name: "Jim",
|
|
164
219
|
});
|
|
165
|
-
const rows = await
|
|
220
|
+
const rows = await personAPI.findMany({
|
|
166
221
|
name: {
|
|
167
222
|
contains: "ll",
|
|
168
223
|
},
|
|
@@ -172,16 +227,16 @@ test("ModelAPI.findMany - contains", async () => {
|
|
|
172
227
|
});
|
|
173
228
|
|
|
174
229
|
test("ModelAPI.findMany - oneOf", async () => {
|
|
175
|
-
const billy = await
|
|
230
|
+
const billy = await personAPI.create({
|
|
176
231
|
name: "Billy",
|
|
177
232
|
});
|
|
178
|
-
const sally = await
|
|
233
|
+
const sally = await personAPI.create({
|
|
179
234
|
name: "Sally",
|
|
180
235
|
});
|
|
181
|
-
await
|
|
236
|
+
await personAPI.create({
|
|
182
237
|
name: "Jim",
|
|
183
238
|
});
|
|
184
|
-
const rows = await
|
|
239
|
+
const rows = await personAPI.findMany({
|
|
185
240
|
name: {
|
|
186
241
|
oneOf: ["Billy", "Sally"],
|
|
187
242
|
},
|
|
@@ -191,13 +246,13 @@ test("ModelAPI.findMany - oneOf", async () => {
|
|
|
191
246
|
});
|
|
192
247
|
|
|
193
248
|
test("ModelAPI.findMany - greaterThan", async () => {
|
|
194
|
-
await
|
|
249
|
+
await personAPI.create({
|
|
195
250
|
favouriteNumber: 1,
|
|
196
251
|
});
|
|
197
|
-
const p = await
|
|
252
|
+
const p = await personAPI.create({
|
|
198
253
|
favouriteNumber: 2,
|
|
199
254
|
});
|
|
200
|
-
const rows = await
|
|
255
|
+
const rows = await personAPI.findMany({
|
|
201
256
|
favouriteNumber: {
|
|
202
257
|
greaterThan: 1,
|
|
203
258
|
},
|
|
@@ -207,16 +262,16 @@ test("ModelAPI.findMany - greaterThan", async () => {
|
|
|
207
262
|
});
|
|
208
263
|
|
|
209
264
|
test("ModelAPI.findMany - greaterThanOrEquals", async () => {
|
|
210
|
-
await
|
|
265
|
+
await personAPI.create({
|
|
211
266
|
favouriteNumber: 1,
|
|
212
267
|
});
|
|
213
|
-
const p = await
|
|
268
|
+
const p = await personAPI.create({
|
|
214
269
|
favouriteNumber: 2,
|
|
215
270
|
});
|
|
216
|
-
const p2 = await
|
|
271
|
+
const p2 = await personAPI.create({
|
|
217
272
|
favouriteNumber: 3,
|
|
218
273
|
});
|
|
219
|
-
const rows = await
|
|
274
|
+
const rows = await personAPI.findMany({
|
|
220
275
|
favouriteNumber: {
|
|
221
276
|
greaterThanOrEquals: 2,
|
|
222
277
|
},
|
|
@@ -226,13 +281,13 @@ test("ModelAPI.findMany - greaterThanOrEquals", async () => {
|
|
|
226
281
|
});
|
|
227
282
|
|
|
228
283
|
test("ModelAPI.findMany - lessThan", async () => {
|
|
229
|
-
const p = await
|
|
284
|
+
const p = await personAPI.create({
|
|
230
285
|
favouriteNumber: 1,
|
|
231
286
|
});
|
|
232
|
-
await
|
|
287
|
+
await personAPI.create({
|
|
233
288
|
favouriteNumber: 2,
|
|
234
289
|
});
|
|
235
|
-
const rows = await
|
|
290
|
+
const rows = await personAPI.findMany({
|
|
236
291
|
favouriteNumber: {
|
|
237
292
|
lessThan: 2,
|
|
238
293
|
},
|
|
@@ -242,16 +297,16 @@ test("ModelAPI.findMany - lessThan", async () => {
|
|
|
242
297
|
});
|
|
243
298
|
|
|
244
299
|
test("ModelAPI.findMany - lessThanOrEquals", async () => {
|
|
245
|
-
const p = await
|
|
300
|
+
const p = await personAPI.create({
|
|
246
301
|
favouriteNumber: 1,
|
|
247
302
|
});
|
|
248
|
-
const p2 = await
|
|
303
|
+
const p2 = await personAPI.create({
|
|
249
304
|
favouriteNumber: 2,
|
|
250
305
|
});
|
|
251
|
-
await
|
|
306
|
+
await personAPI.create({
|
|
252
307
|
favouriteNumber: 3,
|
|
253
308
|
});
|
|
254
|
-
const rows = await
|
|
309
|
+
const rows = await personAPI.findMany({
|
|
255
310
|
favouriteNumber: {
|
|
256
311
|
lessThanOrEquals: 2,
|
|
257
312
|
},
|
|
@@ -261,13 +316,13 @@ test("ModelAPI.findMany - lessThanOrEquals", async () => {
|
|
|
261
316
|
});
|
|
262
317
|
|
|
263
318
|
test("ModelAPI.findMany - before", async () => {
|
|
264
|
-
const p = await
|
|
319
|
+
const p = await personAPI.create({
|
|
265
320
|
date: new Date("2022-01-01"),
|
|
266
321
|
});
|
|
267
|
-
await
|
|
322
|
+
await personAPI.create({
|
|
268
323
|
date: new Date("2022-01-02"),
|
|
269
324
|
});
|
|
270
|
-
const rows = await
|
|
325
|
+
const rows = await personAPI.findMany({
|
|
271
326
|
date: {
|
|
272
327
|
before: new Date("2022-01-02"),
|
|
273
328
|
},
|
|
@@ -277,16 +332,16 @@ test("ModelAPI.findMany - before", async () => {
|
|
|
277
332
|
});
|
|
278
333
|
|
|
279
334
|
test("ModelAPI.findMany - onOrBefore", async () => {
|
|
280
|
-
const p = await
|
|
335
|
+
const p = await personAPI.create({
|
|
281
336
|
date: new Date("2022-01-01"),
|
|
282
337
|
});
|
|
283
|
-
const p2 = await
|
|
338
|
+
const p2 = await personAPI.create({
|
|
284
339
|
date: new Date("2022-01-02"),
|
|
285
340
|
});
|
|
286
|
-
await
|
|
341
|
+
await personAPI.create({
|
|
287
342
|
date: new Date("2022-01-03"),
|
|
288
343
|
});
|
|
289
|
-
const rows = await
|
|
344
|
+
const rows = await personAPI.findMany({
|
|
290
345
|
date: {
|
|
291
346
|
onOrBefore: new Date("2022-01-02"),
|
|
292
347
|
},
|
|
@@ -296,13 +351,13 @@ test("ModelAPI.findMany - onOrBefore", async () => {
|
|
|
296
351
|
});
|
|
297
352
|
|
|
298
353
|
test("ModelAPI.findMany - after", async () => {
|
|
299
|
-
await
|
|
354
|
+
await personAPI.create({
|
|
300
355
|
date: new Date("2022-01-01"),
|
|
301
356
|
});
|
|
302
|
-
const p = await
|
|
357
|
+
const p = await personAPI.create({
|
|
303
358
|
date: new Date("2022-01-02"),
|
|
304
359
|
});
|
|
305
|
-
const rows = await
|
|
360
|
+
const rows = await personAPI.findMany({
|
|
306
361
|
date: {
|
|
307
362
|
after: new Date("2022-01-01"),
|
|
308
363
|
},
|
|
@@ -312,16 +367,16 @@ test("ModelAPI.findMany - after", async () => {
|
|
|
312
367
|
});
|
|
313
368
|
|
|
314
369
|
test("ModelAPI.findMany - onOrAfter", async () => {
|
|
315
|
-
await
|
|
370
|
+
await personAPI.create({
|
|
316
371
|
date: new Date("2022-01-01"),
|
|
317
372
|
});
|
|
318
|
-
const p = await
|
|
373
|
+
const p = await personAPI.create({
|
|
319
374
|
date: new Date("2022-01-02"),
|
|
320
375
|
});
|
|
321
|
-
const p2 = await
|
|
376
|
+
const p2 = await personAPI.create({
|
|
322
377
|
date: new Date("2022-01-03"),
|
|
323
378
|
});
|
|
324
|
-
const rows = await
|
|
379
|
+
const rows = await personAPI.findMany({
|
|
325
380
|
date: {
|
|
326
381
|
onOrAfter: new Date("2022-01-02"),
|
|
327
382
|
},
|
|
@@ -331,13 +386,13 @@ test("ModelAPI.findMany - onOrAfter", async () => {
|
|
|
331
386
|
});
|
|
332
387
|
|
|
333
388
|
test("ModelAPI.findMany - equals", async () => {
|
|
334
|
-
const p = await
|
|
389
|
+
const p = await personAPI.create({
|
|
335
390
|
name: "Jim",
|
|
336
391
|
});
|
|
337
|
-
await
|
|
392
|
+
await personAPI.create({
|
|
338
393
|
name: "Sally",
|
|
339
394
|
});
|
|
340
|
-
const rows = await
|
|
395
|
+
const rows = await personAPI.findMany({
|
|
341
396
|
name: {
|
|
342
397
|
equals: "Jim",
|
|
343
398
|
},
|
|
@@ -347,13 +402,13 @@ test("ModelAPI.findMany - equals", async () => {
|
|
|
347
402
|
});
|
|
348
403
|
|
|
349
404
|
test("ModelAPI.findMany - notEquals", async () => {
|
|
350
|
-
const p = await
|
|
405
|
+
const p = await personAPI.create({
|
|
351
406
|
name: "Jim",
|
|
352
407
|
});
|
|
353
|
-
await
|
|
408
|
+
await personAPI.create({
|
|
354
409
|
name: "Sally",
|
|
355
410
|
});
|
|
356
|
-
const rows = await
|
|
411
|
+
const rows = await personAPI.findMany({
|
|
357
412
|
name: {
|
|
358
413
|
notEquals: "Sally",
|
|
359
414
|
},
|
|
@@ -363,23 +418,23 @@ test("ModelAPI.findMany - notEquals", async () => {
|
|
|
363
418
|
});
|
|
364
419
|
|
|
365
420
|
test("ModelAPI.findMany - complex query", async () => {
|
|
366
|
-
const p = await
|
|
421
|
+
const p = await personAPI.create({
|
|
367
422
|
name: "Jake",
|
|
368
423
|
favouriteNumber: 8,
|
|
369
424
|
date: new Date("2021-12-31"),
|
|
370
425
|
});
|
|
371
|
-
await
|
|
426
|
+
await personAPI.create({
|
|
372
427
|
name: "Jane",
|
|
373
428
|
favouriteNumber: 12,
|
|
374
429
|
date: new Date("2022-01-11"),
|
|
375
430
|
});
|
|
376
|
-
const p2 = await
|
|
431
|
+
const p2 = await personAPI.create({
|
|
377
432
|
name: "Billy",
|
|
378
433
|
favouriteNumber: 16,
|
|
379
434
|
date: new Date("2022-01-05"),
|
|
380
435
|
});
|
|
381
436
|
|
|
382
|
-
const rows = await
|
|
437
|
+
const rows = await personAPI
|
|
383
438
|
// Will match Jake
|
|
384
439
|
.where({
|
|
385
440
|
name: {
|
|
@@ -402,18 +457,113 @@ test("ModelAPI.findMany - complex query", async () => {
|
|
|
402
457
|
expect(rows.map((x) => x.id).sort()).toEqual([p.id, p2.id].sort());
|
|
403
458
|
});
|
|
404
459
|
|
|
460
|
+
test("ModelAPI.findMany - relationships - one to many", async () => {
|
|
461
|
+
const person = await personAPI.create({
|
|
462
|
+
name: "Jim",
|
|
463
|
+
});
|
|
464
|
+
const person2 = await personAPI.create({
|
|
465
|
+
name: "Bob",
|
|
466
|
+
});
|
|
467
|
+
const post1 = await postAPI.create({
|
|
468
|
+
title: "My First Post",
|
|
469
|
+
authorId: person.id,
|
|
470
|
+
});
|
|
471
|
+
const post2 = await postAPI.create({
|
|
472
|
+
title: "My Second Post",
|
|
473
|
+
authorId: person.id,
|
|
474
|
+
});
|
|
475
|
+
await postAPI.create({
|
|
476
|
+
title: "My Third Post",
|
|
477
|
+
authorId: person2.id,
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const posts = await postAPI.findMany({
|
|
481
|
+
author: {
|
|
482
|
+
name: "Jim",
|
|
483
|
+
},
|
|
484
|
+
});
|
|
485
|
+
expect(posts.length).toEqual(2);
|
|
486
|
+
expect(posts.map((x) => x.id).sort()).toEqual([post1.id, post2.id].sort());
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("ModelAPI.findMany - relationships - many to one", async () => {
|
|
490
|
+
const person = await personAPI.create({
|
|
491
|
+
name: "Jim",
|
|
492
|
+
});
|
|
493
|
+
await postAPI.create({
|
|
494
|
+
title: "My First Post",
|
|
495
|
+
authorId: person.id,
|
|
496
|
+
});
|
|
497
|
+
await postAPI.create({
|
|
498
|
+
title: "My Second Post",
|
|
499
|
+
authorId: person.id,
|
|
500
|
+
});
|
|
501
|
+
await postAPI.create({
|
|
502
|
+
title: "My Second Post",
|
|
503
|
+
authorId: person.id,
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const people = await personAPI.findMany({
|
|
507
|
+
posts: {
|
|
508
|
+
title: {
|
|
509
|
+
startsWith: "My ",
|
|
510
|
+
endsWith: " Post",
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
// This tests that many to one joins work for findMany() but also
|
|
516
|
+
// that the same row is not returned more than once e.g. Jim has
|
|
517
|
+
// three posts but should only be returned once
|
|
518
|
+
expect(people.length).toEqual(1);
|
|
519
|
+
expect(people[0].id).toEqual(person.id);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("ModelAPI.findMany - relationships - duplicate joins handled", async () => {
|
|
523
|
+
const person = await personAPI.create({
|
|
524
|
+
name: "Jim",
|
|
525
|
+
});
|
|
526
|
+
const person2 = await personAPI.create({
|
|
527
|
+
name: "Bob",
|
|
528
|
+
});
|
|
529
|
+
const post1 = await postAPI.create({
|
|
530
|
+
title: "My First Post",
|
|
531
|
+
authorId: person.id,
|
|
532
|
+
});
|
|
533
|
+
const post2 = await postAPI.create({
|
|
534
|
+
title: "My Second Post",
|
|
535
|
+
authorId: person2.id,
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
const posts = await postAPI
|
|
539
|
+
.where({
|
|
540
|
+
author: {
|
|
541
|
+
name: "Jim",
|
|
542
|
+
},
|
|
543
|
+
})
|
|
544
|
+
.orWhere({
|
|
545
|
+
author: {
|
|
546
|
+
name: "Bob",
|
|
547
|
+
},
|
|
548
|
+
})
|
|
549
|
+
.findMany();
|
|
550
|
+
|
|
551
|
+
expect(posts.length).toEqual(2);
|
|
552
|
+
expect(posts.map((x) => x.id).sort()).toEqual([post1.id, post2.id].sort());
|
|
553
|
+
});
|
|
554
|
+
|
|
405
555
|
test("ModelAPI.update", async () => {
|
|
406
|
-
let jim = await
|
|
556
|
+
let jim = await personAPI.create({
|
|
407
557
|
name: "Jim",
|
|
408
558
|
married: false,
|
|
409
559
|
favouriteNumber: 10,
|
|
410
560
|
});
|
|
411
|
-
let bob = await
|
|
561
|
+
let bob = await personAPI.create({
|
|
412
562
|
name: "Bob",
|
|
413
563
|
married: false,
|
|
414
564
|
favouriteNumber: 11,
|
|
415
565
|
});
|
|
416
|
-
jim = await
|
|
566
|
+
jim = await personAPI.update(
|
|
417
567
|
{
|
|
418
568
|
id: jim.id,
|
|
419
569
|
},
|
|
@@ -424,12 +574,12 @@ test("ModelAPI.update", async () => {
|
|
|
424
574
|
expect(jim.married).toEqual(true);
|
|
425
575
|
expect(jim.name).toEqual("Jim");
|
|
426
576
|
|
|
427
|
-
bob = await
|
|
577
|
+
bob = await personAPI.findOne({ id: bob.id });
|
|
428
578
|
expect(bob.married).toEqual(false);
|
|
429
579
|
});
|
|
430
580
|
|
|
431
581
|
test("ModelAPI.update - throws if not found", async () => {
|
|
432
|
-
const result =
|
|
582
|
+
const result = personAPI.update(
|
|
433
583
|
{
|
|
434
584
|
id: "doesntexist",
|
|
435
585
|
},
|
|
@@ -441,14 +591,14 @@ test("ModelAPI.update - throws if not found", async () => {
|
|
|
441
591
|
});
|
|
442
592
|
|
|
443
593
|
test("ModelAPI.delete", async () => {
|
|
444
|
-
const jim = await
|
|
594
|
+
const jim = await personAPI.create({
|
|
445
595
|
name: "Jim",
|
|
446
596
|
});
|
|
447
597
|
const id = jim.id;
|
|
448
|
-
const deletedId = await
|
|
598
|
+
const deletedId = await personAPI.delete({
|
|
449
599
|
name: "Jim",
|
|
450
600
|
});
|
|
451
601
|
|
|
452
602
|
expect(deletedId).toEqual(id);
|
|
453
|
-
await expect(
|
|
603
|
+
await expect(personAPI.findOne({ id })).resolves.toEqual(null);
|
|
454
604
|
});
|
package/src/QueryBuilder.js
CHANGED
|
@@ -1,27 +1,40 @@
|
|
|
1
1
|
const { applyWhereConditions } = require("./applyWhereConditions");
|
|
2
|
+
const { applyJoins } = require("./applyJoins");
|
|
2
3
|
const { camelCaseObject } = require("./casing");
|
|
3
4
|
|
|
4
5
|
class QueryBuilder {
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* @param {import("./QueryContext").QueryContext} context
|
|
8
|
+
* @param {import("kysely").Kysely} db
|
|
9
|
+
*/
|
|
10
|
+
constructor(context, db) {
|
|
11
|
+
this._context = context;
|
|
6
12
|
this._db = db;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
|
-
where(
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
15
|
+
where(where) {
|
|
16
|
+
const context = this._context.clone();
|
|
17
|
+
|
|
18
|
+
let builder = applyJoins(context, this._db, where);
|
|
19
|
+
builder = applyWhereConditions(context, builder, where);
|
|
20
|
+
|
|
21
|
+
return new QueryBuilder(context, builder);
|
|
14
22
|
}
|
|
15
23
|
|
|
16
|
-
orWhere(
|
|
17
|
-
const
|
|
18
|
-
|
|
24
|
+
orWhere(where) {
|
|
25
|
+
const context = this._context.clone();
|
|
26
|
+
|
|
27
|
+
let builder = applyJoins(context, this._db, where);
|
|
28
|
+
|
|
29
|
+
builder = builder.orWhere((qb) => {
|
|
30
|
+
return applyWhereConditions(context, qb, where);
|
|
19
31
|
});
|
|
20
|
-
|
|
32
|
+
|
|
33
|
+
return new QueryBuilder(context, builder);
|
|
21
34
|
}
|
|
22
35
|
|
|
23
36
|
async findMany() {
|
|
24
|
-
const rows = await this._db.execute();
|
|
37
|
+
const rows = await this._db.orderBy("id").execute();
|
|
25
38
|
return rows.map((x) => camelCaseObject(x));
|
|
26
39
|
}
|
|
27
40
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueryContext is used to store state about the current query, for example
|
|
3
|
+
* which joins have already been applied. It is used by applyJoins and
|
|
4
|
+
* applyWhereConditions to generate consistent table aliases for joins.
|
|
5
|
+
*
|
|
6
|
+
* This class has the concept of a "table path". This is just a list of tables, starting
|
|
7
|
+
* with some "root" table and ending with the table we're currently joining to. So
|
|
8
|
+
* for example if we started with a "product" table and joined from there to "order_item"
|
|
9
|
+
* and then to "order" and then to "customer" the table path would be:
|
|
10
|
+
* ["product", "order_item", "order", "customer"]
|
|
11
|
+
* At this point the "current" table is "customer" and it's alias would be:
|
|
12
|
+
* "product$order_item$order$customer"
|
|
13
|
+
*/
|
|
14
|
+
class QueryContext {
|
|
15
|
+
/**
|
|
16
|
+
* @param {string[]} tablePath This is the path from the "root" table to the "current table".
|
|
17
|
+
* @param {import("./ModelAPI").TableConfigMap} tableConfigMap
|
|
18
|
+
* @param {string[]} joins
|
|
19
|
+
*/
|
|
20
|
+
constructor(tablePath, tableConfigMap, joins = []) {
|
|
21
|
+
this._tablePath = tablePath;
|
|
22
|
+
this._tableConfigMap = tableConfigMap;
|
|
23
|
+
this._joins = joins;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
clone() {
|
|
27
|
+
return new QueryContext([...this._tablePath], this._tableConfigMap, [
|
|
28
|
+
...this._joins,
|
|
29
|
+
]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns true if, given the current table path, a join to the given
|
|
34
|
+
* table has already been added.
|
|
35
|
+
* @param {string} table
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
hasJoin(table) {
|
|
39
|
+
const alias = joinAlias([...this._tablePath, table]);
|
|
40
|
+
return this._joins.includes(alias);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Adds table to the QueryContext's path and registers the join,
|
|
45
|
+
* calls fn, then pops the table off the path.
|
|
46
|
+
* @param {string} table
|
|
47
|
+
* @param {Function} fn
|
|
48
|
+
*/
|
|
49
|
+
withJoin(table, fn) {
|
|
50
|
+
this._tablePath.push(table);
|
|
51
|
+
this._joins.push(this.tableAlias());
|
|
52
|
+
|
|
53
|
+
fn();
|
|
54
|
+
|
|
55
|
+
// Don't change the _joins list, we want to remember those
|
|
56
|
+
this._tablePath.pop();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns the alias that will be used for the current table
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
tableAlias() {
|
|
64
|
+
return joinAlias(this._tablePath);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Returns the current table name
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
tableName() {
|
|
72
|
+
return this._tablePath[this._tablePath.length - 1];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Return the TableConfig for the current table
|
|
77
|
+
* @returns {import("./ModelAPI").TableConfig | undefined}
|
|
78
|
+
*/
|
|
79
|
+
tableConfig() {
|
|
80
|
+
return this._tableConfigMap[this.tableName()];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function joinAlias(tablePath) {
|
|
85
|
+
return tablePath.join("$");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
module.exports = {
|
|
89
|
+
QueryContext,
|
|
90
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds the joins required by the where conditions to the given
|
|
3
|
+
* Kysely instance and returns the resulting new Kysely instance.
|
|
4
|
+
* @param {import("./QueryContext").QueryContext} context
|
|
5
|
+
* @param {import("kysely").Kysely} qb
|
|
6
|
+
* @param {Object} where
|
|
7
|
+
* @returns {import("kysely").Kysely}
|
|
8
|
+
*/
|
|
9
|
+
function applyJoins(context, qb, where) {
|
|
10
|
+
const conf = context.tableConfig();
|
|
11
|
+
if (!conf) {
|
|
12
|
+
return qb;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const srcTableName = context.tableName();
|
|
16
|
+
|
|
17
|
+
for (const key of Object.keys(where)) {
|
|
18
|
+
const rel = conf[key];
|
|
19
|
+
if (!rel) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const targetTable = rel.referencesTable;
|
|
24
|
+
|
|
25
|
+
if (context.hasJoin(targetTable)) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
context.withJoin(targetTable, () => {
|
|
30
|
+
switch (rel.relationshipType) {
|
|
31
|
+
case "hasMany":
|
|
32
|
+
// For hasMany the primary key is on the source table
|
|
33
|
+
// and the foreign key is on the target table
|
|
34
|
+
qb = qb.innerJoin(
|
|
35
|
+
`${targetTable} as ${context.tableAlias()}`,
|
|
36
|
+
`${srcTableName}.id`,
|
|
37
|
+
`${context.tableAlias()}.${rel.foreignKey}`
|
|
38
|
+
);
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
case "belongsTo":
|
|
42
|
+
// For belongsTo the primary key is on the target table
|
|
43
|
+
// and the foreign key is on the source table
|
|
44
|
+
qb = qb.innerJoin(
|
|
45
|
+
`${targetTable} as ${context.tableAlias()}`,
|
|
46
|
+
`${srcTableName}.${rel.foreignKey}`,
|
|
47
|
+
`${context.tableAlias()}.id`
|
|
48
|
+
);
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
throw new Error(`unknown relationshipType: ${rel.relationshipType}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Keep traversing through the where conditions to see if
|
|
55
|
+
// more joins need to be applied
|
|
56
|
+
qb = applyJoins(context, qb, where[key]);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return qb;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = {
|
|
64
|
+
applyJoins,
|
|
65
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const { snakeCase } = require("./casing");
|
|
2
1
|
const { sql } = require("kysely");
|
|
2
|
+
const { snakeCase } = require("./casing");
|
|
3
3
|
|
|
4
4
|
const opMapping = {
|
|
5
5
|
startsWith: { op: "like", value: (v) => `${v}%` },
|
|
@@ -18,10 +18,30 @@ const opMapping = {
|
|
|
18
18
|
notEquals: { op: sql`is distinct from` },
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Applies the given where conditions to the provided Kysely
|
|
23
|
+
* instance and returns the resulting new Kysely instance.
|
|
24
|
+
* @param {import("./QueryContext").QueryContext} context
|
|
25
|
+
* @param {import("kysely").Kysely} qb
|
|
26
|
+
* @param {Object} where
|
|
27
|
+
* @returns {import("kysely").Kysely}
|
|
28
|
+
*/
|
|
29
|
+
function applyWhereConditions(context, qb, where) {
|
|
30
|
+
const conf = context.tableConfig();
|
|
31
|
+
|
|
22
32
|
for (const key of Object.keys(where)) {
|
|
23
33
|
const v = where[key];
|
|
24
|
-
|
|
34
|
+
|
|
35
|
+
// Handle nested where conditions e.g. using a join table
|
|
36
|
+
if (conf && conf[key]) {
|
|
37
|
+
const rel = conf[key];
|
|
38
|
+
context.withJoin(rel.referencesTable, () => {
|
|
39
|
+
qb = applyWhereConditions(context, qb, v);
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fieldName = `${context.tableAlias()}.${snakeCase(key)}`;
|
|
25
45
|
|
|
26
46
|
if (Object.prototype.toString.call(v) !== "[object Object]") {
|
|
27
47
|
qb = qb.where(fieldName, "=", v);
|
|
@@ -45,4 +65,6 @@ function applyWhereConditions(qb, where) {
|
|
|
45
65
|
return qb;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
module.exports
|
|
68
|
+
module.exports = {
|
|
69
|
+
applyWhereConditions,
|
|
70
|
+
};
|