@crvouga/mockingbird-service-junction 1.7.1 → 1.8.0
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/CHANGELOG.md +10 -0
- package/dist/{chunk-R4GHJ6XN.js → chunk-OW7EOZRN.js} +2 -2
- package/dist/{chunk-KWBRKBUI.js → chunk-WDRX5POF.js} +268 -14
- package/dist/chunk-WDRX5POF.js.map +7 -0
- package/dist/cli.js +2 -2
- package/dist/index.js +1 -1
- package/dist/server.js +2 -2
- package/package.json +2 -2
- package/dist/chunk-KWBRKBUI.js.map +0 -7
- /package/dist/{chunk-R4GHJ6XN.js.map → chunk-OW7EOZRN.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog — @crvouga/mockingbird-service-junction
|
|
2
2
|
|
|
3
|
+
## 1.8.0 (2026-10-01)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- record SQL timelines and serve the admin explorer ([8ced17a](https://github.com/crvouga/mockingbird/commit/8ced17ac398e386ef4e9d7e01fc7744c34b95d07))
|
|
8
|
+
|
|
9
|
+
### Dependencies
|
|
10
|
+
|
|
11
|
+
- `@crvouga/mockingbird-service-sqlite`
|
|
12
|
+
|
|
3
13
|
## 1.7.1 (2026-09-30)
|
|
4
14
|
|
|
5
15
|
### Fixes and improvements
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
createRuntime,
|
|
4
4
|
defaultBillingTypesFrom,
|
|
5
5
|
pullCorpus
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-WDRX5POF.js";
|
|
7
7
|
import {
|
|
8
8
|
defaultCorpus
|
|
9
9
|
} from "./chunk-C2UGIPPM.js";
|
|
@@ -537,4 +537,4 @@ export {
|
|
|
537
537
|
createServer2 as createServer,
|
|
538
538
|
serveTarget
|
|
539
539
|
};
|
|
540
|
-
//# sourceMappingURL=chunk-
|
|
540
|
+
//# sourceMappingURL=chunk-OW7EOZRN.js.map
|
|
@@ -1671,6 +1671,167 @@ function faultPresetList(value) {
|
|
|
1671
1671
|
})
|
|
1672
1672
|
};
|
|
1673
1673
|
}
|
|
1674
|
+
function mountSqlExplorer(root, api) {
|
|
1675
|
+
const cell = (value) => {
|
|
1676
|
+
if (value === null || value === void 0)
|
|
1677
|
+
return "";
|
|
1678
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
1679
|
+
return String(value);
|
|
1680
|
+
}
|
|
1681
|
+
return JSON.stringify(value) ?? "";
|
|
1682
|
+
};
|
|
1683
|
+
root.replaceChildren();
|
|
1684
|
+
const layout = document.createElement("div");
|
|
1685
|
+
layout.className = "sql-layout";
|
|
1686
|
+
const tables = document.createElement("div");
|
|
1687
|
+
tables.className = "sql-tables";
|
|
1688
|
+
const stack = document.createElement("div");
|
|
1689
|
+
stack.className = "stack";
|
|
1690
|
+
const banner = document.createElement("p");
|
|
1691
|
+
banner.className = "banner";
|
|
1692
|
+
const grid = document.createElement("div");
|
|
1693
|
+
const input = document.createElement("textarea");
|
|
1694
|
+
input.className = "sql-input";
|
|
1695
|
+
input.spellcheck = false;
|
|
1696
|
+
input.placeholder = "SELECT * FROM ...";
|
|
1697
|
+
const actions = document.createElement("div");
|
|
1698
|
+
actions.className = "row-actions";
|
|
1699
|
+
const run = document.createElement("button");
|
|
1700
|
+
run.type = "button";
|
|
1701
|
+
run.textContent = "Run";
|
|
1702
|
+
actions.append(run);
|
|
1703
|
+
const result = document.createElement("pre");
|
|
1704
|
+
stack.append(banner, grid, input, actions, result);
|
|
1705
|
+
layout.append(tables, stack);
|
|
1706
|
+
root.append(layout);
|
|
1707
|
+
const showError = (error) => {
|
|
1708
|
+
banner.textContent = error instanceof Error ? error.message : String(error);
|
|
1709
|
+
banner.classList.add("show");
|
|
1710
|
+
};
|
|
1711
|
+
const clearError = () => {
|
|
1712
|
+
banner.textContent = "";
|
|
1713
|
+
banner.classList.remove("show");
|
|
1714
|
+
};
|
|
1715
|
+
const renderGrid = (columns, rows) => {
|
|
1716
|
+
grid.replaceChildren();
|
|
1717
|
+
if (columns.length === 0) {
|
|
1718
|
+
const empty = document.createElement("p");
|
|
1719
|
+
empty.className = "empty";
|
|
1720
|
+
empty.textContent = "No rows.";
|
|
1721
|
+
grid.append(empty);
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const table = document.createElement("table");
|
|
1725
|
+
const head = document.createElement("thead");
|
|
1726
|
+
const headRow = document.createElement("tr");
|
|
1727
|
+
for (const column of columns) {
|
|
1728
|
+
const th = document.createElement("th");
|
|
1729
|
+
th.textContent = column;
|
|
1730
|
+
headRow.append(th);
|
|
1731
|
+
}
|
|
1732
|
+
head.append(headRow);
|
|
1733
|
+
const body = document.createElement("tbody");
|
|
1734
|
+
for (const row of rows) {
|
|
1735
|
+
const tr = document.createElement("tr");
|
|
1736
|
+
for (const column of columns) {
|
|
1737
|
+
const td = document.createElement("td");
|
|
1738
|
+
td.dataset.label = column;
|
|
1739
|
+
td.textContent = cell(row[column]);
|
|
1740
|
+
tr.append(td);
|
|
1741
|
+
}
|
|
1742
|
+
body.append(tr);
|
|
1743
|
+
}
|
|
1744
|
+
table.append(head, body);
|
|
1745
|
+
grid.append(table);
|
|
1746
|
+
};
|
|
1747
|
+
const rowsOf = (value) => {
|
|
1748
|
+
if (!isRecord2(value) || !Array.isArray(value.rows))
|
|
1749
|
+
return [];
|
|
1750
|
+
return value.rows.flatMap((row) => isRecord2(row) ? [row] : []);
|
|
1751
|
+
};
|
|
1752
|
+
const columnsOf = (value, rows) => {
|
|
1753
|
+
if (isRecord2(value) && Array.isArray(value.columns)) {
|
|
1754
|
+
const named = value.columns.flatMap((column) => typeof column === "string" ? [column] : []);
|
|
1755
|
+
if (named.length > 0)
|
|
1756
|
+
return named;
|
|
1757
|
+
}
|
|
1758
|
+
const first = rows[0];
|
|
1759
|
+
return first === void 0 ? [] : Object.keys(first);
|
|
1760
|
+
};
|
|
1761
|
+
let selected = "";
|
|
1762
|
+
const loadPage = async (schema, name) => {
|
|
1763
|
+
selected = `${schema}.${name}`;
|
|
1764
|
+
const page = await api.get(`/sql/tables/${encodeURIComponent(schema)}/${encodeURIComponent(name)}?limit=50`);
|
|
1765
|
+
const rows = rowsOf(page);
|
|
1766
|
+
renderGrid(columnsOf(page, rows), rows);
|
|
1767
|
+
const total = isRecord2(page) && typeof page.total === "number" ? page.total : rows.length;
|
|
1768
|
+
result.textContent = `${selected} \xB7 ${total} rows`;
|
|
1769
|
+
for (const button of tables.querySelectorAll("button")) {
|
|
1770
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1771
|
+
continue;
|
|
1772
|
+
button.setAttribute("aria-current", button.dataset.schema === schema && button.dataset.table === name ? "true" : "false");
|
|
1773
|
+
}
|
|
1774
|
+
};
|
|
1775
|
+
const loadTables = async () => {
|
|
1776
|
+
const payload = await api.get("/sql/tables");
|
|
1777
|
+
const listed = isRecord2(payload) && Array.isArray(payload.tables) ? payload.tables : [];
|
|
1778
|
+
tables.replaceChildren();
|
|
1779
|
+
if (listed.length === 0) {
|
|
1780
|
+
const empty = document.createElement("p");
|
|
1781
|
+
empty.className = "empty";
|
|
1782
|
+
empty.textContent = "No tables.";
|
|
1783
|
+
tables.append(empty);
|
|
1784
|
+
return;
|
|
1785
|
+
}
|
|
1786
|
+
for (const item of listed) {
|
|
1787
|
+
if (!isRecord2(item) || typeof item.schema !== "string" || typeof item.name !== "string")
|
|
1788
|
+
continue;
|
|
1789
|
+
const button = document.createElement("button");
|
|
1790
|
+
button.type = "button";
|
|
1791
|
+
button.dataset.schema = item.schema;
|
|
1792
|
+
button.dataset.table = item.name;
|
|
1793
|
+
button.textContent = `${item.schema}.${item.name}`;
|
|
1794
|
+
tables.append(button);
|
|
1795
|
+
}
|
|
1796
|
+
};
|
|
1797
|
+
tables.addEventListener("click", (event) => {
|
|
1798
|
+
const target = event.target;
|
|
1799
|
+
if (!(target instanceof Element))
|
|
1800
|
+
return;
|
|
1801
|
+
const button = target.closest("button");
|
|
1802
|
+
if (!(button instanceof HTMLButtonElement))
|
|
1803
|
+
return;
|
|
1804
|
+
const schema = button.dataset.schema;
|
|
1805
|
+
const name = button.dataset.table;
|
|
1806
|
+
if (schema === void 0 || name === void 0)
|
|
1807
|
+
return;
|
|
1808
|
+
clearError();
|
|
1809
|
+
loadPage(schema, name).catch(showError);
|
|
1810
|
+
});
|
|
1811
|
+
run.addEventListener("click", () => {
|
|
1812
|
+
const sql = input.value.trim();
|
|
1813
|
+
if (sql === "") {
|
|
1814
|
+
showError(new Error("Enter a statement"));
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
clearError();
|
|
1818
|
+
api.send("POST", "/sql/query", { sql }).then((payload) => {
|
|
1819
|
+
const rows = rowsOf(payload);
|
|
1820
|
+
renderGrid(columnsOf(payload, rows), rows);
|
|
1821
|
+
const count = isRecord2(payload) && typeof payload.rowCount === "number" ? payload.rowCount : rows.length;
|
|
1822
|
+
const truncated = isRecord2(payload) && payload.truncated === true ? " (truncated)" : "";
|
|
1823
|
+
result.textContent = `${count} rows${truncated}`;
|
|
1824
|
+
}).catch(showError);
|
|
1825
|
+
});
|
|
1826
|
+
const namespace = document.getElementById("namespace");
|
|
1827
|
+
if (namespace) {
|
|
1828
|
+
namespace.addEventListener("change", () => {
|
|
1829
|
+
clearError();
|
|
1830
|
+
loadTables().catch(showError);
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
loadTables().catch(showError);
|
|
1834
|
+
}
|
|
1674
1835
|
function bootAdmin(config) {
|
|
1675
1836
|
const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
|
|
1676
1837
|
const sources = ["declared", "inferred", "mixed"];
|
|
@@ -2347,6 +2508,22 @@ function bootAdmin(config) {
|
|
|
2347
2508
|
panel.script = value.script;
|
|
2348
2509
|
return panel;
|
|
2349
2510
|
};
|
|
2511
|
+
const decodeExtension = (value) => {
|
|
2512
|
+
if (!isRecord2(value))
|
|
2513
|
+
return void 0;
|
|
2514
|
+
if (value.kind === "sql") {
|
|
2515
|
+
return {
|
|
2516
|
+
kind: "sql",
|
|
2517
|
+
id: typeof value.id === "string" && value.id !== "" ? value.id : "sql",
|
|
2518
|
+
title: typeof value.title === "string" && value.title !== "" ? value.title : "SQL",
|
|
2519
|
+
description: typeof value.description === "string" ? value.description : "Browse tables and run queries."
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
if (value.kind !== "panel")
|
|
2523
|
+
return void 0;
|
|
2524
|
+
const panel = decodePanel(value);
|
|
2525
|
+
return panel === void 0 ? void 0 : { kind: "panel", ...panel };
|
|
2526
|
+
};
|
|
2350
2527
|
const mountPanels = (panels) => {
|
|
2351
2528
|
const nav = el("nav", HTMLElement);
|
|
2352
2529
|
const views = el("views", HTMLElement);
|
|
@@ -2400,10 +2577,48 @@ function bootAdmin(config) {
|
|
|
2400
2577
|
return decoded === void 0 ? [] : [decoded];
|
|
2401
2578
|
});
|
|
2402
2579
|
mountPanels(panels);
|
|
2580
|
+
const extensions = requireList(isRecord2(manifest) ? manifest.extensions : void 0, "Extensions").flatMap((extension) => {
|
|
2581
|
+
const decoded = decodeExtension(extension);
|
|
2582
|
+
return decoded === void 0 ? [] : [decoded];
|
|
2583
|
+
});
|
|
2584
|
+
const extensionPanels = extensions.flatMap((extension) => extension.kind === "panel" ? [extension] : []);
|
|
2585
|
+
mountPanels(extensionPanels);
|
|
2586
|
+
for (const extension of extensions) {
|
|
2587
|
+
if (extension.kind !== "sql")
|
|
2588
|
+
continue;
|
|
2589
|
+
const nav = el("nav", HTMLElement);
|
|
2590
|
+
const views = el("views", HTMLElement);
|
|
2591
|
+
const button = document.createElement("button");
|
|
2592
|
+
button.type = "button";
|
|
2593
|
+
button.dataset.view = extension.id;
|
|
2594
|
+
button.textContent = extension.title;
|
|
2595
|
+
nav.append(button);
|
|
2596
|
+
const section = document.createElement("section");
|
|
2597
|
+
section.className = "view";
|
|
2598
|
+
section.id = `view-${extension.id}`;
|
|
2599
|
+
section.hidden = true;
|
|
2600
|
+
const title = document.createElement("h2");
|
|
2601
|
+
title.textContent = extension.title;
|
|
2602
|
+
const lede = document.createElement("p");
|
|
2603
|
+
lede.className = "lede";
|
|
2604
|
+
lede.textContent = extension.description;
|
|
2605
|
+
const body = document.createElement("div");
|
|
2606
|
+
body.className = "panel-body";
|
|
2607
|
+
section.append(title, lede, body);
|
|
2608
|
+
views.append(section);
|
|
2609
|
+
mountSqlExplorer(body, panelApi);
|
|
2610
|
+
}
|
|
2403
2611
|
return refresh();
|
|
2404
2612
|
}).catch(showError);
|
|
2405
2613
|
}
|
|
2406
|
-
var clientPrelude = [
|
|
2614
|
+
var clientPrelude = [
|
|
2615
|
+
isRecord2,
|
|
2616
|
+
mountAdminBrand,
|
|
2617
|
+
startAdminBrand,
|
|
2618
|
+
faultPresetList,
|
|
2619
|
+
mountSqlExplorer,
|
|
2620
|
+
bootAdmin
|
|
2621
|
+
].map((fn) => fn.toString()).join("\n");
|
|
2407
2622
|
var adminClientSource = (config) => {
|
|
2408
2623
|
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2409
2624
|
const entry = bootAdmin.name;
|
|
@@ -2453,19 +2668,33 @@ var STANDARD_ADMIN_ROUTES = [
|
|
|
2453
2668
|
"GET /ui/manifest"
|
|
2454
2669
|
];
|
|
2455
2670
|
var PANEL_ID = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
2671
|
+
var RESERVED_VIEWS = /* @__PURE__ */ new Set(["overview", "state", "clock", "faults", "journal", "routes"]);
|
|
2672
|
+
var checkPanel = (panel, seen, label) => {
|
|
2673
|
+
if (!PANEL_ID.test(panel.id)) {
|
|
2674
|
+
throw new RangeError(`${label} id must match ${PANEL_ID}: ${JSON.stringify(panel.id)}`);
|
|
2675
|
+
}
|
|
2676
|
+
if (RESERVED_VIEWS.has(panel.id)) {
|
|
2677
|
+
throw new RangeError(`${label} id ${panel.id} is reserved by the admin shell`);
|
|
2678
|
+
}
|
|
2679
|
+
if (seen.has(panel.id))
|
|
2680
|
+
throw new RangeError(`duplicate admin view id ${panel.id}`);
|
|
2681
|
+
seen.add(panel.id);
|
|
2682
|
+
if (panel.title.trim() === "")
|
|
2683
|
+
throw new RangeError(`${label} ${panel.id} needs a title`);
|
|
2684
|
+
};
|
|
2456
2685
|
var assertAdminUi = (ui) => {
|
|
2457
|
-
if (!ui
|
|
2686
|
+
if (!ui)
|
|
2458
2687
|
return;
|
|
2459
2688
|
const seen = /* @__PURE__ */ new Set();
|
|
2460
|
-
for (const panel of ui.panels)
|
|
2461
|
-
|
|
2462
|
-
|
|
2689
|
+
for (const panel of ui.panels ?? [])
|
|
2690
|
+
checkPanel(panel, seen, "admin panel");
|
|
2691
|
+
for (const extension of ui.extensions ?? []) {
|
|
2692
|
+
if (extension.kind === "panel") {
|
|
2693
|
+
checkPanel(extension, seen, "admin extension");
|
|
2694
|
+
continue;
|
|
2463
2695
|
}
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
seen.add(panel.id);
|
|
2467
|
-
if (panel.title.trim() === "")
|
|
2468
|
-
throw new RangeError(`admin panel ${panel.id} needs a title`);
|
|
2696
|
+
const id = extension.id ?? "sql";
|
|
2697
|
+
checkPanel({ id, title: extension.title ?? "SQL" }, seen, "admin extension");
|
|
2469
2698
|
}
|
|
2470
2699
|
};
|
|
2471
2700
|
|
|
@@ -2699,6 +2928,12 @@ var renderAdminDocument = (service) => {
|
|
|
2699
2928
|
border-radius: 999px; padding: 4px 8px;
|
|
2700
2929
|
}
|
|
2701
2930
|
.chip i { color: var(--faint); font-style: normal; }
|
|
2931
|
+
.sql-layout {
|
|
2932
|
+
display: grid; grid-template-columns: minmax(10rem, 16rem) minmax(0, 1fr); gap: 16px;
|
|
2933
|
+
}
|
|
2934
|
+
.sql-tables { display: flex; flex-direction: column; gap: 4px; max-height: 28rem; overflow: auto; }
|
|
2935
|
+
.sql-tables button { text-align: left; }
|
|
2936
|
+
.sql-input { font-family: var(--mono); min-height: 7rem; width: 100%; }
|
|
2702
2937
|
table { width: 100%; border-collapse: collapse; font-size: 14px; }
|
|
2703
2938
|
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
|
2704
2939
|
th { color: var(--faint); font-weight: 600; font-size: 12px; }
|
|
@@ -2762,7 +2997,7 @@ var renderAdminDocument = (service) => {
|
|
|
2762
2997
|
padding: 8px;
|
|
2763
2998
|
}
|
|
2764
2999
|
nav.side button { white-space: nowrap; flex: none; }
|
|
2765
|
-
.split { grid-template-columns: minmax(0, 1fr); }
|
|
3000
|
+
.split, .sql-layout { grid-template-columns: minmax(0, 1fr); }
|
|
2766
3001
|
main { padding: 14px; }
|
|
2767
3002
|
table, thead, tbody, tr, th, td { display: block; }
|
|
2768
3003
|
thead { display: none; }
|
|
@@ -2933,6 +3168,24 @@ var json2 = (body) => new Response(JSON.stringify(body), {
|
|
|
2933
3168
|
status: 200,
|
|
2934
3169
|
headers: { "content-type": "application/json; charset=utf-8" }
|
|
2935
3170
|
});
|
|
3171
|
+
var describeExtension = (extension) => {
|
|
3172
|
+
if (extension.kind === "sql") {
|
|
3173
|
+
return {
|
|
3174
|
+
kind: "sql",
|
|
3175
|
+
id: extension.id ?? "sql",
|
|
3176
|
+
title: extension.title ?? "SQL",
|
|
3177
|
+
...extension.description !== void 0 ? { description: extension.description } : {}
|
|
3178
|
+
};
|
|
3179
|
+
}
|
|
3180
|
+
return {
|
|
3181
|
+
kind: "panel",
|
|
3182
|
+
id: extension.id,
|
|
3183
|
+
title: extension.title,
|
|
3184
|
+
...extension.description !== void 0 ? { description: extension.description } : {},
|
|
3185
|
+
html: extension.html,
|
|
3186
|
+
...extension.script !== void 0 ? { script: extension.script } : {}
|
|
3187
|
+
};
|
|
3188
|
+
};
|
|
2936
3189
|
var adminUiRoutes = (service, ui) => {
|
|
2937
3190
|
const shell = () => renderAdminDocument(service);
|
|
2938
3191
|
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
@@ -2947,7 +3200,8 @@ var adminUiRoutes = (service, ui) => {
|
|
|
2947
3200
|
...panel.description !== void 0 ? { description: panel.description } : {},
|
|
2948
3201
|
html: panel.html,
|
|
2949
3202
|
...panel.script !== void 0 ? { script: panel.script } : {}
|
|
2950
|
-
}))
|
|
3203
|
+
})),
|
|
3204
|
+
extensions: (ui?.extensions ?? []).map(describeExtension)
|
|
2951
3205
|
})
|
|
2952
3206
|
};
|
|
2953
3207
|
};
|
|
@@ -3550,7 +3804,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
3550
3804
|
};
|
|
3551
3805
|
|
|
3552
3806
|
// ../core/dist/version.js
|
|
3553
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3807
|
+
var PACKAGE_VERSION = true ? "1.8.0" : UNRELEASED_VERSION;
|
|
3554
3808
|
|
|
3555
3809
|
// ../core/dist/webhooks.js
|
|
3556
3810
|
var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
@@ -12069,4 +12323,4 @@ export {
|
|
|
12069
12323
|
JunctionAPI,
|
|
12070
12324
|
LabAccountConflict
|
|
12071
12325
|
};
|
|
12072
|
-
//# sourceMappingURL=chunk-
|
|
12326
|
+
//# sourceMappingURL=chunk-WDRX5POF.js.map
|