@objectstack/rest 9.11.0 → 10.2.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 +85 -8
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +13 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.js +85 -8
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -348,6 +348,7 @@ function mapDataError(error, object) {
|
|
|
348
348
|
error: `${field} is required`,
|
|
349
349
|
code: "VALIDATION_FAILED",
|
|
350
350
|
fields: [{ field, code: "required", message: `${field} is required` }],
|
|
351
|
+
hint: `If '${field}' is optional in your object metadata, the database column is still NOT NULL \u2014 the physical schema has drifted from metadata. Run 'os migrate' to reconcile (or reset the dev database).`,
|
|
351
352
|
...object ? { object } : {}
|
|
352
353
|
}
|
|
353
354
|
};
|
|
@@ -489,7 +490,7 @@ function rowsToCsv(fields, rows, includeHeader) {
|
|
|
489
490
|
return lines.join("\r\n") + (lines.length > 0 ? "\r\n" : "");
|
|
490
491
|
}
|
|
491
492
|
var RestServer = class {
|
|
492
|
-
constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider) {
|
|
493
|
+
constructor(server, protocol, config = {}, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider) {
|
|
493
494
|
/**
|
|
494
495
|
* Short-TTL cache for `hostname → environmentId` (P1-4). `resolveByHostname`
|
|
495
496
|
* is a control-plane lookup (typically a DB query) that otherwise runs on
|
|
@@ -521,6 +522,7 @@ var RestServer = class {
|
|
|
521
522
|
this.i18nServiceProvider = i18nServiceProvider;
|
|
522
523
|
this.analyticsServiceProvider = analyticsServiceProvider;
|
|
523
524
|
this.settingsServiceProvider = settingsServiceProvider;
|
|
525
|
+
this.serviceExistsProvider = serviceExistsProvider;
|
|
524
526
|
}
|
|
525
527
|
/**
|
|
526
528
|
* Resolve the protocol for a given request. When `environmentId` is present
|
|
@@ -918,18 +920,22 @@ var RestServer = class {
|
|
|
918
920
|
}
|
|
919
921
|
let timezone;
|
|
920
922
|
let locale;
|
|
923
|
+
let currency;
|
|
921
924
|
try {
|
|
922
925
|
const settings = this.settingsServiceProvider ? await this.settingsServiceProvider(environmentId).catch(() => void 0) : void 0;
|
|
923
926
|
if (settings && typeof settings.get === "function") {
|
|
924
927
|
const sctx = { tenantId, userId };
|
|
925
|
-
const [tzRes, localeRes] = await Promise.all([
|
|
928
|
+
const [tzRes, localeRes, currencyRes] = await Promise.all([
|
|
926
929
|
settings.get("localization", "timezone", sctx).catch(() => void 0),
|
|
927
|
-
settings.get("localization", "locale", sctx).catch(() => void 0)
|
|
930
|
+
settings.get("localization", "locale", sctx).catch(() => void 0),
|
|
931
|
+
settings.get("localization", "currency", sctx).catch(() => void 0)
|
|
928
932
|
]);
|
|
929
933
|
const tzVal = tzRes?.value;
|
|
930
934
|
const localeVal = localeRes?.value;
|
|
935
|
+
const currencyVal = currencyRes?.value;
|
|
931
936
|
if (typeof tzVal === "string" && tzVal.trim()) timezone = tzVal.trim();
|
|
932
937
|
if (typeof localeVal === "string" && localeVal.trim()) locale = localeVal.trim();
|
|
938
|
+
if (typeof currencyVal === "string" && currencyVal.trim()) currency = currencyVal.trim().toUpperCase();
|
|
933
939
|
}
|
|
934
940
|
} catch {
|
|
935
941
|
}
|
|
@@ -943,7 +949,12 @@ var RestServer = class {
|
|
|
943
949
|
isSystem: false,
|
|
944
950
|
org_user_ids,
|
|
945
951
|
...timezone ? { timezone } : {},
|
|
946
|
-
...locale ? { locale } : {}
|
|
952
|
+
...locale ? { locale } : {},
|
|
953
|
+
...currency ? { currency } : {},
|
|
954
|
+
// Internal: resolved kernel so the nav-serving path can probe
|
|
955
|
+
// requiresService capability gates (ADR-0057 D10). NOT an
|
|
956
|
+
// authorization input — never read by RLS/permission logic.
|
|
957
|
+
__kernel: kernel
|
|
947
958
|
};
|
|
948
959
|
} catch {
|
|
949
960
|
return void 0;
|
|
@@ -962,8 +973,12 @@ var RestServer = class {
|
|
|
962
973
|
* shallow copy with a filtered `navigation` tree otherwise — the original
|
|
963
974
|
* is never mutated so cached metadata stays clean.
|
|
964
975
|
*/
|
|
965
|
-
filterAppForUser(item, sysPerms) {
|
|
976
|
+
filterAppForUser(item, sysPerms, serviceGate) {
|
|
966
977
|
if (!item || typeof item !== "object") return item;
|
|
978
|
+
if (isMetaEnvelope(item)) {
|
|
979
|
+
const body = this.filterAppForUser(item.item, sysPerms, serviceGate);
|
|
980
|
+
return body == null ? null : { ...item, item: body };
|
|
981
|
+
}
|
|
967
982
|
if (item.hidden === true && !sysPerms.has("studio.access") && !sysPerms.has("setup.access")) {
|
|
968
983
|
return null;
|
|
969
984
|
}
|
|
@@ -971,6 +986,9 @@ var RestServer = class {
|
|
|
971
986
|
if (reqApp.length > 0 && !reqApp.every((p) => sysPerms.has(p))) {
|
|
972
987
|
return null;
|
|
973
988
|
}
|
|
989
|
+
if (typeof item.requiresService === "string" && serviceGate && serviceGate(item.requiresService) === false) {
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
974
992
|
const nav = Array.isArray(item.navigation) ? item.navigation : null;
|
|
975
993
|
if (!nav) return item;
|
|
976
994
|
const filterNav = (entries) => {
|
|
@@ -979,6 +997,7 @@ var RestServer = class {
|
|
|
979
997
|
if (!e || typeof e !== "object") continue;
|
|
980
998
|
const req = Array.isArray(e.requiredPermissions) ? e.requiredPermissions : [];
|
|
981
999
|
if (req.length > 0 && !req.every((p) => sysPerms.has(p))) continue;
|
|
1000
|
+
if (typeof e.requiresService === "string" && serviceGate && serviceGate(e.requiresService) === false) continue;
|
|
982
1001
|
if (Array.isArray(e.children) && e.children.length > 0) {
|
|
983
1002
|
const kids = filterNav(e.children);
|
|
984
1003
|
if (e.type === "group" && kids.length === 0) continue;
|
|
@@ -991,6 +1010,53 @@ var RestServer = class {
|
|
|
991
1010
|
};
|
|
992
1011
|
return { ...item, navigation: filterNav(nav) };
|
|
993
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Probe which `requiresService` capability gates referenced anywhere in
|
|
1015
|
+
* `items` are actually registered in the runtime kernel. Returns `null`
|
|
1016
|
+
* when the kernel can't be probed — callers then SKIP service gating
|
|
1017
|
+
* (fail-open, matching the prior "send everything, let the client hide"
|
|
1018
|
+
* behaviour). ADR-0057 addendum D10.
|
|
1019
|
+
*/
|
|
1020
|
+
async resolveRegisteredServices(kernel, items) {
|
|
1021
|
+
let probe = null;
|
|
1022
|
+
if (kernel && typeof kernel.getServiceAsync === "function") {
|
|
1023
|
+
probe = async (name) => {
|
|
1024
|
+
try {
|
|
1025
|
+
return await kernel.getServiceAsync(name) != null;
|
|
1026
|
+
} catch {
|
|
1027
|
+
return false;
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
} else if (this.serviceExistsProvider) {
|
|
1031
|
+
const exists = this.serviceExistsProvider;
|
|
1032
|
+
probe = async (name) => {
|
|
1033
|
+
try {
|
|
1034
|
+
return exists(name) === true;
|
|
1035
|
+
} catch {
|
|
1036
|
+
return false;
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
if (!probe) return null;
|
|
1041
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
1042
|
+
const walk = (e) => {
|
|
1043
|
+
if (!e || typeof e !== "object") return;
|
|
1044
|
+
if (isMetaEnvelope(e)) {
|
|
1045
|
+
walk(e.item);
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
if (typeof e.requiresService === "string") wanted.add(e.requiresService);
|
|
1049
|
+
const kids = Array.isArray(e.navigation) ? e.navigation : Array.isArray(e.children) ? e.children : null;
|
|
1050
|
+
if (kids) for (const k of kids) walk(k);
|
|
1051
|
+
};
|
|
1052
|
+
for (const it of items) walk(it);
|
|
1053
|
+
if (wanted.size === 0) return /* @__PURE__ */ new Set();
|
|
1054
|
+
const registered = /* @__PURE__ */ new Set();
|
|
1055
|
+
for (const name of wanted) {
|
|
1056
|
+
if (await probe(name)) registered.add(name);
|
|
1057
|
+
}
|
|
1058
|
+
return registered;
|
|
1059
|
+
}
|
|
994
1060
|
/**
|
|
995
1061
|
* Build a `TranslationBundle` (`Record<locale, TranslationData>`) from an
|
|
996
1062
|
* `II18nService` instance. Returns `undefined` when no locales are
|
|
@@ -1601,7 +1667,9 @@ var RestServer = class {
|
|
|
1601
1667
|
const sysPerms = new Set(
|
|
1602
1668
|
Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
|
|
1603
1669
|
);
|
|
1604
|
-
const
|
|
1670
|
+
const registered = await this.resolveRegisteredServices(ctx.__kernel, list);
|
|
1671
|
+
const serviceGate = registered ? (n) => registered.has(n) : void 0;
|
|
1672
|
+
const filtered = list.map((it) => this.filterAppForUser(it, sysPerms, serviceGate)).filter((it) => it != null);
|
|
1605
1673
|
visible = Array.isArray(raw) ? filtered : { ...raw, items: filtered };
|
|
1606
1674
|
}
|
|
1607
1675
|
}
|
|
@@ -1801,7 +1869,9 @@ var RestServer = class {
|
|
|
1801
1869
|
const sysPerms = new Set(
|
|
1802
1870
|
Array.isArray(ctx.systemPermissions) ? ctx.systemPermissions : []
|
|
1803
1871
|
);
|
|
1804
|
-
|
|
1872
|
+
const registered = await this.resolveRegisteredServices(ctx.__kernel, [item]);
|
|
1873
|
+
const serviceGate = registered ? (n) => registered.has(n) : void 0;
|
|
1874
|
+
visible = this.filterAppForUser(item, sysPerms, serviceGate);
|
|
1805
1875
|
if (visible == null) {
|
|
1806
1876
|
res.status(404).json({
|
|
1807
1877
|
error: "not_found",
|
|
@@ -4493,8 +4563,15 @@ function createRestApiPlugin(config = {}) {
|
|
|
4493
4563
|
return;
|
|
4494
4564
|
}
|
|
4495
4565
|
ctx.logger.info("Hydrating REST API from Protocol...");
|
|
4566
|
+
const serviceExistsProvider = (name) => {
|
|
4567
|
+
try {
|
|
4568
|
+
return ctx.getService(name) != null;
|
|
4569
|
+
} catch {
|
|
4570
|
+
return false;
|
|
4571
|
+
}
|
|
4572
|
+
};
|
|
4496
4573
|
try {
|
|
4497
|
-
const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider);
|
|
4574
|
+
const restServer = new RestServer(server, protocol, config.api, kernelManager, envRegistry, defaultEnvironmentIdProvider, authServiceProvider, objectQLProvider, emailServiceProvider, sharingServiceProvider, reportsServiceProvider, approvalsServiceProvider, sharingRulesServiceProvider, i18nServiceProvider, analyticsServiceProvider, settingsServiceProvider, serviceExistsProvider);
|
|
4498
4575
|
restServer.registerRoutes();
|
|
4499
4576
|
ctx.logger.info("REST API successfully registered");
|
|
4500
4577
|
if (!config.api?.requireAuth) {
|