@objectstack/driver-memory 3.0.10 → 3.0.11

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.
@@ -6,6 +6,20 @@ import { Logger, createLogger } from '@objectstack/core';
6
6
  import { Query, Aggregator } from 'mingo';
7
7
  import { getValueByPath } from './memory-matcher.js';
8
8
 
9
+ /**
10
+ * Persistence adapter interface.
11
+ * Matches the PersistenceAdapterSchema contract from @objectstack/spec.
12
+ */
13
+ export interface PersistenceAdapterInterface {
14
+ load(): Promise<Record<string, any[]> | null>;
15
+ save(db: Record<string, any[]>): Promise<void>;
16
+ flush(): Promise<void>;
17
+ /** Optional: Start periodic auto-save (used by FileSystemPersistenceAdapter). */
18
+ startAutoSave?(): void;
19
+ /** Optional: Stop auto-save timer and flush pending writes. */
20
+ stopAutoSave?(): Promise<void>;
21
+ }
22
+
9
23
  /**
10
24
  * Configuration options for the InMemory driver.
11
25
  * Aligned with @objectstack/spec MemoryConfigSchema.
@@ -17,6 +31,24 @@ export interface InMemoryDriverConfig {
17
31
  strictMode?: boolean;
18
32
  /** Optional: Logger instance */
19
33
  logger?: Logger;
34
+ /**
35
+ * Persistence configuration. Defaults to `'auto'`.
36
+ * - `'auto'` (default) — Auto-detect environment (browser → localStorage, Node.js → file)
37
+ * - `'file'` — File-system persistence with defaults (Node.js only)
38
+ * - `'local'` — localStorage persistence with defaults (Browser only)
39
+ * - `{ type: 'file', path?: string, autoSaveInterval?: number }` — File-system with options
40
+ * - `{ type: 'local', key?: string }` — localStorage with options
41
+ * - `{ type: 'auto', path?: string, key?: string, autoSaveInterval?: number }` — Auto-detect with options
42
+ * - `{ adapter: PersistenceAdapterInterface }` — Custom adapter
43
+ * - `false` — Disable persistence (pure in-memory)
44
+ */
45
+ persistence?: string | false | {
46
+ type?: 'file' | 'local' | 'auto';
47
+ path?: string;
48
+ key?: string;
49
+ autoSaveInterval?: number;
50
+ adapter?: PersistenceAdapterInterface;
51
+ };
20
52
  }
21
53
 
22
54
  /**
@@ -51,6 +83,7 @@ export class InMemoryDriver implements DriverInterface {
51
83
  private logger: Logger;
52
84
  private idCounters: Map<string, number> = new Map();
53
85
  private transactions: Map<string, MemoryTransaction> = new Map();
86
+ private persistenceAdapter: PersistenceAdapterInterface | null = null;
54
87
 
55
88
  constructor(config?: InMemoryDriverConfig) {
56
89
  this.config = config || {};
@@ -100,6 +133,37 @@ export class InMemoryDriver implements DriverInterface {
100
133
  // ===================================
101
134
 
102
135
  async connect() {
136
+ // Initialize persistence adapter if configured
137
+ await this.initPersistence();
138
+
139
+ // Load persisted data if available
140
+ if (this.persistenceAdapter) {
141
+ const persisted = await this.persistenceAdapter.load();
142
+ if (persisted) {
143
+ for (const [objectName, records] of Object.entries(persisted)) {
144
+ this.db[objectName] = records;
145
+ // Update ID counters based on persisted data
146
+ for (const record of records) {
147
+ if (record.id && typeof record.id === 'string') {
148
+ // ID format: {objectName}-{timestamp}-{counter}
149
+ const parts = record.id.split('-');
150
+ const lastPart = parts[parts.length - 1];
151
+ const counter = parseInt(lastPart, 10);
152
+ if (!isNaN(counter)) {
153
+ const current = this.idCounters.get(objectName) || 0;
154
+ if (counter > current) {
155
+ this.idCounters.set(objectName, counter);
156
+ }
157
+ }
158
+ }
159
+ }
160
+ }
161
+ this.logger.info('InMemory Database restored from persistence', {
162
+ tables: Object.keys(persisted).length,
163
+ });
164
+ }
165
+ }
166
+
103
167
  // Load initial data if provided
104
168
  if (this.config.initialData) {
105
169
  for (const [objectName, records] of Object.entries(this.config.initialData)) {
@@ -115,9 +179,22 @@ export class InMemoryDriver implements DriverInterface {
115
179
  } else {
116
180
  this.logger.info('InMemory Database Connected (Virtual)');
117
181
  }
182
+
183
+ // Start auto-save if using file adapter
184
+ if (this.persistenceAdapter?.startAutoSave) {
185
+ this.persistenceAdapter.startAutoSave();
186
+ }
118
187
  }
119
188
 
120
189
  async disconnect() {
190
+ // Stop auto-save and flush pending writes
191
+ if (this.persistenceAdapter) {
192
+ if (this.persistenceAdapter.stopAutoSave) {
193
+ await this.persistenceAdapter.stopAutoSave();
194
+ }
195
+ await this.persistenceAdapter.flush();
196
+ }
197
+
121
198
  const tableCount = Object.keys(this.db).length;
122
199
  const recordCount = Object.values(this.db).reduce((sum, table) => sum + table.length, 0);
123
200
 
@@ -226,6 +303,7 @@ export class InMemoryDriver implements DriverInterface {
226
303
  };
227
304
 
228
305
  table.push(newRecord);
306
+ this.markDirty();
229
307
  this.logger.debug('Record created', { object, id: newRecord.id, tableSize: table.length });
230
308
  return { ...newRecord };
231
309
  }
@@ -253,6 +331,7 @@ export class InMemoryDriver implements DriverInterface {
253
331
  };
254
332
 
255
333
  table[index] = updatedRecord;
334
+ this.markDirty();
256
335
  this.logger.debug('Record updated', { object, id });
257
336
  return { ...updatedRecord };
258
337
  }
@@ -293,6 +372,7 @@ export class InMemoryDriver implements DriverInterface {
293
372
  }
294
373
 
295
374
  table.splice(index, 1);
375
+ this.markDirty();
296
376
  this.logger.debug('Record deleted', { object, id, tableSize: table.length });
297
377
  return true;
298
378
  }
@@ -350,6 +430,7 @@ export class InMemoryDriver implements DriverInterface {
350
430
  }
351
431
  }
352
432
 
433
+ if (count > 0) this.markDirty();
353
434
  this.logger.debug('UpdateMany completed', { object, count });
354
435
  return { count };
355
436
  }
@@ -377,6 +458,7 @@ export class InMemoryDriver implements DriverInterface {
377
458
  }
378
459
 
379
460
  const count = initialLength - this.db[object].length;
461
+ if (count > 0) this.markDirty();
380
462
  this.logger.debug('DeleteMany completed', { object, count });
381
463
  return { count };
382
464
  }
@@ -435,6 +517,7 @@ export class InMemoryDriver implements DriverInterface {
435
517
  // Restore the snapshot
436
518
  this.db = tx.snapshot;
437
519
  this.transactions.delete(txId);
520
+ this.markDirty();
438
521
  this.logger.debug('Transaction rolled back', { txId });
439
522
  }
440
523
 
@@ -448,6 +531,7 @@ export class InMemoryDriver implements DriverInterface {
448
531
  async clear() {
449
532
  this.db = {};
450
533
  this.idCounters.clear();
534
+ this.markDirty();
451
535
  this.logger.debug('All data cleared');
452
536
  }
453
537
 
@@ -818,4 +902,99 @@ export class InMemoryDriver implements DriverInterface {
818
902
  const timestamp = Date.now();
819
903
  return `${key}-${timestamp}-${counter}`;
820
904
  }
905
+
906
+ // ===================================
907
+ // Persistence
908
+ // ===================================
909
+
910
+ /**
911
+ * Mark the database as dirty, triggering persistence save.
912
+ */
913
+ private markDirty(): void {
914
+ if (this.persistenceAdapter) {
915
+ this.persistenceAdapter.save(this.db);
916
+ }
917
+ }
918
+
919
+ /**
920
+ * Flush pending persistence writes to ensure data is safely stored.
921
+ */
922
+ async flush(): Promise<void> {
923
+ if (this.persistenceAdapter) {
924
+ await this.persistenceAdapter.flush();
925
+ }
926
+ }
927
+
928
+ /**
929
+ * Detect whether the current runtime is a browser environment.
930
+ */
931
+ private isBrowserEnvironment(): boolean {
932
+ return typeof globalThis.localStorage !== 'undefined';
933
+ }
934
+
935
+ /**
936
+ * Initialize the persistence adapter based on configuration.
937
+ * Defaults to 'auto' when persistence is not specified.
938
+ * Use `persistence: false` to explicitly disable persistence.
939
+ */
940
+ private async initPersistence(): Promise<void> {
941
+ const persistence = this.config.persistence === undefined ? 'auto' : this.config.persistence;
942
+ if (persistence === false) return;
943
+
944
+ if (typeof persistence === 'string') {
945
+ if (persistence === 'auto') {
946
+ if (this.isBrowserEnvironment()) {
947
+ const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
948
+ this.persistenceAdapter = new LocalStoragePersistenceAdapter();
949
+ this.logger.debug('Auto-detected browser environment, using localStorage persistence');
950
+ } else {
951
+ const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
952
+ this.persistenceAdapter = new FileSystemPersistenceAdapter();
953
+ this.logger.debug('Auto-detected Node.js environment, using file persistence');
954
+ }
955
+ } else if (persistence === 'file') {
956
+ const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
957
+ this.persistenceAdapter = new FileSystemPersistenceAdapter();
958
+ } else if (persistence === 'local') {
959
+ const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
960
+ this.persistenceAdapter = new LocalStoragePersistenceAdapter();
961
+ } else {
962
+ throw new Error(`Unknown persistence type: "${persistence}". Use 'file', 'local', or 'auto'.`);
963
+ }
964
+ } else if ('adapter' in persistence && persistence.adapter) {
965
+ this.persistenceAdapter = persistence.adapter;
966
+ } else if ('type' in persistence) {
967
+ if (persistence.type === 'auto') {
968
+ if (this.isBrowserEnvironment()) {
969
+ const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
970
+ this.persistenceAdapter = new LocalStoragePersistenceAdapter({
971
+ key: persistence.key,
972
+ });
973
+ this.logger.debug('Auto-detected browser environment, using localStorage persistence');
974
+ } else {
975
+ const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
976
+ this.persistenceAdapter = new FileSystemPersistenceAdapter({
977
+ path: persistence.path,
978
+ autoSaveInterval: persistence.autoSaveInterval,
979
+ });
980
+ this.logger.debug('Auto-detected Node.js environment, using file persistence');
981
+ }
982
+ } else if (persistence.type === 'file') {
983
+ const { FileSystemPersistenceAdapter } = await import('./persistence/file-adapter.js');
984
+ this.persistenceAdapter = new FileSystemPersistenceAdapter({
985
+ path: persistence.path,
986
+ autoSaveInterval: persistence.autoSaveInterval,
987
+ });
988
+ } else if (persistence.type === 'local') {
989
+ const { LocalStoragePersistenceAdapter } = await import('./persistence/local-storage-adapter.js');
990
+ this.persistenceAdapter = new LocalStoragePersistenceAdapter({
991
+ key: persistence.key,
992
+ });
993
+ }
994
+ }
995
+
996
+ if (this.persistenceAdapter) {
997
+ this.logger.debug('Persistence adapter initialized');
998
+ }
999
+ }
821
1000
  }
@@ -0,0 +1,103 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+
6
+ /**
7
+ * FileSystemPersistenceAdapter
8
+ *
9
+ * Persists the in-memory database to a JSON file on disk.
10
+ * Supports atomic writes (write to temp file then rename) and auto-save with dirty tracking.
11
+ *
12
+ * Node.js only — will throw if used in non-Node.js environments.
13
+ */
14
+ export class FileSystemPersistenceAdapter {
15
+ private readonly filePath: string;
16
+ private readonly autoSaveInterval: number;
17
+ private dirty = false;
18
+ private timer: ReturnType<typeof setInterval> | null = null;
19
+ private currentDb: Record<string, any[]> | null = null;
20
+
21
+ constructor(options?: { path?: string; autoSaveInterval?: number }) {
22
+ this.filePath = options?.path || path.join('.objectstack', 'data', 'memory-driver.json');
23
+ this.autoSaveInterval = options?.autoSaveInterval ?? 2000;
24
+ }
25
+
26
+ /**
27
+ * Load persisted data from disk.
28
+ * Returns null if no file exists.
29
+ */
30
+ async load(): Promise<Record<string, any[]> | null> {
31
+ try {
32
+ if (!fs.existsSync(this.filePath)) {
33
+ return null;
34
+ }
35
+ const raw = fs.readFileSync(this.filePath, 'utf-8');
36
+ const data = JSON.parse(raw);
37
+ return data as Record<string, any[]>;
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Save data to disk using atomic write (temp file + rename).
45
+ */
46
+ async save(db: Record<string, any[]>): Promise<void> {
47
+ this.currentDb = db;
48
+ this.dirty = true;
49
+ }
50
+
51
+ /**
52
+ * Flush pending writes to disk immediately.
53
+ */
54
+ async flush(): Promise<void> {
55
+ if (!this.dirty || !this.currentDb) return;
56
+ await this.writeToDisk(this.currentDb);
57
+ this.dirty = false;
58
+ }
59
+
60
+ /**
61
+ * Start the auto-save timer.
62
+ */
63
+ startAutoSave(): void {
64
+ if (this.timer) return;
65
+ this.timer = setInterval(async () => {
66
+ if (this.dirty && this.currentDb) {
67
+ await this.writeToDisk(this.currentDb);
68
+ this.dirty = false;
69
+ }
70
+ }, this.autoSaveInterval);
71
+
72
+ // Allow process to exit even if timer is running
73
+ if (this.timer) {
74
+ this.timer.unref();
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Stop the auto-save timer and flush pending writes.
80
+ */
81
+ async stopAutoSave(): Promise<void> {
82
+ if (this.timer) {
83
+ clearInterval(this.timer);
84
+ this.timer = null;
85
+ }
86
+ await this.flush();
87
+ }
88
+
89
+ /**
90
+ * Atomic write: write to temp file, then rename.
91
+ */
92
+ private async writeToDisk(db: Record<string, any[]>): Promise<void> {
93
+ const dir = path.dirname(this.filePath);
94
+ if (!fs.existsSync(dir)) {
95
+ fs.mkdirSync(dir, { recursive: true });
96
+ }
97
+
98
+ const tmpPath = this.filePath + '.tmp';
99
+ const json = JSON.stringify(db, null, 2);
100
+ fs.writeFileSync(tmpPath, json, 'utf-8');
101
+ fs.renameSync(tmpPath, this.filePath);
102
+ }
103
+ }
@@ -0,0 +1,4 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ export { FileSystemPersistenceAdapter } from './file-adapter.js';
4
+ export { LocalStoragePersistenceAdapter } from './local-storage-adapter.js';
@@ -0,0 +1,60 @@
1
+ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
+
3
+ /**
4
+ * LocalStoragePersistenceAdapter
5
+ *
6
+ * Persists the in-memory database to browser localStorage.
7
+ * Synchronous storage with a ~5MB size limit warning.
8
+ *
9
+ * Browser only — will throw if used in non-browser environments.
10
+ */
11
+ export class LocalStoragePersistenceAdapter {
12
+ private readonly storageKey: string;
13
+ private static readonly SIZE_WARNING_BYTES = 4.5 * 1024 * 1024; // 4.5MB warning threshold
14
+
15
+ constructor(options?: { key?: string }) {
16
+ this.storageKey = options?.key || 'objectstack:memory-db';
17
+ }
18
+
19
+ /**
20
+ * Load persisted data from localStorage.
21
+ * Returns null if no data exists.
22
+ */
23
+ async load(): Promise<Record<string, any[]> | null> {
24
+ try {
25
+ const raw = localStorage.getItem(this.storageKey);
26
+ if (!raw) return null;
27
+ return JSON.parse(raw) as Record<string, any[]>;
28
+ } catch {
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Save data to localStorage.
35
+ * Warns if data size approaches the ~5MB localStorage limit.
36
+ */
37
+ async save(db: Record<string, any[]>): Promise<void> {
38
+ const json = JSON.stringify(db);
39
+
40
+ if (json.length > LocalStoragePersistenceAdapter.SIZE_WARNING_BYTES) {
41
+ console.warn(
42
+ `[ObjectStack] localStorage persistence data size (${(json.length / 1024 / 1024).toFixed(2)}MB) ` +
43
+ `is approaching the ~5MB limit. Consider using a different persistence strategy.`
44
+ );
45
+ }
46
+
47
+ try {
48
+ localStorage.setItem(this.storageKey, json);
49
+ } catch (e: any) {
50
+ console.error('[ObjectStack] Failed to persist data to localStorage:', e?.message || e);
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Flush is a no-op for localStorage (writes are synchronous).
56
+ */
57
+ async flush(): Promise<void> {
58
+ // localStorage writes are synchronous, no flushing needed
59
+ }
60
+ }
@@ -0,0 +1,215 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { InMemoryDriver } from '../memory-driver.js';
3
+ import * as fs from 'node:fs';
4
+ import * as path from 'node:path';
5
+
6
+ const TEST_DATA_DIR = path.join('/tmp', 'objectstack-test-persistence');
7
+ const TEST_FILE_PATH = path.join(TEST_DATA_DIR, 'test-db.json');
8
+
9
+ describe('InMemoryDriver Persistence', () => {
10
+ beforeEach(() => {
11
+ // Clean up test directory
12
+ if (fs.existsSync(TEST_DATA_DIR)) {
13
+ fs.rmSync(TEST_DATA_DIR, { recursive: true });
14
+ }
15
+ });
16
+
17
+ afterEach(() => {
18
+ if (fs.existsSync(TEST_DATA_DIR)) {
19
+ fs.rmSync(TEST_DATA_DIR, { recursive: true });
20
+ }
21
+ });
22
+
23
+ describe('File Persistence', () => {
24
+ it('should persist and restore data via file adapter', async () => {
25
+ // Create and populate driver with file persistence
26
+ const driver1 = new InMemoryDriver({
27
+ persistence: { type: 'file', path: TEST_FILE_PATH, autoSaveInterval: 100 },
28
+ });
29
+ await driver1.connect();
30
+ await driver1.create('users', { id: '1', name: 'Alice' });
31
+ await driver1.create('users', { id: '2', name: 'Bob' });
32
+
33
+ // Flush and disconnect
34
+ await driver1.flush();
35
+ await driver1.disconnect();
36
+
37
+ // Verify file was created
38
+ expect(fs.existsSync(TEST_FILE_PATH)).toBe(true);
39
+
40
+ // Create a new driver and verify data is restored
41
+ const driver2 = new InMemoryDriver({
42
+ persistence: { type: 'file', path: TEST_FILE_PATH, autoSaveInterval: 100 },
43
+ });
44
+ await driver2.connect();
45
+
46
+ const users = await driver2.find('users', { object: 'users' });
47
+ expect(users).toHaveLength(2);
48
+ expect(users[0].name).toBe('Alice');
49
+ expect(users[1].name).toBe('Bob');
50
+
51
+ await driver2.disconnect();
52
+ });
53
+
54
+ it('should support shorthand "file" persistence string', async () => {
55
+ // Use shorthand — just verifies no error is thrown with 'file'
56
+ const driver = new InMemoryDriver({ persistence: 'file' });
57
+ await driver.connect();
58
+ await driver.create('items', { id: '1', name: 'Widget' });
59
+ await driver.disconnect();
60
+ });
61
+
62
+ it('should persist updates and deletes', async () => {
63
+ const driver1 = new InMemoryDriver({
64
+ persistence: { type: 'file', path: TEST_FILE_PATH, autoSaveInterval: 100 },
65
+ });
66
+ await driver1.connect();
67
+
68
+ // Create, update, and delete
69
+ await driver1.create('tasks', { id: '1', title: 'Task A', done: false });
70
+ await driver1.create('tasks', { id: '2', title: 'Task B', done: false });
71
+ await driver1.update('tasks', '1', { done: true });
72
+ await driver1.delete('tasks', '2');
73
+
74
+ await driver1.flush();
75
+ await driver1.disconnect();
76
+
77
+ // Restore
78
+ const driver2 = new InMemoryDriver({
79
+ persistence: { type: 'file', path: TEST_FILE_PATH, autoSaveInterval: 100 },
80
+ });
81
+ await driver2.connect();
82
+
83
+ const tasks = await driver2.find('tasks', { object: 'tasks' });
84
+ expect(tasks).toHaveLength(1);
85
+ expect(tasks[0].id).toBe('1');
86
+ expect(tasks[0].done).toBe(true);
87
+
88
+ await driver2.disconnect();
89
+ });
90
+
91
+ it('should handle missing persistence file gracefully', async () => {
92
+ const driver = new InMemoryDriver({
93
+ persistence: { type: 'file', path: '/tmp/nonexistent/path/db.json' },
94
+ });
95
+ await driver.connect();
96
+
97
+ const users = await driver.find('users', { object: 'users' });
98
+ expect(users).toHaveLength(0);
99
+
100
+ await driver.disconnect();
101
+ });
102
+ });
103
+
104
+ describe('Custom Adapter Persistence', () => {
105
+ it('should use a custom adapter for persistence', async () => {
106
+ const stored: Record<string, any[]> = {};
107
+ const customAdapter = {
108
+ load: async () => Object.keys(stored).length > 0 ? { ...stored } : null,
109
+ save: async (db: Record<string, any[]>) => {
110
+ for (const [k, v] of Object.entries(db)) {
111
+ stored[k] = [...v];
112
+ }
113
+ },
114
+ flush: async () => {},
115
+ };
116
+
117
+ const driver1 = new InMemoryDriver({
118
+ persistence: { adapter: customAdapter },
119
+ });
120
+ await driver1.connect();
121
+ await driver1.create('projects', { id: '1', name: 'Alpha' });
122
+ await driver1.disconnect();
123
+
124
+ // Verify data was saved via custom adapter
125
+ expect(stored.projects).toBeDefined();
126
+ expect(stored.projects).toHaveLength(1);
127
+
128
+ // Restore from custom adapter
129
+ const driver2 = new InMemoryDriver({
130
+ persistence: { adapter: customAdapter },
131
+ });
132
+ await driver2.connect();
133
+ const projects = await driver2.find('projects', { object: 'projects' });
134
+ expect(projects).toHaveLength(1);
135
+ expect(projects[0].name).toBe('Alpha');
136
+
137
+ await driver2.disconnect();
138
+ });
139
+ });
140
+
141
+ describe('Pure Memory (No Persistence)', () => {
142
+ it('should work without persistence when explicitly disabled', async () => {
143
+ const driver = new InMemoryDriver({ persistence: false });
144
+ await driver.connect();
145
+
146
+ await driver.create('items', { id: '1', name: 'Widget' });
147
+ const items = await driver.find('items', { object: 'items' });
148
+ expect(items).toHaveLength(1);
149
+
150
+ await driver.disconnect();
151
+
152
+ // After disconnect, data is gone
153
+ const itemsAfter = await driver.find('items', { object: 'items' });
154
+ expect(itemsAfter).toHaveLength(0);
155
+ });
156
+ });
157
+
158
+ describe('Auto Persistence', () => {
159
+ it('should auto-detect Node.js environment and use file persistence with shorthand', async () => {
160
+ // In Node.js, 'auto' should select file persistence
161
+ const driver = new InMemoryDriver({ persistence: 'auto' });
162
+ await driver.connect();
163
+ await driver.create('items', { id: '1', name: 'Widget' });
164
+ await driver.disconnect();
165
+ });
166
+
167
+ it('should auto-detect Node.js environment and use file persistence with object config', async () => {
168
+ const filePath = path.join(TEST_DATA_DIR, 'auto-test-db.json');
169
+ const driver1 = new InMemoryDriver({
170
+ persistence: { type: 'auto', path: filePath, autoSaveInterval: 100 },
171
+ });
172
+ await driver1.connect();
173
+ await driver1.create('users', { id: '1', name: 'Alice' });
174
+ await driver1.flush();
175
+ await driver1.disconnect();
176
+
177
+ // Verify file was created (Node.js env selects file adapter)
178
+ expect(fs.existsSync(filePath)).toBe(true);
179
+
180
+ // Restore from file
181
+ const driver2 = new InMemoryDriver({
182
+ persistence: { type: 'auto', path: filePath, autoSaveInterval: 100 },
183
+ });
184
+ await driver2.connect();
185
+ const users = await driver2.find('users', { object: 'users' });
186
+ expect(users).toHaveLength(1);
187
+ expect(users[0].name).toBe('Alice');
188
+ await driver2.disconnect();
189
+ });
190
+ });
191
+
192
+ describe('Bulk Operations with Persistence', () => {
193
+ it('should persist bulk creates', async () => {
194
+ const driver1 = new InMemoryDriver({
195
+ persistence: { type: 'file', path: TEST_FILE_PATH, autoSaveInterval: 100 },
196
+ });
197
+ await driver1.connect();
198
+ await driver1.bulkCreate('items', [
199
+ { id: '1', name: 'A' },
200
+ { id: '2', name: 'B' },
201
+ { id: '3', name: 'C' },
202
+ ]);
203
+ await driver1.flush();
204
+ await driver1.disconnect();
205
+
206
+ const driver2 = new InMemoryDriver({
207
+ persistence: { type: 'file', path: TEST_FILE_PATH, autoSaveInterval: 100 },
208
+ });
209
+ await driver2.connect();
210
+ const items = await driver2.find('items', { object: 'items' });
211
+ expect(items).toHaveLength(3);
212
+ await driver2.disconnect();
213
+ });
214
+ });
215
+ });