@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 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 filtered = list.map((it) => this.filterAppForUser(it, sysPerms)).filter((it) => it != null);
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
- visible = this.filterAppForUser(item, sysPerms);
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) {