@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.d.cts CHANGED
@@ -268,6 +268,14 @@ declare class RestServer {
268
268
  * next request.
269
269
  */
270
270
  private resolveHostnameCached;
271
+ /**
272
+ * Resolve the environment a request targets: explicit id → tenant hostname
273
+ * → `X-Environment-Id` header → single-project default. Returns undefined
274
+ * for control-plane requests. Shared by every per-environment service
275
+ * resolution (protocol, analytics, …) so they can never disagree about
276
+ * which kernel a request belongs to.
277
+ */
278
+ private resolveRequestEnvironmentId;
271
279
  private resolveProtocol;
272
280
  /**
273
281
  * Resolve the i18n service for the request's project (or control plane
package/dist/index.d.ts CHANGED
@@ -268,6 +268,14 @@ declare class RestServer {
268
268
  * next request.
269
269
  */
270
270
  private resolveHostnameCached;
271
+ /**
272
+ * Resolve the environment a request targets: explicit id → tenant hostname
273
+ * → `X-Environment-Id` header → single-project default. Returns undefined
274
+ * for control-plane requests. Shared by every per-environment service
275
+ * resolution (protocol, analytics, …) so they can never disagree about
276
+ * which kernel a request belongs to.
277
+ */
278
+ private resolveRequestEnvironmentId;
271
279
  private resolveProtocol;
272
280
  /**
273
281
  * Resolve the i18n service for the request's project (or control plane
package/dist/index.js CHANGED
@@ -214,6 +214,9 @@ var RouteGroupBuilder = class {
214
214
  // src/rest-server.ts
215
215
  var logError = (...args) => globalThis.console?.error(...args);
216
216
  var TRANSLATABLE_META_TYPES = /* @__PURE__ */ new Set(["view", "action", "object", "app", "dashboard"]);
217
+ function isMetaEnvelope(value) {
218
+ return !!value && typeof value === "object" && typeof value.type === "string" && typeof value.name === "string" && value.item != null && typeof value.item === "object" && !Array.isArray(value.item);
219
+ }
217
220
  function mapDataError(error, object) {
218
221
  if (error?.code === "CONCURRENT_UPDATE" || error?.name === "ConcurrentUpdateError") {
219
222
  return {
@@ -503,37 +506,49 @@ var RestServer = class {
503
506
  this.hostnameCache.set(host, { value: result, expiresAt: now + this.hostnameCacheTtlMs });
504
507
  return result;
505
508
  }
506
- async resolveProtocol(environmentId, req) {
507
- if (environmentId === "platform") return this.protocol;
508
- if (!environmentId && req && this.envRegistry && this.kernelManager) {
509
+ /**
510
+ * Resolve the environment a request targets: explicit id → tenant hostname
511
+ * `X-Environment-Id` header single-project default. Returns undefined
512
+ * for control-plane requests. Shared by every per-environment service
513
+ * resolution (protocol, analytics, …) so they can never disagree about
514
+ * which kernel a request belongs to.
515
+ */
516
+ async resolveRequestEnvironmentId(environmentId, req) {
517
+ if (environmentId) return environmentId;
518
+ if (req && this.envRegistry && this.kernelManager) {
509
519
  const host = this.extractHostname(req);
510
520
  if (host) {
511
521
  try {
512
522
  const result = await this.resolveHostnameCached(host);
513
- if (result?.environmentId) environmentId = result.environmentId;
523
+ if (result?.environmentId) return result.environmentId;
514
524
  } catch {
515
525
  }
516
526
  }
517
- if (!environmentId && typeof this.envRegistry.resolveById === "function") {
527
+ if (typeof this.envRegistry.resolveById === "function") {
518
528
  const headerVal = this.extractProjectIdHeader(req);
519
529
  if (headerVal) {
520
530
  try {
521
531
  const driver = await this.envRegistry.resolveById(headerVal);
522
- if (driver) environmentId = headerVal;
532
+ if (driver) return headerVal;
523
533
  } catch {
524
534
  }
525
535
  }
526
536
  }
527
537
  }
528
- if (!environmentId && this.defaultEnvironmentIdProvider) {
538
+ if (this.defaultEnvironmentIdProvider) {
529
539
  try {
530
540
  const def = this.defaultEnvironmentIdProvider();
531
- if (def) environmentId = def;
541
+ if (def) return def;
532
542
  } catch {
533
543
  }
534
544
  }
535
- if (!environmentId || !this.kernelManager) return this.protocol;
536
- const kernel = await this.kernelManager.getOrCreate(environmentId);
545
+ return void 0;
546
+ }
547
+ async resolveProtocol(environmentId, req) {
548
+ if (environmentId === "platform") return this.protocol;
549
+ const envId = await this.resolveRequestEnvironmentId(environmentId, req);
550
+ if (!envId || !this.kernelManager) return this.protocol;
551
+ const kernel = await this.kernelManager.getOrCreate(envId);
537
552
  return kernel.getServiceAsync("protocol");
538
553
  }
539
554
  /**
@@ -897,21 +912,28 @@ var RestServer = class {
897
912
  const locale = this.extractLocale(req, i18n);
898
913
  if (!locale) return item;
899
914
  const { translateMetadataDocument } = await import("@objectstack/spec/system");
915
+ if (isMetaEnvelope(item)) {
916
+ return { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) };
917
+ }
900
918
  return translateMetadataDocument(type, item, bundle, { locale });
901
919
  }
902
920
  /**
903
921
  * Translate a list of metadata documents using `translateMetaItem`.
904
922
  */
905
923
  async translateMetaItems(req, type, environmentId, items) {
906
- if (!Array.isArray(items)) return items;
907
924
  if (!TRANSLATABLE_META_TYPES.has(type)) return items;
925
+ const arr = Array.isArray(items) ? items : items && typeof items === "object" && Array.isArray(items.items) ? items.items : null;
926
+ if (!arr) return items;
908
927
  const i18n = await this.resolveI18nService(environmentId, req);
909
928
  const bundle = this.buildTranslationBundle(i18n);
910
929
  if (!bundle) return items;
911
930
  const locale = this.extractLocale(req, i18n);
912
931
  if (!locale) return items;
913
932
  const { translateMetadataDocument } = await import("@objectstack/spec/system");
914
- return items.map((item) => translateMetadataDocument(type, item, bundle, { locale }));
933
+ const translated = arr.map(
934
+ (item) => isMetaEnvelope(item) ? { ...item, item: translateMetadataDocument(type, item.item, bundle, { locale }) } : translateMetadataDocument(type, item, bundle, { locale })
935
+ );
936
+ return Array.isArray(items) ? translated : { ...items, items: translated };
915
937
  }
916
938
  /**
917
939
  * Translate the `entries` payload returned by `getMetaTypes()` — applies
@@ -2921,7 +2943,16 @@ var RestServer = class {
2921
2943
  */
2922
2944
  registerAnalyticsEndpoints(basePath) {
2923
2945
  const isScoped = basePath.includes("/environments/:environmentId");
2924
- const resolveService = async (environmentId) => {
2946
+ const resolveService = async (environmentId, req) => {
2947
+ try {
2948
+ const envId = await this.resolveRequestEnvironmentId(environmentId, req);
2949
+ if (envId && envId !== "platform" && this.kernelManager) {
2950
+ const kernel = await this.kernelManager.getOrCreate(envId);
2951
+ const svc = await kernel.getServiceAsync("analytics").catch(() => void 0);
2952
+ if (svc) return svc;
2953
+ }
2954
+ } catch {
2955
+ }
2925
2956
  if (!this.analyticsServiceProvider) return void 0;
2926
2957
  try {
2927
2958
  return await this.analyticsServiceProvider(environmentId);
@@ -2937,7 +2968,7 @@ var RestServer = class {
2937
2968
  const environmentId = isScoped ? req.params?.environmentId : void 0;
2938
2969
  const context = await this.resolveExecCtx(environmentId, req);
2939
2970
  if (this.enforceAuth(req, res, context)) return;
2940
- const svc = await resolveService(environmentId);
2971
+ const svc = await resolveService(environmentId, req);
2941
2972
  if (!svc || typeof svc.queryDataset !== "function") {
2942
2973
  return res.status(501).json({
2943
2974
  code: "NOT_IMPLEMENTED",
@@ -2952,10 +2983,11 @@ var RestServer = class {
2952
2983
  message: "body.selection.measures must be a non-empty array of measure names."
2953
2984
  });
2954
2985
  }
2986
+ const previewDrafts = body.previewDrafts === true || req.query?.preview === "draft";
2955
2987
  let dataset = body.dataset;
2956
2988
  if (!dataset && body.datasetName) {
2957
2989
  const p = await this.resolveProtocol(environmentId, req);
2958
- const items = await p.getMetaItems?.({ type: "dataset" }).catch(() => null);
2990
+ const items = await p.getMetaItems?.({ type: "dataset", previewDrafts }).catch(() => null);
2959
2991
  const list = Array.isArray(items?.items) ? items.items : Array.isArray(items) ? items : [];
2960
2992
  dataset = list.find((d) => d?.name === body.datasetName);
2961
2993
  if (!dataset) {
@@ -2975,7 +3007,12 @@ var RestServer = class {
2975
3007
  detail: String(verr?.message ?? verr).slice(0, 1e3)
2976
3008
  });
2977
3009
  }
2978
- const result = await svc.queryDataset(dataset, selection, context ?? void 0);
3010
+ const result = await svc.queryDataset(
3011
+ dataset,
3012
+ selection,
3013
+ context ?? void 0,
3014
+ previewDrafts ? { previewDrafts: true } : void 0
3015
+ );
2979
3016
  res.json(result);
2980
3017
  } catch (error) {
2981
3018
  const msg = String(error?.message ?? error ?? "");
@@ -3603,6 +3640,34 @@ var RestServer = class {
3603
3640
  };
3604
3641
  decisionRoute("approve");
3605
3642
  decisionRoute("reject");
3643
+ this.routeManager.register({
3644
+ method: "POST",
3645
+ path: `${dataPath}/approvals/requests/:id/recall`,
3646
+ handler: async (req, res) => {
3647
+ try {
3648
+ const environmentId = isScoped ? req.params?.environmentId : void 0;
3649
+ const context = await this.resolveExecCtx(environmentId, req);
3650
+ if (this.enforceAuth(req, res, context)) return;
3651
+ const svc = await resolveService(environmentId);
3652
+ if (!svc || typeof svc.recall !== "function") return respond501(res);
3653
+ const body = req.body ?? {};
3654
+ try {
3655
+ const out = await svc.recall(req.params.id, {
3656
+ actorId: body.actorId ?? body.actor_id ?? context?.userId,
3657
+ comment: body.comment
3658
+ }, context ?? {});
3659
+ res.json(out);
3660
+ } catch (err) {
3661
+ if (handleApprovalError(res, err)) return;
3662
+ throw err;
3663
+ }
3664
+ } catch (error) {
3665
+ logError("[REST] recall approval error:", error);
3666
+ res.status(500).json({ code: "APPROVAL_RECALL_FAILED", error: String(error?.message ?? error).slice(0, 500) });
3667
+ }
3668
+ },
3669
+ metadata: { summary: "Recall (withdraw) an approval request", tags: ["approvals"] }
3670
+ });
3606
3671
  this.routeManager.register({
3607
3672
  method: "GET",
3608
3673
  path: `${dataPath}/approvals/requests/:id/actions`,