@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.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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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);
|