@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.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
  /**
@@ -1862,13 +2127,10 @@ var ObjectStackAdapter = class {
1862
2127
  async getObjectSchema(objectName) {
1863
2128
  await this.connect();
1864
2129
  try {
1865
- const schema = await this.metadataCache.get(objectName, async () => {
1866
- const result = await this.client.meta.getItem("object", objectName);
1867
- if (result && result.item) {
1868
- return result.item;
1869
- }
1870
- return result;
1871
- });
2130
+ const schema = await this.metadataCache.get(
2131
+ objectName,
2132
+ () => this.fetchObjectSchemaFresh(objectName)
2133
+ );
1872
2134
  return schema;
1873
2135
  } catch (error) {
1874
2136
  const errorObj = error;
@@ -1881,6 +2143,48 @@ var ObjectStackAdapter = class {
1881
2143
  throw createErrorFromResponse(errorObj, `getObjectSchema(${objectName})`);
1882
2144
  }
1883
2145
  }
2146
+ /**
2147
+ * Fetch a single object's schema while always revalidating the browser cache.
2148
+ *
2149
+ * The server serves `GET /api/v1/meta/object/:name` with
2150
+ * `Cache-Control: public, max-age=3600`, so the default `fetch` the SDK uses
2151
+ * keeps returning the same response from the browser HTTP cache for up to an
2152
+ * hour without contacting the origin. Because the create/edit form reads the
2153
+ * object schema through {@link getObjectSchema}, a field added + published in
2154
+ * the same session never appears in the form even though it is live (the LIST
2155
+ * endpoint, `/meta/object`, is uncached — which is why list views update).
2156
+ *
2157
+ * Issuing the read with `cache: 'no-cache'` forces a conditional revalidation
2158
+ * (`If-None-Match`): a changed ETag returns the fresh schema, an unchanged one
2159
+ * still gets a cheap `304`. We go through `fetchImpl` (the adapter's
2160
+ * authenticated fetch) rather than `client.meta.getItem` because the SDK does
2161
+ * not expose the request cache mode.
2162
+ */
2163
+ async fetchObjectSchemaFresh(objectName) {
2164
+ const baseUrl = (this.baseUrl || "").replace(/\/$/, "");
2165
+ const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
2166
+ const metaPath = hasApiVersionSuffix ? "/meta" : "/api/v1/meta";
2167
+ const url = `${baseUrl}${metaPath}/object/${encodeURIComponent(objectName)}`;
2168
+ const headers = { "Content-Type": "application/json" };
2169
+ if (this.token) {
2170
+ headers["Authorization"] = `Bearer ${this.token}`;
2171
+ }
2172
+ const res = await this.fetchImpl(url, {
2173
+ method: "GET",
2174
+ headers,
2175
+ // Revalidate instead of serving the stale `max-age` body (see doc above).
2176
+ cache: "no-cache"
2177
+ });
2178
+ if (!res.ok) {
2179
+ const errBody = await res.json().catch(() => ({ message: res.statusText }));
2180
+ const err = new Error(errBody?.error?.message || errBody?.message || res.statusText);
2181
+ err.status = res.status;
2182
+ throw err;
2183
+ }
2184
+ const body = await res.json();
2185
+ const data = body && typeof body === "object" && "success" in body && "data" in body ? body.data : body;
2186
+ return data && typeof data === "object" && "item" in data ? data.item : data;
2187
+ }
1884
2188
  /**
1885
2189
  * Get access to the underlying ObjectStack client for advanced operations.
1886
2190
  */
@@ -1956,7 +2260,7 @@ var ObjectStackAdapter = class {
1956
2260
  try {
1957
2261
  const cacheKey = `view:${objectName}:${viewId}`;
1958
2262
  return await this.metadataCache.get(cacheKey, async () => {
1959
- const result = await this.client.meta.getItem(objectName, viewId);
2263
+ const result = await this.client.meta.getItem("view", viewId);
1960
2264
  if (result && result.item) return result.item;
1961
2265
  return result ?? null;
1962
2266
  });
@@ -2015,12 +2319,26 @@ var ObjectStackAdapter = class {
2015
2319
  try {
2016
2320
  const result = await this.client.meta.getItems("view");
2017
2321
  const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
2322
+ const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
2018
2323
  return items.filter((v) => {
2019
2324
  if (!v) return false;
2020
2325
  const spec = v.list ?? v;
2021
2326
  const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
2022
- return obj === objectName;
2023
- }).map((v) => v.list ?? v);
2327
+ if (obj !== objectName) return false;
2328
+ const viewKind = v.viewKind ?? spec?.viewKind;
2329
+ return !(viewKind && FORM_FAMILY.has(viewKind));
2330
+ }).map((v) => {
2331
+ const spec = v.list ?? v;
2332
+ if (spec && spec.config && typeof spec.config === "object") {
2333
+ return {
2334
+ ...spec.config,
2335
+ name: spec.name ?? spec.config.name,
2336
+ label: spec.label ?? spec.config.label,
2337
+ isDefault: !!spec.isDefault
2338
+ };
2339
+ }
2340
+ return spec;
2341
+ });
2024
2342
  } catch (err) {
2025
2343
  console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
2026
2344
  return [];
@@ -2237,6 +2555,56 @@ var ObjectStackAdapter = class {
2237
2555
  return this.aggregateClientSide(records, params);
2238
2556
  }
2239
2557
  }
2558
+ /**
2559
+ * Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
2560
+ *
2561
+ * Posts to `POST /api/v1/analytics/dataset/query` (see `@objectstack/rest`
2562
+ * `registerAnalyticsEndpoints`). Accepts either a saved dataset name or an
2563
+ * inline draft definition — the inline form is what the Studio dataset
2564
+ * editor sends to preview an unsaved draft. The adapter's bearer token is
2565
+ * forwarded so tenant/RLS scoping (ADR-0021 D-C) is enforced server-side.
2566
+ *
2567
+ * Unlike {@link aggregate}, this does NOT fall back to client-side
2568
+ * aggregation: cross-object joins can only run on the server, so a failure
2569
+ * is surfaced to the caller (the preview panel shows the error) rather than
2570
+ * silently returning wrong numbers.
2571
+ *
2572
+ * @param dataset - An inline dataset definition (draft) OR a saved dataset name.
2573
+ * @param selection - Dimension/measure names to project + runtime directives.
2574
+ */
2575
+ async queryDataset(dataset, selection) {
2576
+ await this.connect();
2577
+ const base = (this.baseUrl || "").replace(/\/$/, "");
2578
+ const url = `${base}/api/v1/analytics/dataset/query`;
2579
+ const previewDrafts = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("preview") === "draft";
2580
+ const requestBody = typeof dataset === "string" ? { datasetName: dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} } : { dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} };
2581
+ const res = await this.fetchImpl(url, {
2582
+ method: "POST",
2583
+ headers: {
2584
+ "Content-Type": "application/json",
2585
+ ...this.token ? { Authorization: `Bearer ${this.token}` } : {}
2586
+ },
2587
+ body: JSON.stringify(requestBody)
2588
+ });
2589
+ if (!res.ok) {
2590
+ let detail = "";
2591
+ try {
2592
+ const errBody = await res.json();
2593
+ detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
2594
+ } catch {
2595
+ }
2596
+ throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
2597
+ }
2598
+ const payload = await res.json();
2599
+ const data = payload && typeof payload === "object" && "success" in payload && "data" in payload ? payload.data : payload;
2600
+ const rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
2601
+ const fields = Array.isArray(data?.fields) ? data.fields : [];
2602
+ const object = typeof data?.object === "string" ? data.object : void 0;
2603
+ const dimensionFields = data?.dimensionFields && typeof data.dimensionFields === "object" ? data.dimensionFields : void 0;
2604
+ const drillRawRows = Array.isArray(data?.drillRawRows) ? data.drillRawRows : void 0;
2605
+ const totals = Array.isArray(data?.totals) ? data.totals : void 0;
2606
+ return { rows, fields, object, dimensionFields, drillRawRows, totals };
2607
+ }
2240
2608
  /** Client-side aggregation fallback */
2241
2609
  aggregateClientSide(records, params) {
2242
2610
  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.1.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.1.0",
27
+ "@object-ui/types": "7.1.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"
@@ -0,0 +1,77 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
10
+ import { ObjectStackAdapter, clearSharedDiscoveryCache } from './index';
11
+
12
+ /**
13
+ * A fetch mock that answers discovery + records the `GET /meta/object/:name`
14
+ * read so we can assert how it was issued.
15
+ */
16
+ function makeFetch(objectBody: unknown) {
17
+ const calls: Array<{ url: string; init?: any }> = [];
18
+ const fetchImpl = vi.fn(async (url: any, init?: any) => {
19
+ const u = String(url);
20
+ calls.push({ url: u, init });
21
+ if (u.includes('/api/v1/discovery')) {
22
+ return { ok: true, status: 200, statusText: 'OK', json: async () => ({ success: true, data: { version: 'v1', routes: {} } }) } as any;
23
+ }
24
+ if (u.includes('/api/v1/meta/object/')) {
25
+ return { ok: true, status: 200, statusText: 'OK', json: async () => objectBody } as any;
26
+ }
27
+ return { ok: true, status: 200, statusText: 'OK', json: async () => ({}) } as any;
28
+ });
29
+ return { fetchImpl, calls };
30
+ }
31
+
32
+ describe('ObjectStackAdapter.getObjectSchema', () => {
33
+ beforeEach(() => clearSharedDiscoveryCache());
34
+
35
+ it('reads with cache: "no-cache" so a freshly published field is not masked by the server max-age', async () => {
36
+ // The server marks single-object metadata `public, max-age=3600`; without
37
+ // revalidation the browser HTTP cache would keep serving the pre-publish
38
+ // schema and the create/edit form would miss fields published this session.
39
+ const { fetchImpl, calls } = makeFetch({
40
+ name: 'leave_request',
41
+ fields: { employee_name: { type: 'text' }, department: { type: 'text' } },
42
+ });
43
+ const adapter = new ObjectStackAdapter({
44
+ baseUrl: 'http://localhost:3000',
45
+ token: 'tok_123',
46
+ autoReconnect: false,
47
+ fetch: fetchImpl as any,
48
+ });
49
+
50
+ const schema: any = await adapter.getObjectSchema('leave_request');
51
+
52
+ // Returns the (unwrapped) object schema with its fields.
53
+ expect(Object.keys(schema.fields)).toEqual(['employee_name', 'department']);
54
+
55
+ const get = calls.find((c) => c.url.includes('/api/v1/meta/object/'))!;
56
+ expect(get.url).toBe('http://localhost:3000/api/v1/meta/object/leave_request');
57
+ expect(get.init.method).toBe('GET');
58
+ // The crux of the fix: revalidate the HTTP cache instead of serving the
59
+ // stale `max-age` body.
60
+ expect(get.init.cache).toBe('no-cache');
61
+ expect(get.init.headers.Authorization).toBe('Bearer tok_123');
62
+ });
63
+
64
+ it('unwraps the { item } envelope when the server wraps the object', async () => {
65
+ const { fetchImpl } = makeFetch({ item: { name: 'account', fields: { x: { type: 'text' } } } });
66
+ const adapter = new ObjectStackAdapter({
67
+ baseUrl: 'http://localhost:3000',
68
+ autoReconnect: false,
69
+ fetch: fetchImpl as any,
70
+ });
71
+
72
+ const schema: any = await adapter.getObjectSchema('account');
73
+
74
+ expect(schema.name).toBe('account');
75
+ expect(Object.keys(schema.fields)).toEqual(['x']);
76
+ });
77
+ });