@object-ui/data-objectstack 11.2.0 → 11.4.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,51 @@
1
1
  # @object-ui/data-objectstack
2
2
 
3
+ ## 11.4.0
4
+
5
+ ### Patch Changes
6
+
7
+ - c0164ad: fix(studio): surface spec-validation failures on the field at save/publish
8
+
9
+ When a Studio metadata draft failed spec validation, the designer got a single
10
+ opaque banner (and, on a partial publish, a false "published!" toast) — the
11
+ server was already returning field-anchored issues, but the client threw them
12
+ away. Two problems, both fixed:
13
+
14
+ - **`parseError` (data-objectstack)** read `String(body.error)`, which yields
15
+ `"[object Object]"` for the dispatcher's object-shaped error, and ignored the
16
+ validation `issues`. It now reads the message from either shape (string or
17
+ `{ message }`) and exposes `MetadataError.issues`, accepting all live server
18
+ shapes — top-level `body.issues` (REST server) and `error.details.issues`
19
+ (HTTP dispatcher).
20
+
21
+ - **Studio save/publish (app-shell)** now render those issues **field-anchored**.
22
+ A new `formatMetadataError` helper turns a caught error into one line per
23
+ offending field (`• fields.amount.type — Invalid option: …`); the save banners
24
+ render it with `whitespace-pre-line`. `doPublish` no longer claims success when
25
+ the response carries `data.failed[]` — it lists which drafts failed and why
26
+ (the server returns 200 with the failures buried, so the UI used to swallow
27
+ them). `formatPublishFailures` formats those per-draft.
28
+
29
+ Verified end-to-end against a live backend: an invalid object draft returns 422
30
+ with field-anchored issues, and the Studio banner shows
31
+ `• fields.amount.type — Invalid option: expected one of "text"|…` instead of a
32
+ generic message. Unit-tested: `parseError` on the dispatcher shape, and the
33
+ `formatMetadataError` / `formatPublishFailures` helpers.
34
+
35
+ - Updated dependencies [8bf6295]
36
+ - Updated dependencies [1948c5b]
37
+ - Updated dependencies [c38d107]
38
+ - @object-ui/types@11.4.0
39
+ - @object-ui/core@11.4.0
40
+
41
+ ## 11.3.0
42
+
43
+ ### Patch Changes
44
+
45
+ - Updated dependencies [d23d6eb]
46
+ - @object-ui/core@11.3.0
47
+ - @object-ui/types@11.3.0
48
+
3
49
  ## 11.2.0
4
50
 
5
51
  ### Patch Changes
package/dist/index.cjs CHANGED
@@ -585,6 +585,7 @@ function createObjectStackUserStateAdapter(options) {
585
585
  }
586
586
  } = options;
587
587
  let cachedRowId = null;
588
+ let saveChain = Promise.resolve();
588
589
  const findExisting = async () => {
589
590
  const params = {
590
591
  $filter: {
@@ -615,6 +616,42 @@ function createObjectStackUserStateAdapter(options) {
615
616
  }
616
617
  return [];
617
618
  };
619
+ const upsert = async (items) => {
620
+ const now = (/* @__PURE__ */ new Date()).toISOString();
621
+ if (cachedRowId !== null) {
622
+ try {
623
+ await dataSource.update(resource, cachedRowId, { value: items, updated_at: now });
624
+ return;
625
+ } catch (updateError) {
626
+ cachedRowId = null;
627
+ onError("save", updateError);
628
+ }
629
+ }
630
+ const existing = await findExisting();
631
+ if (existing && existing.id !== void 0 && existing.id !== null) {
632
+ cachedRowId = existing.id;
633
+ await dataSource.update(resource, existing.id, { value: items, updated_at: now });
634
+ return;
635
+ }
636
+ try {
637
+ const created = await dataSource.create(resource, {
638
+ user_id: userId,
639
+ key,
640
+ value: items,
641
+ updated_at: now
642
+ });
643
+ const newId = created?.id;
644
+ if (newId !== void 0 && newId !== null) cachedRowId = newId;
645
+ } catch (createError) {
646
+ const recovered = await findExisting();
647
+ if (recovered && recovered.id !== void 0 && recovered.id !== null) {
648
+ cachedRowId = recovered.id;
649
+ await dataSource.update(resource, recovered.id, { value: items, updated_at: now });
650
+ return;
651
+ }
652
+ throw createError;
653
+ }
654
+ };
618
655
  return {
619
656
  async load() {
620
657
  try {
@@ -631,40 +668,11 @@ function createObjectStackUserStateAdapter(options) {
631
668
  }
632
669
  },
633
670
  async save(items) {
634
- try {
635
- const now = (/* @__PURE__ */ new Date()).toISOString();
636
- if (cachedRowId !== null) {
637
- try {
638
- await dataSource.update(resource, cachedRowId, {
639
- value: items,
640
- updated_at: now
641
- });
642
- return;
643
- } catch (updateError) {
644
- cachedRowId = null;
645
- onError("save", updateError);
646
- }
647
- }
648
- const existing = await findExisting();
649
- if (existing && existing.id !== void 0 && existing.id !== null) {
650
- cachedRowId = existing.id;
651
- await dataSource.update(resource, existing.id, {
652
- value: items,
653
- updated_at: now
654
- });
655
- return;
656
- }
657
- const created = await dataSource.create(resource, {
658
- user_id: userId,
659
- key,
660
- value: items,
661
- updated_at: now
662
- });
663
- const newId = created?.id;
664
- if (newId !== void 0 && newId !== null) cachedRowId = newId;
665
- } catch (error) {
671
+ const run = saveChain.then(() => upsert(items)).catch((error) => {
666
672
  onError("save", error);
667
- }
673
+ });
674
+ saveChain = run;
675
+ return run;
668
676
  }
669
677
  };
670
678
  }
@@ -800,13 +808,17 @@ async function parseError(res) {
800
808
  } catch {
801
809
  body = await res.text().catch(() => void 0);
802
810
  }
803
- const message = (body && typeof body === "object" && "error" in body ? String(body.error) : void 0) ?? `Metadata request failed: ${res.status} ${res.statusText}`;
804
- const err = new Error(message);
811
+ const bodyObj = body && typeof body === "object" ? body : void 0;
812
+ const errNode = bodyObj?.error;
813
+ const message = (errNode && typeof errNode === "object" ? errNode.message : errNode) ?? bodyObj?.message ?? `Metadata request failed: ${res.status} ${res.statusText}`;
814
+ const err = new Error(String(message));
805
815
  err.status = res.status;
806
816
  err.body = body;
807
- if (body && typeof body === "object" && "code" in body) {
808
- err.code = String(body.code);
809
- }
817
+ const details = errNode && typeof errNode === "object" ? errNode.details : void 0;
818
+ const issues = details?.issues ?? (errNode && typeof errNode === "object" ? errNode.issues : void 0) ?? bodyObj?.issues;
819
+ if (Array.isArray(issues)) err.issues = issues;
820
+ const code = details?.code ?? (errNode && typeof errNode === "object" ? errNode.code : void 0) ?? bodyObj?.code;
821
+ if (code != null) err.code = String(code);
810
822
  return err;
811
823
  }
812
824
  var MetadataClient = class _MetadataClient {
@@ -1516,6 +1528,11 @@ var ObjectStackAdapter = class {
1516
1528
  // so optional collections like sys_presence don't hammer the server with
1517
1529
  // failing requests on every record open / panel render.
1518
1530
  __publicField(this, "missingResources", /* @__PURE__ */ new Set());
1531
+ // Subscribers registered via onMutation(). Emitted after each successful
1532
+ // create/update/delete so data-bound views (ListView, ObjectView, kanban,
1533
+ // calendar) auto-refresh — the interface ListView relies on to reflect
1534
+ // inline-edit "Save All" writes without a manual reload.
1535
+ __publicField(this, "mutationListeners", /* @__PURE__ */ new Set());
1519
1536
  this.client = new import_client.ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
1520
1537
  this.metadataCache = new MetadataCache(config.cache);
1521
1538
  this.autoReconnect = config.autoReconnect ?? true;
@@ -1724,9 +1741,35 @@ var ObjectStackAdapter = class {
1724
1741
  /**
1725
1742
  * Create a new record.
1726
1743
  */
1744
+ /**
1745
+ * Notify all mutation subscribers. A throwing listener must not break the
1746
+ * mutation or starve the other subscribers, so each is isolated.
1747
+ */
1748
+ emitMutation(event) {
1749
+ for (const listener of this.mutationListeners) {
1750
+ try {
1751
+ listener(event);
1752
+ } catch (err) {
1753
+ console.warn("ObjectStackAdapter: mutation listener error", err);
1754
+ }
1755
+ }
1756
+ }
1757
+ /**
1758
+ * Subscribe to create/update/delete events on any resource. Returns an
1759
+ * unsubscribe function. Data-bound views use this to auto-refresh after a
1760
+ * mutation (e.g. inline-edit "Save All", which writes through `update` and
1761
+ * must repaint the list without a manual reload).
1762
+ */
1763
+ onMutation(callback) {
1764
+ this.mutationListeners.add(callback);
1765
+ return () => {
1766
+ this.mutationListeners.delete(callback);
1767
+ };
1768
+ }
1727
1769
  async create(resource, data) {
1728
1770
  await this.connect();
1729
1771
  const result = await this.client.data.create(resource, data);
1772
+ this.emitMutation({ type: "create", resource, record: { ...result.record } });
1730
1773
  return result.record;
1731
1774
  }
1732
1775
  /**
@@ -1749,6 +1792,7 @@ var ObjectStackAdapter = class {
1749
1792
  data,
1750
1793
  opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
1751
1794
  );
1795
+ this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
1752
1796
  return result.record;
1753
1797
  } catch (err) {
1754
1798
  throw normaliseClientError(err);
@@ -1769,6 +1813,9 @@ var ObjectStackAdapter = class {
1769
1813
  String(id),
1770
1814
  opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
1771
1815
  );
1816
+ if (result.deleted) {
1817
+ this.emitMutation({ type: "delete", resource, id });
1818
+ }
1772
1819
  return result.deleted;
1773
1820
  } catch (err) {
1774
1821
  throw normaliseClientError(err);
@@ -1793,15 +1840,19 @@ var ObjectStackAdapter = class {
1793
1840
  await this.connect();
1794
1841
  if (!ids || ids.length === 0) return 0;
1795
1842
  const records = ids.map((id) => ({ id: String(id), data: patch }));
1843
+ const emitBulk = (count) => {
1844
+ if (count > 0) this.emitMutation({ type: "update", resource });
1845
+ return count;
1846
+ };
1796
1847
  const updateMany = this.client.data.updateMany;
1797
1848
  if (typeof updateMany === "function") {
1798
1849
  try {
1799
1850
  const res = await updateMany(resource, records, { continueOnError: true });
1800
1851
  if (res && typeof res === "object" && typeof res.succeeded === "number") {
1801
- return res.succeeded;
1852
+ return emitBulk(res.succeeded);
1802
1853
  }
1803
- if (Array.isArray(res)) return res.length;
1804
- return ids.length;
1854
+ if (Array.isArray(res)) return emitBulk(res.length);
1855
+ return emitBulk(ids.length);
1805
1856
  } catch (err) {
1806
1857
  throw normaliseClientError(err);
1807
1858
  }
@@ -1814,7 +1865,7 @@ var ObjectStackAdapter = class {
1814
1865
  } catch {
1815
1866
  }
1816
1867
  }
1817
- return succeeded;
1868
+ return emitBulk(succeeded);
1818
1869
  }
1819
1870
  /**
1820
1871
  * Single-call bulk delete. Mirrors the bulkUpdate contract: prefers
@@ -1827,15 +1878,19 @@ var ObjectStackAdapter = class {
1827
1878
  await this.connect();
1828
1879
  if (!ids || ids.length === 0) return 0;
1829
1880
  const strIds = ids.map((id) => String(id));
1881
+ const emitBulk = (count) => {
1882
+ if (count > 0) this.emitMutation({ type: "delete", resource });
1883
+ return count;
1884
+ };
1830
1885
  const deleteMany = this.client.data.deleteMany;
1831
1886
  if (typeof deleteMany === "function") {
1832
1887
  try {
1833
1888
  const res = await deleteMany(resource, strIds, { continueOnError: true });
1834
1889
  if (res && typeof res === "object" && typeof res.succeeded === "number") {
1835
- return res.succeeded;
1890
+ return emitBulk(res.succeeded);
1836
1891
  }
1837
- if (Array.isArray(res)) return res.length;
1838
- return strIds.length;
1892
+ if (Array.isArray(res)) return emitBulk(res.length);
1893
+ return emitBulk(strIds.length);
1839
1894
  } catch (err) {
1840
1895
  throw normaliseClientError(err);
1841
1896
  }
@@ -1848,7 +1903,7 @@ var ObjectStackAdapter = class {
1848
1903
  } catch {
1849
1904
  }
1850
1905
  }
1851
- return succeeded;
1906
+ return emitBulk(succeeded);
1852
1907
  }
1853
1908
  /**
1854
1909
  * Bulk operations with optimized batch processing and error handling.
@@ -2008,6 +2063,157 @@ var ObjectStackAdapter = class {
2008
2063
  );
2009
2064
  }
2010
2065
  }
2066
+ /**
2067
+ * Bulk-import raw spreadsheet rows in a single server round-trip via
2068
+ * `POST /api/v1/data/:object/import`. The server performs all value coercion
2069
+ * (booleans, numbers, dates→ISO, select label→code, lookup name→id) from the
2070
+ * object's field metadata, so this method forwards the request verbatim and
2071
+ * returns the aggregate + per-row result untouched.
2072
+ *
2073
+ * Requires `@objectstack/client` with `data.import` (server `/import` route).
2074
+ * Callers should feature-detect (`typeof dataSource.importRecords`) and fall
2075
+ * back to a per-row `create` loop when unavailable.
2076
+ */
2077
+ async importRecords(resource, request) {
2078
+ await this.connect();
2079
+ const importFn = this.client.data.import;
2080
+ if (typeof importFn !== "function") {
2081
+ throw new ObjectStackError(
2082
+ "The connected @objectstack/client does not support data.import(). Upgrade the client, or import via a per-row create fallback.",
2083
+ "UNSUPPORTED_OPERATION",
2084
+ 400
2085
+ );
2086
+ }
2087
+ try {
2088
+ const result = await importFn.call(this.client.data, resource, request);
2089
+ return result;
2090
+ } catch (err) {
2091
+ throw normaliseClientError(err);
2092
+ }
2093
+ }
2094
+ /**
2095
+ * Feature-detect the async import-job API on the connected client. Older
2096
+ * clients/servers lack these routes; callers fall back to {@link importRecords}.
2097
+ */
2098
+ importJobApi() {
2099
+ const d = this.client.data;
2100
+ if (typeof d.createImportJob !== "function") return void 0;
2101
+ return d;
2102
+ }
2103
+ /**
2104
+ * Start an asynchronous import job — the large-file counterpart to
2105
+ * {@link importRecords}. Posts the whole payload once; the server processes
2106
+ * rows in the background. Requires an `@objectstack/client` new enough to
2107
+ * expose `data.createImportJob` (server `/import/jobs` route). Callers should
2108
+ * feature-detect (`typeof dataSource.createImportJob`) and fall back to the
2109
+ * synchronous path when unavailable.
2110
+ */
2111
+ async createImportJob(resource, request) {
2112
+ await this.connect();
2113
+ const api = this.importJobApi();
2114
+ if (!api) {
2115
+ throw new ObjectStackError(
2116
+ "The connected @objectstack/client does not support async import jobs (data.createImportJob). Upgrade the client, or use the synchronous importRecords() path.",
2117
+ "UNSUPPORTED_OPERATION",
2118
+ 400
2119
+ );
2120
+ }
2121
+ try {
2122
+ return await api.createImportJob.call(this.client.data, resource, request);
2123
+ } catch (err) {
2124
+ throw normaliseClientError(err);
2125
+ }
2126
+ }
2127
+ /** Poll an import job's progress. Requires {@link createImportJob} support. */
2128
+ async getImportJobProgress(jobId) {
2129
+ await this.connect();
2130
+ const api = this.importJobApi();
2131
+ if (!api) {
2132
+ throw new ObjectStackError(
2133
+ "The connected @objectstack/client does not support async import jobs.",
2134
+ "UNSUPPORTED_OPERATION",
2135
+ 400
2136
+ );
2137
+ }
2138
+ try {
2139
+ return await api.getImportJobProgress.call(this.client.data, jobId);
2140
+ } catch (err) {
2141
+ throw normaliseClientError(err);
2142
+ }
2143
+ }
2144
+ /** Fetch an import job's capped per-row results. */
2145
+ async getImportJobResults(jobId) {
2146
+ await this.connect();
2147
+ const api = this.importJobApi();
2148
+ if (!api) {
2149
+ throw new ObjectStackError(
2150
+ "The connected @objectstack/client does not support async import jobs.",
2151
+ "UNSUPPORTED_OPERATION",
2152
+ 400
2153
+ );
2154
+ }
2155
+ try {
2156
+ return await api.getImportJobResults.call(this.client.data, jobId);
2157
+ } catch (err) {
2158
+ throw normaliseClientError(err);
2159
+ }
2160
+ }
2161
+ /** List recent import jobs (history), newest first. */
2162
+ async listImportJobs(options = {}) {
2163
+ await this.connect();
2164
+ const api = this.importJobApi();
2165
+ if (!api) {
2166
+ throw new ObjectStackError(
2167
+ "The connected @objectstack/client does not support async import jobs.",
2168
+ "UNSUPPORTED_OPERATION",
2169
+ 400
2170
+ );
2171
+ }
2172
+ try {
2173
+ return await api.listImportJobs.call(this.client.data, options);
2174
+ } catch (err) {
2175
+ throw normaliseClientError(err);
2176
+ }
2177
+ }
2178
+ /** Cancel a pending/running import job (cooperative). */
2179
+ async cancelImportJob(jobId) {
2180
+ await this.connect();
2181
+ const api = this.importJobApi();
2182
+ if (!api) {
2183
+ throw new ObjectStackError(
2184
+ "The connected @objectstack/client does not support async import jobs.",
2185
+ "UNSUPPORTED_OPERATION",
2186
+ 400
2187
+ );
2188
+ }
2189
+ try {
2190
+ await api.cancelImportJob.call(this.client.data, jobId);
2191
+ } catch (err) {
2192
+ throw normaliseClientError(err);
2193
+ }
2194
+ }
2195
+ /**
2196
+ * Logically roll back a finished import job — delete the records it created
2197
+ * and restore the records it updated to their pre-import values. Requires an
2198
+ * `@objectstack/client` new enough to expose `data.undoImportJob`, and a job
2199
+ * the server captured an undo log for (see {@link ImportJobProgressInfo.undoable}).
2200
+ */
2201
+ async undoImportJob(jobId) {
2202
+ await this.connect();
2203
+ const api = this.importJobApi();
2204
+ if (!api || typeof api.undoImportJob !== "function") {
2205
+ throw new ObjectStackError(
2206
+ "The connected @objectstack/client does not support undoing import jobs (data.undoImportJob).",
2207
+ "UNSUPPORTED_OPERATION",
2208
+ 400
2209
+ );
2210
+ }
2211
+ try {
2212
+ return await api.undoImportJob.call(this.client.data, jobId);
2213
+ } catch (err) {
2214
+ throw normaliseClientError(err);
2215
+ }
2216
+ }
2011
2217
  /**
2012
2218
  * Normalize the result from data.find() or data.query() into a consistent QueryResult.
2013
2219
  */
@@ -2105,6 +2311,53 @@ var ObjectStackAdapter = class {
2105
2311
  }
2106
2312
  return body;
2107
2313
  }
2314
+ /**
2315
+ * Synchronously download a server-streamed export (csv / json / xlsx).
2316
+ *
2317
+ * Hits `GET /api/v1/data/:object/export`, which streams matching rows in the
2318
+ * requested format, formats values for readability (lookup → name, select →
2319
+ * label, boolean → 是/否, dates formatted) and enforces permissions. The
2320
+ * filter / sort are translated the same way as `rawFindWithPopulate` so the
2321
+ * exported file mirrors the active list view. Returns the file as a Blob;
2322
+ * the caller triggers the browser download.
2323
+ */
2324
+ async exportDownload(resource, request = {}) {
2325
+ const queryParams = new URLSearchParams();
2326
+ const format = request.format === "xlsx" ? "xlsx" : request.format === "json" ? "json" : "csv";
2327
+ queryParams.set("format", format);
2328
+ if (request.fields && request.fields.length > 0) {
2329
+ queryParams.set("fields", request.fields.join(","));
2330
+ }
2331
+ if (request.limit && request.limit > 0) {
2332
+ queryParams.set("limit", String(request.limit));
2333
+ }
2334
+ if (request.includeHeaders === false) {
2335
+ queryParams.set("header", "false");
2336
+ }
2337
+ if (request.sort && request.sort.length > 0) {
2338
+ const orderby = request.sort.filter((s) => s && s.field).map((s) => `${s.field}:${s.direction === "desc" ? "desc" : "asc"}`).join(",");
2339
+ if (orderby) queryParams.set("orderby", orderby);
2340
+ }
2341
+ if (request.filter !== void 0 && request.filter !== null) {
2342
+ const translated = translateFilterToAST(request.filter);
2343
+ if (translated !== void 0) {
2344
+ queryParams.set("filter", JSON.stringify(translated));
2345
+ }
2346
+ }
2347
+ const baseUrl = this.baseUrl.replace(/\/$/, "");
2348
+ const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
2349
+ const dataPath = hasApiVersionSuffix ? "/data" : "/api/v1/data";
2350
+ const url = `${baseUrl}${dataPath}/${encodeURIComponent(resource)}/export?${queryParams.toString()}`;
2351
+ const headers = { ...this.getAuthHeaders() };
2352
+ const res = await this.fetchImpl(url, { method: "GET", headers, credentials: "include" });
2353
+ if (!res.ok) {
2354
+ const errorBody = await res.json().catch(() => ({ message: res.statusText }));
2355
+ const err = new Error(errorBody?.error?.message || errorBody?.message || res.statusText);
2356
+ err.status = res.status;
2357
+ throw err;
2358
+ }
2359
+ return await res.blob();
2360
+ }
2108
2361
  /**
2109
2362
  * Convert ObjectUI QueryParams to ObjectStack QueryOptions.
2110
2363
  * Maps OData-style conventions to ObjectStack conventions.
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ObjectStackClient } from '@objectstack/client';
2
- import { DataSource, QueryParams, QueryResult, FileUploadResult } from '@object-ui/types';
2
+ import { DataSource, QueryParams, QueryResult, MutationEvent, ImportRequestOptions, ImportRecordsResult, CreateImportJobResult, ImportJobProgressInfo, ImportJobResultsInfo, ListImportJobsOptions, ImportJobSummaryInfo, ImportJobUndoResult, ExportDownloadRequest, FileUploadResult } from '@object-ui/types';
3
3
  export { FileUploadResult } from '@object-ui/types';
4
4
 
5
5
  /**
@@ -764,10 +764,19 @@ interface MetadataHistoryOptions {
764
764
  /** Limit the number of events returned. */
765
765
  limit?: number;
766
766
  }
767
+ /** A single field-anchored spec-validation issue (server `error.details.issues`). */
768
+ interface MetadataValidationIssue {
769
+ /** Dot-path to the offending field, e.g. `fields.amount.type` (`''` = root). */
770
+ path: string;
771
+ message: string;
772
+ code?: string;
773
+ }
767
774
  interface MetadataError extends Error {
768
775
  status: number;
769
776
  code?: string;
770
777
  body?: unknown;
778
+ /** Structured spec-validation issues, when the failure was a validation error. */
779
+ issues?: MetadataValidationIssue[];
771
780
  }
772
781
  /**
773
782
  * MetadataClient — read/write protocol metadata via the framework REST API.
@@ -1302,6 +1311,7 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1302
1311
  private fetchImpl;
1303
1312
  private inflightFinds;
1304
1313
  private missingResources;
1314
+ private mutationListeners;
1305
1315
  constructor(config: {
1306
1316
  baseUrl: string;
1307
1317
  token?: string;
@@ -1359,6 +1369,18 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1359
1369
  /**
1360
1370
  * Create a new record.
1361
1371
  */
1372
+ /**
1373
+ * Notify all mutation subscribers. A throwing listener must not break the
1374
+ * mutation or starve the other subscribers, so each is isolated.
1375
+ */
1376
+ private emitMutation;
1377
+ /**
1378
+ * Subscribe to create/update/delete events on any resource. Returns an
1379
+ * unsubscribe function. Data-bound views use this to auto-refresh after a
1380
+ * mutation (e.g. inline-edit "Save All", which writes through `update` and
1381
+ * must repaint the list without a manual reload).
1382
+ */
1383
+ onMutation(callback: (event: MutationEvent<T>) => void): () => void;
1362
1384
  create(resource: string, data: Partial<T>): Promise<T>;
1363
1385
  /**
1364
1386
  * Update an existing record.
@@ -1432,6 +1454,47 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1432
1454
  results: any[];
1433
1455
  }>;
1434
1456
  bulk(resource: string, operation: 'create' | 'update' | 'delete', data: Partial<T>[]): Promise<T[]>;
1457
+ /**
1458
+ * Bulk-import raw spreadsheet rows in a single server round-trip via
1459
+ * `POST /api/v1/data/:object/import`. The server performs all value coercion
1460
+ * (booleans, numbers, dates→ISO, select label→code, lookup name→id) from the
1461
+ * object's field metadata, so this method forwards the request verbatim and
1462
+ * returns the aggregate + per-row result untouched.
1463
+ *
1464
+ * Requires `@objectstack/client` with `data.import` (server `/import` route).
1465
+ * Callers should feature-detect (`typeof dataSource.importRecords`) and fall
1466
+ * back to a per-row `create` loop when unavailable.
1467
+ */
1468
+ importRecords(resource: string, request: ImportRequestOptions): Promise<ImportRecordsResult>;
1469
+ /**
1470
+ * Feature-detect the async import-job API on the connected client. Older
1471
+ * clients/servers lack these routes; callers fall back to {@link importRecords}.
1472
+ */
1473
+ private importJobApi;
1474
+ /**
1475
+ * Start an asynchronous import job — the large-file counterpart to
1476
+ * {@link importRecords}. Posts the whole payload once; the server processes
1477
+ * rows in the background. Requires an `@objectstack/client` new enough to
1478
+ * expose `data.createImportJob` (server `/import/jobs` route). Callers should
1479
+ * feature-detect (`typeof dataSource.createImportJob`) and fall back to the
1480
+ * synchronous path when unavailable.
1481
+ */
1482
+ createImportJob(resource: string, request: ImportRequestOptions): Promise<CreateImportJobResult>;
1483
+ /** Poll an import job's progress. Requires {@link createImportJob} support. */
1484
+ getImportJobProgress(jobId: string): Promise<ImportJobProgressInfo>;
1485
+ /** Fetch an import job's capped per-row results. */
1486
+ getImportJobResults(jobId: string): Promise<ImportJobResultsInfo>;
1487
+ /** List recent import jobs (history), newest first. */
1488
+ listImportJobs(options?: ListImportJobsOptions): Promise<ImportJobSummaryInfo[]>;
1489
+ /** Cancel a pending/running import job (cooperative). */
1490
+ cancelImportJob(jobId: string): Promise<void>;
1491
+ /**
1492
+ * Logically roll back a finished import job — delete the records it created
1493
+ * and restore the records it updated to their pre-import values. Requires an
1494
+ * `@objectstack/client` new enough to expose `data.undoImportJob`, and a job
1495
+ * the server captured an undo log for (see {@link ImportJobProgressInfo.undoable}).
1496
+ */
1497
+ undoImportJob(jobId: string): Promise<ImportJobUndoResult>;
1435
1498
  /**
1436
1499
  * Normalize the result from data.find() or data.query() into a consistent QueryResult.
1437
1500
  */
@@ -1443,6 +1506,17 @@ declare class ObjectStackAdapter<T = unknown> implements DataSource<T> {
1443
1506
  * to findData({ object, query: req.query }) which processes `populate`.
1444
1507
  */
1445
1508
  private rawFindWithPopulate;
1509
+ /**
1510
+ * Synchronously download a server-streamed export (csv / json / xlsx).
1511
+ *
1512
+ * Hits `GET /api/v1/data/:object/export`, which streams matching rows in the
1513
+ * requested format, formats values for readability (lookup → name, select →
1514
+ * label, boolean → 是/否, dates formatted) and enforces permissions. The
1515
+ * filter / sort are translated the same way as `rawFindWithPopulate` so the
1516
+ * exported file mirrors the active list view. Returns the file as a Blob;
1517
+ * the caller triggers the browser download.
1518
+ */
1519
+ exportDownload(resource: string, request?: ExportDownloadRequest): Promise<Blob>;
1446
1520
  /**
1447
1521
  * Convert ObjectUI QueryParams to ObjectStack QueryOptions.
1448
1522
  * Maps OData-style conventions to ObjectStack conventions.
@@ -1770,4 +1844,4 @@ declare function createObjectStackAdapter<T = unknown>(config: {
1770
1844
  reconnectDelay?: number;
1771
1845
  }): DataSource<T>;
1772
1846
 
1773
- export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, type MetadataAuditEntry, type MetadataAuditResponse, MetadataClient, type MetadataClientConfig, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, MetadataNotFoundError, type MetadataReference, type MetadataSaveOptions, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, generateContractManifest, getSharedDiscovery, is404Error, isConcurrentUpdateError, isErrorType, isObjectStackError, normaliseClientError, snapToGrid, validatePluginContract };
1847
+ export { type AuditEventType, type AuditLogConfig, type AuditLogEntry, AuthenticationError, type BatchProgressEvent, type BatchProgressListener, BulkOperationError, type CSPConfig, type CacheStats, type CloudDeploymentConfig, type CloudHostingConfig, type CloudMarketplaceEntry, CloudOperations, ConcurrentUpdateError, ConnectionError, type ConnectionState, type ConnectionStateEvent, type ConnectionStateListener, type ContractValidationError, type ContractValidationResult, type DataMaskingConfig, type DataMaskingRule, type EmailIntegrationConfig, type IntegrationConfig, IntegrationManager, type IntegrationProvider, type IntegrationTrigger, type MetadataAuditEntry, type MetadataAuditResponse, MetadataClient, type MetadataClientConfig, type MetadataDeleteOptions, type MetadataDiagnostics, type MetadataDiagnosticsEntry, type MetadataDiagnosticsOptions, type MetadataDiagnosticsSummary, type MetadataDraftHeader, type MetadataError, type MetadataGetOptions, type MetadataHistoryOptions, type MetadataLayered, type MetadataListOptions, MetadataNotFoundError, type MetadataReference, type MetadataSaveOptions, type MetadataValidationIssue, ObjectStackAdapter, ObjectStackError, type ObjectStackUserStateAdapterOptions, type PluginAPIContract, type PluginContract, type PluginExport, SecurityManager, type SecurityPolicy, type SlackIntegrationConfig, type StudioCanvasConfig, type StudioColorPalette, type StudioPropertyEditor, type StudioShadowPreset, type StudioThemeBuilderConfig, type StudioTypographyPreset, type UserDataAdapter, ValidationError, type WebhookIntegrationConfig, calculateAutoLayout, clearSharedDiscoveryCache, createDefaultCanvasConfig, createErrorFromResponse, createObjectStackAdapter, createObjectStackUserStateAdapter, generateContractManifest, getSharedDiscovery, is404Error, isConcurrentUpdateError, isErrorType, isObjectStackError, normaliseClientError, snapToGrid, validatePluginContract };