@object-ui/data-objectstack 6.2.3 → 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.js CHANGED
@@ -766,9 +766,12 @@ var MetadataClient = class _MetadataClient {
766
766
  __publicField(this, "base");
767
767
  __publicField(this, "fetchImpl");
768
768
  __publicField(this, "headers");
769
+ /** ADR-0037: when true, reads render the draft-overlaid world. */
770
+ __publicField(this, "previewDrafts");
769
771
  this.base = buildBase(config);
770
772
  this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
771
773
  this.headers = { Accept: "application/json", ...config.headers ?? {} };
774
+ this.previewDrafts = config.previewDrafts === true;
772
775
  }
773
776
  /** Update the client's environment scope at runtime. */
774
777
  withEnvironment(environmentId) {
@@ -776,7 +779,24 @@ var MetadataClient = class _MetadataClient {
776
779
  baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
777
780
  environmentId,
778
781
  fetch: this.fetchImpl,
779
- headers: this.headers
782
+ headers: this.headers,
783
+ previewDrafts: this.previewDrafts
784
+ });
785
+ }
786
+ /**
787
+ * Derive a client whose reads render the draft-overlaid world (or not).
788
+ * Same base/fetch/headers; used by the app-shell to switch the whole
789
+ * renderer tree into ADR-0037 preview mode off one URL flag.
790
+ */
791
+ withPreviewDrafts(previewDrafts) {
792
+ if (previewDrafts === this.previewDrafts) return this;
793
+ const envMatch = this.base.match(/\/api\/v\d+\/environments\/([^/]+)\/meta$/);
794
+ return new _MetadataClient({
795
+ baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
796
+ ...envMatch ? { environmentId: decodeURIComponent(envMatch[1]) } : {},
797
+ fetch: this.fetchImpl,
798
+ headers: this.headers,
799
+ previewDrafts
780
800
  });
781
801
  }
782
802
  /** List all registered metadata types (returns the registry rows). */
@@ -794,7 +814,10 @@ var MetadataClient = class _MetadataClient {
794
814
  }
795
815
  /** List items of a metadata type (e.g. `object`, `field`, `view`). */
796
816
  async list(type, options = {}) {
797
- const qs = options.packageId ? `?package=${encodeURIComponent(options.packageId)}` : "";
817
+ const params = [];
818
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
819
+ if (this.previewDrafts) params.push("preview=draft");
820
+ const qs = params.length ? `?${params.join("&")}` : "";
798
821
  const url = `${this.base}/${encodeURIComponent(type)}${qs}`;
799
822
  const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
800
823
  if (!res.ok) throw await parseError(res);
@@ -805,25 +828,70 @@ var MetadataClient = class _MetadataClient {
805
828
  }
806
829
  return [];
807
830
  }
831
+ /**
832
+ * List pending DRAFT items (ADR-0033) — what an AI authored but nobody
833
+ * published yet. `list()` only sees published/active metadata, so a
834
+ * just-built app package looks empty there; this surfaces the drafts so the
835
+ * console can show a "pending changes" view and draft-aware package contents.
836
+ * Optionally narrow by `packageId` and/or `type`. Returns light headers
837
+ * (no body) carrying `packageId` for grouping.
838
+ */
839
+ async listDrafts(options = {}) {
840
+ const params = [];
841
+ if (options.packageId) params.push(`packageId=${encodeURIComponent(options.packageId)}`);
842
+ if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
843
+ const qs = params.length ? `?${params.join("&")}` : "";
844
+ const url = `${this.base}/_drafts${qs}`;
845
+ const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
846
+ if (!res.ok) throw await parseError(res);
847
+ const data = await res.json();
848
+ if (Array.isArray(data)) return data;
849
+ return data?.drafts ?? data?.data?.drafts ?? [];
850
+ }
808
851
  /**
809
852
  * Get a single metadata item. Returns the unwrapped item content
810
853
  * (matching the framework REST handler which calls `res.json(item)`).
811
854
  * Returns `null` on 404 to keep the call-site ergonomic.
812
855
  */
813
- async get(type, name) {
814
- const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}`;
856
+ async get(type, name, options = {}) {
857
+ const params = [];
858
+ if (options.state === "draft") params.push("state=draft");
859
+ else if (this.previewDrafts) params.push("preview=draft");
860
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
861
+ const qs = params.length ? `?${params.join("&")}` : "";
862
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
815
863
  const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
816
864
  if (res.status === 404) return null;
817
865
  if (!res.ok) throw await parseError(res);
818
866
  return await res.json();
819
867
  }
868
+ /**
869
+ * Read the pending draft body for an item (`?state=draft`). Returns
870
+ * `null` when there is no draft pending. Draft reads do NOT fall
871
+ * back to the published overlay or the artifact registry — a `null`
872
+ * unambiguously means "nothing to publish".
873
+ *
874
+ * Note: the framework wraps draft responses in an envelope
875
+ * `{ type, name, item }` (matching `getMetaItem`); callers should
876
+ * read `.item` to get the body. The legacy `get()` returns the
877
+ * unwrapped body, so this method preserves that asymmetry by
878
+ * returning whatever the server sent.
879
+ */
880
+ async getDraft(type, name, options = {}) {
881
+ return this.get(type, name, { state: "draft", ...options.packageId ? { packageId: options.packageId } : {} });
882
+ }
820
883
  /**
821
884
  * Save (PUT) a metadata item. The framework accepts both the bare
822
885
  * item payload and the `{ item: ... }` / `{ metadata: ... }`
823
- * envelopes; we send bare for consistency.
886
+ * envelopes; we send bare for consistency. Pass `mode: 'draft'` to
887
+ * stage the change without publishing (Studio's "Save" button).
824
888
  */
825
889
  async save(type, name, item, options = {}) {
826
- const qs = options.force ? "?force=true" : "";
890
+ const params = [];
891
+ if (options.force) params.push("force=true");
892
+ if (options.mode === "draft") params.push("mode=draft");
893
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
894
+ const qs = params.length ? `?${params.join("&")}` : "";
827
895
  const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
828
896
  const headers = {
829
897
  ...this.headers,
@@ -839,13 +907,38 @@ var MetadataClient = class _MetadataClient {
839
907
  if (!res.ok) throw await parseError(res);
840
908
  return await res.json();
841
909
  }
910
+ /**
911
+ * Publish a single pending draft BY REFERENCE — promotes the draft row for
912
+ * `(type, name)` into the active overlay and drops the draft. Works for ANY
913
+ * draft, including ones with no `packageId` binding (which the package-scoped
914
+ * `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
915
+ * exact set returned by {@link listDrafts} without needing a package.
916
+ *
917
+ * Returns the server result. For `seed` drafts the protocol also materializes
918
+ * the rows and reports under `seedApplied` — a data problem never fails the
919
+ * publish, so callers should check `seedApplied?.success` and warn the user
920
+ * rather than assume the data went live.
921
+ */
922
+ async publishDraft(type, name) {
923
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
924
+ const res = await this.fetchImpl(url, {
925
+ method: "POST",
926
+ headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
927
+ body: "{}"
928
+ });
929
+ if (!res.ok) throw await parseError(res);
930
+ const body = await res.json().catch(() => ({}));
931
+ const inner = body?.data && typeof body.data === "object" ? body.data : body;
932
+ return inner;
933
+ }
842
934
  /**
843
935
  * Get the 3-state layered view of a metadata item (Phase 3a). Returns
844
936
  * `code` (the artifact / fallback default), `overlay` (the saved
845
937
  * customisation, if any), and `effective` (what the runtime sees).
846
938
  */
847
- async layered(type, name) {
848
- const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true`;
939
+ async layered(type, name, options = {}) {
940
+ const pkg = options.packageId ? `&package=${encodeURIComponent(options.packageId)}` : "";
941
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true${pkg}`;
849
942
  const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
850
943
  if (res.status === 404) {
851
944
  return { code: null, overlay: null, overlayScope: null, effective: null };
@@ -865,7 +958,50 @@ var MetadataClient = class _MetadataClient {
865
958
  code: body.code ?? null,
866
959
  overlay: body.overlay ?? null,
867
960
  overlayScope: body.overlayScope ?? null,
868
- effective: body.effective ?? null
961
+ effective: body.effective ?? null,
962
+ ...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
963
+ // ADR-0010 Phase 4 — pass through the metadata-protection envelope so
964
+ // the editor can render lock affordances without a second round trip.
965
+ ...body.lock !== void 0 ? { lock: body.lock } : {},
966
+ ...body.lockReason !== void 0 ? { lockReason: body.lockReason } : {},
967
+ ...body.lockSource !== void 0 ? { lockSource: body.lockSource } : {},
968
+ ...body.lockDocsUrl !== void 0 ? { lockDocsUrl: body.lockDocsUrl } : {},
969
+ ...body.provenance !== void 0 ? { provenance: body.provenance } : {},
970
+ ...body.packageId !== void 0 ? { packageId: body.packageId } : {},
971
+ ...body.packageVersion !== void 0 ? { packageVersion: body.packageVersion } : {},
972
+ ...body.editable !== void 0 ? { editable: body.editable } : {},
973
+ ...body.deletable !== void 0 ? { deletable: body.deletable } : {},
974
+ ...body.resettable !== void 0 ? { resettable: body.resettable } : {}
975
+ };
976
+ }
977
+ /**
978
+ * Cross-type sweep of load-time validation results — calls
979
+ * `GET /meta/diagnostics`. Returns every entry the framework
980
+ * considers invalid (or, when `severity: 'warning'`, also entries
981
+ * with only warnings).
982
+ *
983
+ * Used by the Studio's governance overview page and by the
984
+ * directory page to show "N invalid" badges per metadata type.
985
+ */
986
+ async diagnostics(options = {}) {
987
+ const params = [];
988
+ if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
989
+ if (options.severity) params.push(`severity=${encodeURIComponent(options.severity)}`);
990
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
991
+ const qs = params.length ? `?${params.join("&")}` : "";
992
+ const url = `${this.base}/diagnostics${qs}`;
993
+ const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
994
+ if (res.status === 404) {
995
+ return { entries: [], total: 0, scannedTypes: 0, scannedItems: 0, stats: {} };
996
+ }
997
+ if (!res.ok) throw await parseError(res);
998
+ const data = await res.json();
999
+ return {
1000
+ entries: Array.isArray(data.entries) ? data.entries : [],
1001
+ total: typeof data.total === "number" ? data.total : 0,
1002
+ scannedTypes: typeof data.scannedTypes === "number" ? data.scannedTypes : 0,
1003
+ scannedItems: typeof data.scannedItems === "number" ? data.scannedItems : 0,
1004
+ stats: data.stats && typeof data.stats === "object" ? data.stats : {}
869
1005
  };
870
1006
  }
871
1007
  /**
@@ -884,9 +1020,12 @@ var MetadataClient = class _MetadataClient {
884
1020
  /**
885
1021
  * Reset a metadata customization overlay back to the artifact default.
886
1022
  * Idempotent: returns the result even when no overlay row existed.
1023
+ * Pass `state: 'draft'` to discard the pending draft only (keeps the
1024
+ * published overlay intact) — useful for a Studio "Discard draft" button.
887
1025
  */
888
1026
  async reset(type, name, options = {}) {
889
- const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}`;
1027
+ const qs = options.state === "draft" ? "?state=draft" : "";
1028
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
890
1029
  const headers = { ...this.headers };
891
1030
  if (options.ifMatch) headers["If-Match"] = options.ifMatch;
892
1031
  if (options.actor) headers["X-Actor"] = options.actor;
@@ -894,6 +1033,75 @@ var MetadataClient = class _MetadataClient {
894
1033
  if (!res.ok) throw await parseError(res);
895
1034
  return await res.json();
896
1035
  }
1036
+ /**
1037
+ * Promote the pending draft of an item to the active overlay
1038
+ * (POST `/meta/:type/:name/publish`). Returns
1039
+ * `{ success, version, seq, message }`. Throws a `404 no_draft` if
1040
+ * nothing is pending and `409 metadata_conflict` if the published
1041
+ * overlay moved while the draft was sitting.
1042
+ */
1043
+ async publish(type, name, options = {}) {
1044
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
1045
+ const headers = {
1046
+ ...this.headers,
1047
+ "Content-Type": "application/json"
1048
+ };
1049
+ if (options.actor) headers["X-Actor"] = options.actor;
1050
+ const res = await this.fetchImpl(url, {
1051
+ method: "POST",
1052
+ headers,
1053
+ body: JSON.stringify(options.message ? { message: options.message } : {})
1054
+ });
1055
+ if (!res.ok) throw await parseError(res);
1056
+ return await res.json();
1057
+ }
1058
+ /**
1059
+ * Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
1060
+ * The server reads the history row at `toVersion`, writes its body back
1061
+ * as the active overlay with `operation_type='revert'`. Returns
1062
+ * `{ success, version, seq, restoredFromVersion, message }`. Throws
1063
+ * `404 version_not_found` for unknown versions and `409 version_not_restorable`
1064
+ * when the target version is a delete tombstone.
1065
+ */
1066
+ async rollback(type, name, toVersion, options = {}) {
1067
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/rollback`;
1068
+ const headers = {
1069
+ ...this.headers,
1070
+ "Content-Type": "application/json"
1071
+ };
1072
+ if (options.actor) headers["X-Actor"] = options.actor;
1073
+ const res = await this.fetchImpl(url, {
1074
+ method: "POST",
1075
+ headers,
1076
+ body: JSON.stringify({
1077
+ toVersion,
1078
+ ...options.message ? { message: options.message } : {}
1079
+ })
1080
+ });
1081
+ if (!res.ok) throw await parseError(res);
1082
+ return await res.json();
1083
+ }
1084
+ /**
1085
+ * Compute a structured top-level key diff between two history versions
1086
+ * (GET `/meta/:type/:name/diff?from=&to=`). Returns
1087
+ * `{ added, removed, changed }` where each entry carries a `path` and
1088
+ * the relevant value(s). Useful for "what changed in this draft?" and
1089
+ * pre-rollback previews.
1090
+ */
1091
+ async diff(type, name, fromVersion, toVersion) {
1092
+ const params = new URLSearchParams();
1093
+ if (fromVersion !== void 0) params.set("from", String(fromVersion));
1094
+ if (toVersion !== void 0) params.set("to", String(toVersion));
1095
+ const qs = params.toString();
1096
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/diff${qs ? `?${qs}` : ""}`;
1097
+ const res = await this.fetchImpl(url, {
1098
+ method: "GET",
1099
+ headers: this.headers,
1100
+ cache: "no-store"
1101
+ });
1102
+ if (!res.ok) throw await parseError(res);
1103
+ return await res.json();
1104
+ }
897
1105
  /** Fetch the durable history (change log) for one metadata item. */
898
1106
  async history(type, name, options = {}) {
899
1107
  const params = new URLSearchParams();
@@ -905,6 +1113,22 @@ var MetadataClient = class _MetadataClient {
905
1113
  if (!res.ok) throw await parseError(res);
906
1114
  return await res.json();
907
1115
  }
1116
+ /**
1117
+ * Fetch the protection-audit trail for one metadata item
1118
+ * (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
1119
+ * delete/reset attempt — allowed, denied, or forced — so the
1120
+ * Studio "审计日志 / Audit log" tab can render who tried what
1121
+ * and whether a lock blocked it.
1122
+ */
1123
+ async audit(type, name, options = {}) {
1124
+ const params = new URLSearchParams();
1125
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
1126
+ const qs = params.toString();
1127
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/audit${qs ? `?${qs}` : ""}`;
1128
+ const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
1129
+ if (!res.ok) throw await parseError(res);
1130
+ return await res.json();
1131
+ }
908
1132
  };
909
1133
 
910
1134
  // src/security.ts
@@ -1389,7 +1613,7 @@ var ObjectStackAdapter = class {
1389
1613
  if (existing) return existing;
1390
1614
  const promise = (async () => {
1391
1615
  await this.connect();
1392
- if (params?.$expand && params.$expand.length > 0) {
1616
+ if (params?.$expand && params.$expand.length > 0 || params?.$search != null && String(params.$search).trim() !== "") {
1393
1617
  const result = await this.rawFindWithPopulate(resource, params);
1394
1618
  return this.normalizeQueryResult(result, params);
1395
1619
  }
@@ -1587,6 +1811,34 @@ var ObjectStackAdapter = class {
1587
1811
  * @param data - Array of records to process
1588
1812
  * @returns Promise resolving to array of results
1589
1813
  */
1814
+ /**
1815
+ * Cross-object transactional batch (ObjectStack #1604). Runs the operations
1816
+ * in ONE server transaction — commit all or roll back all. A field value of
1817
+ * `{ $ref: <earlier op index> }` resolves to that op's created id, so a child
1818
+ * can reference its parent created earlier in the same batch (master-detail).
1819
+ */
1820
+ async batchTransaction(operations) {
1821
+ const url = `${this.baseUrl}/api/v1/batch`;
1822
+ const response = await fetch(url, {
1823
+ method: "POST",
1824
+ headers: { "Content-Type": "application/json", ...this.getAuthHeaders() },
1825
+ // Send the session cookie too: in the browser console auth is cookie-based
1826
+ // (no bearer `token`), so without `credentials: 'include'` this raw fetch
1827
+ // is unauthenticated — every master-detail batch save would 401. Bearer
1828
+ // (server-to-server) and cookie (console) auth now both work.
1829
+ credentials: "include",
1830
+ body: JSON.stringify({ operations })
1831
+ });
1832
+ if (!response.ok) {
1833
+ const error = await response.json().catch(() => ({ message: response.statusText }));
1834
+ throw new ObjectStackError(
1835
+ error.error || error.message || `Batch failed with status ${response.status}`,
1836
+ "BATCH_ERROR",
1837
+ response.status
1838
+ );
1839
+ }
1840
+ return response.json();
1841
+ }
1590
1842
  async bulk(resource, operation, data) {
1591
1843
  await this.connect();
1592
1844
  if (!data || data.length === 0) {
@@ -1724,13 +1976,14 @@ var ObjectStackAdapter = class {
1724
1976
  const resultObj = result;
1725
1977
  const records = resultObj.records || resultObj.value || [];
1726
1978
  const total = resultObj.total ?? resultObj.count ?? records.length;
1979
+ const hasMore = typeof resultObj.hasMore === "boolean" ? resultObj.hasMore : params?.$top ? records.length === params.$top : false;
1727
1980
  return {
1728
1981
  data: records,
1729
1982
  total,
1730
1983
  // Calculate page number safely
1731
1984
  page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
1732
1985
  pageSize: params?.$top,
1733
- hasMore: params?.$top ? records.length === params.$top : false
1986
+ hasMore
1734
1987
  };
1735
1988
  }
1736
1989
  /**
@@ -1750,6 +2003,12 @@ var ObjectStackAdapter = class {
1750
2003
  if (params.$skip !== void 0) {
1751
2004
  queryParams.set("skip", String(params.$skip));
1752
2005
  }
2006
+ if (params.$search != null && String(params.$search).trim() !== "") {
2007
+ queryParams.set("search", String(params.$search).trim());
2008
+ }
2009
+ if (params.$searchFields && params.$searchFields.length > 0) {
2010
+ queryParams.set("searchFields", params.$searchFields.join(","));
2011
+ }
1753
2012
  if (params.$select && params.$select.length > 0) {
1754
2013
  const selectFields = params.$select.includes("id") ? params.$select : ["id", ...params.$select];
1755
2014
  queryParams.set("select", selectFields.join(","));
@@ -1850,6 +2109,12 @@ var ObjectStackAdapter = class {
1850
2109
  if (params.$top !== void 0) {
1851
2110
  options.top = params.$top;
1852
2111
  }
2112
+ if (params.$search != null && String(params.$search).trim() !== "") {
2113
+ options.search = String(params.$search).trim();
2114
+ }
2115
+ if (params.$searchFields && params.$searchFields.length > 0) {
2116
+ options.searchFields = params.$searchFields;
2117
+ }
1853
2118
  return options;
1854
2119
  }
1855
2120
  /**
@@ -1956,7 +2221,7 @@ var ObjectStackAdapter = class {
1956
2221
  try {
1957
2222
  const cacheKey = `view:${objectName}:${viewId}`;
1958
2223
  return await this.metadataCache.get(cacheKey, async () => {
1959
- const result = await this.client.meta.getItem(objectName, viewId);
2224
+ const result = await this.client.meta.getItem("view", viewId);
1960
2225
  if (result && result.item) return result.item;
1961
2226
  return result ?? null;
1962
2227
  });
@@ -2015,12 +2280,26 @@ var ObjectStackAdapter = class {
2015
2280
  try {
2016
2281
  const result = await this.client.meta.getItems("view");
2017
2282
  const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
2283
+ const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
2018
2284
  return items.filter((v) => {
2019
2285
  if (!v) return false;
2020
2286
  const spec = v.list ?? v;
2021
2287
  const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
2022
- return obj === objectName;
2023
- }).map((v) => v.list ?? v);
2288
+ if (obj !== objectName) return false;
2289
+ const viewKind = v.viewKind ?? spec?.viewKind;
2290
+ return !(viewKind && FORM_FAMILY.has(viewKind));
2291
+ }).map((v) => {
2292
+ const spec = v.list ?? v;
2293
+ if (spec && spec.config && typeof spec.config === "object") {
2294
+ return {
2295
+ ...spec.config,
2296
+ name: spec.name ?? spec.config.name,
2297
+ label: spec.label ?? spec.config.label,
2298
+ isDefault: !!spec.isDefault
2299
+ };
2300
+ }
2301
+ return spec;
2302
+ });
2024
2303
  } catch (err) {
2025
2304
  console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
2026
2305
  return [];
@@ -2237,6 +2516,56 @@ var ObjectStackAdapter = class {
2237
2516
  return this.aggregateClientSide(records, params);
2238
2517
  }
2239
2518
  }
2519
+ /**
2520
+ * Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
2521
+ *
2522
+ * Posts to `POST /api/v1/analytics/dataset/query` (see `@objectstack/rest`
2523
+ * `registerAnalyticsEndpoints`). Accepts either a saved dataset name or an
2524
+ * inline draft definition — the inline form is what the Studio dataset
2525
+ * editor sends to preview an unsaved draft. The adapter's bearer token is
2526
+ * forwarded so tenant/RLS scoping (ADR-0021 D-C) is enforced server-side.
2527
+ *
2528
+ * Unlike {@link aggregate}, this does NOT fall back to client-side
2529
+ * aggregation: cross-object joins can only run on the server, so a failure
2530
+ * is surfaced to the caller (the preview panel shows the error) rather than
2531
+ * silently returning wrong numbers.
2532
+ *
2533
+ * @param dataset - An inline dataset definition (draft) OR a saved dataset name.
2534
+ * @param selection - Dimension/measure names to project + runtime directives.
2535
+ */
2536
+ async queryDataset(dataset, selection) {
2537
+ await this.connect();
2538
+ const base = (this.baseUrl || "").replace(/\/$/, "");
2539
+ const url = `${base}/api/v1/analytics/dataset/query`;
2540
+ const previewDrafts = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("preview") === "draft";
2541
+ const requestBody = typeof dataset === "string" ? { datasetName: dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} } : { dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} };
2542
+ const res = await this.fetchImpl(url, {
2543
+ method: "POST",
2544
+ headers: {
2545
+ "Content-Type": "application/json",
2546
+ ...this.token ? { Authorization: `Bearer ${this.token}` } : {}
2547
+ },
2548
+ body: JSON.stringify(requestBody)
2549
+ });
2550
+ if (!res.ok) {
2551
+ let detail = "";
2552
+ try {
2553
+ const errBody = await res.json();
2554
+ detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
2555
+ } catch {
2556
+ }
2557
+ throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
2558
+ }
2559
+ const payload = await res.json();
2560
+ const data = payload && typeof payload === "object" && "success" in payload && "data" in payload ? payload.data : payload;
2561
+ const rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
2562
+ const fields = Array.isArray(data?.fields) ? data.fields : [];
2563
+ const object = typeof data?.object === "string" ? data.object : void 0;
2564
+ const dimensionFields = data?.dimensionFields && typeof data.dimensionFields === "object" ? data.dimensionFields : void 0;
2565
+ const drillRawRows = Array.isArray(data?.drillRawRows) ? data.drillRawRows : void 0;
2566
+ const totals = Array.isArray(data?.totals) ? data.totals : void 0;
2567
+ return { rows, fields, object, dimensionFields, drillRawRows, totals };
2568
+ }
2240
2569
  /** Client-side aggregation fallback */
2241
2570
  aggregateClientSide(records, params) {
2242
2571
  const { field, function: aggFn, groupBy } = params;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "6.2.3",
3
+ "version": "7.0.0",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,14 +22,14 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@objectstack/client": "^6.7.1",
26
- "@object-ui/core": "6.2.3",
27
- "@object-ui/types": "6.2.3"
25
+ "@objectstack/client": "^10.0.0",
26
+ "@object-ui/core": "7.0.0",
27
+ "@object-ui/types": "7.0.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.5.1",
31
31
  "typescript": "^6.0.3",
32
- "vitest": "^4.1.7"
32
+ "vitest": "^4.1.9"
33
33
  },
34
34
  "publishConfig": {
35
35
  "access": "public"