@objectstack/rest 6.9.0 → 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.d.cts CHANGED
@@ -280,6 +280,20 @@ declare class RestServer {
280
280
  * to the protocol layer (the SecurityPlugin treats undefined as anon).
281
281
  */
282
282
  private resolveExecCtx;
283
+ /**
284
+ * Filter an `App` metadata item by the current user's `systemPermissions`.
285
+ *
286
+ * - Drops the app entirely if its top-level `requiredPermissions` are not
287
+ * a subset of the user's system permissions.
288
+ * - Recursively strips child navigation entries (groups, items) whose
289
+ * `requiredPermissions` are not satisfied. Empty groups collapse so
290
+ * the sidebar doesn't render a label with no children.
291
+ *
292
+ * Returns `null` when the app should be hidden from the user. Returns a
293
+ * shallow copy with a filtered `navigation` tree otherwise — the original
294
+ * is never mutated so cached metadata stays clean.
295
+ */
296
+ private filterAppForUser;
283
297
  /**
284
298
  * Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
285
299
  * `II18nService` instance. Returns `undefined` when no locales are
@@ -304,6 +318,16 @@ declare class RestServer {
304
318
  * Translate a list of metadata documents using `translateMetaItem`.
305
319
  */
306
320
  private translateMetaItems;
321
+ /**
322
+ * Translate the `entries` payload returned by `getMetaTypes()` — applies
323
+ * the active locale to each entry's `label`, `description`, and the
324
+ * nested `form` layout (section labels, field labels, helpText,
325
+ * placeholders) via `metadataForms.<type>` translation namespace.
326
+ *
327
+ * No-ops when no i18n service / locale / matching bundle entry exists,
328
+ * so this is safe to call unconditionally from the `/meta` handler.
329
+ */
330
+ private translateMetaTypesResponse;
307
331
  /**
308
332
  * Pull the request hostname (without port) from a Node-style `req` or
309
333
  * a Fetch-style request wrapper. Returns undefined when no Host header
package/dist/index.d.ts CHANGED
@@ -280,6 +280,20 @@ declare class RestServer {
280
280
  * to the protocol layer (the SecurityPlugin treats undefined as anon).
281
281
  */
282
282
  private resolveExecCtx;
283
+ /**
284
+ * Filter an `App` metadata item by the current user's `systemPermissions`.
285
+ *
286
+ * - Drops the app entirely if its top-level `requiredPermissions` are not
287
+ * a subset of the user's system permissions.
288
+ * - Recursively strips child navigation entries (groups, items) whose
289
+ * `requiredPermissions` are not satisfied. Empty groups collapse so
290
+ * the sidebar doesn't render a label with no children.
291
+ *
292
+ * Returns `null` when the app should be hidden from the user. Returns a
293
+ * shallow copy with a filtered `navigation` tree otherwise — the original
294
+ * is never mutated so cached metadata stays clean.
295
+ */
296
+ private filterAppForUser;
283
297
  /**
284
298
  * Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
285
299
  * `II18nService` instance. Returns `undefined` when no locales are
@@ -304,6 +318,16 @@ declare class RestServer {
304
318
  * Translate a list of metadata documents using `translateMetaItem`.
305
319
  */
306
320
  private translateMetaItems;
321
+ /**
322
+ * Translate the `entries` payload returned by `getMetaTypes()` — applies
323
+ * the active locale to each entry's `label`, `description`, and the
324
+ * nested `form` layout (section labels, field labels, helpText,
325
+ * placeholders) via `metadataForms.<type>` translation namespace.
326
+ *
327
+ * No-ops when no i18n service / locale / matching bundle entry exists,
328
+ * so this is safe to call unconditionally from the `/meta` handler.
329
+ */
330
+ private translateMetaTypesResponse;
307
331
  /**
308
332
  * Pull the request hostname (without port) from a Node-style `req` or
309
333
  * a Fetch-style request wrapper. Returns undefined when no Host header
package/dist/index.js CHANGED
@@ -628,6 +628,7 @@ var RestServer = class {
628
628
  const userId = session.user.id;
629
629
  const tenantId = session.session?.activeOrganizationId ?? void 0;
630
630
  const permissions = [];
631
+ const systemPermissions = [];
631
632
  const roles = [];
632
633
  try {
633
634
  let ql;
@@ -672,6 +673,20 @@ var RestServer = class {
672
673
  }).catch(() => []);
673
674
  for (const ps of psRows ?? []) {
674
675
  if (ps.name && !permissions.includes(ps.name)) permissions.push(ps.name);
676
+ const rawSys = typeof ps.system_permissions === "string" ? (() => {
677
+ try {
678
+ return JSON.parse(ps.system_permissions);
679
+ } catch {
680
+ return [];
681
+ }
682
+ })() : ps.system_permissions ?? ps.systemPermissions;
683
+ if (Array.isArray(rawSys)) {
684
+ for (const sp of rawSys) {
685
+ if (typeof sp === "string" && !systemPermissions.includes(sp)) {
686
+ systemPermissions.push(sp);
687
+ }
688
+ }
689
+ }
675
690
  }
676
691
  }
677
692
  }
@@ -709,6 +724,7 @@ var RestServer = class {
709
724
  tenantId,
710
725
  roles,
711
726
  permissions,
727
+ systemPermissions,
712
728
  isSystem: false,
713
729
  org_user_ids
714
730
  };
@@ -716,6 +732,45 @@ var RestServer = class {
716
732
  return void 0;
717
733
  }
718
734
  }
735
+ /**
736
+ * Filter an `App` metadata item by the current user's `systemPermissions`.
737
+ *
738
+ * - Drops the app entirely if its top-level `requiredPermissions` are not
739
+ * a subset of the user's system permissions.
740
+ * - Recursively strips child navigation entries (groups, items) whose
741
+ * `requiredPermissions` are not satisfied. Empty groups collapse so
742
+ * the sidebar doesn't render a label with no children.
743
+ *
744
+ * Returns `null` when the app should be hidden from the user. Returns a
745
+ * shallow copy with a filtered `navigation` tree otherwise — the original
746
+ * is never mutated so cached metadata stays clean.
747
+ */
748
+ filterAppForUser(item, sysPerms) {
749
+ if (!item || typeof item !== "object") return item;
750
+ const reqApp = Array.isArray(item.requiredPermissions) ? item.requiredPermissions : [];
751
+ if (reqApp.length > 0 && !reqApp.every((p) => sysPerms.has(p))) {
752
+ return null;
753
+ }
754
+ const nav = Array.isArray(item.navigation) ? item.navigation : null;
755
+ if (!nav) return item;
756
+ const filterNav = (entries) => {
757
+ const out = [];
758
+ for (const e of entries) {
759
+ if (!e || typeof e !== "object") continue;
760
+ const req = Array.isArray(e.requiredPermissions) ? e.requiredPermissions : [];
761
+ if (req.length > 0 && !req.every((p) => sysPerms.has(p))) continue;
762
+ if (Array.isArray(e.children) && e.children.length > 0) {
763
+ const kids = filterNav(e.children);
764
+ if (e.type === "group" && kids.length === 0) continue;
765
+ out.push({ ...e, children: kids });
766
+ } else {
767
+ out.push(e);
768
+ }
769
+ }
770
+ return out;
771
+ };
772
+ return { ...item, navigation: filterNav(nav) };
773
+ }
719
774
  /**
720
775
  * Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
721
776
  * `II18nService` instance. Returns `undefined` when no locales are
@@ -789,6 +844,41 @@ var RestServer = class {
789
844
  const { translateMetadataDocument } = await import("@objectstack/spec/system");
790
845
  return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
791
846
  }
847
+ /**
848
+ * Translate the `entries` payload returned by `getMetaTypes()` — applies
849
+ * the active locale to each entry's `label`, `description`, and the
850
+ * nested `form` layout (section labels, field labels, helpText,
851
+ * placeholders) via `metadataForms.<type>` translation namespace.
852
+ *
853
+ * No-ops when no i18n service / locale / matching bundle entry exists,
854
+ * so this is safe to call unconditionally from the `/meta` handler.
855
+ */
856
+ async translateMetaTypesResponse(req, environmentId, payload) {
857
+ if (!payload || typeof payload !== "object" || !Array.isArray(payload.entries)) return payload;
858
+ const i18n = await this.resolveI18nService(environmentId, req);
859
+ const bundle = this.buildTranslationBundle(i18n);
860
+ if (!bundle) return payload;
861
+ const locale = this.extractLocale(req, i18n);
862
+ if (!locale) return payload;
863
+ const {
864
+ resolveMetadataTypeLabel,
865
+ resolveMetadataTypeDescription,
866
+ resolveMetadataFormLabels
867
+ } = await import("@objectstack/spec/system");
868
+ const opts = { locale };
869
+ const entries = payload.entries.map((entry) => {
870
+ if (!entry || typeof entry !== "object" || typeof entry.type !== "string") return entry;
871
+ const next = { ...entry };
872
+ next.label = resolveMetadataTypeLabel(bundle, entry.type, entry.label ?? entry.type, opts);
873
+ const desc = resolveMetadataTypeDescription(bundle, entry.type, entry.description, opts);
874
+ if (desc !== void 0) next.description = desc;
875
+ if (entry.form) {
876
+ next.form = resolveMetadataFormLabels(entry.form, entry.type, bundle, opts);
877
+ }
878
+ return next;
879
+ });
880
+ return { ...payload, entries };
881
+ }
792
882
  /**
793
883
  * Pull the request hostname (without port) from a Node-style `req` or
794
884
  * a Fetch-style request wrapper. Returns undefined when no Host header
@@ -1171,7 +1261,9 @@ var RestServer = class {
1171
1261
  const environmentId = isScoped ? req.params?.environmentId : void 0;
1172
1262
  const p = await this.resolveProtocol(environmentId, req);
1173
1263
  const types = await p.getMetaTypes();
1174
- res.json(types);
1264
+ const translated = await this.translateMetaTypesResponse(req, environmentId, types);
1265
+ res.header("Vary", "Accept-Language");
1266
+ res.json(translated);
1175
1267
  } catch (error) {
1176
1268
  logError("[REST] Unhandled error:", error);
1177
1269
  sendError(res, error);
@@ -1197,7 +1289,22 @@ var RestServer = class {
1197
1289
  packageId,
1198
1290
  ...environmentId ? { environmentId } : {}
1199
1291
  });
1200
- const translated = await this.translateMetaItems(req, req.params.type, environmentId, items);
1292
+ let visible = items;
1293
+ if (req.params.type === "app") {
1294
+ const raw = items;
1295
+ const list = Array.isArray(raw) ? raw : raw && typeof raw === "object" && Array.isArray(raw.items) ? raw.items : null;
1296
+ if (list) {
1297
+ const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
1298
+ if (ctx?.userId) {
1299
+ const sysPerms = new Set(
1300
+ Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
1301
+ );
1302
+ const filtered = list.map((it) => this.filterAppForUser(it, sysPerms)).filter((it) => it != null);
1303
+ visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
1304
+ }
1305
+ }
1306
+ }
1307
+ const translated = await this.translateMetaItems(req, req.params.type, environmentId, visible);
1201
1308
  res.header("Vary", "Accept-Language");
1202
1309
  res.json(translated);
1203
1310
  } catch (error) {
@@ -1256,7 +1363,8 @@ var RestServer = class {
1256
1363
  res.json(layered);
1257
1364
  return;
1258
1365
  }
1259
- if (metadata.enableCache && p.getMetaItemCached) {
1366
+ const isAppType = req.params.type === "app";
1367
+ if (metadata.enableCache && p.getMetaItemCached && !isAppType) {
1260
1368
  const cacheRequest = {
1261
1369
  ifNoneMatch: req.headers["if-none-match"],
1262
1370
  ifModifiedSince: req.headers["if-modified-since"]
@@ -1292,8 +1400,25 @@ var RestServer = class {
1292
1400
  name: req.params.name,
1293
1401
  packageId
1294
1402
  });
1403
+ let visible = item;
1404
+ if (isAppType && item) {
1405
+ const ctx = await this.resolveExecCtx(environmentId, req).catch(() => void 0);
1406
+ if (ctx?.userId) {
1407
+ const sysPerms = new Set(
1408
+ Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
1409
+ );
1410
+ visible = this.filterAppForUser(item, sysPerms);
1411
+ if (visible == null) {
1412
+ res.status(404).json({
1413
+ error: "not_found",
1414
+ message: "Metadata item not found or access denied."
1415
+ });
1416
+ return;
1417
+ }
1418
+ }
1419
+ }
1295
1420
  res.header("Vary", "Accept-Language");
1296
- res.json(await this.translateMetaItem(req, req.params.type, environmentId, item));
1421
+ res.json(await this.translateMetaItem(req, req.params.type, environmentId, visible));
1297
1422
  }
1298
1423
  } catch (error) {
1299
1424
  logError("[REST] Unhandled error:", error);