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.
- package/README.md +38 -8
- package/TESTING.md +59 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +54 -1
- package/dist/testing/index.d.ts +37 -2
- package/dist/testing/index.js +642 -13
- package/dist/{types-Bl6CrBn1.d.ts → types-Plb92iSn.d.ts} +105 -4
- package/package.json +10 -6
package/dist/testing/index.js
CHANGED
|
@@ -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
|
|
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]) =>
|
|
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(
|
|
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(
|
|
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 ?
|
|
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
|
-
|
|
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
|
-
|
|
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: {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
};
|