mindoodb-app-sdk 0.0.64 → 0.0.65
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/TESTING.md +22 -1
- package/dist/testing/index.d.ts +43 -3
- package/dist/testing/index.js +588 -61
- package/package.json +1 -1
package/TESTING.md
CHANGED
|
@@ -108,6 +108,24 @@ Each database entry can provide:
|
|
|
108
108
|
- `methods.attachments`
|
|
109
109
|
- `fulltextSetup` — the initial config returned by `db.getFulltextSetup()`; `db.setFulltextSetup()` overwrites it for the lifetime of the handle. Use this to test your app's full-text bootstrap logic. Note the mock evaluates `text` query clauses regardless of this config.
|
|
110
110
|
- `summarySetup` — the initial config returned by `db.getSummarySetup()`; `db.setSummarySetup()` overwrites it for the lifetime of the handle. The mock's `query()` reads seeded documents directly and ignores this config; use it to test your app's summary bootstrap logic.
|
|
111
|
+
- `automerge: true` — back every document with a real Automerge document, as Haven does (off by default: plain JSON with `mock-head-N` heads). `create`/`update` then return real Automerge heads, `update` applies `set`/`unset`, `json` (with `Automerge.changeAt(json.baseHeads)` when given) and `text` patches as Automerge changes with Haven's rules (missing parents and lists are created, `unset` is idempotent, `counterIncrement` needs a counter, an empty `json` patch is rejected), and `get`/`list`/`query` return the plain projection (atomic strings as strings, counters as numbers, timestamps as ISO strings). `richText` snapshots replace the value at their path and `richTextSteps` are spliced into the text without marks. Seeds are built with a fixed actor, so two mocks from the same seed have identical heads. `mockDatabasesFromDefinition(definition, seed, { automerge: true })` and the test host URL setting `automerge=1` turn it on, too.
|
|
112
|
+
|
|
113
|
+
#### Concurrent edits from another device
|
|
114
|
+
|
|
115
|
+
`applyRemoteUpdate(databaseId, docId, input, { actor? })` on the controller (`createMockMindooDBAppSession`, `createFakeBridgeHost`, the browser test host and `window.__havenTestHost`) writes `input` as another device and syncs it in; live queries update as after any write. With `automerge: true` the device holds the history up to `input.json.baseHeads` (plus its own earlier changes) and writes as its own Automerge actor, so its edit truly merges with an app save at the same heads:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const doc = await database.documents.get("sheet-1"); // heads H
|
|
119
|
+
await mock.applyRemoteUpdate("main", "sheet-1", { // device B, at H
|
|
120
|
+
json: { baseHeads: doc!.heads, listInsert: [{ path: ["rows"], index: 1, values: ["remote"] }] },
|
|
121
|
+
});
|
|
122
|
+
const saved = await database.documents.update("sheet-1", { // the app, also at H
|
|
123
|
+
json: { baseHeads: doc!.heads, listInsert: [{ path: ["rows"], index: 1, values: ["local"] }] },
|
|
124
|
+
});
|
|
125
|
+
// saved.data.rows keeps both entries; saved.heads has two heads
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The relative order of concurrent inserts at the same index follows Automerge's op ids, as in Haven. Without `automerge`, `applyRemoteUpdate` is a plain update.
|
|
111
129
|
|
|
112
130
|
### Evaluating VirtualViews
|
|
113
131
|
|
|
@@ -364,7 +382,7 @@ mountHavenTestHost({
|
|
|
364
382
|
});
|
|
365
383
|
```
|
|
366
384
|
|
|
367
|
-
The panel keeps its settings in the page URL, so a scenario can be linked or opened by Playwright directly: `/__haven-test/?db=main:read,attachments&db=archive:none&enforce=1&users=120&locale=de-DE&theme=dark` (`db=<id>:none` unmaps a database; URL settings win over the options). The same controls are scriptable as `window.__havenTestHost`:
|
|
385
|
+
The panel keeps its settings in the page URL, so a scenario can be linked or opened by Playwright directly: `/__haven-test/?db=main:read,attachments&db=archive:none&enforce=1&users=120&locale=de-DE&theme=dark` (`db=<id>:none` unmaps a database; `automerge=1` backs all databases with Automerge documents; URL settings win over the options). The same controls are scriptable as `window.__havenTestHost`:
|
|
368
386
|
|
|
369
387
|
```ts
|
|
370
388
|
await page.goto("/__haven-test/?enforce=1");
|
|
@@ -376,6 +394,9 @@ await page.evaluate(() => window.__havenTestHost!.setLocale("de-DE"));
|
|
|
376
394
|
await page.evaluate(() => window.__havenTestHost!.setNextScan({ fileName: "receipt.pdf", mimeType: "application/pdf", size: 1024 }));
|
|
377
395
|
await app.getByRole("button", { name: "Scan" }).click();
|
|
378
396
|
expect(await page.evaluate(() => window.__havenTestHost!.log.scans)).toHaveLength(1);
|
|
397
|
+
|
|
398
|
+
// with ?automerge=1: another device edits the document the app has open
|
|
399
|
+
await page.evaluate(() => window.__havenTestHost!.applyRemoteUpdate("main", "sheet-1", { set: { title: "Renamed elsewhere" } }));
|
|
379
400
|
```
|
|
380
401
|
|
|
381
402
|
Agent tools are scriptable through `window.__havenTestHost.agent`, the same controller the Vitest helpers expose as `createFakeBridgeHost().agent`:
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ay as MindooDBAppComponentInfo, bq as MindooDBAppEmbedRect, I as MindooDBAppAgentToolDescriptor, f as MindooDBAppAgentToolErrorCode, V as MindooDBAppBridgeAgentResultMessage, U as MindooDBAppBridgeAgentInvokeMessage, aU as MindooDBAppCreateViewNavigatorInput, de as MindooDBAppViewNavigator, ad as MindooDBAppBridgeRpcRequest, bC as MindooDBAppHostTheme, av as MindooDBAppCapability, j as MindooDBAppDefinition, J as MindooDBAppAttachmentApi, e as MindooDBAppBridge, cQ as MindooDBAppSession, cg as MindooDBAppProposeAppInput, dm as MindooDBAppViewport, d8 as MindooDBAppUiPreferences, cm as MindooDBAppQueryResult, dl as MindooDBAppViewUpdateStats, a9 as MindooDBAppBridgePortMessage, aV as MindooDBAppDatabaseInfo, bl as MindooDBAppEmbedCloseReason, a4 as MindooDBAppBridgeErrorPayload, bX as MindooDBAppLaunchContext, b2 as MindooDBAppDocumentApi, dg as MindooDBAppViewNavigatorOpenOptions, bD as MindooDBAppIdentityApi, d4 as MindooDBAppTimestampApi, b0 as MindooDBAppDirectoryApi,
|
|
1
|
+
import { ay as MindooDBAppComponentInfo, bq as MindooDBAppEmbedRect, I as MindooDBAppAgentToolDescriptor, f as MindooDBAppAgentToolErrorCode, V as MindooDBAppBridgeAgentResultMessage, U as MindooDBAppBridgeAgentInvokeMessage, aU as MindooDBAppCreateViewNavigatorInput, de as MindooDBAppViewNavigator, ad as MindooDBAppBridgeRpcRequest, bC as MindooDBAppHostTheme, av as MindooDBAppCapability, d9 as MindooDBAppUpdateDocumentInput, l as MindooDBAppDocument, j as MindooDBAppDefinition, J as MindooDBAppAttachmentApi, e as MindooDBAppBridge, cQ as MindooDBAppSession, cg as MindooDBAppProposeAppInput, dm as MindooDBAppViewport, d8 as MindooDBAppUiPreferences, cm as MindooDBAppQueryResult, dl as MindooDBAppViewUpdateStats, a9 as MindooDBAppBridgePortMessage, aV as MindooDBAppDatabaseInfo, bl as MindooDBAppEmbedCloseReason, a4 as MindooDBAppBridgeErrorPayload, bX as MindooDBAppLaunchContext, b2 as MindooDBAppDocumentApi, dg as MindooDBAppViewNavigatorOpenOptions, bD as MindooDBAppIdentityApi, d4 as MindooDBAppTimestampApi, b0 as MindooDBAppDirectoryApi, bz as MindooDBAppFulltextSetup, by as MindooDBAppExtractionSetup, cY as MindooDBAppSummarySetup, ch as MindooDBAppProposeAppResult, bN as MindooDBAppIncomingSource, c as MindooDBAppDragStartResult, bj as MindooDBAppDragProfile, bM as MindooDBAppIncomingResult } from '../types-BeRUxGKN.js';
|
|
2
2
|
import * as mindoodb from 'mindoodb';
|
|
3
3
|
import 'mindoodb-view-language';
|
|
4
4
|
|
|
@@ -244,6 +244,14 @@ interface BrowserTestHost {
|
|
|
244
244
|
setEnforceCapabilities(enforce: boolean): void;
|
|
245
245
|
/** Replaces the usernames of the tenant directory (`directory.listUsers()`). */
|
|
246
246
|
setDirectoryUsers(users: readonly string[]): void;
|
|
247
|
+
/**
|
|
248
|
+
* Another device edits a document and the change syncs in: the app's live queries
|
|
249
|
+
* update. With Automerge-backed databases (`automerge=1` / `automerge: true`) the
|
|
250
|
+
* edit is made at `input.json.baseHeads` by another actor and merged, so it is
|
|
251
|
+
* concurrent to app saves at the same heads. See
|
|
252
|
+
* `MockMindooDBAppSessionController.applyRemoteUpdate`.
|
|
253
|
+
*/
|
|
254
|
+
applyRemoteUpdate(databaseId: string, docId: string, input: MindooDBAppUpdateDocumentInput, options?: MockRemoteUpdateOptions): Promise<MindooDBAppDocument>;
|
|
247
255
|
/** Reloads the app frame. The mock data survives; the app's in-memory state does not. */
|
|
248
256
|
reloadApp(): void;
|
|
249
257
|
dispose(): void;
|
|
@@ -271,6 +279,8 @@ interface MockDatabasesFromDefinitionOptions {
|
|
|
271
279
|
* `"none"`: the mock's stateless default.
|
|
272
280
|
*/
|
|
273
281
|
attachments?: "memory" | "none";
|
|
282
|
+
/** Back documents with real Automerge documents (`MockMindooDBAppDatabaseDefinition.automerge`). */
|
|
283
|
+
automerge?: boolean;
|
|
274
284
|
}
|
|
275
285
|
/**
|
|
276
286
|
* Mock databases for every database a `haven-app.json` declares, granted as requested
|
|
@@ -284,10 +294,13 @@ declare function mockDatabasesFromDefinition(definition: Pick<MindooDBAppDefinit
|
|
|
284
294
|
* - `enforce=1` turns the capability checks on
|
|
285
295
|
* - `users=<count>` fills the directory with generated users
|
|
286
296
|
* - `locale=<tag>`, `theme=dark|light`
|
|
297
|
+
* - `automerge=1` backs every database with real Automerge documents (concurrent
|
|
298
|
+
* `baseHeads` merges, real heads); `automerge=0` turns it off
|
|
287
299
|
*/
|
|
288
300
|
interface TestHostUrlSettings {
|
|
289
301
|
overrides: Record<string, MockDatabaseOverride>;
|
|
290
302
|
enforceCapabilities?: boolean;
|
|
303
|
+
automerge?: boolean;
|
|
291
304
|
directoryUserCount?: number;
|
|
292
305
|
locale?: string;
|
|
293
306
|
theme?: "dark" | "light";
|
|
@@ -305,7 +318,7 @@ interface MountHavenTestHostOptions extends Omit<CreateBrowserTestHostOptions, "
|
|
|
305
318
|
label: string;
|
|
306
319
|
}>;
|
|
307
320
|
/**
|
|
308
|
-
* Read `db=`, `enforce=`, `users=`, `locale=` and `
|
|
321
|
+
* Read `db=`, `enforce=`, `users=`, `locale=`, `theme=` and `automerge=` from the page URL (see
|
|
309
322
|
* {@link readTestHostUrlSettings}) and keep the URL in sync with the panel. Default
|
|
310
323
|
* true. URL settings win over the options passed here.
|
|
311
324
|
*/
|
|
@@ -413,6 +426,22 @@ interface MockMindooDBAppDatabaseDefinition {
|
|
|
413
426
|
* results the same way they would in Haven.
|
|
414
427
|
*/
|
|
415
428
|
summarySetup?: MindooDBAppSummarySetup | null;
|
|
429
|
+
/**
|
|
430
|
+
* Back every document with a real Automerge document, like Haven: `update`
|
|
431
|
+
* applies `set`/`unset`, `json` (at `json.baseHeads`) and `text` patches as
|
|
432
|
+
* Automerge changes, heads are real Automerge heads, and `applyRemoteUpdate`
|
|
433
|
+
* on the controller writes as another device so concurrent edits truly
|
|
434
|
+
* merge. Off by default (plain JSON with `mock-head-N` heads).
|
|
435
|
+
*/
|
|
436
|
+
automerge?: boolean;
|
|
437
|
+
}
|
|
438
|
+
/** Options of `applyRemoteUpdate`. */
|
|
439
|
+
interface MockRemoteUpdateOptions {
|
|
440
|
+
/**
|
|
441
|
+
* Automerge actor id (hex) of the simulated device. Defaults to one fixed
|
|
442
|
+
* "remote" actor; pass different ids to simulate several devices.
|
|
443
|
+
*/
|
|
444
|
+
actor?: string;
|
|
416
445
|
}
|
|
417
446
|
/**
|
|
418
447
|
* Stands in for the host-owned sealed channel.
|
|
@@ -497,6 +526,15 @@ interface MockMindooDBAppSessionController {
|
|
|
497
526
|
setEnforceCapabilities(enforce: boolean): void;
|
|
498
527
|
/** Replaces the directory's usernames (see `directoryUsers`). */
|
|
499
528
|
setDirectoryUsers(users: readonly string[]): void;
|
|
529
|
+
/**
|
|
530
|
+
* Writes `input` to a document as another device would, then syncs it in:
|
|
531
|
+
* live queries update like after any write. With `automerge: true` on the
|
|
532
|
+
* database the edit is made by a different Automerge actor on the history
|
|
533
|
+
* up to `input.json.baseHeads` (the current state when omitted) and merged,
|
|
534
|
+
* so it is truly concurrent to app writes at the same heads. Without it, it
|
|
535
|
+
* is a plain update. Bypasses capability checks (it is not the app writing).
|
|
536
|
+
*/
|
|
537
|
+
applyRemoteUpdate(databaseId: string, docId: string, input: MindooDBAppUpdateDocumentInput, options?: MockRemoteUpdateOptions): Promise<MindooDBAppDocument>;
|
|
500
538
|
emitThemeChange(theme: MindooDBAppHostTheme): void;
|
|
501
539
|
emitViewportChange(viewport: MindooDBAppViewport): void;
|
|
502
540
|
emitUiPreferencesChange(uiPreferences: MindooDBAppUiPreferences): void;
|
|
@@ -622,6 +660,8 @@ interface FakeBridgeHostController {
|
|
|
622
660
|
/** See {@link CreateMockMindooDBAppSessionOptions.directoryUsers}. */
|
|
623
661
|
setDirectoryUsers(users: readonly string[]): void;
|
|
624
662
|
getDirectoryUsers(): string[];
|
|
663
|
+
/** See {@link MockMindooDBAppSessionController.applyRemoteUpdate}. */
|
|
664
|
+
applyRemoteUpdate(databaseId: string, docId: string, input: MindooDBAppUpdateDocumentInput, options?: MockRemoteUpdateOptions): Promise<MindooDBAppDocument>;
|
|
625
665
|
/** Embeds the app has open (`session.embeds.open`). */
|
|
626
666
|
listEmbeds(): MockEmbedState[];
|
|
627
667
|
/** Closes an embed and pushes the `embed-event` to the app. */
|
|
@@ -647,4 +687,4 @@ interface FakeBridgeHostController {
|
|
|
647
687
|
}
|
|
648
688
|
declare function createFakeBridgeHost(options?: CreateFakeBridgeHostOptions): FakeBridgeHostController;
|
|
649
689
|
|
|
650
|
-
export { ALL_CAPABILITIES, type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type BrowserTestHostScanMode, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type FakeBridgeRpcResponse, type MockAgentCall, type MockAgentCallResult, type MockAgentConsent, type MockAgentFile, type MockAgentHost, type MockAgentHostController, type MockAgentHostOptions, type MockDatabaseOverride, type MockDatabasesFromDefinitionOptions, type MockEmbedState, type MockEmbeddingFinish, MockForbiddenError, type MockIncomingContentInput, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, TEST_HOST_LOCALES, type TestHostUrlSettings, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMemoryAttachments, createMockAgentHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, generateDirectoryUsers, loadMindoodbForTesting, mockAgentToolPrefix, mockDatabasesFromDefinition, mountHavenTestHost, previewJson, readTestHostUrlSettings, validateMockAgentTools };
|
|
690
|
+
export { ALL_CAPABILITIES, type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type BrowserTestHostScanMode, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type FakeBridgeRpcResponse, type MockAgentCall, type MockAgentCallResult, type MockAgentConsent, type MockAgentFile, type MockAgentHost, type MockAgentHostController, type MockAgentHostOptions, type MockDatabaseOverride, type MockDatabasesFromDefinitionOptions, type MockEmbedState, type MockEmbeddingFinish, MockForbiddenError, type MockIncomingContentInput, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockRemoteUpdateOptions, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, TEST_HOST_LOCALES, type TestHostUrlSettings, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMemoryAttachments, createMockAgentHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, generateDirectoryUsers, loadMindoodbForTesting, mockAgentToolPrefix, mockDatabasesFromDefinition, mountHavenTestHost, previewJson, readTestHostUrlSettings, validateMockAgentTools };
|
package/dist/testing/index.js
CHANGED
|
@@ -173,24 +173,24 @@ async function buildVirtualView(mindoodb, definition, documents, categorizationS
|
|
|
173
173
|
break;
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
|
-
const
|
|
176
|
+
const view2 = await builder.build();
|
|
177
177
|
const changes = /* @__PURE__ */ new Map();
|
|
178
178
|
for (const document2 of documents) {
|
|
179
179
|
if (!matchesFilter(mindoodb, definition, document2)) {
|
|
180
180
|
continue;
|
|
181
181
|
}
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
changes.set(document2.origin,
|
|
182
|
+
const change2 = changes.get(document2.origin) ?? new mindoodb.VirtualViewDataChange(document2.origin);
|
|
183
|
+
change2.addEntry(document2.docId, computeColumnValues(definition, document2));
|
|
184
|
+
changes.set(document2.origin, change2);
|
|
185
185
|
}
|
|
186
|
-
for (const
|
|
187
|
-
if (
|
|
186
|
+
for (const change2 of changes.values()) {
|
|
187
|
+
if (change2.hasChanges()) view2.applyChanges(change2);
|
|
188
188
|
}
|
|
189
|
-
|
|
190
|
-
return
|
|
189
|
+
view2.getRoot();
|
|
190
|
+
return view2;
|
|
191
191
|
}
|
|
192
|
-
function createCoreNavigator(mindoodb,
|
|
193
|
-
const builder = mindoodb.VirtualViewFactory.createNavigator(
|
|
192
|
+
function createCoreNavigator(mindoodb, view2, options) {
|
|
193
|
+
const builder = mindoodb.VirtualViewFactory.createNavigator(view2);
|
|
194
194
|
const includeCategories = options?.includeCategories ?? true;
|
|
195
195
|
const includeDocuments = options?.includeDocuments ?? true;
|
|
196
196
|
if (!includeCategories && includeDocuments) builder.documentsOnly();
|
|
@@ -199,7 +199,7 @@ function createCoreNavigator(mindoodb, view, options) {
|
|
|
199
199
|
if (Array.isArray(options?.rootCategoryPath) && options.rootCategoryPath.length > 0) {
|
|
200
200
|
builder.fromCategory(options.rootCategoryPath.map((part) => String(part)).join("\\"));
|
|
201
201
|
} else if (options?.rootEntryKey) {
|
|
202
|
-
const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(
|
|
202
|
+
const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(view2);
|
|
203
203
|
const rootEntry = temp.getPos(options.rootEntryKey);
|
|
204
204
|
if (rootEntry) builder.fromEntry(rootEntry);
|
|
205
205
|
}
|
|
@@ -447,13 +447,13 @@ function collectSortedDocIds(entries) {
|
|
|
447
447
|
async function createEvaluatingViewNavigator(input, documents) {
|
|
448
448
|
const mindoodb = await loadMindoodbForTesting();
|
|
449
449
|
if (!mindoodb) return null;
|
|
450
|
-
const
|
|
450
|
+
const view2 = await buildVirtualView(
|
|
451
451
|
mindoodb,
|
|
452
452
|
input.definition,
|
|
453
453
|
documents,
|
|
454
454
|
input.categorizationStyle
|
|
455
455
|
);
|
|
456
|
-
const navigator2 = createCoreNavigator(mindoodb,
|
|
456
|
+
const navigator2 = createCoreNavigator(mindoodb, view2, input.options);
|
|
457
457
|
return wrapNavigator(input.definition, navigator2);
|
|
458
458
|
}
|
|
459
459
|
|
|
@@ -1170,6 +1170,7 @@ function mockDatabasesFromDefinition(definition, seed = {}, options = {}) {
|
|
|
1170
1170
|
capabilities: override?.capabilities ? [...new Set(override.capabilities)] : capabilitiesForDefinitionPermissions(database.permissions)
|
|
1171
1171
|
},
|
|
1172
1172
|
documents: seed[database.logicalDatabaseId],
|
|
1173
|
+
...options.automerge ? { automerge: true } : {},
|
|
1173
1174
|
...options.attachments === "none" ? {} : { methods: { attachments: createMemoryAttachments() } }
|
|
1174
1175
|
};
|
|
1175
1176
|
});
|
|
@@ -1190,6 +1191,8 @@ function readTestHostUrlSettings(search) {
|
|
|
1190
1191
|
}
|
|
1191
1192
|
const enforce = params.get("enforce");
|
|
1192
1193
|
if (enforce !== null) settings.enforceCapabilities = enforce === "1" || enforce === "true";
|
|
1194
|
+
const automerge = params.get("automerge");
|
|
1195
|
+
if (automerge !== null) settings.automerge = automerge === "1" || automerge === "true";
|
|
1193
1196
|
const users = Number.parseInt(params.get("users") ?? "", 10);
|
|
1194
1197
|
if (Number.isFinite(users) && users >= 0) settings.directoryUserCount = users;
|
|
1195
1198
|
const locale = params.get("locale");
|
|
@@ -1387,6 +1390,11 @@ function createBrowserTestHost(options) {
|
|
|
1387
1390
|
host.setDirectoryUsers(users);
|
|
1388
1391
|
onChange?.();
|
|
1389
1392
|
},
|
|
1393
|
+
async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
|
|
1394
|
+
const document2 = await host.applyRemoteUpdate(databaseId, docId, input, updateOptions);
|
|
1395
|
+
onChange?.();
|
|
1396
|
+
return document2;
|
|
1397
|
+
},
|
|
1390
1398
|
reloadApp,
|
|
1391
1399
|
dispose() {
|
|
1392
1400
|
window.removeEventListener("message", onMessage);
|
|
@@ -1498,7 +1506,7 @@ function mountHavenTestHost(options) {
|
|
|
1498
1506
|
const databases = (hostOptions.databases ?? []).filter((database) => url.overrides[database.info.id]?.mapped !== false).map((database) => {
|
|
1499
1507
|
const capabilities = url.overrides[database.info.id]?.capabilities;
|
|
1500
1508
|
return capabilities ? { ...database, info: { ...database.info, capabilities } } : database;
|
|
1501
|
-
});
|
|
1509
|
+
}).map((database) => url.automerge === void 0 ? database : { ...database, automerge: url.automerge });
|
|
1502
1510
|
const launchContext = {
|
|
1503
1511
|
...hostOptions.launchContext,
|
|
1504
1512
|
...url.locale ? { locale: url.locale } : {},
|
|
@@ -2343,23 +2351,23 @@ function mergeLaunchContext(current, patch) {
|
|
|
2343
2351
|
...database,
|
|
2344
2352
|
capabilities: [...database.capabilities]
|
|
2345
2353
|
})),
|
|
2346
|
-
views: current.views.map((
|
|
2347
|
-
...
|
|
2348
|
-
sources:
|
|
2349
|
-
columns:
|
|
2354
|
+
views: current.views.map((view2) => ({
|
|
2355
|
+
...view2,
|
|
2356
|
+
sources: view2.sources.map((source) => ({ ...source })),
|
|
2357
|
+
columns: view2.columns.map((column) => ({
|
|
2350
2358
|
...column,
|
|
2351
2359
|
expression: column.expression.mode === "field" ? { ...column.expression } : {
|
|
2352
2360
|
mode: "formula",
|
|
2353
2361
|
expression: structuredClone(column.expression.expression)
|
|
2354
2362
|
}
|
|
2355
2363
|
})),
|
|
2356
|
-
filter:
|
|
2364
|
+
filter: view2.filter.mode === "rules" ? {
|
|
2357
2365
|
mode: "rules",
|
|
2358
|
-
match:
|
|
2359
|
-
rules:
|
|
2366
|
+
match: view2.filter.match,
|
|
2367
|
+
rules: view2.filter.rules.map((rule) => ({ ...rule }))
|
|
2360
2368
|
} : {
|
|
2361
2369
|
mode: "formula",
|
|
2362
|
-
expression: structuredClone(
|
|
2370
|
+
expression: structuredClone(view2.filter.expression)
|
|
2363
2371
|
}
|
|
2364
2372
|
}))
|
|
2365
2373
|
};
|
|
@@ -2381,41 +2389,41 @@ function mergeLaunchContext(current, patch) {
|
|
|
2381
2389
|
...database,
|
|
2382
2390
|
capabilities: [...database.capabilities]
|
|
2383
2391
|
})),
|
|
2384
|
-
views: patch.views ? patch.views.map((
|
|
2385
|
-
...
|
|
2386
|
-
sources:
|
|
2387
|
-
columns:
|
|
2392
|
+
views: patch.views ? patch.views.map((view2) => ({
|
|
2393
|
+
...view2,
|
|
2394
|
+
sources: view2.sources.map((source) => ({ ...source })),
|
|
2395
|
+
columns: view2.columns.map((column) => ({
|
|
2388
2396
|
...column,
|
|
2389
2397
|
expression: column.expression.mode === "field" ? { ...column.expression } : {
|
|
2390
2398
|
mode: "formula",
|
|
2391
2399
|
expression: structuredClone(column.expression.expression)
|
|
2392
2400
|
}
|
|
2393
2401
|
})),
|
|
2394
|
-
filter:
|
|
2402
|
+
filter: view2.filter.mode === "rules" ? {
|
|
2395
2403
|
mode: "rules",
|
|
2396
|
-
match:
|
|
2397
|
-
rules:
|
|
2404
|
+
match: view2.filter.match,
|
|
2405
|
+
rules: view2.filter.rules.map((rule) => ({ ...rule }))
|
|
2398
2406
|
} : {
|
|
2399
2407
|
mode: "formula",
|
|
2400
|
-
expression: structuredClone(
|
|
2408
|
+
expression: structuredClone(view2.filter.expression)
|
|
2401
2409
|
}
|
|
2402
|
-
})) : current.views.map((
|
|
2403
|
-
...
|
|
2404
|
-
sources:
|
|
2405
|
-
columns:
|
|
2410
|
+
})) : current.views.map((view2) => ({
|
|
2411
|
+
...view2,
|
|
2412
|
+
sources: view2.sources.map((source) => ({ ...source })),
|
|
2413
|
+
columns: view2.columns.map((column) => ({
|
|
2406
2414
|
...column,
|
|
2407
2415
|
expression: column.expression.mode === "field" ? { ...column.expression } : {
|
|
2408
2416
|
mode: "formula",
|
|
2409
2417
|
expression: structuredClone(column.expression.expression)
|
|
2410
2418
|
}
|
|
2411
2419
|
})),
|
|
2412
|
-
filter:
|
|
2420
|
+
filter: view2.filter.mode === "rules" ? {
|
|
2413
2421
|
mode: "rules",
|
|
2414
|
-
match:
|
|
2415
|
-
rules:
|
|
2422
|
+
match: view2.filter.match,
|
|
2423
|
+
rules: view2.filter.rules.map((rule) => ({ ...rule }))
|
|
2416
2424
|
} : {
|
|
2417
2425
|
mode: "formula",
|
|
2418
|
-
expression: structuredClone(
|
|
2426
|
+
expression: structuredClone(view2.filter.expression)
|
|
2419
2427
|
}
|
|
2420
2428
|
}))
|
|
2421
2429
|
};
|
|
@@ -2506,9 +2514,12 @@ function mockDecryptionKeyId(document2) {
|
|
|
2506
2514
|
}
|
|
2507
2515
|
return { decryptionKeyId: document2.decryptionKeyId };
|
|
2508
2516
|
}
|
|
2509
|
-
function loadMockAutomergeDocument(document2) {
|
|
2517
|
+
function loadMockAutomergeDocument(document2, actor) {
|
|
2510
2518
|
if (document2.automergeBinary) {
|
|
2511
|
-
return Automerge.load(
|
|
2519
|
+
return Automerge.load(
|
|
2520
|
+
document2.automergeBinary,
|
|
2521
|
+
actor ? { actor } : void 0
|
|
2522
|
+
);
|
|
2512
2523
|
}
|
|
2513
2524
|
const initialDoc = Automerge.from({
|
|
2514
2525
|
...structuredClone(document2.data),
|
|
@@ -2519,9 +2530,401 @@ function loadMockAutomergeDocument(document2) {
|
|
|
2519
2530
|
}
|
|
2520
2531
|
function persistMockAutomergeDocument(document2, automergeDoc) {
|
|
2521
2532
|
document2.automergeBinary = Automerge.save(automergeDoc);
|
|
2522
|
-
document2.data =
|
|
2533
|
+
document2.data = projectMockAutomergeValue(
|
|
2534
|
+
Automerge.load(document2.automergeBinary)
|
|
2535
|
+
);
|
|
2523
2536
|
document2.heads = Automerge.getHeads(automergeDoc);
|
|
2524
2537
|
}
|
|
2538
|
+
var MOCK_AUTOMERGE_SEED_ACTOR = "5eed0000000000000000000000000000";
|
|
2539
|
+
var MOCK_AUTOMERGE_LOCAL_ACTOR = "10ca1000000000000000000000000000";
|
|
2540
|
+
var MOCK_AUTOMERGE_REMOTE_ACTOR = "2e307e00000000000000000000000000";
|
|
2541
|
+
function projectMockAutomergeValue(value, depth = 0) {
|
|
2542
|
+
if (value === null || typeof value !== "object") {
|
|
2543
|
+
return value;
|
|
2544
|
+
}
|
|
2545
|
+
if (Automerge.isCounter(value)) {
|
|
2546
|
+
return value.value;
|
|
2547
|
+
}
|
|
2548
|
+
if (Automerge.isImmutableString(value)) {
|
|
2549
|
+
return String(value);
|
|
2550
|
+
}
|
|
2551
|
+
if (value instanceof Date) {
|
|
2552
|
+
return value.toISOString();
|
|
2553
|
+
}
|
|
2554
|
+
if (value instanceof Uint8Array) {
|
|
2555
|
+
return value.slice();
|
|
2556
|
+
}
|
|
2557
|
+
if (depth > 100) {
|
|
2558
|
+
throw new Error("Document value is nested too deeply");
|
|
2559
|
+
}
|
|
2560
|
+
if (Array.isArray(value)) {
|
|
2561
|
+
return value.map((entry) => projectMockAutomergeValue(entry, depth + 1));
|
|
2562
|
+
}
|
|
2563
|
+
return Object.fromEntries(
|
|
2564
|
+
Object.entries(value).map(([key, entry]) => [
|
|
2565
|
+
key,
|
|
2566
|
+
projectMockAutomergeValue(entry, depth + 1)
|
|
2567
|
+
])
|
|
2568
|
+
);
|
|
2569
|
+
}
|
|
2570
|
+
function hydrateMockAutomergeValues(value, depth = 0) {
|
|
2571
|
+
if (value === null || typeof value !== "object") {
|
|
2572
|
+
return value;
|
|
2573
|
+
}
|
|
2574
|
+
if (depth > 100) {
|
|
2575
|
+
throw new Error("Document value is nested too deeply");
|
|
2576
|
+
}
|
|
2577
|
+
if (value instanceof Uint8Array || value instanceof Date || Automerge.isCounter(value) || Automerge.isImmutableString(value)) {
|
|
2578
|
+
return value;
|
|
2579
|
+
}
|
|
2580
|
+
if (Array.isArray(value)) {
|
|
2581
|
+
return value.map(
|
|
2582
|
+
(entry) => entry === void 0 ? null : hydrateMockAutomergeValues(entry, depth + 1)
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
const record = value;
|
|
2586
|
+
if (Object.prototype.hasOwnProperty.call(record, MINDOODB_APP_VALUE_TAG)) {
|
|
2587
|
+
switch (record[MINDOODB_APP_VALUE_TAG]) {
|
|
2588
|
+
case "atomic":
|
|
2589
|
+
if (typeof record.value !== "string") {
|
|
2590
|
+
throw new Error('Typed value "atomic" needs a string value');
|
|
2591
|
+
}
|
|
2592
|
+
return new Automerge.ImmutableString(record.value);
|
|
2593
|
+
case "counter":
|
|
2594
|
+
assertMockCounterAmount(record.value, 'Typed value "counter"');
|
|
2595
|
+
return new Automerge.Counter(record.value);
|
|
2596
|
+
case "timestamp":
|
|
2597
|
+
return new Date(toMockTimestamp(record.value));
|
|
2598
|
+
default:
|
|
2599
|
+
throw new Error(
|
|
2600
|
+
`Unknown typed value ${JSON.stringify(record[MINDOODB_APP_VALUE_TAG])}; the "${MINDOODB_APP_VALUE_TAG}" key is reserved`
|
|
2601
|
+
);
|
|
2602
|
+
}
|
|
2603
|
+
}
|
|
2604
|
+
const result = {};
|
|
2605
|
+
for (const [key, entry] of Object.entries(record)) {
|
|
2606
|
+
if (entry !== void 0) {
|
|
2607
|
+
result[key] = hydrateMockAutomergeValues(entry, depth + 1);
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
return result;
|
|
2611
|
+
}
|
|
2612
|
+
function createMockAutomergeDocument(values, actor, time) {
|
|
2613
|
+
const entries = Object.entries(values).filter(([, value]) => value !== void 0);
|
|
2614
|
+
const initial = Automerge.init({ actor });
|
|
2615
|
+
const options = time === void 0 ? {} : { time };
|
|
2616
|
+
if (entries.length === 0) {
|
|
2617
|
+
return Automerge.emptyChange(initial, options);
|
|
2618
|
+
}
|
|
2619
|
+
return Automerge.change(initial, options, (doc) => {
|
|
2620
|
+
for (const [key, value] of entries) {
|
|
2621
|
+
doc[key] = hydrateMockAutomergeValues(value);
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2625
|
+
function validateMockJsonPath(path, label) {
|
|
2626
|
+
if (!Array.isArray(path) || path.length === 0) {
|
|
2627
|
+
throw new Error(`${label} path must contain at least one segment`);
|
|
2628
|
+
}
|
|
2629
|
+
for (const segment of path) {
|
|
2630
|
+
if (typeof segment !== "string" && typeof segment !== "number") {
|
|
2631
|
+
throw new Error(`${label} path segments must be strings or numbers`);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
}
|
|
2635
|
+
function assertMockNonNegativeInteger(value, label) {
|
|
2636
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2637
|
+
throw new Error(`${label} must be a non-negative integer`);
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
function validateMockJsonPatch(patch) {
|
|
2641
|
+
const operationCount = (patch.set?.length ?? 0) + (patch.unset?.length ?? 0) + (patch.listDelete?.length ?? 0) + (patch.listInsert?.length ?? 0) + (patch.textSplice?.length ?? 0) + (patch.textMark?.length ?? 0) + (patch.textUnmark?.length ?? 0) + (patch.counterIncrement?.length ?? 0);
|
|
2642
|
+
if (operationCount === 0) {
|
|
2643
|
+
throw new Error("JSON patch must include at least one operation");
|
|
2644
|
+
}
|
|
2645
|
+
for (const operation of patch.set ?? []) {
|
|
2646
|
+
validateMockJsonPath(operation.path, "JSON set");
|
|
2647
|
+
hydrateMockAutomergeValues(operation.value);
|
|
2648
|
+
}
|
|
2649
|
+
for (const operation of patch.unset ?? []) {
|
|
2650
|
+
validateMockJsonPath(operation.path, "JSON unset");
|
|
2651
|
+
}
|
|
2652
|
+
for (const operation of patch.listDelete ?? []) {
|
|
2653
|
+
validateMockJsonPath(operation.path, "JSON listDelete");
|
|
2654
|
+
assertMockNonNegativeInteger(operation.index, "JSON listDelete index");
|
|
2655
|
+
assertMockNonNegativeInteger(operation.deleteCount, "JSON listDelete deleteCount");
|
|
2656
|
+
}
|
|
2657
|
+
for (const operation of patch.listInsert ?? []) {
|
|
2658
|
+
validateMockJsonPath(operation.path, "JSON listInsert");
|
|
2659
|
+
assertMockNonNegativeInteger(operation.index, "JSON listInsert index");
|
|
2660
|
+
if (!Array.isArray(operation.values)) {
|
|
2661
|
+
throw new Error("JSON listInsert values must be an array");
|
|
2662
|
+
}
|
|
2663
|
+
hydrateMockAutomergeValues(operation.values);
|
|
2664
|
+
}
|
|
2665
|
+
for (const operation of patch.textSplice ?? []) {
|
|
2666
|
+
validateMockJsonPath(operation.path, "JSON textSplice");
|
|
2667
|
+
assertMockNonNegativeInteger(operation.index, "JSON textSplice index");
|
|
2668
|
+
assertMockNonNegativeInteger(operation.deleteCount, "JSON textSplice deleteCount");
|
|
2669
|
+
if (operation.insert !== void 0 && typeof operation.insert !== "string") {
|
|
2670
|
+
throw new Error("JSON textSplice insert must be a string");
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
for (const operation of [...patch.textMark ?? [], ...patch.textUnmark ?? []]) {
|
|
2674
|
+
validateMockJsonPath(operation.path, "JSON text mark");
|
|
2675
|
+
assertMockNonNegativeInteger(operation.index, "JSON text mark index");
|
|
2676
|
+
if (!Number.isInteger(operation.length) || operation.length <= 0) {
|
|
2677
|
+
throw new Error("JSON text mark length must be a positive integer");
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
for (const operation of patch.counterIncrement ?? []) {
|
|
2681
|
+
validateMockJsonPath(operation.path, "JSON counterIncrement");
|
|
2682
|
+
assertMockCounterAmount(operation.delta, "JSON counterIncrement delta");
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
function ensureMockAutomergeParent(target, path) {
|
|
2686
|
+
let parent = target;
|
|
2687
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
2688
|
+
const segment = path[index];
|
|
2689
|
+
const nextSegment = path[index + 1];
|
|
2690
|
+
if (parent[segment] === void 0 || parent[segment] === null) {
|
|
2691
|
+
parent[segment] = typeof nextSegment === "number" ? [] : {};
|
|
2692
|
+
}
|
|
2693
|
+
parent = parent[segment];
|
|
2694
|
+
if (parent === null || typeof parent !== "object") {
|
|
2695
|
+
throw new Error(
|
|
2696
|
+
`Cannot apply JSON patch through non-object path segment '${String(segment)}'`
|
|
2697
|
+
);
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
return parent;
|
|
2701
|
+
}
|
|
2702
|
+
function readMockAutomergeValue(target, path) {
|
|
2703
|
+
let value = target;
|
|
2704
|
+
for (const segment of path) {
|
|
2705
|
+
value = value?.[segment];
|
|
2706
|
+
}
|
|
2707
|
+
return value;
|
|
2708
|
+
}
|
|
2709
|
+
function setMockAutomergeValue(target, path, value) {
|
|
2710
|
+
const parent = ensureMockAutomergeParent(target, path);
|
|
2711
|
+
parent[path[path.length - 1]] = value;
|
|
2712
|
+
}
|
|
2713
|
+
function unsetMockAutomergeValue(target, path) {
|
|
2714
|
+
let parent = target;
|
|
2715
|
+
for (let index = 0; index < path.length - 1; index += 1) {
|
|
2716
|
+
parent = parent?.[path[index]];
|
|
2717
|
+
if (parent === null || typeof parent !== "object") {
|
|
2718
|
+
return;
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
delete parent[path[path.length - 1]];
|
|
2722
|
+
}
|
|
2723
|
+
function readMockAutomergeList(target, path) {
|
|
2724
|
+
const value = readMockAutomergeValue(target, path);
|
|
2725
|
+
if (!Array.isArray(value)) {
|
|
2726
|
+
throw new Error(
|
|
2727
|
+
`Cannot apply JSON list operation to non-array value at '${path.map(String).join(".")}'`
|
|
2728
|
+
);
|
|
2729
|
+
}
|
|
2730
|
+
return value;
|
|
2731
|
+
}
|
|
2732
|
+
function ensureMockAutomergeList(target, path) {
|
|
2733
|
+
const parent = ensureMockAutomergeParent(target, path);
|
|
2734
|
+
const leaf = path[path.length - 1];
|
|
2735
|
+
if (parent[leaf] === void 0 || parent[leaf] === null) {
|
|
2736
|
+
parent[leaf] = [];
|
|
2737
|
+
}
|
|
2738
|
+
return readMockAutomergeList(target, path);
|
|
2739
|
+
}
|
|
2740
|
+
function clampMockIndex(index, length) {
|
|
2741
|
+
if (!Number.isFinite(index)) {
|
|
2742
|
+
return length;
|
|
2743
|
+
}
|
|
2744
|
+
return Math.max(0, Math.min(Math.trunc(index), length));
|
|
2745
|
+
}
|
|
2746
|
+
function ensureMockAutomergeText(target, path, label) {
|
|
2747
|
+
const value = readMockAutomergeValue(target, path);
|
|
2748
|
+
if (value === void 0 || value === null) {
|
|
2749
|
+
setMockAutomergeValue(target, path, "");
|
|
2750
|
+
} else if (typeof value !== "string") {
|
|
2751
|
+
throw new Error(
|
|
2752
|
+
`Cannot apply ${label} to non-string value at '${path.map(String).join(".")}'`
|
|
2753
|
+
);
|
|
2754
|
+
}
|
|
2755
|
+
}
|
|
2756
|
+
function spliceMockAutomergeText(target, path, index, deleteCount, insert) {
|
|
2757
|
+
const text = readMockAutomergeValue(target, path);
|
|
2758
|
+
const length = typeof text === "string" ? text.length : 0;
|
|
2759
|
+
const start = clampMockIndex(index, length);
|
|
2760
|
+
Automerge.splice(
|
|
2761
|
+
target,
|
|
2762
|
+
path,
|
|
2763
|
+
start,
|
|
2764
|
+
Math.max(0, Math.min(deleteCount, length - start)),
|
|
2765
|
+
insert
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
function reviveMockRichTextValue(value) {
|
|
2769
|
+
if (Array.isArray(value)) {
|
|
2770
|
+
return value.map((entry) => reviveMockRichTextValue(entry));
|
|
2771
|
+
}
|
|
2772
|
+
if (!value || typeof value !== "object") {
|
|
2773
|
+
return value;
|
|
2774
|
+
}
|
|
2775
|
+
const record = value;
|
|
2776
|
+
if (record.type === "immutableString" && typeof record.value === "string") {
|
|
2777
|
+
return new Automerge.ImmutableString(record.value);
|
|
2778
|
+
}
|
|
2779
|
+
return Object.fromEntries(
|
|
2780
|
+
Object.entries(record).map(([key, entry]) => [key, reviveMockRichTextValue(entry)])
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
function applyMockAutomergeJsonOperations(target, patch) {
|
|
2784
|
+
for (const operation of patch.set ?? []) {
|
|
2785
|
+
setMockAutomergeValue(target, operation.path, hydrateMockAutomergeValues(operation.value));
|
|
2786
|
+
}
|
|
2787
|
+
for (const operation of patch.unset ?? []) {
|
|
2788
|
+
unsetMockAutomergeValue(target, operation.path);
|
|
2789
|
+
}
|
|
2790
|
+
for (const operation of patch.listDelete ?? []) {
|
|
2791
|
+
readMockAutomergeList(target, operation.path).splice(operation.index, operation.deleteCount);
|
|
2792
|
+
}
|
|
2793
|
+
for (const operation of patch.listInsert ?? []) {
|
|
2794
|
+
ensureMockAutomergeList(target, operation.path).splice(
|
|
2795
|
+
operation.index,
|
|
2796
|
+
0,
|
|
2797
|
+
...hydrateMockAutomergeValues(operation.values)
|
|
2798
|
+
);
|
|
2799
|
+
}
|
|
2800
|
+
for (const operation of patch.textSplice ?? []) {
|
|
2801
|
+
ensureMockAutomergeText(target, operation.path, "JSON textSplice");
|
|
2802
|
+
spliceMockAutomergeText(
|
|
2803
|
+
target,
|
|
2804
|
+
operation.path,
|
|
2805
|
+
operation.index,
|
|
2806
|
+
operation.deleteCount,
|
|
2807
|
+
operation.insert ?? ""
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2810
|
+
for (const operation of patch.textMark ?? []) {
|
|
2811
|
+
for (const [name, value] of Object.entries(operation.marks)) {
|
|
2812
|
+
Automerge.mark(
|
|
2813
|
+
target,
|
|
2814
|
+
operation.path,
|
|
2815
|
+
{ start: operation.index, end: operation.index + operation.length, expand: "none" },
|
|
2816
|
+
name,
|
|
2817
|
+
reviveMockRichTextValue(value)
|
|
2818
|
+
);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
for (const operation of patch.textUnmark ?? []) {
|
|
2822
|
+
for (const name of operation.names) {
|
|
2823
|
+
Automerge.unmark(
|
|
2824
|
+
target,
|
|
2825
|
+
operation.path,
|
|
2826
|
+
{ start: operation.index, end: operation.index + operation.length, expand: "none" },
|
|
2827
|
+
name
|
|
2828
|
+
);
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
for (const operation of patch.counterIncrement ?? []) {
|
|
2832
|
+
const parent = ensureMockAutomergeParent(target, operation.path);
|
|
2833
|
+
const leaf = operation.path[operation.path.length - 1];
|
|
2834
|
+
const existing = parent[leaf];
|
|
2835
|
+
if (existing === void 0 || existing === null) {
|
|
2836
|
+
parent[leaf] = new Automerge.Counter(operation.delta);
|
|
2837
|
+
} else if (Automerge.isCounter(existing)) {
|
|
2838
|
+
existing.increment(operation.delta);
|
|
2839
|
+
} else {
|
|
2840
|
+
throw new Error(
|
|
2841
|
+
`Cannot increment non-counter value at '${operation.path.map(String).join(".")}'; create it with MindooValue.counter() first`
|
|
2842
|
+
);
|
|
2843
|
+
}
|
|
2844
|
+
}
|
|
2845
|
+
}
|
|
2846
|
+
function changeMockAutomergeDocument(doc, baseHeads, apply) {
|
|
2847
|
+
if (baseHeads && baseHeads.length > 0) {
|
|
2848
|
+
const { newDoc } = Automerge.changeAt(doc, baseHeads, apply);
|
|
2849
|
+
return Automerge.load(Automerge.save(newDoc), { actor: Automerge.getActorId(doc) });
|
|
2850
|
+
}
|
|
2851
|
+
return Automerge.change(doc, apply);
|
|
2852
|
+
}
|
|
2853
|
+
function applyMockAutomergeUpdate(doc, patch) {
|
|
2854
|
+
let next = doc;
|
|
2855
|
+
if (patch.set || (patch.unset?.length ?? 0) > 0) {
|
|
2856
|
+
next = Automerge.change(next, (target) => {
|
|
2857
|
+
for (const [key, value] of Object.entries(patch.set ?? {})) {
|
|
2858
|
+
if (value !== void 0) {
|
|
2859
|
+
target[key] = hydrateMockAutomergeValues(value);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
for (const key of patch.unset ?? []) {
|
|
2863
|
+
delete target[key];
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
if (patch.json) {
|
|
2868
|
+
const json = patch.json;
|
|
2869
|
+
validateMockJsonPatch(json);
|
|
2870
|
+
next = changeMockAutomergeDocument(
|
|
2871
|
+
next,
|
|
2872
|
+
json.baseHeads,
|
|
2873
|
+
(target) => applyMockAutomergeJsonOperations(target, json)
|
|
2874
|
+
);
|
|
2875
|
+
}
|
|
2876
|
+
for (const textPatch of patch.text ?? []) {
|
|
2877
|
+
validateMockJsonPath(textPatch.path, "Text patch");
|
|
2878
|
+
next = changeMockAutomergeDocument(next, textPatch.baseHeads, (target) => {
|
|
2879
|
+
ensureMockAutomergeText(target, textPatch.path, "text patch");
|
|
2880
|
+
for (const edit of textPatch.edits) {
|
|
2881
|
+
spliceMockAutomergeText(target, textPatch.path, edit.index, edit.deleteCount, edit.insert ?? "");
|
|
2882
|
+
}
|
|
2883
|
+
});
|
|
2884
|
+
}
|
|
2885
|
+
for (const richTextPatch of patch.richText ?? []) {
|
|
2886
|
+
validateMockJsonPath(richTextPatch.path, "Rich-text patch");
|
|
2887
|
+
const spans = richTextPatch.spansSequence?.at(-1) ?? richTextPatch.spans ?? [];
|
|
2888
|
+
next = Automerge.change(next, (target) => {
|
|
2889
|
+
setMockAutomergeValue(target, richTextPatch.path, hydrateMockAutomergeValues(structuredClone(spans)));
|
|
2890
|
+
});
|
|
2891
|
+
}
|
|
2892
|
+
for (const stepPatch of patch.richTextSteps ?? []) {
|
|
2893
|
+
validateMockJsonPath(stepPatch.path, "Rich-text steps patch");
|
|
2894
|
+
next = changeMockAutomergeDocument(next, stepPatch.baseHeads, (target) => {
|
|
2895
|
+
const current = readMockAutomergeValue(target, stepPatch.path);
|
|
2896
|
+
if (current !== void 0 && current !== null && typeof current !== "string") {
|
|
2897
|
+
setMockAutomergeValue(
|
|
2898
|
+
target,
|
|
2899
|
+
stepPatch.path,
|
|
2900
|
+
materializeMockRichTextValue(projectMockAutomergeValue(current))
|
|
2901
|
+
);
|
|
2902
|
+
}
|
|
2903
|
+
ensureMockAutomergeText(target, stepPatch.path, "rich-text steps");
|
|
2904
|
+
for (const step of stepPatch.steps) {
|
|
2905
|
+
spliceMockAutomergeText(target, stepPatch.path, step.index, step.deleteCount, step.insert ?? "");
|
|
2906
|
+
}
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2909
|
+
return next;
|
|
2910
|
+
}
|
|
2911
|
+
function forkMockAutomergeDocument(doc, baseHeads, actor) {
|
|
2912
|
+
if (!baseHeads || baseHeads.length === 0) {
|
|
2913
|
+
return Automerge.clone(doc, { actor });
|
|
2914
|
+
}
|
|
2915
|
+
const heads = new Set(baseHeads);
|
|
2916
|
+
let ownLatest;
|
|
2917
|
+
for (const change2 of Automerge.getAllChanges(doc)) {
|
|
2918
|
+
const decoded = Automerge.decodeChange(change2);
|
|
2919
|
+
if (decoded.actor === actor) {
|
|
2920
|
+
ownLatest = decoded.hash;
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2923
|
+
if (ownLatest) {
|
|
2924
|
+
heads.add(ownLatest);
|
|
2925
|
+
}
|
|
2926
|
+
return Automerge.clone(Automerge.view(doc, [...heads]), { actor });
|
|
2927
|
+
}
|
|
2525
2928
|
function getFieldValue2(source, field) {
|
|
2526
2929
|
return field.split(".").reduce((current, part) => {
|
|
2527
2930
|
if (current && typeof current === "object" && part in current) {
|
|
@@ -3374,9 +3777,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3374
3777
|
let createCounter = 0;
|
|
3375
3778
|
let changeCounter = 0;
|
|
3376
3779
|
const defaultViewFactory = async () => createDefaultViewNavigator();
|
|
3377
|
-
const
|
|
3780
|
+
const automergeMode = definition.automerge === true;
|
|
3781
|
+
let changeSequence = 0;
|
|
3782
|
+
const changedAt = /* @__PURE__ */ new Map();
|
|
3783
|
+
const storedDocuments = new class extends Map {
|
|
3784
|
+
set(id, document2) {
|
|
3785
|
+
changedAt.set(id, ++changeSequence);
|
|
3786
|
+
return super.set(id, document2);
|
|
3787
|
+
}
|
|
3788
|
+
}();
|
|
3378
3789
|
for (const seed of definition.documents ?? []) {
|
|
3379
|
-
|
|
3790
|
+
const stored = {
|
|
3380
3791
|
id: seed.id,
|
|
3381
3792
|
data: structuredClone(seed.data),
|
|
3382
3793
|
heads: seed.heads ? [...seed.heads] : void 0,
|
|
@@ -3387,7 +3798,14 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3387
3798
|
decryptionKeyId: seed.decryptionKeyId,
|
|
3388
3799
|
authorLabel: seed.authorLabel,
|
|
3389
3800
|
createdAt: seed.createdAt
|
|
3390
|
-
}
|
|
3801
|
+
};
|
|
3802
|
+
if (automergeMode) {
|
|
3803
|
+
persistMockAutomergeDocument(
|
|
3804
|
+
stored,
|
|
3805
|
+
createMockAutomergeDocument(seed.data, MOCK_AUTOMERGE_SEED_ACTOR, 0)
|
|
3806
|
+
);
|
|
3807
|
+
}
|
|
3808
|
+
storedDocuments.set(seed.id, stored);
|
|
3391
3809
|
}
|
|
3392
3810
|
const liveQuerySubscriptions = /* @__PURE__ */ new Set();
|
|
3393
3811
|
const refreshLiveQueries = () => {
|
|
@@ -3430,6 +3848,15 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3430
3848
|
throw new Error("Recipient mutators require a document created with `recipients`");
|
|
3431
3849
|
}
|
|
3432
3850
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3851
|
+
if (automergeMode) {
|
|
3852
|
+
return writeAutomergeDocument(
|
|
3853
|
+
docId,
|
|
3854
|
+
existing,
|
|
3855
|
+
Automerge.change(loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR), (target) => {
|
|
3856
|
+
target._encryptFor = hydrateMockAutomergeValues(nextEncryptFor);
|
|
3857
|
+
})
|
|
3858
|
+
);
|
|
3859
|
+
}
|
|
3433
3860
|
const updated = {
|
|
3434
3861
|
id: docId,
|
|
3435
3862
|
data: { ...existing.data, _encryptFor: nextEncryptFor },
|
|
@@ -3448,6 +3875,28 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3448
3875
|
notifyLiveQueries();
|
|
3449
3876
|
return updated;
|
|
3450
3877
|
}
|
|
3878
|
+
function writeAutomergeDocument(docId, existing, automergeDoc) {
|
|
3879
|
+
const stored = {
|
|
3880
|
+
...existing ?? {},
|
|
3881
|
+
id: docId,
|
|
3882
|
+
data: {},
|
|
3883
|
+
attachments: existing?.attachments ? structuredClone(existing.attachments) : [],
|
|
3884
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3885
|
+
isDeleted: false,
|
|
3886
|
+
decryptionKeyId: existing?.decryptionKeyId ?? "default"
|
|
3887
|
+
};
|
|
3888
|
+
persistMockAutomergeDocument(stored, automergeDoc);
|
|
3889
|
+
storedDocuments.set(docId, stored);
|
|
3890
|
+
notifyLiveQueries();
|
|
3891
|
+
return {
|
|
3892
|
+
id: docId,
|
|
3893
|
+
data: structuredClone(stored.data),
|
|
3894
|
+
heads: [...stored.heads ?? []],
|
|
3895
|
+
attachments: structuredClone(stored.attachments ?? []),
|
|
3896
|
+
updatedAt: stored.updatedAt,
|
|
3897
|
+
...mockDecryptionKeyId(stored)
|
|
3898
|
+
};
|
|
3899
|
+
}
|
|
3451
3900
|
const defaultDocuments = {
|
|
3452
3901
|
async query(query) {
|
|
3453
3902
|
return runMockDocumentQuery(
|
|
@@ -3500,12 +3949,17 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3500
3949
|
const offset = decodeMockListCursor(query?.cursor) + skip;
|
|
3501
3950
|
const metadataOnly = query?.metadataOnly ?? false;
|
|
3502
3951
|
const idPrefix = query?.idPrefix;
|
|
3503
|
-
const
|
|
3952
|
+
const after = automergeMode ? decodeMockListCursor(query?.cursor) : 0;
|
|
3953
|
+
const items = Array.from(storedDocuments.values()).filter((document2) => !automergeMode || (changedAt.get(document2.id) ?? 0) > after).filter((document2) => !document2.inaccessible).filter(
|
|
3504
3954
|
(document2) => status === "all" ? true : status === "deleted" ? document2.isDeleted : !document2.isDeleted
|
|
3505
3955
|
).filter((document2) => matchesMockIdPrefix(document2.id, idPrefix)).filter(
|
|
3506
3956
|
(document2) => metadataOnly ? true : matchesDocumentFilter(document2, query?.filter)
|
|
3507
|
-
).sort(
|
|
3508
|
-
|
|
3957
|
+
).sort(
|
|
3958
|
+
(left, right) => automergeMode ? (changedAt.get(left.id) ?? 0) - (changedAt.get(right.id) ?? 0) : left.id.localeCompare(right.id)
|
|
3959
|
+
);
|
|
3960
|
+
const pageStart = automergeMode ? skip : offset;
|
|
3961
|
+
const pageDocuments = items.slice(pageStart, pageStart + limit);
|
|
3962
|
+
const page = pageDocuments.map((document2) => {
|
|
3509
3963
|
if (metadataOnly) {
|
|
3510
3964
|
return {
|
|
3511
3965
|
id: document2.id,
|
|
@@ -3526,6 +3980,13 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3526
3980
|
isDeleted: status !== "existing" ? document2.isDeleted : void 0
|
|
3527
3981
|
};
|
|
3528
3982
|
});
|
|
3983
|
+
if (automergeMode) {
|
|
3984
|
+
const last = pageDocuments[pageDocuments.length - 1];
|
|
3985
|
+
return {
|
|
3986
|
+
items: page,
|
|
3987
|
+
nextCursor: last ? String(changedAt.get(last.id) ?? after) : null
|
|
3988
|
+
};
|
|
3989
|
+
}
|
|
3529
3990
|
const nextCursor = offset + page.length < items.length ? String(offset + page.length) : null;
|
|
3530
3991
|
return {
|
|
3531
3992
|
items: page,
|
|
@@ -3534,7 +3995,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3534
3995
|
},
|
|
3535
3996
|
async getHeadCursor() {
|
|
3536
3997
|
return {
|
|
3537
|
-
cursor: String(storedDocuments.size)
|
|
3998
|
+
cursor: String(automergeMode ? changeSequence : storedDocuments.size)
|
|
3538
3999
|
};
|
|
3539
4000
|
},
|
|
3540
4001
|
async listInaccessible(query) {
|
|
@@ -3630,7 +4091,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3630
4091
|
const currentDoc = loadMockAutomergeDocument(document2);
|
|
3631
4092
|
const [mergedDoc] = Automerge.applyChanges(
|
|
3632
4093
|
currentDoc,
|
|
3633
|
-
patch.changes.map((
|
|
4094
|
+
patch.changes.map((change2) => new Uint8Array(change2))
|
|
3634
4095
|
);
|
|
3635
4096
|
persistMockAutomergeDocument(document2, mergedDoc);
|
|
3636
4097
|
document2.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3643,7 +4104,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3643
4104
|
);
|
|
3644
4105
|
changesSince = {
|
|
3645
4106
|
sinceHeads: [...patch.replicaHeads],
|
|
3646
|
-
changes: missingChanges.map((
|
|
4107
|
+
changes: missingChanges.map((change2) => cloneBytes(new Uint8Array(change2)))
|
|
3647
4108
|
};
|
|
3648
4109
|
}
|
|
3649
4110
|
return {
|
|
@@ -3680,7 +4141,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3680
4141
|
if (existing.isDeleted) {
|
|
3681
4142
|
existing.isDeleted = false;
|
|
3682
4143
|
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3683
|
-
|
|
4144
|
+
if (!automergeMode) {
|
|
4145
|
+
existing.heads = [`mock-head-${++changeCounter}`];
|
|
4146
|
+
}
|
|
4147
|
+
storedDocuments.set(callerId, existing);
|
|
3684
4148
|
notifyLiveQueries();
|
|
3685
4149
|
}
|
|
3686
4150
|
return {
|
|
@@ -3709,11 +4173,39 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3709
4173
|
"recipients: [] with includeSelf: false would produce a document nobody can read"
|
|
3710
4174
|
);
|
|
3711
4175
|
}
|
|
4176
|
+
const encryptFor = Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {};
|
|
4177
|
+
if (automergeMode) {
|
|
4178
|
+
const stored2 = {
|
|
4179
|
+
id,
|
|
4180
|
+
data: {},
|
|
4181
|
+
attachments: [],
|
|
4182
|
+
updatedAt: createdAt,
|
|
4183
|
+
isDeleted: false,
|
|
4184
|
+
decryptionKeyId: input.decryptionKeyId ?? "default"
|
|
4185
|
+
};
|
|
4186
|
+
persistMockAutomergeDocument(
|
|
4187
|
+
stored2,
|
|
4188
|
+
createMockAutomergeDocument(
|
|
4189
|
+
{ ...input.set ?? {}, ...encryptFor },
|
|
4190
|
+
MOCK_AUTOMERGE_LOCAL_ACTOR
|
|
4191
|
+
)
|
|
4192
|
+
);
|
|
4193
|
+
storedDocuments.set(id, stored2);
|
|
4194
|
+
notifyLiveQueries();
|
|
4195
|
+
return {
|
|
4196
|
+
id,
|
|
4197
|
+
data: structuredClone(stored2.data),
|
|
4198
|
+
heads: [...stored2.heads ?? []],
|
|
4199
|
+
attachments: [],
|
|
4200
|
+
updatedAt: createdAt,
|
|
4201
|
+
...mockDecryptionKeyId(stored2)
|
|
4202
|
+
};
|
|
4203
|
+
}
|
|
3712
4204
|
const created = {
|
|
3713
4205
|
id,
|
|
3714
4206
|
data: {
|
|
3715
4207
|
...readMockTypedValues(input.set ?? {}),
|
|
3716
|
-
...
|
|
4208
|
+
...encryptFor
|
|
3717
4209
|
},
|
|
3718
4210
|
heads: [`mock-head-${++changeCounter}`],
|
|
3719
4211
|
attachments: [],
|
|
@@ -3744,6 +4236,10 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3744
4236
|
async update(docId, patch) {
|
|
3745
4237
|
const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
3746
4238
|
const existing = storedDocuments.get(docId);
|
|
4239
|
+
if (automergeMode) {
|
|
4240
|
+
const current = existing?.automergeBinary ? loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR) : createMockAutomergeDocument(existing?.data ?? {}, MOCK_AUTOMERGE_LOCAL_ACTOR);
|
|
4241
|
+
return writeAutomergeDocument(docId, existing, applyMockAutomergeUpdate(current, patch));
|
|
4242
|
+
}
|
|
3747
4243
|
const updated = {
|
|
3748
4244
|
id: docId,
|
|
3749
4245
|
data: applyDocumentUpdatePatch(existing?.data ?? {}, patch),
|
|
@@ -3830,7 +4326,7 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
3830
4326
|
storedDocuments.set(_docId, {
|
|
3831
4327
|
...existing,
|
|
3832
4328
|
isDeleted: false,
|
|
3833
|
-
heads: [`mock-head-${++changeCounter}`],
|
|
4329
|
+
heads: automergeMode ? existing.heads : [`mock-head-${++changeCounter}`],
|
|
3834
4330
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3835
4331
|
});
|
|
3836
4332
|
notifyLiveQueries();
|
|
@@ -4054,7 +4550,26 @@ function createDatabaseHandle(definition, peers, directoryUsers = () => []) {
|
|
|
4054
4550
|
createdAt: document2.updatedAt ?? null
|
|
4055
4551
|
})),
|
|
4056
4552
|
documents: storedDocuments,
|
|
4057
|
-
notifyLiveQueries: refreshLiveQueries
|
|
4553
|
+
notifyLiveQueries: refreshLiveQueries,
|
|
4554
|
+
async applyRemoteUpdate(docId, input, options) {
|
|
4555
|
+
const existing = storedDocuments.get(docId);
|
|
4556
|
+
if (!existing || existing.isDeleted) {
|
|
4557
|
+
throw new Error(`Document ${docId} was not found.`);
|
|
4558
|
+
}
|
|
4559
|
+
if (!automergeMode || !existing.automergeBinary) {
|
|
4560
|
+
return await defaultDocuments.update(docId, input);
|
|
4561
|
+
}
|
|
4562
|
+
const current = loadMockAutomergeDocument(existing, MOCK_AUTOMERGE_LOCAL_ACTOR);
|
|
4563
|
+
const remote = applyMockAutomergeUpdate(
|
|
4564
|
+
forkMockAutomergeDocument(
|
|
4565
|
+
current,
|
|
4566
|
+
input.json?.baseHeads,
|
|
4567
|
+
options?.actor ?? MOCK_AUTOMERGE_REMOTE_ACTOR
|
|
4568
|
+
),
|
|
4569
|
+
input.json ? { ...input, json: { ...input.json, baseHeads: void 0 } } : input
|
|
4570
|
+
);
|
|
4571
|
+
return writeAutomergeDocument(docId, existing, Automerge.merge(current, remote));
|
|
4572
|
+
}
|
|
4058
4573
|
};
|
|
4059
4574
|
}
|
|
4060
4575
|
function createMockSessionState(options = {}) {
|
|
@@ -4080,6 +4595,7 @@ function createMockSessionState(options = {}) {
|
|
|
4080
4595
|
const databaseViewDocumentLists = /* @__PURE__ */ new Map();
|
|
4081
4596
|
const databaseDocumentStores = /* @__PURE__ */ new Map();
|
|
4082
4597
|
const databaseLiveQueryRefreshers = /* @__PURE__ */ new Map();
|
|
4598
|
+
const databaseRemoteUpdaters = /* @__PURE__ */ new Map();
|
|
4083
4599
|
const databasePeers = {
|
|
4084
4600
|
getDocuments: (databaseId) => databaseDocumentStores.get(databaseId),
|
|
4085
4601
|
refreshAllLiveQueries: () => {
|
|
@@ -4170,6 +4686,7 @@ function createMockSessionState(options = {}) {
|
|
|
4170
4686
|
databaseViewApis.clear();
|
|
4171
4687
|
databaseDocumentStores.clear();
|
|
4172
4688
|
databaseLiveQueryRefreshers.clear();
|
|
4689
|
+
databaseRemoteUpdaters.clear();
|
|
4173
4690
|
databaseInfos = definitions.map((definition) => ({
|
|
4174
4691
|
...definition.info,
|
|
4175
4692
|
capabilities: [...definition.info.capabilities]
|
|
@@ -4188,6 +4705,7 @@ function createMockSessionState(options = {}) {
|
|
|
4188
4705
|
databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
|
|
4189
4706
|
databaseDocumentStores.set(definition.info.id, created.documents);
|
|
4190
4707
|
databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
|
|
4708
|
+
databaseRemoteUpdaters.set(definition.info.id, created.applyRemoteUpdate);
|
|
4191
4709
|
databaseViewApis.set(definition.info.id, {
|
|
4192
4710
|
async create(input) {
|
|
4193
4711
|
const build = () => createEvaluatingViewNavigator(
|
|
@@ -4310,10 +4828,10 @@ function createMockSessionState(options = {}) {
|
|
|
4310
4828
|
`Unknown test database for view creation: ${firstDatabaseId}`
|
|
4311
4829
|
);
|
|
4312
4830
|
}
|
|
4313
|
-
const
|
|
4831
|
+
const view2 = await api.create(input);
|
|
4314
4832
|
const viewId = input.definition.id || crypto.randomUUID();
|
|
4315
|
-
sessionViews.set(viewId,
|
|
4316
|
-
return
|
|
4833
|
+
sessionViews.set(viewId, view2);
|
|
4834
|
+
return view2;
|
|
4317
4835
|
},
|
|
4318
4836
|
async openViewNavigator(viewId, options2) {
|
|
4319
4837
|
const existing = sessionViews.get(viewId);
|
|
@@ -4321,7 +4839,7 @@ function createMockSessionState(options = {}) {
|
|
|
4321
4839
|
return existing;
|
|
4322
4840
|
}
|
|
4323
4841
|
const sourceDatabaseId = launchContext.views.find(
|
|
4324
|
-
(
|
|
4842
|
+
(view3) => view3.id === viewId
|
|
4325
4843
|
)?.sources[0]?.databaseId;
|
|
4326
4844
|
if (!sourceDatabaseId) {
|
|
4327
4845
|
throw new Error(`Unknown test view: ${viewId}`);
|
|
@@ -4332,9 +4850,9 @@ function createMockSessionState(options = {}) {
|
|
|
4332
4850
|
`Unknown test database for view ${viewId}: ${sourceDatabaseId}`
|
|
4333
4851
|
);
|
|
4334
4852
|
}
|
|
4335
|
-
const
|
|
4336
|
-
sessionViews.set(viewId,
|
|
4337
|
-
return
|
|
4853
|
+
const view2 = await api.open(viewId, options2);
|
|
4854
|
+
sessionViews.set(viewId, view2);
|
|
4855
|
+
return view2;
|
|
4338
4856
|
},
|
|
4339
4857
|
menus,
|
|
4340
4858
|
drag,
|
|
@@ -4471,6 +4989,13 @@ function createMockSessionState(options = {}) {
|
|
|
4471
4989
|
}
|
|
4472
4990
|
return database;
|
|
4473
4991
|
},
|
|
4992
|
+
async applyRemoteUpdate(databaseId, docId, input, updateOptions) {
|
|
4993
|
+
const applyRemoteUpdate = databaseRemoteUpdaters.get(databaseId);
|
|
4994
|
+
if (!applyRemoteUpdate) {
|
|
4995
|
+
throw new Error(`Unknown test database: ${databaseId}`);
|
|
4996
|
+
}
|
|
4997
|
+
return await applyRemoteUpdate(docId, input, updateOptions);
|
|
4998
|
+
},
|
|
4474
4999
|
getLicensedProducts() {
|
|
4475
5000
|
return [...launchContext.licensedProducts ?? []];
|
|
4476
5001
|
},
|
|
@@ -4587,6 +5112,7 @@ function createMockMindooDBAppSession(options = {}) {
|
|
|
4587
5112
|
setCapabilities: state.setCapabilities,
|
|
4588
5113
|
setEnforceCapabilities: state.setEnforceCapabilities,
|
|
4589
5114
|
setDirectoryUsers: state.setDirectoryUsers,
|
|
5115
|
+
applyRemoteUpdate: state.applyRemoteUpdate,
|
|
4590
5116
|
emitThemeChange: state.emitThemeChange,
|
|
4591
5117
|
emitViewportChange: state.emitViewportChange,
|
|
4592
5118
|
emitUiPreferencesChange: state.emitUiPreferencesChange,
|
|
@@ -5529,6 +6055,7 @@ function createFakeBridgeHost(options = {}) {
|
|
|
5529
6055
|
getEnforceCapabilities: state.getEnforceCapabilities,
|
|
5530
6056
|
setDirectoryUsers: state.setDirectoryUsers,
|
|
5531
6057
|
getDirectoryUsers: state.getDirectoryUsers,
|
|
6058
|
+
applyRemoteUpdate: state.applyRemoteUpdate,
|
|
5532
6059
|
listEmbeds: () => state.embedHost.listEmbeds(),
|
|
5533
6060
|
closeEmbed: (embedId, reason, result) => state.embedHost.closeEmbed(embedId, reason, result),
|
|
5534
6061
|
getEmbeddingFinish: () => state.embedHost.getEmbeddingFinish(),
|