@bod.ee/db 0.7.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/.claude/settings.local.json +23 -0
- package/.claude/skills/config-file.md +54 -0
- package/.claude/skills/deploying-bod-db.md +29 -0
- package/.claude/skills/developing-bod-db.md +127 -0
- package/.claude/skills/using-bod-db.md +403 -0
- package/CLAUDE.md +110 -0
- package/README.md +252 -0
- package/admin/rules.ts +12 -0
- package/admin/server.ts +523 -0
- package/admin/ui.html +2281 -0
- package/cli.ts +177 -0
- package/client.ts +2 -0
- package/config.ts +20 -0
- package/deploy/.env.example +1 -0
- package/deploy/base.yaml +18 -0
- package/deploy/boddb-logs.yaml +10 -0
- package/deploy/boddb.yaml +10 -0
- package/deploy/demo.html +196 -0
- package/deploy/deploy.ts +32 -0
- package/deploy/prod-logs.config.ts +15 -0
- package/deploy/prod.config.ts +15 -0
- package/index.ts +20 -0
- package/mcp.ts +78 -0
- package/package.json +29 -0
- package/react.ts +1 -0
- package/src/client/BodClient.ts +515 -0
- package/src/react/hooks.ts +121 -0
- package/src/server/BodDB.ts +319 -0
- package/src/server/ExpressionRules.ts +250 -0
- package/src/server/FTSEngine.ts +76 -0
- package/src/server/FileAdapter.ts +116 -0
- package/src/server/MCPAdapter.ts +409 -0
- package/src/server/MQEngine.ts +286 -0
- package/src/server/QueryEngine.ts +45 -0
- package/src/server/RulesEngine.ts +108 -0
- package/src/server/StorageEngine.ts +464 -0
- package/src/server/StreamEngine.ts +320 -0
- package/src/server/SubscriptionEngine.ts +120 -0
- package/src/server/Transport.ts +479 -0
- package/src/server/VectorEngine.ts +115 -0
- package/src/shared/errors.ts +15 -0
- package/src/shared/pathUtils.ts +94 -0
- package/src/shared/protocol.ts +59 -0
- package/src/shared/transforms.ts +99 -0
- package/tests/batch.test.ts +60 -0
- package/tests/bench.ts +205 -0
- package/tests/e2e.test.ts +284 -0
- package/tests/expression-rules.test.ts +114 -0
- package/tests/file-adapter.test.ts +57 -0
- package/tests/fts.test.ts +58 -0
- package/tests/mq-flow.test.ts +204 -0
- package/tests/mq.test.ts +326 -0
- package/tests/push.test.ts +55 -0
- package/tests/query.test.ts +60 -0
- package/tests/rules.test.ts +78 -0
- package/tests/sse.test.ts +78 -0
- package/tests/storage.test.ts +199 -0
- package/tests/stream.test.ts +385 -0
- package/tests/stress.test.ts +202 -0
- package/tests/subscriptions.test.ts +86 -0
- package/tests/transforms.test.ts +92 -0
- package/tests/transport.test.ts +209 -0
- package/tests/ttl.test.ts +70 -0
- package/tests/vector.test.ts +69 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { BodDB } from '../src/server/BodDB.ts';
|
|
3
|
+
import { increment, serverTimestamp, arrayUnion, arrayRemove, ref } from '../src/shared/transforms.ts';
|
|
4
|
+
|
|
5
|
+
describe('Transforms & Sentinels', () => {
|
|
6
|
+
test('increment creates/increments a number', () => {
|
|
7
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
8
|
+
db.set('counter', 0);
|
|
9
|
+
db.set('counter', increment(5));
|
|
10
|
+
expect(db.get('counter')).toBe(5);
|
|
11
|
+
db.set('counter', increment(-2));
|
|
12
|
+
expect(db.get('counter')).toBe(3);
|
|
13
|
+
// Increment on missing value starts from 0
|
|
14
|
+
db.set('new_counter', increment(1));
|
|
15
|
+
expect(db.get('new_counter')).toBe(1);
|
|
16
|
+
db.close();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('serverTimestamp sets current time', () => {
|
|
20
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
21
|
+
const before = Date.now();
|
|
22
|
+
db.set('ts', serverTimestamp());
|
|
23
|
+
const after = Date.now();
|
|
24
|
+
const val = db.get('ts') as number;
|
|
25
|
+
expect(val).toBeGreaterThanOrEqual(before);
|
|
26
|
+
expect(val).toBeLessThanOrEqual(after);
|
|
27
|
+
db.close();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('arrayUnion adds unique items', () => {
|
|
31
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
32
|
+
db.set('tags', ['a', 'b']);
|
|
33
|
+
db.set('tags', arrayUnion('b', 'c'));
|
|
34
|
+
expect(db.get('tags')).toEqual(['a', 'b', 'c']);
|
|
35
|
+
db.close();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('arrayRemove removes items', () => {
|
|
39
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
40
|
+
db.set('tags', ['a', 'b', 'c']);
|
|
41
|
+
db.set('tags', arrayRemove('b'));
|
|
42
|
+
expect(db.get('tags')).toEqual(['a', 'c']);
|
|
43
|
+
db.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('nested sentinels in objects', () => {
|
|
47
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
48
|
+
db.set('user/stats', { views: 10, likes: 5 });
|
|
49
|
+
db.set('user/stats', { views: increment(1), likes: increment(2) });
|
|
50
|
+
expect(db.get('user/stats')).toEqual({ views: 11, likes: 7 });
|
|
51
|
+
db.close();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('sentinels work with merge', () => {
|
|
55
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
56
|
+
db.set('item', { count: 3, name: 'test' });
|
|
57
|
+
db.update({ 'item': { count: increment(1) } });
|
|
58
|
+
expect(db.get('item/count')).toBe(4);
|
|
59
|
+
expect(db.get('item/name')).toBe('test');
|
|
60
|
+
db.close();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('ref sentinel stores _ref marker', () => {
|
|
64
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
65
|
+
db.set('users/u1', { name: 'Alice' });
|
|
66
|
+
db.set('posts/p1', { author: ref('users/u1'), title: 'Hello' });
|
|
67
|
+
const post = db.get('posts/p1') as Record<string, unknown>;
|
|
68
|
+
expect(post.author).toEqual({ _ref: 'users/u1' });
|
|
69
|
+
db.close();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('ref resolution via storage.get with resolve option', () => {
|
|
73
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
74
|
+
db.set('users/u1', { name: 'Alice' });
|
|
75
|
+
db.set('posts/p1', { author: ref('users/u1'), title: 'Hello' });
|
|
76
|
+
const post = db.storage.get('posts/p1', { resolve: true }) as Record<string, unknown>;
|
|
77
|
+
expect(post.author).toEqual({ name: 'Alice' });
|
|
78
|
+
expect(post.title).toBe('Hello');
|
|
79
|
+
db.close();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('ref resolution with specific fields', () => {
|
|
83
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
84
|
+
db.set('users/u1', { name: 'Alice' });
|
|
85
|
+
db.set('cats/c1', { name: 'Whiskers' });
|
|
86
|
+
db.set('posts/p1', { author: ref('users/u1'), cat: ref('cats/c1') });
|
|
87
|
+
const post = db.storage.get('posts/p1', { resolve: ['author'] }) as Record<string, unknown>;
|
|
88
|
+
expect(post.author).toEqual({ name: 'Alice' });
|
|
89
|
+
expect(post.cat).toEqual({ _ref: 'cats/c1' }); // Not resolved
|
|
90
|
+
db.close();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { describe, test, expect, afterEach } from 'bun:test';
|
|
2
|
+
import { BodDB } from '../src/server/BodDB.ts';
|
|
3
|
+
|
|
4
|
+
let db: BodDB;
|
|
5
|
+
|
|
6
|
+
function setup(rules?: Record<string, any>) {
|
|
7
|
+
db = new BodDB({ rules, port: 0 }); // port 0 = random
|
|
8
|
+
const server = db.serve();
|
|
9
|
+
return server.port;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
db?.close();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
function ws(port: number): Promise<WebSocket> {
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const socket = new WebSocket(`ws://localhost:${port}`);
|
|
19
|
+
socket.onopen = () => resolve(socket);
|
|
20
|
+
socket.onerror = (e) => reject(e);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function send(socket: WebSocket, msg: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
25
|
+
return new Promise((resolve) => {
|
|
26
|
+
const handler = (e: MessageEvent) => {
|
|
27
|
+
const data = JSON.parse(e.data);
|
|
28
|
+
if (data.id === msg.id) {
|
|
29
|
+
socket.removeEventListener('message', handler);
|
|
30
|
+
resolve(data);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
socket.addEventListener('message', handler);
|
|
34
|
+
socket.send(JSON.stringify(msg));
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('Transport — WebSocket', () => {
|
|
39
|
+
test('CRUD via WebSocket', async () => {
|
|
40
|
+
const port = setup();
|
|
41
|
+
const socket = await ws(port);
|
|
42
|
+
|
|
43
|
+
// Set
|
|
44
|
+
const setRes = await send(socket, { id: '1', op: 'set', path: 'users/u1', value: { name: 'Alice', role: 'admin' } });
|
|
45
|
+
expect(setRes.ok).toBe(true);
|
|
46
|
+
|
|
47
|
+
// Get
|
|
48
|
+
const getRes = await send(socket, { id: '2', op: 'get', path: 'users/u1' });
|
|
49
|
+
expect(getRes.ok).toBe(true);
|
|
50
|
+
expect(getRes.data).toEqual({ name: 'Alice', role: 'admin' });
|
|
51
|
+
|
|
52
|
+
// Update
|
|
53
|
+
const updRes = await send(socket, { id: '3', op: 'update', updates: { 'users/u1/name': 'Bob' } });
|
|
54
|
+
expect(updRes.ok).toBe(true);
|
|
55
|
+
|
|
56
|
+
const getRes2 = await send(socket, { id: '4', op: 'get', path: 'users/u1/name' });
|
|
57
|
+
expect(getRes2.data).toBe('Bob');
|
|
58
|
+
|
|
59
|
+
// Delete
|
|
60
|
+
const delRes = await send(socket, { id: '5', op: 'delete', path: 'users/u1' });
|
|
61
|
+
expect(delRes.ok).toBe(true);
|
|
62
|
+
|
|
63
|
+
const getRes3 = await send(socket, { id: '6', op: 'get', path: 'users/u1' });
|
|
64
|
+
expect(getRes3.data).toBeNull();
|
|
65
|
+
|
|
66
|
+
socket.close();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('query via WebSocket', async () => {
|
|
70
|
+
const port = setup();
|
|
71
|
+
const socket = await ws(port);
|
|
72
|
+
|
|
73
|
+
await send(socket, { id: '1', op: 'set', path: 'users/u1', value: { name: 'Alice', role: 'admin' } });
|
|
74
|
+
await send(socket, { id: '2', op: 'set', path: 'users/u2', value: { name: 'Bob', role: 'user' } });
|
|
75
|
+
|
|
76
|
+
const qRes = await send(socket, {
|
|
77
|
+
id: '3', op: 'query', path: 'users',
|
|
78
|
+
filters: [{ field: 'role', op: '==', value: 'admin' }],
|
|
79
|
+
});
|
|
80
|
+
expect(qRes.ok).toBe(true);
|
|
81
|
+
expect((qRes.data as any[]).length).toBe(1);
|
|
82
|
+
expect((qRes.data as any[])[0].name).toBe('Alice');
|
|
83
|
+
|
|
84
|
+
socket.close();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('value subscription via WebSocket', async () => {
|
|
88
|
+
const port = setup();
|
|
89
|
+
const socket = await ws(port);
|
|
90
|
+
|
|
91
|
+
// Subscribe
|
|
92
|
+
await send(socket, { id: '1', op: 'sub', path: 'users/u1', event: 'value' });
|
|
93
|
+
|
|
94
|
+
// Collect push messages
|
|
95
|
+
const pushes: any[] = [];
|
|
96
|
+
socket.addEventListener('message', (e: MessageEvent) => {
|
|
97
|
+
const data = JSON.parse(e.data);
|
|
98
|
+
if (data.type === 'value') pushes.push(data);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Trigger a write from server side
|
|
102
|
+
db.set('users/u1', { name: 'Alice' });
|
|
103
|
+
await Bun.sleep(50);
|
|
104
|
+
|
|
105
|
+
expect(pushes.length).toBe(1);
|
|
106
|
+
expect(pushes[0].data).toEqual({ name: 'Alice' });
|
|
107
|
+
|
|
108
|
+
// Unsub
|
|
109
|
+
await send(socket, { id: '2', op: 'unsub', path: 'users/u1', event: 'value' });
|
|
110
|
+
db.set('users/u1', { name: 'Bob' });
|
|
111
|
+
await Bun.sleep(50);
|
|
112
|
+
|
|
113
|
+
expect(pushes.length).toBe(1); // no new push
|
|
114
|
+
|
|
115
|
+
socket.close();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('rules block unauthorized write', async () => {
|
|
119
|
+
const port = setup({
|
|
120
|
+
'secret': {
|
|
121
|
+
read: () => false,
|
|
122
|
+
write: () => false,
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
const socket = await ws(port);
|
|
126
|
+
|
|
127
|
+
const res = await send(socket, { id: '1', op: 'set', path: 'secret/data', value: 'hack' });
|
|
128
|
+
expect(res.ok).toBe(false);
|
|
129
|
+
expect(res.code).toBe('PERMISSION_DENIED');
|
|
130
|
+
|
|
131
|
+
socket.close();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('auth flow', async () => {
|
|
135
|
+
db = new BodDB({
|
|
136
|
+
rules: {
|
|
137
|
+
'private': {
|
|
138
|
+
write: (ctx) => !!ctx.auth,
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
port: 0,
|
|
142
|
+
auth: (token) => token === 'valid' ? { uid: 'u1' } : null,
|
|
143
|
+
});
|
|
144
|
+
const server = db.serve();
|
|
145
|
+
const socket = await ws(server.port);
|
|
146
|
+
|
|
147
|
+
// Write without auth
|
|
148
|
+
const res1 = await send(socket, { id: '1', op: 'set', path: 'private/data', value: 'test' });
|
|
149
|
+
expect(res1.ok).toBe(false);
|
|
150
|
+
|
|
151
|
+
// Auth with bad token
|
|
152
|
+
const authBad = await send(socket, { id: '2', op: 'auth', token: 'invalid' });
|
|
153
|
+
expect(authBad.ok).toBe(false);
|
|
154
|
+
|
|
155
|
+
// Auth with good token
|
|
156
|
+
const authOk = await send(socket, { id: '3', op: 'auth', token: 'valid' });
|
|
157
|
+
expect(authOk.ok).toBe(true);
|
|
158
|
+
|
|
159
|
+
// Write after auth
|
|
160
|
+
const res2 = await send(socket, { id: '4', op: 'set', path: 'private/data', value: 'test' });
|
|
161
|
+
expect(res2.ok).toBe(true);
|
|
162
|
+
|
|
163
|
+
socket.close();
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
describe('Transport — REST', () => {
|
|
168
|
+
test('GET /db/path', async () => {
|
|
169
|
+
const port = setup();
|
|
170
|
+
db.set('users/u1', { name: 'Alice' });
|
|
171
|
+
|
|
172
|
+
const res = await fetch(`http://localhost:${port}/db/users/u1`);
|
|
173
|
+
const json = await res.json();
|
|
174
|
+
expect(json.ok).toBe(true);
|
|
175
|
+
expect(json.data).toEqual({ name: 'Alice' });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('PUT /db/path', async () => {
|
|
179
|
+
const port = setup();
|
|
180
|
+
|
|
181
|
+
const res = await fetch(`http://localhost:${port}/db/items/a`, {
|
|
182
|
+
method: 'PUT',
|
|
183
|
+
body: JSON.stringify({ v: 1 }),
|
|
184
|
+
headers: { 'Content-Type': 'application/json' },
|
|
185
|
+
});
|
|
186
|
+
const json = await res.json();
|
|
187
|
+
expect(json.ok).toBe(true);
|
|
188
|
+
expect(db.get('items/a')).toEqual({ v: 1 });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('DELETE /db/path', async () => {
|
|
192
|
+
const port = setup();
|
|
193
|
+
db.set('tmp/data', 'hello');
|
|
194
|
+
|
|
195
|
+
const res = await fetch(`http://localhost:${port}/db/tmp/data`, { method: 'DELETE' });
|
|
196
|
+
const json = await res.json();
|
|
197
|
+
expect(json.ok).toBe(true);
|
|
198
|
+
expect(db.get('tmp/data')).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('REST rules enforcement', async () => {
|
|
202
|
+
const port = setup({
|
|
203
|
+
'locked': { read: () => false, write: () => false },
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const res = await fetch(`http://localhost:${port}/db/locked/path`);
|
|
207
|
+
expect(res.status).toBe(403);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { BodDB } from '../src/server/BodDB.ts';
|
|
3
|
+
|
|
4
|
+
describe('TTL / Auto-Expiry', () => {
|
|
5
|
+
/** Helper: force-expire all entries at a path by setting expires_at to 1 (way in the past) */
|
|
6
|
+
function forceExpire(db: BodDB, path: string) {
|
|
7
|
+
db.storage.db.run('UPDATE nodes SET expires_at = 1 WHERE path = ? OR path LIKE ?', [path, path + '/%']);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
test('set with ttl marks for expiry', () => {
|
|
11
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
12
|
+
db.set('temp', 'hello', { ttl: 60 });
|
|
13
|
+
expect(db.get('temp')).toBe('hello');
|
|
14
|
+
|
|
15
|
+
// Not expired yet (60s TTL)
|
|
16
|
+
const swept = db.sweep();
|
|
17
|
+
expect(swept).toEqual([]);
|
|
18
|
+
expect(db.get('temp')).toBe('hello');
|
|
19
|
+
db.close();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('expired entries are swept', () => {
|
|
23
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
24
|
+
db.set('temp', 'hello', { ttl: 60 });
|
|
25
|
+
expect(db.get('temp')).toBe('hello');
|
|
26
|
+
|
|
27
|
+
forceExpire(db, 'temp');
|
|
28
|
+
const swept = db.sweep();
|
|
29
|
+
expect(swept.length).toBeGreaterThan(0);
|
|
30
|
+
expect(db.get('temp')).toBeNull();
|
|
31
|
+
db.close();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('non-ttl entries are not swept', () => {
|
|
35
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
36
|
+
db.set('permanent', 'stays');
|
|
37
|
+
db.set('temp', 'goes', { ttl: 60 });
|
|
38
|
+
|
|
39
|
+
forceExpire(db, 'temp');
|
|
40
|
+
db.sweep();
|
|
41
|
+
expect(db.get('permanent')).toBe('stays');
|
|
42
|
+
expect(db.get('temp')).toBeNull();
|
|
43
|
+
db.close();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('ttl on object sets expiry on all leaves', () => {
|
|
47
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
48
|
+
db.set('session/data', { user: 'alice', token: 'abc' }, { ttl: 60 });
|
|
49
|
+
expect(db.get('session/data')).toEqual({ user: 'alice', token: 'abc' });
|
|
50
|
+
|
|
51
|
+
forceExpire(db, 'session/data');
|
|
52
|
+
db.sweep();
|
|
53
|
+
expect(db.get('session/data')).toBeNull();
|
|
54
|
+
db.close();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('sweep triggers subscription notifications', () => {
|
|
58
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
59
|
+
db.set('temp', 'hello', { ttl: 60 });
|
|
60
|
+
|
|
61
|
+
const events: unknown[] = [];
|
|
62
|
+
db.on('temp', (snap) => events.push(snap.val()));
|
|
63
|
+
|
|
64
|
+
forceExpire(db, 'temp');
|
|
65
|
+
db.sweep();
|
|
66
|
+
expect(events.length).toBeGreaterThan(0);
|
|
67
|
+
expect(events[events.length - 1]).toBeNull();
|
|
68
|
+
db.close();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, test, expect } from 'bun:test';
|
|
2
|
+
import { BodDB } from '../src/server/BodDB.ts';
|
|
3
|
+
|
|
4
|
+
describe('VectorEngine', () => {
|
|
5
|
+
const dims = 4; // Small for tests
|
|
6
|
+
|
|
7
|
+
test('store and search vectors', () => {
|
|
8
|
+
const db = new BodDB({ sweepInterval: 0, vectors: { dimensions: dims } });
|
|
9
|
+
db.set('docs/d1', { text: 'hello' });
|
|
10
|
+
db.set('docs/d2', { text: 'world' });
|
|
11
|
+
db.set('docs/d3', { text: 'other' });
|
|
12
|
+
|
|
13
|
+
db.vectors!.store('docs/d1', [1, 0, 0, 0]);
|
|
14
|
+
db.vectors!.store('docs/d2', [0.9, 0.1, 0, 0]);
|
|
15
|
+
db.vectors!.store('docs/d3', [0, 0, 1, 0]);
|
|
16
|
+
|
|
17
|
+
const results = db.vectorSearch({ query: [1, 0, 0, 0] });
|
|
18
|
+
expect(results.length).toBe(3);
|
|
19
|
+
expect(results[0].path).toBe('docs/d1'); // Most similar
|
|
20
|
+
expect(results[0].score).toBeCloseTo(1.0, 3);
|
|
21
|
+
expect(results[1].path).toBe('docs/d2'); // Second most similar
|
|
22
|
+
db.close();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('search with prefix filter', () => {
|
|
26
|
+
const db = new BodDB({ sweepInterval: 0, vectors: { dimensions: dims } });
|
|
27
|
+
db.vectors!.store('cats/c1', [1, 0, 0, 0]);
|
|
28
|
+
db.vectors!.store('dogs/d1', [0.9, 0.1, 0, 0]);
|
|
29
|
+
|
|
30
|
+
const results = db.vectorSearch({ query: [1, 0, 0, 0], path: 'cats' });
|
|
31
|
+
expect(results.length).toBe(1);
|
|
32
|
+
expect(results[0].path).toBe('cats/c1');
|
|
33
|
+
db.close();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('search with threshold', () => {
|
|
37
|
+
const db = new BodDB({ sweepInterval: 0, vectors: { dimensions: dims } });
|
|
38
|
+
db.vectors!.store('a', [1, 0, 0, 0]);
|
|
39
|
+
db.vectors!.store('b', [0, 1, 0, 0]); // Orthogonal, similarity ~0
|
|
40
|
+
|
|
41
|
+
const results = db.vectorSearch({ query: [1, 0, 0, 0], threshold: 0.5 });
|
|
42
|
+
expect(results.length).toBe(1);
|
|
43
|
+
expect(results[0].path).toBe('a');
|
|
44
|
+
db.close();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('search with limit', () => {
|
|
48
|
+
const db = new BodDB({ sweepInterval: 0, vectors: { dimensions: dims } });
|
|
49
|
+
for (let i = 0; i < 10; i++) {
|
|
50
|
+
db.vectors!.store(`item/${i}`, [Math.random(), Math.random(), Math.random(), Math.random()]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const results = db.vectorSearch({ query: [1, 0, 0, 0], limit: 3 });
|
|
54
|
+
expect(results.length).toBe(3);
|
|
55
|
+
db.close();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('throws without vectors config', () => {
|
|
59
|
+
const db = new BodDB({ sweepInterval: 0 });
|
|
60
|
+
expect(() => db.vectorSearch({ query: [1, 0, 0, 0] })).toThrow('Vectors not configured');
|
|
61
|
+
db.close();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('dimension mismatch throws', () => {
|
|
65
|
+
const db = new BodDB({ sweepInterval: 0, vectors: { dimensions: dims } });
|
|
66
|
+
expect(() => db.vectors!.store('a', [1, 0])).toThrow('Expected 4 dimensions');
|
|
67
|
+
db.close();
|
|
68
|
+
});
|
|
69
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Enable latest features
|
|
4
|
+
"lib": ["ESNext", "DOM"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
|
|
22
|
+
// Some stricter flags (disabled by default)
|
|
23
|
+
"noUnusedLocals": false,
|
|
24
|
+
"noUnusedParameters": false,
|
|
25
|
+
"noPropertyAccessFromIndexSignature": false
|
|
26
|
+
}
|
|
27
|
+
}
|