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.
@@ -1,11 +1,12 @@
1
1
  import {
2
2
  releaseMindooDBAppBridgeSessions
3
- } from "../chunk-XPPTUWW6.js";
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, doc, origin, createdAt, lastModifiedAt, witnessed, awaitingWitness) {
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, doc, origin, createdAt, lastModifiedAt, witnessed, awaitingWitness) {
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 rows = matches.slice(offset, offset + limit).map(({ document, textScore }) => ({
990
+ const page = matches.slice(offset, offset + limit);
991
+ const rows = page.map(({ document, textScore }) => ({
1002
992
  docId: document.id,
1003
- fields: query?.fields ? Object.fromEntries(
1004
- query.fields.map((field) => [
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 notifyLiveQueries = () => {
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(storedDocuments, definition.info.id, query);
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();