mindoodb-app-sdk 0.0.14 → 0.0.17
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 +410 -132
- package/dist/index.d.ts +97 -4
- package/dist/index.js +367 -55
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +300 -40
- package/dist/{types-B1iKRIuy.d.ts → types-Dw6YRApH.d.ts} +135 -9
- package/package.json +13 -11
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ The bridge carries RPC calls (documents, views, database info), binary attachmen
|
|
|
49
49
|
## Install
|
|
50
50
|
|
|
51
51
|
```bash
|
|
52
|
-
|
|
52
|
+
pnpm add mindoodb-app-sdk
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
## Get started in 5 minutes
|
|
@@ -59,8 +59,8 @@ The fastest way to start building is to clone the **example app**, run it locall
|
|
|
59
59
|
```bash
|
|
60
60
|
git clone https://github.com/klehmann/mindoodb-app-example.git
|
|
61
61
|
cd mindoodb-app-example
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
pnpm install
|
|
63
|
+
pnpm dev:local
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
This spins up a local Vite dev server on `http://localhost:4200` with hot module reload.
|
|
@@ -90,9 +90,12 @@ const session = await bridge.connect();
|
|
|
90
90
|
|
|
91
91
|
// 2. Read the launch context
|
|
92
92
|
const ctx = await session.getLaunchContext();
|
|
93
|
-
console.log("Runtime:", ctx.runtime);
|
|
94
|
-
console.log("Theme:", ctx.theme.mode);
|
|
95
|
-
console.log(
|
|
93
|
+
console.log("Runtime:", ctx.runtime); // "iframe" | "window"
|
|
94
|
+
console.log("Theme:", ctx.theme.mode); // "light" | "dark"
|
|
95
|
+
console.log(
|
|
96
|
+
"Databases:",
|
|
97
|
+
ctx.databases.map((db) => db.title),
|
|
98
|
+
);
|
|
96
99
|
|
|
97
100
|
// 3. Subscribe to live host events
|
|
98
101
|
session.onThemeChange((theme) => {
|
|
@@ -230,15 +233,15 @@ Important constraints:
|
|
|
230
233
|
|
|
231
234
|
Each database mapped to your app carries a set of **capabilities** that Haven controls. Your app should check capabilities before attempting operations and adapt its UI accordingly.
|
|
232
235
|
|
|
233
|
-
| Capability
|
|
234
|
-
|
|
235
|
-
| `read`
|
|
236
|
-
| `create`
|
|
237
|
-
| `update`
|
|
238
|
-
| `delete`
|
|
239
|
-
| `history`
|
|
236
|
+
| Capability | Allows |
|
|
237
|
+
| ------------- | ------------------------------------------------------------ |
|
|
238
|
+
| `read` | List and read documents |
|
|
239
|
+
| `create` | Create new documents |
|
|
240
|
+
| `update` | Update existing documents |
|
|
241
|
+
| `delete` | Delete documents |
|
|
242
|
+
| `history` | Access document revision history and historical snapshots |
|
|
240
243
|
| `attachments` | List, upload, download, remove, and preview file attachments |
|
|
241
|
-
| `views`
|
|
244
|
+
| `views` | Create app-defined virtual views for this database |
|
|
242
245
|
|
|
243
246
|
```ts
|
|
244
247
|
const db = ctx.databases[0]!;
|
|
@@ -298,7 +301,11 @@ console.log(result.nextCursor); // opaque changefeed checkpoint or null
|
|
|
298
301
|
|
|
299
302
|
// Continue from the last checkpoint
|
|
300
303
|
const nextPage = result.nextCursor
|
|
301
|
-
? await db.documents.list({
|
|
304
|
+
? await db.documents.list({
|
|
305
|
+
cursor: result.nextCursor,
|
|
306
|
+
limit: 50,
|
|
307
|
+
fields: ["title"],
|
|
308
|
+
})
|
|
302
309
|
: null;
|
|
303
310
|
|
|
304
311
|
// Fast metadata-only listing (IDs + deletion state only)
|
|
@@ -319,54 +326,259 @@ const window = await db.documents.list({
|
|
|
319
326
|
// Read
|
|
320
327
|
const doc = await db.documents.get(docId);
|
|
321
328
|
|
|
322
|
-
// Create (optionally
|
|
329
|
+
// Create (MindooDB generates a UUID7 id; optionally use a named document key)
|
|
323
330
|
const created = await db.documents.create({
|
|
324
331
|
set: { title: "Meeting notes", content: "..." },
|
|
325
|
-
decryptionKeyId: "team-key",
|
|
332
|
+
decryptionKeyId: "team-key", // omit to use "default"
|
|
326
333
|
});
|
|
327
334
|
|
|
328
|
-
//
|
|
335
|
+
// Create with a caller-provided id (e.g. a known well-known document the app
|
|
336
|
+
// always loads on launch, or migrating an id from another system).
|
|
337
|
+
const settings = await db.documents.create({
|
|
338
|
+
id: "AppSettings",
|
|
339
|
+
set: { theme: "dark" },
|
|
340
|
+
});
|
|
341
|
+
// Subsequent calls with the same id return the existing document and DO NOT
|
|
342
|
+
// overwrite `set`, so `documents.create({ id })` is safe to call on every app
|
|
343
|
+
// launch as a "create-if-missing" primitive. Custom ids must match
|
|
344
|
+
// /^[A-Za-z][A-Za-z0-9_]*$/ (first char must be a letter, subsequent chars
|
|
345
|
+
// may be letters, digits, or `_`). Documents created with the same custom id
|
|
346
|
+
// on different replicas merge correctly when synced.
|
|
347
|
+
|
|
348
|
+
// Update top-level fields
|
|
329
349
|
const updated = await db.documents.update(docId, {
|
|
330
350
|
set: { title: "Updated title" },
|
|
331
351
|
unset: ["legacyField"],
|
|
332
352
|
});
|
|
333
353
|
|
|
354
|
+
// Update with a granular text patch (see "Collaborative text editing" below).
|
|
355
|
+
// `baseHeads` is the document version this edit was authored against; Haven
|
|
356
|
+
// merges these splices with any concurrent changes since `baseHeads`.
|
|
357
|
+
const merged = await db.documents.update(docId, {
|
|
358
|
+
text: [
|
|
359
|
+
{
|
|
360
|
+
path: ["body"],
|
|
361
|
+
baseHeads: doc!.heads,
|
|
362
|
+
edits: [
|
|
363
|
+
{ index: 0, deleteCount: 0, insert: "Hello, " },
|
|
364
|
+
{ index: 12, deleteCount: 5, insert: "team" },
|
|
365
|
+
],
|
|
366
|
+
},
|
|
367
|
+
],
|
|
368
|
+
});
|
|
369
|
+
|
|
334
370
|
// Delete
|
|
335
371
|
await db.documents.delete(docId);
|
|
372
|
+
|
|
373
|
+
// Undelete (resurrect a deleted document)
|
|
374
|
+
await db.documents.undelete(docId);
|
|
336
375
|
```
|
|
337
376
|
|
|
377
|
+
Every `MindooDBAppDocument` snapshot also exposes a `heads?: string[]` field. Those heads identify the document's current Automerge version and are the value you pass to `baseHeads` when issuing the next text patch. After Haven applies and merges your patch, the returned document carries the new `heads` you should use for the following edit cycle.
|
|
378
|
+
|
|
338
379
|
`documents.list()` is backed by Haven's internal changefeed, not by a positional offset. The query object supports:
|
|
339
380
|
|
|
340
|
-
| Field
|
|
341
|
-
|
|
342
|
-
| `cursor?: string \| null`
|
|
343
|
-
| `limit?: number`
|
|
344
|
-
| `skip?: number`
|
|
345
|
-
| `status?: "all" \| "existing" \| "deleted"` | Filter by deletion state (default `"existing"`)
|
|
346
|
-
| `metadataOnly?: boolean`
|
|
347
|
-
| `fields?: string[]`
|
|
348
|
-
| `filter?: Record<string, unknown>`
|
|
381
|
+
| Field | Meaning |
|
|
382
|
+
| ------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
383
|
+
| `cursor?: string \| null` | Opaque changefeed checkpoint previously returned by `nextCursor` |
|
|
384
|
+
| `limit?: number` | Maximum number of matching entries to return (default `50`) |
|
|
385
|
+
| `skip?: number` | Skip matching entries after the cursor without loading full document bodies |
|
|
386
|
+
| `status?: "all" \| "existing" \| "deleted"` | Filter by deletion state (default `"existing"`) |
|
|
387
|
+
| `metadataOnly?: boolean` | Return only `{ id, isDeleted }` for speed |
|
|
388
|
+
| `fields?: string[]` | Project specific JSON fields when `metadataOnly` is `false` |
|
|
389
|
+
| `filter?: Record<string, unknown>` | Simple equality filter applied to document data when `metadataOnly` is `false` |
|
|
349
390
|
|
|
350
391
|
`nextCursor` is the latest checkpoint reached by the page. Persist it after each successful call when you are building your own index or sync loop. If there were no changes after the supplied cursor, `nextCursor` is `null`.
|
|
351
392
|
|
|
352
|
-
`documents.update()` accepts
|
|
393
|
+
`documents.update()` accepts three independent operations on the same call:
|
|
394
|
+
|
|
395
|
+
- `set` -- assign top-level fields (shallow JSON merge).
|
|
396
|
+
- `unset` -- remove top-level fields entirely.
|
|
397
|
+
- `text` -- one or more **granular text patches** applied to specific JSON paths. Each patch carries the `baseHeads` version it was authored against and is merged on the Haven side with any concurrent changes that arrived since. This is the recommended way to edit collaborative text fields like a markdown body. See [Collaborative text editing](#collaborative-text-editing) below.
|
|
398
|
+
|
|
399
|
+
Prefer sending small, intentional field-level changes instead of rewriting whole documents when only a few fields changed. This keeps MindooDB change tracking more meaningful and helps Automerge produce cleaner merge results.
|
|
353
400
|
|
|
354
401
|
When Haven can resolve the signing identity from the tenant directory, list items may also include `identityLabel` and `publicKeyFingerprint` for the latest visible change. Apps can use this to show who last touched a document without loading the full revision history first.
|
|
355
402
|
|
|
356
403
|
### Document history
|
|
357
404
|
|
|
358
|
-
When the `history` capability is granted, you can walk the
|
|
405
|
+
When the `history` capability is granted, you can walk the document revision list and load historical snapshots. Prefer `revisionId` for follow-up reads: it is a stable, DAG-backed identifier for the exact document revision, while timestamps are mainly useful for display and backwards-compatible point-in-time lookups.
|
|
359
406
|
|
|
360
407
|
```ts
|
|
361
408
|
// List all revisions
|
|
362
409
|
const history = await db.documents.listHistory(docId);
|
|
363
|
-
// Each entry:
|
|
410
|
+
// Each entry:
|
|
411
|
+
// {
|
|
412
|
+
// revisionId,
|
|
413
|
+
// timestamp,
|
|
414
|
+
// heads?,
|
|
415
|
+
// publicKey,
|
|
416
|
+
// identityLabel?,
|
|
417
|
+
// isDeleted,
|
|
418
|
+
// isCurrent,
|
|
419
|
+
// summary?
|
|
420
|
+
// }
|
|
421
|
+
|
|
422
|
+
// Load the document state at an exact DAG revision
|
|
423
|
+
const snapshot = await db.documents.getAtRevision(
|
|
424
|
+
docId,
|
|
425
|
+
history[0]!.revisionId,
|
|
426
|
+
);
|
|
427
|
+
// {
|
|
428
|
+
// id,
|
|
429
|
+
// revisionId,
|
|
430
|
+
// timestamp,
|
|
431
|
+
// heads?,
|
|
432
|
+
// state: "exists" | "deleted" | "missing",
|
|
433
|
+
// data,
|
|
434
|
+
// attachments?,
|
|
435
|
+
// attachmentSnapshotRevisionId?
|
|
436
|
+
// }
|
|
437
|
+
|
|
438
|
+
// Timestamp lookup is still available when all you have is a timestamp.
|
|
439
|
+
const pointInTime = await db.documents.getAtTimestamp(
|
|
440
|
+
docId,
|
|
441
|
+
history[0]!.timestamp,
|
|
442
|
+
);
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
Historical snapshots include the document data payload as it existed at that revision. When the snapshot has attachments, `attachments` reflects the historical `_attachments` array for that same revision, including the attachment reference that Haven needs to stop at the correct historical `lastChunkId`.
|
|
446
|
+
|
|
447
|
+
### Collaborative text editing
|
|
448
|
+
|
|
449
|
+
MindooDB stores collaborative text fields as Automerge text CRDTs internally, but apps never touch Automerge directly -- the SDK bridge is JSON-only. Instead, your app describes text changes as **granular splice operations** against a known document version, and Haven applies and merges them on its side.
|
|
450
|
+
|
|
451
|
+
Two layers are available:
|
|
452
|
+
|
|
453
|
+
- **Low level**: raw text patches via `documents.update({ text: [...] })`.
|
|
454
|
+
- **High level**: `createMindooDBTextBuffer(...)` -- a small editor-friendly helper that tracks local edits and flushes them as one patch.
|
|
455
|
+
|
|
456
|
+
The end-to-end flow is the same in both cases:
|
|
457
|
+
|
|
458
|
+
```
|
|
459
|
+
┌────────────────────────────────────────────────────┐
|
|
460
|
+
edit ───► │ App buffer (local string + pending splices) │ ──► flush
|
|
461
|
+
└─────────────────────────┬──────────────────────────┘
|
|
462
|
+
│ documents.update({
|
|
463
|
+
│ text: [{ path, baseHeads, edits }]
|
|
464
|
+
│ })
|
|
465
|
+
▼
|
|
466
|
+
┌────────────────────────────────────────────────────┐
|
|
467
|
+
│ Haven applies edits at `baseHeads` and merges with │
|
|
468
|
+
│ concurrent changes via Automerge │
|
|
469
|
+
└─────────────────────────┬──────────────────────────┘
|
|
470
|
+
│ canonical merged document
|
|
471
|
+
▼
|
|
472
|
+
┌────────────────────────────────────────────────────┐
|
|
473
|
+
│ App reconciles its editor with the canonical text │
|
|
474
|
+
└────────────────────────────────────────────────────┘
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
#### Text patch shape
|
|
478
|
+
|
|
479
|
+
```ts
|
|
480
|
+
interface MindooDBAppTextEdit {
|
|
481
|
+
index: number; // 0-based position in the current string at baseHeads
|
|
482
|
+
deleteCount: number; // number of characters to remove at `index`
|
|
483
|
+
insert?: string; // text to insert at `index` after deletion
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
interface MindooDBAppTextPatch {
|
|
487
|
+
path: Array<string | number>; // path inside `document.data`, e.g. ["body"]
|
|
488
|
+
baseHeads?: string[]; // document version the edits were authored against
|
|
489
|
+
edits: MindooDBAppTextEdit[]; // applied in order
|
|
490
|
+
}
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
Each edit is a JavaScript-style `splice(index, deleteCount, insert)`. Multiple patches in a single `update()` call are applied atomically.
|
|
494
|
+
|
|
495
|
+
#### Low-level: `documents.update({ text: [...] })`
|
|
496
|
+
|
|
497
|
+
If your editor already exposes raw text operations (e.g. ProseMirror or CodeMirror transactions), forward them directly:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
const doc = await db.documents.get(docId);
|
|
501
|
+
|
|
502
|
+
// User typed "Hello, " at the beginning of the body.
|
|
503
|
+
const result = await db.documents.update(docId, {
|
|
504
|
+
text: [
|
|
505
|
+
{
|
|
506
|
+
path: ["body"],
|
|
507
|
+
baseHeads: doc!.heads,
|
|
508
|
+
edits: [{ index: 0, deleteCount: 0, insert: "Hello, " }],
|
|
509
|
+
},
|
|
510
|
+
],
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// `result.data.body` -- canonical merged text
|
|
514
|
+
// `result.heads` -- new heads to use as `baseHeads` next time
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
When two app instances send patches concurrently against the same `baseHeads`, Haven merges both via Automerge and returns the canonical text. If the merged result differs from what your editor currently displays, replace the editor content with `result.data[path]`.
|
|
518
|
+
|
|
519
|
+
#### High-level: `createMindooDBTextBuffer`
|
|
520
|
+
|
|
521
|
+
For WYSIWYG editors that emit a full markdown / plain-text string after each transaction (Milkdown, TinyMCE, etc.), the SDK ships a small buffer helper that does the splice diffing and flush bookkeeping for you:
|
|
364
522
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
523
|
+
```ts
|
|
524
|
+
import { createMindooDBTextBuffer } from "mindoodb-app-sdk";
|
|
525
|
+
|
|
526
|
+
const doc = await db.documents.get(docId);
|
|
527
|
+
const buffer = createMindooDBTextBuffer({
|
|
528
|
+
database: db,
|
|
529
|
+
document: doc!,
|
|
530
|
+
path: ["body"], // string field inside `document.data`
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
editor.value = buffer.value;
|
|
534
|
+
|
|
535
|
+
// Editor reports a new full markdown value -- the buffer turns it into one splice.
|
|
536
|
+
editor.on("update", (next: string) => {
|
|
537
|
+
buffer.replaceText(next);
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
// On save (e.g. Cmd+S), flush all pending edits to Haven.
|
|
541
|
+
async function save() {
|
|
542
|
+
const result = await buffer.flush();
|
|
543
|
+
if (result.reconciled) {
|
|
544
|
+
// Haven merged in concurrent changes; show the canonical merged text.
|
|
545
|
+
editor.value = result.value;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Re-read the document without saving (e.g. a Refresh button).
|
|
550
|
+
async function refresh() {
|
|
551
|
+
const fresh = await db.documents.get(docId);
|
|
552
|
+
if (fresh && buffer.reconcile(fresh)) {
|
|
553
|
+
editor.value = buffer.value;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
368
556
|
```
|
|
369
557
|
|
|
558
|
+
Buffer API at a glance:
|
|
559
|
+
|
|
560
|
+
| Member | Description |
|
|
561
|
+
| ------------------------------------- | -------------------------------------------------------------------------- |
|
|
562
|
+
| `value` / `toString()` | Current local text value |
|
|
563
|
+
| `dirty` / `pendingCount` | Whether/how many local splices are buffered |
|
|
564
|
+
| `splice(index, deleteCount, insert?)` | Record an exact text operation |
|
|
565
|
+
| `replaceText(next)` | Diff `value` against `next` and record one minimal splice |
|
|
566
|
+
| `reconcile(document)` | Adopt a fresh document snapshot, drop pending edits |
|
|
567
|
+
| `flush()` | Send pending edits to Haven; resolves to `{ document, value, reconciled }` |
|
|
568
|
+
|
|
569
|
+
`flush()` always sends the original `baseHeads` captured when the buffer was created (or last reconciled), so two windows editing the same document at the same heads will both produce a clean merge -- they will not overwrite each other.
|
|
570
|
+
|
|
571
|
+
#### Concurrent-edit walkthrough
|
|
572
|
+
|
|
573
|
+
1. Window A and window B both `documents.get(docId)` and create a buffer at the same `baseHeads`.
|
|
574
|
+
2. Window A inserts text at the top, window B deletes a paragraph in the middle.
|
|
575
|
+
3. Window A calls `buffer.flush()` first. Haven applies its patch, advances the document heads, and returns the canonical text to A.
|
|
576
|
+
4. Window B calls `buffer.flush()` next. Haven still applies B's patch causally at B's original `baseHeads`, then merges with A's already-stored change. B's `result.reconciled` is `true` and `result.value` contains both A's and B's edits.
|
|
577
|
+
|
|
578
|
+
#### End-to-end sample
|
|
579
|
+
|
|
580
|
+
The [`mindoodb-app-teamedit`](https://github.com/klehmann/mindoodb-app-teamedit) sample wires `MindooDBTextBuffer` to a Milkdown markdown editor with Save / Refresh / preview pane, and is the recommended starting point for building your own collaborative text app.
|
|
581
|
+
|
|
370
582
|
### Incremental sync
|
|
371
583
|
|
|
372
584
|
The changefeed-backed `documents.list()` API is also the right primitive for app-side indexes, search, and other derived caches.
|
|
@@ -449,31 +661,64 @@ while (chunk !== null) {
|
|
|
449
661
|
await reader.close();
|
|
450
662
|
|
|
451
663
|
// Upload (push-based write stream)
|
|
452
|
-
const writer = await db.attachments.openWriteStream(
|
|
664
|
+
const writer = await db.attachments.openWriteStream(
|
|
665
|
+
docId,
|
|
666
|
+
"photo.jpg",
|
|
667
|
+
"image/jpeg",
|
|
668
|
+
);
|
|
453
669
|
await writer.write(fileBytes);
|
|
454
670
|
await writer.close();
|
|
455
671
|
|
|
672
|
+
// Host-provided document scanner
|
|
673
|
+
const scan = await db.attachments.scan(docId, {
|
|
674
|
+
defaultFileName: "invoice.pdf",
|
|
675
|
+
preset: "a4-portrait",
|
|
676
|
+
mimeType: "application/pdf",
|
|
677
|
+
});
|
|
678
|
+
|
|
456
679
|
// Remove
|
|
457
680
|
await db.attachments.remove(docId, "old-file.txt");
|
|
458
681
|
```
|
|
459
682
|
|
|
683
|
+
`attachments.scan()` asks Haven to open its document scanner UI. Haven handles camera/file selection, edge detection, manual corner correction, output sizing, and filename confirmation, then writes the resulting image as a normal attachment. Hosts that do not provide scanner UI may reject this method or return `{ ok: false }`.
|
|
684
|
+
|
|
685
|
+
For historical reads, pass the same `revisionId` you received from `documents.listHistory()` or from a `MindooDBAppHistoricalDocument`. This tells Haven to resolve the attachment from that revision's document payload instead of the latest document state:
|
|
686
|
+
|
|
687
|
+
```ts
|
|
688
|
+
const history = await db.documents.listHistory(docId);
|
|
689
|
+
const revisionId = history[0]!.revisionId;
|
|
690
|
+
const historical = await db.documents.getAtRevision(docId, revisionId);
|
|
691
|
+
|
|
692
|
+
const historicalFiles = await db.attachments.list(docId, {
|
|
693
|
+
revisionId,
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
const historicalReader = await db.attachments.openReadStream(
|
|
697
|
+
docId,
|
|
698
|
+
historicalFiles[0]!.fileName,
|
|
699
|
+
{ revisionId },
|
|
700
|
+
);
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
Use `revisionId` instead of `timestamp` whenever possible. Attachment blobs can grow over time by appending chunks and changing the attachment's `lastChunkId` in newer document revisions; `revisionId` ensures Haven reads from the historical attachment reference and does not accidentally include chunks added later.
|
|
704
|
+
|
|
460
705
|
### Attachment previews
|
|
461
706
|
|
|
462
707
|
Haven includes a **built-in file viewer** that your app can open for common attachment formats. This is one of the most powerful SDK features -- it works with both online and offline data, and media formats support streaming playback with skip/seek.
|
|
463
708
|
|
|
464
709
|
**Supported preview formats:**
|
|
465
710
|
|
|
466
|
-
| Mode
|
|
467
|
-
|
|
468
|
-
| `image`
|
|
469
|
-
| `pdf`
|
|
470
|
-
| `markdown`
|
|
471
|
-
| `text`
|
|
472
|
-
| `docx`
|
|
473
|
-
| `pptx`
|
|
474
|
-
| `spreadsheet` | Microsoft Excel (.xls, .xlsx, .xlsm, .xlsb)
|
|
475
|
-
| `video`
|
|
476
|
-
| `audio`
|
|
711
|
+
| Mode | Formats |
|
|
712
|
+
| ------------- | ------------------------------------------------------------------------------------------------------ |
|
|
713
|
+
| `image` | All common image formats (JPEG, PNG, GIF, WebP, SVG, ...) |
|
|
714
|
+
| `pdf` | PDF documents |
|
|
715
|
+
| `markdown` | Markdown rendered to HTML (.md, .markdown, .mdown, .mkd, .mkdn) |
|
|
716
|
+
| `text` | Plain text, CSV, JSON, XML, YAML, SVG, log files |
|
|
717
|
+
| `docx` | Microsoft Word (.docx) |
|
|
718
|
+
| `pptx` | Microsoft PowerPoint (.pptx) |
|
|
719
|
+
| `spreadsheet` | Microsoft Excel (.xls, .xlsx, .xlsm, .xlsb) |
|
|
720
|
+
| `video` | Video files with embedded player, streaming + seek (.mp4, .m4v, .webm, .ogv, .ogg, plus any `video/*`) |
|
|
721
|
+
| `audio` | Audio files with embedded player, streaming + seek (.mp3, .m4a, .wav, .aac, .oga, plus any `audio/*`) |
|
|
477
722
|
|
|
478
723
|
**Open the Haven preview dialog:**
|
|
479
724
|
|
|
@@ -485,10 +730,12 @@ You can also preview attachments from a **historical document snapshot**:
|
|
|
485
730
|
|
|
486
731
|
```ts
|
|
487
732
|
await db.attachments.openPreview(docId, "report.pdf", {
|
|
488
|
-
|
|
733
|
+
revisionId: historyEntry.revisionId,
|
|
489
734
|
});
|
|
490
735
|
```
|
|
491
736
|
|
|
737
|
+
`timestamp` is still accepted in attachment options for compatibility, but `revisionId` is the preferred identifier because it targets an exact DAG revision.
|
|
738
|
+
|
|
492
739
|
**Check preview support locally** before showing a preview button:
|
|
493
740
|
|
|
494
741
|
```ts
|
|
@@ -497,7 +744,10 @@ import { canPreviewAttachment } from "mindoodb-app-sdk";
|
|
|
497
744
|
const mode = canPreviewAttachment("slides.pptx", "application/octet-stream");
|
|
498
745
|
// Returns "pptx" -- Haven can preview this file
|
|
499
746
|
|
|
500
|
-
const unsupported = canPreviewAttachment(
|
|
747
|
+
const unsupported = canPreviewAttachment(
|
|
748
|
+
"data.bin",
|
|
749
|
+
"application/octet-stream",
|
|
750
|
+
);
|
|
501
751
|
// Returns null -- no built-in preview available
|
|
502
752
|
```
|
|
503
753
|
|
|
@@ -585,7 +835,10 @@ do {
|
|
|
585
835
|
```ts
|
|
586
836
|
const page1 = await navigator.entriesForward({ limit: 50 });
|
|
587
837
|
const page2 = page1.nextPosition
|
|
588
|
-
? await navigator.entriesForward({
|
|
838
|
+
? await navigator.entriesForward({
|
|
839
|
+
limit: 50,
|
|
840
|
+
startPosition: page1.nextPosition,
|
|
841
|
+
})
|
|
589
842
|
: null;
|
|
590
843
|
```
|
|
591
844
|
|
|
@@ -604,7 +857,11 @@ if (category) {
|
|
|
604
857
|
```ts
|
|
605
858
|
const category = await navigator.findCategoryEntryByParts(["Ada"]);
|
|
606
859
|
if (category) {
|
|
607
|
-
const exact = await navigator.childDocumentsByKey(
|
|
860
|
+
const exact = await navigator.childDocumentsByKey(
|
|
861
|
+
category.key,
|
|
862
|
+
"2026-04-14",
|
|
863
|
+
true,
|
|
864
|
+
);
|
|
608
865
|
const range = await navigator.childDocumentsBetween(category.key, {
|
|
609
866
|
startKey: "2026-04-01",
|
|
610
867
|
endKey: "2026-04-30",
|
|
@@ -667,7 +924,7 @@ MindooDB Apps are standard web applications -- deploy them anywhere you can serv
|
|
|
667
924
|
**Cloudflare Pages / Workers** is one of the easiest options. The [example app](https://github.com/klehmann/mindoodb-app-example) is deployed this way at https://app-example.mindoodb.com using a simple `wrangler.jsonc` configuration:
|
|
668
925
|
|
|
669
926
|
```bash
|
|
670
|
-
|
|
927
|
+
pnpm build
|
|
671
928
|
wrangler deploy
|
|
672
929
|
```
|
|
673
930
|
|
|
@@ -680,7 +937,7 @@ Other static hosting options work just as well: Netlify, Vercel, any web server
|
|
|
680
937
|
For interactive development with the full Haven host bridge:
|
|
681
938
|
|
|
682
939
|
1. Run Haven locally.
|
|
683
|
-
2. Run your app on its own dev server (e.g. `
|
|
940
|
+
2. Run your app on its own dev server (e.g. `pnpm dev` with Vite).
|
|
684
941
|
3. Register the dev server URL in Haven's Application settings.
|
|
685
942
|
4. Map databases and optionally views to the app.
|
|
686
943
|
5. Launch from Haven -- you get hot reload, real bridge communication, and live theme/viewport events.
|
|
@@ -691,111 +948,132 @@ This is separate from the automated testing workflow above. Running Haven is use
|
|
|
691
948
|
|
|
692
949
|
### MindooDBAppBridge
|
|
693
950
|
|
|
694
|
-
| Method
|
|
695
|
-
|
|
951
|
+
| Method | Returns |
|
|
952
|
+
| ------------------- | ----------------------------- |
|
|
696
953
|
| `connect(options?)` | `Promise<MindooDBAppSession>` |
|
|
697
954
|
|
|
698
955
|
Connect options: `launchId?`, `targetOrigin?`, `connectTimeoutMs?`.
|
|
699
956
|
|
|
700
957
|
### MindooDBAppSession
|
|
701
958
|
|
|
702
|
-
| Method
|
|
703
|
-
|
|
704
|
-
| `getLaunchContext()`
|
|
705
|
-
| `listDatabases()`
|
|
706
|
-
| `openDatabase(databaseId)`
|
|
707
|
-
| `createViewNavigator(input)`
|
|
708
|
-
| `openViewNavigator(viewId, options?)` | `Promise<MindooDBAppViewNavigator>`
|
|
709
|
-
| `menus.show(input)`
|
|
710
|
-
| `menus.hide()`
|
|
711
|
-
| `onThemeChange(listener)`
|
|
712
|
-
| `onViewportChange(listener)`
|
|
713
|
-
| `disconnect()`
|
|
959
|
+
| Method | Returns |
|
|
960
|
+
| ------------------------------------- | ------------------------------------ |
|
|
961
|
+
| `getLaunchContext()` | `Promise<MindooDBAppLaunchContext>` |
|
|
962
|
+
| `listDatabases()` | `Promise<MindooDBAppDatabaseInfo[]>` |
|
|
963
|
+
| `openDatabase(databaseId)` | `Promise<MindooDBAppDatabase>` |
|
|
964
|
+
| `createViewNavigator(input)` | `Promise<MindooDBAppViewNavigator>` |
|
|
965
|
+
| `openViewNavigator(viewId, options?)` | `Promise<MindooDBAppViewNavigator>` |
|
|
966
|
+
| `menus.show(input)` | `Promise<MindooDBAppShowMenuResult>` |
|
|
967
|
+
| `menus.hide()` | `Promise<void>` |
|
|
968
|
+
| `onThemeChange(listener)` | `() => void` (unsubscribe) |
|
|
969
|
+
| `onViewportChange(listener)` | `() => void` (unsubscribe) |
|
|
970
|
+
| `disconnect()` | `Promise<void>` |
|
|
714
971
|
|
|
715
972
|
### MindooDBAppDatabase
|
|
716
973
|
|
|
717
|
-
| Property / Method | Returns
|
|
718
|
-
|
|
719
|
-
| `info()`
|
|
720
|
-
| `documents`
|
|
721
|
-
| `attachments`
|
|
974
|
+
| Property / Method | Returns |
|
|
975
|
+
| ----------------- | ---------------------------------- |
|
|
976
|
+
| `info()` | `Promise<MindooDBAppDatabaseInfo>` |
|
|
977
|
+
| `documents` | `MindooDBAppDocumentApi` |
|
|
978
|
+
| `attachments` | `MindooDBAppAttachmentApi` |
|
|
722
979
|
|
|
723
980
|
### MindooDBAppDocumentApi
|
|
724
981
|
|
|
725
|
-
| Method
|
|
726
|
-
|
|
727
|
-
| `list(query?)`
|
|
728
|
-
| `get(docId)`
|
|
729
|
-
| `create(input)`
|
|
730
|
-
| `update(docId, patch)`
|
|
731
|
-
| `delete(docId)`
|
|
732
|
-
| `
|
|
733
|
-
| `
|
|
982
|
+
| Method | Returns |
|
|
983
|
+
| ---------------------------------- | -------------------------------------------- |
|
|
984
|
+
| `list(query?)` | `Promise<MindooDBAppDocumentListResult>` |
|
|
985
|
+
| `get(docId)` | `Promise<MindooDBAppDocument \| null>` |
|
|
986
|
+
| `create(input)` | `Promise<MindooDBAppDocument>` |
|
|
987
|
+
| `update(docId, patch)` | `Promise<MindooDBAppDocument>` |
|
|
988
|
+
| `delete(docId)` | `Promise<{ ok: true }>` |
|
|
989
|
+
| `undelete(docId)` | `Promise<{ ok: true }>` |
|
|
990
|
+
| `listHistory(docId)` | `Promise<MindooDBAppDocumentHistoryEntry[]>` |
|
|
991
|
+
| `getAtTimestamp(docId, timestamp)` | `Promise<MindooDBAppHistoricalDocument>` |
|
|
992
|
+
| `getAtRevision(docId, revisionId)` | `Promise<MindooDBAppHistoricalDocument>` |
|
|
734
993
|
|
|
735
994
|
`list(query?)` accepts the changefeed query options documented above. The `cursor` value is an opaque checkpoint string managed by Haven and should be stored and passed back unchanged.
|
|
736
995
|
|
|
737
|
-
For `
|
|
996
|
+
For `create(input)`, use `MindooDBAppCreateDocumentInput` with shape `{ set: Record<string, unknown>; decryptionKeyId?: string; id?: string }`. The optional `id` lets the app pick the document id instead of letting MindooDB generate a UUID7; when provided it must match `/^[A-Za-z][A-Za-z0-9_]*$/` and existing live documents with that id are returned as-is (idempotent create-if-missing). If an existing custom-id document is deleted, `create({ id })` undeletes it and preserves the previous document body. Documents created with the same custom id on different replicas converge correctly when synced.
|
|
997
|
+
|
|
998
|
+
`delete(docId)` writes a lifecycle tombstone; it does not erase the document body from the append-only history. Use `undelete(docId)` to make the latest document state live again. If you want to record a deletion reason, first call `update(docId, { set: { intendedDeletionReason: "..." } })`, then call `delete(docId)`.
|
|
999
|
+
|
|
1000
|
+
For `update(docId, patch)`, use `MindooDBAppUpdateDocumentInput`:
|
|
1001
|
+
|
|
1002
|
+
```ts
|
|
1003
|
+
interface MindooDBAppUpdateDocumentInput {
|
|
1004
|
+
set?: Record<string, unknown>; // shallow assign top-level fields
|
|
1005
|
+
unset?: string[]; // remove top-level fields entirely
|
|
1006
|
+
text?: MindooDBAppTextPatch[]; // granular collaborative text edits
|
|
1007
|
+
}
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
`set` and `unset` apply to top-level fields only. `text` patches operate on string fields at any JSON path inside `document.data` and are merged on the Haven side using the document's Automerge heads. See [Collaborative text editing](#collaborative-text-editing).
|
|
738
1011
|
|
|
739
1012
|
### MindooDBAppAttachmentApi
|
|
740
1013
|
|
|
741
|
-
| Method
|
|
742
|
-
|
|
743
|
-
| `list(docId)`
|
|
744
|
-
| `remove(docId, attachmentName)`
|
|
745
|
-
| `openReadStream(docId, attachmentName)`
|
|
746
|
-
| `openWriteStream(docId, attachmentName, contentType?)`
|
|
747
|
-
| `
|
|
1014
|
+
| Method | Returns |
|
|
1015
|
+
| -------------------------------------------------------- | ---------------------------------------------- |
|
|
1016
|
+
| `list(docId, options?)` | `Promise<MindooDBAppAttachmentInfo[]>` |
|
|
1017
|
+
| `remove(docId, attachmentName)` | `Promise<{ ok: true }>` |
|
|
1018
|
+
| `openReadStream(docId, attachmentName, options?)` | `Promise<MindooDBAppReadableAttachmentStream>` |
|
|
1019
|
+
| `openWriteStream(docId, attachmentName, contentType?)` | `Promise<MindooDBAppWritableAttachmentStream>` |
|
|
1020
|
+
| `scan(docId, options?)` | `Promise<MindooDBAppScanAttachmentResult>` |
|
|
1021
|
+
| `preparePreviewSession(docId, attachmentName, options?)` | `Promise<MindooDBAppAttachmentPreviewSession>` |
|
|
1022
|
+
| `openPreview(docId, attachmentName, options?)` | `Promise<{ ok: true }>` |
|
|
1023
|
+
|
|
1024
|
+
`options` accepts `{ timestamp?: number; revisionId?: string }` for read-only historical attachment access. Mutation methods (`openWriteStream`, `remove`) always target the current document.
|
|
748
1025
|
|
|
749
1026
|
### MindooDBAppViewNavigator
|
|
750
1027
|
|
|
751
|
-
| Method
|
|
752
|
-
|
|
753
|
-
| `getDefinition()`
|
|
754
|
-
| `refresh()`
|
|
755
|
-
| `getCurrentEntry()`
|
|
756
|
-
| `gotoFirst()` / `gotoLast()`
|
|
757
|
-
| `gotoNext()` / `gotoPrev()`
|
|
758
|
-
| `gotoNextSibling()` / `gotoPrevSibling()`
|
|
759
|
-
| `gotoParent()`
|
|
760
|
-
| `gotoFirstChild()` / `gotoLastChild()`
|
|
761
|
-
| `gotoPos(position)`
|
|
762
|
-
| `getPos(position)`
|
|
763
|
-
| `findCategoryEntryByParts(parts)`
|
|
764
|
-
| `entriesForward(options?)`
|
|
765
|
-
| `entriesBackward(options?)`
|
|
766
|
-
| `gotoNextSelected()` / `gotoPrevSelected()`
|
|
767
|
-
| `select(origin, docId, selectParentCategories?)`
|
|
768
|
-
| `deselect(origin, docId)`
|
|
769
|
-
| `selectAllEntries()` / `deselectAllEntries()`
|
|
770
|
-
| `isSelected(origin, docId)`
|
|
771
|
-
| `getSelectionState()`
|
|
772
|
-
| `setSelectionState(state)`
|
|
773
|
-
| `expand(origin, docId)` / `collapse(origin, docId)`
|
|
774
|
-
| `expandAll()` / `collapseAll()`
|
|
775
|
-
| `expandToLevel(level)`
|
|
776
|
-
| `isExpanded(entryKey)`
|
|
777
|
-
| `getExpansionState()`
|
|
778
|
-
| `setExpansionState(state)`
|
|
779
|
-
| `childEntries(entryKey, descending?)`
|
|
780
|
-
| `childCategories(entryKey, descending?)`
|
|
781
|
-
| `childDocuments(entryKey, descending?)`
|
|
782
|
-
| `childCategoriesByKey(entryKey, key, exact?, descending?)` | `Promise<MindooDBAppViewEntry[]>`
|
|
783
|
-
| `childDocumentsByKey(entryKey, key, exact?, descending?)`
|
|
784
|
-
| `childCategoriesBetween(entryKey, range)`
|
|
785
|
-
| `childDocumentsBetween(entryKey, range)`
|
|
786
|
-
| `getSortedDocIds(descending?)`
|
|
787
|
-
| `getSortedDocIdsScoped(entryKey, descending?)`
|
|
788
|
-
| `dispose()`
|
|
1028
|
+
| Method | Returns |
|
|
1029
|
+
| ---------------------------------------------------------- | ------------------------------------------------- |
|
|
1030
|
+
| `getDefinition()` | `Promise<MindooDBAppViewDefinition>` |
|
|
1031
|
+
| `refresh()` | `Promise<void>` |
|
|
1032
|
+
| `getCurrentEntry()` | `Promise<MindooDBAppViewEntry \| null>` |
|
|
1033
|
+
| `gotoFirst()` / `gotoLast()` | `Promise<boolean>` |
|
|
1034
|
+
| `gotoNext()` / `gotoPrev()` | `Promise<boolean>` |
|
|
1035
|
+
| `gotoNextSibling()` / `gotoPrevSibling()` | `Promise<boolean>` |
|
|
1036
|
+
| `gotoParent()` | `Promise<boolean>` |
|
|
1037
|
+
| `gotoFirstChild()` / `gotoLastChild()` | `Promise<boolean>` |
|
|
1038
|
+
| `gotoPos(position)` | `Promise<boolean>` |
|
|
1039
|
+
| `getPos(position)` | `Promise<MindooDBAppViewEntry \| null>` |
|
|
1040
|
+
| `findCategoryEntryByParts(parts)` | `Promise<MindooDBAppViewEntry \| null>` |
|
|
1041
|
+
| `entriesForward(options?)` | `Promise<MindooDBAppViewNavigatorPageResult>` |
|
|
1042
|
+
| `entriesBackward(options?)` | `Promise<MindooDBAppViewNavigatorPageResult>` |
|
|
1043
|
+
| `gotoNextSelected()` / `gotoPrevSelected()` | `Promise<boolean>` |
|
|
1044
|
+
| `select(origin, docId, selectParentCategories?)` | `Promise<void>` |
|
|
1045
|
+
| `deselect(origin, docId)` | `Promise<void>` |
|
|
1046
|
+
| `selectAllEntries()` / `deselectAllEntries()` | `Promise<void>` |
|
|
1047
|
+
| `isSelected(origin, docId)` | `Promise<boolean>` |
|
|
1048
|
+
| `getSelectionState()` | `Promise<MindooDBAppViewNavigatorSelectionState>` |
|
|
1049
|
+
| `setSelectionState(state)` | `Promise<void>` |
|
|
1050
|
+
| `expand(origin, docId)` / `collapse(origin, docId)` | `Promise<void>` |
|
|
1051
|
+
| `expandAll()` / `collapseAll()` | `Promise<void>` |
|
|
1052
|
+
| `expandToLevel(level)` | `Promise<void>` |
|
|
1053
|
+
| `isExpanded(entryKey)` | `Promise<boolean>` |
|
|
1054
|
+
| `getExpansionState()` | `Promise<MindooDBAppViewNavigatorExpansionState>` |
|
|
1055
|
+
| `setExpansionState(state)` | `Promise<void>` |
|
|
1056
|
+
| `childEntries(entryKey, descending?)` | `Promise<MindooDBAppViewEntry[]>` |
|
|
1057
|
+
| `childCategories(entryKey, descending?)` | `Promise<MindooDBAppViewEntry[]>` |
|
|
1058
|
+
| `childDocuments(entryKey, descending?)` | `Promise<MindooDBAppViewEntry[]>` |
|
|
1059
|
+
| `childCategoriesByKey(entryKey, key, exact?, descending?)` | `Promise<MindooDBAppViewEntry[]>` |
|
|
1060
|
+
| `childDocumentsByKey(entryKey, key, exact?, descending?)` | `Promise<MindooDBAppViewEntry[]>` |
|
|
1061
|
+
| `childCategoriesBetween(entryKey, range)` | `Promise<MindooDBAppViewEntry[]>` |
|
|
1062
|
+
| `childDocumentsBetween(entryKey, range)` | `Promise<MindooDBAppViewEntry[]>` |
|
|
1063
|
+
| `getSortedDocIds(descending?)` | `Promise<MindooDBAppScopedDocId[]>` |
|
|
1064
|
+
| `getSortedDocIdsScoped(entryKey, descending?)` | `Promise<MindooDBAppScopedDocId[]>` |
|
|
1065
|
+
| `dispose()` | `Promise<void>` |
|
|
789
1066
|
|
|
790
1067
|
### Exported functions
|
|
791
1068
|
|
|
792
|
-
| Function
|
|
793
|
-
|
|
794
|
-
| `createMindooDBAppBridge()`
|
|
795
|
-
| `
|
|
796
|
-
| `
|
|
797
|
-
| `
|
|
798
|
-
| `
|
|
1069
|
+
| Function | Description |
|
|
1070
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
|
1071
|
+
| `createMindooDBAppBridge()` | Create the bridge object used to connect to Haven |
|
|
1072
|
+
| `createMindooDBTextBuffer(options)` | Create an editor-friendly buffer for granular collaborative text edits on a document field |
|
|
1073
|
+
| `canPreviewAttachment(fileName, mimeType)` | Check if Haven can preview a file (returns mode or `null`) |
|
|
1074
|
+
| `createViewLanguage<T>()` | Create a typed expression builder for view definitions |
|
|
1075
|
+
| `abbreviateCanonicalName(value)` | Convert a canonical Notes-style name like `cn=Jane/ou=Dev/o=Mindoo` to `Jane/Dev/Mindoo` |
|
|
1076
|
+
| `expandAbbreviatedName(value)` | Convert an abbreviated Notes-style name like `Jane/Dev/Mindoo` to `cn=Jane/ou=Dev/o=Mindoo` |
|
|
799
1077
|
|
|
800
1078
|
## Permissions and mappings
|
|
801
1079
|
|