jotdb 0.1.8 → 0.1.10
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 +296 -6
- package/dist/index.d.ts +58 -1
- package/dist/index.js +552 -31
- package/dist/site-worker.d.ts +7 -0
- package/dist/site-worker.js +6 -0
- package/dist/src/index.d.ts +107 -0
- package/dist/src/index.js +802 -0
- package/dist/src/site-worker.d.ts +7 -0
- package/dist/src/site-worker.js +6 -0
- package/dist/src/store.d.ts +60 -0
- package/dist/src/store.js +63 -0
- package/dist/store.d.ts +60 -0
- package/dist/store.js +64 -0
- package/package.json +19 -8
- package/bun.lock +0 -220
- package/src/index.ts +0 -538
- package/tsconfig.json +0 -33
- package/wrangler.jsonc +0 -29
package/README.md
CHANGED
|
@@ -37,21 +37,312 @@ JotDB uses Cloudflare Durable Objects under the hood, which means you can organi
|
|
|
37
37
|
|
|
38
38
|
1. **Global Store**: Use a single instance for your entire application
|
|
39
39
|
```typescript
|
|
40
|
-
const db = env.JOTDB.
|
|
40
|
+
const db = env.JOTDB.getByName("global");
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
2. **Per-User Store**: Create a separate instance for each user
|
|
44
44
|
```typescript
|
|
45
|
-
const userDb = env.JOTDB.
|
|
45
|
+
const userDb = env.JOTDB.getByName(`user:${userId}`);
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
3. **Per-Event Store**: Create temporary stores for events or sessions
|
|
49
49
|
```typescript
|
|
50
|
-
const eventDb = env.JOTDB.
|
|
50
|
+
const eventDb = env.JOTDB.getByName(`event:${eventId}`);
|
|
51
51
|
```
|
|
52
52
|
|
|
53
53
|
Each instance is isolated and can have its own schema and options. This follows the Actor Model pattern, where each instance is an independent actor that manages its own state.
|
|
54
54
|
|
|
55
|
+
## JotDB vs D1
|
|
56
|
+
|
|
57
|
+
Cloudflare offers D1 (a managed SQLite at the edge). JotDB is not a replacement — it sits in a different niche. Pick deliberately:
|
|
58
|
+
|
|
59
|
+
| Concern | JotDB | D1 |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| Storage model | One blob per Durable Object (`data` key holds the whole object or array) | Relational tables, rows, indexes |
|
|
62
|
+
| Query language | Direct method calls: `get`, `set`, `getAll`, `keys`, `has`, `push` | SQL (`SELECT`, `JOIN`, `WHERE`, `ORDER BY`) |
|
|
63
|
+
| Indexes | None — `getAll()` returns everything in the DO | B-tree indexes, query planner |
|
|
64
|
+
| Schema | Optional, auto-inferred Zod (`string` / `number` / `boolean` / `email` / `array` / `object` / `any`) | Strict DDL, `ALTER TABLE`, types enforced at write |
|
|
65
|
+
| Migrations | None — schema changes only emit `console.warn`; existing data is left as-is | Required: managed via `wrangler d1 migrations` |
|
|
66
|
+
| Consistency | Strong per-DO (single-writer actor) | Strong per-database; primary/replica replication for reads |
|
|
67
|
+
| Locality | The DO lives near its first caller; one hop for all reads/writes against that instance | Database has a primary region; reads can be served from replicas |
|
|
68
|
+
| Concurrency | Serialized inside one DO; unlimited DOs in parallel | Many concurrent connections, transactions across rows |
|
|
69
|
+
| Hot dataset size | Small — the whole document is rehydrated into memory on access. Practical ceiling: hundreds of KB to a few MB per instance. | Gigabytes per database |
|
|
70
|
+
| Cross-entity queries | Not possible — each DO is an island | Trivial — joins are the point |
|
|
71
|
+
| Setup cost | `getByName("foo")` | Create database, write migrations, manage schema |
|
|
72
|
+
| Best fit | Per-user / per-room / per-tenant documents | Shared, queryable application data |
|
|
73
|
+
|
|
74
|
+
Rule of thumb: **if the question "give me all rows where X" needs to span instances, you want D1.** If every read/write naturally scopes to one user, one room, one event, one job — JotDB.
|
|
75
|
+
|
|
76
|
+
## Why not D1?
|
|
77
|
+
|
|
78
|
+
D1 is excellent. Reach for JotDB instead when:
|
|
79
|
+
|
|
80
|
+
- **The data is naturally partitioned.** A user's notes, a chat room's messages, a workflow's state — these never need to be joined across partitions. D1 forces you to add a `user_id` column and remember to filter by it on every query. With JotDB, isolation is structural: `env.JOTDB.getByName(\`user:${userId}\`)` cannot accidentally leak across users.
|
|
81
|
+
- **You want zero migration overhead.** D1 schema changes require a migration file, deployment, and a backfill plan. JotDB shapes are inferred from the first write and re-inferred whenever you call `setSchema` — adding a new field is just writing it.
|
|
82
|
+
- **Latency matters more than queryability.** A Durable Object lives in one location and serves reads from in-memory state after the first hit. There is no SQL parser, no query planner, no network hop to a separate database service.
|
|
83
|
+
- **The access pattern is RPC, not query.** If your code already looks like `db.get("settings")` and `db.set("settings", {...})`, putting SQL in front of it is overhead.
|
|
84
|
+
- **You need stateful coordination, not just storage.** Because JotDB extends `DurableObject`, you can add your own methods (broadcasts, alarms, websockets) on top of the same actor that owns the data.
|
|
85
|
+
|
|
86
|
+
Reach for D1 instead when: you need ad-hoc queries, reporting, aggregations, joins across users, full-text search, or anything resembling "show me the top 10 X across the whole system." JotDB cannot do that — it does not have a query engine.
|
|
87
|
+
|
|
88
|
+
## Starter recipes
|
|
89
|
+
|
|
90
|
+
### 1. Per-user document store
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Each user gets an isolated DO. No `WHERE user_id = ?` to forget.
|
|
94
|
+
const userDb = env.JOTDB.getByName(`user:${userId}`);
|
|
95
|
+
|
|
96
|
+
await userDb.set("profile", { name: "Ada", email: "ada@example.com" });
|
|
97
|
+
await userDb.set("preferences", { theme: "dark", density: "compact" });
|
|
98
|
+
|
|
99
|
+
const profile = await userDb.get("profile");
|
|
100
|
+
const everything = await userDb.getAll();
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Append-only event log (array mode)
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
// Calling push() puts the DO in array mode and infers the item schema.
|
|
107
|
+
const log = env.JOTDB.getByName(`audit:${tenantId}`);
|
|
108
|
+
|
|
109
|
+
await log.push({ at: Date.now(), actor: "ada", action: "login" });
|
|
110
|
+
await log.push({ at: Date.now(), actor: "ada", action: "open-doc", docId: "d1" });
|
|
111
|
+
|
|
112
|
+
const events = await log.getAll(); // unknown[]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Caveat: arrays are stored as a single value. Keep the log bounded (rotate to R2 or another DO when it grows past a few thousand entries).
|
|
116
|
+
|
|
117
|
+
### 3. Session / ephemeral store
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
const session = env.JOTDB.getByName(`session:${sessionId}`);
|
|
121
|
+
|
|
122
|
+
await session.setSchema({ userId: "string", csrf: "string", expiresAt: "number" });
|
|
123
|
+
await session.setAll({ userId, csrf: crypto.randomUUID(), expiresAt: Date.now() + 3600_000 });
|
|
124
|
+
|
|
125
|
+
const s = await session.getAll();
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 4. Feature flags (read-mostly, validated)
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
const flags = env.JOTDB.getByName("flags:global");
|
|
132
|
+
|
|
133
|
+
await flags.setSchema({ newCheckout: "boolean", maxUploadMb: "number", betaUsers: "array" });
|
|
134
|
+
await flags.setAll({ newCheckout: false, maxUploadMb: 25, betaUsers: ["ada", "linus"] });
|
|
135
|
+
|
|
136
|
+
// Lock it after deploy:
|
|
137
|
+
await flags.setOptions({ readOnly: true });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### 5. Form submission collector
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const responses = env.JOTDB.getByName(`form:${formId}`);
|
|
144
|
+
|
|
145
|
+
// First push infers the schema from the submission shape.
|
|
146
|
+
await responses.push({ email: "ada@example.com", rating: 5, comment: "great" });
|
|
147
|
+
|
|
148
|
+
// Subsequent pushes are validated against that inferred shape.
|
|
149
|
+
await responses.push({ email: "not-an-email", rating: 5, comment: "..." });
|
|
150
|
+
// throws: Validation failed: Invalid email
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### 6. Wrapping for an external HTTP API
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
import { Hono } from "hono";
|
|
157
|
+
|
|
158
|
+
const app = new Hono<{ Bindings: Env }>();
|
|
159
|
+
|
|
160
|
+
app.get("/users/:id", async (c) => {
|
|
161
|
+
const db = c.env.JOTDB.getByName(`user:${c.req.param("id")}`);
|
|
162
|
+
return c.json(await db.getAll());
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
app.put("/users/:id/profile", async (c) => {
|
|
166
|
+
const db = c.env.JOTDB.getByName(`user:${c.req.param("id")}`);
|
|
167
|
+
await db.set("profile", await c.req.json());
|
|
168
|
+
return c.json({ ok: true });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
export default app;
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Migration-less schema evolution
|
|
175
|
+
|
|
176
|
+
Most databases require a migration step when the shape of your data changes. JotDB doesn't have one — there is no DDL. Here is what actually happens, and where the sharp edges are.
|
|
177
|
+
|
|
178
|
+
### Adding a field
|
|
179
|
+
|
|
180
|
+
Use `extendSchema` for additive changes, or replace the full schema with `setSchema`. Auto-inference only happens on a fresh database and should not be relied on for schema evolution.
|
|
181
|
+
|
|
182
|
+
```typescript
|
|
183
|
+
const db = env.JOTDB.getByName("user:42");
|
|
184
|
+
|
|
185
|
+
// v1
|
|
186
|
+
await db.setAll({ name: "Ada", email: "ada@example.com" });
|
|
187
|
+
|
|
188
|
+
// v2: add `plan` — no migration, no downtime
|
|
189
|
+
await db.extendSchema({ plan: "string" });
|
|
190
|
+
await db.set("plan", "pro");
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Existing instances that have not been touched still hold the v1 shape. They are valid until the next write — at which point validation runs against the current in-memory schema for that DO.
|
|
194
|
+
|
|
195
|
+
### Removing or renaming a field
|
|
196
|
+
|
|
197
|
+
```typescript
|
|
198
|
+
// Old shape contained `nickname`. New writes drop it.
|
|
199
|
+
await db.setSchema({ name: "string", email: "email" });
|
|
200
|
+
await db.migrate((old) => ({ name: old.name, email: old.email }));
|
|
201
|
+
// stored: { name: "Ada", email: "ada@example.com" }
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
JotDB does not currently perform an automatic destructive migration for removed fields. Use `migrate()` when you want to rewrite existing data deliberately.
|
|
205
|
+
|
|
206
|
+
### Changing a field's type
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
// v1: age was a string ("30")
|
|
210
|
+
// v2: age is a number
|
|
211
|
+
await db.setSchema({ name: "string", age: "number" });
|
|
212
|
+
// console: [JotDB] Type changed for "age": string → number
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
JotDB will log a warning via `console.warn`, but **it does not migrate existing data**. The next write that includes `age` must conform to the new type, or validation throws. If you need to coerce, do it explicitly:
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
const current = (await db.getAll()) as { name: string; age: string };
|
|
219
|
+
await db.setAll({ name: current.name, age: Number(current.age) });
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Honest caveats
|
|
223
|
+
|
|
224
|
+
- **There is no global "apply migration" step.** Each DO instance carries its own copy of the schema and its own data. A schema change in one tenant's DO does not propagate to another tenant's DO until that DO is accessed and rewritten.
|
|
225
|
+
- **`setSchema` does not validate existing data.** It only affects future writes. Old data that conflicts with the new schema will sit there until it is read and rewritten.
|
|
226
|
+
- **Schema inference is shallow.** Nested objects collapse to `"object"` (i.e. `z.record(z.any())`) and arrays-of-arrays collapse to `"array"` (i.e. `z.array(z.any())`). For deep validation, set the schema explicitly and validate at the application boundary.
|
|
227
|
+
- **`email` detection during inference is a heuristic** (`includes("@")`). For trustworthy validation, set the schema yourself.
|
|
228
|
+
|
|
229
|
+
If your data lifecycle requires "all rows must conform to schema vN before deploying code that assumes vN," you want D1 with migrations, not JotDB.
|
|
230
|
+
|
|
231
|
+
## Honest tradeoffs
|
|
232
|
+
|
|
233
|
+
JotDB is small on purpose. Things it deliberately does not do:
|
|
234
|
+
|
|
235
|
+
- **No queries, no indexes, no joins.** `getAll()` returns the entire blob. If you need to filter, do it in your Worker after fetching. If you need to filter across users, you have the wrong tool.
|
|
236
|
+
- **The whole document is rehydrated on access.** Each DO loads its `data` key into memory on first call after eviction. Keep the per-instance payload small — think tens to low-hundreds of KB. If you are heading toward megabytes per instance, split into more DOs.
|
|
237
|
+
- **One writer per instance.** Durable Objects serialize writes to one instance. That is a feature (no race conditions) and a limit (no parallel writes inside one DO). Shard across DOs by tenant / user / room.
|
|
238
|
+
- **Audit log is bounded to 100 entries.** `getAuditLog()` returns the most recent 100 actions per DO. It is intended for debugging and lightweight forensics, not as a system of record.
|
|
239
|
+
- **Schema enforcement is best-effort.** Types are limited to `string`, `number`, `boolean`, `email`, `array`, `object`, `any`. There is no `enum`, no `union`, no nested validation. For richer validation, validate with your own Zod schema in the Worker before calling `set`.
|
|
240
|
+
- **No transactions across instances.** A write to `user:42` and a write to `user:99` are independent. If you need atomicity across entities, you are modelling the wrong boundary.
|
|
241
|
+
- **Cold-start cost.** The first request to an idle DO pays a hydration round-trip to storage. Subsequent requests serve from memory. For latency-sensitive paths, keep DOs warm or accept the first-hit penalty.
|
|
242
|
+
- **Cost model is per-DO.** Many small DOs is the intended shape; that means many DO requests and many storage operations. Model your cost against `requests × instances`, not against a single shared database.
|
|
243
|
+
|
|
244
|
+
If those tradeoffs do not fit your workload, that is a useful signal — either you want D1 (relational, queryable) or you want raw Durable Object storage (no validation layer, more control).
|
|
245
|
+
|
|
246
|
+
## Benchmarking
|
|
247
|
+
|
|
248
|
+
JotDB includes a real Durable Object benchmark endpoint for testing the workloads it is designed for:
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
curl 'http://localhost:5173/bench?mode=user-prefs&count=100'
|
|
252
|
+
curl 'http://localhost:5173/bench?mode=feature-flags&count=1000'
|
|
253
|
+
curl 'http://localhost:5173/bench?mode=chat-append&count=100'
|
|
254
|
+
curl 'http://localhost:5173/bench?mode=hot-key&count=100'
|
|
255
|
+
curl 'http://localhost:5173/bench?mode=multi-instance&count=100'
|
|
256
|
+
curl 'http://localhost:5173/bench?mode=cold-warm&count=100'
|
|
257
|
+
curl 'http://localhost:5173/bench?mode=schema-validation&count=100'
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Each benchmark returns real Worker timing data:
|
|
261
|
+
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"mode": "user-prefs",
|
|
265
|
+
"operations": 100,
|
|
266
|
+
"durationMs": 42.1,
|
|
267
|
+
"opsPerSecond": 2375,
|
|
268
|
+
"p50Ms": 0.31,
|
|
269
|
+
"p95Ms": 0.89,
|
|
270
|
+
"p99Ms": 1.2,
|
|
271
|
+
"errors": 0
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
The benchmark modes intentionally map to practical JotDB workloads rather than synthetic key-value loops.
|
|
276
|
+
|
|
277
|
+
To collect production numbers safely, do not expose the benchmark route on `workers.dev`. This repo sets `workers_dev: false` by default, and the Worker refuses every HTTP request unless `HTTP_ENABLED` is explicitly configured. First claim a personal custom route with a placeholder Worker, put Cloudflare Access in front of it, verify Access from an incognito browser, then deploy the real Worker. Also set a benchmark token:
|
|
278
|
+
|
|
279
|
+
```bash
|
|
280
|
+
wrangler secret put BENCH_TOKEN
|
|
281
|
+
wrangler secret put HTTP_ENABLED
|
|
282
|
+
npm run deploy
|
|
283
|
+
curl -H "Authorization: Bearer $BENCH_TOKEN" 'https://jotdb.<your-personal-domain>/bench?mode=user-prefs&count=100'
|
|
284
|
+
curl -H "Authorization: Bearer $BENCH_TOKEN" 'https://jotdb.<your-personal-domain>/bench?mode=feature-flags&count=1000'
|
|
285
|
+
curl -H "Authorization: Bearer $BENCH_TOKEN" 'https://jotdb.<your-personal-domain>/bench?mode=chat-append&count=100'
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The token gate is intentionally checked before any Durable Object access. If you later add Workers AI or other billable bindings, keep the same pattern: Access at the edge plus a code-level token/rate-limit gate before touching the binding.
|
|
289
|
+
|
|
290
|
+
For a useful README benchmark snapshot, run each mode 5-10 times and report the median result. Keep `count` fixed when comparing local and deployed runs, and avoid claiming one-off best-case numbers. The endpoint is intentionally capped at 1,000 operations per request so benchmark requests do not accidentally become load tests.
|
|
291
|
+
|
|
292
|
+
The benchmark modes intentionally map to practical JotDB workloads rather than synthetic key-value loops:
|
|
293
|
+
|
|
294
|
+
- `user-prefs`: repeated updates to one user settings object
|
|
295
|
+
- `feature-flags`: repeated reads from a tenant flag object
|
|
296
|
+
- `chat-append`: append-only room history
|
|
297
|
+
- `hot-key`: many concurrent writes to one Durable Object
|
|
298
|
+
- `multi-instance`: one write across many isolated users
|
|
299
|
+
- `cold-warm`: first-hit versus repeated access to the same object
|
|
300
|
+
- `schema-validation`: repeated Zod-validated writes
|
|
301
|
+
|
|
302
|
+
## SQLite-backed collections
|
|
303
|
+
|
|
304
|
+
For bounded collections, receipts, chat history, or per-entity streams, JotDB now supports a SQLite-backed store inside the Durable Object:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
const db = env.JOTDB.getByName("loop:42");
|
|
308
|
+
|
|
309
|
+
await db.append("receipt", { status: "ok", body: "finished" });
|
|
310
|
+
await db.appendCapped("receipt", { status: "ok", body: "finished" }, 100);
|
|
311
|
+
|
|
312
|
+
const page = await db.scan("receipt:", { limit: 20 });
|
|
313
|
+
const next = await db.scan("receipt:", { limit: 20, cursor: page.cursor });
|
|
314
|
+
|
|
315
|
+
await db.retention("receipt:", 30 * 24 * 60 * 60 * 1000);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
This is different from the original object-mode API. Use object mode for one bounded document per entity; use SQLite-backed collections for bounded append streams, receipts, chat history, and paginated local records. JotDB is still not a replacement for D1 joins, analytics, or cross-entity queries.
|
|
319
|
+
|
|
320
|
+
For local verification, run the Worker with real workerd storage:
|
|
321
|
+
|
|
322
|
+
```bash
|
|
323
|
+
npx wrangler dev --local --port 8789 --var HTTP_ENABLED:1
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
The Durable Object uses `new_sqlite_classes` in `wrangler.jsonc`. SQLite mode must be chosen before first production deployment because it cannot be retrofitted onto an already-deployed non-SQLite Durable Object class.
|
|
327
|
+
|
|
328
|
+
## Demo site
|
|
329
|
+
|
|
330
|
+
A deliberately minimal landing page lives in `demo/index.html`. It is meant to be the simplest possible public-facing demo before a fuller design pass. It currently communicates the core JotDB story:
|
|
331
|
+
|
|
332
|
+
- one Durable Object per entity
|
|
333
|
+
- no SQL or migrations
|
|
334
|
+
- schema-validated state
|
|
335
|
+
- user preferences
|
|
336
|
+
- feature flags
|
|
337
|
+
- chat history
|
|
338
|
+
- why JotDB is different from D1
|
|
339
|
+
|
|
340
|
+
Open it locally with any static file server:
|
|
341
|
+
|
|
342
|
+
```bash
|
|
343
|
+
npx serve demo
|
|
344
|
+
```
|
|
345
|
+
|
|
55
346
|
## Installation
|
|
56
347
|
|
|
57
348
|
```bash
|
|
@@ -92,8 +383,7 @@ export interface Env {
|
|
|
92
383
|
export default {
|
|
93
384
|
async fetch(request: Request, env: Env) {
|
|
94
385
|
// Initialize the database
|
|
95
|
-
const
|
|
96
|
-
const db = env.JOTDB.get(jotId);
|
|
386
|
+
const db = env.JOTDB.getByName("my-db");
|
|
97
387
|
|
|
98
388
|
// Example operations
|
|
99
389
|
await db.set("user:123", { name: "John", age: 30 });
|
|
@@ -132,7 +422,7 @@ export default {
|
|
|
132
422
|
|
|
133
423
|
```typescript
|
|
134
424
|
interface JotDBOptions {
|
|
135
|
-
autoStrip: boolean; //
|
|
425
|
+
autoStrip: boolean; // Reserved for future explicit strip behavior
|
|
136
426
|
readOnly: boolean; // Enable read-only mode
|
|
137
427
|
}
|
|
138
428
|
```
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { DurableObject } from "cloudflare:workers";
|
|
3
3
|
type SchemaType = "string" | "number" | "boolean" | "email" | "array" | "object" | "any";
|
|
4
|
-
|
|
4
|
+
interface FieldDescriptor {
|
|
5
|
+
type: SchemaType;
|
|
6
|
+
default?: unknown;
|
|
7
|
+
optional?: boolean;
|
|
8
|
+
}
|
|
9
|
+
type FieldSpec = SchemaType | FieldDescriptor;
|
|
10
|
+
type ObjectSchema = Record<string, FieldSpec>;
|
|
11
|
+
type ArraySchema = {
|
|
12
|
+
__arrayType: SchemaType | ObjectSchema;
|
|
13
|
+
};
|
|
14
|
+
type SchemaDefinition = ObjectSchema | ArraySchema;
|
|
5
15
|
interface JotDBOptions {
|
|
6
16
|
autoStrip: boolean;
|
|
7
17
|
readOnly: boolean;
|
|
@@ -17,11 +27,14 @@ export declare class JotDB extends DurableObject {
|
|
|
17
27
|
private zodSchema;
|
|
18
28
|
private options;
|
|
19
29
|
private auditLog;
|
|
30
|
+
private store;
|
|
20
31
|
constructor(state: any, env: Env);
|
|
21
32
|
load(): Promise<void>;
|
|
22
33
|
save(): Promise<void>;
|
|
23
34
|
isArrayMode(): boolean;
|
|
35
|
+
private inferSchemaFromValue;
|
|
24
36
|
push(item: unknown): Promise<void>;
|
|
37
|
+
private applyDefaultsForObjectSchema;
|
|
25
38
|
setAll(objOrArr: Record<string, unknown> | unknown[]): Promise<void>;
|
|
26
39
|
getAll(): Promise<unknown>;
|
|
27
40
|
logAudit(action: string, keys: string[] | string): Promise<void>;
|
|
@@ -33,16 +46,60 @@ export declare class JotDB extends DurableObject {
|
|
|
33
46
|
getSchema(): Promise<SchemaDefinition>;
|
|
34
47
|
private warnSchemaDiff;
|
|
35
48
|
setSchema(schemaObj: SchemaDefinition): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Non-destructively merge additional fields into the current object schema.
|
|
51
|
+
* Useful for additive schema evolution without a full migration.
|
|
52
|
+
* Throws if the current schema is an array schema.
|
|
53
|
+
* New fields with `default` are backfilled into existing stored data.
|
|
54
|
+
* New fields without `default` and without `optional: true` will cause
|
|
55
|
+
* existing stored objects to fail validation on next write — prefer adding
|
|
56
|
+
* `default` or `optional: true` for safe rollouts.
|
|
57
|
+
*/
|
|
58
|
+
extendSchema(partial: ObjectSchema): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* Validate arbitrary data (or current stored data if omitted) against the
|
|
61
|
+
* current schema without throwing. Returns `{ ok, errors, data }` where
|
|
62
|
+
* `data` is the (possibly default-applied / coerced) parsed result on
|
|
63
|
+
* success. If no schema is set, returns `{ ok: true }`.
|
|
64
|
+
*/
|
|
65
|
+
validate(data?: unknown): Promise<{
|
|
66
|
+
ok: boolean;
|
|
67
|
+
errors?: string[];
|
|
68
|
+
data?: unknown;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Apply a transform to the stored data. For object mode, `fn` is called once
|
|
72
|
+
* with the entire object and must return the new object. For array mode, `fn`
|
|
73
|
+
* is called once per item and must return the replacement item (return
|
|
74
|
+
* undefined to drop the item).
|
|
75
|
+
* If a schema is set, the resulting data is validated before being saved;
|
|
76
|
+
* on validation failure the original data is left untouched and the function
|
|
77
|
+
* throws. Records an audit entry on success.
|
|
78
|
+
*/
|
|
79
|
+
migrate(fn: (value: any, key?: string | number) => any): Promise<void>;
|
|
36
80
|
setOptions(opts: Partial<JotDBOptions>): Promise<void>;
|
|
37
81
|
getOptions(): Promise<JotDBOptions>;
|
|
38
82
|
getAuditLog(): Promise<AuditLogEntry[]>;
|
|
39
83
|
clearAuditLog(): Promise<void>;
|
|
40
84
|
private buildZodSchema;
|
|
85
|
+
private applyObjectDefaults;
|
|
41
86
|
fetch(request: Request): Promise<Response>;
|
|
87
|
+
scan<T = unknown>(prefix?: string, options?: {
|
|
88
|
+
limit?: number;
|
|
89
|
+
cursor?: string;
|
|
90
|
+
}): Promise<{
|
|
91
|
+
items: T[];
|
|
92
|
+
cursor?: string;
|
|
93
|
+
}>;
|
|
94
|
+
append<T = unknown>(stream: string, value: T): Promise<string>;
|
|
95
|
+
appendCapped<T = unknown>(stream: string, value: T, max: number): Promise<string>;
|
|
96
|
+
retention(prefix: string, maxAgeMs: number): Promise<void>;
|
|
42
97
|
set(key: string, value: unknown): Promise<void>;
|
|
43
98
|
}
|
|
44
99
|
export interface Env {
|
|
45
100
|
JOTDB: DurableObjectNamespace;
|
|
101
|
+
BENCH_TOKEN: string;
|
|
102
|
+
HTTP_ENABLED?: string;
|
|
46
103
|
}
|
|
47
104
|
declare const app: Hono<{
|
|
48
105
|
Bindings: Env;
|