@quatrain/backend-sqlite 1.0.16 → 1.1.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/dist/SQLiteAdapter.d.ts +19 -0
- package/dist/SQLiteAdapter.js +111 -35
- package/package.json +6 -3
- package/src/SQLiteAdapter.ts +117 -40
- 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) {
|
|
@@ -138,7 +148,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
138
148
|
id TEXT PRIMARY KEY`;
|
|
139
149
|
// Add columns based on dataObject properties
|
|
140
150
|
Object.entries(dataObject.properties).forEach(([prop, propDef]) => {
|
|
141
|
-
const propName = prop
|
|
151
|
+
const propName = prop;
|
|
142
152
|
let columnType = 'TEXT';
|
|
143
153
|
// Map property types to SQLite column types
|
|
144
154
|
if (propDef.constructor.name === 'NumberProperty') {
|
|
@@ -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
|
|
209
|
+
columns.push(`"${key}"`);
|
|
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
|
}
|
|
@@ -251,7 +261,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
251
261
|
const propDef = dataObject.properties[prop];
|
|
252
262
|
if (propDef.constructor.name === 'ObjectProperty' &&
|
|
253
263
|
propDef.instanceOf) {
|
|
254
|
-
const propValue = result[prop
|
|
264
|
+
const propValue = result[prop];
|
|
255
265
|
if (propValue) {
|
|
256
266
|
let refTable = undefined;
|
|
257
267
|
if (this._params.mapping && this._params.mapping[propDef.instanceOf]) {
|
|
@@ -273,21 +283,17 @@ 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
|
-
if (result[prop
|
|
280
|
-
result[prop] = JSON.parse(result[prop
|
|
289
|
+
if (result[prop]) {
|
|
290
|
+
result[prop] = JSON.parse(result[prop]);
|
|
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
|
-
// Normalize property name case
|
|
288
|
-
if (prop.toLowerCase() !== prop) {
|
|
289
|
-
result[prop] = result[prop.toLowerCase()];
|
|
290
|
-
}
|
|
291
297
|
}
|
|
292
298
|
dataObject.populate(result);
|
|
293
299
|
return dataObject;
|
|
@@ -312,7 +318,6 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
312
318
|
backend_1.Backend.warn('[SQLA] Nothing to update');
|
|
313
319
|
return dataObject;
|
|
314
320
|
}
|
|
315
|
-
backend_1.Backend.debug(`[SQLA] Data to update ${JSON.stringify(data)}`);
|
|
316
321
|
const db = yield this._connect();
|
|
317
322
|
const collection = this.getCollection(dataObject);
|
|
318
323
|
// Ensure table exists
|
|
@@ -320,7 +325,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
320
325
|
let updates = [];
|
|
321
326
|
let values = [];
|
|
322
327
|
Object.entries(data).forEach(([key, value]) => {
|
|
323
|
-
updates.push(
|
|
328
|
+
updates.push(`"${key}" = ?`);
|
|
324
329
|
// Convert arrays and objects to JSON strings
|
|
325
330
|
if (Array.isArray(value) ||
|
|
326
331
|
(typeof value === 'object' && value !== null)) {
|
|
@@ -353,7 +358,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
353
358
|
useDateFormat: true,
|
|
354
359
|
});
|
|
355
360
|
const db = yield this._connect();
|
|
356
|
-
if (!hardDelete) {
|
|
361
|
+
if (this._params.softDelete !== false && !hardDelete) {
|
|
357
362
|
dataObject.set('status', core_1.statuses.DELETED);
|
|
358
363
|
yield db.run(`UPDATE ${collection.toLowerCase()} SET status = ? WHERE id = ?`, [core_1.statuses.DELETED, dataObject.uid]);
|
|
359
364
|
}
|
|
@@ -419,7 +424,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
419
424
|
Object.entries(dataObject.properties).forEach(([prop, propDef]) => {
|
|
420
425
|
if (propDef.constructor.name === 'ObjectProperty' &&
|
|
421
426
|
propDef.instanceOf) {
|
|
422
|
-
const propName = prop
|
|
427
|
+
const propName = prop;
|
|
423
428
|
const joinAlias = `${propName}_table`;
|
|
424
429
|
let table = undefined;
|
|
425
430
|
if (this._params.mapping && this._params.mapping[propDef.instanceOf]) {
|
|
@@ -450,14 +455,14 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
450
455
|
// list of filters objects
|
|
451
456
|
filters.forEach((filter, i) => {
|
|
452
457
|
query.push(parent && i === 0 ? 'AND' : i > 0 ? 'AND' : 'WHERE');
|
|
453
|
-
let realProp = filter.prop
|
|
458
|
+
let realProp = filter.prop;
|
|
454
459
|
let realOperator = operatorsMap[filter.operator];
|
|
455
460
|
let realValue = filter.value;
|
|
456
461
|
if (filter.prop === 'keywords') {
|
|
457
462
|
const keywordFilters = [];
|
|
458
463
|
const props = dataObject.getProperties(core_1.StringProperty.name);
|
|
459
464
|
Object.keys(props).forEach((rp) => {
|
|
460
|
-
keywordFilters.push(`${collection.toLowerCase()}.${rp
|
|
465
|
+
keywordFilters.push(`${collection.toLowerCase()}.${rp} LIKE ?`);
|
|
461
466
|
params.push(`%${filter.value}%`);
|
|
462
467
|
});
|
|
463
468
|
query.push(`(${keywordFilters.join(' OR ')})`);
|
|
@@ -471,7 +476,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
471
476
|
}
|
|
472
477
|
else {
|
|
473
478
|
const property = dataObject.get(filter.prop);
|
|
474
|
-
realProp = filter.prop
|
|
479
|
+
realProp = filter.prop;
|
|
475
480
|
if (property.constructor.name === 'ArrayProperty' &&
|
|
476
481
|
Array.isArray(realValue)) {
|
|
477
482
|
// Use EXISTS with json_each for array containment in SQLite
|
|
@@ -578,7 +583,7 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
578
583
|
if (pagination && pagination.sortings) {
|
|
579
584
|
pagination.sortings.forEach((sorting, i) => {
|
|
580
585
|
query.push(i === 0 ? `ORDER BY` : ',');
|
|
581
|
-
query.push(`${collection.toLowerCase()}.${sorting.prop
|
|
586
|
+
query.push(`${collection.toLowerCase()}.${sorting.prop} ${sorting.order}`);
|
|
582
587
|
if (sorting.prop !== undefined) {
|
|
583
588
|
sortField.push(`${sorting.prop} ${sorting.order.toUpperCase()}`);
|
|
584
589
|
}
|
|
@@ -608,15 +613,14 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
608
613
|
for (let doc of results || []) {
|
|
609
614
|
// Process document before populating
|
|
610
615
|
Object.entries(dataObject.properties).forEach(([prop, propDef]) => {
|
|
611
|
-
const lcProp = prop.toLowerCase();
|
|
612
616
|
// Handle ObjectProperty references
|
|
613
617
|
if (propDef.constructor.name === 'ObjectProperty' &&
|
|
614
618
|
propDef.instanceOf) {
|
|
615
|
-
const refValue = doc[
|
|
619
|
+
const refValue = doc[prop];
|
|
616
620
|
if (refValue) {
|
|
617
621
|
const info = joinTables[prop];
|
|
618
622
|
if (info) {
|
|
619
|
-
const label = doc[`${
|
|
623
|
+
const label = doc[`${prop}_table_name`] || '';
|
|
620
624
|
doc[prop] = {
|
|
621
625
|
ref: `${info.table}/${refValue}`,
|
|
622
626
|
path: `${info.table}/${refValue}`,
|
|
@@ -643,22 +647,22 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
643
647
|
}
|
|
644
648
|
}
|
|
645
649
|
}
|
|
646
|
-
// Handle array properties
|
|
647
|
-
else if (propDef.constructor.name === 'ArrayProperty') {
|
|
650
|
+
// Handle array and map properties
|
|
651
|
+
else if (propDef.constructor.name === 'ArrayProperty' || propDef.constructor.name === 'MapProperty') {
|
|
648
652
|
try {
|
|
649
|
-
if (doc[
|
|
650
|
-
doc[prop] = JSON.parse(doc[
|
|
653
|
+
if (doc[prop] && typeof doc[prop] === 'string') {
|
|
654
|
+
doc[prop] = JSON.parse(doc[prop]);
|
|
651
655
|
}
|
|
652
656
|
}
|
|
653
657
|
catch (e) {
|
|
654
|
-
backend_1.Backend.warn(`[SQLA] Failed to parse
|
|
658
|
+
backend_1.Backend.warn(`[SQLA] Failed to parse JSON for ${prop}: ${e}`);
|
|
655
659
|
}
|
|
656
660
|
}
|
|
657
|
-
// Ensure property is available with original case
|
|
658
|
-
if (prop !== lcProp) {
|
|
659
|
-
doc[prop] = doc[lcProp];
|
|
660
|
-
}
|
|
661
661
|
});
|
|
662
|
+
// Map SQLite 'id' column back to Quatrain 'uid' property
|
|
663
|
+
if (doc.id && !doc.uid) {
|
|
664
|
+
doc.uid = doc.id;
|
|
665
|
+
}
|
|
662
666
|
const newDataObject = yield dataObject.clone(Object.assign({}, doc));
|
|
663
667
|
let newDataObjectUri = ``;
|
|
664
668
|
if (newDataObject.has('parent')) {
|
|
@@ -692,5 +696,77 @@ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
|
|
|
692
696
|
}
|
|
693
697
|
});
|
|
694
698
|
}
|
|
699
|
+
/**
|
|
700
|
+
* Generates the SQL up and down statements to create a collection table.
|
|
701
|
+
*/
|
|
702
|
+
generateCreateSql(collection, properties) {
|
|
703
|
+
let query = `CREATE TABLE IF NOT EXISTS "${collection.toLowerCase()}" (\n id TEXT PRIMARY KEY`;
|
|
704
|
+
properties.forEach((propDef) => {
|
|
705
|
+
const propName = propDef.name;
|
|
706
|
+
let columnType = 'TEXT';
|
|
707
|
+
const type = propDef.type || propDef.constructor.name;
|
|
708
|
+
if (type === 'NumberProperty') {
|
|
709
|
+
columnType = 'REAL';
|
|
710
|
+
}
|
|
711
|
+
else if (type === 'BooleanProperty') {
|
|
712
|
+
columnType = 'INTEGER';
|
|
713
|
+
}
|
|
714
|
+
else if (type === 'DateTimeProperty') {
|
|
715
|
+
columnType = 'INTEGER';
|
|
716
|
+
}
|
|
717
|
+
else if (type === 'ArrayProperty') {
|
|
718
|
+
columnType = 'TEXT';
|
|
719
|
+
}
|
|
720
|
+
else if (type === 'ObjectProperty') {
|
|
721
|
+
columnType = 'TEXT';
|
|
722
|
+
}
|
|
723
|
+
query += `,\n "${propName}" ${columnType}`;
|
|
724
|
+
});
|
|
725
|
+
query += `\n)`;
|
|
726
|
+
return {
|
|
727
|
+
upSql: `await adapter.rawQuery(\`${query}\`)`,
|
|
728
|
+
downSql: `await adapter.rawQuery(\`DROP TABLE IF EXISTS "${collection.toLowerCase()}"\`)`,
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* Generates the SQL up and down statements to apply a schema delta to a collection.
|
|
733
|
+
*/
|
|
734
|
+
generateDeltaSql(collection, delta) {
|
|
735
|
+
const upSql = [];
|
|
736
|
+
const downSql = [];
|
|
737
|
+
// Added columns
|
|
738
|
+
delta.added.forEach((propDef) => {
|
|
739
|
+
const propName = propDef.name;
|
|
740
|
+
let columnType = 'TEXT';
|
|
741
|
+
const type = propDef.type || propDef.constructor.name;
|
|
742
|
+
if (type === 'NumberProperty') {
|
|
743
|
+
columnType = 'REAL';
|
|
744
|
+
}
|
|
745
|
+
else if (type === 'BooleanProperty') {
|
|
746
|
+
columnType = 'INTEGER';
|
|
747
|
+
}
|
|
748
|
+
else if (type === 'DateTimeProperty') {
|
|
749
|
+
columnType = 'INTEGER';
|
|
750
|
+
}
|
|
751
|
+
upSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`);
|
|
752
|
+
// SQLite < 3.35 doesn't support DROP COLUMN, so we add a comment but keep the syntax
|
|
753
|
+
downSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`);
|
|
754
|
+
});
|
|
755
|
+
// Removed columns
|
|
756
|
+
delta.removed.forEach((propDef) => {
|
|
757
|
+
const propName = propDef.name;
|
|
758
|
+
let columnType = 'TEXT';
|
|
759
|
+
const type = propDef.type || propDef.constructor.name;
|
|
760
|
+
if (type === 'NumberProperty')
|
|
761
|
+
columnType = 'REAL';
|
|
762
|
+
else if (type === 'BooleanProperty')
|
|
763
|
+
columnType = 'INTEGER';
|
|
764
|
+
else if (type === 'DateTimeProperty')
|
|
765
|
+
columnType = 'INTEGER';
|
|
766
|
+
upSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`);
|
|
767
|
+
downSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`);
|
|
768
|
+
});
|
|
769
|
+
return { upSql, downSql };
|
|
770
|
+
}
|
|
695
771
|
}
|
|
696
772
|
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.2",
|
|
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.2",
|
|
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
|
|
@@ -171,7 +178,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
171
178
|
// Add columns based on dataObject properties
|
|
172
179
|
Object.entries(dataObject.properties).forEach(
|
|
173
180
|
([prop, propDef]: [prop: string, propDef: any]) => {
|
|
174
|
-
const propName = prop
|
|
181
|
+
const propName = prop
|
|
175
182
|
let columnType = 'TEXT'
|
|
176
183
|
|
|
177
184
|
// Map property types to SQLite column types
|
|
@@ -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
|
|
251
|
+
columns.push(`"${key}"`)
|
|
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(
|
|
@@ -325,7 +332,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
325
332
|
propDef.constructor.name === 'ObjectProperty' &&
|
|
326
333
|
propDef.instanceOf
|
|
327
334
|
) {
|
|
328
|
-
const propValue = result[prop
|
|
335
|
+
const propValue = result[prop]
|
|
329
336
|
|
|
330
337
|
if (propValue) {
|
|
331
338
|
let refTable = undefined
|
|
@@ -351,21 +358,18 @@ 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
|
-
if (result[prop
|
|
358
|
-
result[prop] = JSON.parse(result[prop
|
|
364
|
+
if (result[prop]) {
|
|
365
|
+
result[prop] = JSON.parse(result[prop])
|
|
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
|
|
|
365
|
-
|
|
366
|
-
if (prop.toLowerCase() !== prop) {
|
|
367
|
-
result[prop] = result[prop.toLowerCase()]
|
|
368
|
-
}
|
|
372
|
+
|
|
369
373
|
}
|
|
370
374
|
|
|
371
375
|
dataObject.populate(result)
|
|
@@ -397,8 +401,6 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
397
401
|
return dataObject
|
|
398
402
|
}
|
|
399
403
|
|
|
400
|
-
Backend.debug(`[SQLA] Data to update ${JSON.stringify(data)}`)
|
|
401
|
-
|
|
402
404
|
const db = await this._connect()
|
|
403
405
|
const collection = this.getCollection(dataObject)
|
|
404
406
|
|
|
@@ -410,7 +412,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
410
412
|
|
|
411
413
|
Object.entries(data).forEach(
|
|
412
414
|
([key, value]: [key: string, value: any]) => {
|
|
413
|
-
updates.push(
|
|
415
|
+
updates.push(`"${key}" = ?`)
|
|
414
416
|
|
|
415
417
|
// Convert arrays and objects to JSON strings
|
|
416
418
|
if (
|
|
@@ -460,7 +462,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
460
462
|
|
|
461
463
|
const db = await this._connect()
|
|
462
464
|
|
|
463
|
-
if (!hardDelete) {
|
|
465
|
+
if (this._params.softDelete !== false && !hardDelete) {
|
|
464
466
|
dataObject.set('status', statuses.DELETED)
|
|
465
467
|
await db.run(
|
|
466
468
|
`UPDATE ${collection.toLowerCase()} SET status = ? WHERE id = ?`,
|
|
@@ -551,7 +553,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
551
553
|
propDef.constructor.name === 'ObjectProperty' &&
|
|
552
554
|
propDef.instanceOf
|
|
553
555
|
) {
|
|
554
|
-
const propName = prop
|
|
556
|
+
const propName = prop
|
|
555
557
|
const joinAlias = `${propName}_table`
|
|
556
558
|
|
|
557
559
|
let table = undefined
|
|
@@ -590,7 +592,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
590
592
|
filters.forEach((filter, i) => {
|
|
591
593
|
query.push(parent && i === 0 ? 'AND' : i > 0 ? 'AND' : 'WHERE')
|
|
592
594
|
|
|
593
|
-
let realProp: any = filter.prop
|
|
595
|
+
let realProp: any = filter.prop
|
|
594
596
|
let realOperator: string = operatorsMap[filter.operator]
|
|
595
597
|
let realValue = filter.value
|
|
596
598
|
|
|
@@ -600,7 +602,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
600
602
|
const props = dataObject.getProperties(StringProperty.name)
|
|
601
603
|
Object.keys(props).forEach((rp) => {
|
|
602
604
|
keywordFilters.push(
|
|
603
|
-
`${collection.toLowerCase()}.${rp
|
|
605
|
+
`${collection.toLowerCase()}.${rp} LIKE ?`
|
|
604
606
|
)
|
|
605
607
|
params.push(`%${filter.value as string}%`)
|
|
606
608
|
})
|
|
@@ -619,7 +621,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
619
621
|
realProp = 'id'
|
|
620
622
|
} else {
|
|
621
623
|
const property = dataObject.get(filter.prop)
|
|
622
|
-
realProp = filter.prop
|
|
624
|
+
realProp = filter.prop
|
|
623
625
|
|
|
624
626
|
if (
|
|
625
627
|
property.constructor.name === 'ArrayProperty' &&
|
|
@@ -765,7 +767,7 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
765
767
|
pagination.sortings.forEach((sorting: Sorting, i) => {
|
|
766
768
|
query.push(i === 0 ? `ORDER BY` : ',')
|
|
767
769
|
query.push(
|
|
768
|
-
`${collection.toLowerCase()}.${sorting.prop
|
|
770
|
+
`${collection.toLowerCase()}.${sorting.prop} ${
|
|
769
771
|
sorting.order
|
|
770
772
|
}`
|
|
771
773
|
)
|
|
@@ -805,19 +807,17 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
805
807
|
for (let doc of results || []) {
|
|
806
808
|
// Process document before populating
|
|
807
809
|
Object.entries(dataObject.properties).forEach(([prop, propDef]: [prop: string, propDef: any]) => {
|
|
808
|
-
const lcProp = prop.toLowerCase()
|
|
809
|
-
|
|
810
810
|
// Handle ObjectProperty references
|
|
811
811
|
if (
|
|
812
812
|
propDef.constructor.name === 'ObjectProperty' &&
|
|
813
813
|
propDef.instanceOf
|
|
814
814
|
) {
|
|
815
|
-
const refValue = doc[
|
|
815
|
+
const refValue = doc[prop]
|
|
816
816
|
if (refValue) {
|
|
817
817
|
const info = joinTables[prop]
|
|
818
818
|
|
|
819
819
|
if (info) {
|
|
820
|
-
const label = doc[`${
|
|
820
|
+
const label = doc[`${prop}_table_name`] || ''
|
|
821
821
|
|
|
822
822
|
doc[prop] = {
|
|
823
823
|
ref: `${info.table}/${refValue}`,
|
|
@@ -844,25 +844,25 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
844
844
|
}
|
|
845
845
|
}
|
|
846
846
|
}
|
|
847
|
-
// Handle array properties
|
|
848
|
-
else if (propDef.constructor.name === 'ArrayProperty') {
|
|
847
|
+
// Handle array and map properties
|
|
848
|
+
else if (propDef.constructor.name === 'ArrayProperty' || propDef.constructor.name === 'MapProperty') {
|
|
849
849
|
try {
|
|
850
|
-
if (doc[
|
|
851
|
-
doc[prop] = JSON.parse(doc[
|
|
850
|
+
if (doc[prop] && typeof doc[prop] === 'string') {
|
|
851
|
+
doc[prop] = JSON.parse(doc[prop])
|
|
852
852
|
}
|
|
853
853
|
} catch (e) {
|
|
854
854
|
Backend.warn(
|
|
855
|
-
`[SQLA] Failed to parse
|
|
855
|
+
`[SQLA] Failed to parse JSON for ${prop}: ${e}`
|
|
856
856
|
)
|
|
857
857
|
}
|
|
858
858
|
}
|
|
859
|
-
|
|
860
|
-
// Ensure property is available with original case
|
|
861
|
-
if (prop !== lcProp) {
|
|
862
|
-
doc[prop] = doc[lcProp]
|
|
863
|
-
}
|
|
864
859
|
})
|
|
865
860
|
|
|
861
|
+
// Map SQLite 'id' column back to Quatrain 'uid' property
|
|
862
|
+
if (doc.id && !doc.uid) {
|
|
863
|
+
doc.uid = doc.id
|
|
864
|
+
}
|
|
865
|
+
|
|
866
866
|
const newDataObject: DataObjectClass<any> = await dataObject.clone({
|
|
867
867
|
...doc,
|
|
868
868
|
})
|
|
@@ -913,4 +913,81 @@ export class SQLiteAdapter extends AbstractBackendAdapter {
|
|
|
913
913
|
this._connection = undefined
|
|
914
914
|
}
|
|
915
915
|
}
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Generates the SQL up and down statements to create a collection table.
|
|
919
|
+
*/
|
|
920
|
+
generateCreateSql(collection: string, properties: any[]): { upSql: string; downSql: string } {
|
|
921
|
+
let query = `CREATE TABLE IF NOT EXISTS "${collection.toLowerCase()}" (\n id TEXT PRIMARY KEY`
|
|
922
|
+
|
|
923
|
+
properties.forEach((propDef: any) => {
|
|
924
|
+
const propName = propDef.name
|
|
925
|
+
let columnType = 'TEXT'
|
|
926
|
+
|
|
927
|
+
const type = propDef.type || propDef.constructor.name
|
|
928
|
+
if (type === 'NumberProperty') {
|
|
929
|
+
columnType = 'REAL'
|
|
930
|
+
} else if (type === 'BooleanProperty') {
|
|
931
|
+
columnType = 'INTEGER'
|
|
932
|
+
} else if (type === 'DateTimeProperty') {
|
|
933
|
+
columnType = 'INTEGER'
|
|
934
|
+
} else if (type === 'ArrayProperty') {
|
|
935
|
+
columnType = 'TEXT'
|
|
936
|
+
} else if (type === 'ObjectProperty') {
|
|
937
|
+
columnType = 'TEXT'
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
query += `,\n "${propName}" ${columnType}`
|
|
941
|
+
})
|
|
942
|
+
|
|
943
|
+
query += `\n)`
|
|
944
|
+
|
|
945
|
+
return {
|
|
946
|
+
upSql: `await adapter.rawQuery(\`${query}\`)`,
|
|
947
|
+
downSql: `await adapter.rawQuery(\`DROP TABLE IF EXISTS "${collection.toLowerCase()}"\`)`,
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Generates the SQL up and down statements to apply a schema delta to a collection.
|
|
953
|
+
*/
|
|
954
|
+
generateDeltaSql(collection: string, delta: any): { upSql: string[]; downSql: string[] } {
|
|
955
|
+
const upSql: string[] = []
|
|
956
|
+
const downSql: string[] = []
|
|
957
|
+
|
|
958
|
+
// Added columns
|
|
959
|
+
delta.added.forEach((propDef: any) => {
|
|
960
|
+
const propName = propDef.name
|
|
961
|
+
let columnType = 'TEXT'
|
|
962
|
+
|
|
963
|
+
const type = propDef.type || propDef.constructor.name
|
|
964
|
+
if (type === 'NumberProperty') {
|
|
965
|
+
columnType = 'REAL'
|
|
966
|
+
} else if (type === 'BooleanProperty') {
|
|
967
|
+
columnType = 'INTEGER'
|
|
968
|
+
} else if (type === 'DateTimeProperty') {
|
|
969
|
+
columnType = 'INTEGER'
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
upSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`)
|
|
973
|
+
// SQLite < 3.35 doesn't support DROP COLUMN, so we add a comment but keep the syntax
|
|
974
|
+
downSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`)
|
|
975
|
+
})
|
|
976
|
+
|
|
977
|
+
// Removed columns
|
|
978
|
+
delta.removed.forEach((propDef: any) => {
|
|
979
|
+
const propName = propDef.name
|
|
980
|
+
let columnType = 'TEXT'
|
|
981
|
+
|
|
982
|
+
const type = propDef.type || propDef.constructor.name
|
|
983
|
+
if (type === 'NumberProperty') columnType = 'REAL'
|
|
984
|
+
else if (type === 'BooleanProperty') columnType = 'INTEGER'
|
|
985
|
+
else if (type === 'DateTimeProperty') columnType = 'INTEGER'
|
|
986
|
+
|
|
987
|
+
upSql.push(`// NOTE: DROP COLUMN requires SQLite >= 3.35\n // await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" DROP COLUMN "${propName}"\`)`)
|
|
988
|
+
downSql.push(`await adapter.rawQuery(\`ALTER TABLE "${collection.toLowerCase()}" ADD COLUMN "${propName}" ${columnType}\`)`)
|
|
989
|
+
})
|
|
990
|
+
|
|
991
|
+
return { upSql, downSql }
|
|
992
|
+
}
|
|
916
993
|
}
|
|
@@ -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'
|