mindoodb-app-sdk 0.0.46 → 0.0.48
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 +40 -3
- package/TESTING.md +35 -0
- package/dist/{chunk-XPPTUWW6.js → chunk-TPYDERAK.js} +25 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +268 -2
- package/dist/testing/index.d.ts +10 -1
- package/dist/testing/index.js +395 -42
- package/dist/{types-BaXvRKXY.d.ts → types-C-eyC839.d.ts} +274 -7
- package/package.json +4 -4
package/dist/testing/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
releaseMindooDBAppBridgeSessions
|
|
3
|
-
} from "../chunk-
|
|
3
|
+
} from "../chunk-TPYDERAK.js";
|
|
4
4
|
|
|
5
5
|
// src/testing/index.ts
|
|
6
6
|
import * as Automerge from "@automerge/automerge";
|
|
7
7
|
import {
|
|
8
8
|
evaluateExpression as evaluateExpression2,
|
|
9
|
+
formatMindooDBFormulaExpression,
|
|
9
10
|
parseMindooDBFormulaBooleanExpression
|
|
10
11
|
} from "mindoodb-view-language";
|
|
11
12
|
|
|
@@ -27,14 +28,16 @@ function toFormulaBoolean(value) {
|
|
|
27
28
|
}
|
|
28
29
|
return Boolean(value);
|
|
29
30
|
}
|
|
30
|
-
function matchesFilter(mindoodb, definition,
|
|
31
|
+
function matchesFilter(mindoodb, definition, document) {
|
|
31
32
|
void mindoodb;
|
|
32
33
|
if (!definition.filter) return true;
|
|
34
|
+
const { data: doc, origin, docId, createdAt, lastModifiedAt, witnessed, awaitingWitness } = document;
|
|
33
35
|
return toFormulaBoolean(
|
|
34
36
|
evaluateExpression(definition.filter.expression, {
|
|
35
37
|
doc,
|
|
36
38
|
values: doc,
|
|
37
39
|
origin,
|
|
40
|
+
docId,
|
|
38
41
|
createdAt,
|
|
39
42
|
lastModifiedAt,
|
|
40
43
|
witnessed,
|
|
@@ -43,7 +46,8 @@ function matchesFilter(mindoodb, definition, doc, origin, createdAt, lastModifie
|
|
|
43
46
|
})
|
|
44
47
|
);
|
|
45
48
|
}
|
|
46
|
-
function computeColumnValues(definition,
|
|
49
|
+
function computeColumnValues(definition, document) {
|
|
50
|
+
const { data: doc, origin, docId, createdAt, lastModifiedAt, witnessed, awaitingWitness } = document;
|
|
47
51
|
const values = {};
|
|
48
52
|
for (const [key, value] of Object.entries(doc)) {
|
|
49
53
|
if (!key.startsWith("_")) values[key] = value;
|
|
@@ -54,6 +58,7 @@ function computeColumnValues(definition, doc, origin, createdAt, lastModifiedAt,
|
|
|
54
58
|
doc,
|
|
55
59
|
values,
|
|
56
60
|
origin,
|
|
61
|
+
docId,
|
|
57
62
|
createdAt,
|
|
58
63
|
lastModifiedAt,
|
|
59
64
|
witnessed,
|
|
@@ -167,31 +172,11 @@ async function buildVirtualView(mindoodb, definition, documents, categorizationS
|
|
|
167
172
|
const view = await builder.build();
|
|
168
173
|
const changes = /* @__PURE__ */ new Map();
|
|
169
174
|
for (const document of documents) {
|
|
170
|
-
if (!matchesFilter(
|
|
171
|
-
mindoodb,
|
|
172
|
-
definition,
|
|
173
|
-
document.data,
|
|
174
|
-
document.origin,
|
|
175
|
-
document.createdAt,
|
|
176
|
-
document.lastModifiedAt,
|
|
177
|
-
document.witnessed,
|
|
178
|
-
document.awaitingWitness
|
|
179
|
-
)) {
|
|
175
|
+
if (!matchesFilter(mindoodb, definition, document)) {
|
|
180
176
|
continue;
|
|
181
177
|
}
|
|
182
178
|
const change = changes.get(document.origin) ?? new mindoodb.VirtualViewDataChange(document.origin);
|
|
183
|
-
change.addEntry(
|
|
184
|
-
document.docId,
|
|
185
|
-
computeColumnValues(
|
|
186
|
-
definition,
|
|
187
|
-
document.data,
|
|
188
|
-
document.origin,
|
|
189
|
-
document.createdAt,
|
|
190
|
-
document.lastModifiedAt,
|
|
191
|
-
document.witnessed,
|
|
192
|
-
document.awaitingWitness
|
|
193
|
-
)
|
|
194
|
-
);
|
|
179
|
+
change.addEntry(document.docId, computeColumnValues(definition, document));
|
|
195
180
|
changes.set(document.origin, change);
|
|
196
181
|
}
|
|
197
182
|
for (const change of changes.values()) {
|
|
@@ -679,6 +664,7 @@ function mergeLaunchContext(current, patch) {
|
|
|
679
664
|
viewport: current.viewport ? { ...current.viewport } : null,
|
|
680
665
|
uiPreferences: { ...current.uiPreferences },
|
|
681
666
|
user: { ...current.user },
|
|
667
|
+
appPermissions: [...current.appPermissions ?? []],
|
|
682
668
|
launchParameters: { ...current.launchParameters },
|
|
683
669
|
databases: current.databases.map((database) => ({
|
|
684
670
|
...database,
|
|
@@ -712,6 +698,7 @@ function mergeLaunchContext(current, patch) {
|
|
|
712
698
|
viewport: patch.viewport === void 0 ? current.viewport ? { ...current.viewport } : null : patch.viewport ? { ...patch.viewport } : null,
|
|
713
699
|
uiPreferences: patch.uiPreferences ? { ...current.uiPreferences, ...patch.uiPreferences } : { ...current.uiPreferences },
|
|
714
700
|
user: patch.user ? { ...current.user, ...patch.user } : { ...current.user },
|
|
701
|
+
appPermissions: [...patch.appPermissions ?? current.appPermissions ?? []],
|
|
715
702
|
launchParameters: patch.launchParameters ? { ...current.launchParameters, ...patch.launchParameters } : { ...current.launchParameters },
|
|
716
703
|
databases: patch.databases ? patch.databases.map((database) => ({
|
|
717
704
|
...database,
|
|
@@ -785,6 +772,7 @@ function createDefaultLaunchContext(patch) {
|
|
|
785
772
|
username: "Test User"
|
|
786
773
|
},
|
|
787
774
|
licensedProducts: [],
|
|
775
|
+
appPermissions: [],
|
|
788
776
|
launchParameters: {},
|
|
789
777
|
databases: [],
|
|
790
778
|
views: []
|
|
@@ -948,7 +936,7 @@ function computeMockTextScore(document, clause) {
|
|
|
948
936
|
const matches = combineWith === "AND" ? matchedTerms === terms.length : matchedTerms > 0;
|
|
949
937
|
return matches ? score : void 0;
|
|
950
938
|
}
|
|
951
|
-
function runMockDocumentQuery(storedDocuments, databaseId, query) {
|
|
939
|
+
function runMockDocumentQuery(storedDocuments, databaseId, query, resolveDatabase) {
|
|
952
940
|
const filter = typeof query?.filter === "string" ? parseMindooDBFormulaBooleanExpression(query.filter) : query?.filter;
|
|
953
941
|
const sortBy = query?.sortBy && query.sortBy.length > 0 ? query.sortBy : query?.text ? [{ special: "textScore", direction: "descending" }] : [];
|
|
954
942
|
const matches = [];
|
|
@@ -967,6 +955,7 @@ function runMockDocumentQuery(storedDocuments, databaseId, query) {
|
|
|
967
955
|
doc: document.data,
|
|
968
956
|
values: {},
|
|
969
957
|
origin: databaseId,
|
|
958
|
+
docId: document.id,
|
|
970
959
|
variables: {}
|
|
971
960
|
};
|
|
972
961
|
if (filter && !mockExpressionToBoolean(evaluateExpression2(filter, context))) {
|
|
@@ -998,19 +987,343 @@ function runMockDocumentQuery(storedDocuments, databaseId, query) {
|
|
|
998
987
|
}
|
|
999
988
|
const offset = Math.max(0, query?.offset ?? 0);
|
|
1000
989
|
const limit = Math.max(1, query?.limit ?? 200);
|
|
1001
|
-
const
|
|
990
|
+
const page = matches.slice(offset, offset + limit);
|
|
991
|
+
const rows = page.map(({ document, textScore }) => ({
|
|
1002
992
|
docId: document.id,
|
|
1003
|
-
fields: query?.fields
|
|
1004
|
-
|
|
1005
|
-
field,
|
|
1006
|
-
getFieldValue2(document.data, field)
|
|
1007
|
-
])
|
|
1008
|
-
) : structuredClone(document.data),
|
|
1009
|
-
lastModified: document.updatedAt ? Date.parse(document.updatedAt) || 0 : 0,
|
|
993
|
+
fields: projectMockFields(document.data, query?.fields),
|
|
994
|
+
lastModified: mockLastModified(document),
|
|
1010
995
|
...textScore === void 0 ? {} : { textScore }
|
|
1011
996
|
}));
|
|
997
|
+
if (query?.include) {
|
|
998
|
+
hydrateMockIncludes({
|
|
999
|
+
includes: query.include,
|
|
1000
|
+
parents: page.map(({ document }, index) => ({
|
|
1001
|
+
row: rows[index],
|
|
1002
|
+
docId: document.id,
|
|
1003
|
+
data: document.data
|
|
1004
|
+
})),
|
|
1005
|
+
parentDatabaseId: databaseId,
|
|
1006
|
+
parentDocuments: storedDocuments,
|
|
1007
|
+
resolveDatabase,
|
|
1008
|
+
depth: 1,
|
|
1009
|
+
slotPath: ""
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
1012
|
return { rows, total: matches.length, coverage: "full" };
|
|
1013
1013
|
}
|
|
1014
|
+
function projectMockFields(data, fields) {
|
|
1015
|
+
return fields ? Object.fromEntries(fields.map((field) => [field, getFieldValue2(data, field)])) : structuredClone(data);
|
|
1016
|
+
}
|
|
1017
|
+
function mockLastModified(document) {
|
|
1018
|
+
return document.updatedAt ? Date.parse(document.updatedAt) || 0 : 0;
|
|
1019
|
+
}
|
|
1020
|
+
var MOCK_MAX_INCLUDE_DEPTH = 3;
|
|
1021
|
+
var MOCK_DEFAULT_INCLUDE_LIMIT = 200;
|
|
1022
|
+
function mockIncludeFilter(include) {
|
|
1023
|
+
return typeof include.filter === "string" ? parseMindooDBFormulaBooleanExpression(include.filter) : include.filter;
|
|
1024
|
+
}
|
|
1025
|
+
function flattenMockConjuncts(expression) {
|
|
1026
|
+
return expression.kind === "operation" && expression.op === "and" ? expression.args.flatMap((arg) => flattenMockConjuncts(arg)) : [expression];
|
|
1027
|
+
}
|
|
1028
|
+
function mockReferencesParent(expression) {
|
|
1029
|
+
if (expression.kind === "parent") {
|
|
1030
|
+
return true;
|
|
1031
|
+
}
|
|
1032
|
+
if (expression.kind === "operation") {
|
|
1033
|
+
return expression.op === "parentDocId" || expression.args.some((arg) => mockReferencesParent(arg));
|
|
1034
|
+
}
|
|
1035
|
+
if (expression.kind === "if") {
|
|
1036
|
+
return [expression.condition, expression.whenTrue, expression.whenFalse].some(
|
|
1037
|
+
(part) => mockReferencesParent(part)
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
if (expression.kind === "let") {
|
|
1041
|
+
return Object.values(expression.bindings).some((value) => mockReferencesParent(value)) || mockReferencesParent(expression.result);
|
|
1042
|
+
}
|
|
1043
|
+
return false;
|
|
1044
|
+
}
|
|
1045
|
+
function mockChildJoinSide(expression) {
|
|
1046
|
+
if (expression.kind === "field") {
|
|
1047
|
+
return { kind: "field", path: expression.path };
|
|
1048
|
+
}
|
|
1049
|
+
if (expression.kind === "operation" && expression.op === "docId") {
|
|
1050
|
+
return { kind: "docId" };
|
|
1051
|
+
}
|
|
1052
|
+
return null;
|
|
1053
|
+
}
|
|
1054
|
+
function mockParentJoinSide(expression) {
|
|
1055
|
+
if (expression.kind === "parent") {
|
|
1056
|
+
return { kind: "field", path: expression.path };
|
|
1057
|
+
}
|
|
1058
|
+
if (expression.kind === "operation" && expression.op === "parentDocId") {
|
|
1059
|
+
return { kind: "docId" };
|
|
1060
|
+
}
|
|
1061
|
+
return null;
|
|
1062
|
+
}
|
|
1063
|
+
function combineMockConjuncts(conjuncts) {
|
|
1064
|
+
if (conjuncts.length === 0) {
|
|
1065
|
+
return null;
|
|
1066
|
+
}
|
|
1067
|
+
if (conjuncts.length === 1) {
|
|
1068
|
+
return conjuncts[0];
|
|
1069
|
+
}
|
|
1070
|
+
return {
|
|
1071
|
+
kind: "operation",
|
|
1072
|
+
op: "and",
|
|
1073
|
+
args: conjuncts
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
function planMockInclude(label, include, filter) {
|
|
1077
|
+
if (include.localKey !== void 0) {
|
|
1078
|
+
if (filter && mockReferencesParent(filter)) {
|
|
1079
|
+
throw new Error(
|
|
1080
|
+
`Include "${label}" combines localKey with a filter that also references the parent document. Use either localKey or a parent join condition, not both.`
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
return {
|
|
1084
|
+
child: { kind: "docId" },
|
|
1085
|
+
parent: { kind: "field", path: include.localKey },
|
|
1086
|
+
residualFilter: filter ?? null
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
const conjuncts = flattenMockConjuncts(filter);
|
|
1090
|
+
const parentConjuncts = conjuncts.filter((conjunct) => mockReferencesParent(conjunct));
|
|
1091
|
+
const parentFree = conjuncts.filter((conjunct) => !mockReferencesParent(conjunct));
|
|
1092
|
+
if (parentConjuncts.length === 0) {
|
|
1093
|
+
throw new Error(
|
|
1094
|
+
`Include "${label}" cannot be joined because it does not reference the parent document. Add the join condition v.eq(v.field("<child field>"), v.parentDocId()) (or use localKey).`
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
if (parentConjuncts.length > 1) {
|
|
1098
|
+
throw new Error(
|
|
1099
|
+
`Include "${label}" cannot be joined because it references the parent document in ${parentConjuncts.length} separate conditions. Exactly one parent equality can be used as the join key.`
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
const join = parentConjuncts[0];
|
|
1103
|
+
if (join.kind !== "operation" || join.op !== "eq" || join.args.length !== 2) {
|
|
1104
|
+
throw new Error(
|
|
1105
|
+
`Include "${label}" cannot be joined because the condition referencing the parent is not an equality: ${formatMindooDBFormulaExpression(join)}. Only v.eq(v.field("<child field>"), v.parentDocId()) and friends can be used as the join key.`
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
const [left, right] = join.args;
|
|
1109
|
+
const sides = mockJoinSides(left, right) ?? mockJoinSides(right, left) ?? null;
|
|
1110
|
+
if (!sides) {
|
|
1111
|
+
throw new Error(
|
|
1112
|
+
`Include "${label}" cannot be joined because the join equality compares ${formatMindooDBFormulaExpression(join)}. One side must read the related document (v.field(...) or v.docId()), the other the parent (v.parent(...) or v.parentDocId()).`
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
return { ...sides, residualFilter: combineMockConjuncts(parentFree) };
|
|
1116
|
+
}
|
|
1117
|
+
function mockJoinSides(childCandidate, parentCandidate) {
|
|
1118
|
+
const child = mockChildJoinSide(childCandidate);
|
|
1119
|
+
const parent = mockParentJoinSide(parentCandidate);
|
|
1120
|
+
return child && parent ? { child, parent } : null;
|
|
1121
|
+
}
|
|
1122
|
+
function mockJoinValues(value) {
|
|
1123
|
+
if (value === void 0 || value === null) {
|
|
1124
|
+
return [];
|
|
1125
|
+
}
|
|
1126
|
+
return (Array.isArray(value) ? value : [value]).filter(
|
|
1127
|
+
(item) => item !== void 0 && item !== null
|
|
1128
|
+
);
|
|
1129
|
+
}
|
|
1130
|
+
function validateMockInclude(slot, label, include, depth) {
|
|
1131
|
+
if (slot.trim() === "") {
|
|
1132
|
+
throw new Error("Include slot names must not be empty.");
|
|
1133
|
+
}
|
|
1134
|
+
if (depth > MOCK_MAX_INCLUDE_DEPTH) {
|
|
1135
|
+
throw new Error(
|
|
1136
|
+
`Include "${label}" nests ${depth} levels deep, more than the maximum of ${MOCK_MAX_INCLUDE_DEPTH}.`
|
|
1137
|
+
);
|
|
1138
|
+
}
|
|
1139
|
+
if (include.cardinality !== "one" && include.cardinality !== "many") {
|
|
1140
|
+
throw new Error(
|
|
1141
|
+
`Include "${label}" needs an explicit cardinality of "one" or "many" (got ${JSON.stringify(include.cardinality)}).`
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
if (include.localKey === void 0 && include.filter === void 0) {
|
|
1145
|
+
throw new Error(
|
|
1146
|
+
`Include "${label}" needs a localKey or a filter to relate documents to the parent row.`
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
if (include.localKey !== void 0 && include.localKey.trim() === "") {
|
|
1150
|
+
throw new Error(`Include "${label}" has an empty localKey.`);
|
|
1151
|
+
}
|
|
1152
|
+
if (include.cardinality === "one") {
|
|
1153
|
+
if (include.sortBy !== void 0) {
|
|
1154
|
+
throw new Error(
|
|
1155
|
+
`Include "${label}" has cardinality "one", which resolves to a single row \u2014 sortBy is meaningless there.`
|
|
1156
|
+
);
|
|
1157
|
+
}
|
|
1158
|
+
if (include.limit !== void 0) {
|
|
1159
|
+
throw new Error(
|
|
1160
|
+
`Include "${label}" has cardinality "one", which resolves to a single row \u2014 limit is meaningless there.`
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
if (include.limit !== void 0 && (!Number.isFinite(include.limit) || include.limit < 0)) {
|
|
1165
|
+
throw new Error(`Include "${label}" has an invalid limit (${include.limit}).`);
|
|
1166
|
+
}
|
|
1167
|
+
for (const sortKey of include.sortBy ?? []) {
|
|
1168
|
+
if (sortKey.special === "textScore") {
|
|
1169
|
+
throw new Error(
|
|
1170
|
+
`Include "${label}" sorts by textScore, but includes have no text clause to score against.`
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function hydrateMockIncludes(params) {
|
|
1176
|
+
const { includes, parents, parentDocuments, resolveDatabase, depth, slotPath } = params;
|
|
1177
|
+
for (const [slot, include] of Object.entries(includes)) {
|
|
1178
|
+
const label = slotPath ? `${slotPath}.${slot}` : slot;
|
|
1179
|
+
validateMockInclude(slot, label, include, depth);
|
|
1180
|
+
const filter = mockIncludeFilter(include);
|
|
1181
|
+
const plan = planMockInclude(label, include, filter);
|
|
1182
|
+
const childDatabaseId = include.databaseId ?? params.parentDatabaseId;
|
|
1183
|
+
let childDocuments = parentDocuments;
|
|
1184
|
+
if (include.databaseId !== void 0) {
|
|
1185
|
+
const resolved = resolveDatabase?.(include.databaseId);
|
|
1186
|
+
if (!resolved) {
|
|
1187
|
+
throw new Error(
|
|
1188
|
+
`Include "${label}" refers to the database "${include.databaseId}", which this app is not mapped to.`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
childDocuments = resolved;
|
|
1192
|
+
}
|
|
1193
|
+
if (parents.length === 0) {
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
const parentsByKey = /* @__PURE__ */ new Map();
|
|
1197
|
+
for (const parent of parents) {
|
|
1198
|
+
const parentValue = plan.parent.kind === "docId" ? parent.docId : getFieldValue2(parent.data, plan.parent.path);
|
|
1199
|
+
for (const key of mockJoinValues(parentValue)) {
|
|
1200
|
+
const bucket = parentsByKey.get(key);
|
|
1201
|
+
if (bucket) {
|
|
1202
|
+
bucket.push(parent);
|
|
1203
|
+
} else {
|
|
1204
|
+
parentsByKey.set(key, [parent]);
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const sortBy = include.sortBy ?? [];
|
|
1209
|
+
const childrenByKey = /* @__PURE__ */ new Map();
|
|
1210
|
+
if (parentsByKey.size > 0) {
|
|
1211
|
+
for (const document of childDocuments.values()) {
|
|
1212
|
+
if (document.isDeleted || document.inaccessible) {
|
|
1213
|
+
continue;
|
|
1214
|
+
}
|
|
1215
|
+
const childValue = plan.child.kind === "docId" ? document.id : getFieldValue2(document.data, plan.child.path);
|
|
1216
|
+
const matchedKeys = mockJoinValues(childValue).filter((key) => parentsByKey.has(key));
|
|
1217
|
+
if (matchedKeys.length === 0) {
|
|
1218
|
+
continue;
|
|
1219
|
+
}
|
|
1220
|
+
const context = {
|
|
1221
|
+
doc: document.data,
|
|
1222
|
+
values: {},
|
|
1223
|
+
origin: childDatabaseId,
|
|
1224
|
+
docId: document.id,
|
|
1225
|
+
variables: {}
|
|
1226
|
+
};
|
|
1227
|
+
if (plan.residualFilter && !mockExpressionToBoolean(evaluateExpression2(plan.residualFilter, context))) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const candidate = {
|
|
1231
|
+
docId: document.id,
|
|
1232
|
+
data: document.data,
|
|
1233
|
+
row: {
|
|
1234
|
+
docId: document.id,
|
|
1235
|
+
fields: projectMockFields(document.data, include.fields),
|
|
1236
|
+
lastModified: mockLastModified(document)
|
|
1237
|
+
},
|
|
1238
|
+
sortValues: sortBy.map(
|
|
1239
|
+
(key) => key.expression ? evaluateExpression2(key.expression, context) : getFieldValue2(document.data, key.field ?? "")
|
|
1240
|
+
)
|
|
1241
|
+
};
|
|
1242
|
+
for (const key of matchedKeys) {
|
|
1243
|
+
const bucket = childrenByKey.get(key);
|
|
1244
|
+
if (bucket) {
|
|
1245
|
+
bucket.push(candidate);
|
|
1246
|
+
} else {
|
|
1247
|
+
childrenByKey.set(key, [candidate]);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
const childParents = attachMockIncludeSlot(
|
|
1253
|
+
slot,
|
|
1254
|
+
label,
|
|
1255
|
+
include,
|
|
1256
|
+
plan,
|
|
1257
|
+
parents,
|
|
1258
|
+
childrenByKey,
|
|
1259
|
+
sortBy
|
|
1260
|
+
);
|
|
1261
|
+
if (include.include && childParents.length > 0) {
|
|
1262
|
+
hydrateMockIncludes({
|
|
1263
|
+
includes: include.include,
|
|
1264
|
+
parents: childParents,
|
|
1265
|
+
parentDatabaseId: childDatabaseId,
|
|
1266
|
+
parentDocuments: childDocuments,
|
|
1267
|
+
resolveDatabase,
|
|
1268
|
+
depth: depth + 1,
|
|
1269
|
+
slotPath: label
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
function attachMockIncludeSlot(slot, label, include, plan, parents, childrenByKey, sortBy) {
|
|
1275
|
+
const limit = include.limit ?? MOCK_DEFAULT_INCLUDE_LIMIT;
|
|
1276
|
+
const nextParents = [];
|
|
1277
|
+
for (const parent of parents) {
|
|
1278
|
+
const parentValue = plan.parent.kind === "docId" ? parent.docId : getFieldValue2(parent.data, plan.parent.path);
|
|
1279
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1280
|
+
const matches = [];
|
|
1281
|
+
for (const key of mockJoinValues(parentValue)) {
|
|
1282
|
+
for (const candidate of childrenByKey.get(key) ?? []) {
|
|
1283
|
+
if (seen.has(candidate.docId)) {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
seen.add(candidate.docId);
|
|
1287
|
+
matches.push(candidate);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
const slots = parent.row.includes ??= {};
|
|
1291
|
+
if (include.cardinality === "one") {
|
|
1292
|
+
if (matches.length > 1) {
|
|
1293
|
+
const sample = matches.slice(0, 3).map((match2) => match2.docId).join(", ");
|
|
1294
|
+
throw new Error(
|
|
1295
|
+
`Include "${label}" has cardinality "one", but document "${parent.docId}" matched ${matches.length} related documents (${sample}${matches.length > 3 ? ", \u2026" : ""}). Use cardinality "many" if several matches are expected.`
|
|
1296
|
+
);
|
|
1297
|
+
}
|
|
1298
|
+
if (matches.length === 0) {
|
|
1299
|
+
slots[slot] = null;
|
|
1300
|
+
continue;
|
|
1301
|
+
}
|
|
1302
|
+
const match = matches[0];
|
|
1303
|
+
const row = structuredClone(match.row);
|
|
1304
|
+
slots[slot] = row;
|
|
1305
|
+
nextParents.push({ row, docId: match.docId, data: match.data });
|
|
1306
|
+
continue;
|
|
1307
|
+
}
|
|
1308
|
+
matches.sort((left, right) => {
|
|
1309
|
+
for (let i = 0; i < sortBy.length; i++) {
|
|
1310
|
+
const result = compareMockQueryValues(left.sortValues[i], right.sortValues[i]);
|
|
1311
|
+
if (result !== 0) {
|
|
1312
|
+
return sortBy[i].direction === "descending" ? -result : result;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
return left.docId.localeCompare(right.docId);
|
|
1316
|
+
});
|
|
1317
|
+
const rows = [];
|
|
1318
|
+
for (const candidate of matches.slice(0, limit)) {
|
|
1319
|
+
const row = structuredClone(candidate.row);
|
|
1320
|
+
rows.push(row);
|
|
1321
|
+
nextParents.push({ row, docId: candidate.docId, data: candidate.data });
|
|
1322
|
+
}
|
|
1323
|
+
slots[slot] = rows;
|
|
1324
|
+
}
|
|
1325
|
+
return nextParents;
|
|
1326
|
+
}
|
|
1014
1327
|
function createDefaultViewNavigator() {
|
|
1015
1328
|
const definition = {
|
|
1016
1329
|
title: "Mock View",
|
|
@@ -1227,7 +1540,7 @@ async function mockSha256Hex(value) {
|
|
|
1227
1540
|
const digest = await subtle.digest("SHA-256", new TextEncoder().encode(value));
|
|
1228
1541
|
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1229
1542
|
}
|
|
1230
|
-
function createDatabaseHandle(definition) {
|
|
1543
|
+
function createDatabaseHandle(definition, peers) {
|
|
1231
1544
|
let createCounter = 0;
|
|
1232
1545
|
let changeCounter = 0;
|
|
1233
1546
|
const defaultViewFactory = async () => createDefaultViewNavigator();
|
|
@@ -1247,12 +1560,13 @@ function createDatabaseHandle(definition) {
|
|
|
1247
1560
|
});
|
|
1248
1561
|
}
|
|
1249
1562
|
const liveQuerySubscriptions = /* @__PURE__ */ new Set();
|
|
1250
|
-
const
|
|
1563
|
+
const refreshLiveQueries = () => {
|
|
1251
1564
|
for (const subscription of liveQuerySubscriptions) {
|
|
1252
1565
|
const result = runMockDocumentQuery(
|
|
1253
1566
|
storedDocuments,
|
|
1254
1567
|
definition.info.id,
|
|
1255
|
-
subscription.query
|
|
1568
|
+
subscription.query,
|
|
1569
|
+
peers.getDocuments
|
|
1256
1570
|
);
|
|
1257
1571
|
const resultJson = JSON.stringify(result);
|
|
1258
1572
|
if (resultJson !== subscription.lastResultJson) {
|
|
@@ -1261,6 +1575,9 @@ function createDatabaseHandle(definition) {
|
|
|
1261
1575
|
}
|
|
1262
1576
|
}
|
|
1263
1577
|
};
|
|
1578
|
+
const notifyLiveQueries = () => {
|
|
1579
|
+
peers.refreshAllLiveQueries();
|
|
1580
|
+
};
|
|
1264
1581
|
function mockEncryptForMap(names) {
|
|
1265
1582
|
const encryptFor = {};
|
|
1266
1583
|
for (const name of names) {
|
|
@@ -1301,13 +1618,19 @@ function createDatabaseHandle(definition) {
|
|
|
1301
1618
|
}
|
|
1302
1619
|
const defaultDocuments = {
|
|
1303
1620
|
async query(query) {
|
|
1304
|
-
return runMockDocumentQuery(
|
|
1621
|
+
return runMockDocumentQuery(
|
|
1622
|
+
storedDocuments,
|
|
1623
|
+
definition.info.id,
|
|
1624
|
+
query,
|
|
1625
|
+
peers.getDocuments
|
|
1626
|
+
);
|
|
1305
1627
|
},
|
|
1306
1628
|
async liveQuery(query, onResult) {
|
|
1307
1629
|
const initial = runMockDocumentQuery(
|
|
1308
1630
|
storedDocuments,
|
|
1309
1631
|
definition.info.id,
|
|
1310
|
-
query
|
|
1632
|
+
query,
|
|
1633
|
+
peers.getDocuments
|
|
1311
1634
|
);
|
|
1312
1635
|
const subscription = {
|
|
1313
1636
|
query,
|
|
@@ -1324,7 +1647,8 @@ function createDatabaseHandle(definition) {
|
|
|
1324
1647
|
const result = runMockDocumentQuery(
|
|
1325
1648
|
storedDocuments,
|
|
1326
1649
|
definition.info.id,
|
|
1327
|
-
query
|
|
1650
|
+
query,
|
|
1651
|
+
peers.getDocuments
|
|
1328
1652
|
);
|
|
1329
1653
|
subscription.lastResultJson = JSON.stringify(result);
|
|
1330
1654
|
onResult(result);
|
|
@@ -1842,7 +2166,9 @@ function createDatabaseHandle(definition) {
|
|
|
1842
2166
|
docId: document.id,
|
|
1843
2167
|
data: structuredClone(document.data),
|
|
1844
2168
|
createdAt: document.updatedAt ?? null
|
|
1845
|
-
}))
|
|
2169
|
+
})),
|
|
2170
|
+
documents: storedDocuments,
|
|
2171
|
+
notifyLiveQueries: refreshLiveQueries
|
|
1846
2172
|
};
|
|
1847
2173
|
}
|
|
1848
2174
|
function createMockSessionState(options = {}) {
|
|
@@ -1854,6 +2180,16 @@ function createMockSessionState(options = {}) {
|
|
|
1854
2180
|
const beforeCloseListeners = /* @__PURE__ */ new Set();
|
|
1855
2181
|
const databaseHandles = /* @__PURE__ */ new Map();
|
|
1856
2182
|
const databaseViewDocumentLists = /* @__PURE__ */ new Map();
|
|
2183
|
+
const databaseDocumentStores = /* @__PURE__ */ new Map();
|
|
2184
|
+
const databaseLiveQueryRefreshers = /* @__PURE__ */ new Map();
|
|
2185
|
+
const databasePeers = {
|
|
2186
|
+
getDocuments: (databaseId) => databaseDocumentStores.get(databaseId),
|
|
2187
|
+
refreshAllLiveQueries: () => {
|
|
2188
|
+
for (const refresh of databaseLiveQueryRefreshers.values()) {
|
|
2189
|
+
refresh();
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
1857
2193
|
const databaseViewApis = /* @__PURE__ */ new Map();
|
|
1858
2194
|
const sessionViews = /* @__PURE__ */ new Map();
|
|
1859
2195
|
let activeMenuResolve = null;
|
|
@@ -1861,6 +2197,7 @@ function createMockSessionState(options = {}) {
|
|
|
1861
2197
|
let dragProfile = null;
|
|
1862
2198
|
let databaseInfos = [];
|
|
1863
2199
|
const openSealedChannels = /* @__PURE__ */ new Map();
|
|
2200
|
+
const proposedApps = [];
|
|
1864
2201
|
const appStorage = new Map(Object.entries(options.storage ?? {}));
|
|
1865
2202
|
const requireCapability = (databaseId, capability) => {
|
|
1866
2203
|
const info = databaseInfos.find((entry) => entry.id === databaseId);
|
|
@@ -1927,14 +2264,18 @@ function createMockSessionState(options = {}) {
|
|
|
1927
2264
|
databaseHandles.clear();
|
|
1928
2265
|
databaseViewDocumentLists.clear();
|
|
1929
2266
|
databaseViewApis.clear();
|
|
2267
|
+
databaseDocumentStores.clear();
|
|
2268
|
+
databaseLiveQueryRefreshers.clear();
|
|
1930
2269
|
databaseInfos = definitions.map((definition) => ({
|
|
1931
2270
|
...definition.info,
|
|
1932
2271
|
capabilities: [...definition.info.capabilities]
|
|
1933
2272
|
}));
|
|
1934
2273
|
for (const definition of definitions) {
|
|
1935
|
-
const created = createDatabaseHandle(definition);
|
|
2274
|
+
const created = createDatabaseHandle(definition, databasePeers);
|
|
1936
2275
|
databaseHandles.set(definition.info.id, created.handle);
|
|
1937
2276
|
databaseViewDocumentLists.set(definition.info.id, created.listViewDocuments);
|
|
2277
|
+
databaseDocumentStores.set(definition.info.id, created.documents);
|
|
2278
|
+
databaseLiveQueryRefreshers.set(definition.info.id, created.notifyLiveQueries);
|
|
1938
2279
|
databaseViewApis.set(definition.info.id, {
|
|
1939
2280
|
async create(input) {
|
|
1940
2281
|
const documents = input.databaseIds.flatMap((databaseId) => {
|
|
@@ -1996,6 +2337,11 @@ function createMockSessionState(options = {}) {
|
|
|
1996
2337
|
async getLicensedProducts() {
|
|
1997
2338
|
return [...launchContext.licensedProducts ?? []];
|
|
1998
2339
|
},
|
|
2340
|
+
async proposeApp(input) {
|
|
2341
|
+
proposedApps.push({ ...input });
|
|
2342
|
+
const outcome = options.proposeApp?.(input);
|
|
2343
|
+
return outcome ?? { ok: false, reason: "declined" };
|
|
2344
|
+
},
|
|
1999
2345
|
async listDatabases() {
|
|
2000
2346
|
return databaseInfos.map((entry) => ({
|
|
2001
2347
|
...entry,
|
|
@@ -2153,6 +2499,7 @@ function createMockSessionState(options = {}) {
|
|
|
2153
2499
|
getLicensedProducts() {
|
|
2154
2500
|
return [...launchContext.licensedProducts ?? []];
|
|
2155
2501
|
},
|
|
2502
|
+
proposedApps,
|
|
2156
2503
|
createViewNavigator: session.createViewNavigator,
|
|
2157
2504
|
openViewNavigator: session.openViewNavigator,
|
|
2158
2505
|
bridge,
|
|
@@ -2197,6 +2544,7 @@ function createMockMindooDBAppSession(options = {}) {
|
|
|
2197
2544
|
getLaunchContext: state.getLaunchContext,
|
|
2198
2545
|
setLaunchContext: state.setLaunchContext,
|
|
2199
2546
|
getLicensedProducts: state.getLicensedProducts,
|
|
2547
|
+
proposedApps: state.proposedApps,
|
|
2200
2548
|
listDatabases: state.listDatabaseInfos,
|
|
2201
2549
|
setDatabases: state.setDatabases,
|
|
2202
2550
|
emitThemeChange: state.emitThemeChange,
|
|
@@ -2421,6 +2769,8 @@ function createFakeBridgeHost(options = {}) {
|
|
|
2421
2769
|
case "sealedChannel.close":
|
|
2422
2770
|
await state.session.closeSealedChannel(String(params.channelId));
|
|
2423
2771
|
return { ok: true };
|
|
2772
|
+
case "apps.propose":
|
|
2773
|
+
return await state.session.proposeApp(params);
|
|
2424
2774
|
case "documents.list":
|
|
2425
2775
|
return await state.getDatabase(String(params.databaseId)).documents.list(
|
|
2426
2776
|
params.query
|
|
@@ -2915,6 +3265,9 @@ function createFakeBridgeHost(options = {}) {
|
|
|
2915
3265
|
get requests() {
|
|
2916
3266
|
return requests;
|
|
2917
3267
|
},
|
|
3268
|
+
get proposedApps() {
|
|
3269
|
+
return state.proposedApps;
|
|
3270
|
+
},
|
|
2918
3271
|
install,
|
|
2919
3272
|
dispose() {
|
|
2920
3273
|
restoreWindow();
|