@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.
@@ -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) {
@@ -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.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.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
  }
@@ -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 array for ${prop}: ${e}`);
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(`${key.toLowerCase()} = ?`);
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 array for ${prop}: ${e}`);
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.0.16",
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.37",
24
- "@quatrain/core": "^1.1.51",
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
  }
@@ -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
@@ -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.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.log(`[SQLA] Getting document ${path}`)
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 array for ${prop}: ${e}`)
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(`${key.toLowerCase()} = ?`)
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 array for ${prop}: ${e}`
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'