@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
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
|
+
};
|