@objectql/driver-fs 4.0.1 → 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/dist/index.js CHANGED
@@ -69,839 +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
- // Driver metadata (ObjectStack-compatible)
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.supports = {
85
- transactions: false,
86
- joins: false,
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.config.dataDir)) {
107
- fs.mkdirSync(this.config.dataDir, { recursive: true });
100
+ if (!fs.existsSync(this.dataDir)) {
101
+ fs.mkdirSync(this.dataDir, { recursive: true });
108
102
  }
109
- // Load initial data if provided
110
- if (config.initialData) {
111
- this.loadInitialData(config.initialData);
112
- }
113
- }
114
- /**
115
- * Connect to the database (for DriverInterface compatibility)
116
- * This is a no-op for filesystem driver as there's no external connection.
117
- */
118
- async connect() {
119
- // No-op: FileSystem driver doesn't need connection
103
+ // Load all existing data files
104
+ this.loadAllFromDisk();
120
105
  }
121
106
  /**
122
- * Check database connection health
107
+ * Load all JSON files from disk into memory
123
108
  */
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
- * Load initial data into the store.
140
- */
141
- loadInitialData(data) {
142
- for (const [objectName, records] of Object.entries(data)) {
143
- // Only load if file doesn't exist yet
144
- const filePath = this.getFilePath(objectName);
145
- if (!fs.existsSync(filePath)) {
146
- const recordsWithIds = records.map(record => ({
147
- ...record,
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
- * Get the file path for an object type.
128
+ * Load records for an object type from disk
158
129
  */
159
- getFilePath(objectName) {
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
- // Handle empty file
179
- if (!content || content.trim() === '') {
180
- this.cache.set(objectName, []);
137
+ if (!content.trim()) {
181
138
  return [];
182
139
  }
183
- let records;
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: 'FILE_READ_ERROR',
215
- message: `Failed to read file for object '${objectName}': ${error.message}`,
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 to file with atomic write strategy.
150
+ * Save records for an object type to disk
222
151
  */
223
- saveRecords(objectName, records) {
152
+ saveRecordsToDisk(objectName, records) {
224
153
  const filePath = this.getFilePath(objectName);
225
- const tempPath = `${filePath}.tmp`;
226
- const backupPath = `${filePath}.bak`;
227
- try {
228
- // Create backup if file exists and backup is enabled
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);
272
- }
273
- if (normalizedQuery.limit) {
274
- results = results.slice(0, normalizedQuery.limit);
154
+ // Create backup if enabled
155
+ if (this.enableBackup && fs.existsSync(filePath)) {
156
+ const backupPath = `${filePath}.bak`;
157
+ fs.copyFileSync(filePath, backupPath);
275
158
  }
276
- // Apply field projection
277
- if (normalizedQuery.fields && Array.isArray(normalizedQuery.fields)) {
278
- results = results.map(doc => this.projectFields(doc, normalizedQuery.fields));
159
+ // Ensure directory exists
160
+ const dir = path.dirname(filePath);
161
+ if (!fs.existsSync(dir)) {
162
+ fs.mkdirSync(dir, { recursive: true });
279
163
  }
280
- // Return deep copies to prevent external modifications
281
- return results.map(r => ({ ...r }));
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
- * Find a single record by ID or query.
175
+ * Get file path for an object type
285
176
  */
286
- async findOne(objectName, id, query, options) {
287
- const records = this.loadRecords(objectName);
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
- * Create a new record.
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
- const records = this.loadRecords(objectName);
313
- // Generate ID if not provided
314
- const id = data.id || data._id || this.generateId(objectName);
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 now = new Date().toISOString();
325
- const doc = {
326
- ...data,
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
- * Update an existing record.
199
+ * Override update to persist to disk
337
200
  */
338
201
  async update(objectName, id, data, options) {
339
- const records = this.loadRecords(objectName);
340
- const index = records.findIndex(r => r.id === id || r._id === id);
341
- if (index === -1) {
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
- const existing = records[index];
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
- * Delete a record.
209
+ * Override delete to persist to disk
365
210
  */
366
211
  async delete(objectName, id, options) {
367
- const records = this.loadRecords(objectName);
368
- const index = records.findIndex(r => r.id === id || r._id === id);
369
- if (index === -1) {
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;
212
+ const result = await super.delete(objectName, id, options);
213
+ if (result) {
214
+ this.syncObjectToDisk(objectName);
378
215
  }
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
- // Extract actual filters from query object if needed
389
- let actualFilters = filters;
390
- if (filters && !Array.isArray(filters) && filters.filters) {
391
- actualFilters = filters.filters;
392
- }
393
- // If no filters or empty object/array, return total count
394
- if (!actualFilters ||
395
- (Array.isArray(actualFilters) && actualFilters.length === 0) ||
396
- (typeof actualFilters === 'object' && !Array.isArray(actualFilters) && Object.keys(actualFilters).length === 0)) {
397
- return records.length;
398
- }
399
- // Count only records matching filters
400
- return records.filter(record => this.matchesFilters(record, actualFilters)).length;
216
+ return result;
401
217
  }
402
218
  /**
403
- * Get distinct values for a field.
219
+ * Override find to load from disk if not in cache
404
220
  */
405
- async distinct(objectName, field, filters, options) {
406
- const records = this.loadRecords(objectName);
407
- const values = new Set();
408
- for (const record of records) {
409
- if (!filters || this.matchesFilters(record, filters)) {
410
- const value = record[field];
411
- if (value !== undefined && value !== null) {
412
- values.add(value);
413
- }
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);
414
233
  }
415
234
  }
416
- return Array.from(values);
417
- }
418
- /**
419
- * Create multiple records at once.
420
- */
421
- async createMany(objectName, data, options) {
422
- const results = [];
423
- for (const item of data) {
424
- const result = await this.create(objectName, item, options);
425
- results.push(result);
426
- }
427
- return results;
235
+ return super.find(objectName, query, options);
428
236
  }
429
237
  /**
430
- * Update multiple records matching filters.
238
+ * Sync an object type's data to disk
431
239
  */
432
- async updateMany(objectName, filters, data, options) {
433
- const records = this.loadRecords(objectName);
434
- let count = 0;
435
- for (let i = 0; i < records.length; i++) {
436
- if (this.matchesFilters(records[i], filters)) {
437
- const updated = {
438
- ...records[i],
439
- ...data,
440
- id: records[i].id || records[i]._id, // Preserve ID
441
- created_at: records[i].created_at, // Preserve created_at
442
- updated_at: new Date().toISOString()
443
- };
444
- records[i] = updated;
445
- 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);
446
246
  }
447
247
  }
448
- if (count > 0) {
449
- this.saveRecords(objectName, records);
450
- }
451
- return { modifiedCount: count };
452
- }
453
- /**
454
- * Delete multiple records matching filters.
455
- */
456
- async deleteMany(objectName, filters, options) {
457
- const records = this.loadRecords(objectName);
458
- const initialCount = records.length;
459
- const remaining = records.filter(record => !this.matchesFilters(record, filters));
460
- const deletedCount = initialCount - remaining.length;
461
- if (deletedCount > 0) {
462
- this.saveRecords(objectName, remaining);
463
- }
464
- return { deletedCount };
465
- }
466
- /**
467
- * Disconnect (flush cache).
468
- */
469
- async disconnect() {
470
- this.cache.clear();
248
+ this.saveRecordsToDisk(objectName, records);
471
249
  }
472
250
  /**
473
- * Clear all data from a specific object.
474
- * Useful for testing or data reset scenarios.
251
+ * Clear cache for an object
475
252
  */
476
- async clear(objectName) {
477
- const filePath = this.getFilePath(objectName);
478
- // Remove file if exists
479
- if (fs.existsSync(filePath)) {
480
- fs.unlinkSync(filePath);
481
- }
482
- // Remove backup if exists
483
- const backupPath = `${filePath}.bak`;
484
- if (fs.existsSync(backupPath)) {
485
- fs.unlinkSync(backupPath);
253
+ invalidateCache(objectName) {
254
+ if (objectName) {
255
+ this.cache.delete(objectName);
486
256
  }
487
- // Clear cache
488
- this.cache.delete(objectName);
489
- this.idCounters.delete(objectName);
490
- }
491
- /**
492
- * Clear all data from all objects.
493
- * Removes all JSON files in the data directory.
494
- */
495
- async clearAll() {
496
- const files = fs.readdirSync(this.config.dataDir);
497
- for (const file of files) {
498
- if (file.endsWith('.json') || file.endsWith('.json.bak') || file.endsWith('.json.tmp')) {
499
- const filePath = path.join(this.config.dataDir, file);
500
- fs.unlinkSync(filePath);
501
- }
257
+ else {
258
+ this.cache.clear();
502
259
  }
503
- this.cache.clear();
504
- this.idCounters.clear();
505
- }
506
- /**
507
- * Invalidate cache for a specific object.
508
- * Forces reload from file on next access.
509
- */
510
- invalidateCache(objectName) {
511
- this.cache.delete(objectName);
512
260
  }
513
261
  /**
514
- * Get the size of the cache (number of objects cached).
262
+ * Get the number of cached objects
515
263
  */
516
264
  getCacheSize() {
517
265
  return this.cache.size;
518
266
  }
519
267
  /**
520
- * Execute a query using QueryAST (DriverInterface v4.0 method)
521
- *
522
- * This method handles all query operations using the standard QueryAST format
523
- * from @objectstack/spec. It converts the AST to the legacy query format
524
- * and delegates to the existing find() method.
525
- *
526
- * @param ast - The query AST to execute
527
- * @param options - Optional execution options
528
- * @returns Query results with value array and count
529
- */
530
- async executeQuery(ast, options) {
531
- var _a;
532
- const objectName = ast.object || '';
533
- // Convert QueryAST to legacy query format
534
- const legacyQuery = {
535
- fields: ast.fields,
536
- filters: this.convertFilterNodeToLegacy(ast.filters),
537
- sort: (_a = ast.sort) === null || _a === void 0 ? void 0 : _a.map((s) => [s.field, s.order]),
538
- limit: ast.top,
539
- skip: ast.skip,
540
- };
541
- // Use existing find method
542
- const results = await this.find(objectName, legacyQuery, options);
543
- return {
544
- value: results,
545
- count: results.length
546
- };
547
- }
548
- /**
549
- * Execute a command (DriverInterface v4.0 method)
550
- *
551
- * This method handles all mutation operations (create, update, delete)
552
- * using a unified command interface.
553
- *
554
- * @param command - The command to execute
555
- * @param options - Optional execution options
556
- * @returns Command execution result
557
- */
558
- async executeCommand(command, options) {
559
- try {
560
- const cmdOptions = { ...options, ...command.options };
561
- switch (command.type) {
562
- case 'create':
563
- if (!command.data) {
564
- throw new Error('Create command requires data');
565
- }
566
- const created = await this.create(command.object, command.data, cmdOptions);
567
- return {
568
- success: true,
569
- data: created,
570
- affected: 1
571
- };
572
- case 'update':
573
- if (!command.id || !command.data) {
574
- throw new Error('Update command requires id and data');
575
- }
576
- const updated = await this.update(command.object, command.id, command.data, cmdOptions);
577
- return {
578
- success: true,
579
- data: updated,
580
- affected: 1
581
- };
582
- case 'delete':
583
- if (!command.id) {
584
- throw new Error('Delete command requires id');
585
- }
586
- await this.delete(command.object, command.id, cmdOptions);
587
- return {
588
- success: true,
589
- affected: 1
590
- };
591
- case 'bulkCreate':
592
- if (!command.records || !Array.isArray(command.records)) {
593
- throw new Error('BulkCreate command requires records array');
594
- }
595
- const bulkCreated = [];
596
- for (const record of command.records) {
597
- const created = await this.create(command.object, record, cmdOptions);
598
- bulkCreated.push(created);
599
- }
600
- return {
601
- success: true,
602
- data: bulkCreated,
603
- affected: command.records.length
604
- };
605
- case 'bulkUpdate':
606
- if (!command.updates || !Array.isArray(command.updates)) {
607
- throw new Error('BulkUpdate command requires updates array');
608
- }
609
- const updateResults = [];
610
- for (const update of command.updates) {
611
- const result = await this.update(command.object, update.id, update.data, cmdOptions);
612
- updateResults.push(result);
613
- }
614
- return {
615
- success: true,
616
- data: updateResults,
617
- affected: command.updates.length
618
- };
619
- case 'bulkDelete':
620
- if (!command.ids || !Array.isArray(command.ids)) {
621
- throw new Error('BulkDelete command requires ids array');
622
- }
623
- let deleted = 0;
624
- for (const id of command.ids) {
625
- const result = await this.delete(command.object, id, cmdOptions);
626
- if (result)
627
- deleted++;
628
- }
629
- return {
630
- success: true,
631
- affected: deleted
632
- };
633
- default:
634
- throw new Error(`Unsupported command type: ${command.type}`);
635
- }
636
- }
637
- catch (error) {
638
- return {
639
- success: false,
640
- affected: 0,
641
- error: error.message || 'Unknown error occurred'
642
- };
643
- }
644
- }
645
- /**
646
- * Execute raw command (for compatibility)
647
- *
648
- * @param command - Command string or object
649
- * @param parameters - Command parameters
650
- * @param options - Execution options
651
- */
652
- async execute(command, parameters, options) {
653
- throw new Error('FileSystem driver does not support raw command execution. Use executeCommand() instead.');
654
- }
655
- // ========== Helper Methods ==========
656
- /**
657
- * Convert FilterNode from QueryAST to legacy filter format.
658
- *
659
- * @param node - The FilterNode to convert
660
- * @returns Legacy filter array format
661
- */
662
- convertFilterNodeToLegacy(node) {
663
- if (!node)
664
- return undefined;
665
- switch (node.type) {
666
- case 'comparison':
667
- // Convert comparison node to [field, operator, value] format
668
- const operator = node.operator || '=';
669
- return [[node.field, operator, node.value]];
670
- case 'and':
671
- // Convert AND node to array with 'and' separator
672
- if (!node.children || node.children.length === 0)
673
- return undefined;
674
- const andResults = [];
675
- for (const child of node.children) {
676
- const converted = this.convertFilterNodeToLegacy(child);
677
- if (converted) {
678
- if (andResults.length > 0) {
679
- andResults.push('and');
680
- }
681
- andResults.push(...(Array.isArray(converted) ? converted : [converted]));
682
- }
683
- }
684
- return andResults.length > 0 ? andResults : undefined;
685
- case 'or':
686
- // Convert OR node to array with 'or' separator
687
- if (!node.children || node.children.length === 0)
688
- return undefined;
689
- const orResults = [];
690
- for (const child of node.children) {
691
- const converted = this.convertFilterNodeToLegacy(child);
692
- if (converted) {
693
- if (orResults.length > 0) {
694
- orResults.push('or');
695
- }
696
- orResults.push(...(Array.isArray(converted) ? converted : [converted]));
697
- }
698
- }
699
- return orResults.length > 0 ? orResults : undefined;
700
- case 'not':
701
- // NOT is complex - we'll just process the first child for now
702
- if (node.children && node.children.length > 0) {
703
- return this.convertFilterNodeToLegacy(node.children[0]);
704
- }
705
- return undefined;
706
- default:
707
- return undefined;
708
- }
709
- }
710
- /**
711
- * Normalizes query format to support both legacy UnifiedQuery and QueryAST formats.
712
- * This ensures backward compatibility while supporting the new @objectstack/spec interface.
713
- *
714
- * QueryAST format uses 'top' for limit, while UnifiedQuery uses 'limit'.
715
- * QueryAST sort is array of {field, order}, while UnifiedQuery is array of [field, order].
716
- */
717
- normalizeQuery(query) {
718
- if (!query)
719
- return {};
720
- const normalized = { ...query };
721
- // Normalize limit/top
722
- if (normalized.top !== undefined && normalized.limit === undefined) {
723
- normalized.limit = normalized.top;
724
- }
725
- // Normalize sort format
726
- if (normalized.sort && Array.isArray(normalized.sort)) {
727
- // Check if it's already in the array format [field, order]
728
- const firstSort = normalized.sort[0];
729
- if (firstSort && typeof firstSort === 'object' && !Array.isArray(firstSort)) {
730
- // Convert from QueryAST format {field, order} to internal format [field, order]
731
- normalized.sort = normalized.sort.map((item) => [
732
- item.field,
733
- item.order || item.direction || item.dir || 'asc'
734
- ]);
735
- }
736
- }
737
- return normalized;
738
- }
739
- /**
740
- * Apply filters to an array of records.
741
- *
742
- * Supports ObjectQL filter format with logical operators (AND/OR):
743
- * [
744
- * ['field', 'operator', value],
745
- * 'or',
746
- * ['field2', 'operator', value2]
747
- * ]
748
- */
749
- applyFilters(records, filters) {
750
- if (!filters || filters.length === 0) {
751
- return records;
752
- }
753
- return records.filter(record => this.matchesFilters(record, filters));
754
- }
755
- /**
756
- * Check if a single record matches the filter conditions.
268
+ * Clear data for a specific object or all data
757
269
  */
758
- matchesFilters(record, filters) {
759
- if (!filters || filters.length === 0) {
760
- return true;
761
- }
762
- let conditions = [];
763
- let operators = [];
764
- for (const item of filters) {
765
- if (typeof item === 'string') {
766
- // Logical operator (and/or)
767
- operators.push(item.toLowerCase());
768
- }
769
- else if (Array.isArray(item)) {
770
- const [field, operator, value] = item;
771
- // Handle nested filter groups
772
- if (typeof field !== 'string') {
773
- conditions.push(this.matchesFilters(record, item));
774
- }
775
- else {
776
- const matches = this.evaluateCondition(record[field], operator, value);
777
- conditions.push(matches);
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);
778
278
  }
779
279
  }
780
- }
781
- // Combine conditions with operators
782
- if (conditions.length === 0) {
783
- return true;
784
- }
785
- let result = conditions[0];
786
- for (let i = 0; i < operators.length && i + 1 < conditions.length; i++) {
787
- const op = operators[i];
788
- const nextCondition = conditions[i + 1];
789
- if (op === 'or') {
790
- result = result || nextCondition;
280
+ for (const key of keysToDelete) {
281
+ this.store.delete(key);
791
282
  }
792
- else { // 'and' or default
793
- result = result && nextCondition;
283
+ // Delete from disk
284
+ const filePath = this.getFilePath(objectName);
285
+ if (fs.existsSync(filePath)) {
286
+ fs.unlinkSync(filePath);
794
287
  }
288
+ // Clear cache
289
+ this.cache.delete(objectName);
795
290
  }
796
- return result;
797
- }
798
- /**
799
- * Evaluate a single filter condition.
800
- */
801
- evaluateCondition(fieldValue, operator, compareValue) {
802
- switch (operator) {
803
- case '=':
804
- case '==':
805
- return fieldValue === compareValue;
806
- case '!=':
807
- case '<>':
808
- return fieldValue !== compareValue;
809
- case '>':
810
- return fieldValue > compareValue;
811
- case '>=':
812
- return fieldValue >= compareValue;
813
- case '<':
814
- return fieldValue < compareValue;
815
- case '<=':
816
- return fieldValue <= compareValue;
817
- case 'in':
818
- return Array.isArray(compareValue) && compareValue.includes(fieldValue);
819
- case 'nin':
820
- case 'not in':
821
- return Array.isArray(compareValue) && !compareValue.includes(fieldValue);
822
- case 'contains':
823
- case 'like':
824
- return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase());
825
- case 'startswith':
826
- case 'starts_with':
827
- return String(fieldValue).toLowerCase().startsWith(String(compareValue).toLowerCase());
828
- case 'endswith':
829
- case 'ends_with':
830
- return String(fieldValue).toLowerCase().endsWith(String(compareValue).toLowerCase());
831
- case 'between':
832
- return Array.isArray(compareValue) &&
833
- fieldValue >= compareValue[0] &&
834
- fieldValue <= compareValue[1];
835
- default:
836
- throw new types_1.ObjectQLError({
837
- code: 'UNSUPPORTED_OPERATOR',
838
- message: `[FileSystemDriver] Unsupported operator: ${operator}`,
839
- });
291
+ else {
292
+ // Clear all data
293
+ await super.clear();
294
+ this.cache.clear();
840
295
  }
841
296
  }
842
297
  /**
843
- * Apply sorting to an array of records.
298
+ * Clear all data from the store and disk
844
299
  */
845
- applySort(records, sort) {
846
- const sorted = [...records];
847
- // Apply sorts in reverse order for correct precedence
848
- for (let i = sort.length - 1; i >= 0; i--) {
849
- const sortItem = sort[i];
850
- let field;
851
- let direction;
852
- if (Array.isArray(sortItem)) {
853
- [field, direction] = sortItem;
854
- }
855
- else if (typeof sortItem === 'object') {
856
- field = sortItem.field;
857
- direction = sortItem.order || sortItem.direction || sortItem.dir || 'asc';
858
- }
859
- else {
860
- continue;
861
- }
862
- sorted.sort((a, b) => {
863
- const aVal = a[field];
864
- const bVal = b[field];
865
- // Handle null/undefined
866
- if (aVal == null && bVal == null)
867
- return 0;
868
- if (aVal == null)
869
- return 1;
870
- if (bVal == null)
871
- return -1;
872
- // Compare values
873
- if (aVal < bVal)
874
- return direction === 'asc' ? -1 : 1;
875
- if (aVal > bVal)
876
- return direction === 'asc' ? 1 : -1;
877
- return 0;
878
- });
879
- }
880
- return sorted;
881
- }
882
- /**
883
- * Project specific fields from a document.
884
- */
885
- projectFields(doc, fields) {
886
- const result = {};
887
- for (const field of fields) {
888
- if (doc[field] !== undefined) {
889
- result[field] = doc[field];
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);
312
+ }
890
313
  }
891
314
  }
892
- return result;
893
- }
894
- /**
895
- * Generate a unique ID for a record.
896
- * Uses timestamp + counter for uniqueness.
897
- * Note: For production use with high-frequency writes, consider using crypto.randomUUID().
898
- */
899
- generateId(objectName) {
900
- const counter = (this.idCounters.get(objectName) || 0) + 1;
901
- this.idCounters.set(objectName, counter);
902
- // Use timestamp + counter for better uniqueness
903
- const timestamp = Date.now();
904
- return `${objectName}-${timestamp}-${counter}`;
905
315
  }
906
316
  }
907
317
  exports.FileSystemDriver = FileSystemDriver;