@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.
- package/dist/SQLiteAdapter.d.ts +55 -0
- package/dist/SQLiteAdapter.js +696 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +5 -0
- package/package.json +3 -3
|
@@ -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;
|
package/dist/index.d.ts
ADDED
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.
|
|
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.
|
|
18
|
-
"@quatrain/core": "^1.1.
|
|
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
|
},
|