@objectql/driver-knex 1.0.0 → 1.2.0
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/CHANGELOG.md +22 -0
- package/README.md +29 -0
- package/dist/index.d.ts +11 -1
- package/dist/index.js +287 -46
- package/dist/index.js.map +1 -1
- package/package.json +9 -6
- package/src/index.ts +276 -41
- package/test/index.test.ts +95 -45
- package/test/schema.test.ts +253 -0
- package/tsconfig.json +1 -1
- package/tsconfig.tsbuildinfo +1 -1
package/src/index.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { Driver } from '@objectql/
|
|
1
|
+
import { Driver } from '@objectql/types';
|
|
2
2
|
import knex, { Knex } from 'knex';
|
|
3
3
|
|
|
4
4
|
export class KnexDriver implements Driver {
|
|
5
5
|
private knex: Knex;
|
|
6
|
+
private config: any;
|
|
7
|
+
private jsonFields: Record<string, string[]> = {};
|
|
6
8
|
|
|
7
9
|
constructor(config: any) {
|
|
10
|
+
this.config = config;
|
|
8
11
|
this.knex = knex(config);
|
|
9
12
|
}
|
|
10
13
|
|
|
@@ -19,8 +22,6 @@ export class KnexDriver implements Driver {
|
|
|
19
22
|
private applyFilters(builder: Knex.QueryBuilder, filters: any) {
|
|
20
23
|
if (!filters || filters.length === 0) return;
|
|
21
24
|
|
|
22
|
-
// Simple linear parser handling [cond, 'or', cond, 'and', cond]
|
|
23
|
-
// Default join is AND.
|
|
24
25
|
let nextJoin = 'and';
|
|
25
26
|
|
|
26
27
|
for (const item of filters) {
|
|
@@ -31,46 +32,57 @@ export class KnexDriver implements Driver {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
if (Array.isArray(item)) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const apply = (b: any) => {
|
|
38
|
-
// b is the builder to apply on (could be root or a where clause)
|
|
39
|
-
// But here we call directly on builder using 'where' or 'orWhere'
|
|
40
|
-
|
|
41
|
-
// Method selection
|
|
42
|
-
let method = nextJoin === 'or' ? 'orWhere' : 'where';
|
|
43
|
-
let methodIn = nextJoin === 'or' ? 'orWhereIn' : 'whereIn';
|
|
44
|
-
let methodNotIn = nextJoin === 'or' ? 'orWhereNotIn' : 'whereNotIn';
|
|
45
|
-
|
|
46
|
-
switch (op) {
|
|
47
|
-
case '=': b[method](field, value); break;
|
|
48
|
-
case '!=': b[method](field, '<>', value); break;
|
|
49
|
-
case 'in': b[methodIn](field, value); break;
|
|
50
|
-
case 'nin': b[methodNotIn](field, value); break;
|
|
51
|
-
case 'contains': b[method](field, 'like', `%${value}%`); break; // Simple LIKE
|
|
52
|
-
default: b[method](field, op, value);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
35
|
+
// Heuristic to detect if it is a criterion [field, op, value] or a nested group
|
|
36
|
+
const [fieldRaw, op, value] = item;
|
|
37
|
+
const isCriterion = typeof fieldRaw === 'string' && typeof op === 'string';
|
|
55
38
|
|
|
56
|
-
|
|
39
|
+
if (isCriterion) {
|
|
40
|
+
const field = this.mapSortField(fieldRaw);
|
|
41
|
+
// Handle specific operators that map to different knex methods
|
|
42
|
+
const apply = (b: any) => {
|
|
43
|
+
let method = nextJoin === 'or' ? 'orWhere' : 'where';
|
|
44
|
+
let methodIn = nextJoin === 'or' ? 'orWhereIn' : 'whereIn';
|
|
45
|
+
let methodNotIn = nextJoin === 'or' ? 'orWhereNotIn' : 'whereNotIn';
|
|
46
|
+
|
|
47
|
+
// Fix for 'contains' mapping
|
|
48
|
+
if (op === 'contains') {
|
|
49
|
+
b[method](field, 'like', `%${value}%`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
switch (op) {
|
|
54
|
+
case '=': b[method](field, value); break;
|
|
55
|
+
case '!=': b[method](field, '<>', value); break;
|
|
56
|
+
case 'in': b[methodIn](field, value); break;
|
|
57
|
+
case 'nin': b[methodNotIn](field, value); break;
|
|
58
|
+
default: b[method](field, op, value);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
apply(builder);
|
|
62
|
+
} else {
|
|
63
|
+
// Recursive Group
|
|
64
|
+
const method = nextJoin === 'or' ? 'orWhere' : 'where';
|
|
65
|
+
builder[method]((qb) => {
|
|
66
|
+
this.applyFilters(qb, item);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
57
69
|
|
|
58
|
-
// Reset join to 'and' for subsequent terms unless strictly specified?
|
|
59
|
-
// In SQL `A or B and C` means `A or (B and C)`.
|
|
60
|
-
// If we chain `.where(A).orWhere(B).where(C)` in Knex:
|
|
61
|
-
// It produces `... WHERE A OR B AND C`.
|
|
62
|
-
// So linear application matches SQL precedence usually if implicit validation is ok.
|
|
63
|
-
// But explicit AND after OR is necessary in our array format.
|
|
64
70
|
nextJoin = 'and';
|
|
65
71
|
}
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
private mapSortField(field: string): string {
|
|
76
|
+
if (field === 'createdAt') return 'created_at';
|
|
77
|
+
if (field === 'updatedAt') return 'updated_at';
|
|
78
|
+
return field;
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
async find(objectName: string, query: any, options?: any): Promise<any[]> {
|
|
70
82
|
const builder = this.getBuilder(objectName, options);
|
|
71
83
|
|
|
72
84
|
if (query.fields) {
|
|
73
|
-
builder.select(query.fields);
|
|
85
|
+
builder.select(query.fields.map((f: string) => this.mapSortField(f)));
|
|
74
86
|
} else {
|
|
75
87
|
builder.select('*');
|
|
76
88
|
}
|
|
@@ -81,19 +93,26 @@ export class KnexDriver implements Driver {
|
|
|
81
93
|
|
|
82
94
|
if (query.sort) {
|
|
83
95
|
for (const [field, dir] of query.sort) {
|
|
84
|
-
builder.orderBy(field, dir);
|
|
96
|
+
builder.orderBy(this.mapSortField(field), dir);
|
|
85
97
|
}
|
|
86
98
|
}
|
|
87
99
|
|
|
88
100
|
if (query.skip) builder.offset(query.skip);
|
|
89
101
|
if (query.limit) builder.limit(query.limit);
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
const results = await builder;
|
|
104
|
+
if (this.config.client === 'sqlite3') {
|
|
105
|
+
for (const row of results) {
|
|
106
|
+
this.formatOutput(objectName, row);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return results;
|
|
92
110
|
}
|
|
93
111
|
|
|
94
112
|
async findOne(objectName: string, id: string | number, query?: any, options?: any) {
|
|
95
113
|
if (id) {
|
|
96
|
-
|
|
114
|
+
const res = await this.getBuilder(objectName, options).where('id', id).first();
|
|
115
|
+
return this.formatOutput(objectName, res);
|
|
97
116
|
}
|
|
98
117
|
if (query) {
|
|
99
118
|
const results = await this.find(objectName, { ...query, limit: 1 }, options);
|
|
@@ -103,19 +122,32 @@ export class KnexDriver implements Driver {
|
|
|
103
122
|
}
|
|
104
123
|
|
|
105
124
|
async create(objectName: string, data: any, options?: any) {
|
|
125
|
+
// Handle _id -> id mapping for compatibility
|
|
126
|
+
const { _id, ...rest } = data;
|
|
127
|
+
const toInsert = { ...rest };
|
|
128
|
+
// If _id exists and id doesn't, map _id to id
|
|
129
|
+
if (_id !== undefined && toInsert.id === undefined) {
|
|
130
|
+
toInsert.id = _id;
|
|
131
|
+
} else if (toInsert.id !== undefined) {
|
|
132
|
+
// normal case
|
|
133
|
+
}
|
|
134
|
+
|
|
106
135
|
// Knex insert returns Result array (e.g. ids)
|
|
107
136
|
// We want the created document.
|
|
108
|
-
// Some DBs support .returning('*'), others don't easily.
|
|
109
|
-
// Assuming Postgres/SQLite/Modern MySQL for now support returning.
|
|
110
137
|
const builder = this.getBuilder(objectName, options);
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
|
|
139
|
+
const formatted = this.formatInput(objectName, toInsert);
|
|
140
|
+
|
|
141
|
+
// We should insert 'toInsert' instead of 'data'
|
|
142
|
+
const result = await builder.insert(formatted).returning('*');
|
|
143
|
+
return this.formatOutput(objectName, result[0]);
|
|
113
144
|
}
|
|
114
145
|
|
|
115
146
|
async update(objectName: string, id: string | number, data: any, options?: any) {
|
|
116
147
|
const builder = this.getBuilder(objectName, options);
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
const formatted = this.formatInput(objectName, data);
|
|
149
|
+
await builder.where('id', id).update(formatted);
|
|
150
|
+
return { id, ...data };
|
|
119
151
|
}
|
|
120
152
|
|
|
121
153
|
async delete(objectName: string, id: string | number, options?: any) {
|
|
@@ -167,5 +199,208 @@ export class KnexDriver implements Driver {
|
|
|
167
199
|
if(filters) this.applyFilters(builder, filters);
|
|
168
200
|
return await builder.delete();
|
|
169
201
|
}
|
|
202
|
+
|
|
203
|
+
async init(objects: any[]): Promise<void> {
|
|
204
|
+
await this.ensureDatabaseExists();
|
|
205
|
+
|
|
206
|
+
for (const obj of objects) {
|
|
207
|
+
const tableName = obj.name;
|
|
208
|
+
|
|
209
|
+
// Cache JSON fields
|
|
210
|
+
const jsonCols: string[] = [];
|
|
211
|
+
if (obj.fields) {
|
|
212
|
+
for (const [name, field] of Object.entries<any>(obj.fields)) {
|
|
213
|
+
const type = field.type || 'string';
|
|
214
|
+
if (this.isJsonField(type, field)) {
|
|
215
|
+
jsonCols.push(name);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
this.jsonFields[tableName] = jsonCols;
|
|
220
|
+
|
|
221
|
+
let exists = await this.knex.schema.hasTable(tableName);
|
|
222
|
+
|
|
223
|
+
if (exists) {
|
|
224
|
+
const columnInfo = await this.knex(tableName).columnInfo();
|
|
225
|
+
const existingColumns = Object.keys(columnInfo);
|
|
226
|
+
|
|
227
|
+
// Check for _id vs id conflict (Legacy _id from mongo-style init)
|
|
228
|
+
if (existingColumns.includes('_id') && !existingColumns.includes('id')) {
|
|
229
|
+
console.log(`[KnexDriver] Detected legacy '_id' in '${tableName}'. Recreating table for 'id' compatibility...`);
|
|
230
|
+
await this.knex.schema.dropTable(tableName);
|
|
231
|
+
exists = false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!exists) {
|
|
236
|
+
await this.knex.schema.createTable(tableName, (table) => {
|
|
237
|
+
// Use standard 'id' for SQL databases
|
|
238
|
+
table.string('id').primary();
|
|
239
|
+
table.timestamp('created_at').defaultTo(this.knex.fn.now());
|
|
240
|
+
table.timestamp('updated_at').defaultTo(this.knex.fn.now());
|
|
241
|
+
if (obj.fields) {
|
|
242
|
+
for (const [name, field] of Object.entries(obj.fields)) {
|
|
243
|
+
this.createColumn(table, name, field);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
console.log(`[KnexDriver] Created table '${tableName}'`);
|
|
248
|
+
} else {
|
|
249
|
+
const columnInfo = await this.knex(tableName).columnInfo();
|
|
250
|
+
const existingColumns = Object.keys(columnInfo);
|
|
251
|
+
|
|
252
|
+
await this.knex.schema.alterTable(tableName, (table) => {
|
|
253
|
+
if (obj.fields) {
|
|
254
|
+
for (const [name, field] of Object.entries(obj.fields)) {
|
|
255
|
+
if (!existingColumns.includes(name)) {
|
|
256
|
+
this.createColumn(table, name, field);
|
|
257
|
+
console.log(`[KnexDriver] Added column '${name}' to '${tableName}'`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private createColumn(table: Knex.CreateTableBuilder, name: string, field: any) {
|
|
267
|
+
if (field.multiple) {
|
|
268
|
+
table.json(name);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const type = field.type || 'string';
|
|
273
|
+
let col;
|
|
274
|
+
switch(type) {
|
|
275
|
+
case 'string':
|
|
276
|
+
case 'email':
|
|
277
|
+
case 'url':
|
|
278
|
+
case 'phone':
|
|
279
|
+
case 'password':
|
|
280
|
+
col = table.string(name); break;
|
|
281
|
+
case 'text':
|
|
282
|
+
case 'textarea':
|
|
283
|
+
case 'html':
|
|
284
|
+
case 'markdown':
|
|
285
|
+
col = table.text(name); break;
|
|
286
|
+
case 'integer':
|
|
287
|
+
case 'int': col = table.integer(name); break;
|
|
288
|
+
case 'float':
|
|
289
|
+
case 'number':
|
|
290
|
+
case 'currency':
|
|
291
|
+
case 'percent': col = table.float(name); break;
|
|
292
|
+
case 'boolean': col = table.boolean(name); break;
|
|
293
|
+
case 'date': col = table.date(name); break;
|
|
294
|
+
case 'datetime': col = table.timestamp(name); break;
|
|
295
|
+
case 'time': col = table.time(name); break;
|
|
296
|
+
case 'json':
|
|
297
|
+
case 'object':
|
|
298
|
+
case 'array':
|
|
299
|
+
case 'image':
|
|
300
|
+
case 'file':
|
|
301
|
+
case 'avatar':
|
|
302
|
+
case 'location': col = table.json(name); break;
|
|
303
|
+
case 'summary': col = table.float(name); break; // Stored calculation result
|
|
304
|
+
case 'auto_number': col = table.string(name); break; // Generated string
|
|
305
|
+
case 'formula': return; // Virtual field, do not create column
|
|
306
|
+
default: col = table.string(name);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (field.unique) {
|
|
310
|
+
col.unique();
|
|
311
|
+
}
|
|
312
|
+
if (field.required) {
|
|
313
|
+
col.notNullable();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private async ensureDatabaseExists() {
|
|
318
|
+
if (this.config.client !== 'pg' && this.config.client !== 'postgresql') return;
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await this.knex.raw('SELECT 1');
|
|
322
|
+
} catch (e: any) {
|
|
323
|
+
if (e.code === '3D000') { // Database does not exist
|
|
324
|
+
await this.createDatabase();
|
|
325
|
+
} else {
|
|
326
|
+
throw e;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private async createDatabase() {
|
|
332
|
+
const config = this.config;
|
|
333
|
+
const connection = config.connection;
|
|
334
|
+
let dbName = '';
|
|
335
|
+
let adminConfig = { ...config };
|
|
336
|
+
|
|
337
|
+
if (typeof connection === 'string') {
|
|
338
|
+
const url = new URL(connection);
|
|
339
|
+
dbName = url.pathname.slice(1);
|
|
340
|
+
url.pathname = '/postgres';
|
|
341
|
+
adminConfig.connection = url.toString();
|
|
342
|
+
} else {
|
|
343
|
+
dbName = connection.database;
|
|
344
|
+
adminConfig.connection = { ...connection, database: 'postgres' };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
console.log(`[KnexDriver] Database '${dbName}' does not exist. Creating...`);
|
|
348
|
+
|
|
349
|
+
const adminKnex = knex(adminConfig);
|
|
350
|
+
try {
|
|
351
|
+
await adminKnex.raw(`CREATE DATABASE "${dbName}"`);
|
|
352
|
+
console.log(`[KnexDriver] Database '${dbName}' created successfully.`);
|
|
353
|
+
} catch (e: any) {
|
|
354
|
+
console.error(`[KnexDriver] Failed to create database '${dbName}':`, e.message);
|
|
355
|
+
if (e.code === '42501') {
|
|
356
|
+
console.error(`[KnexDriver] Hint: The user '${adminConfig.connection.user || 'current user'}' does not have CREATEDB privileges.`);
|
|
357
|
+
console.error(`[KnexDriver] Please run: createdb ${dbName}`);
|
|
358
|
+
}
|
|
359
|
+
throw e;
|
|
360
|
+
} finally {
|
|
361
|
+
await adminKnex.destroy();
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private isJsonField(type: string, field: any) {
|
|
366
|
+
return ['json', 'object', 'array', 'image', 'file', 'avatar', 'location'].includes(type) || field.multiple;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private formatInput(objectName: string, data: any) {
|
|
370
|
+
// Only needed for SQLite usually, PG handles JSON
|
|
371
|
+
const isSqlite = this.config.client === 'sqlite3';
|
|
372
|
+
if (!isSqlite) return data;
|
|
373
|
+
|
|
374
|
+
const fields = this.jsonFields[objectName];
|
|
375
|
+
if (!fields || fields.length === 0) return data;
|
|
376
|
+
|
|
377
|
+
const copy = { ...data };
|
|
378
|
+
for (const field of fields) {
|
|
379
|
+
if (copy[field] !== undefined && typeof copy[field] === 'object' && copy[field] !== null) {
|
|
380
|
+
copy[field] = JSON.stringify(copy[field]);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
return copy;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private formatOutput(objectName: string, data: any) {
|
|
387
|
+
const isSqlite = this.config.client === 'sqlite3';
|
|
388
|
+
if (!isSqlite) return data;
|
|
389
|
+
|
|
390
|
+
const fields = this.jsonFields[objectName];
|
|
391
|
+
if (!fields || fields.length === 0) return data;
|
|
392
|
+
|
|
393
|
+
// data is a single row object
|
|
394
|
+
for (const field of fields) {
|
|
395
|
+
if (data[field] !== undefined && typeof data[field] === 'string') {
|
|
396
|
+
try {
|
|
397
|
+
data[field] = JSON.parse(data[field]);
|
|
398
|
+
} catch (e) {
|
|
399
|
+
// ignore parse error, keep as string
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
return data;
|
|
404
|
+
}
|
|
170
405
|
}
|
|
171
406
|
|
package/test/index.test.ts
CHANGED
|
@@ -1,30 +1,38 @@
|
|
|
1
1
|
import { KnexDriver } from '../src';
|
|
2
|
-
import
|
|
2
|
+
import { UnifiedQuery } from '@objectql/types';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
methods.forEach(m => {
|
|
7
|
-
mock[m] = jest.fn().mockReturnThis();
|
|
8
|
-
});
|
|
9
|
-
mock.then = jest.fn((resolve) => resolve([]));
|
|
10
|
-
mock.catch = jest.fn();
|
|
11
|
-
return mock;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
const mockBuilder = mockChain(['select', 'where', 'orWhere', 'orderBy', 'offset', 'limit', 'insert', 'update', 'delete', 'transacting', 'count']);
|
|
15
|
-
const mockFirst = jest.fn();
|
|
16
|
-
mockBuilder.first = mockFirst;
|
|
4
|
+
describe('KnexDriver (SQLite Integration)', () => {
|
|
5
|
+
let driver: KnexDriver;
|
|
17
6
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
// Init ephemeral in-memory database
|
|
9
|
+
driver = new KnexDriver({
|
|
10
|
+
client: 'sqlite3',
|
|
11
|
+
connection: {
|
|
12
|
+
filename: ':memory:'
|
|
13
|
+
},
|
|
14
|
+
useNullAsDefault: true
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const k = (driver as any).knex;
|
|
18
|
+
|
|
19
|
+
await k.schema.createTable('users', (t: any) => {
|
|
20
|
+
t.string('id').primary();
|
|
21
|
+
t.string('name');
|
|
22
|
+
t.integer('age');
|
|
23
|
+
});
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
await k('users').insert([
|
|
26
|
+
{ id: '1', name: 'Alice', age: 25 },
|
|
27
|
+
{ id: '2', name: 'Bob', age: 17 },
|
|
28
|
+
{ id: '3', name: 'Charlie', age: 30 },
|
|
29
|
+
{ id: '4', name: 'Dave', age: 17 }
|
|
30
|
+
]);
|
|
31
|
+
});
|
|
24
32
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
33
|
+
afterEach(async () => {
|
|
34
|
+
const k = (driver as any).knex;
|
|
35
|
+
await k.destroy();
|
|
28
36
|
});
|
|
29
37
|
|
|
30
38
|
it('should be instantiable', () => {
|
|
@@ -32,39 +40,81 @@ describe('KnexDriver', () => {
|
|
|
32
40
|
expect(driver).toBeInstanceOf(KnexDriver);
|
|
33
41
|
});
|
|
34
42
|
|
|
35
|
-
it('should find objects', async () => {
|
|
36
|
-
const query = {
|
|
43
|
+
it('should find objects with filters', async () => {
|
|
44
|
+
const query: UnifiedQuery = {
|
|
37
45
|
fields: ['name', 'age'],
|
|
38
46
|
filters: [['age', '>', 18]],
|
|
39
|
-
sort: [['name', 'asc']]
|
|
40
|
-
skip: 10,
|
|
41
|
-
limit: 5
|
|
47
|
+
sort: [['name', 'asc']]
|
|
42
48
|
};
|
|
43
|
-
await driver.find('users', query);
|
|
49
|
+
const results = await driver.find('users', query);
|
|
44
50
|
|
|
45
|
-
expect(
|
|
46
|
-
expect(
|
|
47
|
-
expect(mockBuilder.orderBy).toHaveBeenCalledWith('name', 'asc');
|
|
48
|
-
expect(mockBuilder.offset).toHaveBeenCalledWith(10);
|
|
49
|
-
expect(mockBuilder.limit).toHaveBeenCalledWith(5);
|
|
51
|
+
expect(results.length).toBe(2);
|
|
52
|
+
expect(results.map((r: any) => r.name)).toEqual(['Alice', 'Charlie']);
|
|
50
53
|
});
|
|
51
54
|
|
|
52
|
-
it('should apply OR
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
it('should apply simple AND/OR logic', async () => {
|
|
56
|
+
// age = 17 OR age > 29
|
|
57
|
+
const query: UnifiedQuery = {
|
|
58
|
+
filters: [
|
|
59
|
+
['age', '=', 17],
|
|
60
|
+
'or',
|
|
61
|
+
['age', '>', 29]
|
|
62
|
+
]
|
|
55
63
|
};
|
|
56
|
-
await driver.find('users', query);
|
|
57
|
-
|
|
58
|
-
expect(
|
|
64
|
+
const results = await driver.find('users', query);
|
|
65
|
+
const names = results.map((r: any) => r.name).sort();
|
|
66
|
+
expect(names).toEqual(['Bob', 'Charlie', 'Dave']);
|
|
59
67
|
});
|
|
60
68
|
|
|
61
69
|
it('should find one object by id', async () => {
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
expect(
|
|
65
|
-
|
|
66
|
-
|
|
70
|
+
// First get an ID
|
|
71
|
+
const [alice] = await driver.find('users', { filters: [['name', '=', 'Alice']] });
|
|
72
|
+
expect(alice).toBeDefined();
|
|
73
|
+
|
|
74
|
+
const fetched = await driver.findOne('users', alice.id);
|
|
75
|
+
expect(fetched).toBeDefined();
|
|
76
|
+
expect(fetched.name).toBe('Alice');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should create an object', async () => {
|
|
80
|
+
const newItem = { name: 'Eve', age: 22 };
|
|
81
|
+
await driver.create('users', newItem);
|
|
82
|
+
|
|
83
|
+
const [eve] = await driver.find('users', { filters: [['name', '=', 'Eve']] });
|
|
84
|
+
expect(eve).toBeDefined();
|
|
85
|
+
expect(eve.age).toBe(22);
|
|
67
86
|
});
|
|
68
87
|
|
|
69
|
-
|
|
88
|
+
it('should update an object', async () => {
|
|
89
|
+
const [bob] = await driver.find('users', { filters: [['name', '=', 'Bob']] });
|
|
90
|
+
await driver.update('users', bob.id, { age: 18 });
|
|
91
|
+
|
|
92
|
+
const updated = await driver.findOne('users', bob.id);
|
|
93
|
+
expect(updated.age).toBe(18);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should delete an object', async () => {
|
|
97
|
+
const [charlie] = await driver.find('users', { filters: [['name', '=', 'Charlie']] });
|
|
98
|
+
await driver.delete('users', charlie.id);
|
|
99
|
+
|
|
100
|
+
const deleted = await driver.findOne('users', charlie.id);
|
|
101
|
+
expect(deleted).toBeUndefined();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should count objects', async () => {
|
|
105
|
+
const count = await driver.count('users', [['age', '=', 17]]);
|
|
106
|
+
expect(count).toBe(2);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should map _id to id if provided', async () => {
|
|
110
|
+
const newItemWithId = { _id: 'custom-id', name: 'Frank', age: 40 };
|
|
111
|
+
const created = await driver.create('users', newItemWithId);
|
|
112
|
+
|
|
113
|
+
expect(created.id).toBe('custom-id');
|
|
114
|
+
// Check if we can retrieve it by id
|
|
115
|
+
const fetched = await driver.findOne('users', 'custom-id');
|
|
116
|
+
expect(fetched).toBeDefined();
|
|
117
|
+
expect(fetched.name).toBe('Frank');
|
|
118
|
+
});
|
|
70
119
|
});
|
|
120
|
+
|