@objectstack/rest 8.0.1 → 9.0.1

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
@@ -254,6 +254,9 @@ var RouteGroupBuilder = class {
254
254
  var import_meta = {};
255
255
  var logError = (...args) => globalThis.console?.error(...args);
256
256
  var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
257
+ function isMetaEnvelope(value) {
258
+ return !!value && typeof value === "object" && typeof value.type === "string" && typeof value.name === "string" && value.item != null && typeof value.item === "object" && !Array.isArray(value.item);
259
+ }
257
260
  function mapDataError(error, object) {
258
261
  if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
259
262
  return {
@@ -543,37 +546,49 @@ var RestServer = class {
543
546
  this.hostnameCache.set(host, { value: result, expiresAt: now + this.hostnameCacheTtlMs });
544
547
  return result;
545
548
  }
546
- async resolveProtocol(environmentId, req) {
547
- if (environmentId === "platform") return this.protocol;
548
- if (!environmentId && req && this.envRegistry && this.kernelManager) {
549
+ /**
550
+ * Resolve the environment a request targets: explicit id → tenant hostname
551
+ * `X-Environment-Id` header single-project default. Returns undefined
552
+ * for control-plane requests. Shared by every per-environment service
553
+ * resolution (protocol, analytics, …) so they can never disagree about
554
+ * which kernel a request belongs to.
555
+ */
556
+ async resolveRequestEnvironmentId(environmentId, req) {
557
+ if (environmentId) return environmentId;
558
+ if (req && this.envRegistry && this.kernelManager) {
549
559
  const host = this.extractHostname(req);
550
560
  if (host) {
551
561
  try {
552
562
  const result = await this.resolveHostnameCached(host);
553
- if (result?.environmentId) environmentId = result.environmentId;
563
+ if (result?.environmentId) return result.environmentId;
554
564
  } catch {
555
565
  }
556
566
  }
557
- if (!environmentId && typeof this.envRegistry.resolveById === "function") {
567
+ if (typeof this.envRegistry.resolveById === "function") {
558
568
  const headerVal = this.extractProjectIdHeader(req);
559
569
  if (headerVal) {
560
570
  try {
561
571
  const driver = await this.envRegistry.resolveById(headerVal);
562
- if (driver) environmentId = headerVal;
572
+ if (driver) return headerVal;
563
573
  } catch {
564
574
  }
565
575
  }
566
576
  }
567
577
  }
568
- if (!environmentId && this.defaultEnvironmentIdProvider) {
578
+ if (this.defaultEnvironmentIdProvider) {
569
579
  try {
570
580
  const def = this.defaultEnvironmentIdProvider();
571
- if (def) environmentId = def;
581
+ if (def) return def;
572
582
  } catch {
573
583
  }
574
584
  }
575
- if (!environmentId || !this.kernelManager) return this.protocol;
576
- const kernel = await this.kernelManager.getOrCreate(environmentId);
585
+ return void 0;
586
+ }
587
+ async resolveProtocol(environmentId, req) {
588
+ if (environmentId === "platform") return this.protocol;
589
+ const envId = await this.resolveRequestEnvironmentId(environmentId, req);
590
+ if (!envId || !this.kernelManager) return this.protocol;
591
+ const kernel = await this.kernelManager.getOrCreate(envId);
577
592
  return kernel.getServiceAsync("protocol");
578
593
  }
579
594
  /**
@@ -937,21 +952,28 @@ var RestServer = class {
937
952
  const locale = this.extractLocale(req, i18n);
938
953
  if (!locale) return item;
939
954
  const { translateMetadataDocument } = await import("@objectstack/spec/system");
955
+ if (isMetaEnvelope(item)) {
956
+ return { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) };
957
+ }
940
958
  return translateMetadataDocument(type, item, bundle, { locale });
941
959
  }
942
960
  /**
943
961
  * Translate a list of metadata documents using `translateMetaItem`.
944
962
  */
945
963
  async translateMetaItems(req, type, environmentId, items) {
946
- if (!Array.isArray(items)) return items;
947
964
  if (!TRANSLATABLE_META_TYPES.has(type)) return items;
965
+ const arr = Array.isArray(items) ? items : items && typeof items === "object" && Array.isArray(items.items) ? items.items : null;
966
+ if (!arr) return items;
948
967
  const i18n = await this.resolveI18nService(environmentId, req);
949
968
  const bundle = this.buildTranslationBundle(i18n);
950
969
  if (!bundle) return items;
951
970
  const locale = this.extractLocale(req, i18n);
952
971
  if (!locale) return items;
953
972
  const { translateMetadataDocument } = await import("@objectstack/spec/system");
954
- return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
973
+ const translated = arr.map(
974
+ (item) => isMetaEnvelope(item) ? { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) } : translateMetadataDocument(type, item, bundle, { locale })
975
+ );
976
+ return Array.isArray(items) ? translated : { ...items, items: translated };
955
977
  }
956
978
  /**
957
979
  * Translate the `entries` payload returned by `getMetaTypes()` — applies
@@ -2961,7 +2983,16 @@ var RestServer = class {
2961
2983
  */
2962
2984
  registerAnalyticsEndpoints(basePath) {
2963
2985
  const isScoped = basePath.includes("/environments/:environmentId");
2964
- const resolveService = async (environmentId) => {
2986
+ const resolveService = async (environmentId, req) => {
2987
+ try {
2988
+ const envId = await this.resolveRequestEnvironmentId(environmentId, req);
2989
+ if (envId && envId !== "platform" && this.kernelManager) {
2990
+ const kernel = await this.kernelManager.getOrCreate(envId);
2991
+ const svc = await kernel.getServiceAsync("analytics").catch(() => void 0);
2992
+ if (svc) return svc;
2993
+ }
2994
+ } catch {
2995
+ }
2965
2996
  if (!this.analyticsServiceProvider) return void 0;
2966
2997
  try {
2967
2998
  return await this.analyticsServiceProvider(environmentId);
@@ -2977,7 +3008,7 @@ var RestServer = class {
2977
3008
  const environmentId = isScoped ? req.params?.environmentId : void 0;
2978
3009
  const context = await this.resolveExecCtx(environmentId, req);
2979
3010
  if (this.enforceAuth(req, res, context)) return;
2980
- const svc = await resolveService(environmentId);
3011
+ const svc = await resolveService(environmentId, req);
2981
3012
  if (!svc || typeof svc.queryDataset !== "function") {
2982
3013
  return res.status(501).json({
2983
3014
  code: "NOT_IMPLEMENTED",
@@ -2992,10 +3023,11 @@ var RestServer = class {
2992
3023
  message: "body.selection.measures must be a non-empty array of measure names."
2993
3024
  });
2994
3025
  }
3026
+ const previewDrafts = body.previewDrafts === true || req.query?.preview === "draft";
2995
3027
  let dataset = body.dataset;
2996
3028
  if (!dataset && body.datasetName) {
2997
3029
  const p = await this.resolveProtocol(environmentId, req);
2998
- const items = await p.getMetaItems?.({ type: "dataset" }).catch(() => null);
3030
+ const items = await p.getMetaItems?.({ type: "dataset", previewDrafts }).catch(() => null);
2999
3031
  const list = Array.isArray(items?.items) ? items.items : Array.isArray(items) ? items : [];
3000
3032
  dataset = list.find((d) => d?.name === body.datasetName);
3001
3033
  if (!dataset) {
@@ -3015,7 +3047,12 @@ var RestServer = class {
3015
3047
  detail: String(verr?.message ?? verr).slice(0, 1e3)
3016
3048
  });
3017
3049
  }
3018
- const result = await svc.queryDataset(dataset, selection, context ?? void 0);
3050
+ const result = await svc.queryDataset(
3051
+ dataset,
3052
+ selection,
3053
+ context ?? void 0,
3054
+ previewDrafts ? { previewDrafts: true } : void 0
3055
+ );
3019
3056
  res.json(result);
3020
3057
  } catch (error) {
3021
3058
  const msg = String(error?.message ?? error ?? "");
@@ -3643,6 +3680,34 @@ var RestServer = class {
3643
3680
  };
3644
3681
  decisionRoute("approve");
3645
3682
  decisionRoute("reject");
3683
+ this.routeManager.register({
3684
+ method: "POST",
3685
+ path: `${dataPath}/approvals/requests/:id/recall`,
3686
+ handler: async (req, res) => {
3687
+ try {
3688
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
3689
+ const context = await this.resolveExecCtx(environmentId, req);
3690
+ if (this.enforceAuth(req, res, context)) return;
3691
+ const svc = await resolveService(environmentId);
3692
+ if (!svc || typeof svc.recall !== "function") return respond501(res);
3693
+ const body = req.body ?? {};
3694
+ try {
3695
+ const out = await svc.recall(req.params.id, {
3696
+ actorId: body.actorId ?? body.actor_id ?? context?.userId,
3697
+ comment: body.comment
3698
+ }, context ?? {});
3699
+ res.json(out);
3700
+ } catch (err) {
3701
+ if (handleApprovalError(res, err)) return;
3702
+ throw err;
3703
+ }
3704
+ } catch (error) {
3705
+ logError("[REST] recall approval error:", error);
3706
+ res.status(500).json({ code: "APPROVAL_RECALL_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3707
+ }
3708
+ },
3709
+ metadata: { summary: "Recall (withdraw) an approval request", tags: ["approvals"] }
3710
+ });
3646
3711
  this.routeManager.register({
3647
3712
  method: "GET",
3648
3713
  path: `${dataPath}/approvals/requests/:id/actions`,