@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.cjs +129 -4
- 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 +129 -4
- 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);
|
|
@@ -1237,7 +1329,22 @@ var RestServer = class {
|
|
|
1237
1329
|
packageId,
|
|
1238
1330
|
...environmentId ? { environmentId } : {}
|
|
1239
1331
|
});
|
|
1240
|
-
|
|
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
|
-
|
|
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,
|
|
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);
|