@object-ui/data-objectstack 6.2.2 → 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/CHANGELOG.md CHANGED
@@ -1,5 +1,145 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 7.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 30ee761: feat(studio): surface pending drafts on the package detail (ADR-0033)
8
+
9
+ After an AI builds an app, its objects/views land as drafts bound to the app package — but Studio's active-only browsers hid them, so the package looked empty and there was no obvious way to find what to review/publish.
10
+
11
+ - `MetadataClient.listDrafts({ packageId?, type? })` calls the new `GET /api/v1/meta/_drafts` endpoint, returning pending draft headers (with `packageId`).
12
+ - The package detail sheet (PackagesPage) now shows a **Pending changes** section listing each drafted item, each linking to the existing per-item review/diff (`?review=1`) so the user can publish it. A just-built app package is no longer shown as empty.
13
+
14
+ - 053c948: feat: ADR-0047 — interface pages, visualization switcher, and Airtable-parity filters
15
+
16
+ End-user interface/list pages reach full rendering and authoring parity:
17
+
18
+ - **Spec tabs + visualization switcher** — `ObjectView` now forwards
19
+ `viewDef.tabs` (stored/served but never rendered) and `viewDef.appearance`
20
+ (`allowedVisualizations` whitelist), turning on the dormant `ViewSwitcher` when
21
+ more than one type is whitelisted; effective options = author whitelist ∩
22
+ capability-resolvable types (kanban needs `groupBy`, calendar a date field, …).
23
+ `ListView` accepts the canonical `ViewFilterRule[]` tab-filter shape.
24
+ - **User filters** — render only when `userFilters` is explicitly configured;
25
+ selections (dropdown values + active tab) mirror into `uf_*` URL params and
26
+ restore on load, so filtered lists survive reload and are shareable.
27
+ - **Toolbar polish** — the visualization switcher becomes a compact right-side
28
+ "Grid ▾" dropdown inside the tool cluster (no extra row); filter tabs and
29
+ dropdown filters are mutually exclusive.
30
+ - **Studio authoring** — a usable, schema-driven interface-page inspector
31
+ (collapsible sections honoured, array-of-enum → multi-select, a None/Tabs/
32
+ Dropdown `filter-mode` selector where None maps to ABSENCE of `userFilters`),
33
+ and the Design/Preview tabs render the live list via `InterfaceListPage`
34
+ (including a non-empty grid when the source view is hollow).
35
+
36
+ - 5332639: feat(app-shell): render full object forms (incl. master-detail) in screen-flow wizard steps
37
+
38
+ `FlowRunner` now renders an `object-form` screen step: when the paused screen
39
+ carries `kind: 'object-form'`, it mounts the real `<ObjectForm>` for the named
40
+ object (auto-routing to `MasterDetailForm` for inline child collections),
41
+ prefilled from the step's `defaults`. The form persists itself (atomic
42
+ master-detail batch), then resumes the run with the saved record id bound to the
43
+ step's `idVariable`. `dataSource`/`objects` are threaded through all three
44
+ `FlowRunner` mount points.
45
+
46
+ Also fixes three pre-existing bugs this surfaced (each affects normal forms too):
47
+
48
+ - **plugin-form**: `ObjectForm` now forwards `initialValues`/`initialData` when
49
+ routing to `MasterDetailForm`, so prefilled header values are no longer
50
+ dropped on master-detail create forms.
51
+ - **fields**: `PercentField` treated values as `0–1` fractions (`value × 100`),
52
+ so a `0–100` field (e.g. `probability` default `50`) rendered as `5000%` —
53
+ exceeding `max=100`, which makes HTML5 constraint validation mark the field
54
+ `:invalid` and silently block the whole form's submit. It now treats a field
55
+ declaring `max > 1` as the `0–100` whole-number convention, matching the
56
+ read-side formatter.
57
+ - **data-objectstack**: `ObjectStackAdapter.batchTransaction` now sends
58
+ `credentials: 'include'`, so master-detail batch saves authenticate under the
59
+ console's cookie session (previously every batch save 401'd).
60
+
61
+ - d16566f: Atomic master-detail create via the cross-object transactional batch endpoint (ObjectStack #1604).
62
+
63
+ When the server exposes the transactional batch endpoint, a NEW parent record and its child line items are now persisted in ONE server transaction — commit all or roll back all — instead of the previous client-orchestrated "create parent → create children → best-effort cleanup on failure" sequence.
64
+
65
+ **`@object-ui/data-objectstack` — `ObjectStackAdapter.batchTransaction(operations)`**
66
+
67
+ - New method posting `{ operations }` to `POST /api/v1/batch`. Operations run in one server transaction. A field value of `{ $ref: <earlier op index> }` resolves to that op's generated id, so a child can reference its parent created earlier in the same batch (master-detail FK). Throws `ObjectStackError('BATCH_ERROR')` on a non-2xx response.
68
+
69
+ **`@object-ui/plugin-form`**
70
+
71
+ - `MasterDetailForm` now detects `dataSource.batchTransaction` and, on a NEW parent, builds one atomic batch (parent at index 0, each child FK set to `{ $ref: 0 }`) via the new pure helper `buildMasterDetailBatch`. Client-side total rollups are merged into the parent payload before the batch. Edit mode and adapters without `batchTransaction` keep the existing client-orchestrated path.
72
+ - `ObjectForm` gained a `submitHandler` hook: when supplied, the form validates and hands the collected values to the host instead of calling `dataSource.create` / `dataSource.update`. `MasterDetailForm` uses it to own the atomic parent+children write while the parent fields are still rendered by `ObjectForm`.
73
+
74
+ **`@object-ui/types`**
75
+
76
+ - `ObjectFormSchema.submitHandler?: (values) => any | Promise<any>` — typed override for host-owned persistence.
77
+
78
+ Pairs with the framework-side ambient-transaction fix (ObjectQL `AsyncLocalStorage` transaction propagation) and the `/api/v1/batch` endpoint added in `@objectstack/rest`.
79
+
80
+ ### Patch Changes
81
+
82
+ - b99d9bd: ADR-0048: package-scope the Studio metadata editor read. Two installed packages
83
+ may ship metadata with the same `type`/`name`; the editor now resolves the right
84
+ one instead of first-match.
85
+
86
+ - `MetadataClient`: `layered()` and `getDraft()` accept `{ packageId }`, and
87
+ `get()` emits the `package` query param (→ server prefer-local, `?package=`).
88
+ - `ResourceListPage`: each item's edit link carries its owning package
89
+ (`?package=<row._packageId>`), so even the unscoped "all" list disambiguates;
90
+ falls back to the workspace suffix for runtime/overlay-only rows.
91
+ - `ResourceEditPage`: reads `?package=` and scopes the layered + draft read to
92
+ that package. (The route's `:appName` is the Studio app, not the edited item's
93
+ owner, so the scope must come from the URL, not the active app.)
94
+
95
+ - a58c6b8: fix(datasource): exclude form-family views from `listViews()`
96
+
97
+ `OBJECTSTACKDataSource.listViews(objectName)` feeds the object list-view
98
+ switcher (`ObjectView` → `ViewTabBar`), but returned **every** view bound to
99
+ the object — including form-family ones. With the backend now exposing each
100
+ view as an independent **ViewItem** carrying a `viewKind` discriminant
101
+ (ADR-0017, "Object has-many View"), a form view such as `crm_activity.default`
102
+ (expanded from `formViews.default`) leaked in as a spurious switcher tab and,
103
+ when opened, fell back to the default grid.
104
+
105
+ `listViews()` now filters out `viewKind` `form`/`detail` items so only
106
+ list-family views reach the switcher. Bare view specs without a `viewKind`
107
+ (legacy artifacts and user-saved views) are still treated as list views.
108
+
109
+ - Updated dependencies [5976ba3]
110
+ - Updated dependencies [eaccefd]
111
+ - Updated dependencies [f7f325d]
112
+ - Updated dependencies [c12986e]
113
+ - Updated dependencies [71d7ce0]
114
+ - Updated dependencies [053c948]
115
+ - Updated dependencies [ddbe4a2]
116
+ - Updated dependencies [2d47e94]
117
+ - Updated dependencies [9049bbe]
118
+ - Updated dependencies [cb2fdb1]
119
+ - Updated dependencies [c3749eb]
120
+ - Updated dependencies [6cfa330]
121
+ - Updated dependencies [ad8ade6]
122
+ - Updated dependencies [d54346c]
123
+ - Updated dependencies [3870c20]
124
+ - Updated dependencies [b88c560]
125
+ - Updated dependencies [d16566f]
126
+ - Updated dependencies [1394e34]
127
+ - Updated dependencies [300d755]
128
+ - Updated dependencies [4eb9cb6]
129
+ - Updated dependencies [7c239fd]
130
+ - Updated dependencies [858ad94]
131
+ - Updated dependencies [2270239]
132
+ - Updated dependencies [8d1195d]
133
+ - @object-ui/core@7.0.0
134
+ - @object-ui/types@7.0.0
135
+
136
+ ## 6.2.3
137
+
138
+ ### Patch Changes
139
+
140
+ - @object-ui/types@6.2.3
141
+ - @object-ui/core@6.2.3
142
+
3
143
  ## 6.2.2
4
144
 
5
145
  ### Patch Changes
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
  /**
@@ -2004,7 +2269,7 @@ var ObjectStackAdapter = class {
2004
2269
  try {
2005
2270
  const cacheKey = `view:${objectName}:${viewId}`;
2006
2271
  return await this.metadataCache.get(cacheKey, async () => {
2007
- const result = await this.client.meta.getItem(objectName, viewId);
2272
+ const result = await this.client.meta.getItem("view", viewId);
2008
2273
  if (result && result.item) return result.item;
2009
2274
  return result ?? null;
2010
2275
  });
@@ -2063,12 +2328,26 @@ var ObjectStackAdapter = class {
2063
2328
  try {
2064
2329
  const result = await this.client.meta.getItems("view");
2065
2330
  const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
2331
+ const FORM_FAMILY = /* @__PURE__ */ new Set(["form", "detail"]);
2066
2332
  return items.filter((v) => {
2067
2333
  if (!v) return false;
2068
2334
  const spec = v.list ?? v;
2069
2335
  const obj = spec?.data?.object ?? spec?.object ?? spec?.objectName;
2070
- return obj === objectName;
2071
- }).map((v) => v.list ?? v);
2336
+ if (obj !== objectName) return false;
2337
+ const viewKind = v.viewKind ?? spec?.viewKind;
2338
+ return !(viewKind && FORM_FAMILY.has(viewKind));
2339
+ }).map((v) => {
2340
+ const spec = v.list ?? v;
2341
+ if (spec && spec.config && typeof spec.config === "object") {
2342
+ return {
2343
+ ...spec.config,
2344
+ name: spec.name ?? spec.config.name,
2345
+ label: spec.label ?? spec.config.label,
2346
+ isDefault: !!spec.isDefault
2347
+ };
2348
+ }
2349
+ return spec;
2350
+ });
2072
2351
  } catch (err) {
2073
2352
  console.warn("[OBJECTSTACKDataSource] listViews failed:", err);
2074
2353
  return [];
@@ -2285,6 +2564,56 @@ var ObjectStackAdapter = class {
2285
2564
  return this.aggregateClientSide(records, params);
2286
2565
  }
2287
2566
  }
2567
+ /**
2568
+ * Run a semantic-layer `dataset` (ADR-0021) and return chart-ready rows.
2569
+ *
2570
+ * Posts to `POST /api/v1/analytics/dataset/query` (see `@objectstack/rest`
2571
+ * `registerAnalyticsEndpoints`). Accepts either a saved dataset name or an
2572
+ * inline draft definition — the inline form is what the Studio dataset
2573
+ * editor sends to preview an unsaved draft. The adapter's bearer token is
2574
+ * forwarded so tenant/RLS scoping (ADR-0021 D-C) is enforced server-side.
2575
+ *
2576
+ * Unlike {@link aggregate}, this does NOT fall back to client-side
2577
+ * aggregation: cross-object joins can only run on the server, so a failure
2578
+ * is surfaced to the caller (the preview panel shows the error) rather than
2579
+ * silently returning wrong numbers.
2580
+ *
2581
+ * @param dataset - An inline dataset definition (draft) OR a saved dataset name.
2582
+ * @param selection - Dimension/measure names to project + runtime directives.
2583
+ */
2584
+ async queryDataset(dataset, selection) {
2585
+ await this.connect();
2586
+ const base = (this.baseUrl || "").replace(/\/$/, "");
2587
+ const url = `${base}/api/v1/analytics/dataset/query`;
2588
+ const previewDrafts = typeof window !== "undefined" && new URLSearchParams(window.location.search).get("preview") === "draft";
2589
+ const requestBody = typeof dataset === "string" ? { datasetName: dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} } : { dataset, selection, ...previewDrafts ? { previewDrafts: true } : {} };
2590
+ const res = await this.fetchImpl(url, {
2591
+ method: "POST",
2592
+ headers: {
2593
+ "Content-Type": "application/json",
2594
+ ...this.token ? { Authorization: `Bearer ${this.token}` } : {}
2595
+ },
2596
+ body: JSON.stringify(requestBody)
2597
+ });
2598
+ if (!res.ok) {
2599
+ let detail = "";
2600
+ try {
2601
+ const errBody = await res.json();
2602
+ detail = errBody?.message || errBody?.error || JSON.stringify(errBody);
2603
+ } catch {
2604
+ }
2605
+ throw new Error(`Dataset query failed: ${res.status} ${res.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
2606
+ }
2607
+ const payload = await res.json();
2608
+ const data = payload && typeof payload === "object" && "success" in payload && "data" in payload ? payload.data : payload;
2609
+ const rows = Array.isArray(data?.rows) ? data.rows : Array.isArray(data) ? data : [];
2610
+ const fields = Array.isArray(data?.fields) ? data.fields : [];
2611
+ const object = typeof data?.object === "string" ? data.object : void 0;
2612
+ const dimensionFields = data?.dimensionFields && typeof data.dimensionFields === "object" ? data.dimensionFields : void 0;
2613
+ const drillRawRows = Array.isArray(data?.drillRawRows) ? data.drillRawRows : void 0;
2614
+ const totals = Array.isArray(data?.totals) ? data.totals : void 0;
2615
+ return { rows, fields, object, dimensionFields, drillRawRows, totals };
2616
+ }
2288
2617
  /** Client-side aggregation fallback */
2289
2618
  aggregateClientSide(records, params) {
2290
2619
  const { field, function: aggFn, groupBy } = params;