@objectql/driver-fs 4.0.2 → 4.0.3
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/CHANGELOG.md +17 -0
- package/dist/index.d.ts +32 -171
- package/dist/index.js +144 -827
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -69,932 +69,249 @@ exports.FileSystemDriver = void 0;
|
|
|
69
69
|
const fs = __importStar(require("fs"));
|
|
70
70
|
const path = __importStar(require("path"));
|
|
71
71
|
const types_1 = require("@objectql/types");
|
|
72
|
+
const driver_memory_1 = require("@objectql/driver-memory");
|
|
72
73
|
/**
|
|
73
74
|
* FileSystem Driver Implementation
|
|
74
75
|
*
|
|
76
|
+
* Extends MemoryDriver with file system persistence.
|
|
77
|
+
* All query and aggregation logic is inherited from MemoryDriver.
|
|
78
|
+
* Only the persistence layer (load/save) is overridden.
|
|
79
|
+
*
|
|
75
80
|
* Stores ObjectQL documents in JSON files with format:
|
|
76
81
|
* - File: `{dataDir}/{objectName}.json`
|
|
77
82
|
* - Content: Array of records `[{id: "1", ...}, {id: "2", ...}]`
|
|
78
83
|
*/
|
|
79
|
-
class FileSystemDriver {
|
|
84
|
+
class FileSystemDriver extends driver_memory_1.MemoryDriver {
|
|
80
85
|
constructor(config) {
|
|
81
|
-
//
|
|
86
|
+
// Initialize parent with inherited config properties
|
|
87
|
+
super({
|
|
88
|
+
strictMode: config.strictMode,
|
|
89
|
+
initialData: config.initialData,
|
|
90
|
+
indexes: config.indexes
|
|
91
|
+
});
|
|
92
|
+
// Override driver name and version
|
|
82
93
|
this.name = 'FileSystemDriver';
|
|
83
94
|
this.version = '4.0.0';
|
|
84
|
-
this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
fullTextSearch: false,
|
|
88
|
-
jsonFields: true,
|
|
89
|
-
arrayFields: true,
|
|
90
|
-
queryFilters: true,
|
|
91
|
-
queryAggregations: false,
|
|
92
|
-
querySorting: true,
|
|
93
|
-
queryPagination: true,
|
|
94
|
-
queryWindowFunctions: false,
|
|
95
|
-
querySubqueries: false
|
|
96
|
-
};
|
|
97
|
-
this.config = {
|
|
98
|
-
prettyPrint: true,
|
|
99
|
-
enableBackup: true,
|
|
100
|
-
strictMode: false,
|
|
101
|
-
...config
|
|
102
|
-
};
|
|
103
|
-
this.idCounters = new Map();
|
|
95
|
+
this.dataDir = path.resolve(config.dataDir);
|
|
96
|
+
this.prettyPrint = config.prettyPrint !== false;
|
|
97
|
+
this.enableBackup = config.enableBackup !== false;
|
|
104
98
|
this.cache = new Map();
|
|
105
99
|
// Ensure data directory exists
|
|
106
|
-
if (!fs.existsSync(this.
|
|
107
|
-
fs.mkdirSync(this.
|
|
108
|
-
}
|
|
109
|
-
// Load initial data if provided
|
|
110
|
-
if (config.initialData) {
|
|
111
|
-
this.loadInitialData(config.initialData);
|
|
100
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
101
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
112
102
|
}
|
|
103
|
+
// Load all existing data files
|
|
104
|
+
this.loadAllFromDisk();
|
|
113
105
|
}
|
|
114
106
|
/**
|
|
115
|
-
*
|
|
116
|
-
* This is a no-op for filesystem driver as there's no external connection.
|
|
107
|
+
* Load all JSON files from disk into memory
|
|
117
108
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Check database connection health
|
|
123
|
-
*/
|
|
124
|
-
async checkHealth() {
|
|
125
|
-
try {
|
|
126
|
-
// Check if data directory is accessible
|
|
127
|
-
if (!fs.existsSync(this.config.dataDir)) {
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
// Try to read directory
|
|
131
|
-
fs.readdirSync(this.config.dataDir);
|
|
132
|
-
return true;
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
return false;
|
|
109
|
+
loadAllFromDisk() {
|
|
110
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
111
|
+
return;
|
|
136
112
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
id: record.id || record._id || this.generateId(objectName),
|
|
149
|
-
created_at: record.created_at || new Date().toISOString(),
|
|
150
|
-
updated_at: record.updated_at || new Date().toISOString()
|
|
151
|
-
}));
|
|
152
|
-
this.saveRecords(objectName, recordsWithIds);
|
|
113
|
+
const files = fs.readdirSync(this.dataDir);
|
|
114
|
+
for (const file of files) {
|
|
115
|
+
if (file.endsWith('.json') && !file.endsWith('.backup.json')) {
|
|
116
|
+
const objectName = file.replace('.json', '');
|
|
117
|
+
const records = this.loadRecordsFromDisk(objectName);
|
|
118
|
+
// Load into parent's store
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
const id = record.id || record._id;
|
|
121
|
+
const key = `${objectName}:${id}`;
|
|
122
|
+
this.store.set(key, record);
|
|
123
|
+
}
|
|
153
124
|
}
|
|
154
125
|
}
|
|
155
126
|
}
|
|
156
127
|
/**
|
|
157
|
-
*
|
|
128
|
+
* Load records for an object type from disk
|
|
158
129
|
*/
|
|
159
|
-
|
|
160
|
-
return path.join(this.config.dataDir, `${objectName}.json`);
|
|
161
|
-
}
|
|
162
|
-
/**
|
|
163
|
-
* Load records from file into memory cache.
|
|
164
|
-
*/
|
|
165
|
-
loadRecords(objectName) {
|
|
166
|
-
// Check cache first
|
|
167
|
-
if (this.cache.has(objectName)) {
|
|
168
|
-
return this.cache.get(objectName);
|
|
169
|
-
}
|
|
130
|
+
loadRecordsFromDisk(objectName) {
|
|
170
131
|
const filePath = this.getFilePath(objectName);
|
|
171
132
|
if (!fs.existsSync(filePath)) {
|
|
172
|
-
// File doesn't exist yet, return empty array
|
|
173
|
-
this.cache.set(objectName, []);
|
|
174
133
|
return [];
|
|
175
134
|
}
|
|
176
135
|
try {
|
|
177
136
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
178
|
-
|
|
179
|
-
if (!content || content.trim() === '') {
|
|
180
|
-
this.cache.set(objectName, []);
|
|
137
|
+
if (!content.trim()) {
|
|
181
138
|
return [];
|
|
182
139
|
}
|
|
183
|
-
|
|
184
|
-
try {
|
|
185
|
-
records = JSON.parse(content);
|
|
186
|
-
}
|
|
187
|
-
catch (parseError) {
|
|
188
|
-
throw new types_1.ObjectQLError({
|
|
189
|
-
code: 'INVALID_JSON_FORMAT',
|
|
190
|
-
message: `File ${filePath} contains invalid JSON: ${parseError.message}`,
|
|
191
|
-
details: { objectName, filePath, parseError }
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
if (!Array.isArray(records)) {
|
|
195
|
-
throw new types_1.ObjectQLError({
|
|
196
|
-
code: 'INVALID_DATA_FORMAT',
|
|
197
|
-
message: `File ${filePath} does not contain a valid array`,
|
|
198
|
-
details: { objectName, filePath }
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
this.cache.set(objectName, records);
|
|
202
|
-
return records;
|
|
140
|
+
return JSON.parse(content);
|
|
203
141
|
}
|
|
204
142
|
catch (error) {
|
|
205
|
-
// If it's already an ObjectQLError, rethrow it
|
|
206
|
-
if (error.code && error.code.startsWith('INVALID_')) {
|
|
207
|
-
throw error;
|
|
208
|
-
}
|
|
209
|
-
if (error.code === 'ENOENT') {
|
|
210
|
-
this.cache.set(objectName, []);
|
|
211
|
-
return [];
|
|
212
|
-
}
|
|
213
143
|
throw new types_1.ObjectQLError({
|
|
214
|
-
code: '
|
|
215
|
-
message: `Failed to
|
|
216
|
-
details: { objectName, filePath, error }
|
|
144
|
+
code: 'INVALID_JSON_FORMAT',
|
|
145
|
+
message: `Failed to parse ${filePath}: invalid JSON format`
|
|
217
146
|
});
|
|
218
147
|
}
|
|
219
148
|
}
|
|
220
149
|
/**
|
|
221
|
-
* Save records
|
|
150
|
+
* Save records for an object type to disk
|
|
222
151
|
*/
|
|
223
|
-
|
|
152
|
+
saveRecordsToDisk(objectName, records) {
|
|
224
153
|
const filePath = this.getFilePath(objectName);
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
if (this.config.enableBackup && fs.existsSync(filePath)) {
|
|
230
|
-
fs.copyFileSync(filePath, backupPath);
|
|
231
|
-
}
|
|
232
|
-
// Write to temporary file
|
|
233
|
-
const content = this.config.prettyPrint
|
|
234
|
-
? JSON.stringify(records, null, 2)
|
|
235
|
-
: JSON.stringify(records);
|
|
236
|
-
fs.writeFileSync(tempPath, content, 'utf8');
|
|
237
|
-
// Atomic rename (replaces original file)
|
|
238
|
-
fs.renameSync(tempPath, filePath);
|
|
239
|
-
// Update cache
|
|
240
|
-
this.cache.set(objectName, records);
|
|
241
|
-
}
|
|
242
|
-
catch (error) {
|
|
243
|
-
// Clean up temp file if it exists
|
|
244
|
-
if (fs.existsSync(tempPath)) {
|
|
245
|
-
fs.unlinkSync(tempPath);
|
|
246
|
-
}
|
|
247
|
-
throw new types_1.ObjectQLError({
|
|
248
|
-
code: 'FILE_WRITE_ERROR',
|
|
249
|
-
message: `Failed to write file for object '${objectName}': ${error.message}`,
|
|
250
|
-
details: { objectName, filePath, error }
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Find multiple records matching the query criteria.
|
|
256
|
-
*/
|
|
257
|
-
async find(objectName, query = {}, options) {
|
|
258
|
-
// Normalize query to support both legacy and QueryAST formats
|
|
259
|
-
const normalizedQuery = this.normalizeQuery(query);
|
|
260
|
-
let results = this.loadRecords(objectName);
|
|
261
|
-
// Apply filters
|
|
262
|
-
if (normalizedQuery.filters) {
|
|
263
|
-
results = this.applyFilters(results, normalizedQuery.filters);
|
|
264
|
-
}
|
|
265
|
-
// Apply sorting
|
|
266
|
-
if (normalizedQuery.sort && Array.isArray(normalizedQuery.sort)) {
|
|
267
|
-
results = this.applySort(results, normalizedQuery.sort);
|
|
268
|
-
}
|
|
269
|
-
// Apply pagination
|
|
270
|
-
if (normalizedQuery.skip) {
|
|
271
|
-
results = results.slice(normalizedQuery.skip);
|
|
154
|
+
// Create backup if enabled
|
|
155
|
+
if (this.enableBackup && fs.existsSync(filePath)) {
|
|
156
|
+
const backupPath = `${filePath}.bak`;
|
|
157
|
+
fs.copyFileSync(filePath, backupPath);
|
|
272
158
|
}
|
|
273
|
-
|
|
274
|
-
|
|
159
|
+
// Ensure directory exists
|
|
160
|
+
const dir = path.dirname(filePath);
|
|
161
|
+
if (!fs.existsSync(dir)) {
|
|
162
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
275
163
|
}
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
164
|
+
// Write to temp file first (atomic write)
|
|
165
|
+
const tempPath = `${filePath}.tmp`;
|
|
166
|
+
const content = this.prettyPrint
|
|
167
|
+
? JSON.stringify(records, null, 2)
|
|
168
|
+
: JSON.stringify(records);
|
|
169
|
+
fs.writeFileSync(tempPath, content, 'utf8');
|
|
170
|
+
fs.renameSync(tempPath, filePath);
|
|
171
|
+
// Update cache
|
|
172
|
+
this.cache.set(objectName, records);
|
|
282
173
|
}
|
|
283
174
|
/**
|
|
284
|
-
*
|
|
175
|
+
* Get file path for an object type
|
|
285
176
|
*/
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
// If ID is provided, fetch directly
|
|
289
|
-
if (id) {
|
|
290
|
-
const record = records.find(r => r.id === id || r._id === id);
|
|
291
|
-
return record ? { ...record } : null;
|
|
292
|
-
}
|
|
293
|
-
// If query is provided, use find and return first result
|
|
294
|
-
if (query) {
|
|
295
|
-
const results = await this.find(objectName, { ...query, limit: 1 }, options);
|
|
296
|
-
return results[0] || null;
|
|
297
|
-
}
|
|
298
|
-
return null;
|
|
177
|
+
getFilePath(objectName) {
|
|
178
|
+
return path.join(this.dataDir, `${objectName}.json`);
|
|
299
179
|
}
|
|
300
180
|
/**
|
|
301
|
-
*
|
|
181
|
+
* Override create to persist to disk
|
|
302
182
|
*/
|
|
303
183
|
async create(objectName, data, options) {
|
|
304
|
-
// Validate object name
|
|
305
184
|
if (!objectName || objectName.trim() === '') {
|
|
306
185
|
throw new types_1.ObjectQLError({
|
|
307
186
|
code: 'INVALID_OBJECT_NAME',
|
|
308
|
-
message: 'Object name cannot be empty'
|
|
309
|
-
details: { objectName }
|
|
187
|
+
message: 'Object name cannot be empty'
|
|
310
188
|
});
|
|
311
189
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
// Check if record already exists
|
|
316
|
-
const existing = records.find(r => r.id === id || r._id === id);
|
|
317
|
-
if (existing) {
|
|
318
|
-
throw new types_1.ObjectQLError({
|
|
319
|
-
code: 'DUPLICATE_RECORD',
|
|
320
|
-
message: `Record with id '${id}' already exists in '${objectName}'`,
|
|
321
|
-
details: { objectName, id }
|
|
322
|
-
});
|
|
190
|
+
// Handle _id field as alias for id (MongoDB compatibility)
|
|
191
|
+
if (data._id && !data.id) {
|
|
192
|
+
data = { ...data, id: data._id };
|
|
323
193
|
}
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
id,
|
|
328
|
-
created_at: data.created_at || now,
|
|
329
|
-
updated_at: data.updated_at || now
|
|
330
|
-
};
|
|
331
|
-
records.push(doc);
|
|
332
|
-
this.saveRecords(objectName, records);
|
|
333
|
-
return { ...doc };
|
|
194
|
+
const result = await super.create(objectName, data, options);
|
|
195
|
+
this.syncObjectToDisk(objectName);
|
|
196
|
+
return result;
|
|
334
197
|
}
|
|
335
198
|
/**
|
|
336
|
-
*
|
|
199
|
+
* Override update to persist to disk
|
|
337
200
|
*/
|
|
338
201
|
async update(objectName, id, data, options) {
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (this.config.strictMode) {
|
|
343
|
-
throw new types_1.ObjectQLError({
|
|
344
|
-
code: 'RECORD_NOT_FOUND',
|
|
345
|
-
message: `Record with id '${id}' not found in '${objectName}'`,
|
|
346
|
-
details: { objectName, id }
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
return null;
|
|
202
|
+
const result = await super.update(objectName, id, data, options);
|
|
203
|
+
if (result) {
|
|
204
|
+
this.syncObjectToDisk(objectName);
|
|
350
205
|
}
|
|
351
|
-
|
|
352
|
-
const doc = {
|
|
353
|
-
...existing,
|
|
354
|
-
...data,
|
|
355
|
-
id: existing.id || existing._id, // Preserve ID
|
|
356
|
-
created_at: existing.created_at, // Preserve created_at
|
|
357
|
-
updated_at: new Date().toISOString()
|
|
358
|
-
};
|
|
359
|
-
records[index] = doc;
|
|
360
|
-
this.saveRecords(objectName, records);
|
|
361
|
-
return { ...doc };
|
|
206
|
+
return result;
|
|
362
207
|
}
|
|
363
208
|
/**
|
|
364
|
-
*
|
|
209
|
+
* Override delete to persist to disk
|
|
365
210
|
*/
|
|
366
211
|
async delete(objectName, id, options) {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
if (this.config.strictMode) {
|
|
371
|
-
throw new types_1.ObjectQLError({
|
|
372
|
-
code: 'RECORD_NOT_FOUND',
|
|
373
|
-
message: `Record with id '${id}' not found in '${objectName}'`,
|
|
374
|
-
details: { objectName, id }
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
records.splice(index, 1);
|
|
380
|
-
this.saveRecords(objectName, records);
|
|
381
|
-
return true;
|
|
382
|
-
}
|
|
383
|
-
/**
|
|
384
|
-
* Count records matching filters.
|
|
385
|
-
*/
|
|
386
|
-
async count(objectName, filters, options) {
|
|
387
|
-
const records = this.loadRecords(objectName);
|
|
388
|
-
// Handle query object with 'where' property
|
|
389
|
-
let actualFilters = filters;
|
|
390
|
-
// If filters is a query object with 'where' property, extract and convert it
|
|
391
|
-
if (filters && typeof filters === 'object' && !Array.isArray(filters) && 'where' in filters) {
|
|
392
|
-
actualFilters = this.convertFilterConditionToArray(filters.where);
|
|
393
|
-
}
|
|
394
|
-
// If filters is a query object with 'filters' property, extract it
|
|
395
|
-
else if (filters && !Array.isArray(filters) && 'filters' in filters) {
|
|
396
|
-
actualFilters = filters.filters;
|
|
397
|
-
}
|
|
398
|
-
// If filters is a FilterCondition object (MongoDB-like), convert it
|
|
399
|
-
else if (filters && !Array.isArray(filters) && typeof filters === 'object' &&
|
|
400
|
-
!('where' in filters) && !('filters' in filters)) {
|
|
401
|
-
actualFilters = this.convertFilterConditionToArray(filters);
|
|
212
|
+
const result = await super.delete(objectName, id, options);
|
|
213
|
+
if (result) {
|
|
214
|
+
this.syncObjectToDisk(objectName);
|
|
402
215
|
}
|
|
403
|
-
|
|
404
|
-
if (!actualFilters ||
|
|
405
|
-
(Array.isArray(actualFilters) && actualFilters.length === 0) ||
|
|
406
|
-
(typeof actualFilters === 'object' && !Array.isArray(actualFilters) && Object.keys(actualFilters).length === 0)) {
|
|
407
|
-
return records.length;
|
|
408
|
-
}
|
|
409
|
-
// Count only records matching filters
|
|
410
|
-
return records.filter(record => this.matchesFilters(record, actualFilters)).length;
|
|
216
|
+
return result;
|
|
411
217
|
}
|
|
412
218
|
/**
|
|
413
|
-
*
|
|
219
|
+
* Override find to load from disk if not in cache
|
|
414
220
|
*/
|
|
415
|
-
async
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
if (
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
values.add(value);
|
|
428
|
-
}
|
|
221
|
+
async find(objectName, query = {}, options) {
|
|
222
|
+
// Check if we need to load from disk (e.g., file created externally)
|
|
223
|
+
const filePath = this.getFilePath(objectName);
|
|
224
|
+
const hasRecordsInMemory = Array.from(this.store.keys()).some(key => key.startsWith(`${objectName}:`));
|
|
225
|
+
if (!hasRecordsInMemory && fs.existsSync(filePath)) {
|
|
226
|
+
// Load from disk - this will throw on invalid JSON
|
|
227
|
+
const records = this.loadRecordsFromDisk(objectName);
|
|
228
|
+
// Load into parent's store
|
|
229
|
+
for (const record of records) {
|
|
230
|
+
const id = record.id || record._id;
|
|
231
|
+
const key = `${objectName}:${id}`;
|
|
232
|
+
this.store.set(key, record);
|
|
429
233
|
}
|
|
430
234
|
}
|
|
431
|
-
return
|
|
235
|
+
return super.find(objectName, query, options);
|
|
432
236
|
}
|
|
433
237
|
/**
|
|
434
|
-
*
|
|
238
|
+
* Sync an object type's data to disk
|
|
435
239
|
*/
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
return results;
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Update multiple records matching filters.
|
|
446
|
-
*/
|
|
447
|
-
async updateMany(objectName, filters, data, options) {
|
|
448
|
-
const records = this.loadRecords(objectName);
|
|
449
|
-
// Convert FilterCondition format (MongoDB-like) to array format if needed
|
|
450
|
-
let actualFilters = filters;
|
|
451
|
-
if (filters && !Array.isArray(filters) && typeof filters === 'object') {
|
|
452
|
-
actualFilters = this.convertFilterConditionToArray(filters);
|
|
453
|
-
}
|
|
454
|
-
let count = 0;
|
|
455
|
-
for (let i = 0; i < records.length; i++) {
|
|
456
|
-
if (this.matchesFilters(records[i], actualFilters)) {
|
|
457
|
-
const updated = {
|
|
458
|
-
...records[i],
|
|
459
|
-
...data,
|
|
460
|
-
id: records[i].id || records[i]._id, // Preserve ID
|
|
461
|
-
created_at: records[i].created_at, // Preserve created_at
|
|
462
|
-
updated_at: new Date().toISOString()
|
|
463
|
-
};
|
|
464
|
-
records[i] = updated;
|
|
465
|
-
count++;
|
|
240
|
+
syncObjectToDisk(objectName) {
|
|
241
|
+
const records = [];
|
|
242
|
+
const prefix = `${objectName}:`;
|
|
243
|
+
for (const [key, value] of this.store.entries()) {
|
|
244
|
+
if (key.startsWith(prefix)) {
|
|
245
|
+
records.push(value);
|
|
466
246
|
}
|
|
467
247
|
}
|
|
468
|
-
|
|
469
|
-
this.saveRecords(objectName, records);
|
|
470
|
-
}
|
|
471
|
-
return { modifiedCount: count };
|
|
248
|
+
this.saveRecordsToDisk(objectName, records);
|
|
472
249
|
}
|
|
473
250
|
/**
|
|
474
|
-
*
|
|
251
|
+
* Clear cache for an object
|
|
475
252
|
*/
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
let actualFilters = filters;
|
|
480
|
-
if (filters && !Array.isArray(filters) && typeof filters === 'object') {
|
|
481
|
-
actualFilters = this.convertFilterConditionToArray(filters);
|
|
482
|
-
}
|
|
483
|
-
const initialCount = records.length;
|
|
484
|
-
const remaining = records.filter(record => !this.matchesFilters(record, actualFilters));
|
|
485
|
-
const deletedCount = initialCount - remaining.length;
|
|
486
|
-
if (deletedCount > 0) {
|
|
487
|
-
this.saveRecords(objectName, remaining);
|
|
488
|
-
}
|
|
489
|
-
return { deletedCount };
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Disconnect (flush cache).
|
|
493
|
-
*/
|
|
494
|
-
async disconnect() {
|
|
495
|
-
this.cache.clear();
|
|
496
|
-
}
|
|
497
|
-
/**
|
|
498
|
-
* Clear all data from a specific object.
|
|
499
|
-
* Useful for testing or data reset scenarios.
|
|
500
|
-
*/
|
|
501
|
-
async clear(objectName) {
|
|
502
|
-
const filePath = this.getFilePath(objectName);
|
|
503
|
-
// Remove file if exists
|
|
504
|
-
if (fs.existsSync(filePath)) {
|
|
505
|
-
fs.unlinkSync(filePath);
|
|
253
|
+
invalidateCache(objectName) {
|
|
254
|
+
if (objectName) {
|
|
255
|
+
this.cache.delete(objectName);
|
|
506
256
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (fs.existsSync(backupPath)) {
|
|
510
|
-
fs.unlinkSync(backupPath);
|
|
257
|
+
else {
|
|
258
|
+
this.cache.clear();
|
|
511
259
|
}
|
|
512
|
-
// Clear cache
|
|
513
|
-
this.cache.delete(objectName);
|
|
514
|
-
this.idCounters.delete(objectName);
|
|
515
|
-
}
|
|
516
|
-
/**
|
|
517
|
-
* Clear all data from all objects.
|
|
518
|
-
* Removes all JSON files in the data directory.
|
|
519
|
-
*/
|
|
520
|
-
async clearAll() {
|
|
521
|
-
const files = fs.readdirSync(this.config.dataDir);
|
|
522
|
-
for (const file of files) {
|
|
523
|
-
if (file.endsWith('.json') || file.endsWith('.json.bak') || file.endsWith('.json.tmp')) {
|
|
524
|
-
const filePath = path.join(this.config.dataDir, file);
|
|
525
|
-
fs.unlinkSync(filePath);
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
this.cache.clear();
|
|
529
|
-
this.idCounters.clear();
|
|
530
260
|
}
|
|
531
261
|
/**
|
|
532
|
-
*
|
|
533
|
-
* Forces reload from file on next access.
|
|
534
|
-
*/
|
|
535
|
-
invalidateCache(objectName) {
|
|
536
|
-
this.cache.delete(objectName);
|
|
537
|
-
}
|
|
538
|
-
/**
|
|
539
|
-
* Get the size of the cache (number of objects cached).
|
|
262
|
+
* Get the number of cached objects
|
|
540
263
|
*/
|
|
541
264
|
getCacheSize() {
|
|
542
265
|
return this.cache.size;
|
|
543
266
|
}
|
|
544
267
|
/**
|
|
545
|
-
*
|
|
546
|
-
*
|
|
547
|
-
* This method handles all query operations using the standard QueryAST format
|
|
548
|
-
* from @objectstack/spec. It converts the AST to the legacy query format
|
|
549
|
-
* and delegates to the existing find() method.
|
|
550
|
-
*
|
|
551
|
-
* @param ast - The query AST to execute
|
|
552
|
-
* @param options - Optional execution options
|
|
553
|
-
* @returns Query results with value array and count
|
|
268
|
+
* Clear data for a specific object or all data
|
|
554
269
|
*/
|
|
555
|
-
async
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
sort: (_a = ast.orderBy) === null || _a === void 0 ? void 0 : _a.map((s) => [s.field, s.order]),
|
|
564
|
-
limit: ast.limit,
|
|
565
|
-
skip: ast.offset,
|
|
566
|
-
};
|
|
567
|
-
// Use existing find method
|
|
568
|
-
const results = await this.find(objectName, legacyQuery, options);
|
|
569
|
-
return {
|
|
570
|
-
value: results,
|
|
571
|
-
count: results.length
|
|
572
|
-
};
|
|
573
|
-
}
|
|
574
|
-
/**
|
|
575
|
-
* Execute a command (DriverInterface v4.0 method)
|
|
576
|
-
*
|
|
577
|
-
* This method handles all mutation operations (create, update, delete)
|
|
578
|
-
* using a unified command interface.
|
|
579
|
-
*
|
|
580
|
-
* @param command - The command to execute
|
|
581
|
-
* @param options - Optional execution options
|
|
582
|
-
* @returns Command execution result
|
|
583
|
-
*/
|
|
584
|
-
async executeCommand(command, options) {
|
|
585
|
-
try {
|
|
586
|
-
const cmdOptions = { ...options, ...command.options };
|
|
587
|
-
switch (command.type) {
|
|
588
|
-
case 'create':
|
|
589
|
-
if (!command.data) {
|
|
590
|
-
throw new Error('Create command requires data');
|
|
591
|
-
}
|
|
592
|
-
const created = await this.create(command.object, command.data, cmdOptions);
|
|
593
|
-
return {
|
|
594
|
-
success: true,
|
|
595
|
-
data: created,
|
|
596
|
-
affected: 1
|
|
597
|
-
};
|
|
598
|
-
case 'update':
|
|
599
|
-
if (!command.id || !command.data) {
|
|
600
|
-
throw new Error('Update command requires id and data');
|
|
601
|
-
}
|
|
602
|
-
const updated = await this.update(command.object, command.id, command.data, cmdOptions);
|
|
603
|
-
return {
|
|
604
|
-
success: true,
|
|
605
|
-
data: updated,
|
|
606
|
-
affected: 1
|
|
607
|
-
};
|
|
608
|
-
case 'delete':
|
|
609
|
-
if (!command.id) {
|
|
610
|
-
throw new Error('Delete command requires id');
|
|
611
|
-
}
|
|
612
|
-
await this.delete(command.object, command.id, cmdOptions);
|
|
613
|
-
return {
|
|
614
|
-
success: true,
|
|
615
|
-
affected: 1
|
|
616
|
-
};
|
|
617
|
-
case 'bulkCreate':
|
|
618
|
-
if (!command.records || !Array.isArray(command.records)) {
|
|
619
|
-
throw new Error('BulkCreate command requires records array');
|
|
620
|
-
}
|
|
621
|
-
const bulkCreated = [];
|
|
622
|
-
for (const record of command.records) {
|
|
623
|
-
const created = await this.create(command.object, record, cmdOptions);
|
|
624
|
-
bulkCreated.push(created);
|
|
625
|
-
}
|
|
626
|
-
return {
|
|
627
|
-
success: true,
|
|
628
|
-
data: bulkCreated,
|
|
629
|
-
affected: command.records.length
|
|
630
|
-
};
|
|
631
|
-
case 'bulkUpdate':
|
|
632
|
-
if (!command.updates || !Array.isArray(command.updates)) {
|
|
633
|
-
throw new Error('BulkUpdate command requires updates array');
|
|
634
|
-
}
|
|
635
|
-
const updateResults = [];
|
|
636
|
-
for (const update of command.updates) {
|
|
637
|
-
const result = await this.update(command.object, update.id, update.data, cmdOptions);
|
|
638
|
-
updateResults.push(result);
|
|
639
|
-
}
|
|
640
|
-
return {
|
|
641
|
-
success: true,
|
|
642
|
-
data: updateResults,
|
|
643
|
-
affected: command.updates.length
|
|
644
|
-
};
|
|
645
|
-
case 'bulkDelete':
|
|
646
|
-
if (!command.ids || !Array.isArray(command.ids)) {
|
|
647
|
-
throw new Error('BulkDelete command requires ids array');
|
|
648
|
-
}
|
|
649
|
-
let deleted = 0;
|
|
650
|
-
for (const id of command.ids) {
|
|
651
|
-
const result = await this.delete(command.object, id, cmdOptions);
|
|
652
|
-
if (result)
|
|
653
|
-
deleted++;
|
|
654
|
-
}
|
|
655
|
-
return {
|
|
656
|
-
success: true,
|
|
657
|
-
affected: deleted
|
|
658
|
-
};
|
|
659
|
-
default:
|
|
660
|
-
throw new Error(`Unsupported command type: ${command.type}`);
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
catch (error) {
|
|
664
|
-
return {
|
|
665
|
-
success: false,
|
|
666
|
-
affected: 0,
|
|
667
|
-
error: error.message || 'Unknown error occurred'
|
|
668
|
-
};
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* Execute raw command (for compatibility)
|
|
673
|
-
*
|
|
674
|
-
* @param command - Command string or object
|
|
675
|
-
* @param parameters - Command parameters
|
|
676
|
-
* @param options - Execution options
|
|
677
|
-
*/
|
|
678
|
-
async execute(command, parameters, options) {
|
|
679
|
-
throw new Error('FileSystem driver does not support raw command execution. Use executeCommand() instead.');
|
|
680
|
-
}
|
|
681
|
-
// ========== Helper Methods ==========
|
|
682
|
-
/**
|
|
683
|
-
* Convert FilterCondition (MongoDB-like format) to legacy array format.
|
|
684
|
-
* This allows the fs driver to use its existing filter evaluation logic.
|
|
685
|
-
*
|
|
686
|
-
* @param condition - FilterCondition object or legacy array
|
|
687
|
-
* @returns Legacy filter array format
|
|
688
|
-
*/
|
|
689
|
-
convertFilterConditionToArray(condition) {
|
|
690
|
-
if (!condition)
|
|
691
|
-
return undefined;
|
|
692
|
-
// If already an array, return as-is
|
|
693
|
-
if (Array.isArray(condition)) {
|
|
694
|
-
return condition;
|
|
695
|
-
}
|
|
696
|
-
// If it's an object (FilterCondition), convert to array format
|
|
697
|
-
// This is a simplified conversion - a full implementation would need to handle all operators
|
|
698
|
-
const result = [];
|
|
699
|
-
for (const [key, value] of Object.entries(condition)) {
|
|
700
|
-
if (key === '$and' && Array.isArray(value)) {
|
|
701
|
-
// Handle $and: [cond1, cond2, ...]
|
|
702
|
-
for (let i = 0; i < value.length; i++) {
|
|
703
|
-
const converted = this.convertFilterConditionToArray(value[i]);
|
|
704
|
-
if (converted && converted.length > 0) {
|
|
705
|
-
if (result.length > 0) {
|
|
706
|
-
result.push('and');
|
|
707
|
-
}
|
|
708
|
-
result.push(...converted);
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
else if (key === '$or' && Array.isArray(value)) {
|
|
713
|
-
// Handle $or: [cond1, cond2, ...]
|
|
714
|
-
for (let i = 0; i < value.length; i++) {
|
|
715
|
-
const converted = this.convertFilterConditionToArray(value[i]);
|
|
716
|
-
if (converted && converted.length > 0) {
|
|
717
|
-
if (result.length > 0) {
|
|
718
|
-
result.push('or');
|
|
719
|
-
}
|
|
720
|
-
result.push(...converted);
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
else if (key === '$not' && typeof value === 'object') {
|
|
725
|
-
// Handle $not: { condition }
|
|
726
|
-
// Note: NOT is complex to represent in array format, so we skip it for now
|
|
727
|
-
const converted = this.convertFilterConditionToArray(value);
|
|
728
|
-
if (converted) {
|
|
729
|
-
result.push(...converted);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
732
|
-
else if (typeof value === 'object' && value !== null) {
|
|
733
|
-
// Handle field-level conditions like { field: { $eq: value } }
|
|
734
|
-
const field = key;
|
|
735
|
-
for (const [operator, operandValue] of Object.entries(value)) {
|
|
736
|
-
let op;
|
|
737
|
-
switch (operator) {
|
|
738
|
-
case '$eq':
|
|
739
|
-
op = '=';
|
|
740
|
-
break;
|
|
741
|
-
case '$ne':
|
|
742
|
-
op = '!=';
|
|
743
|
-
break;
|
|
744
|
-
case '$gt':
|
|
745
|
-
op = '>';
|
|
746
|
-
break;
|
|
747
|
-
case '$gte':
|
|
748
|
-
op = '>=';
|
|
749
|
-
break;
|
|
750
|
-
case '$lt':
|
|
751
|
-
op = '<';
|
|
752
|
-
break;
|
|
753
|
-
case '$lte':
|
|
754
|
-
op = '<=';
|
|
755
|
-
break;
|
|
756
|
-
case '$in':
|
|
757
|
-
op = 'in';
|
|
758
|
-
break;
|
|
759
|
-
case '$nin':
|
|
760
|
-
op = 'nin';
|
|
761
|
-
break;
|
|
762
|
-
case '$regex':
|
|
763
|
-
op = 'like';
|
|
764
|
-
break;
|
|
765
|
-
default: op = '=';
|
|
766
|
-
}
|
|
767
|
-
result.push([field, op, operandValue]);
|
|
270
|
+
async clear(objectName) {
|
|
271
|
+
if (objectName) {
|
|
272
|
+
// Clear specific object from memory
|
|
273
|
+
const prefix = `${objectName}:`;
|
|
274
|
+
const keysToDelete = [];
|
|
275
|
+
for (const key of this.store.keys()) {
|
|
276
|
+
if (key.startsWith(prefix)) {
|
|
277
|
+
keysToDelete.push(key);
|
|
768
278
|
}
|
|
769
279
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
result.push([key, '=', value]);
|
|
280
|
+
for (const key of keysToDelete) {
|
|
281
|
+
this.store.delete(key);
|
|
773
282
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
* Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
|
|
779
|
-
* This ensures backward compatibility while supporting the new @objectstack/spec interface.
|
|
780
|
-
*
|
|
781
|
-
* QueryAST format uses:
|
|
782
|
-
* - 'where' with MongoDB-like filters (convert to 'filters' array)
|
|
783
|
-
* - 'orderBy' with array of {field, order} (convert to 'sort' with [field, order])
|
|
784
|
-
* - 'offset' (convert to 'skip')
|
|
785
|
-
* - 'top' (convert to 'limit')
|
|
786
|
-
*/
|
|
787
|
-
normalizeQuery(query) {
|
|
788
|
-
if (!query)
|
|
789
|
-
return {};
|
|
790
|
-
const normalized = { ...query };
|
|
791
|
-
// Convert 'where' (FilterCondition) to 'filters' (array format)
|
|
792
|
-
if (normalized.where !== undefined && normalized.filters === undefined) {
|
|
793
|
-
normalized.filters = this.convertFilterConditionToArray(normalized.where);
|
|
794
|
-
}
|
|
795
|
-
// Convert 'orderBy' to 'sort'
|
|
796
|
-
if (normalized.orderBy !== undefined && normalized.sort === undefined) {
|
|
797
|
-
if (Array.isArray(normalized.orderBy)) {
|
|
798
|
-
normalized.sort = normalized.orderBy.map((item) => {
|
|
799
|
-
if (Array.isArray(item)) {
|
|
800
|
-
// Already in [field, order] format
|
|
801
|
-
return item;
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
// Convert from {field, order} format
|
|
805
|
-
return [item.field, item.order || item.direction || item.dir || 'asc'];
|
|
806
|
-
}
|
|
807
|
-
});
|
|
808
|
-
}
|
|
809
|
-
}
|
|
810
|
-
// Convert 'offset' to 'skip'
|
|
811
|
-
if (normalized.offset !== undefined && normalized.skip === undefined) {
|
|
812
|
-
normalized.skip = normalized.offset;
|
|
813
|
-
}
|
|
814
|
-
// Normalize limit/top
|
|
815
|
-
if (normalized.top !== undefined && normalized.limit === undefined) {
|
|
816
|
-
normalized.limit = normalized.top;
|
|
817
|
-
}
|
|
818
|
-
// Normalize sort format (in case sort is already present)
|
|
819
|
-
if (normalized.sort && Array.isArray(normalized.sort)) {
|
|
820
|
-
// Check if it's already in the array format [field, order]
|
|
821
|
-
const firstSort = normalized.sort[0];
|
|
822
|
-
if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
|
|
823
|
-
// Convert from QueryAST format {field, order} to internal format [field, order]
|
|
824
|
-
normalized.sort = normalized.sort.map((item) => [
|
|
825
|
-
item.field,
|
|
826
|
-
item.order || item.direction || item.dir || 'asc'
|
|
827
|
-
]);
|
|
283
|
+
// Delete from disk
|
|
284
|
+
const filePath = this.getFilePath(objectName);
|
|
285
|
+
if (fs.existsSync(filePath)) {
|
|
286
|
+
fs.unlinkSync(filePath);
|
|
828
287
|
}
|
|
288
|
+
// Clear cache
|
|
289
|
+
this.cache.delete(objectName);
|
|
829
290
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
*
|
|
835
|
-
* Supports ObjectQL filter format with logical operators (AND/OR):
|
|
836
|
-
* [
|
|
837
|
-
* ['field', 'operator', value],
|
|
838
|
-
* 'or',
|
|
839
|
-
* ['field2', 'operator', value2]
|
|
840
|
-
* ]
|
|
841
|
-
*/
|
|
842
|
-
applyFilters(records, filters) {
|
|
843
|
-
if (!filters || filters.length === 0) {
|
|
844
|
-
return records;
|
|
291
|
+
else {
|
|
292
|
+
// Clear all data
|
|
293
|
+
await super.clear();
|
|
294
|
+
this.cache.clear();
|
|
845
295
|
}
|
|
846
|
-
return records.filter(record => this.matchesFilters(record, filters));
|
|
847
296
|
}
|
|
848
297
|
/**
|
|
849
|
-
*
|
|
298
|
+
* Clear all data from the store and disk
|
|
850
299
|
*/
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
const [field, operator, value] = item;
|
|
864
|
-
// Handle nested filter groups
|
|
865
|
-
if (typeof field !== 'string') {
|
|
866
|
-
conditions.push(this.matchesFilters(record, item));
|
|
867
|
-
}
|
|
868
|
-
else {
|
|
869
|
-
const matches = this.evaluateCondition(record[field], operator, value);
|
|
870
|
-
conditions.push(matches);
|
|
300
|
+
async clearAll() {
|
|
301
|
+
// Clear in-memory store
|
|
302
|
+
await this.clear();
|
|
303
|
+
// Clear cache
|
|
304
|
+
this.cache.clear();
|
|
305
|
+
// Delete all JSON files from disk
|
|
306
|
+
if (fs.existsSync(this.dataDir)) {
|
|
307
|
+
const files = fs.readdirSync(this.dataDir);
|
|
308
|
+
for (const file of files) {
|
|
309
|
+
if (file.endsWith('.json')) {
|
|
310
|
+
const filePath = path.join(this.dataDir, file);
|
|
311
|
+
fs.unlinkSync(filePath);
|
|
871
312
|
}
|
|
872
313
|
}
|
|
873
314
|
}
|
|
874
|
-
// Combine conditions with operators
|
|
875
|
-
if (conditions.length === 0) {
|
|
876
|
-
return true;
|
|
877
|
-
}
|
|
878
|
-
let result = conditions[0];
|
|
879
|
-
for (let i = 0; i < operators.length && i + 1 < conditions.length; i++) {
|
|
880
|
-
const op = operators[i];
|
|
881
|
-
const nextCondition = conditions[i + 1];
|
|
882
|
-
if (op === 'or') {
|
|
883
|
-
result = result || nextCondition;
|
|
884
|
-
}
|
|
885
|
-
else { // 'and' or default
|
|
886
|
-
result = result && nextCondition;
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
return result;
|
|
890
|
-
}
|
|
891
|
-
/**
|
|
892
|
-
* Evaluate a single filter condition.
|
|
893
|
-
*/
|
|
894
|
-
evaluateCondition(fieldValue, operator, compareValue) {
|
|
895
|
-
switch (operator) {
|
|
896
|
-
case '=':
|
|
897
|
-
case '==':
|
|
898
|
-
return fieldValue === compareValue;
|
|
899
|
-
case '!=':
|
|
900
|
-
case '<>':
|
|
901
|
-
return fieldValue !== compareValue;
|
|
902
|
-
case '>':
|
|
903
|
-
return fieldValue > compareValue;
|
|
904
|
-
case '>=':
|
|
905
|
-
return fieldValue >= compareValue;
|
|
906
|
-
case '<':
|
|
907
|
-
return fieldValue < compareValue;
|
|
908
|
-
case '<=':
|
|
909
|
-
return fieldValue <= compareValue;
|
|
910
|
-
case 'in':
|
|
911
|
-
return Array.isArray(compareValue) && compareValue.includes(fieldValue);
|
|
912
|
-
case 'nin':
|
|
913
|
-
case 'not in':
|
|
914
|
-
return Array.isArray(compareValue) && !compareValue.includes(fieldValue);
|
|
915
|
-
case 'contains':
|
|
916
|
-
case 'like':
|
|
917
|
-
return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase());
|
|
918
|
-
case 'startswith':
|
|
919
|
-
case 'starts_with':
|
|
920
|
-
return String(fieldValue).toLowerCase().startsWith(String(compareValue).toLowerCase());
|
|
921
|
-
case 'endswith':
|
|
922
|
-
case 'ends_with':
|
|
923
|
-
return String(fieldValue).toLowerCase().endsWith(String(compareValue).toLowerCase());
|
|
924
|
-
case 'between':
|
|
925
|
-
return Array.isArray(compareValue) &&
|
|
926
|
-
fieldValue >= compareValue[0] &&
|
|
927
|
-
fieldValue <= compareValue[1];
|
|
928
|
-
default:
|
|
929
|
-
throw new types_1.ObjectQLError({
|
|
930
|
-
code: 'UNSUPPORTED_OPERATOR',
|
|
931
|
-
message: `[FileSystemDriver] Unsupported operator: ${operator}`,
|
|
932
|
-
});
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
/**
|
|
936
|
-
* Apply sorting to an array of records.
|
|
937
|
-
*/
|
|
938
|
-
applySort(records, sort) {
|
|
939
|
-
const sorted = [...records];
|
|
940
|
-
// Apply sorts in reverse order for correct precedence
|
|
941
|
-
for (let i = sort.length - 1; i >= 0; i--) {
|
|
942
|
-
const sortItem = sort[i];
|
|
943
|
-
let field;
|
|
944
|
-
let direction;
|
|
945
|
-
if (Array.isArray(sortItem)) {
|
|
946
|
-
[field, direction] = sortItem;
|
|
947
|
-
}
|
|
948
|
-
else if (typeof sortItem === 'object') {
|
|
949
|
-
field = sortItem.field;
|
|
950
|
-
direction = sortItem.order || sortItem.direction || sortItem.dir || 'asc';
|
|
951
|
-
}
|
|
952
|
-
else {
|
|
953
|
-
continue;
|
|
954
|
-
}
|
|
955
|
-
sorted.sort((a, b) => {
|
|
956
|
-
const aVal = a[field];
|
|
957
|
-
const bVal = b[field];
|
|
958
|
-
// Handle null/undefined
|
|
959
|
-
if (aVal == null && bVal == null)
|
|
960
|
-
return 0;
|
|
961
|
-
if (aVal == null)
|
|
962
|
-
return 1;
|
|
963
|
-
if (bVal == null)
|
|
964
|
-
return -1;
|
|
965
|
-
// Compare values
|
|
966
|
-
if (aVal < bVal)
|
|
967
|
-
return direction === 'asc' ? -1 : 1;
|
|
968
|
-
if (aVal > bVal)
|
|
969
|
-
return direction === 'asc' ? 1 : -1;
|
|
970
|
-
return 0;
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
return sorted;
|
|
974
|
-
}
|
|
975
|
-
/**
|
|
976
|
-
* Project specific fields from a document.
|
|
977
|
-
*/
|
|
978
|
-
projectFields(doc, fields) {
|
|
979
|
-
const result = {};
|
|
980
|
-
for (const field of fields) {
|
|
981
|
-
if (doc[field] !== undefined) {
|
|
982
|
-
result[field] = doc[field];
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
return result;
|
|
986
|
-
}
|
|
987
|
-
/**
|
|
988
|
-
* Generate a unique ID for a record.
|
|
989
|
-
* Uses timestamp + counter for uniqueness.
|
|
990
|
-
* Note: For production use with high-frequency writes, consider using crypto.randomUUID().
|
|
991
|
-
*/
|
|
992
|
-
generateId(objectName) {
|
|
993
|
-
const counter = (this.idCounters.get(objectName) || 0) + 1;
|
|
994
|
-
this.idCounters.set(objectName, counter);
|
|
995
|
-
// Use timestamp + counter for better uniqueness
|
|
996
|
-
const timestamp = Date.now();
|
|
997
|
-
return `${objectName}-${timestamp}-${counter}`;
|
|
998
315
|
}
|
|
999
316
|
}
|
|
1000
317
|
exports.FileSystemDriver = FileSystemDriver;
|