@objectstack/rest 6.9.0 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +290 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +290 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -668,6 +668,7 @@ var RestServer = class {
|
|
|
668
668
|
const userId = session.user.id;
|
|
669
669
|
const tenantId = session.session?.activeOrganizationId ?? void 0;
|
|
670
670
|
const permissions = [];
|
|
671
|
+
const systemPermissions = [];
|
|
671
672
|
const roles = [];
|
|
672
673
|
try {
|
|
673
674
|
let ql;
|
|
@@ -712,6 +713,20 @@ var RestServer = class {
|
|
|
712
713
|
}).catch(() => []);
|
|
713
714
|
for (const ps of psRows ?? []) {
|
|
714
715
|
if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
|
|
716
|
+
const rawSys = typeof ps.system_permissions === "string" ? (() => {
|
|
717
|
+
try {
|
|
718
|
+
return JSON.parse(ps.system_permissions);
|
|
719
|
+
} catch {
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
})() : ps.system_permissions ?? ps.systemPermissions;
|
|
723
|
+
if (Array.isArray(rawSys)) {
|
|
724
|
+
for (const sp of rawSys) {
|
|
725
|
+
if (typeof sp === "string" && !systemPermissions.includes(sp)) {
|
|
726
|
+
systemPermissions.push(sp);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
715
730
|
}
|
|
716
731
|
}
|
|
717
732
|
}
|
|
@@ -749,6 +764,7 @@ var RestServer = class {
|
|
|
749
764
|
tenantId,
|
|
750
765
|
roles,
|
|
751
766
|
permissions,
|
|
767
|
+
systemPermissions,
|
|
752
768
|
isSystem: false,
|
|
753
769
|
org_user_ids
|
|
754
770
|
};
|
|
@@ -756,6 +772,45 @@ var RestServer = class {
|
|
|
756
772
|
return void 0;
|
|
757
773
|
}
|
|
758
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Filter an `App` metadata item by the current user's `systemPermissions`.
|
|
777
|
+
*
|
|
778
|
+
* - Drops the app entirely if its top-level `requiredPermissions` are not
|
|
779
|
+
* a subset of the user's system permissions.
|
|
780
|
+
* - Recursively strips child navigation entries (groups, items) whose
|
|
781
|
+
* `requiredPermissions` are not satisfied. Empty groups collapse so
|
|
782
|
+
* the sidebar doesn't render a label with no children.
|
|
783
|
+
*
|
|
784
|
+
* Returns `null` when the app should be hidden from the user. Returns a
|
|
785
|
+
* shallow copy with a filtered `navigation` tree otherwise — the original
|
|
786
|
+
* is never mutated so cached metadata stays clean.
|
|
787
|
+
*/
|
|
788
|
+
filterAppForUser(item, sysPerms) {
|
|
789
|
+
if (!item || typeof item !== "object") return item;
|
|
790
|
+
const reqApp = Array.isArray(item.requiredPermissions) ? item.requiredPermissions : [];
|
|
791
|
+
if (reqApp.length > 0 && !reqApp.every((p) => sysPerms.has(p))) {
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
const nav = Array.isArray(item.navigation) ? item.navigation : null;
|
|
795
|
+
if (!nav) return item;
|
|
796
|
+
const filterNav = (entries) => {
|
|
797
|
+
const out = [];
|
|
798
|
+
for (const e of entries) {
|
|
799
|
+
if (!e || typeof e !== "object") continue;
|
|
800
|
+
const req = Array.isArray(e.requiredPermissions) ? e.requiredPermissions : [];
|
|
801
|
+
if (req.length > 0 && !req.every((p) => sysPerms.has(p))) continue;
|
|
802
|
+
if (Array.isArray(e.children) && e.children.length > 0) {
|
|
803
|
+
const kids = filterNav(e.children);
|
|
804
|
+
if (e.type === "group" && kids.length === 0) continue;
|
|
805
|
+
out.push({ ...e, children: kids });
|
|
806
|
+
} else {
|
|
807
|
+
out.push(e);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
return out;
|
|
811
|
+
};
|
|
812
|
+
return { ...item, navigation: filterNav(nav) };
|
|
813
|
+
}
|
|
759
814
|
/**
|
|
760
815
|
* Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
|
|
761
816
|
* `II18nService` instance. Returns `undefined` when no locales are
|
|
@@ -829,6 +884,41 @@ var RestServer = class {
|
|
|
829
884
|
const { translateMetadataDocument } = await import("@objectstack/spec/system");
|
|
830
885
|
return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
|
|
831
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Translate the `entries` payload returned by `getMetaTypes()` — applies
|
|
889
|
+
* the active locale to each entry's `label`, `description`, and the
|
|
890
|
+
* nested `form` layout (section labels, field labels, helpText,
|
|
891
|
+
* placeholders) via `metadataForms.<type>` translation namespace.
|
|
892
|
+
*
|
|
893
|
+
* No-ops when no i18n service / locale / matching bundle entry exists,
|
|
894
|
+
* so this is safe to call unconditionally from the `/meta` handler.
|
|
895
|
+
*/
|
|
896
|
+
async translateMetaTypesResponse(req, environmentId, payload) {
|
|
897
|
+
if (!payload || typeof payload !== "object" || !Array.isArray(payload.entries)) return payload;
|
|
898
|
+
const i18n = await this.resolveI18nService(environmentId, req);
|
|
899
|
+
const bundle = this.buildTranslationBundle(i18n);
|
|
900
|
+
if (!bundle) return payload;
|
|
901
|
+
const locale = this.extractLocale(req, i18n);
|
|
902
|
+
if (!locale) return payload;
|
|
903
|
+
const {
|
|
904
|
+
resolveMetadataTypeLabel,
|
|
905
|
+
resolveMetadataTypeDescription,
|
|
906
|
+
resolveMetadataFormLabels
|
|
907
|
+
} = await import("@objectstack/spec/system");
|
|
908
|
+
const opts = { locale };
|
|
909
|
+
const entries = payload.entries.map((entry) => {
|
|
910
|
+
if (!entry || typeof entry !== "object" || typeof entry.type !== "string") return entry;
|
|
911
|
+
const next = { ...entry };
|
|
912
|
+
next.label = resolveMetadataTypeLabel(bundle, entry.type, entry.label ?? entry.type, opts);
|
|
913
|
+
const desc = resolveMetadataTypeDescription(bundle, entry.type, entry.description, opts);
|
|
914
|
+
if (desc !== void 0) next.description = desc;
|
|
915
|
+
if (entry.form) {
|
|
916
|
+
next.form = resolveMetadataFormLabels(entry.form, entry.type, bundle, opts);
|
|
917
|
+
}
|
|
918
|
+
return next;
|
|
919
|
+
});
|
|
920
|
+
return { ...payload, entries };
|
|
921
|
+
}
|
|
832
922
|
/**
|
|
833
923
|
* Pull the request hostname (without port) from a Node-style `req` or
|
|
834
924
|
* a Fetch-style request wrapper. Returns undefined when no Host header
|
|
@@ -1211,7 +1301,9 @@ var RestServer = class {
|
|
|
1211
1301
|
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1212
1302
|
const p = await this.resolveProtocol(environmentId, req);
|
|
1213
1303
|
const types = await p.getMetaTypes();
|
|
1214
|
-
|
|
1304
|
+
const translated = await this.translateMetaTypesResponse(req, environmentId, types);
|
|
1305
|
+
res.header("Vary", "Accept-Language");
|
|
1306
|
+
res.json(translated);
|
|
1215
1307
|
} catch (error) {
|
|
1216
1308
|
logError("[REST] Unhandled error:", error);
|
|
1217
1309
|
sendError(res, error);
|
|
@@ -1223,6 +1315,40 @@ var RestServer = class {
|
|
|
1223
1315
|
}
|
|
1224
1316
|
});
|
|
1225
1317
|
}
|
|
1318
|
+
if (metadata.endpoints.items !== false) {
|
|
1319
|
+
this.routeManager.register({
|
|
1320
|
+
method: "GET",
|
|
1321
|
+
path: `${metaPath}/diagnostics`,
|
|
1322
|
+
handler: async (req, res) => {
|
|
1323
|
+
try {
|
|
1324
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1325
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1326
|
+
if (typeof p.getMetaDiagnostics !== "function") {
|
|
1327
|
+
res.status(501).json({
|
|
1328
|
+
error: "not_implemented",
|
|
1329
|
+
message: "protocol.getMetaDiagnostics() is not available in this kernel"
|
|
1330
|
+
});
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
const severityParam = req.query?.severity ?? "error";
|
|
1334
|
+
const severity = severityParam === "warning" ? "warning" : "error";
|
|
1335
|
+
const result = await p.getMetaDiagnostics({
|
|
1336
|
+
type: req.query?.type || void 0,
|
|
1337
|
+
severity,
|
|
1338
|
+
packageId: req.query?.package || void 0
|
|
1339
|
+
});
|
|
1340
|
+
res.json(result);
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
logError("[REST] Unhandled error:", error);
|
|
1343
|
+
sendError(res, error);
|
|
1344
|
+
}
|
|
1345
|
+
},
|
|
1346
|
+
metadata: {
|
|
1347
|
+
summary: "List metadata entries that fail spec validation",
|
|
1348
|
+
tags: ["metadata"]
|
|
1349
|
+
}
|
|
1350
|
+
});
|
|
1351
|
+
}
|
|
1226
1352
|
if (metadata.endpoints.items !== false) {
|
|
1227
1353
|
this.routeManager.register({
|
|
1228
1354
|
method: "GET",
|
|
@@ -1237,7 +1363,22 @@ var RestServer = class {
|
|
|
1237
1363
|
packageId,
|
|
1238
1364
|
...environmentId ? { environmentId } : {}
|
|
1239
1365
|
});
|
|
1240
|
-
|
|
1366
|
+
let visible = items;
|
|
1367
|
+
if (req.params.type === "app") {
|
|
1368
|
+
const raw = items;
|
|
1369
|
+
const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
|
|
1370
|
+
if (list) {
|
|
1371
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1372
|
+
if (ctx?.userId) {
|
|
1373
|
+
const sysPerms = new Set(
|
|
1374
|
+
Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
|
|
1375
|
+
);
|
|
1376
|
+
const filtered = list.map((it) => this.filterAppForUser(it, sysPerms)).filter((it) => it != null);
|
|
1377
|
+
visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
const translated = await this.translateMetaItems(req, req.params.type, environmentId, visible);
|
|
1241
1382
|
res.header("Vary", "Accept-Language");
|
|
1242
1383
|
res.json(translated);
|
|
1243
1384
|
} catch (error) {
|
|
@@ -1296,7 +1437,9 @@ var RestServer = class {
|
|
|
1296
1437
|
res.json(layered);
|
|
1297
1438
|
return;
|
|
1298
1439
|
}
|
|
1299
|
-
|
|
1440
|
+
const isAppType = req.params.type === "app";
|
|
1441
|
+
const isDraftRead = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft";
|
|
1442
|
+
if (metadata.enableCache && p.getMetaItemCached && !isAppType && !isDraftRead) {
|
|
1300
1443
|
const cacheRequest = {
|
|
1301
1444
|
ifNoneMatch: req.headers["if-none-match"],
|
|
1302
1445
|
ifModifiedSince: req.headers["if-modified-since"]
|
|
@@ -1327,13 +1470,32 @@ var RestServer = class {
|
|
|
1327
1470
|
res.json(await this.translateMetaItem(req, req.params.type, environmentId, result.data));
|
|
1328
1471
|
} else {
|
|
1329
1472
|
const packageId = req.query?.package || void 0;
|
|
1473
|
+
const stateParam = typeof req.query?.state === "string" ? req.query.state.toLowerCase() : void 0;
|
|
1330
1474
|
const item = await p.getMetaItem({
|
|
1331
1475
|
type: req.params.type,
|
|
1332
1476
|
name: req.params.name,
|
|
1333
|
-
packageId
|
|
1477
|
+
packageId,
|
|
1478
|
+
...stateParam === "draft" ? { state: "draft" } : {}
|
|
1334
1479
|
});
|
|
1480
|
+
let visible = item;
|
|
1481
|
+
if (isAppType && item) {
|
|
1482
|
+
const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
|
|
1483
|
+
if (ctx?.userId) {
|
|
1484
|
+
const sysPerms = new Set(
|
|
1485
|
+
Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
|
|
1486
|
+
);
|
|
1487
|
+
visible = this.filterAppForUser(item, sysPerms);
|
|
1488
|
+
if (visible == null) {
|
|
1489
|
+
res.status(404).json({
|
|
1490
|
+
error: "not_found",
|
|
1491
|
+
message: "Metadata item not found or access denied."
|
|
1492
|
+
});
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1335
1497
|
res.header("Vary", "Accept-Language");
|
|
1336
|
-
res.json(await this.translateMetaItem(req, req.params.type, environmentId,
|
|
1498
|
+
res.json(await this.translateMetaItem(req, req.params.type, environmentId, visible));
|
|
1337
1499
|
}
|
|
1338
1500
|
} catch (error) {
|
|
1339
1501
|
logError("[REST] Unhandled error:", error);
|
|
@@ -1372,7 +1534,8 @@ var RestServer = class {
|
|
|
1372
1534
|
...environmentId ? { environmentId } : {},
|
|
1373
1535
|
...parentVersion !== void 0 ? { parentVersion } : {},
|
|
1374
1536
|
...actor ? { actor } : {},
|
|
1375
|
-
...force ? { force: true } : {}
|
|
1537
|
+
...force ? { force: true } : {},
|
|
1538
|
+
...typeof req.query?.mode === "string" && req.query.mode.toLowerCase() === "draft" ? { mode: "draft" } : {}
|
|
1376
1539
|
});
|
|
1377
1540
|
res.json(result);
|
|
1378
1541
|
} catch (error) {
|
|
@@ -1402,12 +1565,14 @@ var RestServer = class {
|
|
|
1402
1565
|
const parentVersion = typeof ifMatchHeader === "string" ? ifMatchHeader.replace(/^"|"$/g, "") : void 0;
|
|
1403
1566
|
const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
|
|
1404
1567
|
const actor = typeof actorHeader === "string" ? actorHeader : void 0;
|
|
1568
|
+
const stateParam = typeof req.query?.state === "string" && req.query.state.toLowerCase() === "draft" ? "draft" : void 0;
|
|
1405
1569
|
const result = await p.deleteMetaItem({
|
|
1406
1570
|
type: req.params.type,
|
|
1407
1571
|
name: req.params.name,
|
|
1408
1572
|
...environmentId ? { environmentId } : {},
|
|
1409
1573
|
...parentVersion !== void 0 ? { parentVersion } : {},
|
|
1410
|
-
...actor ? { actor } : {}
|
|
1574
|
+
...actor ? { actor } : {},
|
|
1575
|
+
...stateParam ? { state: stateParam } : {}
|
|
1411
1576
|
});
|
|
1412
1577
|
res.json(result);
|
|
1413
1578
|
} catch (error) {
|
|
@@ -1453,6 +1618,124 @@ var RestServer = class {
|
|
|
1453
1618
|
tags: ["metadata"]
|
|
1454
1619
|
}
|
|
1455
1620
|
});
|
|
1621
|
+
this.routeManager.register({
|
|
1622
|
+
method: "POST",
|
|
1623
|
+
path: `${metaPath}/:type/:name/publish`,
|
|
1624
|
+
handler: async (req, res) => {
|
|
1625
|
+
try {
|
|
1626
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1627
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1628
|
+
if (!p.publishMetaItem) {
|
|
1629
|
+
res.status(501).json({
|
|
1630
|
+
error: "Publish operation not supported by protocol implementation"
|
|
1631
|
+
});
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
|
|
1635
|
+
const actor = typeof actorHeader === "string" ? actorHeader : void 0;
|
|
1636
|
+
const body = req.body && typeof req.body === "object" ? req.body : {};
|
|
1637
|
+
const message = typeof body.message === "string" ? body.message : void 0;
|
|
1638
|
+
const result = await p.publishMetaItem({
|
|
1639
|
+
type: req.params.type,
|
|
1640
|
+
name: req.params.name,
|
|
1641
|
+
...environmentId ? { environmentId } : {},
|
|
1642
|
+
...actor ? { actor } : {},
|
|
1643
|
+
...message ? { message } : {}
|
|
1644
|
+
});
|
|
1645
|
+
res.json(result);
|
|
1646
|
+
} catch (error) {
|
|
1647
|
+
logError("[REST] Unhandled error:", error);
|
|
1648
|
+
sendError(res, error);
|
|
1649
|
+
}
|
|
1650
|
+
},
|
|
1651
|
+
metadata: {
|
|
1652
|
+
summary: "Publish the pending draft overlay (promotes draft \u2192 active)",
|
|
1653
|
+
tags: ["metadata"]
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1656
|
+
this.routeManager.register({
|
|
1657
|
+
method: "POST",
|
|
1658
|
+
path: `${metaPath}/:type/:name/rollback`,
|
|
1659
|
+
handler: async (req, res) => {
|
|
1660
|
+
try {
|
|
1661
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1662
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1663
|
+
if (!p.rollbackMetaItem) {
|
|
1664
|
+
res.status(501).json({
|
|
1665
|
+
error: "Rollback operation not supported by protocol implementation"
|
|
1666
|
+
});
|
|
1667
|
+
return;
|
|
1668
|
+
}
|
|
1669
|
+
const body = req.body && typeof req.body === "object" ? req.body : {};
|
|
1670
|
+
const toVersionRaw = body.toVersion ?? body.version ?? req.query?.toVersion;
|
|
1671
|
+
const toVersion = Number(toVersionRaw);
|
|
1672
|
+
if (!Number.isFinite(toVersion) || toVersion < 1) {
|
|
1673
|
+
res.status(400).json({
|
|
1674
|
+
error: `'toVersion' (positive integer) is required`,
|
|
1675
|
+
code: "invalid_request"
|
|
1676
|
+
});
|
|
1677
|
+
return;
|
|
1678
|
+
}
|
|
1679
|
+
const actorHeader = req.headers?.["x-actor"] ?? req.headers?.["X-Actor"] ?? req.user?.id ?? req.userId;
|
|
1680
|
+
const actor = typeof actorHeader === "string" ? actorHeader : void 0;
|
|
1681
|
+
const message = typeof body.message === "string" ? body.message : void 0;
|
|
1682
|
+
const result = await p.rollbackMetaItem({
|
|
1683
|
+
type: req.params.type,
|
|
1684
|
+
name: req.params.name,
|
|
1685
|
+
toVersion,
|
|
1686
|
+
...environmentId ? { environmentId } : {},
|
|
1687
|
+
...actor ? { actor } : {},
|
|
1688
|
+
...message ? { message } : {}
|
|
1689
|
+
});
|
|
1690
|
+
res.json(result);
|
|
1691
|
+
} catch (error) {
|
|
1692
|
+
logError("[REST] Unhandled error:", error);
|
|
1693
|
+
sendError(res, error);
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
metadata: {
|
|
1697
|
+
summary: "Restore the body at the given history version as the new live row",
|
|
1698
|
+
tags: ["metadata"]
|
|
1699
|
+
}
|
|
1700
|
+
});
|
|
1701
|
+
this.routeManager.register({
|
|
1702
|
+
method: "GET",
|
|
1703
|
+
path: `${metaPath}/:type/:name/diff`,
|
|
1704
|
+
handler: async (req, res) => {
|
|
1705
|
+
try {
|
|
1706
|
+
const environmentId = isScoped ? req.params?.environmentId : void 0;
|
|
1707
|
+
const p = await this.resolveProtocol(environmentId, req);
|
|
1708
|
+
if (!p.diffMetaItem) {
|
|
1709
|
+
res.status(501).json({
|
|
1710
|
+
error: "Diff operation not supported by protocol implementation"
|
|
1711
|
+
});
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const parseV = (raw) => {
|
|
1715
|
+
if (raw === void 0 || raw === null || raw === "") return void 0;
|
|
1716
|
+
const n = Number(raw);
|
|
1717
|
+
return Number.isFinite(n) ? n : void 0;
|
|
1718
|
+
};
|
|
1719
|
+
const fromVersion = parseV(req.query?.from ?? req.query?.fromVersion);
|
|
1720
|
+
const toVersion = parseV(req.query?.to ?? req.query?.toVersion);
|
|
1721
|
+
const result = await p.diffMetaItem({
|
|
1722
|
+
type: req.params.type,
|
|
1723
|
+
name: req.params.name,
|
|
1724
|
+
...environmentId ? { environmentId } : {},
|
|
1725
|
+
...fromVersion !== void 0 ? { fromVersion } : {},
|
|
1726
|
+
...toVersion !== void 0 ? { toVersion } : {}
|
|
1727
|
+
});
|
|
1728
|
+
res.json(result);
|
|
1729
|
+
} catch (error) {
|
|
1730
|
+
logError("[REST] Unhandled error:", error);
|
|
1731
|
+
sendError(res, error);
|
|
1732
|
+
}
|
|
1733
|
+
},
|
|
1734
|
+
metadata: {
|
|
1735
|
+
summary: "Diff two metadata versions (from/to query params; omit for previous-vs-current)",
|
|
1736
|
+
tags: ["metadata"]
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1456
1739
|
if (metadata.endpoints.item !== false) {
|
|
1457
1740
|
this.routeManager.register({
|
|
1458
1741
|
method: "GET",
|