@quatrain/backend-sqlite 1.0.6 → 1.0.8

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.
@@ -0,0 +1,55 @@
1
+ import { DataObjectClass, AbstractBackendAdapter, BackendParameters, QueryResultType, Filters, Filter, SortAndLimit } from '@quatrain/backend';
2
+ import sqlite3 from 'sqlite3';
3
+ import { Database } from 'sqlite';
4
+ /**
5
+ * SQLite Backend Adapter for Quatrain
6
+ */
7
+ export declare class SQLiteAdapter extends AbstractBackendAdapter {
8
+ protected _connection: undefined | Database<sqlite3.Database>;
9
+ protected _dbPath: string;
10
+ constructor(params?: BackendParameters);
11
+ protected _buildPath(dataObject: DataObjectClass<any>, uid?: string): string;
12
+ protected _connect(): Promise<Database<sqlite3.Database>>;
13
+ /**
14
+ * Process data for compatibility
15
+ * @param data
16
+ * @param filterNulls
17
+ * @returns
18
+ */
19
+ protected _prepareData(data: any, filterNulls?: boolean): any;
20
+ /**
21
+ * Ensure the collection table exists in SQLite
22
+ * @param dataObject DataObject to create table for
23
+ */
24
+ private _ensureTable;
25
+ /**
26
+ * Create record in backend
27
+ * @param dataObject DataObject instance to persist in backend
28
+ * @param desiredUid Desired unique ID for record
29
+ * @returns DataObject
30
+ */
31
+ create(dataObject: DataObjectClass<any>, desiredUid: string | undefined): Promise<DataObjectClass<any>>;
32
+ read(dataObject: DataObjectClass<any>): Promise<DataObjectClass<any>>;
33
+ update(dataObject: DataObjectClass<any>): Promise<DataObjectClass<any>>;
34
+ delete(dataObject: DataObjectClass<any>, hardDelete?: boolean): Promise<DataObjectClass<any>>;
35
+ deleteCollection(collection: string, batchSize?: number): Promise<void>;
36
+ /**
37
+ * Convert array into SQL expression
38
+ * @param from Array of strings or numbers
39
+ * @returns string
40
+ */
41
+ protected _array2String(from: (string | number)[]): string;
42
+ /**
43
+ * Execute a query on a collection
44
+ * @param dataObject
45
+ * @param filters
46
+ * @param pagination
47
+ * @params parent
48
+ * @returns
49
+ */
50
+ find(dataObject: DataObjectClass<any>, filters?: Filters | Filter[] | undefined, pagination?: SortAndLimit | undefined, parent?: DataObjectClass<any> | undefined): Promise<QueryResultType<DataObjectClass<any>>>;
51
+ /**
52
+ * Close the SQLite connection
53
+ */
54
+ close(): Promise<void>;
55
+ }
@@ -0,0 +1,696 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.SQLiteAdapter = void 0;
16
+ const core_1 = require("@quatrain/core");
17
+ const backend_1 = require("@quatrain/backend");
18
+ const crypto_1 = require("crypto");
19
+ const sqlite3_1 = __importDefault(require("sqlite3"));
20
+ const sqlite_1 = require("sqlite");
21
+ const operatorsMap = {
22
+ equals: '=',
23
+ notEquals: '!=',
24
+ greater: '>',
25
+ greaterOrEquals: '>=',
26
+ lower: '<',
27
+ lowerOrEquals: '<=', // Corrected from '>' in PostgreSQL implementation
28
+ like: 'LIKE',
29
+ contains: 'IN',
30
+ notContains: 'NOT IN',
31
+ containsAll: 'JSON_EXTRACT', // Use JSON_EXTRACT with custom logic
32
+ containsAny: 'JSON_EXTRACT', // Use JSON_EXTRACT with custom logic
33
+ isNull: 'IS NULL',
34
+ isNotNull: 'IS NOT NULL',
35
+ };
36
+ /**
37
+ * SQLite Backend Adapter for Quatrain
38
+ */
39
+ class SQLiteAdapter extends backend_1.AbstractBackendAdapter {
40
+ constructor(params = {}) {
41
+ var _a;
42
+ super(params);
43
+ this._dbPath = ((_a = params.config) === null || _a === void 0 ? void 0 : _a.database) || ':memory:';
44
+ }
45
+ _buildPath(dataObject, uid) {
46
+ const collection = this.getCollection(dataObject);
47
+ if (!collection) {
48
+ throw new backend_1.BackendError(`[SQLA] Can't define record path without a collection name`);
49
+ }
50
+ // define document path
51
+ let path = `${collection}/${uid}`;
52
+ if (this._params.hierarchy &&
53
+ this._params.hierarchy[collection] ===
54
+ backend_1.CollectionHierarchy.SUBCOLLECTION &&
55
+ dataObject.parentProp &&
56
+ dataObject.has(dataObject.parentProp) &&
57
+ dataObject.val(dataObject.parentProp)) {
58
+ path = `${dataObject.val(dataObject.parentProp).path}/${path}`;
59
+ }
60
+ backend_1.Backend.log(`[SQLA] Record path is '${path}'`);
61
+ return path;
62
+ }
63
+ _connect() {
64
+ return __awaiter(this, void 0, void 0, function* () {
65
+ if (!this._connection) {
66
+ // Open SQLite database
67
+ this._connection = yield (0, sqlite_1.open)({
68
+ filename: this._dbPath,
69
+ driver: sqlite3_1.default.Database,
70
+ });
71
+ // Enable foreign keys support
72
+ yield this._connection.run('PRAGMA foreign_keys = ON');
73
+ // Configure SQLite to handle JSON arrays and objects
74
+ // SQLite doesn't support CREATE FUNCTION syntax, so we'll use built-in JSON functions
75
+ // Enable JSON1 extension if available
76
+ try {
77
+ yield this._connection.exec('SELECT json_valid(\'[]\')'); // Test if JSON1 is available
78
+ }
79
+ catch (err) {
80
+ backend_1.Backend.warn('[SQLA] JSON1 extension not available, array operations may be limited');
81
+ }
82
+ }
83
+ return this._connection;
84
+ });
85
+ }
86
+ /**
87
+ * Process data for compatibility
88
+ * @param data
89
+ * @param filterNulls
90
+ * @returns
91
+ */
92
+ _prepareData(data, filterNulls = true) {
93
+ if (filterNulls) {
94
+ data = Object.entries(data)
95
+ .filter(([_, v]) => v !== null && v !== '')
96
+ .map(([_, v]) => v);
97
+ }
98
+ else {
99
+ data = Object.values(data);
100
+ }
101
+ // Handle arrays by converting them to JSON strings for SQLite
102
+ data.forEach((el, key) => {
103
+ if (Array.isArray(el)) {
104
+ data[key] = JSON.stringify(el);
105
+ }
106
+ });
107
+ if (this._params['useNativeForeignKeys'] &&
108
+ this._params['useNativeForeignKeys'] === true) {
109
+ data.forEach((el, key) => {
110
+ if (typeof el === 'object' &&
111
+ el !== null &&
112
+ Reflect.has(el, 'ref')) {
113
+ const resourcePart = el.ref.split('/').pop();
114
+ if (resourcePart.indexOf('.') === -1) {
115
+ // convert reference for database objects only
116
+ data[key] = el.ref.split('/').pop();
117
+ }
118
+ }
119
+ });
120
+ }
121
+ return data;
122
+ }
123
+ /**
124
+ * Ensure the collection table exists in SQLite
125
+ * @param dataObject DataObject to create table for
126
+ */
127
+ _ensureTable(dataObject) {
128
+ return __awaiter(this, void 0, void 0, function* () {
129
+ const collection = this.getCollection(dataObject);
130
+ if (!collection) {
131
+ throw new backend_1.BackendError(`[SQLA] Cannot determine collection name`);
132
+ }
133
+ const db = yield this._connect();
134
+ const tableExists = yield db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [collection.toLowerCase()]);
135
+ if (!tableExists) {
136
+ // Table doesn't exist, create it
137
+ let query = `CREATE TABLE IF NOT EXISTS ${collection.toLowerCase()} (
138
+ id TEXT PRIMARY KEY`;
139
+ // Add columns based on dataObject properties
140
+ Object.entries(dataObject.properties).forEach(([prop, propDef]) => {
141
+ const propName = prop.toLowerCase();
142
+ let columnType = 'TEXT';
143
+ // Map property types to SQLite column types
144
+ if (propDef.constructor.name === 'NumberProperty') {
145
+ columnType = 'REAL';
146
+ }
147
+ else if (propDef.constructor.name === 'BooleanProperty') {
148
+ columnType = 'INTEGER';
149
+ }
150
+ else if (propDef.constructor.name === 'DateTimeProperty') {
151
+ columnType = 'INTEGER'; // Store as timestamp
152
+ }
153
+ else if (propDef.constructor.name === 'ArrayProperty') {
154
+ columnType = 'TEXT'; // Store as JSON string
155
+ }
156
+ else if (propDef.constructor.name === 'ObjectProperty') {
157
+ columnType = 'TEXT'; // Store reference ID
158
+ }
159
+ query += `,\n${propName} ${columnType}`;
160
+ });
161
+ query += `)`;
162
+ yield db.exec(query);
163
+ backend_1.Backend.log(`[SQLA] Created table ${collection.toLowerCase()}`);
164
+ }
165
+ });
166
+ }
167
+ /**
168
+ * Create record in backend
169
+ * @param dataObject DataObject instance to persist in backend
170
+ * @param desiredUid Desired unique ID for record
171
+ * @returns DataObject
172
+ */
173
+ create(dataObject, desiredUid) {
174
+ return __awaiter(this, void 0, void 0, function* () {
175
+ return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
176
+ try {
177
+ if (dataObject.uid) {
178
+ throw new backend_1.BackendError(`Data object already has an uid and can't be created`);
179
+ }
180
+ const uid = desiredUid || (0, crypto_1.randomUUID)();
181
+ // Make sure table exists
182
+ yield this._ensureTable(dataObject);
183
+ // execute middlewares
184
+ yield this.executeMiddlewares(dataObject, backend_1.BackendAction.CREATE, {
185
+ useDateFormat: true,
186
+ });
187
+ const data = dataObject.toJSON({
188
+ withoutURIData: true,
189
+ converters: {
190
+ datetime: (v) => (v ? new Date(v).getTime() : v), // Store as timestamp in SQLite
191
+ },
192
+ });
193
+ const db = yield this._connect();
194
+ const collection = this.getCollection(dataObject);
195
+ let columns = ['id'];
196
+ let placeholders = ['?'];
197
+ let values = [uid];
198
+ Object.entries(data).forEach(([key, value]) => {
199
+ columns.push(key.toLowerCase());
200
+ placeholders.push('?');
201
+ // Convert arrays and objects to JSON strings
202
+ if (Array.isArray(value) ||
203
+ (typeof value === 'object' && value !== null)) {
204
+ values.push(JSON.stringify(value));
205
+ }
206
+ else {
207
+ values.push(value);
208
+ }
209
+ });
210
+ const query = `INSERT INTO ${collection === null || collection === void 0 ? void 0 : collection.toLowerCase()} (${columns.join(', ')})
211
+ VALUES (${placeholders.join(', ')})`;
212
+ backend_1.Backend.debug(`[SQLA] ${query}`);
213
+ backend_1.Backend.debug(`[SQLA] Values ${JSON.stringify(values)}`);
214
+ yield db.run(query, values);
215
+ dataObject.uri.path = this._buildPath(dataObject, uid);
216
+ dataObject.uri.label = data && Reflect.get(data, 'name');
217
+ dataObject.isPersisted(true);
218
+ backend_1.Backend.info(`[SQLA] Saved object "${data.name}" at path ${dataObject.path}`);
219
+ resolve(dataObject);
220
+ }
221
+ catch (err) {
222
+ console.error(err);
223
+ backend_1.Backend.error(err.message);
224
+ reject(new backend_1.BackendError(err.message));
225
+ }
226
+ }));
227
+ });
228
+ }
229
+ read(dataObject) {
230
+ return __awaiter(this, void 0, void 0, function* () {
231
+ const path = dataObject.path;
232
+ const collection = this.getCollection(dataObject);
233
+ const parts = path.split('/');
234
+ if (parts.length < 2 || parts.length % 2 !== 0) {
235
+ throw new backend_1.BackendError(`[SQLA] path parts number should be even, received: '${path}'`);
236
+ }
237
+ backend_1.Backend.log(`[SQLA] Getting document ${path}`);
238
+ if (!collection) {
239
+ throw new backend_1.BackendError(`[SQLA] Can't find collection matching object to query`);
240
+ }
241
+ const db = yield this._connect();
242
+ const uid = parts[parts.length - 1];
243
+ // Ensure table exists
244
+ yield this._ensureTable(dataObject);
245
+ const result = yield db.get(`SELECT * FROM ${collection.toLowerCase()} WHERE id = ?`, [uid]);
246
+ if (!result) {
247
+ throw new core_1.NotFoundError(`[SQLA] No document matches path '${path}'`);
248
+ }
249
+ // Process object references
250
+ for (const prop in dataObject.properties) {
251
+ const propDef = dataObject.properties[prop];
252
+ if (propDef.constructor.name === 'ObjectProperty' &&
253
+ propDef.instanceOf) {
254
+ const propValue = result[prop.toLowerCase()];
255
+ if (propValue) {
256
+ let refTable = undefined;
257
+ if (this._params.mapping && this._params.mapping[propDef.instanceOf]) {
258
+ refTable = this._params.mapping[propDef.instanceOf];
259
+ }
260
+ else if (propDef.instanceOf && propDef.instanceOf.COLLECTION) {
261
+ refTable = propDef.instanceOf.COLLECTION;
262
+ }
263
+ if (refTable) {
264
+ // Look up the referenced object for its name
265
+ const refObject = yield db.get(`SELECT name FROM ${refTable.toLowerCase()} WHERE id = ?`, [propValue]);
266
+ if (refObject) {
267
+ result[prop] = {
268
+ ref: `${refTable}/${propValue}`,
269
+ path: `${refTable}/${propValue}`,
270
+ label: refObject.name || '',
271
+ };
272
+ }
273
+ }
274
+ }
275
+ }
276
+ else if (propDef.constructor.name === 'ArrayProperty') {
277
+ // Parse JSON arrays
278
+ try {
279
+ if (result[prop.toLowerCase()]) {
280
+ result[prop] = JSON.parse(result[prop.toLowerCase()]);
281
+ }
282
+ }
283
+ catch (e) {
284
+ backend_1.Backend.warn(`[SQLA] Failed to parse array for ${prop}: ${e}`);
285
+ }
286
+ }
287
+ // Normalize property name case
288
+ if (prop.toLowerCase() !== prop) {
289
+ result[prop] = result[prop.toLowerCase()];
290
+ }
291
+ }
292
+ dataObject.populate(result);
293
+ return dataObject;
294
+ });
295
+ }
296
+ update(dataObject) {
297
+ return __awaiter(this, void 0, void 0, function* () {
298
+ if (dataObject.uid === undefined) {
299
+ throw new Error('DataObject has no uid');
300
+ }
301
+ backend_1.Backend.info(`[SQLA] Updating document ${dataObject.path}`);
302
+ // execute middlewares
303
+ yield this.executeMiddlewares(dataObject, backend_1.BackendAction.UPDATE);
304
+ const data = dataObject.toJSON({
305
+ withoutURIData: true,
306
+ ignoreUnchanged: true,
307
+ converters: {
308
+ datetime: (ts) => ts, // Store directly as timestamp in SQLite
309
+ },
310
+ });
311
+ if (Object.keys(data).length === 0) {
312
+ backend_1.Backend.warn('[SQLA] Nothing to update');
313
+ return dataObject;
314
+ }
315
+ backend_1.Backend.debug(`[SQLA] Data to update ${JSON.stringify(data)}`);
316
+ const db = yield this._connect();
317
+ const collection = this.getCollection(dataObject);
318
+ // Ensure table exists
319
+ yield this._ensureTable(dataObject);
320
+ let updates = [];
321
+ let values = [];
322
+ Object.entries(data).forEach(([key, value]) => {
323
+ updates.push(`${key.toLowerCase()} = ?`);
324
+ // Convert arrays and objects to JSON strings
325
+ if (Array.isArray(value) ||
326
+ (typeof value === 'object' && value !== null)) {
327
+ value = JSON.stringify(value);
328
+ }
329
+ values.push(value);
330
+ });
331
+ if (updates.length === 0) {
332
+ return dataObject;
333
+ }
334
+ values.push(dataObject.uid);
335
+ const query = `UPDATE ${collection === null || collection === void 0 ? void 0 : collection.toLowerCase()} SET ${updates.join(', ')} WHERE id = ?`;
336
+ backend_1.Backend.debug(`[SQLA] ${query}`);
337
+ backend_1.Backend.debug(`[SQLA] Values ${JSON.stringify(values)}`);
338
+ yield db.run(query, values);
339
+ return dataObject;
340
+ });
341
+ }
342
+ delete(dataObject, hardDelete = false) {
343
+ return __awaiter(this, void 0, void 0, function* () {
344
+ if (dataObject.uid === undefined) {
345
+ throw new backend_1.BackendError('Dataobject has no uid');
346
+ }
347
+ const collection = this.getCollection(dataObject);
348
+ if (!collection) {
349
+ throw new backend_1.BackendError(`[SQLA] Cannot determine collection name`);
350
+ }
351
+ // execute middlewares
352
+ yield this.executeMiddlewares(dataObject, backend_1.BackendAction.DELETE, {
353
+ useDateFormat: true,
354
+ });
355
+ const db = yield this._connect();
356
+ if (!hardDelete) {
357
+ dataObject.set('status', core_1.statuses.DELETED);
358
+ yield db.run(`UPDATE ${collection.toLowerCase()} SET status = ? WHERE id = ?`, [core_1.statuses.DELETED, dataObject.uid]);
359
+ }
360
+ else {
361
+ yield db.run(`DELETE FROM ${collection.toLowerCase()} WHERE id = ?`, [
362
+ dataObject.uid,
363
+ ]);
364
+ }
365
+ dataObject.uri = new core_1.ObjectUri();
366
+ return dataObject;
367
+ });
368
+ }
369
+ deleteCollection(collection, batchSize = 500) {
370
+ return __awaiter(this, void 0, void 0, function* () {
371
+ backend_1.Backend.log(`Deleting all records from collection '${collection}'`);
372
+ const db = yield this._connect();
373
+ // Check if table exists before trying to delete from it
374
+ const tableExists = yield db.get(`SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [collection.toLowerCase()]);
375
+ if (tableExists) {
376
+ yield db.run(`DELETE FROM ${collection.toLowerCase()}`);
377
+ }
378
+ });
379
+ }
380
+ /**
381
+ * Convert array into SQL expression
382
+ * @param from Array of strings or numbers
383
+ * @returns string
384
+ */
385
+ _array2String(from) {
386
+ // For SQLite, we'll use the JSON functions to check arrays
387
+ return `'${JSON.stringify(from)}'`;
388
+ }
389
+ /**
390
+ * Execute a query on a collection
391
+ * @param dataObject
392
+ * @param filters
393
+ * @param pagination
394
+ * @params parent
395
+ * @returns
396
+ */
397
+ find(dataObject, filters = undefined, pagination = undefined, parent = undefined) {
398
+ return __awaiter(this, void 0, void 0, function* () {
399
+ try {
400
+ // use parent path to start fullPath, if available
401
+ let fullPath = parent ? `${parent.path}/` : '';
402
+ if (dataObject.path && dataObject.path !== core_1.ObjectUri.DEFAULT) {
403
+ fullPath += `${dataObject.path}/`;
404
+ }
405
+ const collection = this.getCollection(dataObject);
406
+ if (!collection) {
407
+ throw new backend_1.BackendError(`[SQLA] Can't find collection matching object to query`);
408
+ }
409
+ backend_1.Backend.debug(`[SQLA] Preparing query on '${collection}'`);
410
+ const db = yield this._connect();
411
+ // Ensure table exists
412
+ yield this._ensureTable(dataObject);
413
+ let hasFilters = false;
414
+ const query = [];
415
+ const params = [];
416
+ const joinTables = {};
417
+ query.push(`SELECT * FROM ${collection.toLowerCase()}`);
418
+ // Add joins for object references
419
+ Object.entries(dataObject.properties).forEach(([prop, propDef]) => {
420
+ if (propDef.constructor.name === 'ObjectProperty' &&
421
+ propDef.instanceOf) {
422
+ const propName = prop.toLowerCase();
423
+ const joinAlias = `${propName}_table`;
424
+ let table = undefined;
425
+ if (this._params.mapping && this._params.mapping[propDef.instanceOf]) {
426
+ table = this._params.mapping[propDef.instanceOf];
427
+ }
428
+ else if (propDef.instanceOf && propDef.instanceOf.COLLECTION) {
429
+ table = propDef.instanceOf.COLLECTION;
430
+ }
431
+ if (table) {
432
+ joinTables[prop] = { table, alias: joinAlias };
433
+ query.push(`LEFT JOIN ${table.toLowerCase()} AS ${joinAlias}
434
+ ON ${joinAlias}.id = ${collection.toLowerCase()}.${propName}`);
435
+ }
436
+ else {
437
+ backend_1.Backend.warn(`[SQLA] Skipping join for property ${prop} - no collection found`);
438
+ }
439
+ }
440
+ });
441
+ if (parent) {
442
+ query.push(`WHERE ${collection.toLowerCase()}.${dataObject.parentProp} = ?`);
443
+ params.push(parent.uid);
444
+ }
445
+ if (filters instanceof backend_1.Filters) {
446
+ hasFilters = true;
447
+ // SQLite doesn't support complex Filters object, but we'll mark it as handled
448
+ }
449
+ else if (Array.isArray(filters)) {
450
+ // list of filters objects
451
+ filters.forEach((filter, i) => {
452
+ query.push(parent && i === 0 ? 'AND' : i > 0 ? 'AND' : 'WHERE');
453
+ let realProp = filter.prop.toLowerCase();
454
+ let realOperator = operatorsMap[filter.operator];
455
+ let realValue = filter.value;
456
+ if (filter.prop === 'keywords') {
457
+ const keywordFilters = [];
458
+ const props = dataObject.getProperties(core_1.StringProperty.name);
459
+ Object.keys(props).forEach((rp) => {
460
+ keywordFilters.push(`${collection.toLowerCase()}.${rp.toLowerCase()} LIKE ?`);
461
+ params.push(`%${filter.value}%`);
462
+ });
463
+ query.push(`(${keywordFilters.join(' OR ')})`);
464
+ }
465
+ else if (filter.prop !== backend_1.AbstractBackendAdapter.PKEY_IDENTIFIER &&
466
+ !dataObject.has(filter.prop)) {
467
+ throw new backend_1.BackendError(`[SQLA] No such property '${filter.prop}' on object'`);
468
+ }
469
+ else if (filter.prop === backend_1.AbstractBackendAdapter.PKEY_IDENTIFIER) {
470
+ realProp = 'id';
471
+ }
472
+ else {
473
+ const property = dataObject.get(filter.prop);
474
+ realProp = filter.prop.toLowerCase();
475
+ if (property.constructor.name === 'ArrayProperty' &&
476
+ Array.isArray(realValue)) {
477
+ // Use EXISTS with json_each for array containment in SQLite
478
+ const placeholders = realValue.map(() => 'json_each.value = ?').join(' OR ');
479
+ query.push(`EXISTS (SELECT 1 FROM json_each(${collection.toLowerCase()}.${realProp}) WHERE ${placeholders})`);
480
+ params.push(...realValue);
481
+ // Skip further processing for this filter
482
+ backend_1.Backend.debug(`[SQLA] Array filter added: ${realProp} EXISTS ${String(realValue)}`);
483
+ }
484
+ else if (property.constructor.name === 'ObjectProperty') {
485
+ if (filter.value instanceof core_1.ObjectUri) {
486
+ realValue = filter.value.uid;
487
+ }
488
+ else if (filter.value &&
489
+ typeof filter.value === 'object' &&
490
+ filter.value.ref) {
491
+ realValue = filter.value.ref.split('/')[1];
492
+ }
493
+ else if (typeof filter.value === 'string') {
494
+ const collectionName = this._params.mapping &&
495
+ this._params.mapping[dataObject.properties[filter.prop].instanceOf]
496
+ ? this._params.mapping[dataObject.properties[filter.prop].instanceOf]
497
+ : dataObject.properties[filter.prop].instanceOf
498
+ .COLLECTION;
499
+ realValue = filter.value.replace(`${collectionName}/`, '');
500
+ }
501
+ else if (filter.value &&
502
+ typeof filter.value === 'object' &&
503
+ filter.value.uid) {
504
+ // Handle DataObject instances
505
+ realValue = filter.value.uid;
506
+ }
507
+ else {
508
+ realValue =
509
+ (filter.value &&
510
+ filter.value.uri &&
511
+ filter.value.uri.path &&
512
+ filter.value.uri.path.split('/')[1]) ||
513
+ filter.value;
514
+ }
515
+ }
516
+ // Only add the filter query if it's not an ArrayProperty (which was already handled above)
517
+ if (!(property.constructor.name === 'ArrayProperty' && Array.isArray(realValue))) {
518
+ if (realOperator === operatorsMap['containsAny']) {
519
+ // Use EXISTS with json_each for array containment in SQLite
520
+ query.push(`EXISTS (SELECT 1 FROM json_each(${collection.toLowerCase()}.${realProp}) WHERE json_each.value = ?)`);
521
+ params.push(realValue);
522
+ }
523
+ else if (realOperator === operatorsMap['equals'] &&
524
+ realValue === 'null') {
525
+ query.push(`${collection.toLowerCase()}.${realProp} IS NULL`);
526
+ }
527
+ else if (realOperator === operatorsMap['contains'] ||
528
+ realOperator === operatorsMap['notContains']) {
529
+ if (Array.isArray(realValue)) {
530
+ const placeholders = realValue.map(() => '?').join(', ');
531
+ query.push(`${collection.toLowerCase()}.${realProp} ${realOperator} (${placeholders})`);
532
+ params.push(...realValue);
533
+ }
534
+ else {
535
+ query.push(`${collection.toLowerCase()}.${realProp} ${realOperator} (?)`);
536
+ params.push(realValue);
537
+ }
538
+ }
539
+ else {
540
+ // Handle ObjectProperty specially for JSON-stored references
541
+ if (property && property.constructor.name === 'ObjectProperty') {
542
+ if (realOperator === operatorsMap.equals) {
543
+ // For ObjectProperty, check if the JSON contains the reference
544
+ query.push(`json_extract(${collection.toLowerCase()}.${realProp}, '$.ref') LIKE ?`);
545
+ params.push(`%${realValue}`);
546
+ }
547
+ else {
548
+ query.push(`${collection.toLowerCase()}.${realProp} ${realOperator} ?`);
549
+ params.push(realValue);
550
+ }
551
+ }
552
+ else if (realOperator === operatorsMap.like) {
553
+ query.push(`${collection.toLowerCase()}.${realProp} ${realOperator} ?`);
554
+ params.push(`%${realValue}%`);
555
+ }
556
+ else if (realOperator === operatorsMap.isNull ||
557
+ realOperator === operatorsMap.isNotNull) {
558
+ query.push(`${collection.toLowerCase()}.${realProp} ${realOperator}`);
559
+ }
560
+ else {
561
+ query.push(`${collection.toLowerCase()}.${realProp} ${realOperator} ?`);
562
+ params.push(realValue);
563
+ }
564
+ }
565
+ backend_1.Backend.debug(`[SQLA] Filter added: ${realProp} ${realOperator} ${String(realValue)}`);
566
+ }
567
+ }
568
+ });
569
+ }
570
+ // Count query - without pagination
571
+ const countQuery = query.join(' ').replace('*', 'COUNT(*) as total');
572
+ backend_1.Backend.debug(`[SQLA] Count SQL ${countQuery}`);
573
+ const countResult = yield db.get(countQuery, params);
574
+ const totalCount = countResult ? countResult.total : 0;
575
+ backend_1.Backend.debug(`[SQLA] Counting records ${totalCount}`);
576
+ // Add sorting and pagination
577
+ let sortField = [];
578
+ if (pagination && pagination.sortings) {
579
+ pagination.sortings.forEach((sorting, i) => {
580
+ query.push(i === 0 ? `ORDER BY` : ',');
581
+ query.push(`${collection.toLowerCase()}.${sorting.prop.toLowerCase()} ${sorting.order}`);
582
+ if (sorting.prop !== undefined) {
583
+ sortField.push(`${sorting.prop} ${sorting.order.toUpperCase()}`);
584
+ }
585
+ });
586
+ if ((pagination === null || pagination === void 0 ? void 0 : pagination.limits.batch) !== -1) {
587
+ query.push(`LIMIT ?`);
588
+ params.push(pagination.limits.batch);
589
+ }
590
+ if (pagination === null || pagination === void 0 ? void 0 : pagination.limits.offset) {
591
+ query.push(`OFFSET ?`);
592
+ params.push(pagination.limits.offset);
593
+ }
594
+ }
595
+ const finalQuery = query.join(' ');
596
+ backend_1.Backend.debug(`[SQLA] Full SQL ${finalQuery}`);
597
+ backend_1.Backend.debug(`[SQLA] Params ${JSON.stringify(params)}`);
598
+ const results = yield db.all(finalQuery, params);
599
+ const meta = {
600
+ count: totalCount,
601
+ offset: (pagination === null || pagination === void 0 ? void 0 : pagination.limits.offset) || 0,
602
+ batch: (pagination === null || pagination === void 0 ? void 0 : pagination.limits.batch) || 20,
603
+ sortField: sortField.join(', '),
604
+ executionTime: backend_1.Backend.timestamp(),
605
+ debug: { sql: finalQuery, params },
606
+ };
607
+ const items = [];
608
+ for (let doc of results || []) {
609
+ // Process document before populating
610
+ Object.entries(dataObject.properties).forEach(([prop, propDef]) => {
611
+ const lcProp = prop.toLowerCase();
612
+ // Handle ObjectProperty references
613
+ if (propDef.constructor.name === 'ObjectProperty' &&
614
+ propDef.instanceOf) {
615
+ const refValue = doc[lcProp];
616
+ if (refValue) {
617
+ const info = joinTables[prop];
618
+ if (info) {
619
+ const label = doc[`${lcProp}_table_name`] || '';
620
+ doc[prop] = {
621
+ ref: `${info.table}/${refValue}`,
622
+ path: `${info.table}/${refValue}`,
623
+ label,
624
+ };
625
+ }
626
+ else {
627
+ // Fallback when no join table info is available
628
+ // Try to determine table name from propDef
629
+ let tableName = undefined;
630
+ if (this._params.mapping && this._params.mapping[propDef.instanceOf]) {
631
+ tableName = this._params.mapping[propDef.instanceOf];
632
+ }
633
+ else if (propDef.instanceOf && propDef.instanceOf.COLLECTION) {
634
+ tableName = propDef.instanceOf.COLLECTION;
635
+ }
636
+ if (tableName) {
637
+ doc[prop] = {
638
+ ref: `${tableName}/${refValue}`,
639
+ path: `${tableName}/${refValue}`,
640
+ label: '',
641
+ };
642
+ }
643
+ }
644
+ }
645
+ }
646
+ // Handle array properties
647
+ else if (propDef.constructor.name === 'ArrayProperty') {
648
+ try {
649
+ if (doc[lcProp]) {
650
+ doc[prop] = JSON.parse(doc[lcProp]);
651
+ }
652
+ }
653
+ catch (e) {
654
+ backend_1.Backend.warn(`[SQLA] Failed to parse array for ${prop}: ${e}`);
655
+ }
656
+ }
657
+ // Ensure property is available with original case
658
+ if (prop !== lcProp) {
659
+ doc[prop] = doc[lcProp];
660
+ }
661
+ });
662
+ const newDataObject = yield dataObject.clone(Object.assign({}, doc));
663
+ let newDataObjectUri = ``;
664
+ if (newDataObject.has('parent')) {
665
+ if (!(newDataObject.val('parent') &&
666
+ newDataObject.val('parent').path)) {
667
+ throw new backend_1.BackendError(`DataObject has parent but parent is not persisted`);
668
+ }
669
+ newDataObjectUri = `${newDataObject.get('parent')._value._path}/`;
670
+ }
671
+ newDataObjectUri += `${this.getCollection(dataObject)}/${doc.id}`;
672
+ newDataObject.uri = new core_1.ObjectUri(newDataObjectUri, newDataObject.val('name'));
673
+ items.push(newDataObject);
674
+ }
675
+ return { items, meta };
676
+ }
677
+ catch (err) {
678
+ console.error(err);
679
+ backend_1.Backend.error(`[SQLA] Query failed: ${err.message}`);
680
+ throw new backend_1.BackendError(`Query failed for '${dataObject.class.name}': ${err.message}`);
681
+ }
682
+ });
683
+ }
684
+ /**
685
+ * Close the SQLite connection
686
+ */
687
+ close() {
688
+ return __awaiter(this, void 0, void 0, function* () {
689
+ if (this._connection) {
690
+ yield this._connection.close();
691
+ this._connection = undefined;
692
+ }
693
+ });
694
+ }
695
+ }
696
+ exports.SQLiteAdapter = SQLiteAdapter;
@@ -0,0 +1,2 @@
1
+ import { SQLiteAdapter } from './SQLiteAdapter';
2
+ export { SQLiteAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SQLiteAdapter = void 0;
4
+ const SQLiteAdapter_1 = require("./SQLiteAdapter");
5
+ Object.defineProperty(exports, "SQLiteAdapter", { enumerable: true, get: function () { return SQLiteAdapter_1.SQLiteAdapter; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quatrain/backend-sqlite",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "license": "AGPL-3.0-only",
5
5
  "description": "Backend adapter for SQLite",
6
6
  "main": "dist/index.js",
@@ -14,8 +14,8 @@
14
14
  ],
15
15
  "author": "Quatrain Développement SAS <developers@quatrain.com>",
16
16
  "dependencies": {
17
- "@quatrain/backend": "^1.1.28",
18
- "@quatrain/core": "^1.1.43",
17
+ "@quatrain/backend": "^1.1.30",
18
+ "@quatrain/core": "^1.1.45",
19
19
  "sqlite": "^5.1.1",
20
20
  "sqlite3": "^5.1.7"
21
21
  },