mindoodb-app-sdk 0.0.34 → 0.0.37

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.
@@ -1,9 +1,470 @@
1
1
  // src/testing/index.ts
2
2
  import * as Automerge from "@automerge/automerge";
3
3
  import {
4
- evaluateExpression,
4
+ evaluateExpression as evaluateExpression2,
5
5
  parseMindooDBFormulaBooleanExpression
6
6
  } from "mindoodb-view-language";
7
+
8
+ // src/testing/evaluatingViewNavigator.ts
9
+ import { evaluateExpression, getFieldValue } from "mindoodb-view-language";
10
+ var mindoodbModulePromise = null;
11
+ async function loadMindoodbForTesting() {
12
+ if (!mindoodbModulePromise) {
13
+ mindoodbModulePromise = import("mindoodb").then((mod) => mod).catch(() => null);
14
+ }
15
+ return mindoodbModulePromise;
16
+ }
17
+ function toFormulaBoolean(value) {
18
+ if (typeof value === "boolean") return value;
19
+ if (typeof value === "number") return value !== 0;
20
+ if (typeof value === "string") {
21
+ const normalized = value.trim().toLowerCase();
22
+ return normalized !== "" && normalized !== "false" && normalized !== "0" && normalized !== "no";
23
+ }
24
+ return Boolean(value);
25
+ }
26
+ function matchesFilter(mindoodb, definition, doc, origin, createdAt, lastModifiedAt, witnessed, awaitingWitness) {
27
+ void mindoodb;
28
+ if (!definition.filter) return true;
29
+ return toFormulaBoolean(
30
+ evaluateExpression(definition.filter.expression, {
31
+ doc,
32
+ values: doc,
33
+ origin,
34
+ createdAt,
35
+ lastModifiedAt,
36
+ witnessed,
37
+ awaitingWitness,
38
+ variables: {}
39
+ })
40
+ );
41
+ }
42
+ function computeColumnValues(definition, doc, origin, createdAt, lastModifiedAt, witnessed, awaitingWitness) {
43
+ const values = {};
44
+ for (const [key, value] of Object.entries(doc)) {
45
+ if (!key.startsWith("_")) values[key] = value;
46
+ }
47
+ for (const column of definition.columns) {
48
+ const expression = column.expression;
49
+ const value = expression.kind === "field" ? getFieldValue(doc, expression.path) ?? getFieldValue(values, expression.path) : evaluateExpression(expression, {
50
+ doc,
51
+ values,
52
+ origin,
53
+ createdAt,
54
+ lastModifiedAt,
55
+ witnessed,
56
+ awaitingWitness,
57
+ variables: {}
58
+ });
59
+ values[column.name] = value;
60
+ }
61
+ return values;
62
+ }
63
+ function mapSorting(mindoodb, direction) {
64
+ switch (direction) {
65
+ case "ascending":
66
+ return mindoodb.ColumnSorting.ASCENDING;
67
+ case "descending":
68
+ return mindoodb.ColumnSorting.DESCENDING;
69
+ default:
70
+ return mindoodb.ColumnSorting.NONE;
71
+ }
72
+ }
73
+ function mapTotalMode(mindoodb, mode) {
74
+ switch (mode) {
75
+ case "sum":
76
+ return mindoodb.TotalMode.SUM;
77
+ case "average":
78
+ return mindoodb.TotalMode.AVERAGE;
79
+ default:
80
+ return mindoodb.TotalMode.NONE;
81
+ }
82
+ }
83
+ function getCategoryPath(entry) {
84
+ const path = [];
85
+ let current = entry.isCategory() ? entry : entry.getParent();
86
+ while (current && !current.isRoot()) {
87
+ if (current.isCategory()) path.unshift(current.getCategoryValue());
88
+ current = current.getParent();
89
+ }
90
+ return path;
91
+ }
92
+ function serializeEntry(navigator, entry) {
93
+ const parent = entry.getParent();
94
+ return {
95
+ key: entry.getPositionStr(),
96
+ kind: entry.isCategory() ? "category" : "document",
97
+ origin: entry.origin,
98
+ docId: entry.docId,
99
+ level: entry.getLevel(),
100
+ parentKey: parent && !parent.isRoot() ? parent.getPositionStr() : null,
101
+ categoryPath: getCategoryPath(entry),
102
+ categoryValue: entry.isCategory() ? entry.getCategoryValue() : null,
103
+ columnValues: entry.getColumnValues(),
104
+ childCount: entry.getChildCount(),
105
+ descendantDocumentCount: entry.getDescendantDocumentCount(),
106
+ descendantCount: entry.getDescendantCount(),
107
+ descendantCategoryCount: entry.getDescendantCategoryCount(),
108
+ siblingCount: entry.getSiblingCount(),
109
+ childCategoryCount: entry.getChildCategoryCount(),
110
+ childDocumentCount: entry.getChildDocumentCount(),
111
+ position: entry.getPositionStr(),
112
+ expanded: entry.isCategory() ? navigator.isExpanded(entry) : false,
113
+ selected: navigator.isSelected(entry.origin, entry.docId),
114
+ isVisible: true
115
+ };
116
+ }
117
+ async function buildVirtualView(mindoodb, definition, documents, categorizationStyle) {
118
+ const builder = mindoodb.VirtualViewFactory.createView().withCategorizationStyle(
119
+ categorizationStyle === "category_then_document" ? mindoodb.CategorizationStyle.CATEGORY_THEN_DOCUMENT : mindoodb.CategorizationStyle.DOCUMENT_THEN_CATEGORY
120
+ );
121
+ for (const column of definition.columns) {
122
+ const commonOptions = {
123
+ title: column.title,
124
+ isHidden: column.hidden,
125
+ expression: column.expression
126
+ };
127
+ switch (column.role) {
128
+ case "category":
129
+ builder.addCategoryColumn(column.name, {
130
+ ...commonOptions,
131
+ sorting: mapSorting(
132
+ mindoodb,
133
+ column.sorting === "none" ? "ascending" : column.sorting
134
+ )
135
+ });
136
+ break;
137
+ case "sort":
138
+ builder.addSortedColumn(
139
+ column.name,
140
+ mapSorting(
141
+ mindoodb,
142
+ !column.sorting || column.sorting === "none" ? "ascending" : column.sorting
143
+ ),
144
+ commonOptions
145
+ );
146
+ break;
147
+ case "display":
148
+ if (column.sorting && column.sorting !== "none") {
149
+ builder.addSortedColumn(column.name, mapSorting(mindoodb, column.sorting), commonOptions);
150
+ } else {
151
+ builder.addDisplayColumn(column.name, commonOptions);
152
+ }
153
+ break;
154
+ case "total":
155
+ builder.addTotalColumn(
156
+ column.name,
157
+ mapTotalMode(mindoodb, column.totalMode === "none" ? "sum" : column.totalMode),
158
+ commonOptions
159
+ );
160
+ break;
161
+ }
162
+ }
163
+ const view = await builder.build();
164
+ const changes = /* @__PURE__ */ new Map();
165
+ for (const document of documents) {
166
+ if (!matchesFilter(
167
+ mindoodb,
168
+ definition,
169
+ document.data,
170
+ document.origin,
171
+ document.createdAt,
172
+ document.lastModifiedAt,
173
+ document.witnessed,
174
+ document.awaitingWitness
175
+ )) {
176
+ continue;
177
+ }
178
+ const change = changes.get(document.origin) ?? new mindoodb.VirtualViewDataChange(document.origin);
179
+ change.addEntry(
180
+ document.docId,
181
+ computeColumnValues(
182
+ definition,
183
+ document.data,
184
+ document.origin,
185
+ document.createdAt,
186
+ document.lastModifiedAt,
187
+ document.witnessed,
188
+ document.awaitingWitness
189
+ )
190
+ );
191
+ changes.set(document.origin, change);
192
+ }
193
+ for (const change of changes.values()) {
194
+ if (change.hasChanges()) view.applyChanges(change);
195
+ }
196
+ view.getRoot();
197
+ return view;
198
+ }
199
+ function createCoreNavigator(mindoodb, view, options) {
200
+ const builder = mindoodb.VirtualViewFactory.createNavigator(view);
201
+ const includeCategories = options?.includeCategories ?? true;
202
+ const includeDocuments = options?.includeDocuments ?? true;
203
+ if (!includeCategories && includeDocuments) builder.documentsOnly();
204
+ else if (includeCategories && !includeDocuments) builder.categoriesOnly();
205
+ if (options?.hideEmptyCategories) builder.hideEmptyCategories();
206
+ if (Array.isArray(options?.rootCategoryPath) && options.rootCategoryPath.length > 0) {
207
+ builder.fromCategory(options.rootCategoryPath.map((part) => String(part)).join("\\"));
208
+ } else if (options?.rootEntryKey) {
209
+ const temp = mindoodb.VirtualViewFactory.createSimpleNavigator(view);
210
+ const rootEntry = temp.getPos(options.rootEntryKey);
211
+ if (rootEntry) builder.fromEntry(rootEntry);
212
+ }
213
+ return builder.build();
214
+ }
215
+ function normalizePageLimit(limit) {
216
+ return Math.max(1, Math.min(1e3, Math.trunc(limit ?? 100)));
217
+ }
218
+ function pageEntries(navigator, direction, options = {}) {
219
+ const entries = [];
220
+ const startOk = options.startPosition ? navigator.gotoPos(options.startPosition) : direction === "forward" ? navigator.gotoFirst() : navigator.gotoLast();
221
+ if (!startOk) {
222
+ return { entries, nextPosition: null, hasMore: false };
223
+ }
224
+ const moveNext = () => {
225
+ if (direction === "forward") {
226
+ return options.selectedOnly ? navigator.gotoNextSelected() : navigator.gotoNext();
227
+ }
228
+ return options.selectedOnly ? navigator.gotoPrevSelected() : navigator.gotoPrev();
229
+ };
230
+ if (options.selectedOnly) {
231
+ const current = navigator.getCurrentEntry();
232
+ if (!current || !navigator.isSelected(current.origin, current.docId)) {
233
+ if (!moveNext()) return { entries, nextPosition: null, hasMore: false };
234
+ }
235
+ }
236
+ const limit = normalizePageLimit(options.limit);
237
+ let nextPosition = null;
238
+ let hasMore = false;
239
+ while (true) {
240
+ const current = navigator.getCurrentEntry();
241
+ if (!current) break;
242
+ entries.push(serializeEntry(navigator, current));
243
+ if (entries.length >= limit) {
244
+ hasMore = moveNext();
245
+ nextPosition = hasMore ? navigator.getCurrentEntry()?.getPositionStr() ?? null : null;
246
+ break;
247
+ }
248
+ if (!moveNext()) break;
249
+ }
250
+ return { entries, nextPosition, hasMore };
251
+ }
252
+ function resolveEntry(navigator, entryKey) {
253
+ const entry = navigator.getPos(entryKey);
254
+ if (!entry) {
255
+ throw new Error(`Unknown view entry: ${entryKey}`);
256
+ }
257
+ return entry;
258
+ }
259
+ function wrapNavigator(definition, navigator) {
260
+ const updateListeners = /* @__PURE__ */ new Set();
261
+ let viewCursor = null;
262
+ return {
263
+ async getDefinition() {
264
+ return definition;
265
+ },
266
+ async getViewCursor() {
267
+ return viewCursor;
268
+ },
269
+ async refresh() {
270
+ return viewCursor;
271
+ },
272
+ onDidUpdate(listener) {
273
+ updateListeners.add(listener);
274
+ return () => updateListeners.delete(listener);
275
+ },
276
+ async getCurrentEntry() {
277
+ const entry = navigator.getCurrentEntry();
278
+ return entry ? serializeEntry(navigator, entry) : null;
279
+ },
280
+ async gotoFirst() {
281
+ return navigator.gotoFirst();
282
+ },
283
+ async gotoLast() {
284
+ return navigator.gotoLast();
285
+ },
286
+ async gotoNext() {
287
+ return navigator.gotoNext();
288
+ },
289
+ async gotoPrev() {
290
+ return navigator.gotoPrev();
291
+ },
292
+ async gotoNextSibling() {
293
+ return navigator.gotoNextSibling();
294
+ },
295
+ async gotoPrevSibling() {
296
+ return navigator.gotoPrevSibling();
297
+ },
298
+ async gotoParent() {
299
+ return navigator.gotoParent();
300
+ },
301
+ async gotoFirstChild() {
302
+ return navigator.gotoFirstChild();
303
+ },
304
+ async gotoLastChild() {
305
+ return navigator.gotoLastChild();
306
+ },
307
+ async gotoPos(position) {
308
+ return navigator.gotoPos(position);
309
+ },
310
+ async getPos(position) {
311
+ const entry = navigator.getPos(position);
312
+ return entry ? serializeEntry(navigator, entry) : null;
313
+ },
314
+ async findCategoryEntryByParts(parts) {
315
+ const entry = navigator.findCategoryEntryByParts(parts);
316
+ return entry ? serializeEntry(navigator, entry) : null;
317
+ },
318
+ async entriesForward(options) {
319
+ return pageEntries(navigator, "forward", options);
320
+ },
321
+ async entriesBackward(options) {
322
+ return pageEntries(navigator, "backward", options);
323
+ },
324
+ async gotoNextSelected() {
325
+ return navigator.gotoNextSelected();
326
+ },
327
+ async gotoPrevSelected() {
328
+ return navigator.gotoPrevSelected();
329
+ },
330
+ async select(origin, docId, selectParentCategories = false) {
331
+ navigator.select(origin, docId, selectParentCategories);
332
+ },
333
+ async deselect(origin, docId) {
334
+ navigator.deselect(origin, docId);
335
+ },
336
+ async selectAllEntries() {
337
+ navigator.selectAllEntries();
338
+ },
339
+ async deselectAllEntries() {
340
+ navigator.deselectAllEntries();
341
+ },
342
+ async isSelected(origin, docId) {
343
+ return navigator.isSelected(origin, docId);
344
+ },
345
+ async getSelectionState() {
346
+ return {
347
+ selectAllByDefault: navigator.isSelectAllByDefault(),
348
+ entryKeys: Array.from(navigator.getSelectedOrDeselectedEntries())
349
+ };
350
+ },
351
+ async setSelectionState(state) {
352
+ if (state.selectAllByDefault) navigator.selectAllEntries();
353
+ else navigator.deselectAllEntries();
354
+ navigator.setSelectedOrDeselectedEntries(new Set(state.entryKeys));
355
+ },
356
+ async expand(origin, docId) {
357
+ navigator.expand(origin, docId);
358
+ },
359
+ async collapse(origin, docId) {
360
+ navigator.collapse(origin, docId);
361
+ },
362
+ async expandAll() {
363
+ navigator.expandAll();
364
+ },
365
+ async collapseAll() {
366
+ navigator.collapseAll();
367
+ },
368
+ async expandToLevel(level) {
369
+ navigator.expandToLevel(level);
370
+ },
371
+ async isExpanded(entryKey) {
372
+ const entry = navigator.getPos(entryKey);
373
+ return entry ? navigator.isExpanded(entry) : false;
374
+ },
375
+ async getExpansionState() {
376
+ return {
377
+ expandAllByDefault: navigator.isExpandAllByDefault(),
378
+ expandLevel: navigator.getExpandLevel(),
379
+ entryKeys: Array.from(navigator.getExpandedOrCollapsedEntries())
380
+ };
381
+ },
382
+ async setExpansionState(state) {
383
+ if (state.expandAllByDefault) navigator.expandAll();
384
+ else navigator.collapseAll();
385
+ navigator.expandToLevel(state.expandLevel);
386
+ navigator.setExpandedOrCollapsedEntries(new Set(state.entryKeys));
387
+ },
388
+ async childEntries(entryKey, descending) {
389
+ return navigator.childEntries(resolveEntry(navigator, entryKey), descending).map((entry) => serializeEntry(navigator, entry));
390
+ },
391
+ async childCategories(entryKey, descending) {
392
+ return navigator.childCategories(resolveEntry(navigator, entryKey), descending).map((entry) => serializeEntry(navigator, entry));
393
+ },
394
+ async childDocuments(entryKey, descending) {
395
+ return navigator.childDocuments(resolveEntry(navigator, entryKey), descending).map((entry) => serializeEntry(navigator, entry));
396
+ },
397
+ async childCategoriesByKey(entryKey, key, exact, descending) {
398
+ return navigator.childCategoriesByKey(
399
+ resolveEntry(navigator, entryKey),
400
+ String(key ?? ""),
401
+ Boolean(exact),
402
+ descending
403
+ ).map((entry) => serializeEntry(navigator, entry));
404
+ },
405
+ async childDocumentsByKey(entryKey, key, exact, descending) {
406
+ return navigator.childDocumentsByKey(
407
+ resolveEntry(navigator, entryKey),
408
+ String(key ?? ""),
409
+ Boolean(exact),
410
+ descending
411
+ ).map((entry) => serializeEntry(navigator, entry));
412
+ },
413
+ async childCategoriesBetween(entryKey, range) {
414
+ return navigator.childCategoriesBetween(
415
+ resolveEntry(navigator, entryKey),
416
+ range.startKey,
417
+ range.endKey,
418
+ range.descending
419
+ ).map((entry) => serializeEntry(navigator, entry));
420
+ },
421
+ async childDocumentsBetween(entryKey, range) {
422
+ return navigator.childDocumentsBetween(
423
+ resolveEntry(navigator, entryKey),
424
+ range.startKey,
425
+ range.endKey,
426
+ range.descending
427
+ ).map((entry) => serializeEntry(navigator, entry));
428
+ },
429
+ async getSortedDocIds(descending) {
430
+ const page = pageEntries(navigator, descending ? "backward" : "forward", { limit: 1e3 });
431
+ return collectSortedDocIds(page.entries);
432
+ },
433
+ async getSortedDocIdsScoped(entryKey, descending) {
434
+ const docs = navigator.childDocuments(resolveEntry(navigator, entryKey), descending);
435
+ return docs.map((entry) => ({ origin: entry.origin, docId: entry.docId }));
436
+ },
437
+ async dispose() {
438
+ updateListeners.clear();
439
+ }
440
+ };
441
+ }
442
+ function collectSortedDocIds(entries) {
443
+ const results = [];
444
+ const seen = /* @__PURE__ */ new Set();
445
+ for (const entry of entries) {
446
+ if (entry.kind !== "document" || !entry.docId) continue;
447
+ const key = `${entry.origin}:${entry.docId}`;
448
+ if (seen.has(key)) continue;
449
+ seen.add(key);
450
+ results.push({ origin: entry.origin, docId: entry.docId });
451
+ }
452
+ return results;
453
+ }
454
+ async function createEvaluatingViewNavigator(input, documents) {
455
+ const mindoodb = await loadMindoodbForTesting();
456
+ if (!mindoodb) return null;
457
+ const view = await buildVirtualView(
458
+ mindoodb,
459
+ input.definition,
460
+ documents,
461
+ input.categorizationStyle
462
+ );
463
+ const navigator = createCoreNavigator(mindoodb, view, input.options);
464
+ return wrapNavigator(input.definition, navigator);
465
+ }
466
+
467
+ // src/testing/index.ts
7
468
  var PROTOCOL = "mindoodb-app-bridge";
8
469
  var MOCK_SIGNING_PUBLIC_KEY_PEM = "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAmockmockmockmockmockmockmockmockmockmockmoc=\n-----END PUBLIC KEY-----\n";
9
470
  function createBridgeErrorPayload(error, fallbackCode = "bridge-error") {
@@ -378,7 +839,7 @@ function persistMockAutomergeDocument(document, automergeDoc) {
378
839
  document.data = structuredClone(automergeDoc);
379
840
  document.heads = Automerge.getHeads(automergeDoc);
380
841
  }
381
- function getFieldValue(source, field) {
842
+ function getFieldValue2(source, field) {
382
843
  return field.split(".").reduce((current, part) => {
383
844
  if (current && typeof current === "object" && part in current) {
384
845
  return current[part];
@@ -391,7 +852,7 @@ function matchesDocumentFilter(document, filter) {
391
852
  return true;
392
853
  }
393
854
  return Object.entries(filter).every(
394
- ([field, expected]) => getFieldValue(document.data, field) === expected
855
+ ([field, expected]) => getFieldValue2(document.data, field) === expected
395
856
  );
396
857
  }
397
858
  function matchesMockIdPrefix(docId, idPrefix) {
@@ -454,7 +915,7 @@ function computeMockTextScore(document, clause) {
454
915
  if (terms.length === 0) {
455
916
  return void 0;
456
917
  }
457
- const haystack = clause.fields ? clause.fields.map((field) => collectMockText(getFieldValue(document.data, field))).join(" ") : collectMockText(
918
+ const haystack = clause.fields ? clause.fields.map((field) => collectMockText(getFieldValue2(document.data, field))).join(" ") : collectMockText(
458
919
  Object.fromEntries(
459
920
  Object.entries(document.data).filter(([key]) => !key.startsWith("_"))
460
921
  )
@@ -497,14 +958,14 @@ function runMockDocumentQuery(storedDocuments, databaseId, query) {
497
958
  origin: databaseId,
498
959
  variables: {}
499
960
  };
500
- if (filter && !mockExpressionToBoolean(evaluateExpression(filter, context))) {
961
+ if (filter && !mockExpressionToBoolean(evaluateExpression2(filter, context))) {
501
962
  continue;
502
963
  }
503
964
  const sortValues = sortBy.map((key) => {
504
965
  if (key.special === "textScore") {
505
966
  return textScore ?? 0;
506
967
  }
507
- return key.expression ? evaluateExpression(key.expression, context) : getFieldValue(document.data, key.field ?? "");
968
+ return key.expression ? evaluateExpression2(key.expression, context) : getFieldValue2(document.data, key.field ?? "");
508
969
  });
509
970
  matches.push({ document, sortValues, textScore });
510
971
  }
@@ -531,7 +992,7 @@ function runMockDocumentQuery(storedDocuments, databaseId, query) {
531
992
  fields: query?.fields ? Object.fromEntries(
532
993
  query.fields.map((field) => [
533
994
  field,
534
- getFieldValue(document.data, field)
995
+ getFieldValue2(document.data, field)
535
996
  ])
536
997
  ) : structuredClone(document.data),
537
998
  lastModified: document.updatedAt ? Date.parse(document.updatedAt) || 0 : 0,
@@ -760,6 +1221,16 @@ function createDatabaseHandle(definition) {
760
1221
  let changeCounter = 0;
761
1222
  const defaultViewFactory = async () => createDefaultViewNavigator();
762
1223
  const storedDocuments = /* @__PURE__ */ new Map();
1224
+ for (const seed of definition.documents ?? []) {
1225
+ storedDocuments.set(seed.id, {
1226
+ id: seed.id,
1227
+ data: structuredClone(seed.data),
1228
+ heads: seed.heads ? [...seed.heads] : void 0,
1229
+ attachments: seed.attachments ? structuredClone(seed.attachments) : [],
1230
+ updatedAt: seed.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
1231
+ isDeleted: seed.isDeleted ?? false
1232
+ });
1233
+ }
763
1234
  const liveQuerySubscriptions = /* @__PURE__ */ new Set();
764
1235
  const notifyLiveQueries = () => {
765
1236
  for (const subscription of liveQuerySubscriptions) {
@@ -775,6 +1246,43 @@ function createDatabaseHandle(definition) {
775
1246
  }
776
1247
  }
777
1248
  };
1249
+ function mockEncryptForMap(names) {
1250
+ const encryptFor = {};
1251
+ for (const name of names) {
1252
+ const trimmed = name.trim();
1253
+ if (trimmed) {
1254
+ encryptFor[trimmed] = { kind: "user" };
1255
+ }
1256
+ }
1257
+ return encryptFor;
1258
+ }
1259
+ function persistEncryptFor(docId, nextEncryptFor) {
1260
+ const existing = storedDocuments.get(docId);
1261
+ if (!existing || existing.isDeleted) {
1262
+ throw new Error(`Recipient mutators require an existing document (${docId})`);
1263
+ }
1264
+ const raw = existing.data._encryptFor;
1265
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1266
+ throw new Error("Recipient mutators require a document created with `recipients`");
1267
+ }
1268
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1269
+ const updated = {
1270
+ id: docId,
1271
+ data: { ...existing.data, _encryptFor: nextEncryptFor },
1272
+ heads: [`mock-head-${++changeCounter}`],
1273
+ attachments: existing.attachments ? structuredClone(existing.attachments) : [],
1274
+ updatedAt
1275
+ };
1276
+ storedDocuments.set(docId, {
1277
+ ...updated,
1278
+ data: structuredClone(updated.data),
1279
+ attachments: updated.attachments ? structuredClone(updated.attachments) : [],
1280
+ isDeleted: false,
1281
+ automergeBinary: void 0
1282
+ });
1283
+ notifyLiveQueries();
1284
+ return updated;
1285
+ }
778
1286
  const defaultDocuments = {
779
1287
  async query(query) {
780
1288
  return runMockDocumentQuery(storedDocuments, definition.info.id, query);
@@ -832,7 +1340,7 @@ function createDatabaseHandle(definition) {
832
1340
  const projectedData = query?.fields ? Object.fromEntries(
833
1341
  query.fields.map((field) => [
834
1342
  field,
835
- getFieldValue(document.data, field)
1343
+ getFieldValue2(document.data, field)
836
1344
  ])
837
1345
  ) : document.data;
838
1346
  return {
@@ -962,9 +1470,17 @@ function createDatabaseHandle(definition) {
962
1470
  id = `doc-${createCounter}`;
963
1471
  }
964
1472
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
1473
+ if (Array.isArray(input.recipients) && input.recipientOptions?.includeSelf === false && input.recipients.every((name) => name.trim().length === 0)) {
1474
+ throw new Error(
1475
+ "recipients: [] with includeSelf: false would produce a document nobody can read"
1476
+ );
1477
+ }
965
1478
  const created = {
966
1479
  id,
967
- data: { ...input.set },
1480
+ data: {
1481
+ ...input.set,
1482
+ ...Array.isArray(input.recipients) ? { _encryptFor: mockEncryptForMap(input.recipients) } : {}
1483
+ },
968
1484
  heads: [`mock-head-${++changeCounter}`],
969
1485
  attachments: [],
970
1486
  updatedAt: createdAt
@@ -1008,6 +1524,49 @@ function createDatabaseHandle(definition) {
1008
1524
  notifyLiveQueries();
1009
1525
  return updated;
1010
1526
  },
1527
+ async addRecipients(docId, recipients) {
1528
+ const existing = storedDocuments.get(docId);
1529
+ const current = existing?.data._encryptFor && typeof existing.data._encryptFor === "object" && !Array.isArray(existing.data._encryptFor) ? { ...existing.data._encryptFor } : null;
1530
+ if (!current) {
1531
+ throw new Error("Recipient mutators require a document created with `recipients`");
1532
+ }
1533
+ for (const name of recipients) {
1534
+ const trimmed = name.trim();
1535
+ if (!trimmed) {
1536
+ continue;
1537
+ }
1538
+ const key = Object.keys(current).find(
1539
+ (entry) => entry.trim().toLowerCase() === trimmed.toLowerCase()
1540
+ );
1541
+ if (key) {
1542
+ delete current[key].removedAt;
1543
+ } else {
1544
+ current[trimmed] = { kind: "user" };
1545
+ }
1546
+ }
1547
+ return persistEncryptFor(docId, current);
1548
+ },
1549
+ async removeRecipients(docId, recipients) {
1550
+ const existing = storedDocuments.get(docId);
1551
+ const current = existing?.data._encryptFor && typeof existing.data._encryptFor === "object" && !Array.isArray(existing.data._encryptFor) ? { ...existing.data._encryptFor } : {};
1552
+ const drop = new Set(
1553
+ recipients.map((name) => name.trim().toLowerCase()).filter(Boolean)
1554
+ );
1555
+ for (const key of Object.keys(current)) {
1556
+ if (drop.has(key.trim().toLowerCase())) {
1557
+ delete current[key];
1558
+ }
1559
+ }
1560
+ return persistEncryptFor(docId, current);
1561
+ },
1562
+ async setRecipients(docId, recipients, options) {
1563
+ if (options?.includeSelf === false && recipients.every((name) => name.trim().length === 0)) {
1564
+ throw new Error(
1565
+ "recipients: [] with includeSelf: false would produce a document nobody can read"
1566
+ );
1567
+ }
1568
+ return persistEncryptFor(docId, mockEncryptForMap(recipients));
1569
+ },
1011
1570
  async delete(_docId) {
1012
1571
  const existing = storedDocuments.get(_docId);
1013
1572
  if (existing) {
@@ -1054,6 +1613,9 @@ function createDatabaseHandle(definition) {
1054
1613
  async getDefaultCreateKeyId() {
1055
1614
  return "default";
1056
1615
  },
1616
+ async listCreateKeys() {
1617
+ return [{ keyId: "default", isDefault: true }];
1618
+ },
1057
1619
  async listHistory(_docId) {
1058
1620
  return [];
1059
1621
  },
@@ -1139,6 +1701,9 @@ function createDatabaseHandle(definition) {
1139
1701
  fingerprint: "0".repeat(64),
1140
1702
  status: "unknown"
1141
1703
  }));
1704
+ },
1705
+ async listUsers() {
1706
+ return [];
1142
1707
  }
1143
1708
  };
1144
1709
  const defaultViews = {
@@ -1181,12 +1746,22 @@ function createDatabaseHandle(definition) {
1181
1746
  },
1182
1747
  async openPreview(_docId, _attachmentName, _options) {
1183
1748
  return { ok: true };
1749
+ },
1750
+ async extractText(input) {
1751
+ const format = input.format ?? "markdown";
1752
+ const text = new TextDecoder("utf-8", { fatal: false }).decode(input.bytes);
1753
+ return {
1754
+ text,
1755
+ handled: true,
1756
+ format,
1757
+ engine: "mock-utf-8"
1758
+ };
1184
1759
  }
1185
1760
  };
1186
1761
  const methods = definition.methods ?? {};
1187
1762
  let fulltextSetup = definition.fulltextSetup ?? null;
1188
1763
  let extractionSetup = definition.extractionSetup ?? null;
1189
- return {
1764
+ const databaseHandle = {
1190
1765
  async info() {
1191
1766
  return {
1192
1767
  ...definition.info,
@@ -1226,6 +1801,15 @@ function createDatabaseHandle(definition) {
1226
1801
  extractionSetup = config === null ? null : { ...config };
1227
1802
  }
1228
1803
  };
1804
+ return {
1805
+ handle: databaseHandle,
1806
+ listViewDocuments: () => Array.from(storedDocuments.values()).filter((document) => !document.isDeleted).map((document) => ({
1807
+ origin: definition.info.id,
1808
+ docId: document.id,
1809
+ data: structuredClone(document.data),
1810
+ createdAt: document.updatedAt ?? null
1811
+ }))
1812
+ };
1229
1813
  }
1230
1814
  function createMockSessionState(options = {}) {
1231
1815
  let launchContext = createDefaultLaunchContext(options.launchContext);
@@ -1234,6 +1818,7 @@ function createMockSessionState(options = {}) {
1234
1818
  const uiPreferencesListeners = /* @__PURE__ */ new Set();
1235
1819
  const localeListeners = /* @__PURE__ */ new Set();
1236
1820
  const databaseHandles = /* @__PURE__ */ new Map();
1821
+ const databaseViewDocumentLists = /* @__PURE__ */ new Map();
1237
1822
  const databaseViewApis = /* @__PURE__ */ new Map();
1238
1823
  const sessionViews = /* @__PURE__ */ new Map();
1239
1824
  let activeMenuResolve = null;
@@ -1260,15 +1845,27 @@ function createMockSessionState(options = {}) {
1260
1845
  };
1261
1846
  const setDatabases = (definitions) => {
1262
1847
  databaseHandles.clear();
1848
+ databaseViewDocumentLists.clear();
1263
1849
  databaseViewApis.clear();
1264
1850
  databaseInfos = definitions.map((definition) => ({
1265
1851
  ...definition.info,
1266
1852
  capabilities: [...definition.info.capabilities]
1267
1853
  }));
1268
1854
  for (const definition of definitions) {
1269
- databaseHandles.set(definition.info.id, createDatabaseHandle(definition));
1855
+ const created = createDatabaseHandle(definition);
1856
+ databaseHandles.set(definition.info.id, created.handle);
1857
+ databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
1270
1858
  databaseViewApis.set(definition.info.id, {
1271
- async create(_input) {
1859
+ async create(input) {
1860
+ const documents = input.databaseIds.flatMap((databaseId) => {
1861
+ const list = databaseViewDocumentLists.get(databaseId);
1862
+ return list ? list() : [];
1863
+ });
1864
+ const evaluating = await createEvaluatingViewNavigator(input, documents);
1865
+ if (evaluating) return evaluating;
1866
+ console.warn(
1867
+ "[mindoodb-app-sdk/testing] mindoodb peer not available; using empty view navigator. Install mindoodb as a devDependency for evaluating VirtualViews."
1868
+ );
1272
1869
  return await createDefaultViewNavigator();
1273
1870
  },
1274
1871
  async open(_viewId) {
@@ -1734,6 +2331,22 @@ function createFakeBridgeHost(options = {}) {
1734
2331
  String(params.docId),
1735
2332
  params.patch
1736
2333
  );
2334
+ case "documents.addRecipients":
2335
+ return await state.getDatabase(String(params.databaseId)).documents.addRecipients(
2336
+ String(params.docId),
2337
+ Array.isArray(params.recipients) ? params.recipients.map(String) : []
2338
+ );
2339
+ case "documents.removeRecipients":
2340
+ return await state.getDatabase(String(params.databaseId)).documents.removeRecipients(
2341
+ String(params.docId),
2342
+ Array.isArray(params.recipients) ? params.recipients.map(String) : []
2343
+ );
2344
+ case "documents.setRecipients":
2345
+ return await state.getDatabase(String(params.databaseId)).documents.setRecipients(
2346
+ String(params.docId),
2347
+ Array.isArray(params.recipients) ? params.recipients.map(String) : [],
2348
+ params.recipientOptions && typeof params.recipientOptions === "object" && !Array.isArray(params.recipientOptions) ? params.recipientOptions : void 0
2349
+ );
1737
2350
  case "documents.delete":
1738
2351
  return await state.getDatabase(String(params.databaseId)).documents.delete(String(params.docId));
1739
2352
  case "documents.undelete":
@@ -1750,6 +2363,8 @@ function createFakeBridgeHost(options = {}) {
1750
2363
  return await state.getDatabase(String(params.databaseId)).documents.canUndelete(String(params.docId));
1751
2364
  case "documents.getDefaultCreateKeyId":
1752
2365
  return await state.getDatabase(String(params.databaseId)).documents.getDefaultCreateKeyId();
2366
+ case "documents.listCreateKeys":
2367
+ return await state.getDatabase(String(params.databaseId)).documents.listCreateKeys();
1753
2368
  case "documents.history.list":
1754
2369
  return await state.getDatabase(String(params.databaseId)).documents.listHistory(String(params.docId));
1755
2370
  case "documents.history.getAtTimestamp":
@@ -1789,6 +2404,8 @@ function createFakeBridgeHost(options = {}) {
1789
2404
  return await state.getDatabase(String(params.databaseId)).directory.excerpt(
1790
2405
  Array.isArray(params.publicKeys) ? params.publicKeys.map(String) : []
1791
2406
  );
2407
+ case "directory.listUsers":
2408
+ return await state.getDatabase(String(params.databaseId)).directory.listUsers();
1792
2409
  case "attachments.list":
1793
2410
  return await state.getDatabase(String(params.databaseId)).attachments.list(
1794
2411
  String(params.docId),
@@ -1831,6 +2448,16 @@ function createFakeBridgeHost(options = {}) {
1831
2448
  String(params.attachmentName),
1832
2449
  typeof params.revisionId === "string" ? { revisionId: params.revisionId } : typeof params.timestamp === "number" ? { timestamp: params.timestamp } : void 0
1833
2450
  );
2451
+ case "attachments.extractText":
2452
+ return await state.getDatabase(String(params.databaseId)).attachments.extractText({
2453
+ bytes: params.bytes instanceof Uint8Array ? params.bytes : new Uint8Array(
2454
+ params.bytes instanceof ArrayBuffer ? params.bytes : Array.isArray(params.bytes) ? params.bytes : []
2455
+ ),
2456
+ mimeType: typeof params.mimeType === "string" ? params.mimeType : "",
2457
+ fileName: typeof params.fileName === "string" ? params.fileName : void 0,
2458
+ format: params.format === "plainText" || params.format === "markdown" ? params.format : void 0,
2459
+ languages: Array.isArray(params.languages) ? params.languages.filter((entry) => typeof entry === "string") : void 0
2460
+ });
1834
2461
  case "attachments.preparePreviewSession":
1835
2462
  return await state.getDatabase(String(params.databaseId)).attachments.preparePreviewSession(
1836
2463
  String(params.docId),
@@ -2180,7 +2807,9 @@ function createFakeBridgeHost(options = {}) {
2180
2807
  return controller;
2181
2808
  }
2182
2809
  export {
2810
+ createEvaluatingViewNavigator,
2183
2811
  createFakeBridgeHost,
2184
2812
  createMockMindooDBAppBridge,
2185
- createMockMindooDBAppSession
2813
+ createMockMindooDBAppSession,
2814
+ loadMindoodbForTesting
2186
2815
  };