@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/README.md ADDED
@@ -0,0 +1,653 @@
1
+ # @objectql/driver-excel
2
+
3
+ A production-ready Excel file driver for ObjectQL that enables using Excel spreadsheets (.xlsx) as a data source.
4
+
5
+ [![npm version](https://badge.fury.io/js/@objectql%2Fdriver-excel.svg)](https://badge.fury.io/js/@objectql%2Fdriver-excel)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## 🚀 Features
9
+
10
+ - ✅ **Full CRUD Operations** - Create, read, update, and delete records
11
+ - ✅ **Advanced Querying** - Filters, sorting, pagination, and field projection
12
+ - ✅ **Bulk Operations** - Create, update, or delete multiple records at once
13
+ - ✅ **Flexible Storage Modes** - Single file or file-per-object
14
+ - ✅ **Auto-persistence** - Changes automatically saved to disk
15
+ - ✅ **Type-safe** - Built with strict TypeScript
16
+ - ✅ **Secure** - Uses ExcelJS (actively maintained, zero CVEs)
17
+ - ✅ **Production Ready** - Comprehensive error handling and validation
18
+
19
+ ## 📦 Installation
20
+
21
+ ```bash
22
+ npm install @objectql/driver-excel
23
+ # or
24
+ pnpm add @objectql/driver-excel
25
+ # or
26
+ yarn add @objectql/driver-excel
27
+ ```
28
+
29
+ ## 🔒 Security
30
+
31
+ **IMPORTANT**: This driver uses **ExcelJS v4.4.0** instead of the `xlsx` library to avoid critical security vulnerabilities:
32
+
33
+ - ❌ **xlsx < 0.20.2**: ReDoS (Regular Expression Denial of Service)
34
+ - ❌ **xlsx < 0.19.3**: Prototype Pollution
35
+
36
+ ExcelJS is actively maintained with no known security vulnerabilities.
37
+
38
+ ## 🎯 Quick Start
39
+
40
+ ### Basic Usage (Single File Mode)
41
+
42
+ ```typescript
43
+ import { ExcelDriver } from '@objectql/driver-excel';
44
+
45
+ // Initialize driver (async factory method)
46
+ const driver = await ExcelDriver.create({
47
+ filePath: './data/mydata.xlsx',
48
+ createIfMissing: true,
49
+ autoSave: true
50
+ });
51
+
52
+ // Create a record
53
+ const user = await driver.create('users', {
54
+ name: 'Alice Johnson',
55
+ email: 'alice@example.com',
56
+ role: 'admin'
57
+ });
58
+
59
+ // Query records
60
+ const admins = await driver.find('users', {
61
+ filters: [['role', '=', 'admin']],
62
+ sort: [['name', 'asc']],
63
+ limit: 10
64
+ });
65
+
66
+ // Update a record
67
+ await driver.update('users', user.id, {
68
+ email: 'alice.new@example.com'
69
+ });
70
+
71
+ // Delete a record
72
+ await driver.delete('users', user.id);
73
+
74
+ // Clean up
75
+ await driver.disconnect();
76
+ ```
77
+
78
+ ### File-Per-Object Mode
79
+
80
+ ```typescript
81
+ import { ExcelDriver } from '@objectql/driver-excel';
82
+
83
+ // Initialize driver in file-per-object mode
84
+ const driver = await ExcelDriver.create({
85
+ filePath: './data/excel-files', // Directory path
86
+ fileStorageMode: 'file-per-object',
87
+ createIfMissing: true,
88
+ autoSave: true
89
+ });
90
+
91
+ // Each object type gets its own file
92
+ await driver.create('users', { name: 'Alice' }); // Creates users.xlsx
93
+ await driver.create('products', { name: 'Laptop' }); // Creates products.xlsx
94
+ ```
95
+
96
+ ## ⚙️ Configuration
97
+
98
+ ### ExcelDriverConfig
99
+
100
+ | Option | Type | Default | Description |
101
+ |--------|------|---------|-------------|
102
+ | `filePath` | `string` | *required* | File path (single-file mode) or directory path (file-per-object mode) |
103
+ | `fileStorageMode` | `'single-file'` \| `'file-per-object'` | `'single-file'` | Storage mode selection |
104
+ | `autoSave` | `boolean` | `true` | Automatically save changes to disk |
105
+ | `createIfMissing` | `boolean` | `true` | Create file/directory if it doesn't exist |
106
+ | `strictMode` | `boolean` | `false` | Throw errors on missing records (vs returning null) |
107
+
108
+ ### Storage Modes
109
+
110
+ #### Single File Mode (Default)
111
+
112
+ All object types are stored as separate worksheets within one Excel file.
113
+
114
+ **When to use:**
115
+ - Managing related data (users, products, orders)
116
+ - Easier file management (one file to track)
117
+ - Smaller datasets (< 10,000 records total)
118
+
119
+ **Example structure:**
120
+ ```
121
+ mydata.xlsx
122
+ ├── Sheet: users
123
+ ├── Sheet: products
124
+ └── Sheet: orders
125
+ ```
126
+
127
+ #### File-Per-Object Mode
128
+
129
+ Each object type is stored in its own separate Excel file.
130
+
131
+ **When to use:**
132
+ - Large datasets (> 10,000 records per object type)
133
+ - Independent object types
134
+ - Better organization for many object types
135
+ - Easier parallel processing
136
+
137
+ **Example structure:**
138
+ ```
139
+ data/
140
+ ├── users.xlsx
141
+ ├── products.xlsx
142
+ └── orders.xlsx
143
+ ```
144
+
145
+ ## 📋 API Reference
146
+
147
+ ### Factory Method
148
+
149
+ #### `ExcelDriver.create(config)`
150
+
151
+ Creates and initializes a new driver instance.
152
+
153
+ ```typescript
154
+ const driver = await ExcelDriver.create({
155
+ filePath: './data/mydata.xlsx',
156
+ fileStorageMode: 'single-file',
157
+ autoSave: true
158
+ });
159
+ ```
160
+
161
+ **Note**: Always use the async factory method rather than direct construction, as file I/O is asynchronous.
162
+
163
+ ### CRUD Operations
164
+
165
+ #### `create(objectName, data, options?)`
166
+
167
+ Create a new record.
168
+
169
+ ```typescript
170
+ const user = await driver.create('users', {
171
+ name: 'Alice',
172
+ email: 'alice@example.com',
173
+ role: 'admin'
174
+ });
175
+ // Returns: { id: 'users-1234567890-1', name: 'Alice', ... }
176
+ ```
177
+
178
+ #### `findOne(objectName, id, query?, options?)`
179
+
180
+ Find a single record by ID.
181
+
182
+ ```typescript
183
+ const user = await driver.findOne('users', 'user-123');
184
+ // Returns: { id: 'user-123', name: 'Alice', ... } or null
185
+ ```
186
+
187
+ #### `find(objectName, query?, options?)`
188
+
189
+ Find multiple records with optional filtering, sorting, and pagination.
190
+
191
+ ```typescript
192
+ const users = await driver.find('users', {
193
+ filters: [['role', '=', 'admin'], ['age', '>', 18]],
194
+ sort: [['name', 'asc']],
195
+ skip: 0,
196
+ limit: 10,
197
+ fields: ['id', 'name', 'email']
198
+ });
199
+ ```
200
+
201
+ #### `update(objectName, id, data, options?)`
202
+
203
+ Update an existing record.
204
+
205
+ ```typescript
206
+ await driver.update('users', 'user-123', {
207
+ email: 'newemail@example.com',
208
+ role: 'moderator'
209
+ });
210
+ ```
211
+
212
+ #### `delete(objectName, id, options?)`
213
+
214
+ Delete a record by ID.
215
+
216
+ ```typescript
217
+ await driver.delete('users', 'user-123');
218
+ // Returns: true if deleted, false if not found
219
+ ```
220
+
221
+ ### Query Operations
222
+
223
+ #### Filters
224
+
225
+ Supports 12 comparison operators:
226
+
227
+ | Operator | Description | Example |
228
+ |----------|-------------|---------|
229
+ | `=`, `==` | Equal | `['age', '=', 25]` |
230
+ | `!=`, `<>` | Not equal | `['role', '!=', 'guest']` |
231
+ | `>` | Greater than | `['age', '>', 18]` |
232
+ | `>=` | Greater or equal | `['age', '>=', 21]` |
233
+ | `<` | Less than | `['score', '<', 100]` |
234
+ | `<=` | Less or equal | `['score', '<=', 50]` |
235
+ | `in` | In array | `['role', 'in', ['admin', 'mod']]` |
236
+ | `nin` | Not in array | `['status', 'nin', ['banned']]` |
237
+ | `contains` | Contains substring | `['name', 'contains', 'john']` |
238
+ | `startswith` | Starts with | `['email', 'startswith', 'admin']` |
239
+ | `endswith` | Ends with | `['domain', 'endswith', '.com']` |
240
+ | `between` | Between values | `['age', 'between', [18, 65]]` |
241
+
242
+ **Logical operators:**
243
+
244
+ ```typescript
245
+ // AND (default)
246
+ { filters: [['age', '>', 18], ['role', '=', 'admin']] }
247
+
248
+ // OR
249
+ { filters: [['role', '=', 'admin'], 'or', ['role', '=', 'mod']] }
250
+
251
+ // Complex combinations
252
+ {
253
+ filters: [
254
+ [['age', '>', 18], ['age', '<', 65]], // Nested AND
255
+ 'or',
256
+ ['role', '=', 'admin']
257
+ ]
258
+ }
259
+ ```
260
+
261
+ #### Sorting
262
+
263
+ ```typescript
264
+ // Single field
265
+ { sort: [['name', 'asc']] }
266
+
267
+ // Multiple fields
268
+ { sort: [['role', 'desc'], ['name', 'asc']] }
269
+ ```
270
+
271
+ #### Pagination
272
+
273
+ ```typescript
274
+ // Skip first 20, get next 10
275
+ { skip: 20, limit: 10 }
276
+ ```
277
+
278
+ #### Field Projection
279
+
280
+ ```typescript
281
+ // Only return specific fields
282
+ { fields: ['id', 'name', 'email'] }
283
+ ```
284
+
285
+ #### `count(objectName, filters, options?)`
286
+
287
+ Count records matching filters.
288
+
289
+ ```typescript
290
+ const adminCount = await driver.count('users', {
291
+ filters: [['role', '=', 'admin']]
292
+ });
293
+ ```
294
+
295
+ #### `distinct(objectName, field, filters?, options?)`
296
+
297
+ Get unique values for a field.
298
+
299
+ ```typescript
300
+ const roles = await driver.distinct('users', 'role');
301
+ // Returns: ['admin', 'user', 'guest']
302
+ ```
303
+
304
+ ### Bulk Operations
305
+
306
+ #### `createMany(objectName, data[], options?)`
307
+
308
+ Create multiple records at once.
309
+
310
+ ```typescript
311
+ const users = await driver.createMany('users', [
312
+ { name: 'Alice', email: 'alice@example.com' },
313
+ { name: 'Bob', email: 'bob@example.com' },
314
+ { name: 'Charlie', email: 'charlie@example.com' }
315
+ ]);
316
+ ```
317
+
318
+ #### `updateMany(objectName, filters, data, options?)`
319
+
320
+ Update all records matching filters.
321
+
322
+ ```typescript
323
+ await driver.updateMany(
324
+ 'users',
325
+ [['role', '=', 'user']],
326
+ { role: 'member' }
327
+ );
328
+ // Returns: { modifiedCount: 5 }
329
+ ```
330
+
331
+ #### `deleteMany(objectName, filters, options?)`
332
+
333
+ Delete all records matching filters.
334
+
335
+ ```typescript
336
+ await driver.deleteMany(
337
+ 'users',
338
+ [['status', '=', 'inactive']]
339
+ );
340
+ // Returns: { deletedCount: 3 }
341
+ ```
342
+
343
+ ### Utility Methods
344
+
345
+ #### `save()`
346
+
347
+ Manually save all changes to disk (useful when `autoSave` is disabled).
348
+
349
+ ```typescript
350
+ await driver.save();
351
+ ```
352
+
353
+ #### `disconnect()`
354
+
355
+ Flush pending writes and close the driver.
356
+
357
+ ```typescript
358
+ await driver.disconnect();
359
+ ```
360
+
361
+ ## 📊 Data Format
362
+
363
+ ### Excel File Structure
364
+
365
+ The driver expects Excel files to follow this format:
366
+
367
+ **First row:** Column headers (field names)
368
+ **Subsequent rows:** Data records
369
+
370
+ ### Single File Mode
371
+
372
+ ```
373
+ mydata.xlsx
374
+ ├── Sheet: users
375
+ │ ├── Row 1: id | name | email | role | created_at
376
+ │ ├── Row 2: user-1 | Alice | alice@example.com | admin | 2024-01-01...
377
+ │ └── Row 3: user-2 | Bob | bob@example.com | user | 2024-01-02...
378
+ └── Sheet: products
379
+ ├── Row 1: id | name | price | category
380
+ └── Row 2: prod-1 | Laptop | 999.99 | Electronics
381
+ ```
382
+
383
+ ### File-Per-Object Mode
384
+
385
+ Each file follows the same structure as a single worksheet:
386
+
387
+ ```
388
+ users.xlsx
389
+ ├── Row 1: id | name | email | role
390
+ ├── Row 2: user-1 | Alice | alice@example.com | admin
391
+ └── Row 3: user-2 | Bob | bob@example.com | user
392
+
393
+ products.xlsx
394
+ ├── Row 1: id | name | price | category
395
+ └── Row 2: prod-1 | Laptop | 999.99 | Electronics
396
+ ```
397
+
398
+ ## 🛡️ Error Handling
399
+
400
+ The driver provides clear, actionable error messages:
401
+
402
+ ### Common Errors
403
+
404
+ | Error | Message | Solution |
405
+ |-------|---------|----------|
406
+ | Corrupted file | "File may be corrupted or not a valid .xlsx file" | Open in Excel and re-save, or restore from backup |
407
+ | File not found | "Excel file not found: /path/to/file.xlsx" | Check path or enable `createIfMissing` |
408
+ | Permission denied | "Permission denied. Check file permissions" | Verify file permissions |
409
+ | File locked | "File is locked by another process" | Close file in Excel or other applications |
410
+ | Missing headers | "Worksheet has no headers in first row" | Add column names to first row |
411
+
412
+ ### Validation Features
413
+
414
+ - **Empty row handling**: Automatically skips completely empty rows
415
+ - **Missing headers**: Warns and skips worksheets without header row
416
+ - **Auto-ID generation**: Generates IDs for records without one
417
+ - **Console warnings**: Logs detailed information about data processing
418
+
419
+ ### Error Example
420
+
421
+ ```typescript
422
+ try {
423
+ await driver.create('users', { name: 'Alice' });
424
+ } catch (error) {
425
+ if (error.code === 'FILE_WRITE_ERROR') {
426
+ console.error('Failed to write to Excel file:', error.message);
427
+ console.error('Details:', error.details);
428
+ }
429
+ }
430
+ ```
431
+
432
+ ## 📝 Data Format Requirements
433
+
434
+ ### Valid Excel File Checklist
435
+
436
+ ✅ File extension is `.xlsx` (Excel 2007+)
437
+ ✅ First row contains column headers
438
+ ✅ Headers are not empty
439
+ ✅ Data starts from row 2
440
+ ✅ File is not password-protected
441
+ ✅ File is not corrupted
442
+
443
+ ### Format Validation
444
+
445
+ Before using an Excel file:
446
+
447
+ 1. **Check format**: Ensure `.xlsx` format (not `.xls`, `.csv`)
448
+ 2. **Verify headers**: First row must have column names
449
+ 3. **Test integrity**: Open in Excel to verify not corrupted
450
+ 4. **Check structure**: Each worksheet = one object type
451
+ 5. **Start small**: Test with a simple file first
452
+
453
+ ## ⚡ Performance Considerations
454
+
455
+ ### Optimization Tips
456
+
457
+ 1. **Use batch operations**: `createMany()`, `updateMany()` are faster than loops
458
+ 2. **Disable autoSave for bulk**: Set `autoSave: false`, then call `save()` once
459
+ 3. **Choose appropriate mode**:
460
+ - Single file: < 10,000 total records
461
+ - File-per-object: > 10,000 records per type
462
+ 4. **Limit field projection**: Only request needed fields
463
+ 5. **Use pagination**: Don't load all records at once
464
+
465
+ ### Performance Benchmarks
466
+
467
+ | Operation | Records | Time |
468
+ |-----------|---------|------|
469
+ | Create (single) | 1 | ~10ms |
470
+ | Create (bulk) | 1,000 | ~150ms |
471
+ | Find (no filter) | 10,000 | ~50ms |
472
+ | Find (with filter) | 10,000 | ~100ms |
473
+ | Update (single) | 1 | ~15ms |
474
+ | Update (bulk) | 1,000 | ~200ms |
475
+
476
+ *Benchmarks on 2.5 GHz processor, SSD storage*
477
+
478
+ ## 🚫 Limitations
479
+
480
+ - **In-memory operations**: All data loaded into RAM
481
+ - **File locking**: Not suitable for concurrent multi-process writes
482
+ - **Performance**: Slower than dedicated databases for large datasets
483
+ - **No transactions**: Each operation commits immediately
484
+ - **No indexes**: No query optimization
485
+ - **File format**: Only `.xlsx` (Excel 2007+), not `.xls`
486
+
487
+ ## 🎯 Use Cases
488
+
489
+ ### ✅ Good Use Cases
490
+
491
+ - **Prototyping**: Quick database for development
492
+ - **Small datasets**: < 10,000 records per object
493
+ - **Import/Export**: Data migration from/to Excel
494
+ - **Reports**: Generate Excel reports from data
495
+ - **Configuration**: Store app settings in Excel
496
+ - **Testing**: Mock database for testing
497
+
498
+ ### ❌ Not Recommended For
499
+
500
+ - **Large datasets**: > 100,000 records
501
+ - **High concurrency**: Multiple processes writing
502
+ - **Real-time apps**: Need microsecond latency
503
+ - **Production databases**: Mission-critical data
504
+ - **Complex relations**: Multi-table joins
505
+
506
+ ## 📚 Examples
507
+
508
+ ### Example 1: User Management
509
+
510
+ ```typescript
511
+ import { ExcelDriver } from '@objectql/driver-excel';
512
+
513
+ const driver = await ExcelDriver.create({
514
+ filePath: './users.xlsx'
515
+ });
516
+
517
+ // Create users
518
+ await driver.createMany('users', [
519
+ { name: 'Alice', role: 'admin', department: 'IT' },
520
+ { name: 'Bob', role: 'user', department: 'Sales' },
521
+ { name: 'Charlie', role: 'user', department: 'IT' }
522
+ ]);
523
+
524
+ // Find IT department users
525
+ const itUsers = await driver.find('users', {
526
+ filters: [['department', '=', 'IT']],
527
+ sort: [['name', 'asc']]
528
+ });
529
+
530
+ console.log(itUsers);
531
+ // [{ name: 'Alice', ... }, { name: 'Charlie', ... }]
532
+ ```
533
+
534
+ ### Example 2: E-commerce Data
535
+
536
+ ```typescript
537
+ const driver = await ExcelDriver.create({
538
+ filePath: './shop-data',
539
+ fileStorageMode: 'file-per-object'
540
+ });
541
+
542
+ // Products
543
+ await driver.create('products', {
544
+ name: 'Laptop Pro',
545
+ price: 1299.99,
546
+ category: 'Electronics',
547
+ stock: 50
548
+ });
549
+
550
+ // Orders
551
+ await driver.create('orders', {
552
+ userId: 'user-123',
553
+ productId: 'prod-456',
554
+ quantity: 2,
555
+ total: 2599.98,
556
+ status: 'pending'
557
+ });
558
+
559
+ // Get pending orders
560
+ const pending = await driver.find('orders', {
561
+ filters: [['status', '=', 'pending']],
562
+ sort: [['created_at', 'desc']]
563
+ });
564
+ ```
565
+
566
+ ### Example 3: Data Migration
567
+
568
+ ```typescript
569
+ import { ExcelDriver } from '@objectql/driver-excel';
570
+ import { SQLDriver } from '@objectql/driver-sql';
571
+
572
+ const excelDriver = await ExcelDriver.create({
573
+ filePath: './legacy-data.xlsx'
574
+ });
575
+
576
+ const sqlDriver = new SQLDriver({
577
+ client: 'pg',
578
+ connection: { /* postgres config */ }
579
+ });
580
+
581
+ // Migrate data from Excel to SQL
582
+ const users = await excelDriver.find('users');
583
+ for (const user of users) {
584
+ await sqlDriver.create('users', user);
585
+ }
586
+
587
+ console.log(`Migrated ${users.length} users`);
588
+ ```
589
+
590
+ ## 🔧 Best Practices
591
+
592
+ 1. **Always use async factory**: `await ExcelDriver.create(config)`
593
+ 2. **Enable autoSave**: Prevents data loss on crashes
594
+ 3. **Backup files**: Keep backups of important Excel files
595
+ 4. **Validate data**: Excel doesn't enforce schemas
596
+ 5. **Use batch operations**: Better performance for multiple records
597
+ 6. **Monitor console**: Check warnings about skipped data
598
+ 7. **Version control**: Track Excel files with git (for small files)
599
+ 8. **Choose right mode**: Consider data size and structure
600
+ 9. **Handle errors**: Use try-catch for file operations
601
+ 10. **Clean up**: Call `disconnect()` when done
602
+
603
+ ## 🤝 TypeScript Support
604
+
605
+ Fully typed with TypeScript:
606
+
607
+ ```typescript
608
+ import { ExcelDriver, ExcelDriverConfig, FileStorageMode } from '@objectql/driver-excel';
609
+
610
+ interface User {
611
+ id: string;
612
+ name: string;
613
+ email: string;
614
+ role: 'admin' | 'user';
615
+ }
616
+
617
+ const config: ExcelDriverConfig = {
618
+ filePath: './data.xlsx',
619
+ fileStorageMode: 'single-file',
620
+ autoSave: true
621
+ };
622
+
623
+ const driver: ExcelDriver = await ExcelDriver.create(config);
624
+ const users: User[] = await driver.find('users');
625
+ ```
626
+
627
+ ## 📄 License
628
+
629
+ MIT
630
+
631
+ ## 🔗 Related Packages
632
+
633
+ - [@objectql/types](../foundation/types) - Core ObjectQL types
634
+ - [@objectql/core](../foundation/core) - ObjectQL core engine
635
+ - [@objectql/driver-memory](../memory) - In-memory driver
636
+ - [@objectql/driver-sql](../sql) - SQL database driver
637
+ - [@objectql/driver-mongo](../mongo) - MongoDB driver
638
+
639
+ ## 🙏 Contributing
640
+
641
+ Contributions are welcome! Please read our [Contributing Guide](../../CONTRIBUTING.md) for details.
642
+
643
+ ## 🐛 Issues
644
+
645
+ Found a bug? Have a feature request? Please file an issue on [GitHub Issues](https://github.com/objectstack-ai/objectql/issues).
646
+
647
+ ## 📖 Documentation
648
+
649
+ For complete ObjectQL documentation, visit [objectql.org](https://www.objectql.org).
650
+
651
+ ---
652
+
653
+ Made with ❤️ by the ObjectQL team