mindoodb-app-sdk 0.0.4 → 0.0.6
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 +493 -253
- package/TESTING.md +3 -1
- package/dist/index.d.ts +29 -3
- package/dist/index.js +93 -29
- package/dist/testing/index.d.ts +7 -3
- package/dist/testing/index.js +185 -39
- package/dist/{types-BGdIfp-D.d.ts → types-BDY_80wd.d.ts} +26 -8
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
# mindoodb-app-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build apps that run inside **MindooDB Haven** -- the browser-based workspace for end-to-end encrypted, offline-first data.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
`mindoodb-app-sdk` is the TypeScript SDK that connects your web application to Haven through a secure message bridge. Your app runs in a sandboxed iframe (or a separate browser window) and gains access to databases, documents, attachments, virtual views, and live host events -- all without ever touching encryption keys or raw sync state.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- access to the databases Haven mapped into the app session
|
|
9
|
-
- document, history, attachment, and view operations through a stable bridge API
|
|
10
|
-
- host-driven UI events such as theme and viewport changes
|
|
7
|
+
## What is a MindooDB App?
|
|
11
8
|
|
|
12
|
-
|
|
9
|
+
[MindooDB](https://mindoodb.com) is an **end-to-end encrypted, offline-first sync database**. Data is encrypted on the client before it ever leaves the device. Under the hood it uses [Automerge](https://automerge.org/) CRDTs so multiple users can edit concurrently and conflicts resolve automatically.
|
|
13
10
|
|
|
14
|
-
|
|
11
|
+
**Haven** is the browser-based workspace that sits on top of MindooDB. Users organize databases, applications, notes, and media on a visual grid -- think of it as a customizable home screen for encrypted data that works offline and syncs in the background.
|
|
15
12
|
|
|
16
|
-
|
|
13
|
+
A **MindooDB App** is any web application that uses this SDK to communicate with Haven. When Haven launches your app, it opens it inside a sandboxed iframe (or a new browser tab) and establishes a `postMessage` + `MessagePort` bridge. Through this bridge your app can:
|
|
17
14
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
- `onViewportChange()` when the iframe size changes inside Haven
|
|
24
|
-
- CRUD, history, attachments, and app-defined virtual views over the same bridge
|
|
15
|
+
- read launch metadata (user, theme, viewport, mapped databases, configured views)
|
|
16
|
+
- perform CRUD operations on JSON documents with full change history
|
|
17
|
+
- upload, download, and preview file attachments
|
|
18
|
+
- query virtual views with categorization, sorting, and aggregation
|
|
19
|
+
- react to live theme and viewport events from Haven
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
**Security model:** Every app runs on a **separate origin** inside a sandboxed iframe. It cannot access Haven's storage, cookies, or other apps. Haven decides exactly which databases and permissions each app receives -- your app only sees data that was explicitly shared with it.
|
|
22
|
+
|
|
23
|
+
## Architecture
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌─────────────────────────────────────────────────────────┐
|
|
27
|
+
│ Haven (browser tab) │
|
|
28
|
+
│ │
|
|
29
|
+
│ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
|
30
|
+
│ │ Haven UI │──▶│ Bridge Host │──▶│ MindooDB │ │
|
|
31
|
+
│ │ (theme, │ │ (RPC + │ │ (encrypted │ │
|
|
32
|
+
│ │ viewport) │ │ streams) │ │ databases) │ │
|
|
33
|
+
│ └──────────────┘ └──────┬──────┘ └─────────────┘ │
|
|
34
|
+
│ │ │
|
|
35
|
+
│ postMessage + MessagePort │
|
|
36
|
+
│ │ │
|
|
37
|
+
│ ┌─────────────────────────┼─────────────────────────┐ │
|
|
38
|
+
│ │ Your App (sandboxed iframe / window) │ │
|
|
39
|
+
│ │ │ │ │
|
|
40
|
+
│ │ ┌──────────▼──────────┐ │ │
|
|
41
|
+
│ │ │ mindoodb-app-sdk │ │ │
|
|
42
|
+
│ │ └─────────────────────┘ │ │
|
|
43
|
+
│ └───────────────────────────────────────────────────┘ │
|
|
44
|
+
└─────────────────────────────────────────────────────────┘
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The bridge carries RPC calls (documents, views, database info), binary attachment streams, and push events (theme changes, viewport resizes) over a single `MessagePort`.
|
|
27
48
|
|
|
28
49
|
## Install
|
|
29
50
|
|
|
@@ -31,302 +52,430 @@ Your app does not open arbitrary MindooDB databases directly. Haven decides whic
|
|
|
31
52
|
npm install mindoodb-app-sdk
|
|
32
53
|
```
|
|
33
54
|
|
|
34
|
-
|
|
55
|
+
## Get started in 5 minutes
|
|
35
56
|
|
|
36
|
-
|
|
37
|
-
# In the SDK package
|
|
38
|
-
npm link
|
|
57
|
+
The fastest way to start building is to clone the **example app**, run it locally, and connect it to Haven:
|
|
39
58
|
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
```bash
|
|
60
|
+
git clone https://github.com/niclas-niclas/mindoodb-app-example.git
|
|
61
|
+
cd mindoodb-app-example
|
|
62
|
+
npm install
|
|
63
|
+
npm run dev:local
|
|
42
64
|
```
|
|
43
65
|
|
|
44
|
-
|
|
66
|
+
This spins up a local Vite dev server on `http://localhost:4200` with hot module reload.
|
|
67
|
+
|
|
68
|
+
Then, in Haven:
|
|
69
|
+
|
|
70
|
+
1. Go to **Application settings** and register a new app pointing to `http://localhost:4200`.
|
|
71
|
+
2. Map one or more databases (and optionally views) to the app.
|
|
72
|
+
3. Launch the app -- it connects via the SDK bridge and displays live data.
|
|
73
|
+
|
|
74
|
+
From here, **fork or duplicate** the example project to start building your own app while exploring the platform in a running Haven client.
|
|
75
|
+
|
|
76
|
+
The example app is also deployed at **https://mindoodb-app-example.mindoo.de** so you can register it in Haven without running anything locally.
|
|
77
|
+
|
|
78
|
+
> See the [mindoodb-app-example README](https://github.com/niclas-niclas/mindoodb-app-example) for the full project walkthrough.
|
|
79
|
+
|
|
80
|
+
## Quick start (from scratch)
|
|
45
81
|
|
|
46
|
-
|
|
82
|
+
If you prefer starting from an empty project:
|
|
47
83
|
|
|
48
|
-
|
|
84
|
+
```ts
|
|
85
|
+
import { createMindooDBAppBridge } from "mindoodb-app-sdk";
|
|
49
86
|
|
|
50
|
-
|
|
87
|
+
// 1. Create the bridge and connect to Haven
|
|
88
|
+
const bridge = createMindooDBAppBridge();
|
|
89
|
+
const session = await bridge.connect();
|
|
51
90
|
|
|
52
|
-
|
|
53
|
-
|
|
91
|
+
// 2. Read the launch context
|
|
92
|
+
const ctx = await session.getLaunchContext();
|
|
93
|
+
console.log("Runtime:", ctx.runtime); // "iframe" | "window"
|
|
94
|
+
console.log("Theme:", ctx.theme.mode); // "light" | "dark"
|
|
95
|
+
console.log("Databases:", ctx.databases.map((db) => db.title));
|
|
54
96
|
|
|
55
|
-
|
|
97
|
+
// 3. Subscribe to live host events
|
|
98
|
+
session.onThemeChange((theme) => {
|
|
99
|
+
document.documentElement.dataset.theme = theme.mode;
|
|
100
|
+
});
|
|
56
101
|
|
|
57
|
-
|
|
58
|
-
|
|
102
|
+
session.onViewportChange((viewport) => {
|
|
103
|
+
console.log("Iframe resized:", viewport.width, viewport.height);
|
|
104
|
+
});
|
|
59
105
|
|
|
60
|
-
|
|
106
|
+
// 4. Open a database and list documents
|
|
107
|
+
const db = await session.openDatabase(ctx.databases[0]!.id);
|
|
108
|
+
const result = await db.documents.list({ limit: 25 });
|
|
109
|
+
console.log("Documents:", result.items);
|
|
110
|
+
```
|
|
61
111
|
|
|
62
|
-
When Haven launches
|
|
112
|
+
When Haven launches your app it injects a `mindoodbAppLaunchId` query parameter into the URL. `connect()` reads it automatically -- no manual wiring needed.
|
|
63
113
|
|
|
64
|
-
|
|
65
|
-
- `mindoodbAppId`
|
|
66
|
-
- `mindoodbAppRuntime`
|
|
114
|
+
## Core concepts
|
|
67
115
|
|
|
68
|
-
|
|
116
|
+
### Launch context
|
|
69
117
|
|
|
70
|
-
|
|
118
|
+
After connecting, call `session.getLaunchContext()` to receive the initial host snapshot:
|
|
71
119
|
|
|
72
120
|
```ts
|
|
73
|
-
|
|
121
|
+
interface MindooDBAppLaunchContext {
|
|
122
|
+
appId: string;
|
|
123
|
+
appInstanceId: string;
|
|
124
|
+
appVersion?: string;
|
|
125
|
+
launchId: string;
|
|
126
|
+
runtime: "iframe" | "window";
|
|
127
|
+
theme: { mode: "light" | "dark"; preset: string };
|
|
128
|
+
viewport: { width: number; height: number } | null;
|
|
129
|
+
tenantId?: string;
|
|
130
|
+
preferredDatabaseId?: string;
|
|
131
|
+
user: { id: string; username: string };
|
|
132
|
+
launchParameters: Record<string, string>;
|
|
133
|
+
databases: MindooDBAppDatabaseInfo[];
|
|
134
|
+
views: MindooDBAppResolvedViewDefinition[];
|
|
135
|
+
}
|
|
136
|
+
```
|
|
74
137
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
138
|
+
`viewport` is the iframe size at launch time. It is `null` when the app runs in `window` mode (separate browser tab). Later changes arrive through `onViewportChange()`.
|
|
139
|
+
|
|
140
|
+
### Databases and capabilities
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
| Capability | Allows |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `read` | List and read documents |
|
|
147
|
+
| `create` | Create new documents |
|
|
148
|
+
| `update` | Update existing documents |
|
|
149
|
+
| `delete` | Delete documents |
|
|
150
|
+
| `history` | Access document revision history and historical snapshots |
|
|
151
|
+
| `attachments` | List, upload, download, remove, and preview file attachments |
|
|
152
|
+
| `views` | Create app-defined virtual views for this database |
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const db = ctx.databases[0]!;
|
|
156
|
+
|
|
157
|
+
if (db.capabilities.includes("history")) {
|
|
158
|
+
// show history UI
|
|
159
|
+
}
|
|
160
|
+
if (!db.capabilities.includes("delete")) {
|
|
161
|
+
// hide delete button
|
|
162
|
+
}
|
|
78
163
|
```
|
|
79
164
|
|
|
80
|
-
|
|
165
|
+
When the database is readable, `documents.list()` can also expose deleted document IDs by setting `status: "all"` or `status: "deleted"`. This is useful for app-side indexes and sync checkpoints.
|
|
166
|
+
|
|
167
|
+
### Theme and viewport events
|
|
168
|
+
|
|
169
|
+
Haven pushes two types of live events to your app:
|
|
170
|
+
|
|
171
|
+
**Theme changes** occur when the user switches between light/dark mode or changes the UI theme preset:
|
|
81
172
|
|
|
82
173
|
```ts
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
174
|
+
// Initial snapshot from launch context
|
|
175
|
+
const { mode, preset } = ctx.theme; // e.g. { mode: "dark", preset: "aura" }
|
|
176
|
+
|
|
177
|
+
// Live updates
|
|
178
|
+
const stopTheme = session.onThemeChange((theme) => {
|
|
179
|
+
document.documentElement.dataset.theme = theme.mode;
|
|
180
|
+
document.documentElement.dataset.themePreset = theme.preset;
|
|
86
181
|
});
|
|
87
182
|
```
|
|
88
183
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
The launch context is your initial snapshot of the Haven host state:
|
|
184
|
+
**Viewport changes** fire when the iframe is resized inside Haven (e.g. the user resizes a chicklet on the workspace grid):
|
|
92
185
|
|
|
93
186
|
```ts
|
|
94
|
-
|
|
187
|
+
// Initial snapshot (null in window mode)
|
|
188
|
+
const viewport = ctx.viewport; // { width: 800, height: 600 }
|
|
95
189
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
console.log(
|
|
99
|
-
|
|
100
|
-
console.log(launchContext.launchParameters);
|
|
190
|
+
// Live updates
|
|
191
|
+
const stopViewport = session.onViewportChange((viewport) => {
|
|
192
|
+
console.log(viewport.width, viewport.height);
|
|
193
|
+
});
|
|
101
194
|
```
|
|
102
195
|
|
|
103
|
-
|
|
196
|
+
Both subscription functions return an unsubscribe callback. Call it during teardown.
|
|
104
197
|
|
|
105
|
-
###
|
|
198
|
+
### Documents
|
|
199
|
+
|
|
200
|
+
Open a database, then use the `documents` API for CRUD, history, and changefeed-backed document listing:
|
|
106
201
|
|
|
107
202
|
```ts
|
|
108
|
-
const
|
|
109
|
-
|
|
203
|
+
const db = await session.openDatabase(databaseId);
|
|
204
|
+
|
|
205
|
+
// List the first page of existing documents
|
|
206
|
+
const result = await db.documents.list({ limit: 50, fields: ["title"] });
|
|
207
|
+
console.log(result.items);
|
|
208
|
+
console.log(result.nextCursor); // opaque changefeed checkpoint or null
|
|
209
|
+
|
|
210
|
+
// Continue from the last checkpoint
|
|
211
|
+
const nextPage = result.nextCursor
|
|
212
|
+
? await db.documents.list({ cursor: result.nextCursor, limit: 50, fields: ["title"] })
|
|
213
|
+
: null;
|
|
214
|
+
|
|
215
|
+
// Fast metadata-only listing (IDs + deletion state only)
|
|
216
|
+
const ids = await db.documents.list({
|
|
217
|
+
limit: 200,
|
|
218
|
+
status: "all",
|
|
219
|
+
metadataOnly: true,
|
|
110
220
|
});
|
|
111
221
|
|
|
112
|
-
|
|
113
|
-
|
|
222
|
+
// Skip N matching entries before returning results
|
|
223
|
+
const window = await db.documents.list({
|
|
224
|
+
cursor: result.nextCursor,
|
|
225
|
+
skip: 100,
|
|
226
|
+
limit: 25,
|
|
227
|
+
status: "existing",
|
|
114
228
|
});
|
|
115
229
|
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
230
|
+
// Read
|
|
231
|
+
const doc = await db.documents.get(docId);
|
|
232
|
+
|
|
233
|
+
// Create (optionally with a named document key)
|
|
234
|
+
const created = await db.documents.create({
|
|
235
|
+
data: { title: "Meeting notes", content: "..." },
|
|
236
|
+
decryptionKeyId: "team-key", // omit to use "default"
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Update
|
|
240
|
+
const updated = await db.documents.update(docId, {
|
|
241
|
+
data: { title: "Updated title" },
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Delete
|
|
245
|
+
await db.documents.delete(docId);
|
|
119
246
|
```
|
|
120
247
|
|
|
121
|
-
The
|
|
248
|
+
`documents.list()` is backed by Haven's internal changefeed, not by a positional offset. The query object supports:
|
|
249
|
+
|
|
250
|
+
| Field | Meaning |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `cursor?: string \| null` | Opaque changefeed checkpoint previously returned by `nextCursor` |
|
|
253
|
+
| `limit?: number` | Maximum number of matching entries to return (default `50`) |
|
|
254
|
+
| `skip?: number` | Skip matching entries after the cursor without loading full document bodies |
|
|
255
|
+
| `status?: "all" \| "existing" \| "deleted"` | Filter by deletion state (default `"existing"`) |
|
|
256
|
+
| `metadataOnly?: boolean` | Return only `{ id, isDeleted }` for speed |
|
|
257
|
+
| `fields?: string[]` | Project specific JSON fields when `metadataOnly` is `false` |
|
|
258
|
+
| `filter?: Record<string, unknown>` | Simple equality filter applied to document data when `metadataOnly` is `false` |
|
|
259
|
+
|
|
260
|
+
`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`.
|
|
261
|
+
|
|
262
|
+
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.
|
|
122
263
|
|
|
123
|
-
|
|
124
|
-
- use `onThemeChange()` and `onViewportChange()` for live updates
|
|
264
|
+
### Document history
|
|
125
265
|
|
|
126
|
-
|
|
266
|
+
When the `history` capability is granted, you can walk the full revision timeline of any document:
|
|
127
267
|
|
|
128
268
|
```ts
|
|
129
|
-
|
|
269
|
+
// List all revisions
|
|
270
|
+
const history = await db.documents.listHistory(docId);
|
|
271
|
+
// Each entry: { timestamp, publicKey, identityLabel?, isDeleted, isCurrent, summary? }
|
|
130
272
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
273
|
+
// Load the document state at a specific point in time
|
|
274
|
+
const snapshot = await db.documents.getAtTimestamp(docId, history[0]!.timestamp);
|
|
275
|
+
// { id, timestamp, state: "exists" | "deleted" | "missing", data }
|
|
134
276
|
```
|
|
135
277
|
|
|
136
|
-
|
|
278
|
+
### Incremental sync
|
|
279
|
+
|
|
280
|
+
The changefeed-backed `documents.list()` API is also the right primitive for app-side indexes, search, and other derived caches.
|
|
137
281
|
|
|
138
|
-
|
|
282
|
+
**Initial scan from the beginning:**
|
|
139
283
|
|
|
140
284
|
```ts
|
|
141
|
-
|
|
142
|
-
|
|
285
|
+
let cursor: string | null = null;
|
|
286
|
+
|
|
287
|
+
while (true) {
|
|
288
|
+
const page = await db.documents.list({
|
|
289
|
+
cursor,
|
|
290
|
+
limit: 500,
|
|
291
|
+
status: "all",
|
|
292
|
+
metadataOnly: true,
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
for (const item of page.items) {
|
|
296
|
+
if (!item.isDeleted) {
|
|
297
|
+
const doc = await db.documents.get(item.id);
|
|
298
|
+
// add or rebuild the derived index entry
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (!page.nextCursor) {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
cursor = page.nextCursor;
|
|
306
|
+
}
|
|
143
307
|
|
|
144
|
-
|
|
145
|
-
|
|
308
|
+
// Save `cursor` after the last non-empty page.
|
|
309
|
+
```
|
|
146
310
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
311
|
+
**Resume later from the saved checkpoint:**
|
|
312
|
+
|
|
313
|
+
```ts
|
|
314
|
+
const page = await db.documents.list({
|
|
315
|
+
cursor: savedCursor,
|
|
316
|
+
limit: 500,
|
|
317
|
+
status: "all",
|
|
318
|
+
metadataOnly: true,
|
|
154
319
|
});
|
|
320
|
+
|
|
321
|
+
let updated = 0;
|
|
322
|
+
let deleted = 0;
|
|
323
|
+
|
|
324
|
+
for (const item of page.items) {
|
|
325
|
+
if (item.isDeleted) {
|
|
326
|
+
deleted += 1;
|
|
327
|
+
// remove from index
|
|
328
|
+
} else {
|
|
329
|
+
updated += 1;
|
|
330
|
+
const doc = await db.documents.get(item.id);
|
|
331
|
+
// update index
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
console.log(`Updated index with ${updated} changes and ${deleted} deletions.`);
|
|
155
336
|
```
|
|
156
337
|
|
|
157
|
-
|
|
338
|
+
Use `status: "all"` for external indexes so deletions are visible and can be removed from your derived state.
|
|
158
339
|
|
|
159
|
-
|
|
340
|
+
### Attachments
|
|
341
|
+
|
|
342
|
+
When the `attachments` capability is granted, each document can carry file attachments. The SDK uses **chunked binary streams** over the bridge for uploads and downloads:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
// List attachments for a document
|
|
346
|
+
const files = await db.attachments.list(docId);
|
|
347
|
+
// Each: { attachmentId, fileName, mimeType, size }
|
|
348
|
+
|
|
349
|
+
// Download (pull-based read stream)
|
|
350
|
+
const reader = await db.attachments.openReadStream(docId, "report.pdf");
|
|
351
|
+
const chunks: Uint8Array[] = [];
|
|
352
|
+
let chunk = await reader.read();
|
|
353
|
+
while (chunk !== null) {
|
|
354
|
+
chunks.push(chunk);
|
|
355
|
+
chunk = await reader.read();
|
|
356
|
+
}
|
|
357
|
+
await reader.close();
|
|
160
358
|
|
|
161
|
-
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
- `documents.delete()`
|
|
166
|
-
- `documents.listHistory()`
|
|
167
|
-
- `documents.getAtTimestamp()`
|
|
359
|
+
// Upload (push-based write stream)
|
|
360
|
+
const writer = await db.attachments.openWriteStream(docId, "photo.jpg", "image/jpeg");
|
|
361
|
+
await writer.write(fileBytes);
|
|
362
|
+
await writer.close();
|
|
168
363
|
|
|
169
|
-
|
|
364
|
+
// Remove
|
|
365
|
+
await db.attachments.remove(docId, "old-file.txt");
|
|
366
|
+
```
|
|
170
367
|
|
|
171
|
-
Attachment
|
|
368
|
+
### Attachment previews
|
|
172
369
|
|
|
173
|
-
-
|
|
174
|
-
- `attachments.remove(docId, attachmentName)`
|
|
175
|
-
- `attachments.openReadStream(docId, attachmentName)`
|
|
176
|
-
- `attachments.openWriteStream(docId, attachmentName, contentType?)`
|
|
370
|
+
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.
|
|
177
371
|
|
|
178
|
-
|
|
372
|
+
**Supported preview formats:**
|
|
179
373
|
|
|
180
|
-
|
|
181
|
-
|
|
374
|
+
| Mode | Formats |
|
|
375
|
+
|---|---|
|
|
376
|
+
| `image` | All common image formats (JPEG, PNG, GIF, WebP, SVG, ...) |
|
|
377
|
+
| `pdf` | PDF documents |
|
|
378
|
+
| `text` | Plain text, Markdown, CSV, JSON, XML, YAML, SVG, log files |
|
|
379
|
+
| `docx` | Microsoft Word (.docx) |
|
|
380
|
+
| `pptx` | Microsoft PowerPoint (.pptx) |
|
|
381
|
+
| `spreadsheet` | Microsoft Excel (.xls, .xlsx, .xlsm, .xlsb) |
|
|
382
|
+
| `video` | Video files with embedded player (streaming + seek support) |
|
|
383
|
+
| `audio` | Audio files with embedded player (streaming + seek support) |
|
|
182
384
|
|
|
183
|
-
|
|
184
|
-
const session = await bridge.connect();
|
|
385
|
+
**Open the Haven preview dialog:**
|
|
185
386
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
387
|
+
```ts
|
|
388
|
+
await db.attachments.openPreview(docId, "presentation.pptx");
|
|
389
|
+
```
|
|
189
390
|
|
|
190
|
-
|
|
191
|
-
document.documentElement.dataset.theme = theme.mode;
|
|
192
|
-
});
|
|
391
|
+
You can also preview attachments from a **historical document snapshot**:
|
|
193
392
|
|
|
194
|
-
|
|
195
|
-
|
|
393
|
+
```ts
|
|
394
|
+
await db.attachments.openPreview(docId, "report.pdf", {
|
|
395
|
+
timestamp: historyEntry.timestamp,
|
|
196
396
|
});
|
|
397
|
+
```
|
|
197
398
|
|
|
198
|
-
|
|
199
|
-
const db = await session.openDatabase(databases[0]!.id);
|
|
399
|
+
**Check preview support locally** before showing a preview button:
|
|
200
400
|
|
|
201
|
-
|
|
202
|
-
|
|
401
|
+
```ts
|
|
402
|
+
import { canPreviewAttachment } from "mindoodb-app-sdk";
|
|
203
403
|
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
title: "Hours by employee",
|
|
207
|
-
defaultExpand: "collapsed",
|
|
208
|
-
filter: {
|
|
209
|
-
mode: "expression",
|
|
210
|
-
expression: v.gt(v.toNumber(v.field("hours")), v.number(0)),
|
|
211
|
-
},
|
|
212
|
-
columns: [
|
|
213
|
-
{
|
|
214
|
-
name: "employee",
|
|
215
|
-
title: "Employee",
|
|
216
|
-
role: "category",
|
|
217
|
-
expression: v.field("employee"),
|
|
218
|
-
sorting: "ascending",
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
name: "amount",
|
|
222
|
-
title: "Amount",
|
|
223
|
-
role: "display",
|
|
224
|
-
expression: v.let(
|
|
225
|
-
{
|
|
226
|
-
hours: v.toNumber(v.field("hours")),
|
|
227
|
-
rate: v.coalesce(v.toNumber(v.field("rate")), v.number(1)),
|
|
228
|
-
},
|
|
229
|
-
({ hours, rate }) => v.mul(v.coalesce(hours, v.number(0)), v.coalesce(rate, v.number(0))),
|
|
230
|
-
),
|
|
231
|
-
},
|
|
232
|
-
],
|
|
233
|
-
});
|
|
404
|
+
const mode = canPreviewAttachment("slides.pptx", "application/octet-stream");
|
|
405
|
+
// Returns "pptx" -- Haven can preview this file
|
|
234
406
|
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
await view.dispose();
|
|
407
|
+
const unsupported = canPreviewAttachment("data.bin", "application/octet-stream");
|
|
408
|
+
// Returns null -- no built-in preview available
|
|
238
409
|
```
|
|
239
410
|
|
|
240
|
-
|
|
411
|
+
### Virtual Views
|
|
412
|
+
|
|
413
|
+
Views let you query documents with categorization, sorting, filtering, and aggregation -- similar to a spreadsheet pivot table. There are two ways to work with views:
|
|
241
414
|
|
|
242
|
-
|
|
415
|
+
**App-defined views** -- create a view definition programmatically:
|
|
243
416
|
|
|
244
417
|
```ts
|
|
245
418
|
import { createViewLanguage } from "mindoodb-app-sdk";
|
|
246
419
|
|
|
247
|
-
const v = createViewLanguage<{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
420
|
+
const v = createViewLanguage<{ employee: string; hours: number }>();
|
|
421
|
+
|
|
422
|
+
const view = await session.createView({
|
|
423
|
+
databaseId: "main",
|
|
424
|
+
definition: {
|
|
425
|
+
title: "Hours by employee",
|
|
426
|
+
defaultExpand: "collapsed",
|
|
427
|
+
columns: [
|
|
428
|
+
{
|
|
429
|
+
name: "employee",
|
|
430
|
+
title: "Employee",
|
|
431
|
+
role: "category",
|
|
432
|
+
expression: v.field("employee"),
|
|
433
|
+
sorting: "ascending",
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: "hours",
|
|
437
|
+
title: "Hours",
|
|
438
|
+
role: "display",
|
|
439
|
+
expression: v.toNumber(v.field("hours")),
|
|
440
|
+
},
|
|
441
|
+
],
|
|
261
442
|
},
|
|
262
|
-
columns: [
|
|
263
|
-
{
|
|
264
|
-
name: "employee",
|
|
265
|
-
title: "Employee",
|
|
266
|
-
role: "category",
|
|
267
|
-
expression: v.field("employee"),
|
|
268
|
-
sorting: "ascending",
|
|
269
|
-
},
|
|
270
|
-
{
|
|
271
|
-
name: "amount",
|
|
272
|
-
title: "Amount",
|
|
273
|
-
role: "display",
|
|
274
|
-
expression: v.let(
|
|
275
|
-
{
|
|
276
|
-
hours: v.toNumber(v.field("hours")),
|
|
277
|
-
rate: v.coalesce(v.toNumber(v.field("rate")), v.number(1)),
|
|
278
|
-
},
|
|
279
|
-
({ hours, rate }) => v.mul(v.coalesce(hours, v.number(0)), v.coalesce(rate, v.number(0))),
|
|
280
|
-
),
|
|
281
|
-
sorting: "descending",
|
|
282
|
-
},
|
|
283
|
-
],
|
|
284
443
|
});
|
|
285
444
|
|
|
286
445
|
const page = await view.page({ pageSize: 100 });
|
|
287
|
-
|
|
446
|
+
console.log(page.rows);
|
|
447
|
+
await view.dispose();
|
|
448
|
+
```
|
|
288
449
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
450
|
+
**Haven-configured views** -- open views that the Haven administrator attached to the app registration:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
const viewDefs = ctx.views; // from launch context
|
|
454
|
+
|
|
455
|
+
const view = await session.openView(viewDefs[0]!.id);
|
|
456
|
+
const page = await view.page({ pageSize: 50 });
|
|
457
|
+
|
|
458
|
+
// Expand/collapse categories
|
|
459
|
+
await view.expandAll();
|
|
460
|
+
await view.collapse(someCategoryKey);
|
|
295
461
|
|
|
296
462
|
await view.dispose();
|
|
297
463
|
```
|
|
298
464
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
- `
|
|
304
|
-
- `
|
|
305
|
-
- `
|
|
306
|
-
- `
|
|
307
|
-
- `
|
|
308
|
-
- `
|
|
309
|
-
- `
|
|
310
|
-
- `
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
- `view.listCategoryDocumentIds(categoryKey)`
|
|
314
|
-
- `view.dispose()`
|
|
315
|
-
|
|
316
|
-
The expression language is fully declarative. Apps define filter and column logic through a typed builder API that compiles to JSON-safe expression objects. No app-provided JavaScript runs inside the bridge host.
|
|
317
|
-
|
|
318
|
-
Helpers currently include:
|
|
319
|
-
|
|
320
|
-
- field/value access: `field()`, `value()`, `origin()`
|
|
321
|
-
- literals and conversion: `literal()`, `string()`, `number()`, `boolean()`, `toNumber()`, `toString()`, `toBoolean()`
|
|
322
|
-
- math and comparisons: `add()`, `sub()`, `mul()`, `div()`, `mod()`, `eq()`, `neq()`, `gt()`, `gte()`, `lt()`, `lte()`
|
|
323
|
-
- boolean logic: `and()`, `or()`, `not()`
|
|
324
|
-
- string helpers: `concat()`, `lower()`, `upper()`, `trim()`, `contains()`, `startsWith()`, `endsWith()`
|
|
325
|
-
- null/existence helpers: `coalesce()`, `exists()`, `notExists()`
|
|
326
|
-
- date/path helpers: `datePart()`, `pathJoin()`
|
|
327
|
-
- control flow: `ifElse()`, `let()`
|
|
328
|
-
|
|
329
|
-
The dedicated language guide lives in the `mindoodb-view-language` package. The SDK re-exports `createViewLanguage()` for convenience.
|
|
465
|
+
The expression language is fully declarative -- apps define filter and column logic through a typed builder API that compiles to JSON-safe expression objects. No app-provided JavaScript runs inside the bridge host.
|
|
466
|
+
|
|
467
|
+
Available expression helpers:
|
|
468
|
+
|
|
469
|
+
- **Field access:** `field()`, `value()`, `origin()`
|
|
470
|
+
- **Literals and conversion:** `literal()`, `string()`, `number()`, `boolean()`, `toNumber()`, `toString()`, `toBoolean()`
|
|
471
|
+
- **Math and comparisons:** `add()`, `sub()`, `mul()`, `div()`, `mod()`, `eq()`, `neq()`, `gt()`, `gte()`, `lt()`, `lte()`
|
|
472
|
+
- **Boolean logic:** `and()`, `or()`, `not()`
|
|
473
|
+
- **String helpers:** `concat()`, `lower()`, `upper()`, `trim()`, `contains()`, `startsWith()`, `endsWith()`
|
|
474
|
+
- **Null/existence:** `coalesce()`, `exists()`, `notExists()`
|
|
475
|
+
- **Date/path:** `datePart()`, `pathJoin()`
|
|
476
|
+
- **Control flow:** `ifElse()`, `let()`
|
|
477
|
+
|
|
478
|
+
The full language guide lives in the `mindoodb-view-language` package. The SDK re-exports `createViewLanguage()` for convenience.
|
|
330
479
|
|
|
331
480
|
## Testing without Haven
|
|
332
481
|
|
|
@@ -334,51 +483,142 @@ You do **not** need a full Haven environment to test apps that use `mindoodb-app
|
|
|
334
483
|
|
|
335
484
|
The SDK ships a dedicated `mindoodb-app-sdk/testing` entrypoint with two levels of test helpers:
|
|
336
485
|
|
|
337
|
-
- Level 1
|
|
338
|
-
- Level 2
|
|
486
|
+
- **Level 1 -- app tests:** Returns a fake `MindooDBAppSession` and bridge for unit, composable, store, and component tests. No `postMessage` or `MessageChannel` involved.
|
|
487
|
+
- **Level 2 -- bridge protocol tests:** A fake host harness that exercises the real `createMindooDBAppBridge()` handshake over `postMessage` and `MessageChannel`. Useful for integration-style tests.
|
|
339
488
|
|
|
340
|
-
Use Level 1 when you
|
|
489
|
+
Use Level 1 when you are testing app behavior. Use Level 2 when you want to validate the bridge transport itself.
|
|
341
490
|
|
|
342
491
|
See [`TESTING.md`](./TESTING.md) for setup instructions and Vitest examples.
|
|
343
492
|
|
|
344
|
-
##
|
|
493
|
+
## Deployment
|
|
345
494
|
|
|
346
|
-
|
|
495
|
+
MindooDB Apps are standard web applications -- deploy them anywhere you can serve static files.
|
|
347
496
|
|
|
348
|
-
|
|
349
|
-
2. Run your app on its own dev server, for example Vite on `http://localhost:4173`.
|
|
350
|
-
3. Register that URL in Haven as an external app.
|
|
351
|
-
4. Launch the app from Haven so it receives a real `mindoodbAppLaunchId`.
|
|
497
|
+
**Cloudflare Pages / Workers** is one of the easiest options. The [example app](https://github.com/niclas-niclas/mindoodb-app-example) is deployed this way at https://mindoodb-app-example.mindoo.de using a simple `wrangler.jsonc` configuration:
|
|
352
498
|
|
|
353
|
-
|
|
499
|
+
```bash
|
|
500
|
+
npm run build
|
|
501
|
+
wrangler deploy
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Other static hosting options work just as well: Netlify, Vercel, any web server serving your `dist/` folder.
|
|
354
505
|
|
|
355
|
-
-
|
|
356
|
-
- hot module reload
|
|
357
|
-
- the full Haven host bridge
|
|
358
|
-
- realistic iframe sizing and theme changes during development
|
|
506
|
+
**Haven-hosted bundles** are an alternative: import your built app directly into Haven, where it is served by a service worker on an opaque origin. Haven-hosted apps load without a network connection and receive a stricter sandbox.
|
|
359
507
|
|
|
360
|
-
|
|
508
|
+
## Local development workflow
|
|
509
|
+
|
|
510
|
+
For interactive development with the full Haven host bridge:
|
|
361
511
|
|
|
362
|
-
|
|
512
|
+
1. Run Haven locally.
|
|
513
|
+
2. Run your app on its own dev server (e.g. `npm run dev` with Vite).
|
|
514
|
+
3. Register the dev server URL in Haven's Application settings.
|
|
515
|
+
4. Map databases and optionally views to the app.
|
|
516
|
+
5. Launch from Haven -- you get hot reload, real bridge communication, and live theme/viewport events.
|
|
517
|
+
|
|
518
|
+
This is separate from the automated testing workflow above. Running Haven is useful for interactive development but not required for Level 1 or Level 2 tests.
|
|
519
|
+
|
|
520
|
+
## API reference
|
|
521
|
+
|
|
522
|
+
### MindooDBAppBridge
|
|
523
|
+
|
|
524
|
+
| Method | Returns |
|
|
525
|
+
|---|---|
|
|
526
|
+
| `connect(options?)` | `Promise<MindooDBAppSession>` |
|
|
527
|
+
|
|
528
|
+
Connect options: `launchId?`, `targetOrigin?`, `connectTimeoutMs?`.
|
|
529
|
+
|
|
530
|
+
### MindooDBAppSession
|
|
531
|
+
|
|
532
|
+
| Method | Returns |
|
|
533
|
+
|---|---|
|
|
534
|
+
| `getLaunchContext()` | `Promise<MindooDBAppLaunchContext>` |
|
|
535
|
+
| `listDatabases()` | `Promise<MindooDBAppDatabaseInfo[]>` |
|
|
536
|
+
| `openDatabase(databaseId)` | `Promise<MindooDBAppDatabase>` |
|
|
537
|
+
| `createView(input)` | `Promise<MindooDBAppViewHandle>` |
|
|
538
|
+
| `openView(viewId)` | `Promise<MindooDBAppViewHandle>` |
|
|
539
|
+
| `onThemeChange(listener)` | `() => void` (unsubscribe) |
|
|
540
|
+
| `onViewportChange(listener)` | `() => void` (unsubscribe) |
|
|
541
|
+
| `disconnect()` | `Promise<void>` |
|
|
542
|
+
|
|
543
|
+
### MindooDBAppDatabase
|
|
544
|
+
|
|
545
|
+
| Property / Method | Returns |
|
|
546
|
+
|---|---|
|
|
547
|
+
| `info()` | `Promise<MindooDBAppDatabaseInfo>` |
|
|
548
|
+
| `documents` | `MindooDBAppDocumentApi` |
|
|
549
|
+
| `attachments` | `MindooDBAppAttachmentApi` |
|
|
550
|
+
|
|
551
|
+
### MindooDBAppDocumentApi
|
|
552
|
+
|
|
553
|
+
| Method | Returns |
|
|
554
|
+
|---|---|
|
|
555
|
+
| `list(query?)` | `Promise<MindooDBAppDocumentListResult>` |
|
|
556
|
+
| `get(docId)` | `Promise<MindooDBAppDocument \| null>` |
|
|
557
|
+
| `create(input)` | `Promise<MindooDBAppDocument>` |
|
|
558
|
+
| `update(docId, patch)` | `Promise<MindooDBAppDocument>` |
|
|
559
|
+
| `delete(docId)` | `Promise<{ ok: true }>` |
|
|
560
|
+
| `listHistory(docId)` | `Promise<MindooDBAppDocumentHistoryEntry[]>` |
|
|
561
|
+
| `getAtTimestamp(docId, timestamp)` | `Promise<MindooDBAppHistoricalDocument>` |
|
|
562
|
+
|
|
563
|
+
`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.
|
|
564
|
+
|
|
565
|
+
### MindooDBAppAttachmentApi
|
|
566
|
+
|
|
567
|
+
| Method | Returns |
|
|
568
|
+
|---|---|
|
|
569
|
+
| `list(docId)` | `Promise<MindooDBAppAttachmentInfo[]>` |
|
|
570
|
+
| `remove(docId, attachmentName)` | `Promise<{ ok: true }>` |
|
|
571
|
+
| `openReadStream(docId, attachmentName)` | `Promise<MindooDBAppReadableAttachmentStream>` |
|
|
572
|
+
| `openWriteStream(docId, attachmentName, contentType?)` | `Promise<MindooDBAppWritableAttachmentStream>` |
|
|
573
|
+
| `openPreview(docId, attachmentName, options?)` | `Promise<{ ok: true }>` |
|
|
574
|
+
|
|
575
|
+
### MindooDBAppViewHandle
|
|
576
|
+
|
|
577
|
+
| Method | Returns |
|
|
578
|
+
|---|---|
|
|
579
|
+
| `getDefinition()` | `Promise<MindooDBAppViewDefinition>` |
|
|
580
|
+
| `refresh()` | `Promise<void>` |
|
|
581
|
+
| `page(input?)` | `Promise<MindooDBAppViewPageResult>` |
|
|
582
|
+
| `getExpansionState()` | `Promise<MindooDBAppViewExpansionState>` |
|
|
583
|
+
| `setExpansionState(state)` | `Promise<MindooDBAppViewExpansionState>` |
|
|
584
|
+
| `expand(rowKey)` | `Promise<MindooDBAppViewExpansionState>` |
|
|
585
|
+
| `collapse(rowKey)` | `Promise<MindooDBAppViewExpansionState>` |
|
|
586
|
+
| `expandAll()` | `Promise<MindooDBAppViewExpansionState>` |
|
|
587
|
+
| `collapseAll()` | `Promise<MindooDBAppViewExpansionState>` |
|
|
588
|
+
| `getRow(rowKey)` | `Promise<MindooDBAppViewRow \| null>` |
|
|
589
|
+
| `getCategory(input)` | `Promise<MindooDBAppViewRow \| null>` |
|
|
590
|
+
| `pageCategory(categoryKey, input?)` | `Promise<MindooDBAppViewPageResult>` |
|
|
591
|
+
| `listCategoryDocumentIds(categoryKey)` | `Promise<string[]>` |
|
|
592
|
+
| `dispose()` | `Promise<void>` |
|
|
593
|
+
|
|
594
|
+
### Exported functions
|
|
595
|
+
|
|
596
|
+
| Function | Description |
|
|
597
|
+
|---|---|
|
|
598
|
+
| `createMindooDBAppBridge()` | Create the bridge object used to connect to Haven |
|
|
599
|
+
| `canPreviewAttachment(fileName, mimeType)` | Check if Haven can preview a file (returns mode or `null`) |
|
|
600
|
+
| `createViewLanguage<T>()` | Create a typed expression builder for view definitions |
|
|
601
|
+
| `abbreviateCanonicalName(value)` | Convert a canonical Notes-style name like `cn=Jane/ou=Dev/o=Mindoo` to `Jane/Dev/Mindoo` |
|
|
602
|
+
| `expandAbbreviatedName(value)` | Convert an abbreviated Notes-style name like `Jane/Dev/Mindoo` to `cn=Jane/ou=Dev/o=Mindoo` |
|
|
363
603
|
|
|
364
604
|
## Permissions and mappings
|
|
365
605
|
|
|
366
|
-
Apps never choose arbitrary database targets
|
|
606
|
+
Apps never choose arbitrary database targets. Haven decides:
|
|
367
607
|
|
|
368
|
-
- which
|
|
369
|
-
-
|
|
370
|
-
- how each logical
|
|
608
|
+
- which databases are visible to the app
|
|
609
|
+
- what capabilities each database has (`read`, `create`, `update`, `delete`, `history`, `attachments`, `views`)
|
|
610
|
+
- how each logical database maps to a concrete tenant and database target
|
|
371
611
|
|
|
372
|
-
Your app should always
|
|
612
|
+
Your app should always adapt to what Haven grants instead of assuming a fixed environment.
|
|
373
613
|
|
|
374
614
|
## When not to use this SDK
|
|
375
615
|
|
|
376
|
-
`mindoodb-app-sdk` is for
|
|
616
|
+
`mindoodb-app-sdk` is for web apps that integrate with the Haven host bridge.
|
|
377
617
|
|
|
378
|
-
If you are embedding generic third-party web content that does not speak the MindooDB bridge protocol, use a general iframe integration
|
|
618
|
+
If you are embedding generic third-party web content that does not speak the MindooDB bridge protocol, use a general iframe integration instead of this SDK.
|
|
379
619
|
|
|
380
620
|
## Current limitations
|
|
381
621
|
|
|
382
|
-
-
|
|
383
|
-
-
|
|
384
|
-
-
|
|
622
|
+
- The bridge expects a browser environment with `window`, `postMessage`, and `MessagePort`.
|
|
623
|
+
- Viewport updates are host-driven and only available when the app is launched by Haven.
|
|
624
|
+
- Your app is responsible for its own UI layout and responsive behavior after receiving host theme and viewport updates.
|