@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.
@@ -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
  }
@@ -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.log(`[SQLA] Record path is '${path}'`);
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.toLowerCase();
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.log(`[SQLA] Created table ${collection.toLowerCase()}`);
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}"`);
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.log(`[SQLA] Getting document ${path}`);
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.toLowerCase()];
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.toLowerCase()]) {
280
- result[prop] = JSON.parse(result[prop.toLowerCase()]);
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 array for ${prop}: ${e}`);
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(`${key.toLowerCase()} = ?`);
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.toLowerCase();
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.toLowerCase();
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.toLowerCase()} LIKE ?`);
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.toLowerCase();
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.toLowerCase()} ${sorting.order}`);
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[lcProp];
619
+ const refValue = doc[prop];
616
620
  if (refValue) {
617
621
  const info = joinTables[prop];
618
622
  if (info) {
619
- const label = doc[`${lcProp}_table_name`] || '';
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[lcProp]) {
650
- doc[prop] = JSON.parse(doc[lcProp]);
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 array for ${prop}: ${e}`);
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.0.16",
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.37",
24
- "@quatrain/core": "^1.1.51",
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
  }
@@ -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.log(`[SQLA] Record path is '${path}'`)
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.toLowerCase()
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.log(`[SQLA] Created table ${collection.toLowerCase()}`)
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}"`)
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.log(`[SQLA] Getting document ${path}`)
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.toLowerCase()]
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.toLowerCase()]) {
358
- result[prop] = JSON.parse(result[prop.toLowerCase()])
364
+ if (result[prop]) {
365
+ result[prop] = JSON.parse(result[prop])
359
366
  }
360
367
  } catch (e) {
361
- Backend.warn(`[SQLA] Failed to parse array for ${prop}: ${e}`)
368
+ Backend.warn(`[SQLA] Failed to parse JSON for ${prop}: ${e}`)
362
369
  }
363
370
  }
364
371
 
365
- // Normalize property name case
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(`${key.toLowerCase()} = ?`)
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.toLowerCase()
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.toLowerCase()
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.toLowerCase()} LIKE ?`
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.toLowerCase()
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.toLowerCase()} ${
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[lcProp]
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[`${lcProp}_table_name`] || ''
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[lcProp]) {
851
- doc[prop] = JSON.parse(doc[lcProp])
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 array for ${prop}: ${e}`
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'