@objectstack/driver-memory 4.0.3 → 4.0.5
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 +89 -8
- package/dist/index.d.mts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +31 -6
- package/.turbo/turbo-build.log +0 -22
- package/CHANGELOG.md +0 -609
- package/objectstack.config.ts +0 -260
- package/src/in-memory-strategy.ts +0 -47
- package/src/index.ts +0 -32
- package/src/memory-analytics.test.ts +0 -346
- package/src/memory-analytics.ts +0 -518
- package/src/memory-driver.test.ts +0 -722
- package/src/memory-driver.ts +0 -1201
- package/src/memory-matcher.ts +0 -177
- package/src/persistence/file-adapter.ts +0 -103
- package/src/persistence/index.ts +0 -4
- package/src/persistence/local-storage-adapter.ts +0 -60
- package/src/persistence/persistence.test.ts +0 -298
- package/tsconfig.json +0 -27
package/src/memory-matcher.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Simple In-Memory Query Matcher
|
|
6
|
-
*
|
|
7
|
-
* Implements a subset of the ObjectStack Filter Protocol (MongoDB-compatible)
|
|
8
|
-
* for evaluating conditions against in-memory JavaScript objects.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
type RecordType = Record<string, any>;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* matches - Check if a record matches a filter criteria
|
|
15
|
-
* @param record The data record to check
|
|
16
|
-
* @param filter The filter condition (where clause)
|
|
17
|
-
*/
|
|
18
|
-
export function match(record: RecordType, filter: any): boolean {
|
|
19
|
-
if (!filter || Object.keys(filter).length === 0) return true;
|
|
20
|
-
|
|
21
|
-
// 1. Handle Top-Level Logical Operators ($and, $or, $not)
|
|
22
|
-
// These usually appear at the root or nested.
|
|
23
|
-
|
|
24
|
-
// $and: [ { ... }, { ... } ]
|
|
25
|
-
if (Array.isArray(filter.$and)) {
|
|
26
|
-
if (!filter.$and.every((f: any) => match(record, f))) {
|
|
27
|
-
return false;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// $or: [ { ... }, { ... } ]
|
|
32
|
-
if (Array.isArray(filter.$or)) {
|
|
33
|
-
if (!filter.$or.some((f: any) => match(record, f))) {
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// $not: { ... }
|
|
39
|
-
if (filter.$not) {
|
|
40
|
-
if (match(record, filter.$not)) {
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// 2. Iterate over field constraints
|
|
46
|
-
for (const key of Object.keys(filter)) {
|
|
47
|
-
// Skip logical operators we already handled (or future ones)
|
|
48
|
-
if (key.startsWith('$')) continue;
|
|
49
|
-
|
|
50
|
-
const condition = filter[key];
|
|
51
|
-
const value = getValueByPath(record, key);
|
|
52
|
-
|
|
53
|
-
if (!checkCondition(value, condition)) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Access nested properties via dot-notation (e.g. "user.name")
|
|
63
|
-
*/
|
|
64
|
-
export function getValueByPath(obj: any, path: string): any {
|
|
65
|
-
if (!path.includes('.')) return obj[path];
|
|
66
|
-
return path.split('.').reduce((o, i) => (o ? o[i] : undefined), obj);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* Evaluate a specific condition against a value
|
|
71
|
-
*/
|
|
72
|
-
function checkCondition(value: any, condition: any): boolean {
|
|
73
|
-
// Case A: Implicit Equality (e.g. status: 'active')
|
|
74
|
-
// If condition is a primitive or Date/Array (exact match), treat as equality.
|
|
75
|
-
if (
|
|
76
|
-
typeof condition !== 'object' ||
|
|
77
|
-
condition === null ||
|
|
78
|
-
condition instanceof Date ||
|
|
79
|
-
Array.isArray(condition)
|
|
80
|
-
) {
|
|
81
|
-
// Loose equality to handle undefined/null mismatch or string/number coercion if desired.
|
|
82
|
-
// But stick to == for JS loose equality which is often convenient in weakly typed queries.
|
|
83
|
-
return value == condition;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Case B: Operator Object (e.g. { $gt: 10, $lt: 20 })
|
|
87
|
-
const keys = Object.keys(condition);
|
|
88
|
-
const isOperatorObject = keys.some(k => k.startsWith('$'));
|
|
89
|
-
|
|
90
|
-
if (!isOperatorObject) {
|
|
91
|
-
// It's just a nested object comparison or implicit equality against an object
|
|
92
|
-
// Simplistic check:
|
|
93
|
-
return JSON.stringify(value) === JSON.stringify(condition);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Iterate operators
|
|
97
|
-
for (const op of keys) {
|
|
98
|
-
const target = condition[op];
|
|
99
|
-
|
|
100
|
-
// Handle undefined values
|
|
101
|
-
if (value === undefined && op !== '$exists' && op !== '$ne' && op !== '$null') {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
switch (op) {
|
|
106
|
-
case '$eq':
|
|
107
|
-
if (value != target) return false;
|
|
108
|
-
break;
|
|
109
|
-
case '$ne':
|
|
110
|
-
if (value == target) return false;
|
|
111
|
-
break;
|
|
112
|
-
|
|
113
|
-
// Numeric / Date
|
|
114
|
-
case '$gt':
|
|
115
|
-
if (!(value > target)) return false;
|
|
116
|
-
break;
|
|
117
|
-
case '$gte':
|
|
118
|
-
if (!(value >= target)) return false;
|
|
119
|
-
break;
|
|
120
|
-
case '$lt':
|
|
121
|
-
if (!(value < target)) return false;
|
|
122
|
-
break;
|
|
123
|
-
case '$lte':
|
|
124
|
-
if (!(value <= target)) return false;
|
|
125
|
-
break;
|
|
126
|
-
case '$between':
|
|
127
|
-
// target should be [min, max]
|
|
128
|
-
if (Array.isArray(target) && (value < target[0] || value > target[1])) return false;
|
|
129
|
-
break;
|
|
130
|
-
|
|
131
|
-
// Sets
|
|
132
|
-
case '$in':
|
|
133
|
-
if (!Array.isArray(target) || !target.includes(value)) return false;
|
|
134
|
-
break;
|
|
135
|
-
case '$nin':
|
|
136
|
-
if (Array.isArray(target) && target.includes(value)) return false;
|
|
137
|
-
break;
|
|
138
|
-
|
|
139
|
-
// Existence
|
|
140
|
-
case '$exists':
|
|
141
|
-
const exists = value !== undefined && value !== null;
|
|
142
|
-
if (exists !== !!target) return false;
|
|
143
|
-
break;
|
|
144
|
-
|
|
145
|
-
// Strings
|
|
146
|
-
case '$contains':
|
|
147
|
-
if (typeof value !== 'string' || !value.includes(target)) return false;
|
|
148
|
-
break;
|
|
149
|
-
case '$notContains':
|
|
150
|
-
if (typeof value !== 'string' || value.includes(target)) return false;
|
|
151
|
-
break;
|
|
152
|
-
case '$startsWith':
|
|
153
|
-
if (typeof value !== 'string' || !value.startsWith(target)) return false;
|
|
154
|
-
break;
|
|
155
|
-
case '$endsWith':
|
|
156
|
-
if (typeof value !== 'string' || !value.endsWith(target)) return false;
|
|
157
|
-
break;
|
|
158
|
-
case '$null':
|
|
159
|
-
// $null: true → value must be null/undefined; $null: false → value must not be null/undefined
|
|
160
|
-
if (target === true && value != null) return false;
|
|
161
|
-
if (target === false && value == null) return false;
|
|
162
|
-
break;
|
|
163
|
-
case '$regex':
|
|
164
|
-
try {
|
|
165
|
-
const re = new RegExp(target, condition.$options || '');
|
|
166
|
-
if (!re.test(String(value))) return false;
|
|
167
|
-
} catch (e) { return false; }
|
|
168
|
-
break;
|
|
169
|
-
|
|
170
|
-
default:
|
|
171
|
-
// Unknown operator, ignore or fail. Ignoring safe for optional features.
|
|
172
|
-
break;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return true;
|
|
177
|
-
}
|
|
@@ -1,103 +0,0 @@
|
|
|
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
|
-
}
|
package/src/persistence/index.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,298 +0,0 @@
|
|
|
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
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../../tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"target": "ES2020",
|
|
5
|
-
"module": "ES2020",
|
|
6
|
-
"moduleResolution": "bundler",
|
|
7
|
-
"declaration": true,
|
|
8
|
-
"outDir": "./dist",
|
|
9
|
-
"strict": true,
|
|
10
|
-
"esModuleInterop": true,
|
|
11
|
-
"skipLibCheck": true,
|
|
12
|
-
"noUnusedLocals": false,
|
|
13
|
-
"noUnusedParameters": false,
|
|
14
|
-
"forceConsistentCasingInFileNames": true,
|
|
15
|
-
"types": [
|
|
16
|
-
"node"
|
|
17
|
-
],
|
|
18
|
-
"rootDir": "./src"
|
|
19
|
-
},
|
|
20
|
-
"include": [
|
|
21
|
-
"src/**/*"
|
|
22
|
-
],
|
|
23
|
-
"exclude": [
|
|
24
|
-
"node_modules",
|
|
25
|
-
"dist"
|
|
26
|
-
]
|
|
27
|
-
}
|