@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.
Files changed (65) hide show
  1. package/.claude/settings.local.json +23 -0
  2. package/.claude/skills/config-file.md +54 -0
  3. package/.claude/skills/deploying-bod-db.md +29 -0
  4. package/.claude/skills/developing-bod-db.md +127 -0
  5. package/.claude/skills/using-bod-db.md +403 -0
  6. package/CLAUDE.md +110 -0
  7. package/README.md +252 -0
  8. package/admin/rules.ts +12 -0
  9. package/admin/server.ts +523 -0
  10. package/admin/ui.html +2281 -0
  11. package/cli.ts +177 -0
  12. package/client.ts +2 -0
  13. package/config.ts +20 -0
  14. package/deploy/.env.example +1 -0
  15. package/deploy/base.yaml +18 -0
  16. package/deploy/boddb-logs.yaml +10 -0
  17. package/deploy/boddb.yaml +10 -0
  18. package/deploy/demo.html +196 -0
  19. package/deploy/deploy.ts +32 -0
  20. package/deploy/prod-logs.config.ts +15 -0
  21. package/deploy/prod.config.ts +15 -0
  22. package/index.ts +20 -0
  23. package/mcp.ts +78 -0
  24. package/package.json +29 -0
  25. package/react.ts +1 -0
  26. package/src/client/BodClient.ts +515 -0
  27. package/src/react/hooks.ts +121 -0
  28. package/src/server/BodDB.ts +319 -0
  29. package/src/server/ExpressionRules.ts +250 -0
  30. package/src/server/FTSEngine.ts +76 -0
  31. package/src/server/FileAdapter.ts +116 -0
  32. package/src/server/MCPAdapter.ts +409 -0
  33. package/src/server/MQEngine.ts +286 -0
  34. package/src/server/QueryEngine.ts +45 -0
  35. package/src/server/RulesEngine.ts +108 -0
  36. package/src/server/StorageEngine.ts +464 -0
  37. package/src/server/StreamEngine.ts +320 -0
  38. package/src/server/SubscriptionEngine.ts +120 -0
  39. package/src/server/Transport.ts +479 -0
  40. package/src/server/VectorEngine.ts +115 -0
  41. package/src/shared/errors.ts +15 -0
  42. package/src/shared/pathUtils.ts +94 -0
  43. package/src/shared/protocol.ts +59 -0
  44. package/src/shared/transforms.ts +99 -0
  45. package/tests/batch.test.ts +60 -0
  46. package/tests/bench.ts +205 -0
  47. package/tests/e2e.test.ts +284 -0
  48. package/tests/expression-rules.test.ts +114 -0
  49. package/tests/file-adapter.test.ts +57 -0
  50. package/tests/fts.test.ts +58 -0
  51. package/tests/mq-flow.test.ts +204 -0
  52. package/tests/mq.test.ts +326 -0
  53. package/tests/push.test.ts +55 -0
  54. package/tests/query.test.ts +60 -0
  55. package/tests/rules.test.ts +78 -0
  56. package/tests/sse.test.ts +78 -0
  57. package/tests/storage.test.ts +199 -0
  58. package/tests/stream.test.ts +385 -0
  59. package/tests/stress.test.ts +202 -0
  60. package/tests/subscriptions.test.ts +86 -0
  61. package/tests/transforms.test.ts +92 -0
  62. package/tests/transport.test.ts +209 -0
  63. package/tests/ttl.test.ts +70 -0
  64. package/tests/vector.test.ts +69 -0
  65. package/tsconfig.json +27 -0
package/CLAUDE.md ADDED
@@ -0,0 +1,110 @@
1
+ # BodDB
2
+
3
+ Embedded reactive database — SQLite-backed (bun:sqlite), zero dependencies.
4
+
5
+ ## Architecture
6
+
7
+ ```
8
+ src/shared/pathUtils.ts — path normalize/validate, flatten/reconstruct, ancestors, prefixEnd
9
+ src/shared/protocol.ts — wire message types (client↔server), batch ops
10
+ src/shared/errors.ts — error codes
11
+ src/shared/transforms.ts — sentinel classes (increment, serverTimestamp, arrayUnion, arrayRemove, ref)
12
+ src/server/StorageEngine.ts — SQLite CRUD, leaf flattening, prefix queries, transforms, TTL, push IDs
13
+ src/server/SubscriptionEngine.ts — value + child subscriptions, ancestor walk, dedup, added/changed/removed
14
+ src/server/QueryEngine.ts — fluent query builder → StorageEngine.query
15
+ src/server/BodDB.ts — main facade: storage + subs + rules + transport + FTS + vectors + TTL sweep
16
+ src/server/RulesEngine.ts — path-based permission checks with $wildcard capture (functions + expressions)
17
+ src/server/ExpressionRules.ts — safe AST-based expression parser (tokenizer → parser → evaluator, no eval)
18
+ src/server/Transport.ts — Bun.serve WebSocket + REST + SSE endpoints, batch/push ops, sub lifecycle
19
+ src/server/FTSEngine.ts — SQLite FTS5 full-text search: index, search, auto-index
20
+ src/server/VectorEngine.ts — vector similarity search: brute-force cosine/euclidean, Float32Array storage
21
+ src/server/StreamEngine.ts — Kafka-like event streaming: consumer groups, offset tracking, replay, durable subs
22
+ src/server/MQEngine.ts — SQS-style message queue: push/fetch/ack/nack, visibility timeout, DLQ, exactly-once claim
23
+ src/server/FileAdapter.ts — file system sync: scan, watch, metadata, content read/write-through
24
+ src/server/MCPAdapter.ts — MCP (Model Context Protocol) server: JSON-RPC dispatch, tool registry, stdio + HTTP transports
25
+ src/client/BodClient.ts — WS client: connect, auth, CRUD, batch, push, subscriptions, auto-reconnect
26
+ src/react/hooks.ts — useValue, useChildren, useQuery, useMutation
27
+ client.ts — client entry point (import from 'bod-db/client')
28
+ react.ts — React hooks entry point (import from 'bod-db/react')
29
+ cli.ts — CLI entry point: bod-db [config] [--port/--path/--memory/--init]
30
+ mcp.ts — MCP server entry point: bod-db-mcp [--stdio/--http] [--url/--port]
31
+ config.ts — demo instance config (open rules, indexes, fts, vectors, mq)
32
+ ```
33
+
34
+ ## Key patterns
35
+
36
+ - **Leaf flattening**: nested objects → flat rows keyed by full path. Subtrees reconstructed via prefix scan.
37
+ - **Push rows**: `db.push()` stores as single JSON row (NOT flattened) with time-sortable key (8-char timestamp + 4-char random).
38
+ - **Prefix queries**: `WHERE path >= 'users/u1/' AND path < 'users/u1/\uffff'`
39
+ - **Subscription notify**: writes produce changedPaths → notify exact + all ancestors (deduped). Child events track added/changed/removed via pre-write existence snapshot.
40
+ - **Path validation**: all public APIs use `validatePath()` — rejects empty paths, normalizes slashes.
41
+ - **Options pattern**: `new XOptions()` defaults merged with partial user options.
42
+ - **Rules**: path patterns with `$wildcard` capture, most-specific match wins. Context: `auth`, `params`, `data`, `newData`. Supports functions, booleans, expression strings, and JSON/TS config files.
43
+ - **Expression rules**: safe tokenizer → recursive descent parser → AST evaluator. Supports `auth.x`, `$wildcard`, `data`/`newData`, comparisons, logical ops, negation, parens. No eval.
44
+ - **Transforms**: sentinel values (`increment`, `serverTimestamp`, `arrayUnion`, `arrayRemove`, `ref`) resolved against current data before write.
45
+ - **Refs**: `ref(path)` stores `{ _ref: path }`. Resolved at read time via `storage.get(path, { resolve: true })`.
46
+ - **Transactions**: `db.transaction(fn)` wraps operations in SQLite transaction. Notifications fire after commit.
47
+ - **Batch protocol**: `{ op: 'batch', operations: [...] }` — executes all ops in single SQLite transaction.
48
+ - **TTL**: `db.set(path, val, { ttl: seconds })` → `expires_at` column. Background sweep (configurable interval) deletes expired + notifies subscribers.
49
+ - **FTS5**: `db.search({ text, path?, limit? })` using SQLite FTS5. Index via `db.index(path, content)` or `db.index(path, fields[])`.
50
+ - **Vectors**: `db.vectorSearch({ query, path?, limit?, threshold? })` — brute-force cosine similarity. Store via `db.vectors.store(path, embedding)`.
51
+ - **Streams**: Kafka-like consumer groups over push paths. `db.stream.read(topic, groupId)` / `.ack()` / `.subscribe()`. Offsets stored in `_streams/<topic>/groups/<groupId>/offset`. Subscribe-then-backfill prevents race conditions. `queryAfterKey()` for SQL-level replay (avoids loading entire topic). Idempotent push via `idempotency_key` column + unique index. **Compaction** folds events into a snapshot (`_streams/<topic>/snapshot`), then deletes them. Snapshot is the base state; live events layer on top. `compact(topic, { maxAge, maxCount, keepKey })`. `materialize(topic, { keepKey })` returns merged snapshot+events view. Auto-compact on sweep via `compact` config. Safety: never folds events beyond minimum consumer group offset.
52
+ - **MQ (Message Queue)**: SQS-style work queue. `db.mq.push(queue, value)` / `.fetch(queue, count)` / `.ack(queue, key)` / `.nack(queue, key)`. Each message claimed by exactly one worker (visibility timeout). Columns: `mq_status` (pending/inflight/dead), `mq_inflight_until`, `mq_delivery_count`. `sweep()` reclaims expired inflight; exhausted messages (>maxDeliveries) move to DLQ at `<queue>/_dlq/<key>` with `mq_status='dead'`. Ack = DELETE (keeps queue lean). `purge(queue)` deletes pending only; `purge(queue, { all: true })` deletes all (pending + inflight + DLQ). Per-queue options via `queues` config (longest prefix match). Client: `MQReader` / `MQMessageSnapshot`.
53
+ - **FileAdapter**: scans directory, syncs metadata to DB, optional fs.watch, content read/write-through.
54
+ - **SSE fallback**: `GET /sse/<path>?event=value|child` returns `text/event-stream`. Initial `: ok` comment flushes the stream connection.
55
+ - **Perf**: `snapshotExisting` and `notify` are skipped when no subscriptions are active. `exists()` uses `LIMIT 1`.
56
+ - **Transport**: WS messages follow `protocol.ts` types. REST at `/db/<path>`. Auth via `op:'auth'` message. Subs cleaned up on disconnect.
57
+ - **BodClient**: id-correlated request/response over WS. Auto-reconnect with exponential backoff. Re-subscribes all active subs on reconnect. `ValueSnapshot` with `.val()`, `.key`, `.path`, `.exists()`.
58
+ - **MCP**: `MCPAdapter` wraps a `BodClient` as a JSON-RPC MCP server (stdio + HTTP). Connects to a running BodDB instance over WebSocket — no embedded DB. Entry point: `mcp.ts`. Tools: CRUD (6), FTS (2), vectors (2), streams (4), MQ (7) = 21 tools. Use `--stdio` for Claude Code/Desktop, `--http` for remote agents.
59
+
60
+ ## MCP Server
61
+
62
+ The MCP server is a **client** that connects to a running BodDB server via WebSocket.
63
+
64
+ ```bash
65
+ # Start BodDB server first
66
+ bun run start # ws://localhost:4400
67
+
68
+ # Then start MCP (separate process)
69
+ bun run mcp.ts --stdio # stdio transport (default, for Claude Code)
70
+ bun run mcp.ts --stdio --port 4400 # explicit port
71
+ bun run mcp.ts --http --http-port 4401 # HTTP transport for remote agents
72
+ ```
73
+
74
+ ### Claude Code integration (.mcp.json)
75
+ ```json
76
+ {
77
+ "mcpServers": {
78
+ "bod-db": {
79
+ "type": "stdio",
80
+ "command": "bun",
81
+ "args": ["run", "/path/to/zuzdb/mcp.ts", "--stdio"]
82
+ }
83
+ }
84
+ }
85
+ ```
86
+
87
+ ### HTTP integration
88
+ ```bash
89
+ curl -X POST http://localhost:4401 \
90
+ -H 'Content-Type: application/json' \
91
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
92
+ ```
93
+
94
+ ## Testing
95
+
96
+ ```bash
97
+ bun test # run all tests
98
+ bun test tests/storage.test.ts # single file
99
+ ```
100
+
101
+ ## Implementation status
102
+
103
+ - [x] Phase 1: Core (storage, subscriptions, queries) — server-only, no network
104
+ - [x] Phase 2: Transport (WebSocket + REST) + RulesEngine (function-based V1)
105
+ - [x] Phase 3: Client SDK (BodClient — CRUD, queries, subscriptions, auto-reconnect)
106
+ - [x] Phase 4: Expression rules V2, SSE fallback, React hooks, benchmarks, examples
107
+ - [x] Phase 5: Rules config files, transforms/sentinels, refs, transactions, batch, push, TTL, FileAdapter, FTS5, VectorEngine
108
+ - [x] Phase 6: Event streaming (consumer groups, offset tracking, replay, idempotent push, StreamEngine)
109
+ - [x] Phase 7: Message queue (MQEngine — push/fetch/ack/nack, visibility timeout, DLQ, exactly-once claim)
110
+ - [x] Phase 8: MCP server (MCPAdapter — stdio + HTTP, BodClient-based, 21 tools)
package/README.md ADDED
@@ -0,0 +1,252 @@
1
+ # BodDB
2
+
3
+
4
+ ----
5
+ 187 tests passing · ~200k ops/sec · Zero dependencies · Full docs + examples
6
+ ----
7
+
8
+
9
+ Embedded reactive database — SQLite-backed, zero dependencies, Firebase-like API.
10
+
11
+ - **In-process**: no cloud, no external services
12
+ - **SQLite WAL**: crash-safe, instant startup
13
+ - **Real-time**: value + child subscriptions with ancestor propagation
14
+ - **Network**: WebSocket + REST + SSE transport with auth and permission rules
15
+ - **Transforms**: increment, serverTimestamp, arrayUnion, arrayRemove, refs
16
+ - **Streaming**: Kafka-like consumer groups with offset tracking, replay, compaction
17
+ - **Message Queue**: SQS-style work queue — exactly-once delivery, visibility timeout, DLQ
18
+ - **Advanced**: transactions, batch ops, push IDs, TTL, FTS5, vector search, file sync
19
+ - **TypeScript-first**: full type safety, Bun-native
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ bun add bod-db
25
+ ```
26
+
27
+ ## Server
28
+
29
+ ```typescript
30
+ import { BodDB, increment, serverTimestamp, ref } from 'bod-db';
31
+
32
+ const db = new BodDB({
33
+ path: './data.db',
34
+ rules: {
35
+ 'users/$uid': { read: true, write: "auth.uid === $uid" },
36
+ },
37
+ indexes: { 'users': ['role', 'createdAt'] },
38
+ sweepInterval: 60000, // TTL sweep every 60s
39
+ fts: {}, // enable full-text search
40
+ vectors: { dimensions: 384 }, // enable vector search
41
+ });
42
+
43
+ // CRUD
44
+ db.set('users/u1', { name: 'Alice', role: 'admin' });
45
+ db.get('users/u1'); // { name: 'Alice', role: 'admin' }
46
+ db.update({ 'users/u1/name': 'Bob', 'counters/visits': 42 });
47
+ db.delete('users/u1');
48
+
49
+ // Transforms
50
+ db.set('counters/visits', increment(1)); // atomic increment
51
+ db.set('posts/p1/updatedAt', serverTimestamp()); // server timestamp
52
+ db.set('posts/p1/author', ref('users/u1')); // reference
53
+
54
+ // Push (append-only with time-sortable key)
55
+ const key = db.push('logs', { level: 'info', msg: 'started' });
56
+
57
+ // TTL
58
+ db.set('sessions/temp', { token: 'abc' }, { ttl: 3600 }); // expires in 1h
59
+
60
+ // Transaction
61
+ db.transaction((tx) => {
62
+ const user = tx.get('users/u1');
63
+ tx.set('users/u1/lastLogin', Date.now());
64
+ tx.update({ 'stats/logins': { total: 1 } });
65
+ });
66
+
67
+ // Query
68
+ db.query('users')
69
+ .where('role', '==', 'admin')
70
+ .order('name')
71
+ .limit(10)
72
+ .get();
73
+
74
+ // Full-text search
75
+ db.index('posts/p1', 'Hello world tutorial');
76
+ db.search({ text: 'hello', path: 'posts', limit: 10 });
77
+
78
+ // Vector search
79
+ db.vectors!.store('docs/d1', embedding);
80
+ db.vectorSearch({ query: embedding, path: 'docs', limit: 5, threshold: 0.7 });
81
+
82
+ // Subscribe
83
+ const off = db.on('users/u1', (snap) => console.log(snap.val()));
84
+ db.onChild('users', (e) => console.log(e.type, e.key, e.val()));
85
+
86
+ // Serve over network
87
+ db.serve({ port: 4400 });
88
+ ```
89
+
90
+ ## Client
91
+
92
+ ```typescript
93
+ import { BodClient } from 'bod-db/client';
94
+
95
+ const client = new BodClient({
96
+ url: 'ws://localhost:4400',
97
+ auth: () => 'my-token',
98
+ reconnect: true,
99
+ });
100
+
101
+ await client.connect();
102
+
103
+ // CRUD
104
+ await client.set('users/u1', { name: 'Alice' });
105
+ const user = await client.get('users/u1');
106
+
107
+ // Batch (atomic multi-op)
108
+ await client.batch([
109
+ { op: 'set', path: 'users/u1/name', value: 'Bob' },
110
+ { op: 'delete', path: 'users/u2' },
111
+ ]);
112
+
113
+ // Push
114
+ const key = await client.push('logs', { msg: 'hello' });
115
+
116
+ // Subscribe
117
+ const off = client.on('users/u1', (snap) => console.log(snap.val()));
118
+ client.onChild('users', (e) => console.log(e.type, e.key));
119
+
120
+ client.disconnect();
121
+ ```
122
+
123
+ ## Streams (Kafka-like)
124
+
125
+ ```typescript
126
+ // Push events to a topic
127
+ db.push('events/orders', { orderId: 'o1', amount: 250 });
128
+
129
+ // Consumer groups — each group tracks its own offset (fan-out)
130
+ const events = db.stream.read('events/orders', 'billing', 50);
131
+ db.stream.ack('events/orders', 'billing', events.at(-1)!.key);
132
+
133
+ // Compact old events into a snapshot
134
+ db.stream.compact('events/orders', { maxCount: 1000, keepKey: 'orderId' });
135
+ const view = db.stream.materialize('events/orders', { keepKey: 'orderId' });
136
+
137
+ // Client
138
+ const reader = client.stream('events/orders', 'billing');
139
+ const unsub = reader.on((events) => { /* live delivery */ });
140
+ ```
141
+
142
+ ## Message Queue (SQS-like)
143
+
144
+ ```typescript
145
+ // Server
146
+ const db = new BodDB({ mq: { visibilityTimeout: 30, maxDeliveries: 5 } });
147
+
148
+ db.mq.push('queues/jobs', { type: 'email', to: 'alice@example.com' });
149
+ const jobs = db.mq.fetch('queues/jobs', 5); // claim up to 5
150
+ db.mq.ack('queues/jobs', jobs[0].key); // done — delete
151
+ db.mq.nack('queues/jobs', jobs[0].key); // failed — back to pending
152
+ db.mq.peek('queues/jobs'); // view without claiming
153
+ db.mq.dlq('queues/jobs'); // dead letter queue
154
+ db.mq.purge('queues/jobs'); // delete pending
155
+ db.mq.purge('queues/jobs', { all: true }); // delete all (pending + inflight + DLQ)
156
+
157
+ // Client
158
+ const q = client.mq('queues/jobs');
159
+ await q.push({ type: 'email' });
160
+ const msgs = await q.fetch(5);
161
+ await q.ack(msgs[0].key);
162
+ ```
163
+
164
+ ## Rules
165
+
166
+ Function-based, expression strings, or JSON config files:
167
+
168
+ ```typescript
169
+ // Inline
170
+ const db = new BodDB({
171
+ rules: {
172
+ 'users/$uid': {
173
+ read: true,
174
+ write: "auth.uid === $uid",
175
+ },
176
+ 'admin': {
177
+ read: "auth.role == 'admin'",
178
+ write: (ctx) => ctx.auth?.role === 'admin',
179
+ },
180
+ },
181
+ });
182
+
183
+ // From JSON file
184
+ const db2 = new BodDB({ rules: './rules.json' });
185
+ // rules.json: { "rules": { "users/$uid": { "read": true, "write": "auth.uid === $uid" } } }
186
+
187
+ // From TS file (async)
188
+ const db3 = await BodDB.create({ rules: './rules.ts' });
189
+ ```
190
+
191
+ Expression rules support: `auth.*`, `$wildcard` params, `data`/`newData`, comparisons (`==`, `!=`, `>=`, `<`, etc.), logical ops (`&&`, `||`, `!`), parens, `null` checks. Safe AST evaluation — no `eval`.
192
+
193
+ ## REST API
194
+
195
+ ```
196
+ GET /db/users/u1 → { ok: true, data: { name: 'Alice' } }
197
+ PUT /db/users/u1 → body: JSON → { ok: true }
198
+ DELETE /db/users/u1 → { ok: true }
199
+ ```
200
+
201
+ ## SSE
202
+
203
+ ```
204
+ GET /sse/users/u1 → text/event-stream (value events)
205
+ GET /sse/users?event=child → text/event-stream (child events)
206
+ ```
207
+
208
+ ## React Hooks
209
+
210
+ ```typescript
211
+ import { useValue, useChildren, useQuery, useMutation } from 'bod-db/react';
212
+
213
+ const { data, loading } = useValue(client, 'users/u1');
214
+ const { children, loading } = useChildren(client, 'users');
215
+ const { data, loading } = useQuery(client, 'users', { filters: [...], limit: 10 });
216
+ const { set, update, del, loading } = useMutation(client);
217
+ ```
218
+
219
+ ## File Adapter
220
+
221
+ ```typescript
222
+ import { FileAdapter } from 'bod-db';
223
+
224
+ const adapter = new FileAdapter(db, {
225
+ root: './uploads',
226
+ basePath: 'files',
227
+ watch: true,
228
+ metadata: true,
229
+ });
230
+ await adapter.start();
231
+ // Files synced to db as: files/<relPath> → { size, mtime, mime }
232
+ ```
233
+
234
+ ## Benchmark
235
+
236
+ ```bash
237
+ bun run tests/bench.ts
238
+ ```
239
+
240
+ | Workload | ops/sec | p50 | p99 |
241
+ |---|---|---|---|
242
+ | Read-only | ~200K | 0.00ms | 0.03ms |
243
+ | Read-heavy (95/5) | ~170K | 0.00ms | 0.04ms |
244
+ | Update-heavy (50/50) | ~94K | 0.01ms | 0.07ms |
245
+ | Insert-heavy | ~89K | 0.01ms | 0.05ms |
246
+ | Subscription (100 subs) | ~3.5K | 0.03ms | 4ms |
247
+
248
+ ## Test
249
+
250
+ ```bash
251
+ bun test # 187 tests
252
+ ```
package/admin/rules.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type { PathRule } from '../src/server/RulesEngine.ts';
2
+
3
+ /**
4
+ * Edit this file to configure access rules for the admin server.
5
+ * Pattern syntax: exact segments or $wildcard (e.g. users/$uid)
6
+ * Context: { auth, path, params, data, newData }
7
+ */
8
+ export const rules: Record<string, PathRule> = {
9
+ '_admin/$any': { read: false, write: false },
10
+ 'users/$uid': { write: ({ auth, params }) => auth?.role === 'admin' || ['alice', 'bob'].includes(params.uid) },
11
+ 'settings/$key': { write: ({ auth }) => !!auth },
12
+ };