@objectstack/rest 6.8.1 → 7.0.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);
@@ -1237,7 +1329,22 @@ var RestServer = class {
1237
1329
  packageId,
1238
1330
  ...environmentId ? { environmentId } : {}
1239
1331
  });
1240
- const translated = await this.translateMetaItems(req, req.params.type, environmentId, items);
1332
+ let visible = items;
1333
+ if (req.params.type === "app") {
1334
+ const raw = items;
1335
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
1336
+ if (list) {
1337
+ const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
1338
+ if (ctx?.userId) {
1339
+ const sysPerms = new Set(
1340
+ Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
1341
+ );
1342
+ const filtered = list.map((it) => this.filterAppForUser(it, sysPerms)).filter((it) => it != null);
1343
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
1344
+ }
1345
+ }
1346
+ }
1347
+ const translated = await this.translateMetaItems(req, req.params.type, environmentId, visible);
1241
1348
  res.header("Vary", "Accept-Language");
1242
1349
  res.json(translated);
1243
1350
  } catch (error) {
@@ -1296,7 +1403,8 @@ var RestServer = class {
1296
1403
  res.json(layered);
1297
1404
  return;
1298
1405
  }
1299
- if (metadata.enableCache && p.getMetaItemCached) {
1406
+ const isAppType = req.params.type === "app";
1407
+ if (metadata.enableCache && p.getMetaItemCached && !isAppType) {
1300
1408
  const cacheRequest = {
1301
1409
  ifNoneMatch: req.headers["if-none-match"],
1302
1410
  ifModifiedSince: req.headers["if-modified-since"]
@@ -1332,8 +1440,25 @@ var RestServer = class {
1332
1440
  name: req.params.name,
1333
1441
  packageId
1334
1442
  });
1443
+ let visible = item;
1444
+ if (isAppType && item) {
1445
+ const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
1446
+ if (ctx?.userId) {
1447
+ const sysPerms = new Set(
1448
+ Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
1449
+ );
1450
+ visible = this.filterAppForUser(item, sysPerms);
1451
+ if (visible == null) {
1452
+ res.status(404).json({
1453
+ error: "not_found",
1454
+ message: "Metadata item not found or access denied."
1455
+ });
1456
+ return;
1457
+ }
1458
+ }
1459
+ }
1335
1460
  res.header("Vary", "Accept-Language");
1336
- res.json(await this.translateMetaItem(req, req.params.type, environmentId, item));
1461
+ res.json(await this.translateMetaItem(req, req.params.type, environmentId, visible));
1337
1462
  }
1338
1463
  } catch (error) {
1339
1464
  logError("[REST] Unhandled error:", error);