@unrdf/kgc-probe 26.4.2

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.
@@ -0,0 +1,827 @@
1
+ /**
2
+ * @fileoverview KGC Probe - Storage Backends
3
+ *
4
+ * Three storage implementations:
5
+ * - MemoryStorage: In-process hash map (development)
6
+ * - FileStorage: Filesystem-based (testing)
7
+ * - DatabaseStorage: Structured storage with query support (production)
8
+ *
9
+ * Interface: { set(key, value), get(key), delete(key), query(pattern) }
10
+ *
11
+ * @module @unrdf/kgc-probe/storage
12
+ */
13
+
14
+ import { promises as fs, existsSync, mkdirSync } from 'fs';
15
+ import { join, dirname } from 'path';
16
+ import { randomUUID } from 'crypto';
17
+
18
+ // ============================================================================
19
+ // STORAGE INTERFACE
20
+ // ============================================================================
21
+
22
+ /**
23
+ * @typedef {Object} StorageInterface
24
+ * @property {string} type - Storage type identifier
25
+ * @property {(key: string, value: any) => Promise<void>} set - Set a value
26
+ * @property {(key: string) => Promise<any>} get - Get a value
27
+ * @property {(key: string) => Promise<boolean>} delete - Delete a value
28
+ * @property {(pattern: string) => Promise<Array>} query - Query values by pattern
29
+ * @property {() => Promise<string[]>} keys - List all keys
30
+ * @property {() => Promise<number>} count - Count entries
31
+ * @property {() => Promise<void>} clear - Clear all entries
32
+ */
33
+
34
+ // ============================================================================
35
+ // MEMORY STORAGE
36
+ // ============================================================================
37
+
38
+ /**
39
+ * MemoryStorage - In-process storage using Map
40
+ *
41
+ * Use for: Development, testing, single-process deployments
42
+ * @implements {StorageInterface}
43
+ */
44
+ export class MemoryStorage {
45
+ /**
46
+ *
47
+ */
48
+ constructor() {
49
+ /** @type {string} */
50
+ this.type = 'memory';
51
+ /** @type {Map<string, any>} */
52
+ this.store = new Map();
53
+ /** @type {Map<string, number>} */
54
+ this.timestamps = new Map();
55
+ }
56
+
57
+ /**
58
+ * Set a value with key
59
+ * @param {string} key - Storage key
60
+ * @param {any} value - Value to store
61
+ * @returns {Promise<void>}
62
+ */
63
+ async set(key, value) {
64
+ if (typeof key !== 'string' || key.length === 0) {
65
+ throw new Error('Key must be a non-empty string');
66
+ }
67
+ this.store.set(key, structuredClone(value));
68
+ this.timestamps.set(key, Date.now());
69
+ }
70
+
71
+ /**
72
+ * Get a value by key
73
+ * @param {string} key - Storage key
74
+ * @returns {Promise<any>} Stored value or undefined
75
+ */
76
+ async get(key) {
77
+ if (!this.store.has(key)) {
78
+ return undefined;
79
+ }
80
+ return structuredClone(this.store.get(key));
81
+ }
82
+
83
+ /**
84
+ * Delete a value by key
85
+ * @param {string} key - Storage key
86
+ * @returns {Promise<boolean>} True if deleted
87
+ */
88
+ async delete(key) {
89
+ this.timestamps.delete(key);
90
+ return this.store.delete(key);
91
+ }
92
+
93
+ /**
94
+ * Query values by pattern (glob-style matching)
95
+ * @param {string} pattern - Pattern to match (supports * wildcard)
96
+ * @returns {Promise<Array<{key: string, value: any}>>} Matching entries
97
+ */
98
+ async query(pattern) {
99
+ const results = [];
100
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
101
+
102
+ for (const [key, value] of this.store) {
103
+ if (regex.test(key)) {
104
+ results.push({ key, value: structuredClone(value) });
105
+ }
106
+ }
107
+
108
+ return results;
109
+ }
110
+
111
+ /**
112
+ * List all keys
113
+ * @returns {Promise<string[]>}
114
+ */
115
+ async keys() {
116
+ return Array.from(this.store.keys());
117
+ }
118
+
119
+ /**
120
+ * Count entries
121
+ * @returns {Promise<number>}
122
+ */
123
+ async count() {
124
+ return this.store.size;
125
+ }
126
+
127
+ /**
128
+ * Clear all entries
129
+ * @returns {Promise<void>}
130
+ */
131
+ async clear() {
132
+ this.store.clear();
133
+ this.timestamps.clear();
134
+ }
135
+
136
+ /**
137
+ * Check if key exists
138
+ * @param {string} key - Storage key
139
+ * @returns {Promise<boolean>}
140
+ */
141
+ async has(key) {
142
+ return this.store.has(key);
143
+ }
144
+
145
+ // Artifact API (backward compatibility)
146
+
147
+ /**
148
+ * Save artifact to memory
149
+ * @param {Object} artifact - Artifact to save
150
+ * @returns {Promise<void>}
151
+ */
152
+ async saveArtifact(artifact) {
153
+ const key = artifact.probe_run_id || artifact.id || randomUUID();
154
+ await this.set(`artifact:${key}`, artifact);
155
+ }
156
+
157
+ /**
158
+ * Load artifact from memory
159
+ * @param {string} artifactId - Artifact ID
160
+ * @returns {Promise<Object>}
161
+ */
162
+ async loadArtifact(artifactId) {
163
+ const artifact = await this.get(`artifact:${artifactId}`);
164
+ if (!artifact) {
165
+ throw new Error(`Artifact not found: ${artifactId}`);
166
+ }
167
+ return artifact;
168
+ }
169
+
170
+ /**
171
+ * Fetch all artifacts (shards)
172
+ * @returns {Promise<Array>}
173
+ */
174
+ async fetchShards() {
175
+ const results = await this.query('artifact:*');
176
+ return results.map(r => r.value);
177
+ }
178
+
179
+ /**
180
+ * List artifact IDs
181
+ * @returns {Promise<string[]>}
182
+ */
183
+ async listArtifacts() {
184
+ const keys = await this.keys();
185
+ return keys
186
+ .filter(k => k.startsWith('artifact:'))
187
+ .map(k => k.replace('artifact:', ''));
188
+ }
189
+
190
+ /**
191
+ * Delete artifact
192
+ * @param {string} artifactId - Artifact ID
193
+ * @returns {Promise<boolean>}
194
+ */
195
+ async deleteArtifact(artifactId) {
196
+ return this.delete(`artifact:${artifactId}`);
197
+ }
198
+ }
199
+
200
+ // ============================================================================
201
+ // FILE STORAGE
202
+ // ============================================================================
203
+
204
+ /**
205
+ * FileStorage - Filesystem-based storage
206
+ *
207
+ * Use for: Testing, single-node deployment, audit trail
208
+ * Structure:
209
+ * <rootDir>/
210
+ * <key>.json
211
+ * ...
212
+ * @implements {StorageInterface}
213
+ */
214
+ export class FileStorage {
215
+ /**
216
+ * Create file storage
217
+ * @param {string} [rootDir] - Root directory for storage
218
+ */
219
+ constructor(rootDir = './storage') {
220
+ /** @type {string} */
221
+ this.type = 'file';
222
+ /** @type {string} */
223
+ this.rootDir = rootDir;
224
+ this._ensureDir();
225
+ }
226
+
227
+ /**
228
+ * Ensure root directory exists
229
+ * @private
230
+ */
231
+ _ensureDir() {
232
+ if (!existsSync(this.rootDir)) {
233
+ mkdirSync(this.rootDir, { recursive: true });
234
+ }
235
+ }
236
+
237
+ /**
238
+ * Get file path for key
239
+ * @param {string} key - Storage key
240
+ * @returns {string}
241
+ * @private
242
+ */
243
+ _getPath(key) {
244
+ // Sanitize key for filesystem
245
+ const sanitized = key.replace(/[^a-zA-Z0-9_-]/g, '_');
246
+ return join(this.rootDir, `${sanitized}.json`);
247
+ }
248
+
249
+ /**
250
+ * Set a value with key
251
+ * @param {string} key - Storage key
252
+ * @param {any} value - Value to store
253
+ * @returns {Promise<void>}
254
+ */
255
+ async set(key, value) {
256
+ if (typeof key !== 'string' || key.length === 0) {
257
+ throw new Error('Key must be a non-empty string');
258
+ }
259
+ this._ensureDir();
260
+ const path = this._getPath(key);
261
+ const data = {
262
+ key,
263
+ value,
264
+ timestamp: Date.now(),
265
+ version: 1
266
+ };
267
+ await fs.writeFile(path, JSON.stringify(data, null, 2), 'utf8');
268
+ }
269
+
270
+ /**
271
+ * Get a value by key
272
+ * @param {string} key - Storage key
273
+ * @returns {Promise<any>}
274
+ */
275
+ async get(key) {
276
+ const path = this._getPath(key);
277
+ try {
278
+ const content = await fs.readFile(path, 'utf8');
279
+ const data = JSON.parse(content);
280
+ return data.value;
281
+ } catch (err) {
282
+ if (err.code === 'ENOENT') {
283
+ return undefined;
284
+ }
285
+ throw err;
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Delete a value by key
291
+ * @param {string} key - Storage key
292
+ * @returns {Promise<boolean>}
293
+ */
294
+ async delete(key) {
295
+ const path = this._getPath(key);
296
+ try {
297
+ await fs.unlink(path);
298
+ return true;
299
+ } catch (err) {
300
+ if (err.code === 'ENOENT') {
301
+ return false;
302
+ }
303
+ throw err;
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Query values by pattern
309
+ * @param {string} pattern - Pattern to match
310
+ * @returns {Promise<Array<{key: string, value: any}>>}
311
+ */
312
+ async query(pattern) {
313
+ const results = [];
314
+ const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
315
+
316
+ try {
317
+ const files = await fs.readdir(this.rootDir);
318
+ for (const file of files) {
319
+ if (!file.endsWith('.json')) continue;
320
+
321
+ try {
322
+ const content = await fs.readFile(join(this.rootDir, file), 'utf8');
323
+ const data = JSON.parse(content);
324
+ if (regex.test(data.key)) {
325
+ results.push({ key: data.key, value: data.value });
326
+ }
327
+ } catch {
328
+ // Skip invalid files
329
+ }
330
+ }
331
+ } catch (err) {
332
+ if (err.code !== 'ENOENT') throw err;
333
+ }
334
+
335
+ return results;
336
+ }
337
+
338
+ /**
339
+ * List all keys
340
+ * @returns {Promise<string[]>}
341
+ */
342
+ async keys() {
343
+ const allKeys = [];
344
+ try {
345
+ const files = await fs.readdir(this.rootDir);
346
+ for (const file of files) {
347
+ if (!file.endsWith('.json')) continue;
348
+ try {
349
+ const content = await fs.readFile(join(this.rootDir, file), 'utf8');
350
+ const data = JSON.parse(content);
351
+ allKeys.push(data.key);
352
+ } catch {
353
+ // Skip invalid files
354
+ }
355
+ }
356
+ } catch (err) {
357
+ if (err.code !== 'ENOENT') throw err;
358
+ }
359
+ return allKeys;
360
+ }
361
+
362
+ /**
363
+ * Count entries
364
+ * @returns {Promise<number>}
365
+ */
366
+ async count() {
367
+ const keys = await this.keys();
368
+ return keys.length;
369
+ }
370
+
371
+ /**
372
+ * Clear all entries
373
+ * @returns {Promise<void>}
374
+ */
375
+ async clear() {
376
+ try {
377
+ const files = await fs.readdir(this.rootDir);
378
+ for (const file of files) {
379
+ if (file.endsWith('.json')) {
380
+ await fs.unlink(join(this.rootDir, file));
381
+ }
382
+ }
383
+ } catch (err) {
384
+ if (err.code !== 'ENOENT') throw err;
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Check if key exists
390
+ * @param {string} key - Storage key
391
+ * @returns {Promise<boolean>}
392
+ */
393
+ async has(key) {
394
+ const path = this._getPath(key);
395
+ return existsSync(path);
396
+ }
397
+
398
+ // Artifact API (backward compatibility)
399
+
400
+ /**
401
+ * Save artifact to file
402
+ * @param {Object} artifact - Artifact to save
403
+ * @returns {Promise<void>}
404
+ */
405
+ async saveArtifact(artifact) {
406
+ const key = artifact.probe_run_id || artifact.id || randomUUID();
407
+ await this.set(`artifact:${key}`, artifact);
408
+ }
409
+
410
+ /**
411
+ * Load artifact from file
412
+ * @param {string} artifactId - Artifact ID
413
+ * @returns {Promise<Object>}
414
+ */
415
+ async loadArtifact(artifactId) {
416
+ const artifact = await this.get(`artifact:${artifactId}`);
417
+ if (!artifact) {
418
+ throw new Error(`Artifact not found: ${artifactId}`);
419
+ }
420
+ return artifact;
421
+ }
422
+
423
+ /**
424
+ * Fetch all artifacts
425
+ * @returns {Promise<Array>}
426
+ */
427
+ async fetchShards() {
428
+ const results = await this.query('artifact:*');
429
+ return results.map(r => r.value);
430
+ }
431
+
432
+ /**
433
+ * List artifact IDs
434
+ * @returns {Promise<string[]>}
435
+ */
436
+ async listArtifacts() {
437
+ const keys = await this.keys();
438
+ return keys
439
+ .filter(k => k.startsWith('artifact:'))
440
+ .map(k => k.replace('artifact:', ''));
441
+ }
442
+
443
+ /**
444
+ * Delete artifact
445
+ * @param {string} artifactId - Artifact ID
446
+ * @returns {Promise<boolean>}
447
+ */
448
+ async deleteArtifact(artifactId) {
449
+ return this.delete(`artifact:${artifactId}`);
450
+ }
451
+ }
452
+
453
+ // ============================================================================
454
+ // DATABASE STORAGE
455
+ // ============================================================================
456
+
457
+ /**
458
+ * DatabaseStorage - In-memory database simulation with advanced querying
459
+ *
460
+ * Use for: Production, distributed deployments
461
+ * Provides: Indexing, range queries, transactions
462
+ * @implements {StorageInterface}
463
+ */
464
+ export class DatabaseStorage {
465
+ /**
466
+ * Create database storage
467
+ * @param {Object} [options] - Configuration
468
+ * @param {string} [options.namespace] - Namespace prefix
469
+ */
470
+ constructor(options = {}) {
471
+ /** @type {string} */
472
+ this.type = 'database';
473
+ /** @type {string} */
474
+ this.namespace = options.namespace || 'probe';
475
+ /** @type {Map<string, any>} */
476
+ this._data = new Map();
477
+ /** @type {Map<string, Map<string, Set<string>>>} */
478
+ this._indices = new Map();
479
+ /** @type {Map<string, number>} */
480
+ this._timestamps = new Map();
481
+ /** @type {number} */
482
+ this._version = 0;
483
+ }
484
+
485
+ /**
486
+ * Create index on field
487
+ * @param {string} field - Field name to index
488
+ * @returns {void}
489
+ */
490
+ createIndex(field) {
491
+ if (!this._indices.has(field)) {
492
+ this._indices.set(field, new Map());
493
+ }
494
+ }
495
+
496
+ /**
497
+ * Update indices for a record
498
+ * @param {string} key - Record key
499
+ * @param {any} value - Record value
500
+ * @private
501
+ */
502
+ _updateIndices(key, value) {
503
+ if (typeof value !== 'object' || value === null) return;
504
+
505
+ for (const [field, index] of this._indices) {
506
+ const fieldValue = value[field];
507
+ if (fieldValue !== undefined) {
508
+ const strValue = String(fieldValue);
509
+ if (!index.has(strValue)) {
510
+ index.set(strValue, new Set());
511
+ }
512
+ index.get(strValue).add(key);
513
+ }
514
+ }
515
+ }
516
+
517
+ /**
518
+ * Remove from indices
519
+ * @param {string} key - Record key
520
+ * @private
521
+ */
522
+ _removeFromIndices(key) {
523
+ for (const index of this._indices.values()) {
524
+ for (const keySet of index.values()) {
525
+ keySet.delete(key);
526
+ }
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Set a value with key
532
+ * @param {string} key - Storage key
533
+ * @param {any} value - Value to store
534
+ * @returns {Promise<void>}
535
+ */
536
+ async set(key, value) {
537
+ if (typeof key !== 'string' || key.length === 0) {
538
+ throw new Error('Key must be a non-empty string');
539
+ }
540
+
541
+ const prefixedKey = `${this.namespace}:${key}`;
542
+ this._removeFromIndices(prefixedKey);
543
+ this._data.set(prefixedKey, structuredClone(value));
544
+ this._timestamps.set(prefixedKey, Date.now());
545
+ this._updateIndices(prefixedKey, value);
546
+ this._version++;
547
+ }
548
+
549
+ /**
550
+ * Get a value by key
551
+ * @param {string} key - Storage key
552
+ * @returns {Promise<any>}
553
+ */
554
+ async get(key) {
555
+ const prefixedKey = `${this.namespace}:${key}`;
556
+ if (!this._data.has(prefixedKey)) {
557
+ return undefined;
558
+ }
559
+ return structuredClone(this._data.get(prefixedKey));
560
+ }
561
+
562
+ /**
563
+ * Delete a value by key
564
+ * @param {string} key - Storage key
565
+ * @returns {Promise<boolean>}
566
+ */
567
+ async delete(key) {
568
+ const prefixedKey = `${this.namespace}:${key}`;
569
+ this._removeFromIndices(prefixedKey);
570
+ this._timestamps.delete(prefixedKey);
571
+ this._version++;
572
+ return this._data.delete(prefixedKey);
573
+ }
574
+
575
+ /**
576
+ * Query values by pattern or criteria
577
+ * @param {string|Object} patternOrCriteria - Pattern string or criteria object
578
+ * @returns {Promise<Array<{key: string, value: any}>>}
579
+ */
580
+ async query(patternOrCriteria) {
581
+ const results = [];
582
+ const prefix = `${this.namespace}:`;
583
+
584
+ if (typeof patternOrCriteria === 'string') {
585
+ // Pattern query
586
+ const regex = new RegExp('^' + prefix + patternOrCriteria.replace(/\*/g, '.*') + '$');
587
+ for (const [key, value] of this._data) {
588
+ if (regex.test(key)) {
589
+ results.push({
590
+ key: key.replace(prefix, ''),
591
+ value: structuredClone(value)
592
+ });
593
+ }
594
+ }
595
+ } else if (typeof patternOrCriteria === 'object') {
596
+ // Criteria query
597
+ const criteria = patternOrCriteria;
598
+
599
+ // Check if we can use an index
600
+ let candidateKeys = null;
601
+ for (const [field, value] of Object.entries(criteria)) {
602
+ if (this._indices.has(field)) {
603
+ const index = this._indices.get(field);
604
+ const matchingKeys = index.get(String(value));
605
+ if (matchingKeys) {
606
+ if (candidateKeys === null) {
607
+ candidateKeys = new Set(matchingKeys);
608
+ } else {
609
+ // Intersection
610
+ candidateKeys = new Set([...candidateKeys].filter(k => matchingKeys.has(k)));
611
+ }
612
+ } else {
613
+ candidateKeys = new Set();
614
+ break;
615
+ }
616
+ }
617
+ }
618
+
619
+ // Filter candidates or scan all
620
+ const keysToCheck = candidateKeys || this._data.keys();
621
+ for (const key of keysToCheck) {
622
+ const value = this._data.get(key);
623
+ if (!value) continue;
624
+
625
+ let matches = true;
626
+ for (const [field, expected] of Object.entries(criteria)) {
627
+ if (value[field] !== expected) {
628
+ matches = false;
629
+ break;
630
+ }
631
+ }
632
+
633
+ if (matches) {
634
+ results.push({
635
+ key: key.replace(prefix, ''),
636
+ value: structuredClone(value)
637
+ });
638
+ }
639
+ }
640
+ }
641
+
642
+ return results;
643
+ }
644
+
645
+ /**
646
+ * List all keys
647
+ * @returns {Promise<string[]>}
648
+ */
649
+ async keys() {
650
+ const prefix = `${this.namespace}:`;
651
+ return Array.from(this._data.keys())
652
+ .filter(k => k.startsWith(prefix))
653
+ .map(k => k.replace(prefix, ''));
654
+ }
655
+
656
+ /**
657
+ * Count entries
658
+ * @returns {Promise<number>}
659
+ */
660
+ async count() {
661
+ const keys = await this.keys();
662
+ return keys.length;
663
+ }
664
+
665
+ /**
666
+ * Clear all entries
667
+ * @returns {Promise<void>}
668
+ */
669
+ async clear() {
670
+ const prefix = `${this.namespace}:`;
671
+ for (const key of this._data.keys()) {
672
+ if (key.startsWith(prefix)) {
673
+ this._data.delete(key);
674
+ this._timestamps.delete(key);
675
+ }
676
+ }
677
+ for (const index of this._indices.values()) {
678
+ index.clear();
679
+ }
680
+ this._version++;
681
+ }
682
+
683
+ /**
684
+ * Check if key exists
685
+ * @param {string} key - Storage key
686
+ * @returns {Promise<boolean>}
687
+ */
688
+ async has(key) {
689
+ const prefixedKey = `${this.namespace}:${key}`;
690
+ return this._data.has(prefixedKey);
691
+ }
692
+
693
+ /**
694
+ * Get database version (for change detection)
695
+ * @returns {number}
696
+ */
697
+ getVersion() {
698
+ return this._version;
699
+ }
700
+
701
+ /**
702
+ * Batch set multiple values
703
+ * @param {Array<{key: string, value: any}>} entries - Entries to set
704
+ * @returns {Promise<void>}
705
+ */
706
+ async batchSet(entries) {
707
+ for (const { key, value } of entries) {
708
+ await this.set(key, value);
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Batch get multiple values
714
+ * @param {string[]} keys - Keys to get
715
+ * @returns {Promise<Map<string, any>>}
716
+ */
717
+ async batchGet(keys) {
718
+ const results = new Map();
719
+ for (const key of keys) {
720
+ results.set(key, await this.get(key));
721
+ }
722
+ return results;
723
+ }
724
+
725
+ // Artifact API (backward compatibility)
726
+
727
+ /**
728
+ * Save artifact
729
+ * @param {Object} artifact - Artifact to save
730
+ * @returns {Promise<void>}
731
+ */
732
+ async saveArtifact(artifact) {
733
+ const key = artifact.probe_run_id || artifact.id || randomUUID();
734
+ await this.set(`artifact:${key}`, artifact);
735
+ }
736
+
737
+ /**
738
+ * Load artifact
739
+ * @param {string} artifactId - Artifact ID
740
+ * @returns {Promise<Object>}
741
+ */
742
+ async loadArtifact(artifactId) {
743
+ const artifact = await this.get(`artifact:${artifactId}`);
744
+ if (!artifact) {
745
+ throw new Error(`Artifact not found: ${artifactId}`);
746
+ }
747
+ return artifact;
748
+ }
749
+
750
+ /**
751
+ * Fetch all artifacts
752
+ * @returns {Promise<Array>}
753
+ */
754
+ async fetchShards() {
755
+ const results = await this.query('artifact:*');
756
+ return results.map(r => r.value);
757
+ }
758
+
759
+ /**
760
+ * List artifact IDs
761
+ * @returns {Promise<string[]>}
762
+ */
763
+ async listArtifacts() {
764
+ const keys = await this.keys();
765
+ return keys
766
+ .filter(k => k.startsWith('artifact:'))
767
+ .map(k => k.replace('artifact:', ''));
768
+ }
769
+
770
+ /**
771
+ * Delete artifact
772
+ * @param {string} artifactId - Artifact ID
773
+ * @returns {Promise<boolean>}
774
+ */
775
+ async deleteArtifact(artifactId) {
776
+ return this.delete(`artifact:${artifactId}`);
777
+ }
778
+ }
779
+
780
+ // ============================================================================
781
+ // FACTORY FUNCTIONS
782
+ // ============================================================================
783
+
784
+ /**
785
+ * Create memory storage
786
+ * @returns {MemoryStorage}
787
+ */
788
+ export function createMemoryStorage() {
789
+ return new MemoryStorage();
790
+ }
791
+
792
+ /**
793
+ * Create file storage
794
+ * @param {string} [rootDir] - Root directory
795
+ * @returns {FileStorage}
796
+ */
797
+ export function createFileStorage(rootDir = './storage') {
798
+ return new FileStorage(rootDir);
799
+ }
800
+
801
+ /**
802
+ * Create database storage
803
+ * @param {Object} [options] - Configuration
804
+ * @returns {DatabaseStorage}
805
+ */
806
+ export function createDatabaseStorage(options = {}) {
807
+ return new DatabaseStorage(options);
808
+ }
809
+
810
+ /**
811
+ * Create storage by type
812
+ * @param {'memory' | 'file' | 'database'} type - Storage type
813
+ * @param {Object} [options] - Configuration
814
+ * @returns {MemoryStorage | FileStorage | DatabaseStorage}
815
+ */
816
+ export function createStorage(type, options = {}) {
817
+ switch (type) {
818
+ case 'memory':
819
+ return createMemoryStorage();
820
+ case 'file':
821
+ return createFileStorage(options.rootDir);
822
+ case 'database':
823
+ return createDatabaseStorage(options);
824
+ default:
825
+ throw new Error(`Unknown storage type: ${type}`);
826
+ }
827
+ }