@objectql/driver-excel 0.2.0

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 ADDED
@@ -0,0 +1,856 @@
1
+ "use strict";
2
+ /**
3
+ * Excel Driver for ObjectQL (Production-Ready)
4
+ *
5
+ * A driver for ObjectQL that reads and writes data from Excel (.xlsx) files.
6
+ * Each worksheet in the Excel file represents an object type, with the first row
7
+ * containing column headers (field names) and subsequent rows containing data.
8
+ *
9
+ * ✅ Features:
10
+ * - Read data from Excel files
11
+ * - Write data back to Excel files
12
+ * - Multiple sheets support (one sheet per object type)
13
+ * - Full CRUD operations
14
+ * - Query support (filters, sorting, pagination)
15
+ * - Automatic data type handling
16
+ * - Secure: Uses ExcelJS (no known vulnerabilities)
17
+ *
18
+ * Use Cases:
19
+ * - Import/export data from Excel spreadsheets
20
+ * - Use Excel as a simple database for prototyping
21
+ * - Data migration from Excel to other databases
22
+ * - Generate reports in Excel format
23
+ */
24
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
25
+ if (k2 === undefined) k2 = k;
26
+ var desc = Object.getOwnPropertyDescriptor(m, k);
27
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
28
+ desc = { enumerable: true, get: function() { return m[k]; } };
29
+ }
30
+ Object.defineProperty(o, k2, desc);
31
+ }) : (function(o, m, k, k2) {
32
+ if (k2 === undefined) k2 = k;
33
+ o[k2] = m[k];
34
+ }));
35
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
36
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
37
+ }) : function(o, v) {
38
+ o["default"] = v;
39
+ });
40
+ var __importStar = (this && this.__importStar) || (function () {
41
+ var ownKeys = function(o) {
42
+ ownKeys = Object.getOwnPropertyNames || function (o) {
43
+ var ar = [];
44
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
45
+ return ar;
46
+ };
47
+ return ownKeys(o);
48
+ };
49
+ return function (mod) {
50
+ if (mod && mod.__esModule) return mod;
51
+ var result = {};
52
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
53
+ __setModuleDefault(result, mod);
54
+ return result;
55
+ };
56
+ })();
57
+ Object.defineProperty(exports, "__esModule", { value: true });
58
+ exports.ExcelDriver = void 0;
59
+ const types_1 = require("@objectql/types");
60
+ const ExcelJS = __importStar(require("exceljs"));
61
+ const fs = __importStar(require("fs"));
62
+ const path = __importStar(require("path"));
63
+ /**
64
+ * Excel Driver Implementation
65
+ *
66
+ * Stores ObjectQL documents in Excel worksheets. Each object type is stored
67
+ * in a separate worksheet, with the first row containing column headers.
68
+ *
69
+ * Uses ExcelJS library for secure Excel file operations.
70
+ */
71
+ class ExcelDriver {
72
+ constructor(config) {
73
+ this.config = {
74
+ autoSave: true,
75
+ createIfMissing: true,
76
+ strictMode: false,
77
+ fileStorageMode: 'single-file',
78
+ ...config
79
+ };
80
+ this.filePath = path.resolve(config.filePath);
81
+ this.fileStorageMode = this.config.fileStorageMode;
82
+ this.data = new Map();
83
+ this.idCounters = new Map();
84
+ this.workbooks = new Map();
85
+ // Initialize workbook for single-file mode
86
+ if (this.fileStorageMode === 'single-file') {
87
+ this.workbook = new ExcelJS.Workbook();
88
+ }
89
+ // Note: Actual file loading happens in init()
90
+ // Call init() after construction or use the async create() factory method
91
+ }
92
+ /**
93
+ * Initialize the driver by loading the workbook from file.
94
+ * This must be called after construction before using the driver.
95
+ */
96
+ async init() {
97
+ await this.loadWorkbook();
98
+ }
99
+ /**
100
+ * Factory method to create and initialize the driver.
101
+ */
102
+ static async create(config) {
103
+ const driver = new ExcelDriver(config);
104
+ await driver.init();
105
+ return driver;
106
+ }
107
+ /**
108
+ * Load workbook from file or create a new one.
109
+ * Handles both single-file and file-per-object modes.
110
+ */
111
+ async loadWorkbook() {
112
+ if (this.fileStorageMode === 'single-file') {
113
+ await this.loadSingleFileWorkbook();
114
+ }
115
+ else {
116
+ await this.loadFilePerObjectWorkbooks();
117
+ }
118
+ }
119
+ /**
120
+ * Load workbook in single-file mode (all objects in one Excel file).
121
+ */
122
+ async loadSingleFileWorkbook() {
123
+ this.workbook = new ExcelJS.Workbook();
124
+ if (fs.existsSync(this.filePath)) {
125
+ try {
126
+ await this.workbook.xlsx.readFile(this.filePath);
127
+ this.loadDataFromWorkbook();
128
+ }
129
+ catch (error) {
130
+ const errorMessage = error.message;
131
+ // Provide helpful error messages for common issues
132
+ let detailedMessage = `Failed to read Excel file: ${this.filePath}`;
133
+ if (errorMessage.includes('corrupted') || errorMessage.includes('invalid')) {
134
+ detailedMessage += ' - File may be corrupted or not a valid .xlsx file';
135
+ }
136
+ else if (errorMessage.includes('permission') || errorMessage.includes('EACCES')) {
137
+ detailedMessage += ' - Permission denied. Check file permissions.';
138
+ }
139
+ else if (errorMessage.includes('EBUSY')) {
140
+ detailedMessage += ' - File is locked by another process. Close it and try again.';
141
+ }
142
+ throw new types_1.ObjectQLError({
143
+ code: 'FILE_READ_ERROR',
144
+ message: detailedMessage,
145
+ details: {
146
+ filePath: this.filePath,
147
+ error: errorMessage
148
+ }
149
+ });
150
+ }
151
+ }
152
+ else if (this.config.createIfMissing) {
153
+ // Create new empty workbook
154
+ await this.saveWorkbook();
155
+ }
156
+ else {
157
+ throw new types_1.ObjectQLError({
158
+ code: 'FILE_NOT_FOUND',
159
+ message: `Excel file not found: ${this.filePath}`,
160
+ details: { filePath: this.filePath }
161
+ });
162
+ }
163
+ }
164
+ /**
165
+ * Load workbooks in file-per-object mode (each object in separate Excel file).
166
+ */
167
+ async loadFilePerObjectWorkbooks() {
168
+ // Ensure directory exists
169
+ if (!fs.existsSync(this.filePath)) {
170
+ if (this.config.createIfMissing) {
171
+ fs.mkdirSync(this.filePath, { recursive: true });
172
+ }
173
+ else {
174
+ throw new types_1.ObjectQLError({
175
+ code: 'DIRECTORY_NOT_FOUND',
176
+ message: `Directory not found: ${this.filePath}`,
177
+ details: { filePath: this.filePath }
178
+ });
179
+ }
180
+ }
181
+ // Check if it's actually a directory
182
+ const stats = fs.statSync(this.filePath);
183
+ if (!stats.isDirectory()) {
184
+ throw new types_1.ObjectQLError({
185
+ code: 'INVALID_PATH',
186
+ message: `Path must be a directory in file-per-object mode: ${this.filePath}`,
187
+ details: { filePath: this.filePath }
188
+ });
189
+ }
190
+ // Load all existing .xlsx files in the directory
191
+ const files = fs.readdirSync(this.filePath);
192
+ for (const file of files) {
193
+ if (file.endsWith('.xlsx') && !file.startsWith('~$')) {
194
+ const objectName = file.replace('.xlsx', '');
195
+ const filePath = path.join(this.filePath, file);
196
+ try {
197
+ const workbook = new ExcelJS.Workbook();
198
+ await workbook.xlsx.readFile(filePath);
199
+ this.workbooks.set(objectName, workbook);
200
+ // Load data from first worksheet
201
+ const worksheet = workbook.worksheets[0];
202
+ if (worksheet) {
203
+ this.loadDataFromSingleWorksheet(worksheet, objectName);
204
+ }
205
+ }
206
+ catch (error) {
207
+ console.warn(`[ExcelDriver] Warning: Failed to load file ${file}:`, error.message);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ /**
213
+ * Load data from workbook into memory.
214
+ *
215
+ * Expected Excel format:
216
+ * - First row contains column headers (field names)
217
+ * - Subsequent rows contain data records
218
+ * - Each worksheet represents one object type
219
+ */
220
+ loadDataFromWorkbook() {
221
+ this.workbook.eachSheet((worksheet) => {
222
+ this.loadDataFromSingleWorksheet(worksheet, worksheet.name);
223
+ });
224
+ }
225
+ /**
226
+ * Load data from a single worksheet into memory.
227
+ */
228
+ loadDataFromSingleWorksheet(worksheet, objectName) {
229
+ const records = [];
230
+ // Get headers from first row
231
+ const headerRow = worksheet.getRow(1);
232
+ const headers = [];
233
+ headerRow.eachCell((cell, colNumber) => {
234
+ const headerValue = cell.value;
235
+ if (headerValue) {
236
+ headers[colNumber - 1] = String(headerValue);
237
+ }
238
+ });
239
+ // Warn if worksheet has no headers (might be corrupted or wrong format)
240
+ if (headers.length === 0 && worksheet.rowCount > 0) {
241
+ console.warn(`[ExcelDriver] Warning: Worksheet "${objectName}" has no headers in first row. Skipping.`);
242
+ return;
243
+ }
244
+ // Skip first row (headers) and read data rows
245
+ let rowsProcessed = 0;
246
+ let rowsSkipped = 0;
247
+ worksheet.eachRow((row, rowNumber) => {
248
+ if (rowNumber === 1)
249
+ return; // Skip header row
250
+ const record = {};
251
+ let hasData = false;
252
+ row.eachCell((cell, colNumber) => {
253
+ const header = headers[colNumber - 1];
254
+ if (header) {
255
+ record[header] = cell.value;
256
+ hasData = true;
257
+ }
258
+ });
259
+ // Skip completely empty rows
260
+ if (!hasData) {
261
+ rowsSkipped++;
262
+ return;
263
+ }
264
+ // Ensure ID exists
265
+ if (!record.id) {
266
+ record.id = this.generateId(objectName);
267
+ }
268
+ records.push(record);
269
+ rowsProcessed++;
270
+ });
271
+ // Log summary for debugging
272
+ if (rowsSkipped > 0) {
273
+ console.warn(`[ExcelDriver] Worksheet "${objectName}": Processed ${rowsProcessed} rows, skipped ${rowsSkipped} empty rows`);
274
+ }
275
+ this.data.set(objectName, records);
276
+ this.updateIdCounter(objectName, records);
277
+ }
278
+ /**
279
+ * Update ID counter based on existing records.
280
+ *
281
+ * Attempts to extract counter from auto-generated IDs (format: objectName-timestamp-counter).
282
+ * If IDs don't follow this format, counter starts from existing record count.
283
+ */
284
+ updateIdCounter(objectName, records) {
285
+ let maxCounter = 0;
286
+ for (const record of records) {
287
+ if (record.id) {
288
+ // Try to extract counter from generated IDs (format: objectName-timestamp-counter)
289
+ const idStr = String(record.id);
290
+ const parts = idStr.split('-');
291
+ // Only parse if it matches the expected auto-generated format
292
+ if (parts.length === 3 && parts[0] === objectName && !isNaN(Number(parts[2]))) {
293
+ const counter = Number(parts[2]);
294
+ if (counter > maxCounter) {
295
+ maxCounter = counter;
296
+ }
297
+ }
298
+ }
299
+ }
300
+ // If no auto-generated IDs found, start from record count to avoid collisions
301
+ if (maxCounter === 0 && records.length > 0) {
302
+ maxCounter = records.length;
303
+ }
304
+ this.idCounters.set(objectName, maxCounter);
305
+ }
306
+ /**
307
+ * Save workbook to file.
308
+ */
309
+ /**
310
+ * Save workbook to file.
311
+ * Handles both single-file and file-per-object modes.
312
+ */
313
+ async saveWorkbook(objectName) {
314
+ if (this.fileStorageMode === 'single-file') {
315
+ await this.saveSingleFileWorkbook();
316
+ }
317
+ else if (objectName) {
318
+ await this.saveFilePerObjectWorkbook(objectName);
319
+ }
320
+ }
321
+ /**
322
+ * Save workbook in single-file mode.
323
+ */
324
+ async saveSingleFileWorkbook() {
325
+ try {
326
+ // Ensure directory exists
327
+ const dir = path.dirname(this.filePath);
328
+ if (!fs.existsSync(dir)) {
329
+ fs.mkdirSync(dir, { recursive: true });
330
+ }
331
+ // Write workbook to file
332
+ await this.workbook.xlsx.writeFile(this.filePath);
333
+ }
334
+ catch (error) {
335
+ throw new types_1.ObjectQLError({
336
+ code: 'FILE_WRITE_ERROR',
337
+ message: `Failed to write Excel file: ${this.filePath}`,
338
+ details: { error: error.message }
339
+ });
340
+ }
341
+ }
342
+ /**
343
+ * Save workbook in file-per-object mode.
344
+ */
345
+ async saveFilePerObjectWorkbook(objectName) {
346
+ const workbook = this.workbooks.get(objectName);
347
+ if (!workbook) {
348
+ throw new types_1.ObjectQLError({
349
+ code: 'WORKBOOK_NOT_FOUND',
350
+ message: `Workbook not found for object: ${objectName}`,
351
+ details: { objectName }
352
+ });
353
+ }
354
+ try {
355
+ const filePath = path.join(this.filePath, `${objectName}.xlsx`);
356
+ await workbook.xlsx.writeFile(filePath);
357
+ }
358
+ catch (error) {
359
+ throw new types_1.ObjectQLError({
360
+ code: 'FILE_WRITE_ERROR',
361
+ message: `Failed to write Excel file for object: ${objectName}`,
362
+ details: { objectName, error: error.message }
363
+ });
364
+ }
365
+ }
366
+ /**
367
+ * Sync in-memory data to workbook.
368
+ *
369
+ * Creates or updates a worksheet for the given object type.
370
+ */
371
+ async syncToWorkbook(objectName) {
372
+ if (this.fileStorageMode === 'single-file') {
373
+ await this.syncToSingleFileWorkbook(objectName);
374
+ }
375
+ else {
376
+ await this.syncToFilePerObjectWorkbook(objectName);
377
+ }
378
+ }
379
+ /**
380
+ * Sync data to worksheet in single-file mode.
381
+ */
382
+ async syncToSingleFileWorkbook(objectName) {
383
+ const records = this.data.get(objectName) || [];
384
+ // Remove existing worksheet if it exists
385
+ const existingSheet = this.workbook.getWorksheet(objectName);
386
+ if (existingSheet) {
387
+ this.workbook.removeWorksheet(existingSheet.id);
388
+ }
389
+ // Create new worksheet
390
+ const worksheet = this.workbook.addWorksheet(objectName);
391
+ if (records.length > 0) {
392
+ // Get all unique keys from records to create headers
393
+ const headers = new Set();
394
+ records.forEach(record => {
395
+ Object.keys(record).forEach(key => headers.add(key));
396
+ });
397
+ const headerArray = Array.from(headers);
398
+ // Add header row
399
+ worksheet.addRow(headerArray);
400
+ // Add data rows
401
+ records.forEach(record => {
402
+ const row = headerArray.map(header => record[header]);
403
+ worksheet.addRow(row);
404
+ });
405
+ // Auto-fit columns
406
+ worksheet.columns.forEach((column) => {
407
+ if (column.header) {
408
+ column.width = Math.max(10, String(column.header).length + 2);
409
+ }
410
+ });
411
+ }
412
+ // Auto-save if enabled
413
+ if (this.config.autoSave) {
414
+ await this.saveWorkbook();
415
+ }
416
+ }
417
+ /**
418
+ * Sync data to separate file in file-per-object mode.
419
+ */
420
+ async syncToFilePerObjectWorkbook(objectName) {
421
+ const records = this.data.get(objectName) || [];
422
+ // Get or create workbook for this object
423
+ let workbook = this.workbooks.get(objectName);
424
+ if (!workbook) {
425
+ workbook = new ExcelJS.Workbook();
426
+ this.workbooks.set(objectName, workbook);
427
+ }
428
+ // Remove all existing worksheets
429
+ workbook.worksheets.forEach(ws => workbook.removeWorksheet(ws.id));
430
+ // Create new worksheet
431
+ const worksheet = workbook.addWorksheet(objectName);
432
+ if (records.length > 0) {
433
+ // Get all unique keys from records to create headers
434
+ const headers = new Set();
435
+ records.forEach(record => {
436
+ Object.keys(record).forEach(key => headers.add(key));
437
+ });
438
+ const headerArray = Array.from(headers);
439
+ // Add header row
440
+ worksheet.addRow(headerArray);
441
+ // Add data rows
442
+ records.forEach(record => {
443
+ const row = headerArray.map(header => record[header]);
444
+ worksheet.addRow(row);
445
+ });
446
+ // Auto-fit columns
447
+ worksheet.columns.forEach((column) => {
448
+ if (column.header) {
449
+ column.width = Math.max(10, String(column.header).length + 2);
450
+ }
451
+ });
452
+ }
453
+ // Auto-save if enabled
454
+ if (this.config.autoSave) {
455
+ await this.saveWorkbook(objectName);
456
+ }
457
+ }
458
+ /**
459
+ * Find multiple records matching the query criteria.
460
+ */
461
+ async find(objectName, query = {}, options) {
462
+ let results = this.data.get(objectName) || [];
463
+ // Return empty array if no data
464
+ if (results.length === 0) {
465
+ return [];
466
+ }
467
+ // Deep copy to avoid mutations
468
+ results = results.map(r => ({ ...r }));
469
+ // Apply filters
470
+ if (query.filters) {
471
+ results = this.applyFilters(results, query.filters);
472
+ }
473
+ // Apply sorting
474
+ if (query.sort && Array.isArray(query.sort)) {
475
+ results = this.applySort(results, query.sort);
476
+ }
477
+ // Apply pagination
478
+ if (query.skip) {
479
+ results = results.slice(query.skip);
480
+ }
481
+ if (query.limit) {
482
+ results = results.slice(0, query.limit);
483
+ }
484
+ // Apply field projection
485
+ if (query.fields && Array.isArray(query.fields)) {
486
+ results = results.map(doc => this.projectFields(doc, query.fields));
487
+ }
488
+ return results;
489
+ }
490
+ /**
491
+ * Find a single record by ID or query.
492
+ */
493
+ async findOne(objectName, id, query, options) {
494
+ const records = this.data.get(objectName) || [];
495
+ // If ID is provided, fetch directly
496
+ if (id) {
497
+ const record = records.find(r => r.id === String(id));
498
+ return record ? { ...record } : null;
499
+ }
500
+ // If query is provided, use find and return first result
501
+ if (query) {
502
+ const results = await this.find(objectName, { ...query, limit: 1 }, options);
503
+ return results[0] || null;
504
+ }
505
+ return null;
506
+ }
507
+ /**
508
+ * Create a new record.
509
+ */
510
+ async create(objectName, data, options) {
511
+ // Get or create object data array
512
+ if (!this.data.has(objectName)) {
513
+ this.data.set(objectName, []);
514
+ }
515
+ const records = this.data.get(objectName);
516
+ // Generate ID if not provided
517
+ const id = data.id || this.generateId(objectName);
518
+ // Check if record already exists
519
+ if (records.some(r => r.id === id)) {
520
+ throw new types_1.ObjectQLError({
521
+ code: 'DUPLICATE_RECORD',
522
+ message: `Record with id '${id}' already exists in '${objectName}'`,
523
+ details: { objectName, id }
524
+ });
525
+ }
526
+ const now = new Date().toISOString();
527
+ const doc = {
528
+ id,
529
+ ...data,
530
+ created_at: data.created_at || now,
531
+ updated_at: data.updated_at || now
532
+ };
533
+ records.push(doc);
534
+ await this.syncToWorkbook(objectName);
535
+ return { ...doc };
536
+ }
537
+ /**
538
+ * Update an existing record.
539
+ */
540
+ async update(objectName, id, data, options) {
541
+ const records = this.data.get(objectName) || [];
542
+ const index = records.findIndex(r => r.id === String(id));
543
+ if (index === -1) {
544
+ if (this.config.strictMode) {
545
+ throw new types_1.ObjectQLError({
546
+ code: 'RECORD_NOT_FOUND',
547
+ message: `Record with id '${id}' not found in '${objectName}'`,
548
+ details: { objectName, id }
549
+ });
550
+ }
551
+ return null;
552
+ }
553
+ const existing = records[index];
554
+ const doc = {
555
+ ...existing,
556
+ ...data,
557
+ id: existing.id, // Preserve ID
558
+ created_at: existing.created_at, // Preserve created_at
559
+ updated_at: new Date().toISOString()
560
+ };
561
+ records[index] = doc;
562
+ await this.syncToWorkbook(objectName);
563
+ return { ...doc };
564
+ }
565
+ /**
566
+ * Delete a record.
567
+ */
568
+ async delete(objectName, id, options) {
569
+ const records = this.data.get(objectName) || [];
570
+ const index = records.findIndex(r => r.id === String(id));
571
+ if (index === -1) {
572
+ if (this.config.strictMode) {
573
+ throw new types_1.ObjectQLError({
574
+ code: 'RECORD_NOT_FOUND',
575
+ message: `Record with id '${id}' not found in '${objectName}'`,
576
+ details: { objectName, id }
577
+ });
578
+ }
579
+ return false;
580
+ }
581
+ records.splice(index, 1);
582
+ await this.syncToWorkbook(objectName);
583
+ return true;
584
+ }
585
+ /**
586
+ * Count records matching filters.
587
+ */
588
+ async count(objectName, filters, options) {
589
+ const records = this.data.get(objectName) || [];
590
+ // Extract actual filters from query object if needed
591
+ let actualFilters = filters;
592
+ if (filters && !Array.isArray(filters) && filters.filters) {
593
+ actualFilters = filters.filters;
594
+ }
595
+ // If no filters or empty object/array, return total count
596
+ if (!actualFilters ||
597
+ (Array.isArray(actualFilters) && actualFilters.length === 0) ||
598
+ (typeof actualFilters === 'object' && !Array.isArray(actualFilters) && Object.keys(actualFilters).length === 0)) {
599
+ return records.length;
600
+ }
601
+ // Count only records matching filters
602
+ return records.filter(record => this.matchesFilters(record, actualFilters)).length;
603
+ }
604
+ /**
605
+ * Get distinct values for a field.
606
+ */
607
+ async distinct(objectName, field, filters, options) {
608
+ const records = this.data.get(objectName) || [];
609
+ const values = new Set();
610
+ for (const record of records) {
611
+ if (!filters || this.matchesFilters(record, filters)) {
612
+ const value = record[field];
613
+ if (value !== undefined && value !== null) {
614
+ values.add(value);
615
+ }
616
+ }
617
+ }
618
+ return Array.from(values);
619
+ }
620
+ /**
621
+ * Create multiple records at once.
622
+ */
623
+ async createMany(objectName, data, options) {
624
+ const results = [];
625
+ for (const item of data) {
626
+ const result = await this.create(objectName, item, options);
627
+ results.push(result);
628
+ }
629
+ return results;
630
+ }
631
+ /**
632
+ * Update multiple records matching filters.
633
+ */
634
+ async updateMany(objectName, filters, data, options) {
635
+ const records = this.data.get(objectName) || [];
636
+ let count = 0;
637
+ for (let i = 0; i < records.length; i++) {
638
+ if (this.matchesFilters(records[i], filters)) {
639
+ records[i] = {
640
+ ...records[i],
641
+ ...data,
642
+ id: records[i].id, // Preserve ID
643
+ created_at: records[i].created_at, // Preserve created_at
644
+ updated_at: new Date().toISOString()
645
+ };
646
+ count++;
647
+ }
648
+ }
649
+ if (count > 0) {
650
+ await this.syncToWorkbook(objectName);
651
+ }
652
+ return { modifiedCount: count };
653
+ }
654
+ /**
655
+ * Delete multiple records matching filters.
656
+ */
657
+ async deleteMany(objectName, filters, options) {
658
+ const records = this.data.get(objectName) || [];
659
+ const initialLength = records.length;
660
+ const filtered = records.filter(record => !this.matchesFilters(record, filters));
661
+ this.data.set(objectName, filtered);
662
+ const deletedCount = initialLength - filtered.length;
663
+ if (deletedCount > 0) {
664
+ await this.syncToWorkbook(objectName);
665
+ }
666
+ return { deletedCount };
667
+ }
668
+ /**
669
+ * Manually save the workbook to file.
670
+ */
671
+ /**
672
+ * Manually save the workbook to file.
673
+ */
674
+ async save() {
675
+ if (this.fileStorageMode === 'single-file') {
676
+ await this.saveWorkbook();
677
+ }
678
+ else {
679
+ // Save all object files in file-per-object mode
680
+ for (const objectName of this.data.keys()) {
681
+ await this.saveWorkbook(objectName);
682
+ }
683
+ }
684
+ }
685
+ /**
686
+ * Disconnect (flush any pending writes).
687
+ */
688
+ async disconnect() {
689
+ if (this.config.autoSave) {
690
+ await this.save();
691
+ }
692
+ }
693
+ // ========== Helper Methods ==========
694
+ /**
695
+ * Apply filters to an array of records (in-memory filtering).
696
+ */
697
+ applyFilters(records, filters) {
698
+ if (!filters || filters.length === 0) {
699
+ return records;
700
+ }
701
+ return records.filter(record => this.matchesFilters(record, filters));
702
+ }
703
+ /**
704
+ * Check if a single record matches the filter conditions.
705
+ */
706
+ matchesFilters(record, filters) {
707
+ if (!filters || filters.length === 0) {
708
+ return true;
709
+ }
710
+ let conditions = [];
711
+ let operators = [];
712
+ for (const item of filters) {
713
+ if (typeof item === 'string') {
714
+ // Logical operator (and/or)
715
+ operators.push(item.toLowerCase());
716
+ }
717
+ else if (Array.isArray(item)) {
718
+ const [field, operator, value] = item;
719
+ // Handle nested filter groups
720
+ if (typeof field !== 'string') {
721
+ // Nested group - recursively evaluate
722
+ conditions.push(this.matchesFilters(record, item));
723
+ }
724
+ else {
725
+ // Single condition
726
+ const matches = this.evaluateCondition(record[field], operator, value);
727
+ conditions.push(matches);
728
+ }
729
+ }
730
+ }
731
+ // Combine conditions with operators
732
+ if (conditions.length === 0) {
733
+ return true;
734
+ }
735
+ let result = conditions[0];
736
+ for (let i = 0; i < operators.length; i++) {
737
+ const op = operators[i];
738
+ const nextCondition = conditions[i + 1];
739
+ if (op === 'or') {
740
+ result = result || nextCondition;
741
+ }
742
+ else { // 'and' or default
743
+ result = result && nextCondition;
744
+ }
745
+ }
746
+ return result;
747
+ }
748
+ /**
749
+ * Evaluate a single filter condition.
750
+ */
751
+ evaluateCondition(fieldValue, operator, compareValue) {
752
+ switch (operator) {
753
+ case '=':
754
+ case '==':
755
+ return fieldValue === compareValue;
756
+ case '!=':
757
+ case '<>':
758
+ return fieldValue !== compareValue;
759
+ case '>':
760
+ return fieldValue > compareValue;
761
+ case '>=':
762
+ return fieldValue >= compareValue;
763
+ case '<':
764
+ return fieldValue < compareValue;
765
+ case '<=':
766
+ return fieldValue <= compareValue;
767
+ case 'in':
768
+ return Array.isArray(compareValue) && compareValue.includes(fieldValue);
769
+ case 'nin':
770
+ case 'not in':
771
+ return Array.isArray(compareValue) && !compareValue.includes(fieldValue);
772
+ case 'contains':
773
+ case 'like':
774
+ return String(fieldValue).toLowerCase().includes(String(compareValue).toLowerCase());
775
+ case 'startswith':
776
+ case 'starts_with':
777
+ return String(fieldValue).toLowerCase().startsWith(String(compareValue).toLowerCase());
778
+ case 'endswith':
779
+ case 'ends_with':
780
+ return String(fieldValue).toLowerCase().endsWith(String(compareValue).toLowerCase());
781
+ case 'between':
782
+ return Array.isArray(compareValue) &&
783
+ fieldValue >= compareValue[0] &&
784
+ fieldValue <= compareValue[1];
785
+ default:
786
+ throw new types_1.ObjectQLError({
787
+ code: 'UNSUPPORTED_OPERATOR',
788
+ message: `[ExcelDriver] Unsupported operator: ${operator}`,
789
+ });
790
+ }
791
+ }
792
+ /**
793
+ * Apply sorting to an array of records.
794
+ */
795
+ applySort(records, sort) {
796
+ const sorted = [...records];
797
+ // Apply sorts in reverse order for correct precedence
798
+ for (let i = sort.length - 1; i >= 0; i--) {
799
+ const sortItem = sort[i];
800
+ let field;
801
+ let direction;
802
+ if (Array.isArray(sortItem)) {
803
+ [field, direction] = sortItem;
804
+ }
805
+ else if (typeof sortItem === 'object') {
806
+ field = sortItem.field;
807
+ direction = sortItem.order || sortItem.direction || sortItem.dir || 'asc';
808
+ }
809
+ else {
810
+ continue;
811
+ }
812
+ sorted.sort((a, b) => {
813
+ const aVal = a[field];
814
+ const bVal = b[field];
815
+ // Handle null/undefined
816
+ if (aVal == null && bVal == null)
817
+ return 0;
818
+ if (aVal == null)
819
+ return 1;
820
+ if (bVal == null)
821
+ return -1;
822
+ // Compare values
823
+ if (aVal < bVal)
824
+ return direction === 'asc' ? -1 : 1;
825
+ if (aVal > bVal)
826
+ return direction === 'asc' ? 1 : -1;
827
+ return 0;
828
+ });
829
+ }
830
+ return sorted;
831
+ }
832
+ /**
833
+ * Project specific fields from a document.
834
+ */
835
+ projectFields(doc, fields) {
836
+ const result = {};
837
+ for (const field of fields) {
838
+ if (doc[field] !== undefined) {
839
+ result[field] = doc[field];
840
+ }
841
+ }
842
+ return result;
843
+ }
844
+ /**
845
+ * Generate a unique ID for a record.
846
+ */
847
+ generateId(objectName) {
848
+ const counter = (this.idCounters.get(objectName) || 0) + 1;
849
+ this.idCounters.set(objectName, counter);
850
+ // Use timestamp + counter for better uniqueness
851
+ const timestamp = Date.now();
852
+ return `${objectName}-${timestamp}-${counter}`;
853
+ }
854
+ }
855
+ exports.ExcelDriver = ExcelDriver;
856
+ //# sourceMappingURL=index.js.map