mindoodb-app-sdk 0.0.53 → 0.0.55
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 +120 -55
- package/TESTING.md +36 -2
- package/dist/{chunk-6NTVTL6G.js → chunk-S73KTYZF.js} +139 -6
- package/dist/index.d.ts +80 -3
- package/dist/index.js +276 -8
- package/dist/testing/index.d.ts +170 -10
- package/dist/testing/index.js +731 -62
- package/dist/{types-CsKw_niD.d.ts → types-Dl70Tlfj.d.ts} +144 -2
- package/dist/vite/index.d.ts +18 -5
- package/dist/vite/index.js +156 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
MINDOODB_APP_MAX_DRAG_TYPES,
|
|
9
9
|
MINDOODB_APP_VALUE_TAG,
|
|
10
10
|
MINIMAL_DRAG_PREVIEW_PNG,
|
|
11
|
+
MindooDBAppAgentToolError,
|
|
11
12
|
MindooDBAppDragInputError,
|
|
12
13
|
MindooDBAppValue,
|
|
13
14
|
applyHostedDocumentOverscrollContain,
|
|
@@ -23,7 +24,7 @@ import {
|
|
|
23
24
|
releaseMindooDBAppBridgeSessions,
|
|
24
25
|
sanitizeHostShortcutBindings,
|
|
25
26
|
snapshotElementToPng
|
|
26
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-S73KTYZF.js";
|
|
27
28
|
import {
|
|
28
29
|
MINDOODB_APP_BUNDLE_ARCHIVE_FILE_NAME,
|
|
29
30
|
MINDOODB_APP_BUNDLE_MANIFEST_FILE_NAME,
|
|
@@ -286,6 +287,13 @@ function readListing(value, errors) {
|
|
|
286
287
|
errors
|
|
287
288
|
);
|
|
288
289
|
if (description) listing.description = description;
|
|
290
|
+
const descriptionMarkdown = readLocalizedText(
|
|
291
|
+
value.descriptionMarkdown,
|
|
292
|
+
"App definition listing.descriptionMarkdown",
|
|
293
|
+
MINDOODB_APP_LISTING_DESCRIPTION_MAX,
|
|
294
|
+
errors
|
|
295
|
+
);
|
|
296
|
+
if (descriptionMarkdown) listing.descriptionMarkdown = descriptionMarkdown;
|
|
289
297
|
const icon = readListingAssetPath(value.icon, "App definition listing.icon", errors);
|
|
290
298
|
if (icon) listing.icon = icon;
|
|
291
299
|
if (value.screenshots !== void 0 && value.screenshots !== null) {
|
|
@@ -575,6 +583,212 @@ function isHostedBundleRuntime(search) {
|
|
|
575
583
|
return readMindooDBAppHostingMode(search) === "hosted";
|
|
576
584
|
}
|
|
577
585
|
|
|
586
|
+
// src/listingMarkdown.ts
|
|
587
|
+
var MAX_INLINE_DEPTH = 4;
|
|
588
|
+
var HEADING = /^#{1,6}\s+(.*?)\s*#*\s*$/;
|
|
589
|
+
var BULLET = /^[-*+]\s+(.*)$/;
|
|
590
|
+
var ORDERED = /^\d{1,9}[.)]\s+(.*)$/;
|
|
591
|
+
var ESCAPABLE = new Set("\\`*_[]()#+-.!<>".split(""));
|
|
592
|
+
var BARE_URL_TRAILING_PUNCTUATION = /[.,;:!?)\]'"]+$/;
|
|
593
|
+
function sanitizeListingHref(raw) {
|
|
594
|
+
const cleaned = raw.replace(/[\t\n\r]/g, "").trim();
|
|
595
|
+
if (!cleaned) {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
try {
|
|
599
|
+
const url = new URL(cleaned);
|
|
600
|
+
if (url.protocol === "https:") {
|
|
601
|
+
return url.toString();
|
|
602
|
+
}
|
|
603
|
+
if (url.protocol === "mailto:" && url.pathname) {
|
|
604
|
+
return url.toString();
|
|
605
|
+
}
|
|
606
|
+
} catch {
|
|
607
|
+
}
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
function isWordCharacter(char) {
|
|
611
|
+
return char !== void 0 && /[\p{L}\p{N}]/u.test(char);
|
|
612
|
+
}
|
|
613
|
+
function pushText(nodes, text) {
|
|
614
|
+
if (!text) {
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
const last = nodes[nodes.length - 1];
|
|
618
|
+
if (last?.type === "text") {
|
|
619
|
+
last.text += text;
|
|
620
|
+
} else {
|
|
621
|
+
nodes.push({ type: "text", text });
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function findClosingDelimiter(text, from, delimiter) {
|
|
625
|
+
let index = text.indexOf(delimiter, from);
|
|
626
|
+
while (index !== -1) {
|
|
627
|
+
const before = text[index - 1];
|
|
628
|
+
const after = text[index + delimiter.length];
|
|
629
|
+
const closesAfterContent = index > from && before !== " " && before !== "\\";
|
|
630
|
+
const underscoreBoundary = delimiter[0] !== "_" || !isWordCharacter(after);
|
|
631
|
+
if (closesAfterContent && underscoreBoundary) {
|
|
632
|
+
return index;
|
|
633
|
+
}
|
|
634
|
+
index = text.indexOf(delimiter, index + 1);
|
|
635
|
+
}
|
|
636
|
+
return -1;
|
|
637
|
+
}
|
|
638
|
+
function findLinkTargetEnd(text, from) {
|
|
639
|
+
let open = 0;
|
|
640
|
+
for (let index = from; index < text.length; index += 1) {
|
|
641
|
+
const char = text[index];
|
|
642
|
+
if (char === "(") {
|
|
643
|
+
open += 1;
|
|
644
|
+
} else if (char === ")") {
|
|
645
|
+
if (open === 0) return index;
|
|
646
|
+
open -= 1;
|
|
647
|
+
} else if (char === " ") {
|
|
648
|
+
return -1;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return -1;
|
|
652
|
+
}
|
|
653
|
+
function parseInline(text, depth = 0) {
|
|
654
|
+
const nodes = [];
|
|
655
|
+
let index = 0;
|
|
656
|
+
while (index < text.length) {
|
|
657
|
+
const char = text[index];
|
|
658
|
+
if (char === "\\" && ESCAPABLE.has(text[index + 1] ?? "")) {
|
|
659
|
+
pushText(nodes, text[index + 1]);
|
|
660
|
+
index += 2;
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
if (char === "`") {
|
|
664
|
+
const end = text.indexOf("`", index + 1);
|
|
665
|
+
if (end > index + 1) {
|
|
666
|
+
nodes.push({ type: "code", text: text.slice(index + 1, end) });
|
|
667
|
+
index = end + 1;
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
if (depth < MAX_INLINE_DEPTH && (char === "*" || char === "_")) {
|
|
672
|
+
const strong = text[index + 1] === char;
|
|
673
|
+
const delimiter = strong ? char + char : char;
|
|
674
|
+
const next = text[index + delimiter.length];
|
|
675
|
+
const opens = next !== void 0 && next !== " " && (char !== "_" || !isWordCharacter(text[index - 1]));
|
|
676
|
+
if (opens) {
|
|
677
|
+
const end = findClosingDelimiter(text, index + delimiter.length, delimiter);
|
|
678
|
+
if (end !== -1) {
|
|
679
|
+
const children = parseInline(text.slice(index + delimiter.length, end), depth + 1);
|
|
680
|
+
nodes.push(strong ? { type: "strong", children } : { type: "em", children });
|
|
681
|
+
index = end + delimiter.length;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (char === "[" && depth < MAX_INLINE_DEPTH) {
|
|
687
|
+
const labelEnd = text.indexOf("](", index + 1);
|
|
688
|
+
const urlEnd = labelEnd === -1 ? -1 : findLinkTargetEnd(text, labelEnd + 2);
|
|
689
|
+
if (labelEnd > index + 1 && urlEnd !== -1) {
|
|
690
|
+
const label = parseInline(text.slice(index + 1, labelEnd), depth + 1);
|
|
691
|
+
const href = sanitizeListingHref(text.slice(labelEnd + 2, urlEnd));
|
|
692
|
+
if (href) {
|
|
693
|
+
nodes.push({ type: "link", href, children: label });
|
|
694
|
+
} else {
|
|
695
|
+
nodes.push(...label);
|
|
696
|
+
}
|
|
697
|
+
index = urlEnd + 1;
|
|
698
|
+
continue;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (char === "h" && text.startsWith("https://", index) && !isWordCharacter(text[index - 1])) {
|
|
702
|
+
let end = index;
|
|
703
|
+
while (end < text.length && !/\s/.test(text[end])) end += 1;
|
|
704
|
+
const candidate = text.slice(index, end).replace(BARE_URL_TRAILING_PUNCTUATION, "");
|
|
705
|
+
const href = sanitizeListingHref(candidate);
|
|
706
|
+
if (href) {
|
|
707
|
+
nodes.push({ type: "link", href, children: [{ type: "text", text: candidate }] });
|
|
708
|
+
index += candidate.length;
|
|
709
|
+
continue;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
pushText(nodes, char);
|
|
713
|
+
index += 1;
|
|
714
|
+
}
|
|
715
|
+
return nodes;
|
|
716
|
+
}
|
|
717
|
+
function parseListingMarkdown(markdown) {
|
|
718
|
+
const blocks = [];
|
|
719
|
+
let paragraph = [];
|
|
720
|
+
let list = null;
|
|
721
|
+
const flushParagraph = () => {
|
|
722
|
+
if (paragraph.length) {
|
|
723
|
+
blocks.push({ type: "paragraph", children: parseInline(paragraph.join(" ")) });
|
|
724
|
+
paragraph = [];
|
|
725
|
+
}
|
|
726
|
+
};
|
|
727
|
+
const flushList = () => {
|
|
728
|
+
if (list) {
|
|
729
|
+
blocks.push({ type: "list", ordered: list.ordered, items: list.items.map((item) => parseInline(item)) });
|
|
730
|
+
list = null;
|
|
731
|
+
}
|
|
732
|
+
};
|
|
733
|
+
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
734
|
+
const line = rawLine.trim();
|
|
735
|
+
if (!line) {
|
|
736
|
+
flushParagraph();
|
|
737
|
+
flushList();
|
|
738
|
+
continue;
|
|
739
|
+
}
|
|
740
|
+
const heading = HEADING.exec(line);
|
|
741
|
+
const bullet = BULLET.exec(line);
|
|
742
|
+
const ordered = bullet ? null : ORDERED.exec(line);
|
|
743
|
+
if (heading) {
|
|
744
|
+
flushParagraph();
|
|
745
|
+
flushList();
|
|
746
|
+
blocks.push({ type: "heading", children: parseInline(heading[1]) });
|
|
747
|
+
} else if (bullet || ordered) {
|
|
748
|
+
flushParagraph();
|
|
749
|
+
const isOrdered = Boolean(ordered);
|
|
750
|
+
if (list && list.ordered !== isOrdered) {
|
|
751
|
+
flushList();
|
|
752
|
+
}
|
|
753
|
+
list ??= { ordered: isOrdered, items: [] };
|
|
754
|
+
list.items.push((bullet ?? ordered)[1]);
|
|
755
|
+
} else if (list) {
|
|
756
|
+
list.items[list.items.length - 1] += ` ${line}`;
|
|
757
|
+
} else {
|
|
758
|
+
paragraph.push(line);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
flushParagraph();
|
|
762
|
+
flushList();
|
|
763
|
+
return blocks;
|
|
764
|
+
}
|
|
765
|
+
function inlineToPlainText(nodes) {
|
|
766
|
+
return nodes.map((node) => {
|
|
767
|
+
if (node.type === "text" || node.type === "code") {
|
|
768
|
+
return node.text;
|
|
769
|
+
}
|
|
770
|
+
const label = inlineToPlainText(node.children);
|
|
771
|
+
if (node.type !== "link") {
|
|
772
|
+
return label;
|
|
773
|
+
}
|
|
774
|
+
const target = node.href.replace(/^mailto:/, "").replace(/^https:\/\//, "").replace(/\/$/, "");
|
|
775
|
+
return label.includes(target) ? label : `${label} (${node.href.replace(/^mailto:/, "")})`;
|
|
776
|
+
}).join("");
|
|
777
|
+
}
|
|
778
|
+
function listingMarkdownToPlainText(markdown) {
|
|
779
|
+
const paragraphs = [];
|
|
780
|
+
for (const block of parseListingMarkdown(markdown)) {
|
|
781
|
+
if (block.type === "list") {
|
|
782
|
+
block.items.forEach((item, index) => {
|
|
783
|
+
paragraphs.push(`${block.ordered ? `${index + 1}.` : "\u2022"} ${inlineToPlainText(item)}`);
|
|
784
|
+
});
|
|
785
|
+
} else {
|
|
786
|
+
paragraphs.push(inlineToPlainText(block.children));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return paragraphs.filter((paragraph) => paragraph.trim()).join("\n\n");
|
|
790
|
+
}
|
|
791
|
+
|
|
578
792
|
// src/landing.ts
|
|
579
793
|
var DEFAULT_HAVEN_URL = "https://haven.mindoodb.com";
|
|
580
794
|
var APP_ICON_FALLBACKS = ["appicon.png", "appicon.webp", "appicon.svg"];
|
|
@@ -638,6 +852,43 @@ function el(tag, className, text) {
|
|
|
638
852
|
if (text !== void 0) node.textContent = text;
|
|
639
853
|
return node;
|
|
640
854
|
}
|
|
855
|
+
function externalLink(href) {
|
|
856
|
+
const link = el("a");
|
|
857
|
+
link.href = href;
|
|
858
|
+
link.rel = "noopener noreferrer";
|
|
859
|
+
link.target = "_blank";
|
|
860
|
+
return link;
|
|
861
|
+
}
|
|
862
|
+
function appendInline(parent, nodes) {
|
|
863
|
+
for (const node of nodes) {
|
|
864
|
+
if (node.type === "text") {
|
|
865
|
+
parent.append(node.text);
|
|
866
|
+
} else if (node.type === "code") {
|
|
867
|
+
parent.append(el("code", void 0, node.text));
|
|
868
|
+
} else {
|
|
869
|
+
const child = node.type === "link" ? externalLink(node.href) : el(node.type);
|
|
870
|
+
appendInline(child, node.children);
|
|
871
|
+
parent.append(child);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
function renderMarkdownDescription(block, markdown) {
|
|
876
|
+
for (const entry of parseListingMarkdown(markdown)) {
|
|
877
|
+
if (entry.type === "list") {
|
|
878
|
+
const list = el(entry.ordered ? "ol" : "ul");
|
|
879
|
+
for (const item of entry.items) {
|
|
880
|
+
const li = el("li");
|
|
881
|
+
appendInline(li, item);
|
|
882
|
+
list.append(li);
|
|
883
|
+
}
|
|
884
|
+
block.append(list);
|
|
885
|
+
} else {
|
|
886
|
+
const node = el(entry.type === "heading" ? "h2" : "p");
|
|
887
|
+
appendInline(node, entry.children);
|
|
888
|
+
block.append(node);
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
641
892
|
var STYLE_ID = "mdb-landing-style";
|
|
642
893
|
var STYLE = `
|
|
643
894
|
.mdb-landing{--bg:#f4f6fb;--card:#fff;--text:#172033;--muted:#5b6478;--line:#e2e6ef;--accent:#1f3a8a;--accent-text:#fff;
|
|
@@ -657,7 +908,12 @@ var STYLE = `
|
|
|
657
908
|
.mdb-landing__button:focus-visible{outline:3px solid var(--accent);outline-offset:3px}
|
|
658
909
|
.mdb-landing__hint{margin:0;max-width:420px;color:var(--muted);font-size:14px}
|
|
659
910
|
.mdb-landing__description{margin:32px 0 0}
|
|
660
|
-
.mdb-landing__description p{margin:0 0 12px}
|
|
911
|
+
.mdb-landing__description p,.mdb-landing__description ul,.mdb-landing__description ol{margin:0 0 12px}
|
|
912
|
+
.mdb-landing__description ul,.mdb-landing__description ol{padding-left:22px}
|
|
913
|
+
.mdb-landing__description li+li{margin-top:4px}
|
|
914
|
+
.mdb-landing__description h2{margin:20px 0 8px;font-size:17px}
|
|
915
|
+
.mdb-landing__description code{font:.92em ui-monospace,SFMono-Regular,Menlo,monospace;padding:1px 5px;border-radius:5px;background:var(--line)}
|
|
916
|
+
.mdb-landing__description a,.mdb-landing__meta a{color:var(--accent)}
|
|
661
917
|
.mdb-landing__shots-title{margin:32px 0 12px;font-size:15px;color:var(--muted);font-weight:600}
|
|
662
918
|
.mdb-landing__shots{display:grid;grid-auto-flow:column;grid-auto-columns:min(78%,420px);gap:16px;overflow-x:auto;padding-bottom:8px;scroll-snap-type:x mandatory}
|
|
663
919
|
.mdb-landing__shot{margin:0;scroll-snap-align:start}
|
|
@@ -706,11 +962,14 @@ async function renderHavenAppLandingPage(options = {}) {
|
|
|
706
962
|
}
|
|
707
963
|
const titles = el("div");
|
|
708
964
|
titles.append(el("p", "mdb-landing__eyebrow", strings.eyebrow), el("h1", "mdb-landing__title", label));
|
|
709
|
-
const meta =
|
|
965
|
+
const meta = el("p", "mdb-landing__meta");
|
|
710
966
|
if (listing?.publisher) {
|
|
711
|
-
|
|
967
|
+
const [before, after = ""] = strings.by.split("{publisher}");
|
|
968
|
+
const name = listing.publisher.url ? Object.assign(externalLink(listing.publisher.url), { textContent: listing.publisher.name }) : listing.publisher.name;
|
|
969
|
+
meta.append(before ?? "", name, after, " \xB7 ");
|
|
712
970
|
}
|
|
713
|
-
|
|
971
|
+
meta.append(format(strings.from, { origin }));
|
|
972
|
+
titles.append(meta);
|
|
714
973
|
head.append(titles);
|
|
715
974
|
card.append(head);
|
|
716
975
|
const summary = resolveMindooDBAppLocalizedText(listing?.summary, locale) || definition?.description || strings.fallbackSummary;
|
|
@@ -722,11 +981,16 @@ async function renderHavenAppLandingPage(options = {}) {
|
|
|
722
981
|
button.dataset.testid = "haven-app-landing-install";
|
|
723
982
|
cta.append(button, el("p", "mdb-landing__hint", format(strings.ctaHint, { label })));
|
|
724
983
|
card.append(cta);
|
|
984
|
+
const descriptionMarkdown = resolveMindooDBAppLocalizedText(listing?.descriptionMarkdown, locale);
|
|
725
985
|
const description = resolveMindooDBAppLocalizedText(listing?.description, locale);
|
|
726
|
-
if (description) {
|
|
986
|
+
if (descriptionMarkdown || description) {
|
|
727
987
|
const block = el("section", "mdb-landing__description");
|
|
728
|
-
|
|
729
|
-
|
|
988
|
+
if (descriptionMarkdown) {
|
|
989
|
+
renderMarkdownDescription(block, descriptionMarkdown);
|
|
990
|
+
} else {
|
|
991
|
+
for (const paragraph of description.split(/\n\s*\n/)) {
|
|
992
|
+
if (paragraph.trim()) block.append(el("p", void 0, paragraph.trim()));
|
|
993
|
+
}
|
|
730
994
|
}
|
|
731
995
|
card.append(block);
|
|
732
996
|
}
|
|
@@ -1438,6 +1702,7 @@ export {
|
|
|
1438
1702
|
MINDOODB_APP_VALUE_TAG,
|
|
1439
1703
|
MINDOODB_APP_WELL_KNOWN_DRAG_TYPES,
|
|
1440
1704
|
MINIMAL_DRAG_PREVIEW_PNG,
|
|
1705
|
+
MindooDBAppAgentToolError,
|
|
1441
1706
|
MindooDBAppDragInputError,
|
|
1442
1707
|
MindooDBAppValue,
|
|
1443
1708
|
MindooDBRichTextHandle,
|
|
@@ -1461,11 +1726,13 @@ export {
|
|
|
1461
1726
|
isLaunchedByHaven,
|
|
1462
1727
|
isMindooDBAppBundleHash,
|
|
1463
1728
|
isMindooDBAppDragType,
|
|
1729
|
+
listingMarkdownToPlainText,
|
|
1464
1730
|
matchHostShortcut,
|
|
1465
1731
|
normalizeDragAccepts,
|
|
1466
1732
|
normalizeDragPointerInput,
|
|
1467
1733
|
normalizeDragStartInput,
|
|
1468
1734
|
normalizeMindooDBAppBundlePath,
|
|
1735
|
+
parseListingMarkdown,
|
|
1469
1736
|
queryDocuments,
|
|
1470
1737
|
readMindooDBAppHostingMode,
|
|
1471
1738
|
readMindooDBAppLaunchId,
|
|
@@ -1476,6 +1743,7 @@ export {
|
|
|
1476
1743
|
resolveMindooDBAppListingAssetUrl,
|
|
1477
1744
|
resolveMindooDBAppLocalizedText,
|
|
1478
1745
|
sanitizeHostShortcutBindings,
|
|
1746
|
+
sanitizeListingHref,
|
|
1479
1747
|
snapshotElementToPng,
|
|
1480
1748
|
validateMindooDBAppBundleManifest,
|
|
1481
1749
|
validateMindooDBAppDefinition,
|
package/dist/testing/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { aG as MindooDBAppCreateViewNavigatorInput, cM as MindooDBAppViewNavigator, a7 as MindooDBAppBridgeRpcRequest, bf as MindooDBAppHostTheme, ao as MindooDBAppCapability, j as MindooDBAppDefinition, G as MindooDBAppAttachmentApi, e as MindooDBAppBridge, cm as MindooDBAppSession, bO as MindooDBAppProposeAppInput, cU as MindooDBAppViewport, cG as MindooDBAppUiPreferences, bU as MindooDBAppQueryResult, cT as MindooDBAppViewUpdateStats, a3 as MindooDBAppBridgePortMessage, aH as MindooDBAppDatabaseInfo, $ as MindooDBAppBridgeErrorPayload, bt as MindooDBAppLaunchContext, aQ as MindooDBAppDocumentApi, cO as MindooDBAppViewNavigatorOpenOptions, bg as MindooDBAppIdentityApi, cC as MindooDBAppTimestampApi, aO as MindooDBAppDirectoryApi, l as MindooDBAppDocument, bc as MindooDBAppFulltextSetup, bb as MindooDBAppExtractionSetup, cu as MindooDBAppSummarySetup, bP as MindooDBAppProposeAppResult, c as MindooDBAppDragStartResult, b5 as MindooDBAppDragProfile } from '../types-Dl70Tlfj.js';
|
|
2
2
|
import * as mindoodb from 'mindoodb';
|
|
3
3
|
import 'mindoodb-view-language';
|
|
4
4
|
|
|
@@ -31,12 +31,22 @@ declare function createEvaluatingViewNavigator(input: MindooDBAppCreateViewNavig
|
|
|
31
31
|
* tenant and an install in a real Haven. It is never meant to be deployed where end
|
|
32
32
|
* users can reach it.
|
|
33
33
|
*/
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* A scan the test host hands out on the app's `attachments.scan()` call. With `bytes`
|
|
36
|
+
* the file is written to the document's attachments, as Haven's scanner does; without,
|
|
37
|
+
* only the attachment info is returned.
|
|
38
|
+
*/
|
|
35
39
|
interface BrowserTestHostScan {
|
|
36
40
|
fileName: string;
|
|
37
41
|
mimeType: "image/jpeg" | "image/png" | "application/pdf";
|
|
38
|
-
size
|
|
42
|
+
size?: number;
|
|
43
|
+
bytes?: Uint8Array;
|
|
39
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* What a scan returns when no {@link BrowserTestHost.setNextScan} is pending: cancel,
|
|
47
|
+
* a generated sample page, or a fixed file.
|
|
48
|
+
*/
|
|
49
|
+
type BrowserTestHostScanMode = "cancel" | "sample" | BrowserTestHostScan;
|
|
40
50
|
/** What the test host recorded, in the order the app asked. */
|
|
41
51
|
interface BrowserTestHostLog {
|
|
42
52
|
notifications: Array<{
|
|
@@ -54,15 +64,22 @@ interface BrowserTestHostLog {
|
|
|
54
64
|
databaseId: string;
|
|
55
65
|
docId: string;
|
|
56
66
|
result: "scanned" | "cancelled";
|
|
67
|
+
fileName?: string;
|
|
68
|
+
mimeType?: string;
|
|
69
|
+
size?: number;
|
|
57
70
|
}>;
|
|
58
71
|
focusRequests: number;
|
|
59
72
|
requests: MindooDBAppBridgeRpcRequest[];
|
|
73
|
+
/** The answer to each request, by request id (result or error, duration). */
|
|
74
|
+
responses: Record<string, FakeBridgeRpcResponse>;
|
|
60
75
|
}
|
|
61
76
|
interface CreateBrowserTestHostOptions extends CreateFakeBridgeHostOptions {
|
|
62
77
|
/** The iframe the app runs in. Its `src` is set by the host. */
|
|
63
78
|
frame: HTMLIFrameElement;
|
|
64
79
|
/** URL of the app page, relative to the test page or absolute. */
|
|
65
80
|
appUrl: string;
|
|
81
|
+
/** What scans return while no `setNextScan()` is pending. Default `"cancel"`. */
|
|
82
|
+
scanMode?: BrowserTestHostScanMode;
|
|
66
83
|
/** Called after every request, e.g. to re-render a control panel. */
|
|
67
84
|
onChange?: () => void;
|
|
68
85
|
}
|
|
@@ -70,30 +87,98 @@ interface BrowserTestHost {
|
|
|
70
87
|
host: FakeBridgeHostController;
|
|
71
88
|
log: BrowserTestHostLog;
|
|
72
89
|
/**
|
|
73
|
-
* Decides the outcome of the app's next `attachments.scan()`: a file
|
|
74
|
-
*
|
|
90
|
+
* Decides the outcome of the app's next `attachments.scan()`: a file to "scan", or
|
|
91
|
+
* `null` to act as if the user cancelled. Later scans follow {@link setScanMode}.
|
|
75
92
|
*/
|
|
76
93
|
setNextScan(scan: BrowserTestHostScan | null): void;
|
|
94
|
+
/** What scans return while no `setNextScan()` is pending. */
|
|
95
|
+
setScanMode(mode: BrowserTestHostScanMode): void;
|
|
96
|
+
getScanMode(): BrowserTestHostScanMode;
|
|
77
97
|
setTheme(theme: MindooDBAppHostTheme): void;
|
|
78
98
|
setHostFocus(focused: boolean): void;
|
|
99
|
+
/** Haven's language changed: the app gets `onLocaleChange`, no reload. */
|
|
100
|
+
setLocale(locale: string): void;
|
|
101
|
+
/**
|
|
102
|
+
* Grants a mapped database these capabilities and reloads the app, like Haven
|
|
103
|
+
* relaunching it after a permission change.
|
|
104
|
+
*/
|
|
105
|
+
setCapabilities(databaseId: string, capabilities: MindooDBAppCapability[]): void;
|
|
106
|
+
/** Turns the capability checks on or off (`enforceCapabilities`). */
|
|
107
|
+
setEnforceCapabilities(enforce: boolean): void;
|
|
108
|
+
/** Replaces the usernames of the tenant directory (`directory.listUsers()`). */
|
|
109
|
+
setDirectoryUsers(users: readonly string[]): void;
|
|
79
110
|
/** Reloads the app frame. The mock data survives; the app's in-memory state does not. */
|
|
80
111
|
reloadApp(): void;
|
|
81
112
|
dispose(): void;
|
|
82
113
|
}
|
|
114
|
+
/** Every capability Haven can grant, in the order the panel lists them. */
|
|
115
|
+
declare const ALL_CAPABILITIES: readonly MindooDBAppCapability[];
|
|
83
116
|
/**
|
|
84
117
|
* The capabilities Haven grants for the permissions a definition requests. `read` is
|
|
85
118
|
* implied by the mapping, `write` is create plus update.
|
|
86
119
|
*/
|
|
87
120
|
declare function capabilitiesForDefinitionPermissions(permissions: readonly string[] | undefined): MindooDBAppCapability[];
|
|
88
|
-
/**
|
|
89
|
-
|
|
121
|
+
/** Changes to one database of a definition, for testing other mappings than requested. */
|
|
122
|
+
interface MockDatabaseOverride {
|
|
123
|
+
title?: string;
|
|
124
|
+
/** Grant exactly these instead of what the definition requests. */
|
|
125
|
+
capabilities?: MindooDBAppCapability[];
|
|
126
|
+
/** `false`: the user did not map this database to the app. */
|
|
127
|
+
mapped?: boolean;
|
|
128
|
+
}
|
|
129
|
+
interface MockDatabasesFromDefinitionOptions {
|
|
130
|
+
/** By logical database id. */
|
|
131
|
+
overrides?: Record<string, MockDatabaseOverride>;
|
|
132
|
+
/**
|
|
133
|
+
* `"memory"` (default): attachments the app writes can be listed and read back.
|
|
134
|
+
* `"none"`: the mock's stateless default.
|
|
135
|
+
*/
|
|
136
|
+
attachments?: "memory" | "none";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Mock databases for every database a `haven-app.json` declares, granted as requested
|
|
140
|
+
* unless `options.overrides` says otherwise.
|
|
141
|
+
*/
|
|
142
|
+
declare function mockDatabasesFromDefinition(definition: Pick<MindooDBAppDefinition, "databases">, seed?: Record<string, MockMindooDBAppDatabaseDefinition["documents"]>, options?: MockDatabasesFromDefinitionOptions): MockMindooDBAppDatabaseDefinition[];
|
|
143
|
+
/**
|
|
144
|
+
* Test-host settings from the page URL, so a scenario can be linked or scripted:
|
|
145
|
+
*
|
|
146
|
+
* - `db=<id>:<capability>,<capability>` grants exactly these; `db=<id>:none` unmaps it
|
|
147
|
+
* - `enforce=1` turns the capability checks on
|
|
148
|
+
* - `users=<count>` fills the directory with generated users
|
|
149
|
+
* - `locale=<tag>`, `theme=dark|light`
|
|
150
|
+
*/
|
|
151
|
+
interface TestHostUrlSettings {
|
|
152
|
+
overrides: Record<string, MockDatabaseOverride>;
|
|
153
|
+
enforceCapabilities?: boolean;
|
|
154
|
+
directoryUserCount?: number;
|
|
155
|
+
locale?: string;
|
|
156
|
+
theme?: "dark" | "light";
|
|
157
|
+
}
|
|
158
|
+
declare function readTestHostUrlSettings(search: string): TestHostUrlSettings;
|
|
90
159
|
declare function createBrowserTestHost(options: CreateBrowserTestHostOptions): BrowserTestHost;
|
|
91
160
|
interface MountHavenTestHostOptions extends Omit<CreateBrowserTestHostOptions, "frame" | "onChange"> {
|
|
92
161
|
/** Element to render into. Defaults to `document.body`, which is cleared. */
|
|
93
162
|
container?: HTMLElement;
|
|
94
163
|
/** Heading of the control panel, e.g. the app label. */
|
|
95
164
|
title?: string;
|
|
165
|
+
/** Locales offered in the panel's language switcher. */
|
|
166
|
+
locales?: ReadonlyArray<{
|
|
167
|
+
tag: string;
|
|
168
|
+
label: string;
|
|
169
|
+
}>;
|
|
170
|
+
/**
|
|
171
|
+
* Read `db=`, `enforce=`, `users=`, `locale=` and `theme=` from the page URL (see
|
|
172
|
+
* {@link readTestHostUrlSettings}) and keep the URL in sync with the panel. Default
|
|
173
|
+
* true. URL settings win over the options passed here.
|
|
174
|
+
*/
|
|
175
|
+
urlSettings?: boolean;
|
|
96
176
|
}
|
|
177
|
+
/** The locales Haven's UI offers; the default of the panel's language switcher. */
|
|
178
|
+
declare const TEST_HOST_LOCALES: ReadonlyArray<{
|
|
179
|
+
tag: string;
|
|
180
|
+
label: string;
|
|
181
|
+
}>;
|
|
97
182
|
declare global {
|
|
98
183
|
interface Window {
|
|
99
184
|
/** Set by {@link mountHavenTestHost} so Playwright can script the host. */
|
|
@@ -101,12 +186,36 @@ declare global {
|
|
|
101
186
|
}
|
|
102
187
|
}
|
|
103
188
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
189
|
+
* JSON for the request log: binary as size plus a hex preview, long strings and arrays
|
|
190
|
+
* cut, deep objects summarized.
|
|
191
|
+
*/
|
|
192
|
+
declare function previewJson(value: unknown, maxString?: number, maxItems?: number, maxDepth?: number): string;
|
|
193
|
+
/**
|
|
194
|
+
* Renders the full test page: the app frame plus a control panel — theme, host focus,
|
|
195
|
+
* language, capabilities per database, directory users, the scanner — and a live log
|
|
196
|
+
* of notifications, previews, scans and requests (click an entry for details).
|
|
106
197
|
* The returned host is also available as `window.__havenTestHost`.
|
|
107
198
|
*/
|
|
108
199
|
declare function mountHavenTestHost(options: MountHavenTestHostOptions): BrowserTestHost;
|
|
109
200
|
|
|
201
|
+
/**
|
|
202
|
+
* In-memory attachments for mock databases: what an app writes can be listed and read
|
|
203
|
+
* back, like in Haven. The default mock attachment API stores nothing; the browser test
|
|
204
|
+
* host (`mockDatabasesFromDefinition`) uses this store instead.
|
|
205
|
+
*/
|
|
206
|
+
|
|
207
|
+
declare function createMemoryAttachments(): Partial<MindooDBAppAttachmentApi>;
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Capability checks for the mock databases, the way Haven enforces them: a call the
|
|
211
|
+
* database was not granted is rejected with a `forbidden` error. Opt in with
|
|
212
|
+
* `enforceCapabilities: true`; without it the mock only lists the capabilities.
|
|
213
|
+
*/
|
|
214
|
+
|
|
215
|
+
declare class MockForbiddenError extends Error {
|
|
216
|
+
name: string;
|
|
217
|
+
}
|
|
218
|
+
|
|
110
219
|
type MaybePromise<T> = T | Promise<T>;
|
|
111
220
|
type MockViewApi = {
|
|
112
221
|
create(input: MindooDBAppCreateViewNavigatorInput): MaybePromise<MindooDBAppViewNavigator>;
|
|
@@ -198,7 +307,27 @@ interface CreateMockMindooDBAppSessionOptions {
|
|
|
198
307
|
proposeApp?: (input: MindooDBAppProposeAppInput) => MindooDBAppProposeAppResult;
|
|
199
308
|
/** Initial contents of `session.storage`. */
|
|
200
309
|
storage?: Record<string, string>;
|
|
310
|
+
/**
|
|
311
|
+
* Reject calls a database was not granted, like Haven does (a `forbidden` error;
|
|
312
|
+
* `documents.canCreate()` and friends answer `allowed: false`). Off by default so
|
|
313
|
+
* existing tests keep passing; turn it on to test how an app handles missing rights.
|
|
314
|
+
*/
|
|
315
|
+
enforceCapabilities?: boolean;
|
|
316
|
+
/**
|
|
317
|
+
* Usernames `directory.listUsers()` returns on every database (Haven's directory is
|
|
318
|
+
* tenant-wide), e.g. `generateDirectoryUsers(120)`. A database's own
|
|
319
|
+
* `methods.directory` still wins.
|
|
320
|
+
*/
|
|
321
|
+
directoryUsers?: readonly string[];
|
|
201
322
|
}
|
|
323
|
+
/**
|
|
324
|
+
* Test usernames in Haven's canonical form: `CN=Test User 001/O=Test`, …
|
|
325
|
+
* Enough of them (e.g. 120) exercise the paging of `directory.listUsers({ … })`.
|
|
326
|
+
*/
|
|
327
|
+
declare function generateDirectoryUsers(count: number, options?: {
|
|
328
|
+
prefix?: string;
|
|
329
|
+
organization?: string;
|
|
330
|
+
}): string[];
|
|
202
331
|
interface MockMindooDBAppSessionController {
|
|
203
332
|
bridge: MindooDBAppBridge;
|
|
204
333
|
session: MindooDBAppSession;
|
|
@@ -209,6 +338,16 @@ interface MockMindooDBAppSessionController {
|
|
|
209
338
|
proposedApps: MindooDBAppProposeAppInput[];
|
|
210
339
|
listDatabases(): MindooDBAppDatabaseInfo[];
|
|
211
340
|
setDatabases(definitions: MockMindooDBAppDatabaseDefinition[]): void;
|
|
341
|
+
/**
|
|
342
|
+
* Replaces the capabilities of one mapped database. Takes effect on the next call
|
|
343
|
+
* (with `enforceCapabilities`) and in the launch context; apps that read the
|
|
344
|
+
* capabilities once at start see the change after a reload.
|
|
345
|
+
*/
|
|
346
|
+
setCapabilities(databaseId: string, capabilities: MindooDBAppCapability[]): void;
|
|
347
|
+
/** Turns the capability checks on or off (see `enforceCapabilities`). */
|
|
348
|
+
setEnforceCapabilities(enforce: boolean): void;
|
|
349
|
+
/** Replaces the directory's usernames (see `directoryUsers`). */
|
|
350
|
+
setDirectoryUsers(users: readonly string[]): void;
|
|
212
351
|
emitThemeChange(theme: MindooDBAppHostTheme): void;
|
|
213
352
|
emitViewportChange(viewport: MindooDBAppViewport): void;
|
|
214
353
|
emitUiPreferencesChange(uiPreferences: MindooDBAppUiPreferences): void;
|
|
@@ -232,11 +371,23 @@ type FakeBridgePortMessageHandler = (message: Exclude<MindooDBAppBridgePortMessa
|
|
|
232
371
|
host: FakeBridgeHostController;
|
|
233
372
|
port: MessagePort;
|
|
234
373
|
}) => MaybePromise<boolean | void>;
|
|
374
|
+
/** How the fake host answered one RPC request (see `onResponse`). */
|
|
375
|
+
type FakeBridgeRpcResponse = {
|
|
376
|
+
ok: true;
|
|
377
|
+
result: unknown;
|
|
378
|
+
durationMs: number;
|
|
379
|
+
} | {
|
|
380
|
+
ok: false;
|
|
381
|
+
error: MindooDBAppBridgeErrorPayload;
|
|
382
|
+
durationMs: number;
|
|
383
|
+
};
|
|
235
384
|
interface CreateFakeBridgeHostOptions extends CreateMockMindooDBAppSessionOptions {
|
|
236
385
|
windowMode?: "parent" | "opener";
|
|
237
386
|
requestHandlers?: Record<string, FakeBridgeRequestHandler>;
|
|
238
387
|
/** Called for every RPC request the app sends, before it is answered. */
|
|
239
388
|
onRequest?: (request: MindooDBAppBridgeRpcRequest) => void;
|
|
389
|
+
/** Called with the answer to every RPC request, right after it was sent. */
|
|
390
|
+
onResponse?: (request: MindooDBAppBridgeRpcRequest, response: FakeBridgeRpcResponse) => void;
|
|
240
391
|
onPortMessage?: FakeBridgePortMessageHandler;
|
|
241
392
|
}
|
|
242
393
|
interface FakeBridgeHostController {
|
|
@@ -272,7 +423,16 @@ interface FakeBridgeHostController {
|
|
|
272
423
|
postPortMessage(message: MindooDBAppBridgePortMessage, transfer?: Transferable[]): void;
|
|
273
424
|
setRequestHandler(method: string, handler: FakeBridgeRequestHandler): void;
|
|
274
425
|
clearRequestHandler(method: string): void;
|
|
426
|
+
listDatabases(): MindooDBAppDatabaseInfo[];
|
|
427
|
+
/** See {@link MockMindooDBAppSessionController.setCapabilities}. */
|
|
428
|
+
setCapabilities(databaseId: string, capabilities: MindooDBAppCapability[]): void;
|
|
429
|
+
/** See {@link CreateMockMindooDBAppSessionOptions.enforceCapabilities}. */
|
|
430
|
+
setEnforceCapabilities(enforce: boolean): void;
|
|
431
|
+
getEnforceCapabilities(): boolean;
|
|
432
|
+
/** See {@link CreateMockMindooDBAppSessionOptions.directoryUsers}. */
|
|
433
|
+
setDirectoryUsers(users: readonly string[]): void;
|
|
434
|
+
getDirectoryUsers(): string[];
|
|
275
435
|
}
|
|
276
436
|
declare function createFakeBridgeHost(options?: CreateFakeBridgeHostOptions): FakeBridgeHostController;
|
|
277
437
|
|
|
278
|
-
export { type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMockMindooDBAppBridge, createMockMindooDBAppSession, loadMindoodbForTesting, mockDatabasesFromDefinition, mountHavenTestHost };
|
|
438
|
+
export { ALL_CAPABILITIES, type BrowserTestHost, type BrowserTestHostLog, type BrowserTestHostScan, type BrowserTestHostScanMode, type CreateBrowserTestHostOptions, type CreateFakeBridgeHostOptions, type CreateMockMindooDBAppSessionOptions, type EvaluatingViewDocument, type FakeBridgeHostController, type FakeBridgePortMessageHandler, type FakeBridgeRequestHandler, type FakeBridgeRpcResponse, type MockDatabaseOverride, type MockDatabasesFromDefinitionOptions, MockForbiddenError, type MockMindooDBAppDatabaseDefinition, type MockMindooDBAppSessionController, type MockSealedChannelOptions, type MockSeedDocument, type MountHavenTestHostOptions, TEST_HOST_LOCALES, type TestHostUrlSettings, capabilitiesForDefinitionPermissions, createBrowserTestHost, createEvaluatingViewNavigator, createFakeBridgeHost, createMemoryAttachments, createMockMindooDBAppBridge, createMockMindooDBAppSession, generateDirectoryUsers, loadMindoodbForTesting, mockDatabasesFromDefinition, mountHavenTestHost, previewJson, readTestHostUrlSettings };
|