@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/CHANGELOG.md +31 -0
- package/dist/index.d.ts +32 -167
- package/dist/index.js +145 -735
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
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
|
-
//
|
|
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.
|
|
100
|
+
if (!fs.existsSync(this.dataDir)) {
|
|
101
|
+
fs.mkdirSync(this.dataDir, { recursive: true });
|
|
108
102
|
}
|
|
109
|
-
// Load
|
|
110
|
-
|
|
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
|
-
*
|
|
107
|
+
* Load all JSON files from disk into memory
|
|
123
108
|
*/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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);
|
|
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
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
159
|
+
// Ensure directory exists
|
|
160
|
+
const dir = path.dirname(filePath);
|
|
161
|
+
if (!fs.existsSync(dir)) {
|
|
162
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
279
163
|
}
|
|
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;
|
|
212
|
+
const result = await super.delete(objectName, id, options);
|
|
213
|
+
if (result) {
|
|
214
|
+
this.syncObjectToDisk(objectName);
|
|
378
215
|
}
|
|
379
|
-
|
|
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
|
-
*
|
|
219
|
+
* Override find to load from disk if not in cache
|
|
404
220
|
*/
|
|
405
|
-
async
|
|
406
|
-
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
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
|
|
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
|
-
*
|
|
238
|
+
* Sync an object type's data to disk
|
|
431
239
|
*/
|
|
432
|
-
|
|
433
|
-
const records =
|
|
434
|
-
|
|
435
|
-
for (
|
|
436
|
-
if (
|
|
437
|
-
|
|
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
|
-
|
|
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
|
|
474
|
-
* Useful for testing or data reset scenarios.
|
|
251
|
+
* Clear cache for an object
|
|
475
252
|
*/
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
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
|
-
|
|
488
|
-
|
|
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
|
|
262
|
+
* Get the number of cached objects
|
|
515
263
|
*/
|
|
516
264
|
getCacheSize() {
|
|
517
265
|
return this.cache.size;
|
|
518
266
|
}
|
|
519
267
|
/**
|
|
520
|
-
*
|
|
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
|
-
|
|
759
|
-
if (
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
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
|
-
|
|
793
|
-
|
|
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
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
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
|
-
*
|
|
298
|
+
* Clear all data from the store and disk
|
|
844
299
|
*/
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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;
|