mindoodb-app-sdk 0.0.5 → 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 +490 -258
- package/dist/index.d.ts +21 -3
- package/dist/index.js +45 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +91 -8
- package/dist/{types-Cvlkh3vJ.d.ts → types-BDY_80wd.d.ts} +8 -2
- 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,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
|
-
|
|
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.
|
|
45
67
|
|
|
46
|
-
|
|
68
|
+
Then, in Haven:
|
|
47
69
|
|
|
48
|
-
|
|
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)
|
|
81
|
+
|
|
82
|
+
If you prefer starting from an empty project:
|
|
83
|
+
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
console.log(
|
|
100
|
-
|
|
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
|
-
|
|
196
|
+
Both subscription functions return an unsubscribe callback. Call it during teardown.
|
|
106
197
|
|
|
107
|
-
###
|
|
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
|
|
111
|
-
|
|
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
|
-
|
|
115
|
-
|
|
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
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
269
|
+
// List all revisions
|
|
270
|
+
const history = await db.documents.listHistory(docId);
|
|
271
|
+
// Each entry: { timestamp, publicKey, identityLabel?, isDeleted, isCurrent, summary? }
|
|
132
272
|
|
|
133
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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
|
-
|
|
282
|
+
**Initial scan from the beginning:**
|
|
141
283
|
|
|
142
284
|
```ts
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
338
|
+
Use `status: "all"` for external indexes so deletions are visible and can be removed from your derived state.
|
|
160
339
|
|
|
161
|
-
|
|
340
|
+
### Attachments
|
|
162
341
|
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
- `attachments.openWriteStream(docId, attachmentName, contentType?)`
|
|
364
|
+
// Remove
|
|
365
|
+
await db.attachments.remove(docId, "old-file.txt");
|
|
366
|
+
```
|
|
179
367
|
|
|
180
|
-
|
|
368
|
+
### Attachment previews
|
|
181
369
|
|
|
182
|
-
|
|
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
|
-
|
|
186
|
-
const session = await bridge.connect();
|
|
372
|
+
**Supported preview formats:**
|
|
187
373
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
document.documentElement.dataset.theme = theme.mode;
|
|
194
|
-
});
|
|
385
|
+
**Open the Haven preview dialog:**
|
|
195
386
|
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
201
|
-
const db = await session.openDatabase(databases[0]!.id);
|
|
399
|
+
**Check preview support locally** before showing a preview button:
|
|
202
400
|
|
|
203
|
-
|
|
204
|
-
|
|
401
|
+
```ts
|
|
402
|
+
import { canPreviewAttachment } from "mindoodb-app-sdk";
|
|
205
403
|
|
|
206
|
-
const
|
|
207
|
-
|
|
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
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
446
|
+
console.log(page.rows);
|
|
447
|
+
await view.dispose();
|
|
448
|
+
```
|
|
296
449
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
- `
|
|
312
|
-
- `
|
|
313
|
-
- `
|
|
314
|
-
- `
|
|
315
|
-
- `
|
|
316
|
-
- `
|
|
317
|
-
- `
|
|
318
|
-
- `
|
|
319
|
-
|
|
320
|
-
|
|
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
|
|
346
|
-
- 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.
|
|
347
488
|
|
|
348
|
-
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.
|
|
349
490
|
|
|
350
491
|
See [`TESTING.md`](./TESTING.md) for setup instructions and Vitest examples.
|
|
351
492
|
|
|
352
|
-
##
|
|
493
|
+
## Deployment
|
|
353
494
|
|
|
354
|
-
|
|
495
|
+
MindooDB Apps are standard web applications -- deploy them anywhere you can serve static files.
|
|
355
496
|
|
|
356
|
-
|
|
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/niclas-niclas/mindoodb-app-example) is deployed this way at https://mindoodb-app-example.mindoo.de using a simple `wrangler.jsonc` configuration:
|
|
360
498
|
|
|
361
|
-
|
|
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
|
-
-
|
|
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
|
-
|
|
508
|
+
## Local development workflow
|
|
509
|
+
|
|
510
|
+
For interactive development with the full Haven host bridge:
|
|
369
511
|
|
|
370
|
-
|
|
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
|
|
606
|
+
Apps never choose arbitrary database targets. Haven decides:
|
|
375
607
|
|
|
376
|
-
- which
|
|
377
|
-
-
|
|
378
|
-
- 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
|
|
379
611
|
|
|
380
|
-
Your app should always
|
|
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
|
|
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
|
|
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
|
-
-
|
|
391
|
-
-
|
|
392
|
-
-
|
|
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-
|
|
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-
|
|
1
|
+
import { M as MindooDBAppAttachmentPreviewMode, a as MindooDBAppBridge } from './types-BDY_80wd.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-BDY_80wd.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()) {
|
|
@@ -648,7 +690,9 @@ import {
|
|
|
648
690
|
mindooDBViewLanguageHelpersByName
|
|
649
691
|
} from "mindoodb-view-language";
|
|
650
692
|
export {
|
|
693
|
+
abbreviateCanonicalName,
|
|
651
694
|
canPreviewAttachment,
|
|
652
695
|
createMindooDBAppBridge,
|
|
653
|
-
createViewLanguage
|
|
696
|
+
createViewLanguage,
|
|
697
|
+
expandAbbreviatedName
|
|
654
698
|
};
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -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-
|
|
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-BDY_80wd.js';
|
|
2
2
|
import { MindooDBAppViewDefinition } from 'mindoodb-view-language';
|
|
3
3
|
|
|
4
4
|
type MaybePromise<T> = T | Promise<T>;
|
package/dist/testing/index.js
CHANGED
|
@@ -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(
|
|
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
|
|
268
|
+
items: page,
|
|
269
|
+
nextCursor
|
|
225
270
|
};
|
|
226
271
|
},
|
|
227
272
|
async get(_docId) {
|
|
228
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -139,6 +139,9 @@ interface MindooDBAppDocumentSummary {
|
|
|
139
139
|
data?: Record<string, unknown>;
|
|
140
140
|
attachmentCount?: number;
|
|
141
141
|
updatedAt?: string;
|
|
142
|
+
identityLabel?: string;
|
|
143
|
+
publicKeyFingerprint?: string;
|
|
144
|
+
isDeleted?: boolean;
|
|
142
145
|
}
|
|
143
146
|
/** Fully loaded document returned by `get()`, `create()`, or `update()`. */
|
|
144
147
|
interface MindooDBAppDocument {
|
|
@@ -164,10 +167,13 @@ interface MindooDBAppHistoricalDocument {
|
|
|
164
167
|
state: "missing" | "deleted" | "exists";
|
|
165
168
|
data: Record<string, unknown> | null;
|
|
166
169
|
}
|
|
167
|
-
/** Query options for paging through
|
|
170
|
+
/** Query options for paging through changefeed-backed document listings in a database. */
|
|
168
171
|
interface MindooDBAppDocumentListQuery {
|
|
169
|
-
limit?: number;
|
|
170
172
|
cursor?: string | null;
|
|
173
|
+
limit?: number;
|
|
174
|
+
skip?: number;
|
|
175
|
+
status?: "all" | "existing" | "deleted";
|
|
176
|
+
metadataOnly?: boolean;
|
|
171
177
|
fields?: string[];
|
|
172
178
|
filter?: Record<string, unknown>;
|
|
173
179
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mindoodb-app-sdk",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
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": "
|
|
43
|
+
"mindoodb-view-language": "0.0.6"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^25.5.0",
|