@object-ui/data-objectstack 6.2.3 → 7.1.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
@@ -814,9 +814,12 @@ var MetadataClient = class _MetadataClient {
814
814
  __publicField(this, "base");
815
815
  __publicField(this, "fetchImpl");
816
816
  __publicField(this, "headers");
817
+ /** ADR-0037: when true, reads render the draft-overlaid world. */
818
+ __publicField(this, "previewDrafts");
817
819
  this.base = buildBase(config);
818
820
  this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
819
821
  this.headers = { Accept: "application/json", ...config.headers ?? {} };
822
+ this.previewDrafts = config.previewDrafts === true;
820
823
  }
821
824
  /** Update the client's environment scope at runtime. */
822
825
  withEnvironment(environmentId) {
@@ -824,7 +827,24 @@ var MetadataClient = class _MetadataClient {
824
827
  baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
825
828
  environmentId,
826
829
  fetch: this.fetchImpl,
827
- headers: this.headers
830
+ headers: this.headers,
831
+ previewDrafts: this.previewDrafts
832
+ });
833
+ }
834
+ /**
835
+ * Derive a client whose reads render the draft-overlaid world (or not).
836
+ * Same base/fetch/headers; used by the app-shell to switch the whole
837
+ * renderer tree into ADR-0037 preview mode off one URL flag.
838
+ */
839
+ withPreviewDrafts(previewDrafts) {
840
+ if (previewDrafts === this.previewDrafts) return this;
841
+ const envMatch = this.base.match(/\/api\/v\d+\/environments\/([^/]+)\/meta$/);
842
+ return new _MetadataClient({
843
+ baseUrl: this.base.replace(/\/api\/v\d+(?:\/environments\/[^/]+)?\/meta$/, "").replace(/\/+$/, ""),
844
+ ...envMatch ? { environmentId: decodeURIComponent(envMatch[1]) } : {},
845
+ fetch: this.fetchImpl,
846
+ headers: this.headers,
847
+ previewDrafts
828
848
  });
829
849
  }
830
850
  /** List all registered metadata types (returns the registry rows). */
@@ -842,7 +862,10 @@ var MetadataClient = class _MetadataClient {
842
862
  }
843
863
  /** List items of a metadata type (e.g. `object`, `field`, `view`). */
844
864
  async list(type, options = {}) {
845
- const qs = options.packageId ? `?package=${encodeURIComponent(options.packageId)}` : "";
865
+ const params = [];
866
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
867
+ if (this.previewDrafts) params.push("preview=draft");
868
+ const qs = params.length ? `?${params.join("&")}` : "";
846
869
  const url = `${this.base}/${encodeURIComponent(type)}${qs}`;
847
870
  const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
848
871
  if (!res.ok) throw await parseError(res);
@@ -853,25 +876,70 @@ var MetadataClient = class _MetadataClient {
853
876
  }
854
877
  return [];
855
878
  }
879
+ /**
880
+ * List pending DRAFT items (ADR-0033) — what an AI authored but nobody
881
+ * published yet. `list()` only sees published/active metadata, so a
882
+ * just-built app package looks empty there; this surfaces the drafts so the
883
+ * console can show a "pending changes" view and draft-aware package contents.
884
+ * Optionally narrow by `packageId` and/or `type`. Returns light headers
885
+ * (no body) carrying `packageId` for grouping.
886
+ */
887
+ async listDrafts(options = {}) {
888
+ const params = [];
889
+ if (options.packageId) params.push(`packageId=${encodeURIComponent(options.packageId)}`);
890
+ if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
891
+ const qs = params.length ? `?${params.join("&")}` : "";
892
+ const url = `${this.base}/_drafts${qs}`;
893
+ const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
894
+ if (!res.ok) throw await parseError(res);
895
+ const data = await res.json();
896
+ if (Array.isArray(data)) return data;
897
+ return data?.drafts ?? data?.data?.drafts ?? [];
898
+ }
856
899
  /**
857
900
  * Get a single metadata item. Returns the unwrapped item content
858
901
  * (matching the framework REST handler which calls `res.json(item)`).
859
902
  * Returns `null` on 404 to keep the call-site ergonomic.
860
903
  */
861
- async get(type, name) {
862
- const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}`;
904
+ async get(type, name, options = {}) {
905
+ const params = [];
906
+ if (options.state === "draft") params.push("state=draft");
907
+ else if (this.previewDrafts) params.push("preview=draft");
908
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
909
+ const qs = params.length ? `?${params.join("&")}` : "";
910
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
863
911
  const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
864
912
  if (res.status === 404) return null;
865
913
  if (!res.ok) throw await parseError(res);
866
914
  return await res.json();
867
915
  }
916
+ /**
917
+ * Read the pending draft body for an item (`?state=draft`). Returns
918
+ * `null` when there is no draft pending. Draft reads do NOT fall
919
+ * back to the published overlay or the artifact registry — a `null`
920
+ * unambiguously means "nothing to publish".
921
+ *
922
+ * Note: the framework wraps draft responses in an envelope
923
+ * `{ type, name, item }` (matching `getMetaItem`); callers should
924
+ * read `.item` to get the body. The legacy `get()` returns the
925
+ * unwrapped body, so this method preserves that asymmetry by
926
+ * returning whatever the server sent.
927
+ */
928
+ async getDraft(type, name, options = {}) {
929
+ return this.get(type, name, { state: "draft", ...options.packageId ? { packageId: options.packageId } : {} });
930
+ }
868
931
  /**
869
932
  * Save (PUT) a metadata item. The framework accepts both the bare
870
933
  * item payload and the `{ item: ... }` / `{ metadata: ... }`
871
- * envelopes; we send bare for consistency.
934
+ * envelopes; we send bare for consistency. Pass `mode: 'draft'` to
935
+ * stage the change without publishing (Studio's "Save" button).
872
936
  */
873
937
  async save(type, name, item, options = {}) {
874
- const qs = options.force ? "?force=true" : "";
938
+ const params = [];
939
+ if (options.force) params.push("force=true");
940
+ if (options.mode === "draft") params.push("mode=draft");
941
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
942
+ const qs = params.length ? `?${params.join("&")}` : "";
875
943
  const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
876
944
  const headers = {
877
945
  ...this.headers,
@@ -887,13 +955,38 @@ var MetadataClient = class _MetadataClient {
887
955
  if (!res.ok) throw await parseError(res);
888
956
  return await res.json();
889
957
  }
958
+ /**
959
+ * Publish a single pending draft BY REFERENCE — promotes the draft row for
960
+ * `(type, name)` into the active overlay and drops the draft. Works for ANY
961
+ * draft, including ones with no `packageId` binding (which the package-scoped
962
+ * `/packages/:id/publish-drafts` flow cannot reach). Use this to publish the
963
+ * exact set returned by {@link listDrafts} without needing a package.
964
+ *
965
+ * Returns the server result. For `seed` drafts the protocol also materializes
966
+ * the rows and reports under `seedApplied` — a data problem never fails the
967
+ * publish, so callers should check `seedApplied?.success` and warn the user
968
+ * rather than assume the data went live.
969
+ */
970
+ async publishDraft(type, name) {
971
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
972
+ const res = await this.fetchImpl(url, {
973
+ method: "POST",
974
+ headers: { ...this.headers, "Content-Type": "application/json", Accept: "application/json" },
975
+ body: "{}"
976
+ });
977
+ if (!res.ok) throw await parseError(res);
978
+ const body = await res.json().catch(() => ({}));
979
+ const inner = body?.data && typeof body.data === "object" ? body.data : body;
980
+ return inner;
981
+ }
890
982
  /**
891
983
  * Get the 3-state layered view of a metadata item (Phase 3a). Returns
892
984
  * `code` (the artifact / fallback default), `overlay` (the saved
893
985
  * customisation, if any), and `effective` (what the runtime sees).
894
986
  */
895
- async layered(type, name) {
896
- const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true`;
987
+ async layered(type, name, options = {}) {
988
+ const pkg = options.packageId ? `&package=${encodeURIComponent(options.packageId)}` : "";
989
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}?layers=true${pkg}`;
897
990
  const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
898
991
  if (res.status === 404) {
899
992
  return { code: null, overlay: null, overlayScope: null, effective: null };
@@ -913,7 +1006,50 @@ var MetadataClient = class _MetadataClient {
913
1006
  code: body.code ?? null,
914
1007
  overlay: body.overlay ?? null,
915
1008
  overlayScope: body.overlayScope ?? null,
916
- effective: body.effective ?? null
1009
+ effective: body.effective ?? null,
1010
+ ...body._diagnostics ? { _diagnostics: body._diagnostics } : {},
1011
+ // ADR-0010 Phase 4 — pass through the metadata-protection envelope so
1012
+ // the editor can render lock affordances without a second round trip.
1013
+ ...body.lock !== void 0 ? { lock: body.lock } : {},
1014
+ ...body.lockReason !== void 0 ? { lockReason: body.lockReason } : {},
1015
+ ...body.lockSource !== void 0 ? { lockSource: body.lockSource } : {},
1016
+ ...body.lockDocsUrl !== void 0 ? { lockDocsUrl: body.lockDocsUrl } : {},
1017
+ ...body.provenance !== void 0 ? { provenance: body.provenance } : {},
1018
+ ...body.packageId !== void 0 ? { packageId: body.packageId } : {},
1019
+ ...body.packageVersion !== void 0 ? { packageVersion: body.packageVersion } : {},
1020
+ ...body.editable !== void 0 ? { editable: body.editable } : {},
1021
+ ...body.deletable !== void 0 ? { deletable: body.deletable } : {},
1022
+ ...body.resettable !== void 0 ? { resettable: body.resettable } : {}
1023
+ };
1024
+ }
1025
+ /**
1026
+ * Cross-type sweep of load-time validation results — calls
1027
+ * `GET /meta/diagnostics`. Returns every entry the framework
1028
+ * considers invalid (or, when `severity: 'warning'`, also entries
1029
+ * with only warnings).
1030
+ *
1031
+ * Used by the Studio's governance overview page and by the
1032
+ * directory page to show "N invalid" badges per metadata type.
1033
+ */
1034
+ async diagnostics(options = {}) {
1035
+ const params = [];
1036
+ if (options.type) params.push(`type=${encodeURIComponent(options.type)}`);
1037
+ if (options.severity) params.push(`severity=${encodeURIComponent(options.severity)}`);
1038
+ if (options.packageId) params.push(`package=${encodeURIComponent(options.packageId)}`);
1039
+ const qs = params.length ? `?${params.join("&")}` : "";
1040
+ const url = `${this.base}/diagnostics${qs}`;
1041
+ const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
1042
+ if (res.status === 404) {
1043
+ return { entries: [], total: 0, scannedTypes: 0, scannedItems: 0, stats: {} };
1044
+ }
1045
+ if (!res.ok) throw await parseError(res);
1046
+ const data = await res.json();
1047
+ return {
1048
+ entries: Array.isArray(data.entries) ? data.entries : [],
1049
+ total: typeof data.total === "number" ? data.total : 0,
1050
+ scannedTypes: typeof data.scannedTypes === "number" ? data.scannedTypes : 0,
1051
+ scannedItems: typeof data.scannedItems === "number" ? data.scannedItems : 0,
1052
+ stats: data.stats && typeof data.stats === "object" ? data.stats : {}
917
1053
  };
918
1054
  }
919
1055
  /**
@@ -932,9 +1068,12 @@ var MetadataClient = class _MetadataClient {
932
1068
  /**
933
1069
  * Reset a metadata customization overlay back to the artifact default.
934
1070
  * Idempotent: returns the result even when no overlay row existed.
1071
+ * Pass `state: 'draft'` to discard the pending draft only (keeps the
1072
+ * published overlay intact) — useful for a Studio "Discard draft" button.
935
1073
  */
936
1074
  async reset(type, name, options = {}) {
937
- const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}`;
1075
+ const qs = options.state === "draft" ? "?state=draft" : "";
1076
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}${qs}`;
938
1077
  const headers = { ...this.headers };
939
1078
  if (options.ifMatch) headers["If-Match"] = options.ifMatch;
940
1079
  if (options.actor) headers["X-Actor"] = options.actor;
@@ -942,6 +1081,75 @@ var MetadataClient = class _MetadataClient {
942
1081
  if (!res.ok) throw await parseError(res);
943
1082
  return await res.json();
944
1083
  }
1084
+ /**
1085
+ * Promote the pending draft of an item to the active overlay
1086
+ * (POST `/meta/:type/:name/publish`). Returns
1087
+ * `{ success, version, seq, message }`. Throws a `404 no_draft` if
1088
+ * nothing is pending and `409 metadata_conflict` if the published
1089
+ * overlay moved while the draft was sitting.
1090
+ */
1091
+ async publish(type, name, options = {}) {
1092
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/publish`;
1093
+ const headers = {
1094
+ ...this.headers,
1095
+ "Content-Type": "application/json"
1096
+ };
1097
+ if (options.actor) headers["X-Actor"] = options.actor;
1098
+ const res = await this.fetchImpl(url, {
1099
+ method: "POST",
1100
+ headers,
1101
+ body: JSON.stringify(options.message ? { message: options.message } : {})
1102
+ });
1103
+ if (!res.ok) throw await parseError(res);
1104
+ return await res.json();
1105
+ }
1106
+ /**
1107
+ * Restore an item to a previous version (POST `/meta/:type/:name/rollback`).
1108
+ * The server reads the history row at `toVersion`, writes its body back
1109
+ * as the active overlay with `operation_type='revert'`. Returns
1110
+ * `{ success, version, seq, restoredFromVersion, message }`. Throws
1111
+ * `404 version_not_found` for unknown versions and `409 version_not_restorable`
1112
+ * when the target version is a delete tombstone.
1113
+ */
1114
+ async rollback(type, name, toVersion, options = {}) {
1115
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/rollback`;
1116
+ const headers = {
1117
+ ...this.headers,
1118
+ "Content-Type": "application/json"
1119
+ };
1120
+ if (options.actor) headers["X-Actor"] = options.actor;
1121
+ const res = await this.fetchImpl(url, {
1122
+ method: "POST",
1123
+ headers,
1124
+ body: JSON.stringify({
1125
+ toVersion,
1126
+ ...options.message ? { message: options.message } : {}
1127
+ })
1128
+ });
1129
+ if (!res.ok) throw await parseError(res);
1130
+ return await res.json();
1131
+ }
1132
+ /**
1133
+ * Compute a structured top-level key diff between two history versions
1134
+ * (GET `/meta/:type/:name/diff?from=&to=`). Returns
1135
+ * `{ added, removed, changed }` where each entry carries a `path` and
1136
+ * the relevant value(s). Useful for "what changed in this draft?" and
1137
+ * pre-rollback previews.
1138
+ */
1139
+ async diff(type, name, fromVersion, toVersion) {
1140
+ const params = new URLSearchParams();
1141
+ if (fromVersion !== void 0) params.set("from", String(fromVersion));
1142
+ if (toVersion !== void 0) params.set("to", String(toVersion));
1143
+ const qs = params.toString();
1144
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/diff${qs ? `?${qs}` : ""}`;
1145
+ const res = await this.fetchImpl(url, {
1146
+ method: "GET",
1147
+ headers: this.headers,
1148
+ cache: "no-store"
1149
+ });
1150
+ if (!res.ok) throw await parseError(res);
1151
+ return await res.json();
1152
+ }
945
1153
  /** Fetch the durable history (change log) for one metadata item. */
946
1154
  async history(type, name, options = {}) {
947
1155
  const params = new URLSearchParams();
@@ -953,6 +1161,22 @@ var MetadataClient = class _MetadataClient {
953
1161
  if (!res.ok) throw await parseError(res);
954
1162
  return await res.json();
955
1163
  }
1164
+ /**
1165
+ * Fetch the protection-audit trail for one metadata item
1166
+ * (ADR-0010 §3.6 / Phase 4.1). Shows every save/publish/rollback/
1167
+ * delete/reset attempt — allowed, denied, or forced — so the
1168
+ * Studio "审计日志 / Audit log" tab can render who tried what
1169
+ * and whether a lock blocked it.
1170
+ */
1171
+ async audit(type, name, options = {}) {
1172
+ const params = new URLSearchParams();
1173
+ if (options.limit !== void 0) params.set("limit", String(options.limit));
1174
+ const qs = params.toString();
1175
+ const url = `${this.base}/${encodeURIComponent(type)}/${encodeURIComponent(name)}/audit${qs ? `?${qs}` : ""}`;
1176
+ const res = await this.fetchImpl(url, { method: "GET", headers: this.headers, cache: "no-store" });
1177
+ if (!res.ok) throw await parseError(res);
1178
+ return await res.json();
1179
+ }
956
1180
  };
957
1181
 
958
1182
  // src/security.ts
@@ -1437,7 +1661,7 @@ var ObjectStackAdapter = class {
1437
1661
  if (existing) return existing;
1438
1662
  const promise = (async () => {
1439
1663
  await this.connect();
1440
- if (params?.$expand && params.$expand.length > 0) {
1664
+ if (params?.$expand && params.$expand.length > 0 || params?.$search != null && String(params.$search).trim() !== "") {
1441
1665
  const result = await this.rawFindWithPopulate(resource, params);
1442
1666
  return this.normalizeQueryResult(result, params);
1443
1667
  }
@@ -1635,6 +1859,34 @@ var ObjectStackAdapter = class {
1635
1859
  * @param data - Array of records to process
1636
1860
  * @returns Promise resolving to array of results
1637
1861
  */
1862
+ /**
1863
+ * Cross-object transactional batch (ObjectStack #1604). Runs the operations
1864
+ * in ONE server transaction — commit all or roll back all. A field value of
1865
+ * `{ $ref: <earlier op index> }` resolves to that op's created id, so a child
1866
+ * can reference its parent created earlier in the same batch (master-detail).
1867
+ */
1868
+ async batchTransaction(operations) {
1869
+ const url = `${this.baseUrl}/api/v1/batch`;
1870
+ const response = await fetch(url, {
1871
+ method: "POST",
1872
+ headers: { "Content-Type": "application/json", ...this.getAuthHeaders() },
1873
+ // Send the session cookie too: in the browser console auth is cookie-based
1874
+ // (no bearer `token`), so without `credentials: 'include'` this raw fetch
1875
+ // is unauthenticated — every master-detail batch save would 401. Bearer
1876
+ // (server-to-server) and cookie (console) auth now both work.
1877
+ credentials: "include",
1878
+ body: JSON.stringify({ operations })
1879
+ });
1880
+ if (!response.ok) {
1881
+ const error = await response.json().catch(() => ({ message: response.statusText }));
1882
+ throw new ObjectStackError(
1883
+ error.error || error.message || `Batch failed with status ${response.status}`,
1884
+ "BATCH_ERROR",
1885
+ response.status
1886
+ );
1887
+ }
1888
+ return response.json();
1889
+ }
1638
1890
  async bulk(resource, operation, data) {
1639
1891
  await this.connect();
1640
1892
  if (!data || data.length === 0) {
@@ -1772,13 +2024,14 @@ var ObjectStackAdapter = class {
1772
2024
  const resultObj = result;
1773
2025
  const records = resultObj.records || resultObj.value || [];
1774
2026
  const total = resultObj.total ?? resultObj.count ?? records.length;
2027
+ const hasMore = typeof resultObj.hasMore === "boolean" ? resultObj.hasMore : params?.$top ? records.length === params.$top : false;
1775
2028
  return {
1776
2029
  data: records,
1777
2030
  total,
1778
2031
  // Calculate page number safely
1779
2032
  page: params?.$skip && params.$top ? Math.floor(params.$skip / params.$top) + 1 : 1,
1780
2033
  pageSize: params?.$top,
1781
- hasMore: params?.$top ? records.length === params.$top : false
2034
+ hasMore
1782
2035
  };
1783
2036
  }
1784
2037
  /**
@@ -1798,6 +2051,12 @@ var ObjectStackAdapter = class {
1798
2051
  if (params.$skip !== void 0) {
1799
2052
  queryParams.set("skip", String(params.$skip));
1800
2053
  }
2054
+ if (params.$search != null && String(params.$search).trim() !== "") {
2055
+ queryParams.set("search", String(params.$search).trim());
2056
+ }
2057
+ if (params.$searchFields && params.$searchFields.length > 0) {
2058
+ queryParams.set("searchFields", params.$searchFields.join(","));
2059
+ }
1801
2060
  if (params.$select && params.$select.length > 0) {
1802
2061
  const selectFields = params.$select.includes("id") ? params.$select : ["id", ...params.$select];
1803
2062
  queryParams.set("select", selectFields.join(","));
@@ -1898,6 +2157,12 @@ var ObjectStackAdapter = class {
1898
2157
  if (params.$top !== void 0) {
1899
2158
  options.top = params.$top;
1900
2159
  }
2160
+ if (params.$search != null && String(params.$search).trim() !== "") {
2161
+ options.search = String(params.$search).trim();
2162
+ }
2163
+ if (params.$searchFields && params.$searchFields.length > 0) {
2164
+ options.searchFields = params.$searchFields;
2165
+ }
1901
2166
  return options;
1902
2167
  }
1903
2168
  /**
@@ -1910,13 +2175,10 @@ var ObjectStackAdapter = class {
1910
2175
  async getObjectSchema(objectName) {
1911
2176
  await this.connect();
1912
2177
  try {
1913
- const schema = await this.metadataCache.get(objectName, async () => {
1914
- const result = await this.client.meta.getItem("object", objectName);
1915
- if (result && result.item) {
1916
- return result.item;
1917
- }
1918
- return result;
1919
- });
2178
+ const schema = await this.metadataCache.get(
2179
+ objectName,
2180
+ () => this.fetchObjectSchemaFresh(objectName)
2181
+ );
1920
2182
  return schema;
1921
2183
  } catch (error) {
1922
2184
  const errorObj = error;
@@ -1929,6 +2191,48 @@ var ObjectStackAdapter = class {
1929
2191
  throw createErrorFromResponse(errorObj, `getObjectSchema(${objectName})`);
1930
2192
  }
1931
2193
  }
2194
+ /**
2195
+ * Fetch a single object's schema while always revalidating the browser cache.
2196
+ *
2197
+ * The server serves `GET /api/v1/meta/object/:name` with
2198
+ * `Cache-Control: public, max-age=3600`, so the default `fetch` the SDK uses
2199
+ * keeps returning the same response from the browser HTTP cache for up to an
2200
+ * hour without contacting the origin. Because the create/edit form reads the
2201
+ * object schema through {@link getObjectSchema}, a field added + published in
2202
+ * the same session never appears in the form even though it is live (the LIST
2203
+ * endpoint, `/meta/object`, is uncached — which is why list views update).
2204
+ *
2205
+ * Issuing the read with `cache: 'no-cache'` forces a conditional revalidation
2206
+ * (`If-None-Match`): a changed ETag returns the fresh schema, an unchanged one
2207
+ * still gets a cheap `304`. We go through `fetchImpl` (the adapter's
2208
+ * authenticated fetch) rather than `client.meta.getItem` because the SDK does
2209
+ * not expose the request cache mode.
2210
+ */
2211
+ async fetchObjectSchemaFresh(objectName) {
2212
+ const baseUrl = (this.baseUrl || "").replace(/\/$/, "");
2213
+ const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
2214
+ const metaPath = hasApiVersionSuffix ? "/meta" : "/api/v1/meta";
2215
+ const url = `${baseUrl}${metaPath}/object/${encodeURIComponent(objectName)}`;
2216
+ const headers = { "Content-Type": "application/json" };
2217
+ if (this.token) {
2218
+ headers["Authorization"] = `Bearer ${this.token}`;
2219
+ }
2220
+ const res = await this.fetchImpl(url, {
2221
+ method: "GET",
2222
+ headers,
2223
+ // Revalidate instead of serving the stale `max-age` body (see doc above).
2224
+ cache: "no-cache"
2225
+ });
2226
+ if (!res.ok) {
2227
+ const errBody = await res.json().catch(() => ({ message: res.statusText }));
2228
+ const err = new Error(errBody?.error?.message || errBody?.message || res.statusText);
2229
+ err.status = res.status;
2230
+ throw err;
2231
+ }
2232
+ const body = await res.json();
2233
+ const data = body && typeof body === "object" && "success" in body && "data" in body ? body.data : body;
2234
+ return data && typeof data === "object" && "item" in data ? data.item : data;
2235
+ }
1932
2236
  /**
1933
2237
  * Get access to the underlying ObjectStack client for advanced operations.
1934
2238
  */
@@ -2004,7 +2308,7 @@ var ObjectStackAdapter = class {
2004
2308
  try {
2005
2309
  const cacheKey = `view:${objectName}:${viewId}`;
2006
2310
  return await this.metadataCache.get(cacheKey, async () => {
2007
- const result = await this.client.meta.getItem(objectName, viewId);
2311
+ const result = await this.client.meta.getItem("view", viewId);
2008
2312
  if (result && result.item) return result.item;
2009
2313
  return result ?? null;
2010
2314
  });
@@ -2063,12 +2367,26 @@ var ObjectStackAdapter = class {
2063
2367
  try {
2064
2368
  const result = await this.client.meta.getItems("view");
2065
2369
  const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
2370
+ const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
2066
2371
  return items.filter((v) => {
2067
2372
  if (!v) return false;
2068
2373
  const spec = v.list ?? v;
2069
2374
  const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
2070
- return obj === objectName;
2071
- }).map((v) => v.list ?? v);
2375
+ if (obj !== objectName) return false;
2376
+ const viewKind = v.viewKind ?? spec?.viewKind;
2377
+ return !(viewKind && FORM_FAMILY.has(viewKind));
2378
+ }).map((v) => {
2379
+ const spec = v.list ?? v;
2380
+ if (spec && spec.config && typeof spec.config === "object") {
2381
+ return {
2382
+ ...spec.config,
2383
+ name: spec.name ?? spec.config.name,
2384
+ label: spec.label ?? spec.config.label,
2385
+ isDefault: !!spec.isDefault
2386
+ };
2387
+ }
2388
+ return spec;
2389
+ });
2072
2390
  } catch (err) {
2073
2391
  console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
2074
2392
  return [];
@@ -2285,6 +2603,56 @@ var ObjectStackAdapter = class {
2285
2603
  return this.aggregateClientSide(records, params);
2286
2604
  }
2287
2605
  }
2606
+ /**
2607
+ * Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
2608
+ *
2609
+ * Posts to `POST /api/v1/analytics/dataset/query` (see `@objectstack/rest`
2610
+ * `registerAnalyticsEndpoints`). Accepts either a saved dataset name or an
2611
+ * inline draft definition — the inline form is what the Studio dataset
2612
+ * editor sends to preview an unsaved draft. The adapter's bearer token is
2613
+ * forwarded so tenant/RLS scoping (ADR-0021 D-C) is enforced server-side.
2614
+ *
2615
+ * Unlike {@link aggregate}, this does NOT fall back to client-side
2616
+ * aggregation: cross-object joins can only run on the server, so a failure
2617
+ * is surfaced to the caller (the preview panel shows the error) rather than
2618
+ * silently returning wrong numbers.
2619
+ *
2620
+ * @param dataset - An inline dataset definition (draft) OR a saved dataset name.
2621
+ * @param selection - Dimension/measure names to project + runtime directives.
2622
+ */
2623
+ async queryDataset(dataset, selection) {
2624
+ await this.connect();
2625
+ const base = (this.baseUrl || "").replace(/\/$/, "");
2626
+ const url = `${base}/api/v1/analytics/dataset/query`;
2627
+ const previewDrafts = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("preview") === "draft";
2628
+ const requestBody = typeof dataset === "string" ? { datasetName: dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} } : { dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} };
2629
+ const res = await this.fetchImpl(url, {
2630
+ method: "POST",
2631
+ headers: {
2632
+ "Content-Type": "application/json",
2633
+ ...this.token ? { Authorization: `Bearer ${this.token}` } : {}
2634
+ },
2635
+ body: JSON.stringify(requestBody)
2636
+ });
2637
+ if (!res.ok) {
2638
+ let detail = "";
2639
+ try {
2640
+ const errBody = await res.json();
2641
+ detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
2642
+ } catch {
2643
+ }
2644
+ throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
2645
+ }
2646
+ const payload = await res.json();
2647
+ const data = payload && typeof payload === "object" && "success" in payload && "data" in payload ? payload.data : payload;
2648
+ const rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
2649
+ const fields = Array.isArray(data?.fields) ? data.fields : [];
2650
+ const object = typeof data?.object === "string" ? data.object : void 0;
2651
+ const dimensionFields = data?.dimensionFields && typeof data.dimensionFields === "object" ? data.dimensionFields : void 0;
2652
+ const drillRawRows = Array.isArray(data?.drillRawRows) ? data.drillRawRows : void 0;
2653
+ const totals = Array.isArray(data?.totals) ? data.totals : void 0;
2654
+ return { rows, fields, object, dimensionFields, drillRawRows, totals };
2655
+ }
2288
2656
  /** Client-side aggregation fallback */
2289
2657
  aggregateClientSide(records, params) {
2290
2658
  const { field, function: aggFn, groupBy } = params;