@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 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
- res.json(types);
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
- const translated = await this.translateMetaItems(req, req.params.type, environmentId, items);
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
- if (metadata.enableCache && p.getMetaItemCached) {
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, item));
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",