@crvouga/mockingbird-service-fullscript 1.4.0 → 1.5.1
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 +16 -0
- package/dist/{chunk-PPJ5XV3E.js → chunk-25RMXYDW.js} +2 -2
- package/dist/{chunk-FLCK7ECX.js → chunk-OBQHBBFC.js} +1426 -622
- package/dist/chunk-OBQHBBFC.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 +3 -3
- package/dist/chunk-FLCK7ECX.js.map +0 -7
- /package/dist/{chunk-PPJ5XV3E.js.map → chunk-25RMXYDW.js.map} +0 -0
|
@@ -540,10 +540,10 @@ var OpenAPIReferenceError = class extends Error {
|
|
|
540
540
|
};
|
|
541
541
|
var unescapePointer = (segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
542
542
|
var isReference = (value) => typeof value === "object" && value !== null && typeof value.$ref === "string";
|
|
543
|
-
var resolveRef = (
|
|
543
|
+
var resolveRef = (document3, ref) => {
|
|
544
544
|
if (!ref.startsWith("#/"))
|
|
545
545
|
throw new OpenAPIReferenceError(ref);
|
|
546
|
-
let cursor =
|
|
546
|
+
let cursor = document3;
|
|
547
547
|
for (const raw of ref.slice(2).split("/")) {
|
|
548
548
|
const segment = unescapePointer(raw);
|
|
549
549
|
if (typeof cursor !== "object" || cursor === null || !(segment in cursor)) {
|
|
@@ -555,14 +555,14 @@ var resolveRef = (document2, ref) => {
|
|
|
555
555
|
throw new OpenAPIReferenceError(ref);
|
|
556
556
|
return cursor;
|
|
557
557
|
};
|
|
558
|
-
var deref = (
|
|
558
|
+
var deref = (document3, value) => {
|
|
559
559
|
let current = value;
|
|
560
560
|
const seen = /* @__PURE__ */ new Set();
|
|
561
561
|
while (isReference(current)) {
|
|
562
562
|
if (seen.has(current.$ref))
|
|
563
563
|
throw new OpenAPIReferenceError(`${current.$ref} (cycle)`);
|
|
564
564
|
seen.add(current.$ref);
|
|
565
|
-
current = resolveRef(
|
|
565
|
+
current = resolveRef(document3, current.$ref);
|
|
566
566
|
}
|
|
567
567
|
return current;
|
|
568
568
|
};
|
|
@@ -580,36 +580,36 @@ var HTTP_METHODS = [
|
|
|
580
580
|
];
|
|
581
581
|
|
|
582
582
|
// ../../openapi/core/dist/document.js
|
|
583
|
-
var mergeParameters = (
|
|
583
|
+
var mergeParameters = (document3, item, own) => {
|
|
584
584
|
const merged = /* @__PURE__ */ new Map();
|
|
585
585
|
for (const raw of item.parameters ?? []) {
|
|
586
|
-
const parameter = deref(
|
|
586
|
+
const parameter = deref(document3, raw);
|
|
587
587
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
588
588
|
}
|
|
589
589
|
for (const raw of own ?? []) {
|
|
590
|
-
const parameter = deref(
|
|
590
|
+
const parameter = deref(document3, raw);
|
|
591
591
|
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
592
592
|
}
|
|
593
593
|
return [...merged.values()];
|
|
594
594
|
};
|
|
595
|
-
var listOperations = (
|
|
595
|
+
var listOperations = (document3) => {
|
|
596
596
|
const operations = [];
|
|
597
|
-
for (const [path, item] of Object.entries(
|
|
597
|
+
for (const [path, item] of Object.entries(document3.paths)) {
|
|
598
598
|
for (const method of HTTP_METHODS) {
|
|
599
599
|
const operation = item[method];
|
|
600
600
|
if (operation?.operationId === void 0)
|
|
601
601
|
continue;
|
|
602
602
|
const responses = {};
|
|
603
603
|
for (const [status, response] of Object.entries(operation.responses)) {
|
|
604
|
-
responses[status] = deref(
|
|
604
|
+
responses[status] = deref(document3, response);
|
|
605
605
|
}
|
|
606
606
|
operations.push({
|
|
607
607
|
operationId: operation.operationId,
|
|
608
608
|
method,
|
|
609
609
|
path,
|
|
610
610
|
operation,
|
|
611
|
-
parameters: mergeParameters(
|
|
612
|
-
requestBody: operation.requestBody === void 0 ? void 0 : deref(
|
|
611
|
+
parameters: mergeParameters(document3, item, operation.parameters),
|
|
612
|
+
requestBody: operation.requestBody === void 0 ? void 0 : deref(document3, operation.requestBody),
|
|
613
613
|
responses
|
|
614
614
|
});
|
|
615
615
|
}
|
|
@@ -618,7 +618,7 @@ var listOperations = (document2) => {
|
|
|
618
618
|
};
|
|
619
619
|
|
|
620
620
|
// ../../openapi/core/dist/schema.js
|
|
621
|
-
var resolveSchema = (
|
|
621
|
+
var resolveSchema = (document3, schema) => {
|
|
622
622
|
let current = schema;
|
|
623
623
|
const seen = /* @__PURE__ */ new Set();
|
|
624
624
|
while (typeof current.$ref === "string") {
|
|
@@ -627,7 +627,7 @@ var resolveSchema = (document2, schema) => {
|
|
|
627
627
|
break;
|
|
628
628
|
seen.add(ref);
|
|
629
629
|
const { $ref: _ignored, ...siblings } = current;
|
|
630
|
-
const target = resolveRef(
|
|
630
|
+
const target = resolveRef(document3, ref);
|
|
631
631
|
current = { ...target, ...siblings };
|
|
632
632
|
}
|
|
633
633
|
if (current.nullable === true) {
|
|
@@ -698,9 +698,9 @@ var FORMAT_PATTERNS = {
|
|
|
698
698
|
ipv4: /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/
|
|
699
699
|
};
|
|
700
700
|
var graphemeLength = (value) => [...value].length;
|
|
701
|
-
var validateValue = (
|
|
701
|
+
var validateValue = (document3, schema, value, path = []) => {
|
|
702
702
|
const errors = [];
|
|
703
|
-
const s = resolveSchema(
|
|
703
|
+
const s = resolveSchema(document3, schema);
|
|
704
704
|
const fail3 = (message) => errors.push({ path, message });
|
|
705
705
|
const actual = jsonTypeOf(value);
|
|
706
706
|
if (actual === "undefined") {
|
|
@@ -762,7 +762,7 @@ var validateValue = (document2, schema, value, path = []) => {
|
|
|
762
762
|
value.forEach((item, i) => {
|
|
763
763
|
const itemSchema = s.prefixItems?.[i] ?? s.items;
|
|
764
764
|
if (itemSchema)
|
|
765
|
-
errors.push(...validateValue(
|
|
765
|
+
errors.push(...validateValue(document3, itemSchema, item, [...path, i]));
|
|
766
766
|
});
|
|
767
767
|
}
|
|
768
768
|
if (actual === "object") {
|
|
@@ -778,16 +778,16 @@ var validateValue = (document2, schema, value, path = []) => {
|
|
|
778
778
|
for (const key of keys) {
|
|
779
779
|
const property = s.properties?.[key];
|
|
780
780
|
if (property) {
|
|
781
|
-
errors.push(...validateValue(
|
|
781
|
+
errors.push(...validateValue(document3, property, record[key], [...path, key]));
|
|
782
782
|
continue;
|
|
783
783
|
}
|
|
784
784
|
if (s.additionalProperties === false)
|
|
785
785
|
fail3(`unexpected property ${key}`);
|
|
786
786
|
else if (typeof s.additionalProperties === "object") {
|
|
787
|
-
errors.push(...validateValue(
|
|
787
|
+
errors.push(...validateValue(document3, s.additionalProperties, record[key], [...path, key]));
|
|
788
788
|
}
|
|
789
789
|
if (s.propertyNames) {
|
|
790
|
-
const nameErrors = validateValue(
|
|
790
|
+
const nameErrors = validateValue(document3, s.propertyNames, key, [...path, key]);
|
|
791
791
|
if (nameErrors.length > 0)
|
|
792
792
|
fail3(`property name ${key} is invalid: ${nameErrors[0]?.message}`);
|
|
793
793
|
}
|
|
@@ -795,15 +795,15 @@ var validateValue = (document2, schema, value, path = []) => {
|
|
|
795
795
|
}
|
|
796
796
|
if (s.allOf)
|
|
797
797
|
for (const branch of s.allOf)
|
|
798
|
-
errors.push(...validateValue(
|
|
799
|
-
if (s.anyOf && !s.anyOf.some((branch) => validateValue(
|
|
798
|
+
errors.push(...validateValue(document3, branch, value, path));
|
|
799
|
+
if (s.anyOf && !s.anyOf.some((branch) => validateValue(document3, branch, value).length === 0))
|
|
800
800
|
fail3("matches no anyOf branch");
|
|
801
801
|
if (s.oneOf) {
|
|
802
|
-
const matches2 = s.oneOf.filter((branch) => validateValue(
|
|
802
|
+
const matches2 = s.oneOf.filter((branch) => validateValue(document3, branch, value).length === 0).length;
|
|
803
803
|
if (matches2 !== 1)
|
|
804
804
|
fail3(`matches ${matches2} oneOf branches, expected exactly 1`);
|
|
805
805
|
}
|
|
806
|
-
if (s.not && validateValue(
|
|
806
|
+
if (s.not && validateValue(document3, s.not, value).length === 0)
|
|
807
807
|
fail3("matches forbidden `not` schema");
|
|
808
808
|
return errors;
|
|
809
809
|
};
|
|
@@ -1358,6 +1358,1232 @@ var clearNamespace = (sqlite, namespace) => {
|
|
|
1358
1358
|
});
|
|
1359
1359
|
};
|
|
1360
1360
|
|
|
1361
|
+
// ../../ui/dist/reset.js
|
|
1362
|
+
var CSS_RESET = `*,
|
|
1363
|
+
*::before,
|
|
1364
|
+
*::after {
|
|
1365
|
+
box-sizing: border-box;
|
|
1366
|
+
margin: 0;
|
|
1367
|
+
padding: 0;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
* {
|
|
1371
|
+
scrollbar-width: thin;
|
|
1372
|
+
scrollbar-color: color-mix(in srgb, CanvasText 35%, Canvas) Canvas;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
*::-webkit-scrollbar {
|
|
1376
|
+
width: 12px;
|
|
1377
|
+
height: 12px;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
*::-webkit-scrollbar-track,
|
|
1381
|
+
*::-webkit-scrollbar-corner {
|
|
1382
|
+
background: Canvas;
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
*::-webkit-scrollbar-thumb {
|
|
1386
|
+
background: color-mix(in srgb, CanvasText 35%, Canvas);
|
|
1387
|
+
border: 3px solid Canvas;
|
|
1388
|
+
border-radius: 999px;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
html {
|
|
1392
|
+
hanging-punctuation: first allow-end last;
|
|
1393
|
+
-webkit-text-size-adjust: none;
|
|
1394
|
+
text-size-adjust: none;
|
|
1395
|
+
tab-size: 4;
|
|
1396
|
+
font-family:
|
|
1397
|
+
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI Variable", "Segoe UI",
|
|
1398
|
+
"Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
|
1399
|
+
font-size: 16px;
|
|
1400
|
+
line-height: 1.5;
|
|
1401
|
+
-webkit-font-smoothing: antialiased;
|
|
1402
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1403
|
+
text-rendering: optimizeLegibility;
|
|
1404
|
+
-webkit-tap-highlight-color: transparent;
|
|
1405
|
+
scroll-behavior: smooth;
|
|
1406
|
+
background: Canvas;
|
|
1407
|
+
color: CanvasText;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
body {
|
|
1411
|
+
min-height: 100svh;
|
|
1412
|
+
line-height: 1.5;
|
|
1413
|
+
background: Canvas;
|
|
1414
|
+
color: CanvasText;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
h1 {
|
|
1418
|
+
font-size: 2em;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
h2 {
|
|
1422
|
+
font-size: 1.5em;
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
h3 {
|
|
1426
|
+
font-size: 1.17em;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
h4 {
|
|
1430
|
+
font-size: 1em;
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
h5 {
|
|
1434
|
+
font-size: 0.83em;
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
h6 {
|
|
1438
|
+
font-size: 0.67em;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
h1,
|
|
1442
|
+
h2,
|
|
1443
|
+
h3,
|
|
1444
|
+
h4,
|
|
1445
|
+
h5,
|
|
1446
|
+
h6 {
|
|
1447
|
+
font-weight: 700;
|
|
1448
|
+
line-height: 1.2;
|
|
1449
|
+
text-wrap: balance;
|
|
1450
|
+
overflow-wrap: break-word;
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
p,
|
|
1454
|
+
li,
|
|
1455
|
+
figcaption,
|
|
1456
|
+
dd,
|
|
1457
|
+
dt,
|
|
1458
|
+
blockquote {
|
|
1459
|
+
overflow-wrap: break-word;
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
p,
|
|
1463
|
+
li,
|
|
1464
|
+
figcaption,
|
|
1465
|
+
blockquote {
|
|
1466
|
+
text-wrap: pretty;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
ul,
|
|
1470
|
+
ol,
|
|
1471
|
+
menu {
|
|
1472
|
+
list-style: none;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
a {
|
|
1476
|
+
color: inherit;
|
|
1477
|
+
text-decoration: underline;
|
|
1478
|
+
text-decoration-thickness: 1px;
|
|
1479
|
+
text-underline-offset: 0.15em;
|
|
1480
|
+
text-decoration-skip-ink: auto;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
b,
|
|
1484
|
+
strong {
|
|
1485
|
+
font-weight: 700;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
small {
|
|
1489
|
+
font-size: 0.875em;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
code,
|
|
1493
|
+
kbd,
|
|
1494
|
+
samp,
|
|
1495
|
+
pre {
|
|
1496
|
+
font-family: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1497
|
+
font-size: 1em;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
sub,
|
|
1501
|
+
sup {
|
|
1502
|
+
font-size: 75%;
|
|
1503
|
+
line-height: 0;
|
|
1504
|
+
position: relative;
|
|
1505
|
+
vertical-align: baseline;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
sub {
|
|
1509
|
+
bottom: -0.25em;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
sup {
|
|
1513
|
+
top: -0.5em;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
abbr[title] {
|
|
1517
|
+
text-decoration: underline dotted;
|
|
1518
|
+
text-underline-offset: 0.15em;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
mark {
|
|
1522
|
+
background: Mark;
|
|
1523
|
+
color: MarkText;
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
hr {
|
|
1527
|
+
height: 0;
|
|
1528
|
+
border: 0;
|
|
1529
|
+
border-top: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1530
|
+
color: inherit;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
img,
|
|
1534
|
+
picture,
|
|
1535
|
+
video,
|
|
1536
|
+
canvas {
|
|
1537
|
+
display: block;
|
|
1538
|
+
max-width: 100%;
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
img,
|
|
1542
|
+
video {
|
|
1543
|
+
height: auto;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
svg {
|
|
1547
|
+
overflow: hidden;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
iframe {
|
|
1551
|
+
border: 0;
|
|
1552
|
+
max-width: 100%;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
table {
|
|
1556
|
+
border-collapse: collapse;
|
|
1557
|
+
border-spacing: 0;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
fieldset {
|
|
1561
|
+
border: 0;
|
|
1562
|
+
min-inline-size: 0;
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
legend {
|
|
1566
|
+
padding: 0;
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
button,
|
|
1570
|
+
input,
|
|
1571
|
+
select,
|
|
1572
|
+
textarea,
|
|
1573
|
+
optgroup {
|
|
1574
|
+
font: inherit;
|
|
1575
|
+
letter-spacing: inherit;
|
|
1576
|
+
color: inherit;
|
|
1577
|
+
margin: 0;
|
|
1578
|
+
vertical-align: middle;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
button,
|
|
1582
|
+
input[type="button"],
|
|
1583
|
+
input[type="submit"],
|
|
1584
|
+
input[type="reset"] {
|
|
1585
|
+
appearance: none;
|
|
1586
|
+
background: none;
|
|
1587
|
+
border: 0;
|
|
1588
|
+
padding: 0;
|
|
1589
|
+
cursor: pointer;
|
|
1590
|
+
text-align: center;
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
input,
|
|
1594
|
+
textarea,
|
|
1595
|
+
select {
|
|
1596
|
+
background-color: Canvas;
|
|
1597
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1598
|
+
border-radius: 0;
|
|
1599
|
+
padding: 0.5em 0.65em;
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
input[type="checkbox"],
|
|
1603
|
+
input[type="radio"],
|
|
1604
|
+
input[type="range"],
|
|
1605
|
+
input[type="file"],
|
|
1606
|
+
input[type="color"],
|
|
1607
|
+
input[type="button"],
|
|
1608
|
+
input[type="submit"],
|
|
1609
|
+
input[type="reset"],
|
|
1610
|
+
input[type="image"],
|
|
1611
|
+
input[type="hidden"] {
|
|
1612
|
+
background: none;
|
|
1613
|
+
border: 0;
|
|
1614
|
+
padding: 0;
|
|
1615
|
+
border-radius: 0;
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
input[type="checkbox"],
|
|
1619
|
+
input[type="radio"] {
|
|
1620
|
+
accent-color: CanvasText;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
input[type="search"] {
|
|
1624
|
+
appearance: none;
|
|
1625
|
+
background-color: Canvas;
|
|
1626
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
input[type="search"]::-webkit-search-decoration,
|
|
1630
|
+
input[type="search"]::-webkit-search-cancel-button,
|
|
1631
|
+
input[type="search"]::-webkit-search-results-button,
|
|
1632
|
+
input[type="search"]::-webkit-search-results-decoration {
|
|
1633
|
+
-webkit-appearance: none;
|
|
1634
|
+
display: none;
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
input[type="number"] {
|
|
1638
|
+
appearance: textfield;
|
|
1639
|
+
background-color: Canvas;
|
|
1640
|
+
border: 1px solid color-mix(in srgb, CanvasText 28%, transparent);
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
input[type="number"]::-webkit-inner-spin-button,
|
|
1644
|
+
input[type="number"]::-webkit-outer-spin-button {
|
|
1645
|
+
-webkit-appearance: none;
|
|
1646
|
+
margin: 0;
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
textarea {
|
|
1650
|
+
resize: vertical;
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
::placeholder {
|
|
1654
|
+
opacity: 1;
|
|
1655
|
+
color: color-mix(in srgb, CanvasText 45%, transparent);
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
:focus-visible {
|
|
1659
|
+
outline: 2px solid CanvasText;
|
|
1660
|
+
outline-offset: 2px;
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
:focus:not(:focus-visible) {
|
|
1664
|
+
outline: none;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
:disabled,
|
|
1668
|
+
[disabled] {
|
|
1669
|
+
cursor: default;
|
|
1670
|
+
opacity: 0.6;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
label,
|
|
1674
|
+
summary,
|
|
1675
|
+
select,
|
|
1676
|
+
[role="button"] {
|
|
1677
|
+
cursor: pointer;
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
summary {
|
|
1681
|
+
display: list-item;
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
details {
|
|
1685
|
+
display: block;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
dialog {
|
|
1689
|
+
margin: auto;
|
|
1690
|
+
padding: 1rem;
|
|
1691
|
+
border: 1px solid color-mix(in srgb, CanvasText 20%, transparent);
|
|
1692
|
+
background: Canvas;
|
|
1693
|
+
color: CanvasText;
|
|
1694
|
+
max-width: calc(100vw - 2rem);
|
|
1695
|
+
max-height: calc(100svh - 2rem);
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
dialog:not([open]) {
|
|
1699
|
+
display: none;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
::backdrop {
|
|
1703
|
+
background: color-mix(in srgb, CanvasText 40%, transparent);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
[hidden] {
|
|
1707
|
+
display: none !important;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
::selection {
|
|
1711
|
+
background: Highlight;
|
|
1712
|
+
color: HighlightText;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
@media (prefers-reduced-motion: reduce) {
|
|
1716
|
+
*,
|
|
1717
|
+
*::before,
|
|
1718
|
+
*::after {
|
|
1719
|
+
animation-duration: 0.01ms !important;
|
|
1720
|
+
animation-iteration-count: 1 !important;
|
|
1721
|
+
transition-duration: 0.01ms !important;
|
|
1722
|
+
scroll-behavior: auto !important;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
@media (pointer: coarse) {
|
|
1727
|
+
input,
|
|
1728
|
+
select,
|
|
1729
|
+
textarea {
|
|
1730
|
+
font-size: max(16px, 1em);
|
|
1731
|
+
}
|
|
1732
|
+
}`;
|
|
1733
|
+
|
|
1734
|
+
// ../core/dist/admin-client.js
|
|
1735
|
+
function isRecord2(value) {
|
|
1736
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1737
|
+
}
|
|
1738
|
+
function mountAdminBrand(doc, host, service, catalog) {
|
|
1739
|
+
const clip = (value, max) => {
|
|
1740
|
+
if (typeof value !== "string")
|
|
1741
|
+
return null;
|
|
1742
|
+
const trimmed = value.trim();
|
|
1743
|
+
if (trimmed === "")
|
|
1744
|
+
return null;
|
|
1745
|
+
return trimmed.length > max ? trimmed.slice(0, max) : trimmed;
|
|
1746
|
+
};
|
|
1747
|
+
const httpUrl = (value) => {
|
|
1748
|
+
if (typeof value !== "string" || value.length > 2e3)
|
|
1749
|
+
return null;
|
|
1750
|
+
try {
|
|
1751
|
+
const url = new URL(value);
|
|
1752
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
1753
|
+
return null;
|
|
1754
|
+
return url.href;
|
|
1755
|
+
} catch {
|
|
1756
|
+
return null;
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
const hexColor = (value) => {
|
|
1760
|
+
if (typeof value !== "string")
|
|
1761
|
+
return null;
|
|
1762
|
+
const raw = value.trim();
|
|
1763
|
+
return /^#[0-9a-fA-F]{3,8}$/.test(raw) ? raw : null;
|
|
1764
|
+
};
|
|
1765
|
+
const hostname = (value) => {
|
|
1766
|
+
try {
|
|
1767
|
+
return new URL(value).hostname.replace(/^www\./, "");
|
|
1768
|
+
} catch {
|
|
1769
|
+
return value;
|
|
1770
|
+
}
|
|
1771
|
+
};
|
|
1772
|
+
host.replaceChildren();
|
|
1773
|
+
host.hidden = true;
|
|
1774
|
+
if (!isRecord2(catalog) || !Object.hasOwn(catalog, service))
|
|
1775
|
+
return;
|
|
1776
|
+
const row = catalog[service];
|
|
1777
|
+
if (!isRecord2(row))
|
|
1778
|
+
return;
|
|
1779
|
+
const vendor = clip(row.vendor, 80);
|
|
1780
|
+
const website = httpUrl(row.website);
|
|
1781
|
+
const guide = httpUrl(row.guide);
|
|
1782
|
+
const docs = httpUrl(row.docs);
|
|
1783
|
+
const logo = httpUrl(row.logo);
|
|
1784
|
+
const color = hexColor(row.color);
|
|
1785
|
+
const description = clip(row.description, 280);
|
|
1786
|
+
if (vendor === null || website === null && guide === null && docs === null)
|
|
1787
|
+
return;
|
|
1788
|
+
const add = (node) => {
|
|
1789
|
+
host.append(node);
|
|
1790
|
+
return node;
|
|
1791
|
+
};
|
|
1792
|
+
if (logo !== null) {
|
|
1793
|
+
const img = doc.createElement("img");
|
|
1794
|
+
img.alt = "";
|
|
1795
|
+
img.src = logo;
|
|
1796
|
+
img.width = 16;
|
|
1797
|
+
img.height = 16;
|
|
1798
|
+
img.decoding = "async";
|
|
1799
|
+
img.referrerPolicy = "no-referrer";
|
|
1800
|
+
if (color !== null)
|
|
1801
|
+
img.style.borderColor = color;
|
|
1802
|
+
add(img);
|
|
1803
|
+
}
|
|
1804
|
+
const name = doc.createElement("span");
|
|
1805
|
+
name.className = "vendor-name";
|
|
1806
|
+
name.textContent = vendor;
|
|
1807
|
+
if (description !== null)
|
|
1808
|
+
name.title = description;
|
|
1809
|
+
add(name);
|
|
1810
|
+
const sep = () => {
|
|
1811
|
+
const node = doc.createElement("span");
|
|
1812
|
+
node.className = "vendor-sep";
|
|
1813
|
+
node.setAttribute("aria-hidden", "true");
|
|
1814
|
+
node.textContent = "\xB7";
|
|
1815
|
+
return node;
|
|
1816
|
+
};
|
|
1817
|
+
const link = (href, label, title) => {
|
|
1818
|
+
const node = doc.createElement("a");
|
|
1819
|
+
node.className = "vendor-link";
|
|
1820
|
+
node.href = href;
|
|
1821
|
+
node.target = "_blank";
|
|
1822
|
+
node.rel = "noopener noreferrer";
|
|
1823
|
+
node.referrerPolicy = "no-referrer";
|
|
1824
|
+
node.textContent = label;
|
|
1825
|
+
node.title = title;
|
|
1826
|
+
return node;
|
|
1827
|
+
};
|
|
1828
|
+
if (website !== null) {
|
|
1829
|
+
add(sep());
|
|
1830
|
+
add(link(website, hostname(website), `${vendor} website`));
|
|
1831
|
+
}
|
|
1832
|
+
if (guide !== null) {
|
|
1833
|
+
add(sep());
|
|
1834
|
+
add(link(guide, "Docs", `Docs for ${vendor}`));
|
|
1835
|
+
}
|
|
1836
|
+
if (docs !== null) {
|
|
1837
|
+
add(sep());
|
|
1838
|
+
add(link(docs, "API", `${vendor} API reference`));
|
|
1839
|
+
}
|
|
1840
|
+
host.removeAttribute("hidden");
|
|
1841
|
+
host.hidden = false;
|
|
1842
|
+
}
|
|
1843
|
+
function startAdminBrand(env) {
|
|
1844
|
+
const requested = env.params.get("brands");
|
|
1845
|
+
let brandsUrl = env.brandsUrl;
|
|
1846
|
+
if (requested) {
|
|
1847
|
+
try {
|
|
1848
|
+
const url = new URL(requested, env.location.href);
|
|
1849
|
+
const local = url.hostname === "localhost" || url.hostname === "127.0.0.1" || url.hostname === "::1" || url.hostname === "[::1]" || url.origin === env.location.origin;
|
|
1850
|
+
if ((url.protocol === "https:" || url.protocol === "http:") && local)
|
|
1851
|
+
brandsUrl = url.href;
|
|
1852
|
+
} catch {
|
|
1853
|
+
brandsUrl = env.brandsUrl;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const vendorHost = env.$("vendor");
|
|
1857
|
+
const brandRoot = env.document.querySelector("[data-mockingbird-admin]");
|
|
1858
|
+
const brandService = brandRoot?.getAttribute("data-service") || "";
|
|
1859
|
+
if (vendorHost === null)
|
|
1860
|
+
return;
|
|
1861
|
+
env.fetch(brandsUrl, {
|
|
1862
|
+
headers: { accept: "application/json" },
|
|
1863
|
+
signal: AbortSignal.timeout(5e3)
|
|
1864
|
+
}).then(async (response) => {
|
|
1865
|
+
if (!response.ok)
|
|
1866
|
+
return null;
|
|
1867
|
+
const catalog = await response.json();
|
|
1868
|
+
return catalog;
|
|
1869
|
+
}).then((catalog) => {
|
|
1870
|
+
mountAdminBrand(env.document, vendorHost, brandService, catalog);
|
|
1871
|
+
}).catch(() => {
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
function faultPresetList(value) {
|
|
1875
|
+
if (!isRecord2(value))
|
|
1876
|
+
throw new Error("Fault presets response was not an object");
|
|
1877
|
+
if (!Object.hasOwn(value, "presets") || value.presets === void 0 || value.presets === null) {
|
|
1878
|
+
return { presets: [] };
|
|
1879
|
+
}
|
|
1880
|
+
if (!Array.isArray(value.presets))
|
|
1881
|
+
throw new Error("Fault presets response was not a list");
|
|
1882
|
+
return {
|
|
1883
|
+
presets: value.presets.flatMap((item) => {
|
|
1884
|
+
if (!isRecord2(item) || typeof item.name !== "string")
|
|
1885
|
+
return [];
|
|
1886
|
+
return [{ name: item.name }];
|
|
1887
|
+
})
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
function bootAdmin(config) {
|
|
1891
|
+
const fieldKinds = ["string", "number", "boolean", "null", "object", "array", "unknown"];
|
|
1892
|
+
const sources = ["declared", "inferred", "mixed"];
|
|
1893
|
+
const isFieldKind = (value) => typeof value === "string" && fieldKinds.some((kind) => kind === value);
|
|
1894
|
+
const isSource = (value) => typeof value === "string" && sources.some((source) => source === value);
|
|
1895
|
+
const readJson2 = (text) => {
|
|
1896
|
+
const value = JSON.parse(text);
|
|
1897
|
+
return value;
|
|
1898
|
+
};
|
|
1899
|
+
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => {
|
|
1900
|
+
switch (char) {
|
|
1901
|
+
case "&":
|
|
1902
|
+
return "&";
|
|
1903
|
+
case "<":
|
|
1904
|
+
return "<";
|
|
1905
|
+
case ">":
|
|
1906
|
+
return ">";
|
|
1907
|
+
case '"':
|
|
1908
|
+
return """;
|
|
1909
|
+
default:
|
|
1910
|
+
return "'";
|
|
1911
|
+
}
|
|
1912
|
+
});
|
|
1913
|
+
const el = (id, kind) => {
|
|
1914
|
+
const node = document.getElementById(id);
|
|
1915
|
+
if (!(node instanceof kind))
|
|
1916
|
+
throw new Error(`admin shell is missing #${id}`);
|
|
1917
|
+
return node;
|
|
1918
|
+
};
|
|
1919
|
+
const errorMessage = (data, fallback) => {
|
|
1920
|
+
if (!isRecord2(data))
|
|
1921
|
+
return fallback;
|
|
1922
|
+
const error = data.error;
|
|
1923
|
+
if (!isRecord2(error) || typeof error.message !== "string" || error.message === "") {
|
|
1924
|
+
return fallback;
|
|
1925
|
+
}
|
|
1926
|
+
return error.message;
|
|
1927
|
+
};
|
|
1928
|
+
const requireList = (value, label) => {
|
|
1929
|
+
if (value === void 0 || value === null)
|
|
1930
|
+
return [];
|
|
1931
|
+
if (!Array.isArray(value))
|
|
1932
|
+
throw new Error(`${label} was not a list`);
|
|
1933
|
+
return value;
|
|
1934
|
+
};
|
|
1935
|
+
const state = {
|
|
1936
|
+
namespace: "default",
|
|
1937
|
+
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1938
|
+
view: "overview",
|
|
1939
|
+
collection: "",
|
|
1940
|
+
shape: null,
|
|
1941
|
+
route: null,
|
|
1942
|
+
editing: null
|
|
1943
|
+
};
|
|
1944
|
+
const standard = new Set(config.standardRoutes);
|
|
1945
|
+
const banner = el("banner", HTMLElement);
|
|
1946
|
+
const keyInput = el("admin-key", HTMLInputElement);
|
|
1947
|
+
keyInput.value = state.key;
|
|
1948
|
+
const params = new URLSearchParams(location.search);
|
|
1949
|
+
const queryKey = params.get("key");
|
|
1950
|
+
if (queryKey) {
|
|
1951
|
+
state.key = queryKey;
|
|
1952
|
+
keyInput.value = state.key;
|
|
1953
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
1954
|
+
params.delete("key");
|
|
1955
|
+
const next = params.toString();
|
|
1956
|
+
history.replaceState(null, "", location.pathname + (next ? `?${next}` : ""));
|
|
1957
|
+
}
|
|
1958
|
+
const queryNamespace = params.get("namespace");
|
|
1959
|
+
if (queryNamespace)
|
|
1960
|
+
state.namespace = queryNamespace;
|
|
1961
|
+
const showError = (error) => {
|
|
1962
|
+
banner.textContent = error instanceof Error ? error.message : String(error);
|
|
1963
|
+
banner.classList.add("show");
|
|
1964
|
+
};
|
|
1965
|
+
const clearError = () => {
|
|
1966
|
+
banner.classList.remove("show");
|
|
1967
|
+
};
|
|
1968
|
+
const api = async (method, path, body) => {
|
|
1969
|
+
const headers = { accept: "application/json" };
|
|
1970
|
+
if (state.key !== "")
|
|
1971
|
+
headers[config.adminKeyHeader] = state.key;
|
|
1972
|
+
if (state.namespace !== "")
|
|
1973
|
+
headers["x-mockingbird-namespace"] = state.namespace;
|
|
1974
|
+
if (body !== void 0)
|
|
1975
|
+
headers["content-type"] = "application/json";
|
|
1976
|
+
const init = { method, headers };
|
|
1977
|
+
if (body !== void 0)
|
|
1978
|
+
init.body = JSON.stringify(body);
|
|
1979
|
+
const response = await fetch(config.adminPrefix + path, init);
|
|
1980
|
+
const text = await response.text();
|
|
1981
|
+
let data = null;
|
|
1982
|
+
if (text !== "") {
|
|
1983
|
+
try {
|
|
1984
|
+
data = readJson2(text);
|
|
1985
|
+
} catch {
|
|
1986
|
+
data = { raw: text };
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
if (response.status === 401) {
|
|
1990
|
+
keyInput.focus();
|
|
1991
|
+
throw new Error(errorMessage(data, "Admin key required"));
|
|
1992
|
+
}
|
|
1993
|
+
if (!response.ok) {
|
|
1994
|
+
throw new Error(errorMessage(data, `${response.status} ${method} ${path}`));
|
|
1995
|
+
}
|
|
1996
|
+
return data;
|
|
1997
|
+
};
|
|
1998
|
+
const originGet = async (path) => {
|
|
1999
|
+
const response = await fetch(path, { headers: { accept: "application/json" } });
|
|
2000
|
+
if (!response.ok)
|
|
2001
|
+
throw new Error(`${response.status} ${path}`);
|
|
2002
|
+
const value = await response.json();
|
|
2003
|
+
return value;
|
|
2004
|
+
};
|
|
2005
|
+
const panelApi = {
|
|
2006
|
+
get: (path) => api("GET", path),
|
|
2007
|
+
send: (method, path, body) => api(method, path, body),
|
|
2008
|
+
namespace: () => state.namespace
|
|
2009
|
+
};
|
|
2010
|
+
const isPanelScript = (value) => typeof value === "function";
|
|
2011
|
+
const setTheme = (theme) => {
|
|
2012
|
+
if (theme)
|
|
2013
|
+
document.documentElement.dataset.theme = theme;
|
|
2014
|
+
else
|
|
2015
|
+
delete document.documentElement.dataset.theme;
|
|
2016
|
+
};
|
|
2017
|
+
const storedTheme = localStorage.getItem("mockingbird-admin-theme");
|
|
2018
|
+
if (storedTheme)
|
|
2019
|
+
setTheme(storedTheme);
|
|
2020
|
+
el("theme", HTMLButtonElement).addEventListener("click", () => {
|
|
2021
|
+
const current = document.documentElement.dataset.theme;
|
|
2022
|
+
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches;
|
|
2023
|
+
const next = dark ? "light" : "dark";
|
|
2024
|
+
localStorage.setItem("mockingbird-admin-theme", next);
|
|
2025
|
+
setTheme(next);
|
|
2026
|
+
});
|
|
2027
|
+
const show = (view) => {
|
|
2028
|
+
state.view = view;
|
|
2029
|
+
for (const button of document.querySelectorAll("#nav button")) {
|
|
2030
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2031
|
+
continue;
|
|
2032
|
+
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false");
|
|
2033
|
+
}
|
|
2034
|
+
for (const section of document.querySelectorAll("main .view")) {
|
|
2035
|
+
if (!(section instanceof HTMLElement))
|
|
2036
|
+
continue;
|
|
2037
|
+
section.hidden = section.id !== `view-${view}`;
|
|
2038
|
+
}
|
|
2039
|
+
};
|
|
2040
|
+
el("nav", HTMLElement).addEventListener("click", (event) => {
|
|
2041
|
+
const target = event.target;
|
|
2042
|
+
if (!(target instanceof Element))
|
|
2043
|
+
return;
|
|
2044
|
+
const button = target.closest("button[data-view]");
|
|
2045
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2046
|
+
return;
|
|
2047
|
+
const view = button.dataset.view;
|
|
2048
|
+
if (view === void 0)
|
|
2049
|
+
return;
|
|
2050
|
+
show(view);
|
|
2051
|
+
refresh().catch(showError);
|
|
2052
|
+
});
|
|
2053
|
+
const cards = (items) => `<div class="grid">${items.map((item) => `<article class="card"><div class="k">${item.k}</div><div class="v">${item.v}</div></article>`).join("")}</div>`;
|
|
2054
|
+
const table = (columns, rows) => {
|
|
2055
|
+
if (rows.length === 0)
|
|
2056
|
+
return '<p class="empty">Nothing here yet.</p>';
|
|
2057
|
+
const head = `<tr>${columns.map((column) => `<th>${column.label}</th>`).join("")}</tr>`;
|
|
2058
|
+
const body = rows.map((row) => {
|
|
2059
|
+
const cells = columns.map((column) => `<td data-label="${column.label}">${column.cell(row)}</td>`).join("");
|
|
2060
|
+
const attrs = "attrs" in row && typeof row.attrs === "string" ? row.attrs : "";
|
|
2061
|
+
return `<tr${attrs}>${cells}</tr>`;
|
|
2062
|
+
}).join("");
|
|
2063
|
+
return `<table><thead>${head}</thead><tbody>${body}</tbody></table>`;
|
|
2064
|
+
};
|
|
2065
|
+
const preview = (value) => {
|
|
2066
|
+
const text = JSON.stringify(value) ?? "null";
|
|
2067
|
+
return esc(text.length > 180 ? `${text.slice(0, 180)}\u2026` : text);
|
|
2068
|
+
};
|
|
2069
|
+
const when = (epoch) => {
|
|
2070
|
+
const date = new Date(epoch);
|
|
2071
|
+
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString();
|
|
2072
|
+
};
|
|
2073
|
+
const clockCard = (epoch) => {
|
|
2074
|
+
const date = new Date(epoch);
|
|
2075
|
+
if (Number.isNaN(date.getTime()))
|
|
2076
|
+
return esc(String(epoch));
|
|
2077
|
+
const iso = date.toISOString();
|
|
2078
|
+
return `${esc(iso.slice(0, 10))}<span class="clock-time">${esc(iso.slice(11, 19))} UTC</span>`;
|
|
2079
|
+
};
|
|
2080
|
+
const decodeClock = (value) => {
|
|
2081
|
+
if (!isRecord2(value) || typeof value.now !== "number" || typeof value.offsetMs !== "number" || typeof value.frozen !== "boolean") {
|
|
2082
|
+
throw new Error("Clock response was not a clock");
|
|
2083
|
+
}
|
|
2084
|
+
return { now: value.now, offsetMs: value.offsetMs, frozen: value.frozen };
|
|
2085
|
+
};
|
|
2086
|
+
const decodeField = (value) => {
|
|
2087
|
+
if (!isRecord2(value) || typeof value.name !== "string" || !isFieldKind(value.kind)) {
|
|
2088
|
+
return void 0;
|
|
2089
|
+
}
|
|
2090
|
+
const field = {
|
|
2091
|
+
name: value.name,
|
|
2092
|
+
kind: value.kind,
|
|
2093
|
+
optional: value.optional === true
|
|
2094
|
+
};
|
|
2095
|
+
if (typeof value.description === "string")
|
|
2096
|
+
field.description = value.description;
|
|
2097
|
+
if (Array.isArray(value.fields)) {
|
|
2098
|
+
field.fields = value.fields.flatMap((item) => {
|
|
2099
|
+
const child = decodeField(item);
|
|
2100
|
+
return child === void 0 ? [] : [child];
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
return field;
|
|
2104
|
+
};
|
|
2105
|
+
const decodeCollection = (value) => {
|
|
2106
|
+
if (!isRecord2(value) || typeof value.name !== "string")
|
|
2107
|
+
return void 0;
|
|
2108
|
+
const fields = Array.isArray(value.fields) ? value.fields.flatMap((item) => {
|
|
2109
|
+
const field = decodeField(item);
|
|
2110
|
+
return field === void 0 ? [] : [field];
|
|
2111
|
+
}) : [];
|
|
2112
|
+
const collection = {
|
|
2113
|
+
name: value.name,
|
|
2114
|
+
label: typeof value.label === "string" ? value.label : value.name,
|
|
2115
|
+
count: typeof value.count === "number" ? value.count : 0,
|
|
2116
|
+
source: isSource(value.source) ? value.source : "inferred",
|
|
2117
|
+
fields
|
|
2118
|
+
};
|
|
2119
|
+
if (typeof value.description === "string")
|
|
2120
|
+
collection.description = value.description;
|
|
2121
|
+
return collection;
|
|
2122
|
+
};
|
|
2123
|
+
const decodeState = (value) => {
|
|
2124
|
+
if (!isRecord2(value) || !Array.isArray(value.collections)) {
|
|
2125
|
+
throw new Error("State response has no collection list");
|
|
2126
|
+
}
|
|
2127
|
+
return {
|
|
2128
|
+
namespace: typeof value.namespace === "string" ? value.namespace : state.namespace,
|
|
2129
|
+
storageNamespace: typeof value.storageNamespace === "string" ? value.storageNamespace : "",
|
|
2130
|
+
collections: value.collections.flatMap((item) => {
|
|
2131
|
+
const collection = decodeCollection(item);
|
|
2132
|
+
return collection === void 0 ? [] : [collection];
|
|
2133
|
+
})
|
|
2134
|
+
};
|
|
2135
|
+
};
|
|
2136
|
+
const decodeRecords = (value) => requireList(isRecord2(value) ? value.records : void 0, "Records").flatMap((item) => {
|
|
2137
|
+
if (!isRecord2(item) || typeof item.id !== "string")
|
|
2138
|
+
return [];
|
|
2139
|
+
return [
|
|
2140
|
+
{
|
|
2141
|
+
id: item.id,
|
|
2142
|
+
seq: typeof item.seq === "number" ? item.seq : 0,
|
|
2143
|
+
value: item.value,
|
|
2144
|
+
attrs: ` class="clickable" data-id="${esc(item.id)}"`
|
|
2145
|
+
}
|
|
2146
|
+
];
|
|
2147
|
+
});
|
|
2148
|
+
const textOf = (value) => typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? String(value) : "";
|
|
2149
|
+
const decodeJournal = (value) => requireList(isRecord2(value) ? value.requests : void 0, "Journal").flatMap((item) => {
|
|
2150
|
+
if (!isRecord2(item))
|
|
2151
|
+
return [];
|
|
2152
|
+
return [
|
|
2153
|
+
{
|
|
2154
|
+
at: textOf(item.at),
|
|
2155
|
+
method: textOf(item.method),
|
|
2156
|
+
path: textOf(item.path),
|
|
2157
|
+
status: textOf(item.status),
|
|
2158
|
+
durationMs: textOf(item.durationMs),
|
|
2159
|
+
operationId: textOf(item.operationId)
|
|
2160
|
+
}
|
|
2161
|
+
];
|
|
2162
|
+
});
|
|
2163
|
+
const decodeFaults = (value) => requireList(isRecord2(value) ? value.faults : void 0, "Faults").flatMap((item) => {
|
|
2164
|
+
if (!isRecord2(item))
|
|
2165
|
+
return [];
|
|
2166
|
+
return [
|
|
2167
|
+
{
|
|
2168
|
+
id: textOf(item.id),
|
|
2169
|
+
method: typeof item.method === "string" ? item.method : void 0,
|
|
2170
|
+
pathPrefix: typeof item.pathPrefix === "string" ? item.pathPrefix : void 0,
|
|
2171
|
+
operationId: typeof item.operationId === "string" ? item.operationId : void 0,
|
|
2172
|
+
status: typeof item.status === "number" ? item.status : void 0,
|
|
2173
|
+
effect: typeof item.effect === "string" ? item.effect : void 0,
|
|
2174
|
+
drop: typeof item.drop === "boolean" ? item.drop : void 0,
|
|
2175
|
+
latencyMs: typeof item.latencyMs === "number" ? item.latencyMs : void 0
|
|
2176
|
+
}
|
|
2177
|
+
];
|
|
2178
|
+
});
|
|
2179
|
+
const loadNamespaces = async () => {
|
|
2180
|
+
const data = await api("GET", "/namespaces");
|
|
2181
|
+
const select = el("namespace", HTMLSelectElement);
|
|
2182
|
+
const listed = requireList(isRecord2(data) ? data.namespaces : void 0, "Namespaces");
|
|
2183
|
+
const fallback = isRecord2(data) && typeof data.default === "string" && data.default !== "" ? data.default : "default";
|
|
2184
|
+
const names = (listed.length > 0 ? listed.map((name) => String(name)) : [fallback]).slice();
|
|
2185
|
+
if (!names.includes(state.namespace))
|
|
2186
|
+
names.unshift(state.namespace);
|
|
2187
|
+
select.innerHTML = names.map((name) => `<option value="${esc(name)}">${esc(name)}</option>`).join("");
|
|
2188
|
+
select.value = state.namespace;
|
|
2189
|
+
};
|
|
2190
|
+
const renderOverview = async () => {
|
|
2191
|
+
const [health, shape, clock, journal] = await Promise.all([
|
|
2192
|
+
originGet("/health"),
|
|
2193
|
+
api("GET", "/state"),
|
|
2194
|
+
api("GET", "/clock"),
|
|
2195
|
+
api("GET", "/requests?limit=8")
|
|
2196
|
+
]);
|
|
2197
|
+
if (!isRecord2(health))
|
|
2198
|
+
throw new Error("Health response was not an object");
|
|
2199
|
+
const decoded = decodeState(shape);
|
|
2200
|
+
state.shape = decoded;
|
|
2201
|
+
const time = decodeClock(clock);
|
|
2202
|
+
el("overview-cards", HTMLElement).innerHTML = cards([
|
|
2203
|
+
{
|
|
2204
|
+
k: "Service",
|
|
2205
|
+
v: esc(typeof health.service === "string" && health.service !== "" ? health.service : config.service)
|
|
2206
|
+
},
|
|
2207
|
+
{
|
|
2208
|
+
k: "Status",
|
|
2209
|
+
v: esc(typeof health.status === "string" && health.status !== "" ? health.status : "ok")
|
|
2210
|
+
},
|
|
2211
|
+
{ k: "Namespace", v: esc(state.namespace) },
|
|
2212
|
+
{ k: "Collections", v: String(decoded.collections.length) },
|
|
2213
|
+
{
|
|
2214
|
+
k: "Records",
|
|
2215
|
+
v: String(decoded.collections.reduce((sum, item) => sum + item.count, 0))
|
|
2216
|
+
},
|
|
2217
|
+
{ k: "Clock", v: clockCard(time.now) }
|
|
2218
|
+
]);
|
|
2219
|
+
el("overview-requests", HTMLElement).innerHTML = table([
|
|
2220
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2221
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2222
|
+
{
|
|
2223
|
+
label: "Status",
|
|
2224
|
+
cell: (row) => `<span class="status-${row.status.charAt(0)}">${esc(row.status)}</span>`
|
|
2225
|
+
},
|
|
2226
|
+
{ label: "Operation", cell: (row) => esc(row.operationId) }
|
|
2227
|
+
], decodeJournal(journal));
|
|
2228
|
+
};
|
|
2229
|
+
const schemaChips = (collection) => {
|
|
2230
|
+
if (collection.fields.length === 0) {
|
|
2231
|
+
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>';
|
|
2232
|
+
}
|
|
2233
|
+
return `<div class="chips">${collection.fields.map((field) => `<span class="chip">${esc(field.name)} <i>${esc(field.kind)}${field.optional ? "?" : ""}</i></span>`).join("")}</div>`;
|
|
2234
|
+
};
|
|
2235
|
+
const renderState = async () => {
|
|
2236
|
+
const shape = decodeState(await api("GET", "/state"));
|
|
2237
|
+
state.shape = shape;
|
|
2238
|
+
const host = el("collections", HTMLElement);
|
|
2239
|
+
if (shape.collections.length === 0) {
|
|
2240
|
+
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>';
|
|
2241
|
+
el("schema", HTMLElement).innerHTML = "";
|
|
2242
|
+
el("records", HTMLElement).innerHTML = "";
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
2246
|
+
const first = shape.collections[0];
|
|
2247
|
+
if (first === void 0)
|
|
2248
|
+
return;
|
|
2249
|
+
state.collection = first.name;
|
|
2250
|
+
}
|
|
2251
|
+
host.innerHTML = shape.collections.map((item) => `<button type="button" class="collection" data-collection="${esc(item.name)}" aria-current="${item.name === state.collection ? "true" : "false"}"><strong>${esc(item.label)}</strong><small>${esc(item.count)} \xB7 ${esc(item.source)}</small></button>`).join("");
|
|
2252
|
+
const selected = shape.collections.find((item) => item.name === state.collection);
|
|
2253
|
+
if (selected === void 0)
|
|
2254
|
+
return;
|
|
2255
|
+
const description = selected.description ? `<p class="lede">${esc(selected.description)}</p>` : "";
|
|
2256
|
+
el("schema", HTMLElement).innerHTML = `<h3>${esc(selected.label)}</h3>${description}${schemaChips(selected)}`;
|
|
2257
|
+
const page2 = await api("GET", `/state/${encodeURIComponent(state.collection)}?limit=50`);
|
|
2258
|
+
el("records", HTMLElement).innerHTML = table([
|
|
2259
|
+
{ label: "Id", cell: (row) => `<code>${esc(row.id)}</code>` },
|
|
2260
|
+
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
2261
|
+
{ label: "Value", cell: (row) => preview(row.value) }
|
|
2262
|
+
], decodeRecords(page2));
|
|
2263
|
+
};
|
|
2264
|
+
const renderClock = async () => {
|
|
2265
|
+
const clock = decodeClock(await api("GET", "/clock"));
|
|
2266
|
+
el("clock-readout", HTMLElement).innerHTML = `<div class="k">Now</div><div class="v">${esc(when(clock.now))}</div><p>${esc(clock.now)} ms \xB7 offset ${esc(clock.offsetMs)} ms ${clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>"}</p>`;
|
|
2267
|
+
const freeze = el("freeze", HTMLButtonElement);
|
|
2268
|
+
freeze.textContent = clock.frozen ? "Unfreeze" : "Freeze";
|
|
2269
|
+
freeze.dataset.frozen = clock.frozen ? "1" : "0";
|
|
2270
|
+
};
|
|
2271
|
+
const renderFaults = async () => {
|
|
2272
|
+
const [faults, presets] = await Promise.all([
|
|
2273
|
+
api("GET", "/faults"),
|
|
2274
|
+
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
2275
|
+
]);
|
|
2276
|
+
const select = el("fault-preset", HTMLSelectElement);
|
|
2277
|
+
const current = select.value;
|
|
2278
|
+
const list = faultPresetList(presets).presets;
|
|
2279
|
+
select.innerHTML = `<option value="">Custom rule</option>${list.map((preset) => `<option value="${esc(preset.name)}">${esc(preset.name)}</option>`).join("")}`;
|
|
2280
|
+
select.value = current;
|
|
2281
|
+
el("fault-list", HTMLElement).innerHTML = table([
|
|
2282
|
+
{ label: "Id", cell: (row) => esc(row.id) },
|
|
2283
|
+
{
|
|
2284
|
+
label: "Match",
|
|
2285
|
+
cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter((part) => Boolean(part)).join(" ") || "all")
|
|
2286
|
+
},
|
|
2287
|
+
{
|
|
2288
|
+
label: "Effect",
|
|
2289
|
+
cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || ""))
|
|
2290
|
+
}
|
|
2291
|
+
], decodeFaults(faults));
|
|
2292
|
+
};
|
|
2293
|
+
const renderJournal = async () => {
|
|
2294
|
+
const journal = await api("GET", "/requests?limit=100");
|
|
2295
|
+
el("journal", HTMLElement).innerHTML = table([
|
|
2296
|
+
{ label: "When", cell: (row) => esc(row.at) },
|
|
2297
|
+
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2298
|
+
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2299
|
+
{ label: "Status", cell: (row) => esc(row.status) },
|
|
2300
|
+
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2301
|
+
], decodeJournal(journal));
|
|
2302
|
+
};
|
|
2303
|
+
const renderRoutes = async () => {
|
|
2304
|
+
const data = await api("GET", "/");
|
|
2305
|
+
if (!isRecord2(data))
|
|
2306
|
+
throw new Error("Route list was not an object");
|
|
2307
|
+
const routes = requireList(data.routes, "Routes").map((route) => String(route));
|
|
2308
|
+
el("route-list", HTMLElement).innerHTML = routes.map((route) => {
|
|
2309
|
+
const extra = standard.has(route) ? "" : " <span class='badge'>extension</span>";
|
|
2310
|
+
const parts = route.split(" ");
|
|
2311
|
+
const method = parts[0] ?? "";
|
|
2312
|
+
const path = parts.slice(1).join(" ");
|
|
2313
|
+
return `<div class="route"><span class="method">${esc(method)}</span><span>${esc(path)}${extra}</span><button class="btn" type="button" data-route="${esc(route)}">Use</button></div>`;
|
|
2314
|
+
}).join("");
|
|
2315
|
+
};
|
|
2316
|
+
const renderers = {
|
|
2317
|
+
overview: renderOverview,
|
|
2318
|
+
state: renderState,
|
|
2319
|
+
clock: renderClock,
|
|
2320
|
+
faults: renderFaults,
|
|
2321
|
+
journal: renderJournal,
|
|
2322
|
+
routes: renderRoutes
|
|
2323
|
+
};
|
|
2324
|
+
async function refresh() {
|
|
2325
|
+
clearError();
|
|
2326
|
+
const render = renderers[state.view];
|
|
2327
|
+
if (render)
|
|
2328
|
+
await render();
|
|
2329
|
+
}
|
|
2330
|
+
el("namespace", HTMLSelectElement).addEventListener("change", () => {
|
|
2331
|
+
state.namespace = el("namespace", HTMLSelectElement).value;
|
|
2332
|
+
refresh().catch(showError);
|
|
2333
|
+
});
|
|
2334
|
+
keyInput.addEventListener("change", () => {
|
|
2335
|
+
state.key = keyInput.value.trim();
|
|
2336
|
+
sessionStorage.setItem("mockingbird-admin-key", state.key);
|
|
2337
|
+
refresh().catch(showError);
|
|
2338
|
+
});
|
|
2339
|
+
el("refresh", HTMLButtonElement).addEventListener("click", () => {
|
|
2340
|
+
refresh().catch(showError);
|
|
2341
|
+
});
|
|
2342
|
+
el("collections", HTMLElement).addEventListener("click", (event) => {
|
|
2343
|
+
const target = event.target;
|
|
2344
|
+
if (!(target instanceof Element))
|
|
2345
|
+
return;
|
|
2346
|
+
const button = target.closest("[data-collection]");
|
|
2347
|
+
if (!(button instanceof HTMLElement))
|
|
2348
|
+
return;
|
|
2349
|
+
const collection = button.dataset.collection;
|
|
2350
|
+
if (collection === void 0)
|
|
2351
|
+
return;
|
|
2352
|
+
state.collection = collection;
|
|
2353
|
+
renderState().catch(showError);
|
|
2354
|
+
});
|
|
2355
|
+
el("records", HTMLElement).addEventListener("click", (event) => {
|
|
2356
|
+
const target = event.target;
|
|
2357
|
+
if (!(target instanceof Element))
|
|
2358
|
+
return;
|
|
2359
|
+
const row = target.closest("[data-id]");
|
|
2360
|
+
if (!(row instanceof HTMLElement))
|
|
2361
|
+
return;
|
|
2362
|
+
const id = row.dataset.id;
|
|
2363
|
+
if (id === void 0)
|
|
2364
|
+
return;
|
|
2365
|
+
openEditor(id).catch(showError);
|
|
2366
|
+
});
|
|
2367
|
+
const editor = el("editor", HTMLDialogElement);
|
|
2368
|
+
const editorId = el("editor-id", HTMLInputElement);
|
|
2369
|
+
const editorValue = el("editor-value", HTMLTextAreaElement);
|
|
2370
|
+
const editorDelete = el("editor-delete", HTMLButtonElement);
|
|
2371
|
+
async function openEditor(id) {
|
|
2372
|
+
state.editing = id;
|
|
2373
|
+
el("editor-title", HTMLElement).textContent = id ? "Edit record" : "New record";
|
|
2374
|
+
editorId.value = id ?? "";
|
|
2375
|
+
editorId.readOnly = id !== null;
|
|
2376
|
+
editorDelete.hidden = id === null;
|
|
2377
|
+
if (id) {
|
|
2378
|
+
const record = await api("GET", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`);
|
|
2379
|
+
if (!isRecord2(record))
|
|
2380
|
+
throw new Error("Record response was not an object");
|
|
2381
|
+
editorValue.value = JSON.stringify(record.value, null, 2) ?? "";
|
|
2382
|
+
} else {
|
|
2383
|
+
editorValue.value = "{\n \n}";
|
|
2384
|
+
}
|
|
2385
|
+
editor.showModal();
|
|
2386
|
+
}
|
|
2387
|
+
el("new-record", HTMLButtonElement).addEventListener("click", () => {
|
|
2388
|
+
openEditor(null).catch(showError);
|
|
2389
|
+
});
|
|
2390
|
+
el("editor-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2391
|
+
const submitter = event.submitter;
|
|
2392
|
+
if (!(submitter instanceof HTMLButtonElement) || submitter.value !== "save")
|
|
2393
|
+
return;
|
|
2394
|
+
event.preventDefault();
|
|
2395
|
+
let value;
|
|
2396
|
+
try {
|
|
2397
|
+
value = readJson2(editorValue.value);
|
|
2398
|
+
} catch {
|
|
2399
|
+
showError(new Error("Value is not JSON"));
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
const id = editorId.value.trim();
|
|
2403
|
+
const path = `/state/${encodeURIComponent(state.collection)}`;
|
|
2404
|
+
const request = state.editing ? api("PUT", `${path}/${encodeURIComponent(id)}`, { value }) : api("POST", path, { id: id || void 0, value });
|
|
2405
|
+
request.then(() => {
|
|
2406
|
+
editor.close();
|
|
2407
|
+
return renderState();
|
|
2408
|
+
}).catch(showError);
|
|
2409
|
+
});
|
|
2410
|
+
editorDelete.addEventListener("click", () => {
|
|
2411
|
+
const id = state.editing;
|
|
2412
|
+
if (!id)
|
|
2413
|
+
return;
|
|
2414
|
+
api("DELETE", `/state/${encodeURIComponent(state.collection)}/${encodeURIComponent(id)}`).then(() => {
|
|
2415
|
+
editor.close();
|
|
2416
|
+
return renderState();
|
|
2417
|
+
}).catch(showError);
|
|
2418
|
+
});
|
|
2419
|
+
for (const button of document.querySelectorAll("[data-advance]")) {
|
|
2420
|
+
if (!(button instanceof HTMLButtonElement))
|
|
2421
|
+
continue;
|
|
2422
|
+
button.addEventListener("click", () => {
|
|
2423
|
+
const raw = button.dataset.advance;
|
|
2424
|
+
if (raw === void 0)
|
|
2425
|
+
return;
|
|
2426
|
+
api("POST", "/clock", { advance: Number(raw) }).then(() => renderClock()).catch(showError);
|
|
2427
|
+
});
|
|
2428
|
+
}
|
|
2429
|
+
el("freeze", HTMLButtonElement).addEventListener("click", () => {
|
|
2430
|
+
const frozen = el("freeze", HTMLButtonElement).dataset.frozen === "1";
|
|
2431
|
+
api("POST", "/clock", { freeze: !frozen }).then(() => renderClock()).catch(showError);
|
|
2432
|
+
});
|
|
2433
|
+
el("clock-reset", HTMLButtonElement).addEventListener("click", () => {
|
|
2434
|
+
api("POST", "/clock", { reset: true }).then(() => renderClock()).catch(showError);
|
|
2435
|
+
});
|
|
2436
|
+
el("clock-set", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2437
|
+
event.preventDefault();
|
|
2438
|
+
const raw = el("clock-instant", HTMLInputElement).value.trim();
|
|
2439
|
+
const set = /^-?\d+$/.test(raw) ? Number(raw) : raw;
|
|
2440
|
+
api("POST", "/clock", { set }).then(() => renderClock()).catch(showError);
|
|
2441
|
+
});
|
|
2442
|
+
el("fault-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2443
|
+
event.preventDefault();
|
|
2444
|
+
const preset = el("fault-preset", HTMLSelectElement).value;
|
|
2445
|
+
if (preset !== "") {
|
|
2446
|
+
api("POST", "/faults", { preset }).then(() => renderFaults()).catch(showError);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
const status = Number(el("fault-status", HTMLInputElement).value);
|
|
2450
|
+
if (!Number.isFinite(status)) {
|
|
2451
|
+
showError(new Error("Status must be a number, or choose a preset"));
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const path = el("fault-path", HTMLInputElement).value;
|
|
2455
|
+
const method = el("fault-method", HTMLInputElement).value;
|
|
2456
|
+
api("POST", "/faults", {
|
|
2457
|
+
status,
|
|
2458
|
+
...path !== "" ? { pathPrefix: path } : {},
|
|
2459
|
+
...method !== "" ? { method } : {}
|
|
2460
|
+
}).then(() => renderFaults()).catch(showError);
|
|
2461
|
+
});
|
|
2462
|
+
el("fault-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2463
|
+
api("DELETE", "/faults").then(() => renderFaults()).catch(showError);
|
|
2464
|
+
});
|
|
2465
|
+
el("journal-clear", HTMLButtonElement).addEventListener("click", () => {
|
|
2466
|
+
api("DELETE", "/requests").then(() => renderJournal()).catch(showError);
|
|
2467
|
+
});
|
|
2468
|
+
el("route-list", HTMLElement).addEventListener("click", (event) => {
|
|
2469
|
+
const target = event.target;
|
|
2470
|
+
if (!(target instanceof Element))
|
|
2471
|
+
return;
|
|
2472
|
+
const button = target.closest("[data-route]");
|
|
2473
|
+
if (!(button instanceof HTMLElement))
|
|
2474
|
+
return;
|
|
2475
|
+
const route = button.dataset.route;
|
|
2476
|
+
if (route === void 0)
|
|
2477
|
+
return;
|
|
2478
|
+
state.route = route;
|
|
2479
|
+
el("route-title", HTMLElement).textContent = route;
|
|
2480
|
+
const method = route.split(" ")[0] ?? "";
|
|
2481
|
+
el("route-body", HTMLTextAreaElement).disabled = method === "GET" || method === "DELETE";
|
|
2482
|
+
});
|
|
2483
|
+
el("route-form", HTMLFormElement).addEventListener("submit", (event) => {
|
|
2484
|
+
event.preventDefault();
|
|
2485
|
+
const send = async () => {
|
|
2486
|
+
if (state.route === null)
|
|
2487
|
+
throw new Error("Choose a route first");
|
|
2488
|
+
const parts = state.route.split(" ");
|
|
2489
|
+
const method = parts[0];
|
|
2490
|
+
const path = parts.slice(1).join(" ");
|
|
2491
|
+
if (method === void 0)
|
|
2492
|
+
throw new Error("Choose a route first");
|
|
2493
|
+
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_match, key) => prompt(`Value for ${key}`) || "");
|
|
2494
|
+
let body;
|
|
2495
|
+
const routeBody = el("route-body", HTMLTextAreaElement);
|
|
2496
|
+
if (method !== "GET" && method !== "DELETE" && routeBody.value.trim() !== "") {
|
|
2497
|
+
try {
|
|
2498
|
+
body = readJson2(routeBody.value);
|
|
2499
|
+
} catch {
|
|
2500
|
+
throw new Error("Body is not JSON");
|
|
2501
|
+
}
|
|
2502
|
+
}
|
|
2503
|
+
const data = await api(method, concrete, body);
|
|
2504
|
+
el("route-result", HTMLElement).textContent = JSON.stringify(data, null, 2);
|
|
2505
|
+
};
|
|
2506
|
+
send().catch(showError);
|
|
2507
|
+
});
|
|
2508
|
+
const decodePanel = (value) => {
|
|
2509
|
+
if (!isRecord2(value) || typeof value.id !== "string" || typeof value.title !== "string") {
|
|
2510
|
+
return void 0;
|
|
2511
|
+
}
|
|
2512
|
+
const panel = {
|
|
2513
|
+
id: value.id,
|
|
2514
|
+
title: value.title,
|
|
2515
|
+
description: typeof value.description === "string" ? value.description : "",
|
|
2516
|
+
html: typeof value.html === "string" ? value.html : ""
|
|
2517
|
+
};
|
|
2518
|
+
if (typeof value.script === "string")
|
|
2519
|
+
panel.script = value.script;
|
|
2520
|
+
return panel;
|
|
2521
|
+
};
|
|
2522
|
+
const mountPanels = (panels) => {
|
|
2523
|
+
const nav = el("nav", HTMLElement);
|
|
2524
|
+
const views = el("views", HTMLElement);
|
|
2525
|
+
for (const panel of panels) {
|
|
2526
|
+
const button = document.createElement("button");
|
|
2527
|
+
button.type = "button";
|
|
2528
|
+
button.dataset.view = panel.id;
|
|
2529
|
+
button.textContent = panel.title;
|
|
2530
|
+
nav.append(button);
|
|
2531
|
+
const section = document.createElement("section");
|
|
2532
|
+
section.className = "view";
|
|
2533
|
+
section.id = `view-${panel.id}`;
|
|
2534
|
+
section.hidden = true;
|
|
2535
|
+
const title = document.createElement("h2");
|
|
2536
|
+
title.textContent = panel.title;
|
|
2537
|
+
const lede = document.createElement("p");
|
|
2538
|
+
lede.className = "lede";
|
|
2539
|
+
lede.textContent = panel.description;
|
|
2540
|
+
const body = document.createElement("div");
|
|
2541
|
+
body.className = "panel-body";
|
|
2542
|
+
body.id = `panel-${panel.id}`;
|
|
2543
|
+
body.innerHTML = panel.html;
|
|
2544
|
+
section.append(title, lede, body);
|
|
2545
|
+
views.append(section);
|
|
2546
|
+
if (panel.script === void 0)
|
|
2547
|
+
continue;
|
|
2548
|
+
try {
|
|
2549
|
+
const compiled = new Function("root", "api", panel.script);
|
|
2550
|
+
if (!isPanelScript(compiled))
|
|
2551
|
+
throw new Error("panel script did not compile");
|
|
2552
|
+
compiled(body, panelApi);
|
|
2553
|
+
} catch (error) {
|
|
2554
|
+
const note = document.createElement("p");
|
|
2555
|
+
note.className = "banner show";
|
|
2556
|
+
note.textContent = error instanceof Error ? error.message : String(error);
|
|
2557
|
+
body.append(note);
|
|
2558
|
+
}
|
|
2559
|
+
}
|
|
2560
|
+
};
|
|
2561
|
+
startAdminBrand({
|
|
2562
|
+
document,
|
|
2563
|
+
$: (id) => document.getElementById(id),
|
|
2564
|
+
params,
|
|
2565
|
+
location: { href: location.href, origin: location.origin },
|
|
2566
|
+
fetch,
|
|
2567
|
+
brandsUrl: config.brandsUrl
|
|
2568
|
+
});
|
|
2569
|
+
loadNamespaces().then(() => api("GET", "/ui/manifest")).then((manifest) => {
|
|
2570
|
+
const panels = requireList(isRecord2(manifest) ? manifest.panels : void 0, "Panels").flatMap((panel) => {
|
|
2571
|
+
const decoded = decodePanel(panel);
|
|
2572
|
+
return decoded === void 0 ? [] : [decoded];
|
|
2573
|
+
});
|
|
2574
|
+
mountPanels(panels);
|
|
2575
|
+
return refresh();
|
|
2576
|
+
}).catch(showError);
|
|
2577
|
+
}
|
|
2578
|
+
var clientPrelude = [isRecord2, mountAdminBrand, startAdminBrand, faultPresetList, bootAdmin].map((fn) => fn.toString()).join("\n");
|
|
2579
|
+
var adminClientSource = (config) => {
|
|
2580
|
+
const payload = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
2581
|
+
return `<script>
|
|
2582
|
+
${clientPrelude}
|
|
2583
|
+
bootAdmin(${payload});
|
|
2584
|
+
</script>`;
|
|
2585
|
+
};
|
|
2586
|
+
|
|
1361
2587
|
// ../core/dist/surface.js
|
|
1362
2588
|
var defineMock = (runtime) => runtime;
|
|
1363
2589
|
var STANDARD_ADMIN_ROUTES = [
|
|
@@ -1426,94 +2652,106 @@ var escapeHtml = (value) => value.replace(/[&<>"']/g, (char) => {
|
|
|
1426
2652
|
return "'";
|
|
1427
2653
|
}
|
|
1428
2654
|
});
|
|
2655
|
+
var ADMIN_BRANDS_URL = "https://mockingbird.chrisvouga.dev/brands.json";
|
|
1429
2656
|
var renderAdminDocument = (service) => {
|
|
1430
2657
|
const name = escapeHtml(service);
|
|
1431
|
-
const
|
|
2658
|
+
const script = adminClientSource({
|
|
2659
|
+
standardRoutes: STANDARD_ADMIN_ROUTES,
|
|
2660
|
+
adminPrefix: ADMIN_PREFIX,
|
|
2661
|
+
adminKeyHeader: ADMIN_KEY_HEADER,
|
|
2662
|
+
brandsUrl: ADMIN_BRANDS_URL,
|
|
2663
|
+
service
|
|
2664
|
+
});
|
|
1432
2665
|
return `<!DOCTYPE html>
|
|
1433
2666
|
<html lang="en">
|
|
1434
2667
|
<head>
|
|
1435
2668
|
<meta charset="utf-8">
|
|
1436
2669
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1437
2670
|
<meta name="color-scheme" content="light dark">
|
|
1438
|
-
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='
|
|
2671
|
+
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Crect width='16' height='16' rx='2' fill='%2327272a'/%3E%3C/svg%3E">
|
|
1439
2672
|
<title>${name} admin</title>
|
|
1440
2673
|
<style>
|
|
2674
|
+
${CSS_RESET}
|
|
1441
2675
|
:root {
|
|
1442
2676
|
color-scheme: light;
|
|
1443
|
-
--bg: #
|
|
2677
|
+
--bg: #fafafa;
|
|
1444
2678
|
--bg-elev: #ffffff;
|
|
1445
|
-
--bg-muted: #
|
|
1446
|
-
--bg-inset: #
|
|
1447
|
-
--line: #
|
|
1448
|
-
--line-strong: #
|
|
1449
|
-
--ink: #
|
|
1450
|
-
--muted: #
|
|
1451
|
-
--faint: #
|
|
1452
|
-
--accent: #
|
|
1453
|
-
--accent-ink: #
|
|
1454
|
-
--accent-soft: #
|
|
1455
|
-
--ok: #
|
|
1456
|
-
--ok-soft: #
|
|
1457
|
-
--warn: #
|
|
1458
|
-
--warn-soft: #
|
|
1459
|
-
--err: #
|
|
1460
|
-
--err-soft: #
|
|
1461
|
-
--shadow:
|
|
1462
|
-
--sans: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2679
|
+
--bg-muted: #f4f4f5;
|
|
2680
|
+
--bg-inset: #f4f4f5;
|
|
2681
|
+
--line: #dedee3;
|
|
2682
|
+
--line-strong: #c4c4cc;
|
|
2683
|
+
--ink: #18181b;
|
|
2684
|
+
--muted: #62626b;
|
|
2685
|
+
--faint: #71717a;
|
|
2686
|
+
--accent: #27272a;
|
|
2687
|
+
--accent-ink: #ffffff;
|
|
2688
|
+
--accent-soft: #f4f4f5;
|
|
2689
|
+
--ok: #166534;
|
|
2690
|
+
--ok-soft: #f0fdf4;
|
|
2691
|
+
--warn: #a16207;
|
|
2692
|
+
--warn-soft: #fefce8;
|
|
2693
|
+
--err: #a82d32;
|
|
2694
|
+
--err-soft: #fef2f2;
|
|
2695
|
+
--shadow: none;
|
|
2696
|
+
--sans: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1463
2697
|
--mono: ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, monospace;
|
|
1464
|
-
--radius:
|
|
1465
|
-
--header:
|
|
1466
|
-
--nav:
|
|
2698
|
+
--radius: 10px;
|
|
2699
|
+
--header: 56px;
|
|
2700
|
+
--nav: 200px;
|
|
1467
2701
|
}
|
|
1468
2702
|
:root[data-theme="dark"] {
|
|
1469
2703
|
color-scheme: dark;
|
|
1470
|
-
--bg: #
|
|
1471
|
-
--bg-elev: #
|
|
1472
|
-
--bg-muted: #
|
|
1473
|
-
--bg-inset: #
|
|
1474
|
-
--line: #
|
|
1475
|
-
--line-strong: #
|
|
1476
|
-
--ink: #
|
|
1477
|
-
--muted: #
|
|
1478
|
-
--faint: #
|
|
1479
|
-
--accent: #
|
|
1480
|
-
--accent-ink: #
|
|
1481
|
-
--accent-soft: #
|
|
1482
|
-
--ok: #
|
|
1483
|
-
--ok-soft: #
|
|
1484
|
-
--warn: #
|
|
1485
|
-
--warn-soft: #
|
|
1486
|
-
--err: #
|
|
1487
|
-
--err-soft: #
|
|
1488
|
-
--shadow:
|
|
2704
|
+
--bg: #111113;
|
|
2705
|
+
--bg-elev: #19191c;
|
|
2706
|
+
--bg-muted: #242428;
|
|
2707
|
+
--bg-inset: #111113;
|
|
2708
|
+
--line: #36363c;
|
|
2709
|
+
--line-strong: #52525b;
|
|
2710
|
+
--ink: #f4f4f5;
|
|
2711
|
+
--muted: #a9a9b2;
|
|
2712
|
+
--faint: #71717a;
|
|
2713
|
+
--accent: #e4e4e7;
|
|
2714
|
+
--accent-ink: #18181b;
|
|
2715
|
+
--accent-soft: #242428;
|
|
2716
|
+
--ok: #86efac;
|
|
2717
|
+
--ok-soft: #14241b;
|
|
2718
|
+
--warn: #fde68a;
|
|
2719
|
+
--warn-soft: #2a2410;
|
|
2720
|
+
--err: #ffaaaa;
|
|
2721
|
+
--err-soft: #2c1516;
|
|
2722
|
+
--shadow: none;
|
|
1489
2723
|
}
|
|
1490
2724
|
@media (prefers-color-scheme: dark) {
|
|
1491
2725
|
:root:not([data-theme="light"]) {
|
|
1492
2726
|
color-scheme: dark;
|
|
1493
|
-
--bg: #
|
|
1494
|
-
--bg-elev: #
|
|
1495
|
-
--bg-muted: #
|
|
1496
|
-
--bg-inset: #
|
|
1497
|
-
--line: #
|
|
1498
|
-
--line-strong: #
|
|
1499
|
-
--ink: #
|
|
1500
|
-
--muted: #
|
|
1501
|
-
--faint: #
|
|
1502
|
-
--accent: #
|
|
1503
|
-
--accent-ink: #
|
|
1504
|
-
--accent-soft: #
|
|
1505
|
-
--ok: #
|
|
1506
|
-
--ok-soft: #
|
|
1507
|
-
--warn: #
|
|
1508
|
-
--warn-soft: #
|
|
1509
|
-
--err: #
|
|
1510
|
-
--err-soft: #
|
|
1511
|
-
--shadow:
|
|
2727
|
+
--bg: #111113;
|
|
2728
|
+
--bg-elev: #19191c;
|
|
2729
|
+
--bg-muted: #242428;
|
|
2730
|
+
--bg-inset: #111113;
|
|
2731
|
+
--line: #36363c;
|
|
2732
|
+
--line-strong: #52525b;
|
|
2733
|
+
--ink: #f4f4f5;
|
|
2734
|
+
--muted: #a9a9b2;
|
|
2735
|
+
--faint: #71717a;
|
|
2736
|
+
--accent: #e4e4e7;
|
|
2737
|
+
--accent-ink: #18181b;
|
|
2738
|
+
--accent-soft: #242428;
|
|
2739
|
+
--ok: #86efac;
|
|
2740
|
+
--ok-soft: #14241b;
|
|
2741
|
+
--warn: #fde68a;
|
|
2742
|
+
--warn-soft: #2a2410;
|
|
2743
|
+
--err: #ffaaaa;
|
|
2744
|
+
--err-soft: #2c1516;
|
|
2745
|
+
--shadow: none;
|
|
1512
2746
|
}
|
|
1513
2747
|
}
|
|
1514
|
-
|
|
1515
|
-
html
|
|
2748
|
+
/* Fill the viewport, then grow with the content so the background covers the scroll. */
|
|
2749
|
+
html { height: 100%; }
|
|
1516
2750
|
body {
|
|
2751
|
+
margin: 0;
|
|
2752
|
+
min-height: 100%;
|
|
2753
|
+
display: flex;
|
|
2754
|
+
flex-direction: column;
|
|
1517
2755
|
font-family: var(--sans);
|
|
1518
2756
|
background: var(--bg);
|
|
1519
2757
|
color: var(--ink);
|
|
@@ -1522,24 +2760,37 @@ var renderAdminDocument = (service) => {
|
|
|
1522
2760
|
padding-right: env(safe-area-inset-right);
|
|
1523
2761
|
-webkit-text-size-adjust: 100%;
|
|
1524
2762
|
}
|
|
1525
|
-
button, input, select, textarea { font: inherit; color: inherit; }
|
|
1526
|
-
button { cursor: pointer; }
|
|
1527
2763
|
:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
1528
|
-
|
|
1529
|
-
* { scroll-behavior: auto !important; transition: none !important; }
|
|
1530
|
-
}
|
|
1531
|
-
.app { min-height: 100%; display: flex; flex-direction: column; }
|
|
2764
|
+
.app { flex: 1 0 auto; display: flex; flex-direction: column; min-width: 0; width: 100%; }
|
|
1532
2765
|
header.top {
|
|
1533
2766
|
position: sticky; top: 0; z-index: 5;
|
|
1534
2767
|
display: flex; align-items: center; gap: 12px;
|
|
1535
2768
|
min-height: var(--header); padding: 10px 16px;
|
|
1536
|
-
background:
|
|
2769
|
+
background: var(--bg-elev);
|
|
1537
2770
|
border-bottom: 1px solid var(--line);
|
|
1538
|
-
|
|
1539
|
-
}
|
|
1540
|
-
.brand {
|
|
1541
|
-
|
|
1542
|
-
|
|
2771
|
+
min-width: 0; max-width: 100%;
|
|
2772
|
+
}
|
|
2773
|
+
.brand {
|
|
2774
|
+
display: flex; flex-flow: row wrap; align-items: center; gap: 14px;
|
|
2775
|
+
min-width: 0; max-width: 100%; margin-right: auto;
|
|
2776
|
+
}
|
|
2777
|
+
.brand-id { display: flex; flex-direction: column; min-width: 0; flex: none; }
|
|
2778
|
+
.brand-id strong { font-size: 15px; letter-spacing: -0.01em; }
|
|
2779
|
+
.brand-id span { color: var(--faint); font-size: 12px; }
|
|
2780
|
+
.vendor {
|
|
2781
|
+
display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
|
|
2782
|
+
min-width: 0; max-width: 100%; font-size: 12px; color: var(--muted);
|
|
2783
|
+
}
|
|
2784
|
+
.vendor[hidden] { display: none; }
|
|
2785
|
+
/* Logos are drawn for a light tile, same as the docs site, so the plate stays white. */
|
|
2786
|
+
.vendor img {
|
|
2787
|
+
width: 16px; height: 16px; padding: 2px; box-sizing: content-box;
|
|
2788
|
+
border-radius: 4px; border: 1px solid var(--line); background: #fff; object-fit: contain;
|
|
2789
|
+
}
|
|
2790
|
+
.vendor-name { color: var(--ink); font-weight: 600; }
|
|
2791
|
+
.vendor-sep { color: var(--faint); }
|
|
2792
|
+
.vendor-link { color: inherit; text-decoration: none; }
|
|
2793
|
+
.vendor-link:hover { color: var(--ink); text-decoration: underline; }
|
|
1543
2794
|
.controls { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
1544
2795
|
label.field { display: flex; flex-direction: column; gap: 2px; font-size: 11px; color: var(--faint); }
|
|
1545
2796
|
label.field input, label.field select, .controls input[type="password"] {
|
|
@@ -1557,7 +2808,7 @@ var renderAdminDocument = (service) => {
|
|
|
1557
2808
|
record or the full nav row would widen the page past the viewport. */
|
|
1558
2809
|
.layout {
|
|
1559
2810
|
display: grid; grid-template-columns: var(--nav) minmax(0, 1fr);
|
|
1560
|
-
flex: 1
|
|
2811
|
+
flex: 1 0 auto; min-width: 0;
|
|
1561
2812
|
}
|
|
1562
2813
|
nav.side {
|
|
1563
2814
|
border-right: 1px solid var(--line); padding: 12px;
|
|
@@ -1572,7 +2823,7 @@ var renderAdminDocument = (service) => {
|
|
|
1572
2823
|
main { padding: 20px; max-width: 1100px; width: 100%; min-width: 0; }
|
|
1573
2824
|
.view h2 { margin: 0 0 4px; font-size: 22px; letter-spacing: -0.02em; }
|
|
1574
2825
|
.lede { margin: 0 0 16px; color: var(--muted); }
|
|
1575
|
-
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
|
|
2826
|
+
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(100%, 180px), 1fr)); gap: 12px; }
|
|
1576
2827
|
.card {
|
|
1577
2828
|
background: var(--bg-elev); border: 1px solid var(--line); border-radius: var(--radius);
|
|
1578
2829
|
padding: 14px 16px; box-shadow: var(--shadow);
|
|
@@ -1611,8 +2862,8 @@ var renderAdminDocument = (service) => {
|
|
|
1611
2862
|
.banner.show { display: block; }
|
|
1612
2863
|
.empty { color: var(--muted); padding: 24px 8px; }
|
|
1613
2864
|
dialog {
|
|
1614
|
-
border: 1px solid var(--line); border-radius:
|
|
1615
|
-
color: var(--ink); width: min(640px, calc(
|
|
2865
|
+
border: 1px solid var(--line); border-radius: var(--radius); padding: 0; background: var(--bg-elev);
|
|
2866
|
+
color: var(--ink); width: min(640px, calc(100% - 24px)); max-width: calc(100vw - 24px); box-shadow: none;
|
|
1616
2867
|
}
|
|
1617
2868
|
dialog::backdrop { background: rgb(20 19 17 / 0.45); }
|
|
1618
2869
|
dialog form, .dialog-body { padding: 16px; display: flex; flex-direction: column; gap: 10px; }
|
|
@@ -1639,7 +2890,7 @@ var renderAdminDocument = (service) => {
|
|
|
1639
2890
|
padding-top: max(10px, env(safe-area-inset-top));
|
|
1640
2891
|
}
|
|
1641
2892
|
input, select, textarea { font-size: 16px; }
|
|
1642
|
-
.controls { width: 100%; }
|
|
2893
|
+
.brand, .vendor, .controls { width: 100%; }
|
|
1643
2894
|
label.field, label.field select, label.field input { flex: 1; min-width: 0; }
|
|
1644
2895
|
.layout { grid-template-columns: minmax(0, 1fr); }
|
|
1645
2896
|
nav.side {
|
|
@@ -1670,8 +2921,11 @@ var renderAdminDocument = (service) => {
|
|
|
1670
2921
|
<div class="app" data-service="${name}" data-mockingbird-admin>
|
|
1671
2922
|
<header class="top">
|
|
1672
2923
|
<div class="brand">
|
|
1673
|
-
<
|
|
1674
|
-
|
|
2924
|
+
<div class="brand-id">
|
|
2925
|
+
<strong>${name}</strong>
|
|
2926
|
+
<span>Admin</span>
|
|
2927
|
+
</div>
|
|
2928
|
+
<div class="vendor" id="vendor" hidden></div>
|
|
1675
2929
|
</div>
|
|
1676
2930
|
<div class="controls">
|
|
1677
2931
|
<label class="field">Namespace
|
|
@@ -1801,460 +3055,7 @@ var renderAdminDocument = (service) => {
|
|
|
1801
3055
|
</div>
|
|
1802
3056
|
</form>
|
|
1803
3057
|
</dialog>
|
|
1804
|
-
|
|
1805
|
-
(() => {
|
|
1806
|
-
const STANDARD = new Set(${standard})
|
|
1807
|
-
const state = {
|
|
1808
|
-
namespace: "default",
|
|
1809
|
-
key: sessionStorage.getItem("mockingbird-admin-key") || "",
|
|
1810
|
-
view: "overview",
|
|
1811
|
-
collection: "",
|
|
1812
|
-
shape: null,
|
|
1813
|
-
route: null,
|
|
1814
|
-
editing: null
|
|
1815
|
-
}
|
|
1816
|
-
const $ = (id) => document.getElementById(id)
|
|
1817
|
-
const banner = $("banner")
|
|
1818
|
-
const keyInput = $("admin-key")
|
|
1819
|
-
keyInput.value = state.key
|
|
1820
|
-
const params = new URLSearchParams(location.search)
|
|
1821
|
-
if (params.get("key")) {
|
|
1822
|
-
state.key = params.get("key")
|
|
1823
|
-
keyInput.value = state.key
|
|
1824
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
1825
|
-
params.delete("key")
|
|
1826
|
-
const next = params.toString()
|
|
1827
|
-
history.replaceState(null, "", location.pathname + (next ? "?" + next : ""))
|
|
1828
|
-
}
|
|
1829
|
-
if (params.get("namespace")) state.namespace = params.get("namespace")
|
|
1830
|
-
|
|
1831
|
-
const showError = (error) => {
|
|
1832
|
-
banner.textContent = error instanceof Error ? error.message : String(error)
|
|
1833
|
-
banner.classList.add("show")
|
|
1834
|
-
}
|
|
1835
|
-
const clearError = () => banner.classList.remove("show")
|
|
1836
|
-
|
|
1837
|
-
async function originGet(path) {
|
|
1838
|
-
const response = await fetch(path, { headers: { accept: "application/json" } })
|
|
1839
|
-
if (!response.ok) throw new Error(response.status + " " + path)
|
|
1840
|
-
return response.json()
|
|
1841
|
-
}
|
|
1842
|
-
async function api(method, path, body) {
|
|
1843
|
-
const headers = { accept: "application/json" }
|
|
1844
|
-
if (state.key) headers["${ADMIN_KEY_HEADER}"] = state.key
|
|
1845
|
-
if (state.namespace) headers["x-mockingbird-namespace"] = state.namespace
|
|
1846
|
-
if (body !== undefined) headers["content-type"] = "application/json"
|
|
1847
|
-
const response = await fetch("${ADMIN_PREFIX}" + path, {
|
|
1848
|
-
method,
|
|
1849
|
-
headers,
|
|
1850
|
-
body: body === undefined ? undefined : JSON.stringify(body)
|
|
1851
|
-
})
|
|
1852
|
-
const text = await response.text()
|
|
1853
|
-
let data = null
|
|
1854
|
-
if (text) {
|
|
1855
|
-
try { data = JSON.parse(text) } catch { data = { raw: text } }
|
|
1856
|
-
}
|
|
1857
|
-
if (response.status === 401) {
|
|
1858
|
-
keyInput.focus()
|
|
1859
|
-
throw new Error((data && data.error && data.error.message) || "Admin key required")
|
|
1860
|
-
}
|
|
1861
|
-
if (!response.ok) {
|
|
1862
|
-
throw new Error((data && data.error && data.error.message) || (response.status + " " + method + " " + path))
|
|
1863
|
-
}
|
|
1864
|
-
return data
|
|
1865
|
-
}
|
|
1866
|
-
const panelApi = {
|
|
1867
|
-
get: (path) => api("GET", path),
|
|
1868
|
-
send: (method, path, body) => api(method, path, body),
|
|
1869
|
-
namespace: () => state.namespace
|
|
1870
|
-
}
|
|
1871
|
-
|
|
1872
|
-
function setTheme(theme) {
|
|
1873
|
-
if (theme) document.documentElement.dataset.theme = theme
|
|
1874
|
-
else delete document.documentElement.dataset.theme
|
|
1875
|
-
}
|
|
1876
|
-
const storedTheme = localStorage.getItem("mockingbird-admin-theme")
|
|
1877
|
-
if (storedTheme) setTheme(storedTheme)
|
|
1878
|
-
$("theme").addEventListener("click", () => {
|
|
1879
|
-
const current = document.documentElement.dataset.theme
|
|
1880
|
-
const dark = current ? current === "dark" : matchMedia("(prefers-color-scheme: dark)").matches
|
|
1881
|
-
const next = dark ? "light" : "dark"
|
|
1882
|
-
localStorage.setItem("mockingbird-admin-theme", next)
|
|
1883
|
-
setTheme(next)
|
|
1884
|
-
})
|
|
1885
|
-
|
|
1886
|
-
function show(view) {
|
|
1887
|
-
state.view = view
|
|
1888
|
-
for (const button of document.querySelectorAll("#nav button")) {
|
|
1889
|
-
button.setAttribute("aria-current", button.dataset.view === view ? "true" : "false")
|
|
1890
|
-
}
|
|
1891
|
-
for (const section of document.querySelectorAll("main .view")) {
|
|
1892
|
-
section.hidden = section.id !== "view-" + view
|
|
1893
|
-
}
|
|
1894
|
-
}
|
|
1895
|
-
document.getElementById("nav").addEventListener("click", (event) => {
|
|
1896
|
-
const button = event.target.closest("button[data-view]")
|
|
1897
|
-
if (!button) return
|
|
1898
|
-
show(button.dataset.view)
|
|
1899
|
-
refresh().catch(showError)
|
|
1900
|
-
})
|
|
1901
|
-
|
|
1902
|
-
function cards(items) {
|
|
1903
|
-
return '<div class="grid">' + items.map((item) =>
|
|
1904
|
-
'<article class="card"><div class="k">' + item.k + '</div><div class="v">' + item.v + '</div></article>'
|
|
1905
|
-
).join("") + "</div>"
|
|
1906
|
-
}
|
|
1907
|
-
function table(columns, rows) {
|
|
1908
|
-
if (rows.length === 0) return '<p class="empty">Nothing here yet.</p>'
|
|
1909
|
-
const head = "<tr>" + columns.map((column) => "<th>" + column.label + "</th>").join("") + "</tr>"
|
|
1910
|
-
const body = rows.map((row) => "<tr" + (row.attrs || "") + ">" + columns.map((column) =>
|
|
1911
|
-
'<td data-label="' + column.label + '">' + column.cell(row) + "</td>"
|
|
1912
|
-
).join("") + "</tr>").join("")
|
|
1913
|
-
return "<table><thead>" + head + "</thead><tbody>" + body + "</tbody></table>"
|
|
1914
|
-
}
|
|
1915
|
-
const esc = (value) => String(value ?? "").replace(/[&<>"']/g, (char) => ({
|
|
1916
|
-
"&": "&", "<": "<", ">": ">", '"': """, "'": "'"
|
|
1917
|
-
}[char]))
|
|
1918
|
-
const preview = (value) => {
|
|
1919
|
-
const text = JSON.stringify(value)
|
|
1920
|
-
return esc(text.length > 180 ? text.slice(0, 180) + "\u2026" : text)
|
|
1921
|
-
}
|
|
1922
|
-
const when = (epoch) => {
|
|
1923
|
-
const date = new Date(epoch)
|
|
1924
|
-
return Number.isNaN(date.getTime()) ? String(epoch) : date.toISOString()
|
|
1925
|
-
}
|
|
1926
|
-
const clockCard = (epoch) => {
|
|
1927
|
-
const date = new Date(epoch)
|
|
1928
|
-
if (Number.isNaN(date.getTime())) return esc(String(epoch))
|
|
1929
|
-
const iso = date.toISOString()
|
|
1930
|
-
return esc(iso.slice(0, 10)) + '<span class="clock-time">' + esc(iso.slice(11, 19)) + " UTC</span>"
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
async function loadNamespaces() {
|
|
1934
|
-
const data = await api("GET", "/namespaces")
|
|
1935
|
-
const select = $("namespace")
|
|
1936
|
-
const names = data.namespaces && data.namespaces.length ? data.namespaces : [data.default || "default"]
|
|
1937
|
-
if (!names.includes(state.namespace)) names.unshift(state.namespace)
|
|
1938
|
-
select.innerHTML = names.map((name) =>
|
|
1939
|
-
'<option value="' + esc(name) + '">' + esc(name) + "</option>"
|
|
1940
|
-
).join("")
|
|
1941
|
-
select.value = state.namespace
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
async function renderOverview() {
|
|
1945
|
-
const [health, shape, clock, journal] = await Promise.all([
|
|
1946
|
-
originGet("/health"),
|
|
1947
|
-
api("GET", "/state"),
|
|
1948
|
-
api("GET", "/clock"),
|
|
1949
|
-
api("GET", "/requests?limit=8")
|
|
1950
|
-
])
|
|
1951
|
-
state.shape = shape
|
|
1952
|
-
$("overview-cards").innerHTML = cards([
|
|
1953
|
-
{ k: "Service", v: esc(health.service || "${name}") },
|
|
1954
|
-
{ k: "Status", v: esc(health.status || "ok") },
|
|
1955
|
-
{ k: "Namespace", v: esc(state.namespace) },
|
|
1956
|
-
{ k: "Collections", v: String(shape.collections.length) },
|
|
1957
|
-
{ k: "Records", v: String(shape.collections.reduce((sum, item) => sum + item.count, 0)) },
|
|
1958
|
-
{ k: "Clock", v: clockCard(clock.now) }
|
|
1959
|
-
])
|
|
1960
|
-
const requests = (journal && journal.requests) || []
|
|
1961
|
-
$("overview-requests").innerHTML = table(
|
|
1962
|
-
[
|
|
1963
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
1964
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
1965
|
-
{ label: "Status", cell: (row) => '<span class="status-' + String(row.status).charAt(0) + '">' + esc(row.status) + "</span>" },
|
|
1966
|
-
{ label: "Operation", cell: (row) => esc(row.operationId || "") }
|
|
1967
|
-
],
|
|
1968
|
-
requests
|
|
1969
|
-
)
|
|
1970
|
-
}
|
|
1971
|
-
|
|
1972
|
-
function schemaChips(collection) {
|
|
1973
|
-
if (!collection || collection.fields.length === 0) {
|
|
1974
|
-
return '<p class="empty">No fields yet. Add a record or declare the collection on the service.</p>'
|
|
1975
|
-
}
|
|
1976
|
-
return '<div class="chips">' + collection.fields.map((field) =>
|
|
1977
|
-
'<span class="chip">' + esc(field.name) + ' <i>' + esc(field.kind) + (field.optional ? "?" : "") + "</i></span>"
|
|
1978
|
-
).join("") + "</div>"
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
async function renderState() {
|
|
1982
|
-
const shape = await api("GET", "/state")
|
|
1983
|
-
state.shape = shape
|
|
1984
|
-
const host = $("collections")
|
|
1985
|
-
if (shape.collections.length === 0) {
|
|
1986
|
-
host.innerHTML = '<p class="empty">This namespace has no collections yet.</p>'
|
|
1987
|
-
$("schema").innerHTML = ""
|
|
1988
|
-
$("records").innerHTML = ""
|
|
1989
|
-
return
|
|
1990
|
-
}
|
|
1991
|
-
if (!shape.collections.some((item) => item.name === state.collection)) {
|
|
1992
|
-
state.collection = shape.collections[0].name
|
|
1993
|
-
}
|
|
1994
|
-
host.innerHTML = shape.collections.map((item) =>
|
|
1995
|
-
'<button type="button" class="collection" data-collection="' + esc(item.name) + '" aria-current="' +
|
|
1996
|
-
(item.name === state.collection ? "true" : "false") + '"><strong>' + esc(item.label) +
|
|
1997
|
-
'</strong><small>' + esc(item.count) + " \xB7 " + esc(item.source) + "</small></button>"
|
|
1998
|
-
).join("")
|
|
1999
|
-
const selected = shape.collections.find((item) => item.name === state.collection)
|
|
2000
|
-
$("schema").innerHTML = "<h3>" + esc(selected.label) + "</h3>" +
|
|
2001
|
-
(selected.description ? "<p class=" + '"lede">' + esc(selected.description) + "</p>" : "") +
|
|
2002
|
-
schemaChips(selected)
|
|
2003
|
-
const page = await api("GET", "/state/" + encodeURIComponent(state.collection) + "?limit=50")
|
|
2004
|
-
const rows = page.records.map((record) => ({
|
|
2005
|
-
...record,
|
|
2006
|
-
attrs: ' class="clickable" data-id="' + esc(record.id) + '"'
|
|
2007
|
-
}))
|
|
2008
|
-
$("records").innerHTML = table(
|
|
2009
|
-
[
|
|
2010
|
-
{ label: "Id", cell: (row) => "<code>" + esc(row.id) + "</code>" },
|
|
2011
|
-
{ label: "Seq", cell: (row) => esc(row.seq) },
|
|
2012
|
-
{ label: "Value", cell: (row) => preview(row.value) }
|
|
2013
|
-
],
|
|
2014
|
-
rows
|
|
2015
|
-
)
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
async function renderClock() {
|
|
2019
|
-
const clock = await api("GET", "/clock")
|
|
2020
|
-
$("clock-readout").innerHTML = "<div class='k'>Now</div><div class='v'>" + esc(when(clock.now)) +
|
|
2021
|
-
"</div><p>" + esc(clock.now) + " ms \xB7 offset " + esc(clock.offsetMs) + " ms " +
|
|
2022
|
-
(clock.frozen ? "<span class='badge warn'>frozen</span>" : "<span class='badge ok'>live</span>") + "</p>"
|
|
2023
|
-
$("freeze").textContent = clock.frozen ? "Unfreeze" : "Freeze"
|
|
2024
|
-
$("freeze").dataset.frozen = clock.frozen ? "1" : "0"
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
async function renderFaults() {
|
|
2028
|
-
const [faults, presets] = await Promise.all([
|
|
2029
|
-
api("GET", "/faults"),
|
|
2030
|
-
api("GET", "/faults/presets").catch(() => ({ presets: [] }))
|
|
2031
|
-
])
|
|
2032
|
-
const select = $("fault-preset")
|
|
2033
|
-
const current = select.value
|
|
2034
|
-
const list = presets.presets || []
|
|
2035
|
-
select.innerHTML = '<option value="">Custom rule</option>' + list.map((preset) =>
|
|
2036
|
-
'<option value="' + esc(preset.name) + '">' + esc(preset.name) + "</option>"
|
|
2037
|
-
).join("")
|
|
2038
|
-
select.value = current
|
|
2039
|
-
const rules = faults.faults || []
|
|
2040
|
-
$("fault-list").innerHTML = table(
|
|
2041
|
-
[
|
|
2042
|
-
{ label: "Id", cell: (row) => esc(row.id) },
|
|
2043
|
-
{ label: "Match", cell: (row) => esc([row.method, row.pathPrefix, row.operationId].filter(Boolean).join(" ") || "all") },
|
|
2044
|
-
{ label: "Effect", cell: (row) => esc(row.status ?? row.effect ?? (row.drop ? "drop" : row.latencyMs || "")) }
|
|
2045
|
-
],
|
|
2046
|
-
rules
|
|
2047
|
-
)
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
async function renderJournal() {
|
|
2051
|
-
const journal = await api("GET", "/requests?limit=100")
|
|
2052
|
-
$("journal").innerHTML = table(
|
|
2053
|
-
[
|
|
2054
|
-
{ label: "When", cell: (row) => esc(row.at || "") },
|
|
2055
|
-
{ label: "Method", cell: (row) => esc(row.method) },
|
|
2056
|
-
{ label: "Path", cell: (row) => esc(row.path) },
|
|
2057
|
-
{ label: "Status", cell: (row) => esc(row.status) },
|
|
2058
|
-
{ label: "ms", cell: (row) => esc(row.durationMs) }
|
|
2059
|
-
],
|
|
2060
|
-
journal.requests || []
|
|
2061
|
-
)
|
|
2062
|
-
}
|
|
2063
|
-
|
|
2064
|
-
async function renderRoutes() {
|
|
2065
|
-
const data = await api("GET", "/")
|
|
2066
|
-
const routes = data.routes || []
|
|
2067
|
-
$("route-list").innerHTML = routes.map((route) => {
|
|
2068
|
-
const extra = STANDARD.has(route) ? "" : " <span class='badge'>extension</span>"
|
|
2069
|
-
return '<div class="route"><span class="method">' + esc(route.split(" ")[0]) +
|
|
2070
|
-
'</span><span>' + esc(route.split(" ").slice(1).join(" ")) + extra +
|
|
2071
|
-
'</span><button class="btn" type="button" data-route="' + esc(route) + '">Use</button></div>'
|
|
2072
|
-
}).join("")
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
const renderers = {
|
|
2076
|
-
overview: renderOverview,
|
|
2077
|
-
state: renderState,
|
|
2078
|
-
clock: renderClock,
|
|
2079
|
-
faults: renderFaults,
|
|
2080
|
-
journal: renderJournal,
|
|
2081
|
-
routes: renderRoutes
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
async function refresh() {
|
|
2085
|
-
clearError()
|
|
2086
|
-
if (state.view in renderers) await renderers[state.view]()
|
|
2087
|
-
}
|
|
2088
|
-
|
|
2089
|
-
$("namespace").addEventListener("change", () => {
|
|
2090
|
-
state.namespace = $("namespace").value
|
|
2091
|
-
refresh().catch(showError)
|
|
2092
|
-
})
|
|
2093
|
-
keyInput.addEventListener("change", () => {
|
|
2094
|
-
state.key = keyInput.value.trim()
|
|
2095
|
-
sessionStorage.setItem("mockingbird-admin-key", state.key)
|
|
2096
|
-
refresh().catch(showError)
|
|
2097
|
-
})
|
|
2098
|
-
$("refresh").addEventListener("click", () => refresh().catch(showError))
|
|
2099
|
-
|
|
2100
|
-
$("collections").addEventListener("click", (event) => {
|
|
2101
|
-
const button = event.target.closest("[data-collection]")
|
|
2102
|
-
if (!button) return
|
|
2103
|
-
state.collection = button.dataset.collection
|
|
2104
|
-
renderState().catch(showError)
|
|
2105
|
-
})
|
|
2106
|
-
$("records").addEventListener("click", (event) => {
|
|
2107
|
-
const row = event.target.closest("[data-id]")
|
|
2108
|
-
if (!row) return
|
|
2109
|
-
openEditor(row.dataset.id).catch(showError)
|
|
2110
|
-
})
|
|
2111
|
-
|
|
2112
|
-
const editor = $("editor")
|
|
2113
|
-
async function openEditor(id) {
|
|
2114
|
-
state.editing = id || null
|
|
2115
|
-
$("editor-title").textContent = id ? "Edit record" : "New record"
|
|
2116
|
-
$("editor-id").value = id || ""
|
|
2117
|
-
$("editor-id").readOnly = Boolean(id)
|
|
2118
|
-
$("editor-delete").hidden = !id
|
|
2119
|
-
if (id) {
|
|
2120
|
-
const record = await api("GET", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
2121
|
-
$("editor-value").value = JSON.stringify(record.value, null, 2)
|
|
2122
|
-
} else {
|
|
2123
|
-
$("editor-value").value = "{\\n \\n}"
|
|
2124
|
-
}
|
|
2125
|
-
editor.showModal()
|
|
2126
|
-
}
|
|
2127
|
-
$("new-record").addEventListener("click", () => openEditor(null).catch(showError))
|
|
2128
|
-
$("editor-form").addEventListener("submit", (event) => {
|
|
2129
|
-
const submitter = event.submitter
|
|
2130
|
-
if (!submitter || submitter.value !== "save") return
|
|
2131
|
-
event.preventDefault()
|
|
2132
|
-
let value
|
|
2133
|
-
try { value = JSON.parse($("editor-value").value) }
|
|
2134
|
-
catch { showError(new Error("Value is not JSON")); return }
|
|
2135
|
-
const id = $("editor-id").value.trim()
|
|
2136
|
-
const path = "/state/" + encodeURIComponent(state.collection)
|
|
2137
|
-
const request = state.editing
|
|
2138
|
-
? api("PUT", path + "/" + encodeURIComponent(id), { value })
|
|
2139
|
-
: api("POST", path, { id: id || undefined, value })
|
|
2140
|
-
request.then(() => { editor.close(); return renderState() }).catch(showError)
|
|
2141
|
-
})
|
|
2142
|
-
$("editor-delete").addEventListener("click", () => {
|
|
2143
|
-
const id = state.editing
|
|
2144
|
-
if (!id) return
|
|
2145
|
-
api("DELETE", "/state/" + encodeURIComponent(state.collection) + "/" + encodeURIComponent(id))
|
|
2146
|
-
.then(() => { editor.close(); return renderState() })
|
|
2147
|
-
.catch(showError)
|
|
2148
|
-
})
|
|
2149
|
-
|
|
2150
|
-
document.querySelectorAll("[data-advance]").forEach((button) => {
|
|
2151
|
-
button.addEventListener("click", () => {
|
|
2152
|
-
api("POST", "/clock", { advance: Number(button.dataset.advance) }).then(renderClock).catch(showError)
|
|
2153
|
-
})
|
|
2154
|
-
})
|
|
2155
|
-
$("freeze").addEventListener("click", () => {
|
|
2156
|
-
const frozen = $("freeze").dataset.frozen === "1"
|
|
2157
|
-
api("POST", "/clock", { freeze: !frozen }).then(renderClock).catch(showError)
|
|
2158
|
-
})
|
|
2159
|
-
$("clock-reset").addEventListener("click", () => {
|
|
2160
|
-
api("POST", "/clock", { reset: true }).then(renderClock).catch(showError)
|
|
2161
|
-
})
|
|
2162
|
-
$("clock-set").addEventListener("submit", (event) => {
|
|
2163
|
-
event.preventDefault()
|
|
2164
|
-
const raw = $("clock-instant").value.trim()
|
|
2165
|
-
const set = /^-?\\d+$/.test(raw) ? Number(raw) : raw
|
|
2166
|
-
api("POST", "/clock", { set }).then(renderClock).catch(showError)
|
|
2167
|
-
})
|
|
2168
|
-
|
|
2169
|
-
$("fault-form").addEventListener("submit", (event) => {
|
|
2170
|
-
event.preventDefault()
|
|
2171
|
-
const preset = $("fault-preset").value
|
|
2172
|
-
const body = preset
|
|
2173
|
-
? { preset }
|
|
2174
|
-
: {
|
|
2175
|
-
status: Number($("fault-status").value),
|
|
2176
|
-
...($("fault-path").value ? { pathPrefix: $("fault-path").value } : {}),
|
|
2177
|
-
...($("fault-method").value ? { method: $("fault-method").value } : {})
|
|
2178
|
-
}
|
|
2179
|
-
if (!preset && !Number.isFinite(body.status)) {
|
|
2180
|
-
showError(new Error("Status must be a number, or choose a preset"))
|
|
2181
|
-
return
|
|
2182
|
-
}
|
|
2183
|
-
api("POST", "/faults", body).then(renderFaults).catch(showError)
|
|
2184
|
-
})
|
|
2185
|
-
$("fault-clear").addEventListener("click", () => api("DELETE", "/faults").then(renderFaults).catch(showError))
|
|
2186
|
-
$("journal-clear").addEventListener("click", () => api("DELETE", "/requests").then(renderJournal).catch(showError))
|
|
2187
|
-
|
|
2188
|
-
$("route-list").addEventListener("click", (event) => {
|
|
2189
|
-
const button = event.target.closest("[data-route]")
|
|
2190
|
-
if (!button) return
|
|
2191
|
-
state.route = button.dataset.route
|
|
2192
|
-
$("route-title").textContent = state.route
|
|
2193
|
-
const method = state.route.split(" ")[0]
|
|
2194
|
-
$("route-body").disabled = method === "GET" || method === "DELETE"
|
|
2195
|
-
})
|
|
2196
|
-
$("route-form").addEventListener("submit", async (event) => {
|
|
2197
|
-
event.preventDefault()
|
|
2198
|
-
if (!state.route) return showError(new Error("Choose a route first"))
|
|
2199
|
-
const [method, path] = [state.route.split(" ")[0], state.route.split(" ").slice(1).join(" ")]
|
|
2200
|
-
const concrete = path.replace(/:([A-Za-z0-9_]+)/g, (_, key) => prompt("Value for " + key) || "")
|
|
2201
|
-
let body
|
|
2202
|
-
if (method !== "GET" && method !== "DELETE" && $("route-body").value.trim()) {
|
|
2203
|
-
try { body = JSON.parse($("route-body").value) }
|
|
2204
|
-
catch { return showError(new Error("Body is not JSON")) }
|
|
2205
|
-
}
|
|
2206
|
-
try {
|
|
2207
|
-
const data = await api(method, concrete, body)
|
|
2208
|
-
$("route-result").textContent = JSON.stringify(data, null, 2)
|
|
2209
|
-
} catch (error) {
|
|
2210
|
-
showError(error)
|
|
2211
|
-
}
|
|
2212
|
-
})
|
|
2213
|
-
|
|
2214
|
-
function mountPanels(panels) {
|
|
2215
|
-
const nav = $("nav")
|
|
2216
|
-
const views = $("views")
|
|
2217
|
-
for (const panel of panels) {
|
|
2218
|
-
const button = document.createElement("button")
|
|
2219
|
-
button.type = "button"
|
|
2220
|
-
button.dataset.view = panel.id
|
|
2221
|
-
button.textContent = panel.title
|
|
2222
|
-
nav.append(button)
|
|
2223
|
-
const section = document.createElement("section")
|
|
2224
|
-
section.className = "view"
|
|
2225
|
-
section.id = "view-" + panel.id
|
|
2226
|
-
section.hidden = true
|
|
2227
|
-
const title = document.createElement("h2")
|
|
2228
|
-
title.textContent = panel.title
|
|
2229
|
-
const lede = document.createElement("p")
|
|
2230
|
-
lede.className = "lede"
|
|
2231
|
-
lede.textContent = panel.description || ""
|
|
2232
|
-
const body = document.createElement("div")
|
|
2233
|
-
body.className = "panel-body"
|
|
2234
|
-
body.id = "panel-" + panel.id
|
|
2235
|
-
body.innerHTML = panel.html || ""
|
|
2236
|
-
section.append(title, lede, body)
|
|
2237
|
-
views.append(section)
|
|
2238
|
-
if (panel.script) {
|
|
2239
|
-
try {
|
|
2240
|
-
const run = new Function("root", "api", panel.script)
|
|
2241
|
-
run(body, panelApi)
|
|
2242
|
-
} catch (error) {
|
|
2243
|
-
const note = document.createElement("p")
|
|
2244
|
-
note.className = "banner show"
|
|
2245
|
-
note.textContent = error instanceof Error ? error.message : String(error)
|
|
2246
|
-
body.append(note)
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
loadNamespaces()
|
|
2253
|
-
.then(() => api("GET", "/ui/manifest"))
|
|
2254
|
-
.then((manifest) => { mountPanels((manifest && manifest.panels) || []); return refresh() })
|
|
2255
|
-
.catch(showError)
|
|
2256
|
-
})()
|
|
2257
|
-
</script>
|
|
3058
|
+
${script}
|
|
2258
3059
|
</body>
|
|
2259
3060
|
</html>`;
|
|
2260
3061
|
};
|
|
@@ -2271,10 +3072,10 @@ var json2 = (body) => new Response(JSON.stringify(body), {
|
|
|
2271
3072
|
});
|
|
2272
3073
|
var adminUiRoutes = (service, ui) => {
|
|
2273
3074
|
const shell = () => renderAdminDocument(service);
|
|
2274
|
-
const
|
|
3075
|
+
const document3 = () => ui?.render ? ui.render({ service, defaultHtml: shell }) : shell();
|
|
2275
3076
|
return {
|
|
2276
|
-
"GET /ui": () => htmlResponse(
|
|
2277
|
-
"GET /ui/": () => htmlResponse(
|
|
3077
|
+
"GET /ui": () => htmlResponse(document3()),
|
|
3078
|
+
"GET /ui/": () => htmlResponse(document3()),
|
|
2278
3079
|
"GET /ui/manifest": () => json2({
|
|
2279
3080
|
service,
|
|
2280
3081
|
panels: (ui?.panels ?? []).map((panel) => ({
|
|
@@ -2300,11 +3101,11 @@ var EXTENSION_KEYS = {
|
|
|
2300
3101
|
};
|
|
2301
3102
|
|
|
2302
3103
|
// ../../openapi/metadata/dist/read.js
|
|
2303
|
-
var
|
|
3104
|
+
var isRecord3 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2304
3105
|
var extensionOf = (holder, key) => holder[key];
|
|
2305
3106
|
var operationMetadata = (operation) => {
|
|
2306
3107
|
const raw = extensionOf(operation, EXTENSION_KEYS.operation);
|
|
2307
|
-
const ext =
|
|
3108
|
+
const ext = isRecord3(raw) ? raw : {};
|
|
2308
3109
|
const supported = ext.supported ?? true;
|
|
2309
3110
|
const parity = ext.parity ?? {};
|
|
2310
3111
|
return {
|
|
@@ -2414,9 +3215,9 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
2414
3215
|
this.name = "OperationRegistryError";
|
|
2415
3216
|
}
|
|
2416
3217
|
};
|
|
2417
|
-
var verifyOperations = (
|
|
3218
|
+
var verifyOperations = (document3, handlers) => {
|
|
2418
3219
|
const problems = [];
|
|
2419
|
-
const operations = listOperations(
|
|
3220
|
+
const operations = listOperations(document3);
|
|
2420
3221
|
const seen = /* @__PURE__ */ new Set();
|
|
2421
3222
|
for (const operation of operations) {
|
|
2422
3223
|
if (seen.has(operation.operationId))
|
|
@@ -2532,7 +3333,7 @@ var SAMPLE = 25;
|
|
|
2532
3333
|
var WALK_LIMIT = 1e4;
|
|
2533
3334
|
var SKIP_CTORS = /* @__PURE__ */ new Set(["Hono", "HonoRequest", "Router"]);
|
|
2534
3335
|
var fail2 = (status, message) => ({ ok: false, status, message });
|
|
2535
|
-
var
|
|
3336
|
+
var isRecord4 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2536
3337
|
var kindOf = (value) => {
|
|
2537
3338
|
if (value === null)
|
|
2538
3339
|
return "null";
|
|
@@ -2631,7 +3432,7 @@ var absorb = (accum, value) => {
|
|
|
2631
3432
|
const kind = kindOf(value);
|
|
2632
3433
|
accum.kinds.add(kind);
|
|
2633
3434
|
accum.present++;
|
|
2634
|
-
if (!
|
|
3435
|
+
if (!isRecord4(value))
|
|
2635
3436
|
return;
|
|
2636
3437
|
for (const [key, child] of Object.entries(value)) {
|
|
2637
3438
|
const nested = accum.nested.get(key) ?? blankAccum(key);
|
|
@@ -2656,7 +3457,7 @@ var inferFields = (records) => {
|
|
|
2656
3457
|
if (records.length === 0)
|
|
2657
3458
|
return [];
|
|
2658
3459
|
const sample = records.slice(0, SAMPLE);
|
|
2659
|
-
const objects = sample.filter((record) =>
|
|
3460
|
+
const objects = sample.filter((record) => isRecord4(record.value));
|
|
2660
3461
|
if (objects.length === 0) {
|
|
2661
3462
|
const kinds = new Set(sample.map((record) => kindOf(record.value)));
|
|
2662
3463
|
const list = [...kinds];
|
|
@@ -2843,7 +3644,7 @@ var patchStateRecord = (scope, name, id, value) => {
|
|
|
2843
3644
|
const parsed = jsonValue(value);
|
|
2844
3645
|
if (!parsed.ok)
|
|
2845
3646
|
return parsed;
|
|
2846
|
-
const next =
|
|
3647
|
+
const next = isRecord4(current.value.value) && isRecord4(parsed.value) ? { ...current.value.value, ...parsed.value } : parsed.value;
|
|
2847
3648
|
return replaceStateRecord(scope, name, id, next);
|
|
2848
3649
|
};
|
|
2849
3650
|
var deleteStateRecord = (scope, name, id) => {
|
|
@@ -2911,7 +3712,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2911
3712
|
const scope = open(namespace);
|
|
2912
3713
|
if (!scope.ok)
|
|
2913
3714
|
return fromResult(scope, 201);
|
|
2914
|
-
if (!
|
|
3715
|
+
if (!isRecord4(body) || !("value" in body)) {
|
|
2915
3716
|
return adminError2(400, 'expected {"id?": "<id>", "value": <json>}');
|
|
2916
3717
|
}
|
|
2917
3718
|
const id = body.id;
|
|
@@ -2929,7 +3730,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2929
3730
|
const scope = open(namespace);
|
|
2930
3731
|
if (!scope.ok)
|
|
2931
3732
|
return fromResult(scope, 200);
|
|
2932
|
-
if (!
|
|
3733
|
+
if (!isRecord4(body) || !("value" in body))
|
|
2933
3734
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2934
3735
|
const replaced = replaceStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2935
3736
|
if (replaced.ok)
|
|
@@ -2940,7 +3741,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2940
3741
|
const scope = open(namespace);
|
|
2941
3742
|
if (!scope.ok)
|
|
2942
3743
|
return fromResult(scope, 200);
|
|
2943
|
-
if (!
|
|
3744
|
+
if (!isRecord4(body) || !("value" in body))
|
|
2944
3745
|
return adminError2(400, 'expected {"value": <json>}');
|
|
2945
3746
|
const patched = patchStateRecord(scope.value, params.collection ?? "", params.id ?? "", body.value);
|
|
2946
3747
|
if (patched.ok)
|
|
@@ -2960,7 +3761,7 @@ var stateAdminRoutes = (routes) => {
|
|
|
2960
3761
|
};
|
|
2961
3762
|
|
|
2962
3763
|
// ../core/dist/version.js
|
|
2963
|
-
var PACKAGE_VERSION = true ? "1.
|
|
3764
|
+
var PACKAGE_VERSION = true ? "1.5.1" : UNRELEASED_VERSION;
|
|
2964
3765
|
|
|
2965
3766
|
// ../core/dist/signing.js
|
|
2966
3767
|
var encoder = new TextEncoder();
|
|
@@ -3297,9 +4098,9 @@ var createWebhookHub = (options) => {
|
|
|
3297
4098
|
};
|
|
3298
4099
|
var json4 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
3299
4100
|
var adminError3 = (status, message) => json4(status, { error: { type: "mockingbird_admin", message } });
|
|
3300
|
-
var
|
|
4101
|
+
var isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3301
4102
|
var parseEndpoint = (value) => {
|
|
3302
|
-
if (!
|
|
4103
|
+
if (!isRecord5(value) || typeof value.url !== "string")
|
|
3303
4104
|
return "each endpoint needs a url";
|
|
3304
4105
|
try {
|
|
3305
4106
|
new URL(value.url);
|
|
@@ -3316,12 +4117,12 @@ var parseEndpoint = (value) => {
|
|
|
3316
4117
|
const events = value.events ?? value.enabledEvents;
|
|
3317
4118
|
if (Array.isArray(events))
|
|
3318
4119
|
endpoint.events = events.map(String);
|
|
3319
|
-
if (
|
|
4120
|
+
if (isRecord5(value.tags)) {
|
|
3320
4121
|
endpoint.tags = Object.fromEntries(Object.entries(value.tags).map(([k, v]) => [k, String(v)]));
|
|
3321
4122
|
}
|
|
3322
4123
|
if (typeof value.account === "string")
|
|
3323
4124
|
endpoint.tags = { ...endpoint.tags, account: value.account };
|
|
3324
|
-
if (
|
|
4125
|
+
if (isRecord5(value.headers)) {
|
|
3325
4126
|
endpoint.headers = Object.fromEntries(Object.entries(value.headers).map(([k, v]) => [k, String(v)]));
|
|
3326
4127
|
}
|
|
3327
4128
|
return endpoint;
|
|
@@ -3348,7 +4149,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
3348
4149
|
return json4(200, { status: "ok" });
|
|
3349
4150
|
},
|
|
3350
4151
|
"POST /webhooks/faults": ({ body, namespace }) => {
|
|
3351
|
-
if (!
|
|
4152
|
+
if (!isRecord5(body) || !["duplicate", "reorder", "drop"].includes(String(body.mode))) {
|
|
3352
4153
|
return adminError3(400, "mode must be duplicate, reorder or drop");
|
|
3353
4154
|
}
|
|
3354
4155
|
const fault = { mode: body.mode };
|
|
@@ -3364,7 +4165,7 @@ var webhookAdminRoutes = (hub) => ({
|
|
|
3364
4165
|
}))
|
|
3365
4166
|
}),
|
|
3366
4167
|
"PUT /webhook-endpoints": ({ body, namespace }) => {
|
|
3367
|
-
const list = Array.isArray(body) ? body :
|
|
4168
|
+
const list = Array.isArray(body) ? body : isRecord5(body) ? body.endpoints : void 0;
|
|
3368
4169
|
if (!Array.isArray(list))
|
|
3369
4170
|
return adminError3(400, "expected [{url, secret?, events?}]");
|
|
3370
4171
|
const parsed = [];
|
|
@@ -3434,8 +4235,8 @@ var DroppedConnectionError = class extends TypeError {
|
|
|
3434
4235
|
this.name = "TypeError";
|
|
3435
4236
|
}
|
|
3436
4237
|
};
|
|
3437
|
-
var operationMatcher = (
|
|
3438
|
-
const matchers = listOperations(
|
|
4238
|
+
var operationMatcher = (document3) => {
|
|
4239
|
+
const matchers = listOperations(document3).map((operation) => ({
|
|
3439
4240
|
operationId: operation.operationId,
|
|
3440
4241
|
method: operation.method.toUpperCase(),
|
|
3441
4242
|
pattern: new RegExp(`^${operation.path.split("/").map((segment) => segment.startsWith("{") ? "[^/]+" : segment.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("/")}/?$`),
|
|
@@ -3530,10 +4331,10 @@ var createRuntime = (options) => {
|
|
|
3530
4331
|
const ensureBranch = (namespace, branch2, at) => {
|
|
3531
4332
|
if (!BRANCH_PATTERN2.test(branch2))
|
|
3532
4333
|
throw new RangeError(`branch must match ${BRANCH_PATTERN2}`);
|
|
3533
|
-
const
|
|
4334
|
+
const history2 = timeline(namespace);
|
|
3534
4335
|
if (branch2 === "main") {
|
|
3535
4336
|
if (at !== void 0) {
|
|
3536
|
-
const point =
|
|
4337
|
+
const point = history2.checkout("main", at);
|
|
3537
4338
|
restoreNamespace(sqlite, storageNamespace(namespace), point.value.snapshot);
|
|
3538
4339
|
captured.set(namespace, point.value.snapshot);
|
|
3539
4340
|
rng.setState(point.value.rngState);
|
|
@@ -3546,10 +4347,10 @@ var createRuntime = (options) => {
|
|
|
3546
4347
|
return namespace;
|
|
3547
4348
|
}
|
|
3548
4349
|
const storage = physicalBranch(namespace, branch2);
|
|
3549
|
-
if (!
|
|
4350
|
+
if (!history2.hasBranch(branch2)) {
|
|
3550
4351
|
if (at === void 0)
|
|
3551
|
-
|
|
3552
|
-
const point =
|
|
4352
|
+
history2.commit(capture(namespace));
|
|
4353
|
+
const point = history2.fork(branch2, at === void 0 ? {} : { from: at });
|
|
3553
4354
|
const branchRng = createRng(options.seed ?? 0);
|
|
3554
4355
|
if (point)
|
|
3555
4356
|
branchRng.setState(point.value.rngState);
|
|
@@ -3558,8 +4359,8 @@ var createRuntime = (options) => {
|
|
|
3558
4359
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
3559
4360
|
if (point)
|
|
3560
4361
|
captured.set(storage, point.value.snapshot);
|
|
3561
|
-
} else if (at !== void 0 &&
|
|
3562
|
-
const point =
|
|
4362
|
+
} else if (at !== void 0 && history2.head(branch2)?.id !== at) {
|
|
4363
|
+
const point = history2.checkout(branch2, at);
|
|
3563
4364
|
if (!instances.has(storage)) {
|
|
3564
4365
|
const branchRng = createRng(options.seed ?? 0);
|
|
3565
4366
|
branchRng.setState(point.value.rngState);
|
|
@@ -3570,7 +4371,7 @@ var createRuntime = (options) => {
|
|
|
3570
4371
|
captured.set(storage, point.value.snapshot);
|
|
3571
4372
|
} else {
|
|
3572
4373
|
if (!instances.has(storage)) {
|
|
3573
|
-
const point =
|
|
4374
|
+
const point = history2.head(branch2);
|
|
3574
4375
|
const branchRng = createRng(options.seed ?? 0);
|
|
3575
4376
|
if (point)
|
|
3576
4377
|
branchRng.setState(point.value.rngState);
|
|
@@ -3594,8 +4395,8 @@ var createRuntime = (options) => {
|
|
|
3594
4395
|
const checkout = (checkpointId, checkoutOptions = {}) => {
|
|
3595
4396
|
const namespace = checkoutOptions.namespace ?? DEFAULT_NAMESPACE;
|
|
3596
4397
|
const branchName = checkoutOptions.branch ?? "main";
|
|
3597
|
-
const
|
|
3598
|
-
const point =
|
|
4398
|
+
const history2 = timeline(namespace);
|
|
4399
|
+
const point = history2.checkout(branchName, checkpointId);
|
|
3599
4400
|
const storage = ensureBranch(namespace, branchName);
|
|
3600
4401
|
restoreNamespace(sqlite, storageNamespace(storage), point.value.snapshot);
|
|
3601
4402
|
captured.set(storage, point.value.snapshot);
|
|
@@ -3645,9 +4446,9 @@ var createRuntime = (options) => {
|
|
|
3645
4446
|
instance(name);
|
|
3646
4447
|
restoreNamespace(sqlite, storageNamespace(name), from);
|
|
3647
4448
|
captured.set(name, from);
|
|
3648
|
-
const
|
|
3649
|
-
if (
|
|
3650
|
-
|
|
4449
|
+
const history2 = timelines.get(name);
|
|
4450
|
+
if (history2)
|
|
4451
|
+
history2.commit(capture(name), { branch: "main" });
|
|
3651
4452
|
else
|
|
3652
4453
|
timeline(name);
|
|
3653
4454
|
};
|
|
@@ -3876,10 +4677,10 @@ var createRuntime = (options) => {
|
|
|
3876
4677
|
},
|
|
3877
4678
|
release: (name, checkpointId) => timeline(name).release(checkpointId),
|
|
3878
4679
|
inspect: (name) => {
|
|
3879
|
-
const
|
|
4680
|
+
const history2 = timeline(name);
|
|
3880
4681
|
return {
|
|
3881
|
-
branches:
|
|
3882
|
-
checkpoints:
|
|
4682
|
+
branches: history2.branches(),
|
|
4683
|
+
checkpoints: history2.checkpoints().map(({ id, branch: branchName, parent, at }) => ({
|
|
3883
4684
|
id,
|
|
3884
4685
|
branch: branchName,
|
|
3885
4686
|
parent,
|
|
@@ -3894,11 +4695,11 @@ var createRuntime = (options) => {
|
|
|
3894
4695
|
} : {},
|
|
3895
4696
|
routes: {
|
|
3896
4697
|
...credentialRoutes(credentials),
|
|
3897
|
-
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3898
4698
|
...options.webhooks ? webhookAdminRoutes(options.webhooks) : {},
|
|
3899
4699
|
...options.admin?.(runtime) ?? {},
|
|
3900
|
-
//
|
|
3901
|
-
//
|
|
4700
|
+
// Shell routes win a colliding key. Fault presets are one of them: the admin page
|
|
4701
|
+
// maps `presets`, and a service route must not replace that list with a record.
|
|
4702
|
+
...options.presets ? presetRoutes(options.presets, runtime) : {},
|
|
3902
4703
|
...stateAdminRoutes({
|
|
3903
4704
|
open: stateScope,
|
|
3904
4705
|
afterWrite: (namespace) => {
|
|
@@ -3973,9 +4774,12 @@ var credentialRoutes = (registry) => ({
|
|
|
3973
4774
|
}
|
|
3974
4775
|
});
|
|
3975
4776
|
var presetRoutes = (presets, runtime) => ({
|
|
3976
|
-
"GET /faults/presets": () =>
|
|
3977
|
-
|
|
3978
|
-
|
|
4777
|
+
"GET /faults/presets": () => {
|
|
4778
|
+
const body = {
|
|
4779
|
+
presets: Object.entries(presets).map(([name, preset]) => ({ name, ...preset }))
|
|
4780
|
+
};
|
|
4781
|
+
return adminJson(200, body);
|
|
4782
|
+
},
|
|
3979
4783
|
"POST /faults/presets/:name": ({ params, body, namespace }) => {
|
|
3980
4784
|
const name = params.name;
|
|
3981
4785
|
if (!presets[name])
|
|
@@ -3986,7 +4790,7 @@ var presetRoutes = (presets, runtime) => ({
|
|
|
3986
4790
|
});
|
|
3987
4791
|
|
|
3988
4792
|
// src/generated/openapi.ts
|
|
3989
|
-
var document = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Fullscript API (Mockingbird subset)","description":"Stateful mock subset of the Fullscript API for lab ordering: per-practitioner OAuth (authorize, token with authorization_code and refresh_token grants, revoke), clinic, embeddable session grants, lab orders, lab-order events, expiring result PDFs, and signed webhooks. Hand-authored from the EMR consumer (fullscript-api-client.ts, fullscript-event-model.ts, fullscript-webhook-signature.ts).\\n","version":"1","x-mockingbird-upstream":{"note":"Wire shapes from the consumer app's EMR backend (its Fullscript services and routers); the vendor's API reference is behind a partner login."}},"servers":[{"url":"https://api-us-snd.fullscript.io"}],"security":[{"bearerAuth":[]}],"paths":{"/api/oauth/token":{"post":{"operationId":"OAuthToken","description":"Exchange an authorization code, or rotate a refresh token (the old refresh token stops working).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenResponse"}}}},"400":{"description":"invalid_grant / unsupported_grant_type / invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}}}},"/api/oauth/revoke":{"post":{"operationId":"OAuthRevoke","description":"Revoke an access or refresh token (RFC 7009: always 200).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}}}}}},"/api/clinic":{"get":{"operationId":"GetClinic","description":"The token's clinic.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Clinic","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClinicResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/embeddable/session_grants":{"post":{"operationId":"CreateSessionGrant","description":"A short-lived secret for the embedded Fullscript widget.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"201":{"description":"Grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionGrant"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/labs/orders":{"get":{"operationId":"ListLabOrders","description":"The clinic's lab orders (optionally one patient's), paged.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrdersPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"patient_id","in":"query","required":false,"schema":{"type":"string","enum":["pat_seed_1","pat_seed_2","pat_missing"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/clinic/labs/orders/{orderId}":{"get":{"operationId":"GetLabOrder","description":"One lab order with tests, per-test results and the aggregated result.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrderResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string","enum":["lo_seed_1","lo_seed_2","lo_seed_3","lo_missing"]}}]}},"/api/events/lab_orders":{"get":{"operationId":"ListLabOrderEvents","description":"The clinic's lab_order.updated events, newest first by default.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"order_by","in":"query","required":false,"schema":{"type":"string","enum":["DESC","ASC"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/events/{eventId}":{"get":{"operationId":"GetEvent","description":"One event with its data.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string","enum":["evt_1","evt_2","evt_3","evt_missing"]}}]}},"/oauth/authorize":{"get":{"operationId":"Authorize","description":"The consent page: auto-approves as the requested (or default) practitioner and redirects to redirect_uri with code and state.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Browser redirect flow; covered by acceptance tests."}},"parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"}},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string"}},{"name":"practitioner_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect back with ?code=&state="},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/results/{artifactId}":{"get":{"operationId":"GetResultPdf","description":"An expiring result PDF URL (what pdf_url points at).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Binary PDF behind an expiring signed URL; covered by acceptance tests."}},"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"}},{"name":"expires","in":"query","required":true,"schema":{"type":"string"}},{"name":"signature","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"403":{"description":"Expired or bad signature"},"404":{"description":"Unknown artifact"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"TokenRequest":{"type":"object","required":["grant_type","client_id","client_secret"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"client_id":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-client"]},"client_secret":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-secret"]},"code":{"type":"string","maxLength":64},"refresh_token":{"type":"string","maxLength":200},"redirect_uri":{"type":"string","maxLength":200,"examples":["https://emr.example.com/v1/fullscript/oauth/callback"]}}},"RevokeRequest":{"type":"object","required":["client_id","client_secret","token"],"properties":{"client_id":{"type":"string","minLength":1,"maxLength":64},"client_secret":{"type":"string","minLength":1,"maxLength":64},"token":{"type":"string","minLength":1,"maxLength":200}}},"OAuthTokenResponse":{"type":"object","required":["oauth"],"properties":{"oauth":{"type":"object","required":["access_token","token_type","expires_in","refresh_token","scope","created_at","resource_owner"],"properties":{"access_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"},"refresh_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"scope":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"resource_owner":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["Practitioner","Staff"]},"clinic_id":{"type":"string"}}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"ApiErrors":{"type":"object","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}},"ClinicResponse":{"type":"object","required":["clinic"],"properties":{"clinic":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}}}},"SessionGrant":{"type":"object","required":["secret_token","expires_at"],"properties":{"secret_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"expires_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrderSummary":{"type":"object","required":["id","state","treatment_plan_id","patient_id","created_at","updated_at"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrdersPage":{"type":"object","required":["orders","meta"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/LabOrderSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"LabOrderResponse":{"type":"object","required":["order"],"properties":{"order":{"type":"object","required":["id","state","treatment_plan_id","tests","results","latest_aggregated_result"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"collection_method":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"tests":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"lab_type":{"type":"string"}}}},"results":{"type":"array","items":{"type":"object","required":["id","name","state","pdf_url"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"state":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}}},"latest_aggregated_result":{"type":["object","null"],"properties":{"id":{"type":"string"},"artifact_id":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"status":{"type":"string"}}}}}}},"EventSummary":{"type":"object","required":["id","type","clinic_id","created_at"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["lab_order.updated"]},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"EventsPage":{"type":"object","required":["events","meta"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/EventSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"EventResponse":{"type":"object","required":["event"],"properties":{"event":{"type":"object","required":["id","type","clinic_id","created_at","data"],"properties":{"id":{"type":"string"},"type":{"type":"string"},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"data":{"type":"object"}}}}}}}}`);
|
|
4793
|
+
var document2 = JSON.parse(`{"openapi":"3.1.0","info":{"title":"Fullscript API (Mockingbird subset)","description":"Stateful mock subset of the Fullscript API for lab ordering: per-practitioner OAuth (authorize, token with authorization_code and refresh_token grants, revoke), clinic, embeddable session grants, lab orders, lab-order events, expiring result PDFs, and signed webhooks. Hand-authored from the EMR consumer (fullscript-api-client.ts, fullscript-event-model.ts, fullscript-webhook-signature.ts).\\n","version":"1","x-mockingbird-upstream":{"note":"Wire shapes from the consumer app's EMR backend (its Fullscript services and routers); the vendor's API reference is behind a partner login."}},"servers":[{"url":"https://api-us-snd.fullscript.io"}],"security":[{"bearerAuth":[]}],"paths":{"/api/oauth/token":{"post":{"operationId":"OAuthToken","description":"Exchange an authorization code, or rotate a refresh token (the old refresh token stops working).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenResponse"}}}},"400":{"description":"invalid_grant / unsupported_grant_type / invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TokenRequest"}}}}}},"/api/oauth/revoke":{"post":{"operationId":"OAuthRevoke","description":"Revoke an access or refresh token (RFC 7009: always 200).","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":false}},"responses":{"200":{"description":"Revoked","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"invalid_request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"401":{"description":"invalid_client","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RevokeRequest"}}}}}},"/api/clinic":{"get":{"operationId":"GetClinic","description":"The token's clinic.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Clinic","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ClinicResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/embeddable/session_grants":{"post":{"operationId":"CreateSessionGrant","description":"A short-lived secret for the embedded Fullscript widget.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"201":{"description":"Grant","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionGrant"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}}}},"/api/clinic/labs/orders":{"get":{"operationId":"ListLabOrders","description":"The clinic's lab orders (optionally one patient's), paged.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrdersPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"patient_id","in":"query","required":false,"schema":{"type":"string","enum":["pat_seed_1","pat_seed_2","pat_missing"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/clinic/labs/orders/{orderId}":{"get":{"operationId":"GetLabOrder","description":"One lab order with tests, per-test results and the aggregated result.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LabOrderResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown order","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"orderId","in":"path","required":true,"schema":{"type":"string","enum":["lo_seed_1","lo_seed_2","lo_seed_3","lo_missing"]}}]}},"/api/events/lab_orders":{"get":{"operationId":"ListLabOrderEvents","description":"The clinic's lab_order.updated events, newest first by default.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsPage"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"order_by","in":"query","required":false,"schema":{"type":"string","enum":["DESC","ASC"]}},{"name":"page[number]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":3}},{"name":"page[size]","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100}}]}},"/api/events/{eventId}":{"get":{"operationId":"GetEvent","description":"One event with its data.","x-mockingbird":{"supported":true,"parity":{"enabled":true,"safe":true}},"responses":{"200":{"description":"Event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventResponse"}}}},"401":{"description":"Missing, invalid, expired or revoked access token","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"404":{"description":"Unknown event","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApiErrors"}}}}},"parameters":[{"name":"eventId","in":"path","required":true,"schema":{"type":"string","enum":["evt_1","evt_2","evt_3","evt_missing"]}}]}},"/oauth/authorize":{"get":{"operationId":"Authorize","description":"The consent page: auto-approves as the requested (or default) practitioner and redirects to redirect_uri with code and state.","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Browser redirect flow; covered by acceptance tests."}},"parameters":[{"name":"client_id","in":"query","required":true,"schema":{"type":"string"}},{"name":"redirect_uri","in":"query","required":true,"schema":{"type":"string"}},{"name":"response_type","in":"query","required":true,"schema":{"type":"string","enum":["code"]}},{"name":"state","in":"query","required":false,"schema":{"type":"string"}},{"name":"scope","in":"query","required":false,"schema":{"type":"string"}},{"name":"practitioner_id","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"302":{"description":"Redirect back with ?code=&state="},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthError"}}}}}}},"/results/{artifactId}":{"get":{"operationId":"GetResultPdf","description":"An expiring result PDF URL (what pdf_url points at).","security":[],"x-mockingbird":{"supported":true,"parity":{"enabled":false,"reason":"Binary PDF behind an expiring signed URL; covered by acceptance tests."}},"parameters":[{"name":"artifactId","in":"path","required":true,"schema":{"type":"string"}},{"name":"expires","in":"query","required":true,"schema":{"type":"string"}},{"name":"signature","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The PDF","content":{"application/pdf":{"schema":{"type":"string","format":"binary"}}}},"403":{"description":"Expired or bad signature"},"404":{"description":"Unknown artifact"}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"TokenRequest":{"type":"object","required":["grant_type","client_id","client_secret"],"properties":{"grant_type":{"type":"string","enum":["authorization_code","refresh_token"]},"client_id":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-client"]},"client_secret":{"type":"string","minLength":1,"maxLength":64,"examples":["parity-secret"]},"code":{"type":"string","maxLength":64},"refresh_token":{"type":"string","maxLength":200},"redirect_uri":{"type":"string","maxLength":200,"examples":["https://emr.example.com/v1/fullscript/oauth/callback"]}}},"RevokeRequest":{"type":"object","required":["client_id","client_secret","token"],"properties":{"client_id":{"type":"string","minLength":1,"maxLength":64},"client_secret":{"type":"string","minLength":1,"maxLength":64},"token":{"type":"string","minLength":1,"maxLength":200}}},"OAuthTokenResponse":{"type":"object","required":["oauth"],"properties":{"oauth":{"type":"object","required":["access_token","token_type","expires_in","refresh_token","scope","created_at","resource_owner"],"properties":{"access_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"token_type":{"type":"string","enum":["Bearer"]},"expires_in":{"type":"integer"},"refresh_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"scope":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"resource_owner":{"type":"object","required":["id","type"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["Practitioner","Staff"]},"clinic_id":{"type":"string"}}}}}}},"OAuthError":{"type":"object","required":["error"],"properties":{"error":{"type":"string"},"error_description":{"type":"string"}}},"ApiErrors":{"type":"object","required":["errors"],"properties":{"errors":{"type":"array","items":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"}}}}}},"ClinicResponse":{"type":"object","required":["clinic"],"properties":{"clinic":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}}}},"SessionGrant":{"type":"object","required":["secret_token","expires_at"],"properties":{"secret_token":{"type":"string","x-mockingbird-volatile":{"kind":"token"}},"expires_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrderSummary":{"type":"object","required":["id","state","treatment_plan_id","patient_id","created_at","updated_at"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"LabOrdersPage":{"type":"object","required":["orders","meta"],"properties":{"orders":{"type":"array","items":{"$ref":"#/components/schemas/LabOrderSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"LabOrderResponse":{"type":"object","required":["order"],"properties":{"order":{"type":"object","required":["id","state","treatment_plan_id","tests","results","latest_aggregated_result"],"properties":{"id":{"type":"string"},"state":{"type":"string","enum":["not_purchased","purchased","schedule_appointment","upcoming_appointment","processing","partial_results","results_ready","interpretation_shared","results_amended"]},"treatment_plan_id":{"type":["string","null"]},"patient_id":{"type":"string"},"name":{"type":"string"},"collection_method":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"updated_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"tests":{"type":"array","items":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"lab_type":{"type":"string"}}}},"results":{"type":"array","items":{"type":"object","required":["id","name","state","pdf_url"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"state":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}}}}},"latest_aggregated_result":{"type":["object","null"],"properties":{"id":{"type":"string"},"artifact_id":{"type":"string"},"pdf_url":{"type":"string","x-mockingbird-volatile":{"kind":"url"}},"status":{"type":"string"}}}}}}},"EventSummary":{"type":"object","required":["id","type","clinic_id","created_at"],"properties":{"id":{"type":"string"},"type":{"type":"string","enum":["lab_order.updated"]},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}}}},"EventsPage":{"type":"object","required":["events","meta"],"properties":{"events":{"type":"array","items":{"$ref":"#/components/schemas/EventSummary"}},"meta":{"type":"object","required":["current_page","next_page","prev_page","total_pages","total_count"],"properties":{"current_page":{"type":"integer"},"next_page":{"type":["integer","null"]},"prev_page":{"type":["integer","null"]},"total_pages":{"type":"integer"},"total_count":{"type":"integer"}}}}},"EventResponse":{"type":"object","required":["event"],"properties":{"event":{"type":"object","required":["id","type","clinic_id","created_at","data"],"properties":{"id":{"type":"string"},"type":{"type":"string"},"clinic_id":{"type":"string"},"created_at":{"type":"string","x-mockingbird-volatile":{"kind":"timestamp"}},"data":{"type":"object"}}}}}}}}`);
|
|
3990
4794
|
var operationIds = ["OAuthToken", "OAuthRevoke", "GetClinic", "CreateSessionGrant", "ListLabOrders", "GetLabOrder", "ListLabOrderEvents", "GetEvent", "Authorize", "GetResultPdf"];
|
|
3991
4795
|
var supportedOperationIds = ["OAuthToken", "OAuthRevoke", "GetClinic", "CreateSessionGrant", "ListLabOrders", "GetLabOrder", "ListLabOrderEvents", "GetEvent", "Authorize", "GetResultPdf"];
|
|
3992
4796
|
|
|
@@ -4173,7 +4977,7 @@ var FULLSCRIPT_PRESETS = {
|
|
|
4173
4977
|
};
|
|
4174
4978
|
var json5 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
4175
4979
|
var adminError4 = (status, message) => json5(status, { error: { type: "mockingbird_admin", message } });
|
|
4176
|
-
var
|
|
4980
|
+
var isRecord6 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4177
4981
|
var echoes = async (response, challenge) => {
|
|
4178
4982
|
try {
|
|
4179
4983
|
const body = await response.clone().json();
|
|
@@ -4184,7 +4988,7 @@ var echoes = async (response, challenge) => {
|
|
|
4184
4988
|
};
|
|
4185
4989
|
var adminRoutes = (send, challenge) => (runtime) => ({
|
|
4186
4990
|
"POST /oauth/codes": ({ body, namespace }) => {
|
|
4187
|
-
const input =
|
|
4991
|
+
const input = isRecord6(body) ? body : {};
|
|
4188
4992
|
const api = runtime.instance(namespace);
|
|
4189
4993
|
const practitionerId = typeof input.practitionerId === "string" ? input.practitionerId : "prac_mock_1";
|
|
4190
4994
|
if (!api.state.practitioners.get(practitionerId))
|
|
@@ -4197,7 +5001,7 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
4197
5001
|
return json5(201, { code });
|
|
4198
5002
|
},
|
|
4199
5003
|
"POST /practitioners": ({ body, namespace }) => {
|
|
4200
|
-
if (!
|
|
5004
|
+
if (!isRecord6(body) || typeof body.id !== "string" || typeof body.clinicId !== "string") {
|
|
4201
5005
|
return adminError4(
|
|
4202
5006
|
400,
|
|
4203
5007
|
'expected {"id", "clinicId", "type"?: "Practitioner"|"Staff", "clinicName"?}'
|
|
@@ -4221,7 +5025,7 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
4221
5025
|
},
|
|
4222
5026
|
"GET /lab-orders": ({ namespace }) => json5(200, { orders: runtime.instance(namespace).labOrders() }),
|
|
4223
5027
|
"POST /lab-orders": ({ body, namespace }) => {
|
|
4224
|
-
if (!
|
|
5028
|
+
if (!isRecord6(body) || typeof body.patientId !== "string") {
|
|
4225
5029
|
return adminError4(
|
|
4226
5030
|
400,
|
|
4227
5031
|
'expected {"patientId", "id"?, "clinicId"?, "treatmentPlanId"?, "name"?, "collectionMethod"?, "tests"?: [names]}'
|
|
@@ -4240,7 +5044,7 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
4240
5044
|
return json5(201, order);
|
|
4241
5045
|
},
|
|
4242
5046
|
"POST /lab-orders/:id/transition": ({ params, body, namespace }) => {
|
|
4243
|
-
if (!
|
|
5047
|
+
if (!isRecord6(body) || !isLabOrderState(body.to)) {
|
|
4244
5048
|
return adminError4(
|
|
4245
5049
|
400,
|
|
4246
5050
|
'expected {"to": "<purchased|schedule_appointment|upcoming_appointment|processing|partial_results|results_ready|interpretation_shared|results_amended>"}'
|
|
@@ -4283,7 +5087,7 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
4283
5087
|
},
|
|
4284
5088
|
"GET /settings": ({ namespace }) => json5(200, runtime.instance(namespace).state.current()),
|
|
4285
5089
|
"PUT /settings": ({ body, namespace }) => {
|
|
4286
|
-
if (!
|
|
5090
|
+
if (!isRecord6(body)) return adminError4(400, "expected a JSON object");
|
|
4287
5091
|
const patch = {};
|
|
4288
5092
|
for (const key of ["accessTokenTtlSeconds", "codeTtlSeconds", "pdfUrlTtlSeconds"]) {
|
|
4289
5093
|
if (body[key] !== void 0) {
|
|
@@ -4299,7 +5103,7 @@ var adminRoutes = (send, challenge) => (runtime) => ({
|
|
|
4299
5103
|
if (body.clients !== void 0) {
|
|
4300
5104
|
if (!Array.isArray(body.clients))
|
|
4301
5105
|
return adminError4(400, "clients: [{clientId, clientSecret}]");
|
|
4302
|
-
patch.clients = body.clients.filter(
|
|
5106
|
+
patch.clients = body.clients.filter(isRecord6).map((c) => ({ clientId: String(c.clientId), clientSecret: String(c.clientSecret) }));
|
|
4303
5107
|
}
|
|
4304
5108
|
return json5(200, runtime.instance(namespace).state.update(patch));
|
|
4305
5109
|
}
|
|
@@ -4321,7 +5125,7 @@ var createRuntime2 = (options = {}) => {
|
|
|
4321
5125
|
});
|
|
4322
5126
|
const runtime = createRuntime({
|
|
4323
5127
|
name: FULLSCRIPT_NAMESPACE,
|
|
4324
|
-
document,
|
|
5128
|
+
document: document2,
|
|
4325
5129
|
...options.sqlite ? { sqlite: options.sqlite } : {},
|
|
4326
5130
|
...options.clock ? { clock: options.clock } : {},
|
|
4327
5131
|
...options.seed !== void 0 ? { seed: options.seed } : {},
|
|
@@ -4512,7 +5316,7 @@ var FullscriptAPI = class {
|
|
|
4512
5316
|
GetResultPdf: (context) => this.pdf(context)
|
|
4513
5317
|
});
|
|
4514
5318
|
this.service = createService({
|
|
4515
|
-
document,
|
|
5319
|
+
document: document2,
|
|
4516
5320
|
handlers,
|
|
4517
5321
|
sqlite,
|
|
4518
5322
|
namespace,
|
|
@@ -4679,11 +5483,11 @@ var FullscriptAPI = class {
|
|
|
4679
5483
|
return oauthError(400, "invalid_request", `Unknown practitioner ${practitionerId}.`);
|
|
4680
5484
|
}
|
|
4681
5485
|
const code = this.issueCode(practitionerId, clientId, redirect);
|
|
4682
|
-
const
|
|
4683
|
-
|
|
5486
|
+
const location2 = new URL(redirect);
|
|
5487
|
+
location2.searchParams.set("code", code);
|
|
4684
5488
|
const state = url.searchParams.get("state");
|
|
4685
|
-
if (state !== null)
|
|
4686
|
-
return new Response(null, { status: 302, headers: { location:
|
|
5489
|
+
if (state !== null) location2.searchParams.set("state", state);
|
|
5490
|
+
return new Response(null, { status: 302, headers: { location: location2.toString() } });
|
|
4687
5491
|
}
|
|
4688
5492
|
summary(order) {
|
|
4689
5493
|
return {
|
|
@@ -4863,7 +5667,7 @@ var FullscriptAPI = class {
|
|
|
4863
5667
|
};
|
|
4864
5668
|
|
|
4865
5669
|
export {
|
|
4866
|
-
document,
|
|
5670
|
+
document2 as document,
|
|
4867
5671
|
operationIds,
|
|
4868
5672
|
supportedOperationIds,
|
|
4869
5673
|
RESULT_PDF,
|
|
@@ -4883,4 +5687,4 @@ export {
|
|
|
4883
5687
|
envelope,
|
|
4884
5688
|
FullscriptAPI
|
|
4885
5689
|
};
|
|
4886
|
-
//# sourceMappingURL=chunk-
|
|
5690
|
+
//# sourceMappingURL=chunk-OBQHBBFC.js.map
|