@quatrain/backend-sqlite 1.0.16 → 1.1.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/dist/SQLiteAdapter.d.ts +19 -0
- package/dist/SQLiteAdapter.js +98 -13
- package/package.json +6 -3
- package/src/SQLiteAdapter.ts +103 -16
- package/src/__test__/SQLiteAdapter.test.ts +14 -0
package/dist/SQLiteAdapter.d.ts
CHANGED
|
@@ -9,6 +9,11 @@ export declare class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
9
9
|
protected _dbPath: string;
|
|
10
10
|
constructor(params?: BackendParameters);
|
|
11
11
|
protected _buildPath(dataObject: DataObjectClass<any>, uid?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Executes a raw query on the backend.
|
|
14
|
+
* Only supported by SQL adapters.
|
|
15
|
+
*/
|
|
16
|
+
rawQuery(sql: string, params?: any[]): Promise<any>;
|
|
12
17
|
protected _connect(): Promise<Database<sqlite3.Database>>;
|
|
13
18
|
/**
|
|
14
19
|
* Process data for compatibility
|
|
@@ -52,4 +57,18 @@ export declare class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
52
57
|
* Close the SQLite connection
|
|
53
58
|
*/
|
|
54
59
|
close(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Generates the SQL up and down statements to create a collection table.
|
|
62
|
+
*/
|
|
63
|
+
generateCreateSql(collection: string, properties: any[]): {
|
|
64
|
+
upSql: string;
|
|
65
|
+
downSql: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Generates the SQL up and down statements to apply a schema delta to a collection.
|
|
69
|
+
*/
|
|
70
|
+
generateDeltaSql(collection: string, delta: any): {
|
|
71
|
+
upSql: string[];
|
|
72
|
+
downSql: string[];
|
|
73
|
+
};
|
|
55
74
|
}
|
package/dist/SQLiteAdapter.js
CHANGED
|
@@ -57,9 +57,19 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
57
57
|
dataObject.val(dataObject.parentProp)) {
|
|
58
58
|
path = `${dataObject.val(dataObject.parentProp).path}/${path}`;
|
|
59
59
|
}
|
|
60
|
-
backend_1.Backend.
|
|
60
|
+
backend_1.Backend.info(`[SQLA] Record path is '${path}'`);
|
|
61
61
|
return path;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Executes a raw query on the backend.
|
|
65
|
+
* Only supported by SQL adapters.
|
|
66
|
+
*/
|
|
67
|
+
rawQuery(sql_1) {
|
|
68
|
+
return __awaiter(this, arguments, void 0, function* (sql, params = []) {
|
|
69
|
+
const connection = yield this._connect();
|
|
70
|
+
return yield connection.all(sql, params);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
63
73
|
_connect() {
|
|
64
74
|
return __awaiter(this, void 0, void 0, function* () {
|
|
65
75
|
if (!this._connection) {
|
|
@@ -160,7 +170,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
160
170
|
});
|
|
161
171
|
query += `)`;
|
|
162
172
|
yield db.exec(query);
|
|
163
|
-
backend_1.Backend.
|
|
173
|
+
backend_1.Backend.info(`[SQLA] Created table ${collection.toLowerCase()}`);
|
|
164
174
|
}
|
|
165
175
|
});
|
|
166
176
|
}
|
|
@@ -196,7 +206,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
196
206
|
let placeholders = ['?'];
|
|
197
207
|
let values = [uid];
|
|
198
208
|
Object.entries(data).forEach(([key, value]) => {
|
|
199
|
-
columns.push(key.toLowerCase());
|
|
209
|
+
columns.push(`"${key.toLowerCase()}"`);
|
|
200
210
|
placeholders.push('?');
|
|
201
211
|
// Convert arrays and objects to JSON strings
|
|
202
212
|
if (Array.isArray(value) ||
|
|
@@ -234,7 +244,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
234
244
|
if (parts.length < 2 || parts.length % 2 !== 0) {
|
|
235
245
|
throw new backend_1.BackendError(`[SQLA] path parts number should be even, received: '${path}'`);
|
|
236
246
|
}
|
|
237
|
-
backend_1.Backend.
|
|
247
|
+
backend_1.Backend.info(`[SQLA] Getting document ${path}`);
|
|
238
248
|
if (!collection) {
|
|
239
249
|
throw new backend_1.BackendError(`[SQLA] Can't find collection matching object to query`);
|
|
240
250
|
}
|
|
@@ -273,15 +283,15 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
273
283
|
}
|
|
274
284
|
}
|
|
275
285
|
}
|
|
276
|
-
else if (propDef.constructor.name === 'ArrayProperty') {
|
|
277
|
-
// Parse JSON arrays
|
|
286
|
+
else if (propDef.constructor.name === 'ArrayProperty' || propDef.constructor.name === 'MapProperty') {
|
|
287
|
+
// Parse JSON arrays and objects
|
|
278
288
|
try {
|
|
279
289
|
if (result[prop.toLowerCase()]) {
|
|
280
290
|
result[prop] = JSON.parse(result[prop.toLowerCase()]);
|
|
281
291
|
}
|
|
282
292
|
}
|
|
283
293
|
catch (e) {
|
|
284
|
-
backend_1.Backend.warn(`[SQLA] Failed to parse
|
|
294
|
+
backend_1.Backend.warn(`[SQLA] Failed to parse JSON for ${prop}: ${e}`);
|
|
285
295
|
}
|
|
286
296
|
}
|
|
287
297
|
// Normalize property name case
|
|
@@ -312,7 +322,6 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
312
322
|
backend_1.Backend.warn('[SQLA] Nothing to update');
|
|
313
323
|
return dataObject;
|
|
314
324
|
}
|
|
315
|
-
backend_1.Backend.debug(`[SQLA] Data to update ${JSON.stringify(data)}`);
|
|
316
325
|
const db = yield this._connect();
|
|
317
326
|
const collection = this.getCollection(dataObject);
|
|
318
327
|
// Ensure table exists
|
|
@@ -320,7 +329,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
320
329
|
let updates = [];
|
|
321
330
|
let values = [];
|
|
322
331
|
Object.entries(data).forEach(([key, value]) => {
|
|
323
|
-
updates.push(
|
|
332
|
+
updates.push(`"${key.toLowerCase()}" = ?`);
|
|
324
333
|
// Convert arrays and objects to JSON strings
|
|
325
334
|
if (Array.isArray(value) ||
|
|
326
335
|
(typeof value === 'object' && value !== null)) {
|
|
@@ -353,7 +362,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
353
362
|
useDateFormat: true,
|
|
354
363
|
});
|
|
355
364
|
const db = yield this._connect();
|
|
356
|
-
if (!hardDelete) {
|
|
365
|
+
if (this._params.softDelete !== false && !hardDelete) {
|
|
357
366
|
dataObject.set('status', core_1.statuses.DELETED);
|
|
358
367
|
yield db.run(`UPDATE ${collection.toLowerCase()} SET status = ? WHERE id = ?`, [core_1.statuses.DELETED, dataObject.uid]);
|
|
359
368
|
}
|
|
@@ -643,15 +652,15 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
643
652
|
}
|
|
644
653
|
}
|
|
645
654
|
}
|
|
646
|
-
// Handle array properties
|
|
647
|
-
else if (propDef.constructor.name === 'ArrayProperty') {
|
|
655
|
+
// Handle array and map properties
|
|
656
|
+
else if (propDef.constructor.name === 'ArrayProperty' || propDef.constructor.name === 'MapProperty') {
|
|
648
657
|
try {
|
|
649
658
|
if (doc[lcProp]) {
|
|
650
659
|
doc[prop] = JSON.parse(doc[lcProp]);
|
|
651
660
|
}
|
|
652
661
|
}
|
|
653
662
|
catch (e) {
|
|
654
|
-
backend_1.Backend.warn(`[SQLA] Failed to parse
|
|
663
|
+
backend_1.Backend.warn(`[SQLA] Failed to parse JSON for ${prop}: ${e}`);
|
|
655
664
|
}
|
|
656
665
|
}
|
|
657
666
|
// Ensure property is available with original case
|
|
@@ -659,6 +668,10 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
659
668
|
doc[prop] = doc[lcProp];
|
|
660
669
|
}
|
|
661
670
|
});
|
|
671
|
+
// Map SQLite 'id' column back to Quatrain 'uid' property
|
|
672
|
+
if (doc.id && !doc.uid) {
|
|
673
|
+
doc.uid = doc.id;
|
|
674
|
+
}
|
|
662
675
|
const newDataObject = yield dataObject.clone(Object.assign({}, doc));
|
|
663
676
|
let newDataObjectUri = ``;
|
|
664
677
|
if (newDataObject.has('parent')) {
|
|
@@ -692,5 +705,77 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
692
705
|
}
|
|
693
706
|
});
|
|
694
707
|
}
|
|
708
|
+
/**
|
|
709
|
+
* Generates the SQL up and down statements to create a collection table.
|
|
710
|
+
*/
|
|
711
|
+
generateCreateSql(collection, properties) {
|
|
712
|
+
let query = `CREATE TABLE IF NOT EXISTS "${collection.toLowerCase()}" (\n id TEXT PRIMARY KEY`;
|
|
713
|
+
properties.forEach((propDef) => {
|
|
714
|
+
const propName = propDef.name.toLowerCase();
|
|
715
|
+
let columnType = 'TEXT';
|
|
716
|
+
const type = propDef.type || propDef.constructor.name;
|
|
717
|
+
if (type === 'NumberProperty') {
|
|
718
|
+
columnType = 'REAL';
|
|
719
|
+
}
|
|
720
|
+
else if (type === 'BooleanProperty') {
|
|
721
|
+
columnType = 'INTEGER';
|
|
722
|
+
}
|
|
723
|
+
else if (type === 'DateTimeProperty') {
|
|
724
|
+
columnType = 'INTEGER';
|
|
725
|
+
}
|
|
726
|
+
else if (type === 'ArrayProperty') {
|
|
727
|
+
columnType = 'TEXT';
|
|
728
|
+
}
|
|
729
|
+
else if (type === 'ObjectProperty') {
|
|
730
|
+
columnType = 'TEXT';
|
|
731
|
+
}
|
|
732
|
+
query += `,\n "${propName}" ${columnType}`;
|
|
733
|
+
});
|
|
734
|
+
query += `\n)`;
|
|
735
|
+
return {
|
|
736
|
+
upSql: `await adapter.rawQuery(\`${query}\`)`,
|
|
737
|
+
downSql: `await adapter.rawQuery(\`DROP TABLE IF EXISTS "${collection.toLowerCase()}"\`)`,
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Generates the SQL up and down statements to apply a schema delta to a collection.
|
|
742
|
+
*/
|
|
743
|
+
generateDeltaSql(collection, delta) {
|
|
744
|
+
const upSql = [];
|
|
745
|
+
const downSql = [];
|
|
746
|
+
// Added columns
|
|
747
|
+
delta.added.forEach((propDef) => {
|
|
748
|
+
const propName = propDef.name.toLowerCase();
|
|
749
|
+
let columnType = 'TEXT';
|
|
750
|
+
const type = propDef.type || propDef.constructor.name;
|
|
751
|
+
if (type === 'NumberProperty') {
|
|
752
|
+
columnType = 'REAL';
|
|
753
|
+
}
|
|
754
|
+
else if (type === 'BooleanProperty') {
|
|
755
|
+
columnType = 'INTEGER';
|
|
756
|
+
}
|
|
757
|
+
else if (type === 'DateTimeProperty') {
|
|
758
|
+
columnType = 'INTEGER';
|
|
759
|
+
}
|
|
760
|
+
upSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`);
|
|
761
|
+
// SQLite < 3.35 doesn't support DROP COLUMN, so we add a comment but keep the syntax
|
|
762
|
+
downSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`);
|
|
763
|
+
});
|
|
764
|
+
// Removed columns
|
|
765
|
+
delta.removed.forEach((propDef) => {
|
|
766
|
+
const propName = propDef.name.toLowerCase();
|
|
767
|
+
let columnType = 'TEXT';
|
|
768
|
+
const type = propDef.type || propDef.constructor.name;
|
|
769
|
+
if (type === 'NumberProperty')
|
|
770
|
+
columnType = 'REAL';
|
|
771
|
+
else if (type === 'BooleanProperty')
|
|
772
|
+
columnType = 'INTEGER';
|
|
773
|
+
else if (type === 'DateTimeProperty')
|
|
774
|
+
columnType = 'INTEGER';
|
|
775
|
+
upSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`);
|
|
776
|
+
downSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`);
|
|
777
|
+
});
|
|
778
|
+
return { upSql, downSql };
|
|
779
|
+
}
|
|
695
780
|
}
|
|
696
781
|
exports.SQLiteAdapter = SQLiteAdapter;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quatrain/backend-sqlite",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"description": "Backend adapter for SQLite",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
},
|
|
21
21
|
"author": "Quatrain Développement SAS <developers@quatrain.com>",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@quatrain/backend": "^1.1
|
|
24
|
-
"@quatrain/core": "^1.
|
|
23
|
+
"@quatrain/backend": "^1.2.1",
|
|
24
|
+
"@quatrain/core": "^1.2.0",
|
|
25
25
|
"sqlite": "^5.1.1",
|
|
26
26
|
"sqlite3": "^5.1.7"
|
|
27
27
|
},
|
|
@@ -43,5 +43,8 @@
|
|
|
43
43
|
"build": "tsc",
|
|
44
44
|
"wbuild": "tsc --watch",
|
|
45
45
|
"bump-to": "yarn version"
|
|
46
|
+
},
|
|
47
|
+
"typedoc": {
|
|
48
|
+
"entryPoint": "src/index.ts"
|
|
46
49
|
}
|
|
47
50
|
}
|
package/src/SQLiteAdapter.ts
CHANGED
|
@@ -3,7 +3,6 @@ import {
|
|
|
3
3
|
NotFoundError,
|
|
4
4
|
statuses,
|
|
5
5
|
StringProperty,
|
|
6
|
-
ObjectProperty,
|
|
7
6
|
} from '@quatrain/core'
|
|
8
7
|
import {
|
|
9
8
|
DataObjectClass,
|
|
@@ -23,7 +22,6 @@ import {
|
|
|
23
22
|
import { randomUUID } from 'crypto'
|
|
24
23
|
import sqlite3, { Statement } from 'sqlite3'
|
|
25
24
|
import { open, Database } from 'sqlite'
|
|
26
|
-
import { AbstractPropertyType } from '@quatrain/core/dist/properties/types/AbstractPropertyType'
|
|
27
25
|
|
|
28
26
|
const operatorsMap: { [x: string]: string } = {
|
|
29
27
|
equals: '=',
|
|
@@ -74,11 +72,20 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
74
72
|
path = `${dataObject.val(dataObject.parentProp).path}/${path}`
|
|
75
73
|
}
|
|
76
74
|
|
|
77
|
-
Backend.
|
|
75
|
+
Backend.info(`[SQLA] Record path is '${path}'`)
|
|
78
76
|
|
|
79
77
|
return path
|
|
80
78
|
}
|
|
81
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Executes a raw query on the backend.
|
|
82
|
+
* Only supported by SQL adapters.
|
|
83
|
+
*/
|
|
84
|
+
async rawQuery(sql: string, params: any[] = []): Promise<any> {
|
|
85
|
+
const connection = await this._connect()
|
|
86
|
+
return await connection.all(sql, params)
|
|
87
|
+
}
|
|
88
|
+
|
|
82
89
|
protected async _connect(): Promise<Database<sqlite3.Database>> {
|
|
83
90
|
if (!this._connection) {
|
|
84
91
|
// Open SQLite database
|
|
@@ -193,7 +200,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
193
200
|
|
|
194
201
|
query += `)`
|
|
195
202
|
await db.exec(query)
|
|
196
|
-
Backend.
|
|
203
|
+
Backend.info(`[SQLA] Created table ${collection.toLowerCase()}`)
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
|
|
@@ -241,7 +248,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
241
248
|
|
|
242
249
|
Object.entries(data).forEach(
|
|
243
250
|
([key, value]: [key: string, value: any]) => {
|
|
244
|
-
columns.push(key.toLowerCase())
|
|
251
|
+
columns.push(`"${key.toLowerCase()}"`)
|
|
245
252
|
placeholders.push('?')
|
|
246
253
|
|
|
247
254
|
// Convert arrays and objects to JSON strings
|
|
@@ -294,7 +301,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
294
301
|
)
|
|
295
302
|
}
|
|
296
303
|
|
|
297
|
-
Backend.
|
|
304
|
+
Backend.info(`[SQLA] Getting document ${path}`)
|
|
298
305
|
|
|
299
306
|
if (!collection) {
|
|
300
307
|
throw new BackendError(
|
|
@@ -351,14 +358,14 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
351
358
|
}
|
|
352
359
|
}
|
|
353
360
|
}
|
|
354
|
-
} else if (propDef.constructor.name === 'ArrayProperty') {
|
|
355
|
-
// Parse JSON arrays
|
|
361
|
+
} else if (propDef.constructor.name === 'ArrayProperty' || propDef.constructor.name === 'MapProperty') {
|
|
362
|
+
// Parse JSON arrays and objects
|
|
356
363
|
try {
|
|
357
364
|
if (result[prop.toLowerCase()]) {
|
|
358
365
|
result[prop] = JSON.parse(result[prop.toLowerCase()])
|
|
359
366
|
}
|
|
360
367
|
} catch (e) {
|
|
361
|
-
Backend.warn(`[SQLA] Failed to parse
|
|
368
|
+
Backend.warn(`[SQLA] Failed to parse JSON for ${prop}: ${e}`)
|
|
362
369
|
}
|
|
363
370
|
}
|
|
364
371
|
|
|
@@ -397,8 +404,6 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
397
404
|
return dataObject
|
|
398
405
|
}
|
|
399
406
|
|
|
400
|
-
Backend.debug(`[SQLA] Data to update ${JSON.stringify(data)}`)
|
|
401
|
-
|
|
402
407
|
const db = await this._connect()
|
|
403
408
|
const collection = this.getCollection(dataObject)
|
|
404
409
|
|
|
@@ -410,7 +415,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
410
415
|
|
|
411
416
|
Object.entries(data).forEach(
|
|
412
417
|
([key, value]: [key: string, value: any]) => {
|
|
413
|
-
updates.push(
|
|
418
|
+
updates.push(`"${key.toLowerCase()}" = ?`)
|
|
414
419
|
|
|
415
420
|
// Convert arrays and objects to JSON strings
|
|
416
421
|
if (
|
|
@@ -460,7 +465,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
460
465
|
|
|
461
466
|
const db = await this._connect()
|
|
462
467
|
|
|
463
|
-
if (!hardDelete) {
|
|
468
|
+
if (this._params.softDelete !== false && !hardDelete) {
|
|
464
469
|
dataObject.set('status', statuses.DELETED)
|
|
465
470
|
await db.run(
|
|
466
471
|
`UPDATE ${collection.toLowerCase()} SET status = ? WHERE id = ?`,
|
|
@@ -844,15 +849,15 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
844
849
|
}
|
|
845
850
|
}
|
|
846
851
|
}
|
|
847
|
-
// Handle array properties
|
|
848
|
-
else if (propDef.constructor.name === 'ArrayProperty') {
|
|
852
|
+
// Handle array and map properties
|
|
853
|
+
else if (propDef.constructor.name === 'ArrayProperty' || propDef.constructor.name === 'MapProperty') {
|
|
849
854
|
try {
|
|
850
855
|
if (doc[lcProp]) {
|
|
851
856
|
doc[prop] = JSON.parse(doc[lcProp])
|
|
852
857
|
}
|
|
853
858
|
} catch (e) {
|
|
854
859
|
Backend.warn(
|
|
855
|
-
`[SQLA] Failed to parse
|
|
860
|
+
`[SQLA] Failed to parse JSON for ${prop}: ${e}`
|
|
856
861
|
)
|
|
857
862
|
}
|
|
858
863
|
}
|
|
@@ -863,6 +868,11 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
863
868
|
}
|
|
864
869
|
})
|
|
865
870
|
|
|
871
|
+
// Map SQLite 'id' column back to Quatrain 'uid' property
|
|
872
|
+
if (doc.id && !doc.uid) {
|
|
873
|
+
doc.uid = doc.id
|
|
874
|
+
}
|
|
875
|
+
|
|
866
876
|
const newDataObject: DataObjectClass<any> = await dataObject.clone({
|
|
867
877
|
...doc,
|
|
868
878
|
})
|
|
@@ -913,4 +923,81 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
913
923
|
this._connection = undefined
|
|
914
924
|
}
|
|
915
925
|
}
|
|
926
|
+
|
|
927
|
+
/**
|
|
928
|
+
* Generates the SQL up and down statements to create a collection table.
|
|
929
|
+
*/
|
|
930
|
+
generateCreateSql(collection: string, properties: any[]): { upSql: string; downSql: string } {
|
|
931
|
+
let query = `CREATE TABLE IF NOT EXISTS "${collection.toLowerCase()}" (\n id TEXT PRIMARY KEY`
|
|
932
|
+
|
|
933
|
+
properties.forEach((propDef: any) => {
|
|
934
|
+
const propName = propDef.name.toLowerCase()
|
|
935
|
+
let columnType = 'TEXT'
|
|
936
|
+
|
|
937
|
+
const type = propDef.type || propDef.constructor.name
|
|
938
|
+
if (type === 'NumberProperty') {
|
|
939
|
+
columnType = 'REAL'
|
|
940
|
+
} else if (type === 'BooleanProperty') {
|
|
941
|
+
columnType = 'INTEGER'
|
|
942
|
+
} else if (type === 'DateTimeProperty') {
|
|
943
|
+
columnType = 'INTEGER'
|
|
944
|
+
} else if (type === 'ArrayProperty') {
|
|
945
|
+
columnType = 'TEXT'
|
|
946
|
+
} else if (type === 'ObjectProperty') {
|
|
947
|
+
columnType = 'TEXT'
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
query += `,\n "${propName}" ${columnType}`
|
|
951
|
+
})
|
|
952
|
+
|
|
953
|
+
query += `\n)`
|
|
954
|
+
|
|
955
|
+
return {
|
|
956
|
+
upSql: `await adapter.rawQuery(\`${query}\`)`,
|
|
957
|
+
downSql: `await adapter.rawQuery(\`DROP TABLE IF EXISTS "${collection.toLowerCase()}"\`)`,
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Generates the SQL up and down statements to apply a schema delta to a collection.
|
|
963
|
+
*/
|
|
964
|
+
generateDeltaSql(collection: string, delta: any): { upSql: string[]; downSql: string[] } {
|
|
965
|
+
const upSql: string[] = []
|
|
966
|
+
const downSql: string[] = []
|
|
967
|
+
|
|
968
|
+
// Added columns
|
|
969
|
+
delta.added.forEach((propDef: any) => {
|
|
970
|
+
const propName = propDef.name.toLowerCase()
|
|
971
|
+
let columnType = 'TEXT'
|
|
972
|
+
|
|
973
|
+
const type = propDef.type || propDef.constructor.name
|
|
974
|
+
if (type === 'NumberProperty') {
|
|
975
|
+
columnType = 'REAL'
|
|
976
|
+
} else if (type === 'BooleanProperty') {
|
|
977
|
+
columnType = 'INTEGER'
|
|
978
|
+
} else if (type === 'DateTimeProperty') {
|
|
979
|
+
columnType = 'INTEGER'
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
upSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`)
|
|
983
|
+
// SQLite < 3.35 doesn't support DROP COLUMN, so we add a comment but keep the syntax
|
|
984
|
+
downSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`)
|
|
985
|
+
})
|
|
986
|
+
|
|
987
|
+
// Removed columns
|
|
988
|
+
delta.removed.forEach((propDef: any) => {
|
|
989
|
+
const propName = propDef.name.toLowerCase()
|
|
990
|
+
let columnType = 'TEXT'
|
|
991
|
+
|
|
992
|
+
const type = propDef.type || propDef.constructor.name
|
|
993
|
+
if (type === 'NumberProperty') columnType = 'REAL'
|
|
994
|
+
else if (type === 'BooleanProperty') columnType = 'INTEGER'
|
|
995
|
+
else if (type === 'DateTimeProperty') columnType = 'INTEGER'
|
|
996
|
+
|
|
997
|
+
upSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`)
|
|
998
|
+
downSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`)
|
|
999
|
+
})
|
|
1000
|
+
|
|
1001
|
+
return { upSql, downSql }
|
|
1002
|
+
}
|
|
916
1003
|
}
|
|
@@ -123,6 +123,20 @@ describe('SQLiteAdapter Tests', () => {
|
|
|
123
123
|
expect(result.val('email')).toBe('john@doe.com')
|
|
124
124
|
})
|
|
125
125
|
|
|
126
|
+
test('should correctly map SQLite id column back to Quatrain uid property', async () => {
|
|
127
|
+
const db = await (adapter as any)._connect()
|
|
128
|
+
// Vérifier que la colonne id existe bien en base
|
|
129
|
+
const rawData = await db.get(`SELECT id, firstname FROM user WHERE id = ?`, [user.uid])
|
|
130
|
+
expect(rawData.id).toBeDefined()
|
|
131
|
+
expect((rawData as any).uid).toBeUndefined() // SQLite ne stocke pas de colonne uid
|
|
132
|
+
|
|
133
|
+
// Vérifier que l'adaptateur remonte bien la valeur dans la propriété uid
|
|
134
|
+
const readUser = await User.factory()
|
|
135
|
+
readUser.dataObject.uri.path = user.dataObject.uri.path
|
|
136
|
+
const result = await adapter.read(readUser.dataObject)
|
|
137
|
+
expect(result.uid).toBe(rawData.id)
|
|
138
|
+
})
|
|
139
|
+
|
|
126
140
|
test('should throw NotFoundError for non-existent record', async () => {
|
|
127
141
|
const nonExistentUser = await User.factory()
|
|
128
142
|
nonExistentUser.dataObject.uri.path = 'user/non-existent-id'
|