mindoodb-app-sdk 0.0.5 → 0.0.7

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 CHANGED
@@ -1,29 +1,50 @@
1
1
  # mindoodb-app-sdk
2
2
 
3
- `mindoodb-app-sdk` is the browser-side SDK for building apps that run inside MindooDB Haven and talk to the Haven host bridge.
3
+ Build apps that run inside **MindooDB Haven** -- the browser-based workspace for end-to-end encrypted, offline-first data.
4
4
 
5
- Use it when your app is launched by Haven in an iframe or in a separate window and needs:
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
- - launch metadata such as user, runtime, theme, and current iframe viewport
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
- ## What the Haven host bridge provides
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
- The Haven host bridge is a `postMessage` + `MessageChannel` / `MessagePort` connection between your app and Haven.
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
- Once connected, Haven provides:
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
- - a `MindooDBAppSession` for the current launch
19
- - a `launchContext` snapshot with app id, user, runtime, theme, launch parameters, and current viewport
20
- - database handles that are already permission-scoped and mapped by Haven
21
- - live host events:
22
- - `onThemeChange()` when Haven switches between light and dark mode or theme presets
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
- Your app does not open arbitrary MindooDB databases directly. Haven decides which logical databases are visible and what permissions each one has.
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,310 +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
- For local development against an unpublished workspace copy, prefer `npm link`:
55
+ ## Get started in 5 minutes
35
56
 
36
- ```bash
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
- # In your app project
41
- npm link mindoodb-app-sdk
59
+ ```bash
60
+ git clone https://github.com/klehmann/mindoodb-app-example.git
61
+ cd mindoodb-app-example
62
+ npm install
63
+ npm run dev:local
42
64
  ```
43
65
 
44
- When you want to switch back to the published npm version, run `npm unlink mindoodb-app-sdk` in the app project and then `npm install`.
66
+ This spins up a local Vite dev server on `http://localhost:4200` with hot module reload.
45
67
 
46
- ## Getting started
68
+ Then, in Haven:
47
69
 
48
- ### 1. Register an app in Haven
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://app-example.mindoodb.com** so you can register it in Haven without running anything locally.
77
+
78
+ > See the [mindoodb-app-example README](https://github.com/klehmann/mindoodb-app-example) for the full project walkthrough.
79
+
80
+ ## Quick start (from scratch)
81
+
82
+ If you prefer starting from an empty project:
83
+
84
+ ```ts
85
+ import { createMindooDBAppBridge } from "mindoodb-app-sdk";
49
86
 
50
- Create an app registration in Haven that points to either:
87
+ // 1. Create the bridge and connect to Haven
88
+ const bridge = createMindooDBAppBridge();
89
+ const session = await bridge.connect();
51
90
 
52
- - an external dev server or deployment URL, such as `http://localhost:4173`
53
- - a hosted app bundle imported into Haven
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
- Pick one runtime:
97
+ // 3. Subscribe to live host events
98
+ session.onThemeChange((theme) => {
99
+ document.documentElement.dataset.theme = theme.mode;
100
+ });
56
101
 
57
- - `iframe`: the app is embedded in Haven
58
- - `window`: the app opens in its own tab or popup
102
+ session.onViewportChange((viewport) => {
103
+ console.log("Iframe resized:", viewport.width, viewport.height);
104
+ });
59
105
 
60
- ### 2. Let Haven launch the app
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 an app, it injects bridge launch parameters into the app URL:
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
- - `mindoodbAppLaunchId`
65
- - `mindoodbAppId`
66
- - `mindoodbAppRuntime`
114
+ ## Core concepts
67
115
 
68
- `createMindooDBAppBridge().connect()` reads `mindoodbAppLaunchId` from the current browser URL automatically.
116
+ ### Launch context
69
117
 
70
- ### 3. Connect to the bridge
118
+ After connecting, call `session.getLaunchContext()` to receive the initial host snapshot:
71
119
 
72
120
  ```ts
73
- import { createMindooDBAppBridge } from "mindoodb-app-sdk";
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
- const bridge = createMindooDBAppBridge();
76
- const session = await bridge.connect();
77
- const launchContext = await session.getLaunchContext();
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
- If you need to override the launch id or target origin manually:
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
- const session = await createMindooDBAppBridge().connect({
84
- launchId: "custom-launch-id",
85
- targetOrigin: "https://haven.example.com",
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
- ### 4. Read the launch context
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
- const launchContext = await session.getLaunchContext();
95
-
96
- console.log(launchContext.runtime); // "iframe" | "window"
97
- console.log(launchContext.theme.mode); // "light" | "dark"
98
- console.log(launchContext.viewport); // { width, height } | null
99
- console.log(launchContext.user.username);
100
- console.log(launchContext.launchParameters);
101
- console.log(launchContext.databases);
102
- console.log(launchContext.views);
187
+ // Initial snapshot (null in window mode)
188
+ const viewport = ctx.viewport; // { width: 800, height: 600 }
189
+
190
+ // Live updates
191
+ const stopViewport = session.onViewportChange((viewport) => {
192
+ console.log(viewport.width, viewport.height);
193
+ });
103
194
  ```
104
195
 
105
- `launchContext.viewport` is especially useful for responsive layouts inside embedded iframes. It is the initial viewport snapshot; later changes arrive through `onViewportChange()`.
196
+ Both subscription functions return an unsubscribe callback. Call it during teardown.
106
197
 
107
- ### 5. Subscribe to host-driven events
198
+ ### Documents
199
+
200
+ Open a database, then use the `documents` API for CRUD, history, and changefeed-backed document listing:
108
201
 
109
202
  ```ts
110
- const stopThemeSync = session.onThemeChange((theme) => {
111
- console.log("Theme changed:", theme.mode, theme.preset);
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,
112
220
  });
113
221
 
114
- const stopViewportSync = session.onViewportChange((viewport) => {
115
- console.log("Viewport changed:", viewport.width, viewport.height);
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",
116
228
  });
117
229
 
118
- // Later, during teardown:
119
- stopThemeSync();
120
- stopViewportSync();
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);
121
246
  ```
122
247
 
123
- The intended pattern is:
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`.
124
261
 
125
- - use `getLaunchContext()` for the initial host snapshot
126
- - use `onThemeChange()` and `onViewportChange()` for live updates
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.
127
263
 
128
- ### 6. Discover the databases Haven exposed
264
+ ### Document history
265
+
266
+ When the `history` capability is granted, you can walk the full revision timeline of any document:
129
267
 
130
268
  ```ts
131
- const databases = launchContext.databases;
269
+ // List all revisions
270
+ const history = await db.documents.listHistory(docId);
271
+ // Each entry: { timestamp, publicKey, identityLabel?, isDeleted, isCurrent, summary? }
132
272
 
133
- for (const database of databases) {
134
- console.log(database.id, database.title, database.capabilities);
135
- }
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 }
136
276
  ```
137
277
 
138
- Prefer `launchContext.databases` for the initial snapshot so your app discovers databases and views from the same source. `session.listDatabases()` is still available when you want to refresh database metadata later.
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.
139
281
 
140
- ### 7. Open a database and work with documents
282
+ **Initial scan from the beginning:**
141
283
 
142
284
  ```ts
143
- const databases = launchContext.databases;
144
- const db = await session.openDatabase(databases[0]!.id);
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
+ }
307
+
308
+ // Save `cursor` after the last non-empty page.
309
+ ```
145
310
 
146
- const list = await db.documents.list({ limit: 25 });
147
- const firstDoc = list.items[0] ? await db.documents.get(list.items[0].id) : null;
311
+ **Resume later from the saved checkpoint:**
148
312
 
149
- await db.documents.create({
150
- data: {
151
- employee: "Jane Doe",
152
- workDate: "2026-04-04",
153
- hours: 8,
154
- },
155
- decryptionKeyId: "payroll",
313
+ ```ts
314
+ const page = await db.documents.list({
315
+ cursor: savedCursor,
316
+ limit: 500,
317
+ status: "all",
318
+ metadataOnly: true,
156
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.`);
157
336
  ```
158
337
 
159
- Omit `decryptionKeyId` to keep using the tenant's `"default"` document key.
338
+ Use `status: "all"` for external indexes so deletions are visible and can be removed from your derived state.
160
339
 
161
- Document operations:
340
+ ### Attachments
162
341
 
163
- - `documents.list()`
164
- - `documents.get()`
165
- - `documents.create()`
166
- - `documents.update()`
167
- - `documents.delete()`
168
- - `documents.listHistory()`
169
- - `documents.getAtTimestamp()`
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:
170
343
 
171
- When creating a document, apps can optionally pass `decryptionKeyId` to use a named document key instead of `"default"`.
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();
172
358
 
173
- Attachment operations:
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();
174
363
 
175
- - `attachments.list(docId)`
176
- - `attachments.remove(docId, attachmentName)`
177
- - `attachments.openReadStream(docId, attachmentName)`
178
- - `attachments.openWriteStream(docId, attachmentName, contentType?)`
364
+ // Remove
365
+ await db.attachments.remove(docId, "old-file.txt");
366
+ ```
179
367
 
180
- ## Quick start example
368
+ ### Attachment previews
181
369
 
182
- ```ts
183
- import { createMindooDBAppBridge, createViewLanguage } from "mindoodb-app-sdk";
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.
184
371
 
185
- const bridge = createMindooDBAppBridge();
186
- const session = await bridge.connect();
372
+ **Supported preview formats:**
187
373
 
188
- const launchContext = await session.getLaunchContext();
189
- console.log("Connected runtime:", launchContext.runtime);
190
- console.log("Initial viewport:", launchContext.viewport);
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) |
191
384
 
192
- session.onThemeChange((theme) => {
193
- document.documentElement.dataset.theme = theme.mode;
194
- });
385
+ **Open the Haven preview dialog:**
195
386
 
196
- session.onViewportChange((viewport) => {
197
- console.log("Resize:", viewport.width, viewport.height);
387
+ ```ts
388
+ await db.attachments.openPreview(docId, "presentation.pptx");
389
+ ```
390
+
391
+ You can also preview attachments from a **historical document snapshot**:
392
+
393
+ ```ts
394
+ await db.attachments.openPreview(docId, "report.pdf", {
395
+ timestamp: historyEntry.timestamp,
198
396
  });
397
+ ```
199
398
 
200
- const databases = launchContext.databases;
201
- const db = await session.openDatabase(databases[0]!.id);
399
+ **Check preview support locally** before showing a preview button:
202
400
 
203
- const list = await db.documents.list({ limit: 25 });
204
- const firstDoc = list.items[0] ? await db.documents.get(list.items[0].id) : null;
401
+ ```ts
402
+ import { canPreviewAttachment } from "mindoodb-app-sdk";
205
403
 
206
- const v = createViewLanguage<{ employee: string; hours: number; rate?: number }>();
207
- const view = await session.createView({
208
- databaseId: databases[0]!.id,
209
- definition: {
210
- title: "Hours by employee",
211
- defaultExpand: "collapsed",
212
- filter: {
213
- mode: "expression",
214
- expression: v.gt(v.toNumber(v.field("hours")), v.number(0)),
215
- },
216
- columns: [
217
- {
218
- name: "employee",
219
- title: "Employee",
220
- role: "category",
221
- expression: v.field("employee"),
222
- sorting: "ascending",
223
- },
224
- {
225
- name: "amount",
226
- title: "Amount",
227
- role: "display",
228
- expression: v.let(
229
- {
230
- hours: v.toNumber(v.field("hours")),
231
- rate: v.coalesce(v.toNumber(v.field("rate")), v.number(1)),
232
- },
233
- ({ hours, rate }) => v.mul(v.coalesce(hours, v.number(0)), v.coalesce(rate, v.number(0))),
234
- ),
235
- },
236
- ],
237
- },
238
- });
404
+ const mode = canPreviewAttachment("slides.pptx", "application/octet-stream");
405
+ // Returns "pptx" -- Haven can preview this file
239
406
 
240
- const page = await view.page({ pageSize: 100 });
241
- console.log(page.rows);
242
- await view.dispose();
407
+ const unsupported = canPreviewAttachment("data.bin", "application/octet-stream");
408
+ // Returns null -- no built-in preview available
243
409
  ```
244
410
 
245
- ## Views and expression language
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:
246
414
 
247
- Views are app-defined and session-scoped. Create a definition with `session.createView()`, open Haven-configured mappings with `session.openView()`, page through rows, inspect categories, manage expansion state, then dispose the handle when done.
415
+ **App-defined views** -- create a view definition programmatically:
248
416
 
249
417
  ```ts
250
418
  import { createViewLanguage } from "mindoodb-app-sdk";
251
419
 
252
- const v = createViewLanguage<{
253
- employee: string;
254
- workDate: string;
255
- hours: number;
256
- rate?: number;
257
- note?: string;
258
- }>();
420
+ const v = createViewLanguage<{ employee: string; hours: number }>();
259
421
 
260
422
  const view = await session.createView({
261
423
  databaseId: "main",
262
424
  definition: {
263
- title: "By employee",
264
- defaultExpand: "collapsed",
265
- filter: {
266
- mode: "expression",
267
- expression: v.gt(v.toNumber(v.field("hours")), v.number(0)),
268
- },
269
- columns: [
270
- {
271
- name: "employee",
272
- title: "Employee",
273
- role: "category",
274
- expression: v.field("employee"),
275
- sorting: "ascending",
276
- },
277
- {
278
- name: "amount",
279
- title: "Amount",
280
- role: "display",
281
- expression: v.let(
282
- {
283
- hours: v.toNumber(v.field("hours")),
284
- rate: v.coalesce(v.toNumber(v.field("rate")), v.number(1)),
285
- },
286
- ({ hours, rate }) => v.mul(v.coalesce(hours, v.number(0)), v.coalesce(rate, v.number(0))),
287
- ),
288
- sorting: "descending",
289
- },
290
- ],
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
+ ],
291
442
  },
292
443
  });
293
444
 
294
445
  const page = await view.page({ pageSize: 100 });
295
- const firstCategory = page.rows.find((row) => row.type === "category");
446
+ console.log(page.rows);
447
+ await view.dispose();
448
+ ```
296
449
 
297
- if (firstCategory) {
298
- await view.expand(firstCategory.key);
299
- const categoryRows = await view.pageCategory(firstCategory.key, { pageSize: 25 });
300
- const categoryDocumentIds = await view.listCategoryDocumentIds(firstCategory.key);
301
- const sameCategory = await view.getCategory({ path: firstCategory.categoryPath });
302
- }
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);
303
461
 
304
462
  await view.dispose();
305
463
  ```
306
464
 
307
- Available view-handle operations:
308
-
309
- - `view.getDefinition()`
310
- - `view.refresh()`
311
- - `view.page({ pageSize, position, expansion, rootRowKey })`
312
- - `view.getExpansionState()`
313
- - `view.setExpansionState(state)`
314
- - `view.expand(rowKey)`
315
- - `view.collapse(rowKey)`
316
- - `view.expandAll()`
317
- - `view.collapseAll()`
318
- - `view.getRow(rowKey)`
319
- - `view.getCategory({ path })`
320
- - `view.pageCategory(categoryKey, { pageSize, position })`
321
- - `view.listCategoryDocumentIds(categoryKey)`
322
- - `view.dispose()`
323
-
324
- 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.
325
-
326
- Helpers currently include:
327
-
328
- - field/value access: `field()`, `value()`, `origin()`
329
- - literals and conversion: `literal()`, `string()`, `number()`, `boolean()`, `toNumber()`, `toString()`, `toBoolean()`
330
- - math and comparisons: `add()`, `sub()`, `mul()`, `div()`, `mod()`, `eq()`, `neq()`, `gt()`, `gte()`, `lt()`, `lte()`
331
- - boolean logic: `and()`, `or()`, `not()`
332
- - string helpers: `concat()`, `lower()`, `upper()`, `trim()`, `contains()`, `startsWith()`, `endsWith()`
333
- - null/existence helpers: `coalesce()`, `exists()`, `notExists()`
334
- - date/path helpers: `datePart()`, `pathJoin()`
335
- - control flow: `ifElse()`, `let()`
336
-
337
- 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.
338
479
 
339
480
  ## Testing without Haven
340
481
 
@@ -342,51 +483,142 @@ You do **not** need a full Haven environment to test apps that use `mindoodb-app
342
483
 
343
484
  The SDK ships a dedicated `mindoodb-app-sdk/testing` entrypoint with two levels of test helpers:
344
485
 
345
- - Level 1: simple app-test helpers that return a fake `MindooDBAppSession` and bridge object for unit, composable, store, and component tests
346
- - Level 2: a fake bridge host harness for tests that should exercise the real `createMindooDBAppBridge()` handshake over `postMessage` and `MessageChannel`
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.
347
488
 
348
- Use Level 1 when you only need app behavior. Use Level 2 when you want to validate your bridge integration itself.
489
+ Use Level 1 when you are testing app behavior. Use Level 2 when you want to validate the bridge transport itself.
349
490
 
350
491
  See [`TESTING.md`](./TESTING.md) for setup instructions and Vitest examples.
351
492
 
352
- ## Local development workflow
493
+ ## Deployment
353
494
 
354
- The recommended local setup is:
495
+ MindooDB Apps are standard web applications -- deploy them anywhere you can serve static files.
355
496
 
356
- 1. Run Haven locally.
357
- 2. Run your app on its own dev server, for example Vite on `http://localhost:4173`.
358
- 3. Register that URL in Haven as an external app.
359
- 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/klehmann/mindoodb-app-example) is deployed this way at https://app-example.mindoodb.com using a simple `wrangler.jsonc` configuration:
360
498
 
361
- This gives you:
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.
362
505
 
363
- - normal browser dev-server behavior
364
- - hot module reload
365
- - the full Haven host bridge
366
- - 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.
367
507
 
368
- The sample `mindoodb-timerecords` app in this workspace is a good reference for this setup.
508
+ ## Local development workflow
509
+
510
+ For interactive development with the full Haven host bridge:
369
511
 
370
- This local development workflow is separate from the automated testing workflow above. Running Haven locally is useful for interactive development, but it is not required for Level 1 or Level 2 automated tests.
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` |
371
603
 
372
604
  ## Permissions and mappings
373
605
 
374
- Apps never choose arbitrary database targets themselves. Haven decides:
606
+ Apps never choose arbitrary database targets. Haven decides:
375
607
 
376
- - which logical database handles are visible to the app
377
- - whether each handle can read, create, update, delete, inspect history, access attachments, or create views
378
- - how each logical handle maps to a concrete tenant/database target
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
379
611
 
380
- Your app should always react to what Haven grants instead of assuming a fixed environment.
612
+ Your app should always adapt to what Haven grants instead of assuming a fixed environment.
381
613
 
382
614
  ## When not to use this SDK
383
615
 
384
- `mindoodb-app-sdk` is for MindooDB apps that intentionally integrate with the Haven host bridge.
616
+ `mindoodb-app-sdk` is for web apps that integrate with the Haven host bridge.
385
617
 
386
- If you are embedding generic third-party web content that does not speak the MindooDB bridge protocol, use a general iframe integration such as `iframe-resizer` instead of this SDK.
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.
387
619
 
388
620
  ## Current limitations
389
621
 
390
- - the bridge currently expects a browser environment with `window`, `postMessage`, and `MessagePort`
391
- - viewport updates are host-driven and only available when the app is actually launched by Haven
392
- - your app is still responsible for its own UI layout and responsive behavior after receiving host theme and viewport updates
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.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { M as MindooDBAppAttachmentPreviewMode, a as MindooDBAppBridge } from './types-Cvlkh3vJ.js';
2
- export { b as MindooDBAppAttachmentApi, c as MindooDBAppAttachmentChunk, d as MindooDBAppAttachmentInfo, e as MindooDBAppAttachmentPreviewOptions, f as MindooDBAppBridgeConnectMessage, g as MindooDBAppBridgeConnectOptions, h as MindooDBAppBridgeConnectedMessage, i as MindooDBAppBridgeErrorPayload, j as MindooDBAppBridgePortMessage, k as MindooDBAppBridgeRpcError, l as MindooDBAppBridgeRpcMessage, m as MindooDBAppBridgeRpcRequest, n as MindooDBAppBridgeRpcSuccess, o as MindooDBAppBridgeStreamAbortRequest, p as MindooDBAppBridgeStreamAck, q as MindooDBAppBridgeStreamCloseRequest, r as MindooDBAppBridgeStreamError, s as MindooDBAppBridgeStreamMessage, t as MindooDBAppBridgeStreamOpenResult, u as MindooDBAppBridgeStreamReadRequest, v as MindooDBAppBridgeStreamWriteRequest, w as MindooDBAppBridgeThemeChangedMessage, x as MindooDBAppBridgeViewportChangedMessage, y as MindooDBAppCapability, z as MindooDBAppConfiguredViewCategorizationStyle, A as MindooDBAppConfiguredViewColumn, B as MindooDBAppConfiguredViewColumnRole, C as MindooDBAppConfiguredViewExpressionMode, D as MindooDBAppConfiguredViewFieldExpression, E as MindooDBAppConfiguredViewFilterDefinition, F as MindooDBAppConfiguredViewFilterMode, G as MindooDBAppConfiguredViewFilterRule, H as MindooDBAppConfiguredViewFormulaExpression, I as MindooDBAppConfiguredViewFormulaFilterDefinition, J as MindooDBAppConfiguredViewPreviewMode, K as MindooDBAppConfiguredViewRuleFilterDefinition, L as MindooDBAppConfiguredViewRuleMatchMode, N as MindooDBAppConfiguredViewRuleOperator, O as MindooDBAppConfiguredViewValueExpression, P as MindooDBAppCreateDocumentInput, Q as MindooDBAppCreateViewInput, R as MindooDBAppDatabase, S as MindooDBAppDatabaseInfo, T as MindooDBAppDocument, U as MindooDBAppDocumentApi, V as MindooDBAppDocumentHistoryEntry, W as MindooDBAppDocumentListQuery, X as MindooDBAppDocumentListResult, Y as MindooDBAppDocumentSummary, Z as MindooDBAppHistoricalDocument, _ as MindooDBAppHistoryQuery, $ as MindooDBAppHostTheme, a0 as MindooDBAppLaunchContext, a1 as MindooDBAppReadableAttachmentStream, a2 as MindooDBAppResolvedViewDefinition, a3 as MindooDBAppResolvedViewSource, a4 as MindooDBAppRuntime, a5 as MindooDBAppSession, a6 as MindooDBAppThemeMode, a7 as MindooDBAppUpdateDocumentInput, a8 as MindooDBAppViewHandle, a9 as MindooDBAppViewport, aa as MindooDBAppWritableAttachmentStream } from './types-Cvlkh3vJ.js';
1
+ import { M as MindooDBAppAttachmentPreviewMode, a as MindooDBAppBridge } from './types-u5bAeQwY.js';
2
+ export { b as MindooDBAppAttachmentApi, c as MindooDBAppAttachmentChunk, d as MindooDBAppAttachmentInfo, e as MindooDBAppAttachmentPreviewOptions, f as MindooDBAppAttachmentPreviewSession, g as MindooDBAppBridgeConnectMessage, h as MindooDBAppBridgeConnectOptions, i as MindooDBAppBridgeConnectedMessage, j as MindooDBAppBridgeErrorPayload, k as MindooDBAppBridgePortMessage, l as MindooDBAppBridgeRpcError, m as MindooDBAppBridgeRpcMessage, n as MindooDBAppBridgeRpcRequest, o as MindooDBAppBridgeRpcSuccess, p as MindooDBAppBridgeStreamAbortRequest, q as MindooDBAppBridgeStreamAck, r as MindooDBAppBridgeStreamCloseRequest, s as MindooDBAppBridgeStreamError, t as MindooDBAppBridgeStreamMessage, u as MindooDBAppBridgeStreamOpenResult, v as MindooDBAppBridgeStreamReadRequest, w as MindooDBAppBridgeStreamWriteRequest, x as MindooDBAppBridgeThemeChangedMessage, y as MindooDBAppBridgeViewportChangedMessage, z as MindooDBAppCapability, A as MindooDBAppConfiguredViewCategorizationStyle, B as MindooDBAppConfiguredViewColumn, C as MindooDBAppConfiguredViewColumnRole, D as MindooDBAppConfiguredViewExpressionMode, E as MindooDBAppConfiguredViewFieldExpression, F as MindooDBAppConfiguredViewFilterDefinition, G as MindooDBAppConfiguredViewFilterMode, H as MindooDBAppConfiguredViewFilterRule, I as MindooDBAppConfiguredViewFormulaExpression, J as MindooDBAppConfiguredViewFormulaFilterDefinition, K as MindooDBAppConfiguredViewPreviewMode, L as MindooDBAppConfiguredViewRuleFilterDefinition, N as MindooDBAppConfiguredViewRuleMatchMode, O as MindooDBAppConfiguredViewRuleOperator, P as MindooDBAppConfiguredViewValueExpression, Q as MindooDBAppCreateDocumentInput, R as MindooDBAppCreateViewInput, S as MindooDBAppDatabase, T as MindooDBAppDatabaseInfo, U as MindooDBAppDocument, V as MindooDBAppDocumentApi, W as MindooDBAppDocumentHistoryEntry, X as MindooDBAppDocumentListQuery, Y as MindooDBAppDocumentListResult, Z as MindooDBAppDocumentSummary, _ as MindooDBAppHistoricalDocument, $ as MindooDBAppHistoryQuery, a0 as MindooDBAppHostTheme, a1 as MindooDBAppLaunchContext, a2 as MindooDBAppReadableAttachmentStream, a3 as MindooDBAppResolvedViewDefinition, a4 as MindooDBAppResolvedViewSource, a5 as MindooDBAppRuntime, a6 as MindooDBAppSession, a7 as MindooDBAppThemeMode, a8 as MindooDBAppUpdateDocumentInput, a9 as MindooDBAppViewHandle, aa as MindooDBAppViewport, ab as MindooDBAppWritableAttachmentStream } from './types-u5bAeQwY.js';
3
3
  export { MindooDBAppBooleanExpression, MindooDBAppExpression, MindooDBAppFilterDefinition, MindooDBAppViewCategoryChildrenPageRequest, MindooDBAppViewColumn, MindooDBAppViewColumnRole, MindooDBAppViewDefinition, MindooDBAppViewExpansionState, MindooDBAppViewExpressionBase, MindooDBAppViewExpressionDatePart, MindooDBAppViewExpressionOperation, MindooDBAppViewFieldExpression, MindooDBAppViewIfExpression, MindooDBAppViewLetExpression, MindooDBAppViewLiteralExpression, MindooDBAppViewLookupByPath, MindooDBAppViewOperationExpression, MindooDBAppViewOriginExpression, MindooDBAppViewPageRequest, MindooDBAppViewPageResult, MindooDBAppViewRow, MindooDBAppViewSortDirection, MindooDBAppViewTotalMode, MindooDBAppViewValueExpressionRef, MindooDBAppViewVariableExpression, createViewLanguage } from 'mindoodb-view-language';
4
4
 
5
5
  /**
@@ -10,6 +10,24 @@ export { MindooDBAppBooleanExpression, MindooDBAppExpression, MindooDBAppFilterD
10
10
  */
11
11
  declare function canPreviewAttachment(fileName: string, mimeType: string): MindooDBAppAttachmentPreviewMode | null;
12
12
 
13
+ /**
14
+ * Convert a canonical Notes name like `cn=Jane/ou=Dev/o=Mindoo`
15
+ * into the abbreviated form `Jane/Dev/Mindoo`.
16
+ *
17
+ * If the input is not a slash-separated canonical name, the trimmed
18
+ * original value is returned unchanged.
19
+ */
20
+ declare function abbreviateCanonicalName(value: string): string;
21
+ /**
22
+ * Convert an abbreviated Notes name like `Jane/Dev/Mindoo`
23
+ * into canonical form `cn=Jane/ou=Dev/o=Mindoo`.
24
+ *
25
+ * The first segment becomes `cn=`, the last becomes `o=`, and any
26
+ * middle segments become `ou=` parts in order. Values with fewer than
27
+ * two segments are returned unchanged.
28
+ */
29
+ declare function expandAbbreviatedName(value: string): string;
30
+
13
31
  /**
14
32
  * Creates the browser-side bridge entry point used by MindooDB apps.
15
33
  *
@@ -19,4 +37,4 @@ declare function canPreviewAttachment(fileName: string, mimeType: string): Mindo
19
37
  */
20
38
  declare function createMindooDBAppBridge(): MindooDBAppBridge;
21
39
 
22
- export { MindooDBAppAttachmentPreviewMode, MindooDBAppBridge, canPreviewAttachment, createMindooDBAppBridge };
40
+ export { MindooDBAppAttachmentPreviewMode, MindooDBAppBridge, abbreviateCanonicalName, canPreviewAttachment, createMindooDBAppBridge, expandAbbreviatedName };
package/dist/index.js CHANGED
@@ -29,6 +29,48 @@ function canPreviewAttachment(fileName, mimeType) {
29
29
  return null;
30
30
  }
31
31
 
32
+ // src/canonicalNames.ts
33
+ var CANONICAL_PART_PATTERN = /^\s*([^=]+)\s*=\s*(.*?)\s*$/;
34
+ function splitNameParts(value) {
35
+ return value.split("/").map((part) => part.trim()).filter(Boolean);
36
+ }
37
+ function abbreviateCanonicalName(value) {
38
+ const parts = splitNameParts(value);
39
+ if (!parts.length) {
40
+ return value.trim();
41
+ }
42
+ const abbreviatedParts = [];
43
+ for (const part of parts) {
44
+ const match = CANONICAL_PART_PATTERN.exec(part);
45
+ if (!match) {
46
+ return value.trim();
47
+ }
48
+ const partValue = match[2].trim();
49
+ if (!partValue) {
50
+ return value.trim();
51
+ }
52
+ abbreviatedParts.push(partValue);
53
+ }
54
+ return abbreviatedParts.join("/");
55
+ }
56
+ function expandAbbreviatedName(value) {
57
+ const parts = splitNameParts(value);
58
+ if (parts.length < 2) {
59
+ return value.trim();
60
+ }
61
+ const [commonName, ...rest] = parts;
62
+ const organization = rest.at(-1);
63
+ if (!commonName || !organization) {
64
+ return value.trim();
65
+ }
66
+ const organizationUnits = rest.slice(0, -1);
67
+ return [
68
+ `cn=${commonName}`,
69
+ ...organizationUnits.map((part) => `ou=${part}`),
70
+ `o=${organization}`
71
+ ].join("/");
72
+ }
73
+
32
74
  // src/client/portRpcClient.ts
33
75
  var PROTOCOL = "mindoodb-app-bridge";
34
76
  function normalizeRpcValue(value, seen = /* @__PURE__ */ new WeakMap()) {
@@ -565,6 +607,12 @@ var MindooDBAppDatabaseImpl = class {
565
607
  });
566
608
  return new MindooDBAppWritableAttachmentStreamImpl(this.rpc, result.streamId);
567
609
  },
610
+ preparePreviewSession: async (docId, attachmentName, options) => await this.rpc.call("attachments.preparePreviewSession", {
611
+ databaseId: this.databaseId,
612
+ docId,
613
+ attachmentName,
614
+ timestamp: options?.timestamp
615
+ }),
568
616
  openPreview: async (docId, attachmentName, options) => await this.rpc.call("attachments.openPreview", {
569
617
  databaseId: this.databaseId,
570
618
  docId,
@@ -648,7 +696,9 @@ import {
648
696
  mindooDBViewLanguageHelpersByName
649
697
  } from "mindoodb-view-language";
650
698
  export {
699
+ abbreviateCanonicalName,
651
700
  canPreviewAttachment,
652
701
  createMindooDBAppBridge,
653
- createViewLanguage
702
+ createViewLanguage,
703
+ expandAbbreviatedName
654
704
  };
@@ -1,4 +1,4 @@
1
- import { a0 as MindooDBAppLaunchContext, S as MindooDBAppDatabaseInfo, U as MindooDBAppDocumentApi, a8 as MindooDBAppViewHandle, b as MindooDBAppAttachmentApi, a as MindooDBAppBridge, a5 as MindooDBAppSession, m as MindooDBAppBridgeRpcRequest, $ as MindooDBAppHostTheme, a9 as MindooDBAppViewport, j as MindooDBAppBridgePortMessage } from '../types-Cvlkh3vJ.js';
1
+ import { a1 as MindooDBAppLaunchContext, T as MindooDBAppDatabaseInfo, V as MindooDBAppDocumentApi, a9 as MindooDBAppViewHandle, b as MindooDBAppAttachmentApi, a as MindooDBAppBridge, a6 as MindooDBAppSession, n as MindooDBAppBridgeRpcRequest, a0 as MindooDBAppHostTheme, aa as MindooDBAppViewport, k as MindooDBAppBridgePortMessage } from '../types-u5bAeQwY.js';
2
2
  import { MindooDBAppViewDefinition } from 'mindoodb-view-language';
3
3
 
4
4
  type MaybePromise<T> = T | Promise<T>;
@@ -151,6 +151,27 @@ function createDefaultWritableAttachmentStream() {
151
151
  }
152
152
  };
153
153
  }
154
+ function getFieldValue(source, field) {
155
+ return field.split(".").reduce((current, part) => {
156
+ if (current && typeof current === "object" && part in current) {
157
+ return current[part];
158
+ }
159
+ return void 0;
160
+ }, source);
161
+ }
162
+ function matchesDocumentFilter(document, filter) {
163
+ if (!filter) {
164
+ return true;
165
+ }
166
+ return Object.entries(filter).every(([field, expected]) => getFieldValue(document.data, field) === expected);
167
+ }
168
+ function decodeMockListCursor(cursor) {
169
+ if (!cursor) {
170
+ return 0;
171
+ }
172
+ const offset = Number.parseInt(cursor, 10);
173
+ return Number.isFinite(offset) && offset >= 0 ? offset : 0;
174
+ }
154
175
  function createDefaultViewHandle() {
155
176
  const definition = {
156
177
  title: "Mock View",
@@ -217,32 +238,94 @@ function createDefaultViewHandle() {
217
238
  function createDatabaseHandle(definition) {
218
239
  let createCounter = 0;
219
240
  const defaultViewFactory = async () => createDefaultViewHandle();
241
+ const storedDocuments = /* @__PURE__ */ new Map();
220
242
  const defaultDocuments = {
221
- async list(_query) {
243
+ async list(query) {
244
+ const status = query?.status ?? "existing";
245
+ const skip = Math.max(0, query?.skip ?? 0);
246
+ const limit = Math.max(1, query?.limit ?? 50);
247
+ const offset = decodeMockListCursor(query?.cursor) + skip;
248
+ const metadataOnly = query?.metadataOnly ?? false;
249
+ const items = Array.from(storedDocuments.values()).filter((document) => status === "all" ? true : status === "deleted" ? document.isDeleted : !document.isDeleted).filter((document) => metadataOnly ? true : matchesDocumentFilter(document, query?.filter)).sort((left, right) => left.id.localeCompare(right.id));
250
+ const page = items.slice(offset, offset + limit).map((document) => {
251
+ if (metadataOnly) {
252
+ return {
253
+ id: document.id,
254
+ isDeleted: document.isDeleted
255
+ };
256
+ }
257
+ const projectedData = query?.fields ? Object.fromEntries(query.fields.map((field) => [field, getFieldValue(document.data, field)])) : document.data;
258
+ return {
259
+ id: document.id,
260
+ data: projectedData,
261
+ attachmentCount: document.attachments?.length ?? 0,
262
+ updatedAt: document.updatedAt,
263
+ isDeleted: status !== "existing" ? document.isDeleted : void 0
264
+ };
265
+ });
266
+ const nextCursor = offset + page.length < items.length ? String(offset + page.length) : null;
222
267
  return {
223
- items: [],
224
- nextCursor: null
268
+ items: page,
269
+ nextCursor
225
270
  };
226
271
  },
227
272
  async get(_docId) {
228
- return null;
273
+ const document = storedDocuments.get(_docId);
274
+ if (!document || document.isDeleted) {
275
+ return null;
276
+ }
277
+ return {
278
+ id: document.id,
279
+ data: structuredClone(document.data),
280
+ attachments: document.attachments ? structuredClone(document.attachments) : [],
281
+ updatedAt: document.updatedAt
282
+ };
229
283
  },
230
284
  async create(input) {
231
285
  createCounter += 1;
232
- return {
286
+ const createdAt = (/* @__PURE__ */ new Date()).toISOString();
287
+ const created = {
233
288
  id: `doc-${createCounter}`,
234
289
  data: { ...input.data },
235
- attachments: []
290
+ attachments: [],
291
+ updatedAt: createdAt
292
+ };
293
+ storedDocuments.set(created.id, {
294
+ ...created,
295
+ data: structuredClone(created.data),
296
+ attachments: [],
297
+ isDeleted: false
298
+ });
299
+ return {
300
+ ...created
236
301
  };
237
302
  },
238
303
  async update(docId, patch) {
239
- return {
304
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
305
+ const existing = storedDocuments.get(docId);
306
+ const updated = {
240
307
  id: docId,
241
308
  data: { ...patch.data },
242
- attachments: []
309
+ attachments: existing?.attachments ? structuredClone(existing.attachments) : [],
310
+ updatedAt
243
311
  };
312
+ storedDocuments.set(docId, {
313
+ ...updated,
314
+ data: structuredClone(updated.data),
315
+ attachments: updated.attachments ? structuredClone(updated.attachments) : [],
316
+ isDeleted: false
317
+ });
318
+ return updated;
244
319
  },
245
320
  async delete(_docId) {
321
+ const existing = storedDocuments.get(_docId);
322
+ if (existing) {
323
+ storedDocuments.set(_docId, {
324
+ ...existing,
325
+ isDeleted: true,
326
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
327
+ });
328
+ }
246
329
  return { ok: true };
247
330
  },
248
331
  async listHistory(_docId) {
@@ -278,6 +361,12 @@ function createDatabaseHandle(definition) {
278
361
  async openWriteStream(_docId, _attachmentName, _contentType) {
279
362
  return createDefaultWritableAttachmentStream();
280
363
  },
364
+ async preparePreviewSession(_docId, _attachmentName, _options) {
365
+ return {
366
+ sessionId: "preview-session-1",
367
+ previewUrl: "about:blank"
368
+ };
369
+ },
281
370
  async openPreview(_docId, _attachmentName, _options) {
282
371
  return { ok: true };
283
372
  }
@@ -662,6 +751,12 @@ function createFakeBridgeHost(options = {}) {
662
751
  String(params.attachmentName),
663
752
  typeof params.timestamp === "number" ? { timestamp: params.timestamp } : void 0
664
753
  );
754
+ case "attachments.preparePreviewSession":
755
+ return await state.getDatabase(String(params.databaseId)).attachments.preparePreviewSession(
756
+ String(params.docId),
757
+ String(params.attachmentName),
758
+ typeof params.timestamp === "number" ? { timestamp: params.timestamp } : void 0
759
+ );
665
760
  case "views.getDefinition":
666
761
  return await (await resolveViewHandle(String(params.viewId))).getDefinition();
667
762
  case "views.refresh":
@@ -126,6 +126,11 @@ interface MindooDBAppAttachmentPreviewOptions {
126
126
  /** Historical snapshot timestamp previously obtained from `documents.history.getAtTimestamp()`. */
127
127
  timestamp?: number;
128
128
  }
129
+ /** Resolved Haven preview session prepared for opening in a separate tab or window. */
130
+ interface MindooDBAppAttachmentPreviewSession {
131
+ sessionId: string;
132
+ previewUrl: string;
133
+ }
129
134
  /** Database entry visible to the app during launch and database listing. */
130
135
  interface MindooDBAppDatabaseInfo {
131
136
  id: string;
@@ -139,6 +144,9 @@ interface MindooDBAppDocumentSummary {
139
144
  data?: Record<string, unknown>;
140
145
  attachmentCount?: number;
141
146
  updatedAt?: string;
147
+ identityLabel?: string;
148
+ publicKeyFingerprint?: string;
149
+ isDeleted?: boolean;
142
150
  }
143
151
  /** Fully loaded document returned by `get()`, `create()`, or `update()`. */
144
152
  interface MindooDBAppDocument {
@@ -164,10 +172,13 @@ interface MindooDBAppHistoricalDocument {
164
172
  state: "missing" | "deleted" | "exists";
165
173
  data: Record<string, unknown> | null;
166
174
  }
167
- /** Query options for paging through documents in a database. */
175
+ /** Query options for paging through changefeed-backed document listings in a database. */
168
176
  interface MindooDBAppDocumentListQuery {
169
- limit?: number;
170
177
  cursor?: string | null;
178
+ limit?: number;
179
+ skip?: number;
180
+ status?: "all" | "existing" | "deleted";
181
+ metadataOnly?: boolean;
171
182
  fields?: string[];
172
183
  filter?: Record<string, unknown>;
173
184
  }
@@ -355,6 +366,7 @@ interface MindooDBAppAttachmentApi {
355
366
  }>;
356
367
  openReadStream(docId: string, attachmentName: string): Promise<MindooDBAppReadableAttachmentStream>;
357
368
  openWriteStream(docId: string, attachmentName: string, contentType?: string): Promise<MindooDBAppWritableAttachmentStream>;
369
+ preparePreviewSession(docId: string, attachmentName: string, options?: MindooDBAppAttachmentPreviewOptions): Promise<MindooDBAppAttachmentPreviewSession>;
358
370
  openPreview(docId: string, attachmentName: string, options?: MindooDBAppAttachmentPreviewOptions): Promise<{
359
371
  ok: true;
360
372
  }>;
@@ -381,4 +393,4 @@ interface MindooDBAppBridge {
381
393
  connect(options?: MindooDBAppBridgeConnectOptions): Promise<MindooDBAppSession>;
382
394
  }
383
395
 
384
- export type { MindooDBAppHostTheme as $, MindooDBAppConfiguredViewColumn as A, MindooDBAppConfiguredViewColumnRole as B, MindooDBAppConfiguredViewExpressionMode as C, MindooDBAppConfiguredViewFieldExpression as D, MindooDBAppConfiguredViewFilterDefinition as E, MindooDBAppConfiguredViewFilterMode as F, MindooDBAppConfiguredViewFilterRule as G, MindooDBAppConfiguredViewFormulaExpression as H, MindooDBAppConfiguredViewFormulaFilterDefinition as I, MindooDBAppConfiguredViewPreviewMode as J, MindooDBAppConfiguredViewRuleFilterDefinition as K, MindooDBAppConfiguredViewRuleMatchMode as L, MindooDBAppAttachmentPreviewMode as M, MindooDBAppConfiguredViewRuleOperator as N, MindooDBAppConfiguredViewValueExpression as O, MindooDBAppCreateDocumentInput as P, MindooDBAppCreateViewInput as Q, MindooDBAppDatabase as R, MindooDBAppDatabaseInfo as S, MindooDBAppDocument as T, MindooDBAppDocumentApi as U, MindooDBAppDocumentHistoryEntry as V, MindooDBAppDocumentListQuery as W, MindooDBAppDocumentListResult as X, MindooDBAppDocumentSummary as Y, MindooDBAppHistoricalDocument as Z, MindooDBAppHistoryQuery as _, MindooDBAppBridge as a, MindooDBAppLaunchContext as a0, MindooDBAppReadableAttachmentStream as a1, MindooDBAppResolvedViewDefinition as a2, MindooDBAppResolvedViewSource as a3, MindooDBAppRuntime as a4, MindooDBAppSession as a5, MindooDBAppThemeMode as a6, MindooDBAppUpdateDocumentInput as a7, MindooDBAppViewHandle as a8, MindooDBAppViewport as a9, MindooDBAppWritableAttachmentStream as aa, MindooDBAppAttachmentApi as b, MindooDBAppAttachmentChunk as c, MindooDBAppAttachmentInfo as d, MindooDBAppAttachmentPreviewOptions as e, MindooDBAppBridgeConnectMessage as f, MindooDBAppBridgeConnectOptions as g, MindooDBAppBridgeConnectedMessage as h, MindooDBAppBridgeErrorPayload as i, MindooDBAppBridgePortMessage as j, MindooDBAppBridgeRpcError as k, MindooDBAppBridgeRpcMessage as l, MindooDBAppBridgeRpcRequest as m, MindooDBAppBridgeRpcSuccess as n, MindooDBAppBridgeStreamAbortRequest as o, MindooDBAppBridgeStreamAck as p, MindooDBAppBridgeStreamCloseRequest as q, MindooDBAppBridgeStreamError as r, MindooDBAppBridgeStreamMessage as s, MindooDBAppBridgeStreamOpenResult as t, MindooDBAppBridgeStreamReadRequest as u, MindooDBAppBridgeStreamWriteRequest as v, MindooDBAppBridgeThemeChangedMessage as w, MindooDBAppBridgeViewportChangedMessage as x, MindooDBAppCapability as y, MindooDBAppConfiguredViewCategorizationStyle as z };
396
+ export type { MindooDBAppHistoryQuery as $, MindooDBAppConfiguredViewCategorizationStyle as A, MindooDBAppConfiguredViewColumn as B, MindooDBAppConfiguredViewColumnRole as C, MindooDBAppConfiguredViewExpressionMode as D, MindooDBAppConfiguredViewFieldExpression as E, MindooDBAppConfiguredViewFilterDefinition as F, MindooDBAppConfiguredViewFilterMode as G, MindooDBAppConfiguredViewFilterRule as H, MindooDBAppConfiguredViewFormulaExpression as I, MindooDBAppConfiguredViewFormulaFilterDefinition as J, MindooDBAppConfiguredViewPreviewMode as K, MindooDBAppConfiguredViewRuleFilterDefinition as L, MindooDBAppAttachmentPreviewMode as M, MindooDBAppConfiguredViewRuleMatchMode as N, MindooDBAppConfiguredViewRuleOperator as O, MindooDBAppConfiguredViewValueExpression as P, MindooDBAppCreateDocumentInput as Q, MindooDBAppCreateViewInput as R, MindooDBAppDatabase as S, MindooDBAppDatabaseInfo as T, MindooDBAppDocument as U, MindooDBAppDocumentApi as V, MindooDBAppDocumentHistoryEntry as W, MindooDBAppDocumentListQuery as X, MindooDBAppDocumentListResult as Y, MindooDBAppDocumentSummary as Z, MindooDBAppHistoricalDocument as _, MindooDBAppBridge as a, MindooDBAppHostTheme as a0, MindooDBAppLaunchContext as a1, MindooDBAppReadableAttachmentStream as a2, MindooDBAppResolvedViewDefinition as a3, MindooDBAppResolvedViewSource as a4, MindooDBAppRuntime as a5, MindooDBAppSession as a6, MindooDBAppThemeMode as a7, MindooDBAppUpdateDocumentInput as a8, MindooDBAppViewHandle as a9, MindooDBAppViewport as aa, MindooDBAppWritableAttachmentStream as ab, MindooDBAppAttachmentApi as b, MindooDBAppAttachmentChunk as c, MindooDBAppAttachmentInfo as d, MindooDBAppAttachmentPreviewOptions as e, MindooDBAppAttachmentPreviewSession as f, MindooDBAppBridgeConnectMessage as g, MindooDBAppBridgeConnectOptions as h, MindooDBAppBridgeConnectedMessage as i, MindooDBAppBridgeErrorPayload as j, MindooDBAppBridgePortMessage as k, MindooDBAppBridgeRpcError as l, MindooDBAppBridgeRpcMessage as m, MindooDBAppBridgeRpcRequest as n, MindooDBAppBridgeRpcSuccess as o, MindooDBAppBridgeStreamAbortRequest as p, MindooDBAppBridgeStreamAck as q, MindooDBAppBridgeStreamCloseRequest as r, MindooDBAppBridgeStreamError as s, MindooDBAppBridgeStreamMessage as t, MindooDBAppBridgeStreamOpenResult as u, MindooDBAppBridgeStreamReadRequest as v, MindooDBAppBridgeStreamWriteRequest as w, MindooDBAppBridgeThemeChangedMessage as x, MindooDBAppBridgeViewportChangedMessage as y, MindooDBAppCapability as z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mindoodb-app-sdk",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "MindooDB app SDK for building apps that connect to the Haven host bridge",
5
5
  "license": "ISC",
6
6
  "author": "Mindoo GmbH",
@@ -40,7 +40,7 @@
40
40
  "prepublishOnly": "npm run build && npm run test"
41
41
  },
42
42
  "dependencies": {
43
- "mindoodb-view-language": "file:../mindoodb-view-language"
43
+ "mindoodb-view-language": "0.0.7"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@types/node": "^25.5.0",