@objectstack/driver-memory 3.0.10 → 3.1.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +16 -0
- package/dist/index.d.mts +151 -1
- package/dist/index.d.ts +151 -1
- package/dist/index.js +337 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +332 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +4 -1
- package/src/memory-driver.test.ts +1 -1
- package/src/memory-driver.ts +229 -0
- package/src/persistence/file-adapter.ts +103 -0
- package/src/persistence/index.ts +4 -0
- package/src/persistence/local-storage-adapter.ts +60 -0
- package/src/persistence/persistence.test.ts +298 -0
|
@@ -0,0 +1,298 @@
|
|
|
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
|
+
|
|
216
|
+
describe('Serverless Environment Detection', () => {
|
|
217
|
+
const serverlessEnvVars = [
|
|
218
|
+
'VERCEL',
|
|
219
|
+
'VERCEL_ENV',
|
|
220
|
+
'AWS_LAMBDA_FUNCTION_NAME',
|
|
221
|
+
'NETLIFY',
|
|
222
|
+
'FUNCTIONS_WORKER_RUNTIME',
|
|
223
|
+
'K_SERVICE',
|
|
224
|
+
'FUNCTION_TARGET',
|
|
225
|
+
'DENO_DEPLOYMENT_ID',
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
afterEach(() => {
|
|
229
|
+
// Clean up all serverless env vars after each test
|
|
230
|
+
for (const key of serverlessEnvVars) {
|
|
231
|
+
delete process.env[key];
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should disable file persistence in auto mode when VERCEL env is set', async () => {
|
|
236
|
+
process.env.VERCEL = '1';
|
|
237
|
+
const filePath = path.join(TEST_DATA_DIR, 'serverless-test.json');
|
|
238
|
+
const driver = new InMemoryDriver({
|
|
239
|
+
persistence: { type: 'auto', path: filePath },
|
|
240
|
+
});
|
|
241
|
+
await driver.connect();
|
|
242
|
+
await driver.create('items', { id: '1', name: 'Widget' });
|
|
243
|
+
await driver.flush();
|
|
244
|
+
await driver.disconnect();
|
|
245
|
+
|
|
246
|
+
// File should NOT have been created because auto mode skips file persistence in serverless
|
|
247
|
+
expect(fs.existsSync(filePath)).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should disable file persistence in auto shorthand mode when AWS_LAMBDA_FUNCTION_NAME is set', async () => {
|
|
251
|
+
process.env.AWS_LAMBDA_FUNCTION_NAME = 'my-function';
|
|
252
|
+
const driver = new InMemoryDriver({ persistence: 'auto' });
|
|
253
|
+
await driver.connect();
|
|
254
|
+
await driver.create('items', { id: '1', name: 'Widget' });
|
|
255
|
+
|
|
256
|
+
// Should work as pure in-memory without errors
|
|
257
|
+
const items = await driver.find('items', { object: 'items' });
|
|
258
|
+
expect(items).toHaveLength(1);
|
|
259
|
+
|
|
260
|
+
await driver.disconnect();
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should still allow explicit file persistence in serverless if user requests it', async () => {
|
|
264
|
+
process.env.VERCEL = '1';
|
|
265
|
+
const filePath = path.join(TEST_DATA_DIR, 'explicit-file-serverless.json');
|
|
266
|
+
const driver = new InMemoryDriver({
|
|
267
|
+
persistence: { type: 'file', path: filePath, autoSaveInterval: 100 },
|
|
268
|
+
});
|
|
269
|
+
await driver.connect();
|
|
270
|
+
await driver.create('items', { id: '1', name: 'Widget' });
|
|
271
|
+
await driver.flush();
|
|
272
|
+
await driver.disconnect();
|
|
273
|
+
|
|
274
|
+
// Explicit 'file' type should still create the file even in serverless
|
|
275
|
+
expect(fs.existsSync(filePath)).toBe(true);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('should still allow custom adapter in serverless', async () => {
|
|
279
|
+
process.env.NETLIFY = 'true';
|
|
280
|
+
const stored: Record<string, any[]> = {};
|
|
281
|
+
const customAdapter = {
|
|
282
|
+
load: async () => Object.keys(stored).length > 0 ? { ...stored } : null,
|
|
283
|
+
save: async (db: Record<string, any[]>) => {
|
|
284
|
+
for (const [k, v] of Object.entries(db)) { stored[k] = [...v]; }
|
|
285
|
+
},
|
|
286
|
+
flush: async () => {},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const driver = new InMemoryDriver({ persistence: { adapter: customAdapter } });
|
|
290
|
+
await driver.connect();
|
|
291
|
+
await driver.create('items', { id: '1', name: 'Widget' });
|
|
292
|
+
await driver.disconnect();
|
|
293
|
+
|
|
294
|
+
expect(stored.items).toBeDefined();
|
|
295
|
+
expect(stored.items).toHaveLength(1);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
});
|