@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/dist/index.d.ts 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 };
package/dist/index.js CHANGED
@@ -537,6 +537,7 @@ function createObjectStackUserStateAdapter(options) {
537
537
  }
538
538
  } = options;
539
539
  let cachedRowId = null;
540
+ let saveChain = Promise.resolve();
540
541
  const findExisting = async () => {
541
542
  const params = {
542
543
  $filter: {
@@ -567,6 +568,42 @@ function createObjectStackUserStateAdapter(options) {
567
568
  }
568
569
  return [];
569
570
  };
571
+ const upsert = async (items) => {
572
+ const now = (/* @__PURE__ */ new Date()).toISOString();
573
+ if (cachedRowId !== null) {
574
+ try {
575
+ await dataSource.update(resource, cachedRowId, { value: items, updated_at: now });
576
+ return;
577
+ } catch (updateError) {
578
+ cachedRowId = null;
579
+ onError("save", updateError);
580
+ }
581
+ }
582
+ const existing = await findExisting();
583
+ if (existing && existing.id !== void 0 && existing.id !== null) {
584
+ cachedRowId = existing.id;
585
+ await dataSource.update(resource, existing.id, { value: items, updated_at: now });
586
+ return;
587
+ }
588
+ try {
589
+ const created = await dataSource.create(resource, {
590
+ user_id: userId,
591
+ key,
592
+ value: items,
593
+ updated_at: now
594
+ });
595
+ const newId = created?.id;
596
+ if (newId !== void 0 && newId !== null) cachedRowId = newId;
597
+ } catch (createError) {
598
+ const recovered = await findExisting();
599
+ if (recovered && recovered.id !== void 0 && recovered.id !== null) {
600
+ cachedRowId = recovered.id;
601
+ await dataSource.update(resource, recovered.id, { value: items, updated_at: now });
602
+ return;
603
+ }
604
+ throw createError;
605
+ }
606
+ };
570
607
  return {
571
608
  async load() {
572
609
  try {
@@ -583,40 +620,11 @@ function createObjectStackUserStateAdapter(options) {
583
620
  }
584
621
  },
585
622
  async save(items) {
586
- try {
587
- const now = (/* @__PURE__ */ new Date()).toISOString();
588
- if (cachedRowId !== null) {
589
- try {
590
- await dataSource.update(resource, cachedRowId, {
591
- value: items,
592
- updated_at: now
593
- });
594
- return;
595
- } catch (updateError) {
596
- cachedRowId = null;
597
- onError("save", updateError);
598
- }
599
- }
600
- const existing = await findExisting();
601
- if (existing && existing.id !== void 0 && existing.id !== null) {
602
- cachedRowId = existing.id;
603
- await dataSource.update(resource, existing.id, {
604
- value: items,
605
- updated_at: now
606
- });
607
- return;
608
- }
609
- const created = await dataSource.create(resource, {
610
- user_id: userId,
611
- key,
612
- value: items,
613
- updated_at: now
614
- });
615
- const newId = created?.id;
616
- if (newId !== void 0 && newId !== null) cachedRowId = newId;
617
- } catch (error) {
623
+ const run = saveChain.then(() => upsert(items)).catch((error) => {
618
624
  onError("save", error);
619
- }
625
+ });
626
+ saveChain = run;
627
+ return run;
620
628
  }
621
629
  };
622
630
  }
@@ -752,13 +760,17 @@ async function parseError(res) {
752
760
  } catch {
753
761
  body = await res.text().catch(() => void 0);
754
762
  }
755
- const message = (body && typeof body === "object" && "error" in body ? String(body.error) : void 0) ?? `Metadata request failed: ${res.status} ${res.statusText}`;
756
- const err = new Error(message);
763
+ const bodyObj = body && typeof body === "object" ? body : void 0;
764
+ const errNode = bodyObj?.error;
765
+ const message = (errNode && typeof errNode === "object" ? errNode.message : errNode) ?? bodyObj?.message ?? `Metadata request failed: ${res.status} ${res.statusText}`;
766
+ const err = new Error(String(message));
757
767
  err.status = res.status;
758
768
  err.body = body;
759
- if (body && typeof body === "object" && "code" in body) {
760
- err.code = String(body.code);
761
- }
769
+ const details = errNode && typeof errNode === "object" ? errNode.details : void 0;
770
+ const issues = details?.issues ?? (errNode && typeof errNode === "object" ? errNode.issues : void 0) ?? bodyObj?.issues;
771
+ if (Array.isArray(issues)) err.issues = issues;
772
+ const code = details?.code ?? (errNode && typeof errNode === "object" ? errNode.code : void 0) ?? bodyObj?.code;
773
+ if (code != null) err.code = String(code);
762
774
  return err;
763
775
  }
764
776
  var MetadataClient = class _MetadataClient {
@@ -1468,6 +1480,11 @@ var ObjectStackAdapter = class {
1468
1480
  // so optional collections like sys_presence don't hammer the server with
1469
1481
  // failing requests on every record open / panel render.
1470
1482
  __publicField(this, "missingResources", /* @__PURE__ */ new Set());
1483
+ // Subscribers registered via onMutation(). Emitted after each successful
1484
+ // create/update/delete so data-bound views (ListView, ObjectView, kanban,
1485
+ // calendar) auto-refresh — the interface ListView relies on to reflect
1486
+ // inline-edit "Save All" writes without a manual reload.
1487
+ __publicField(this, "mutationListeners", /* @__PURE__ */ new Set());
1471
1488
  this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
1472
1489
  this.metadataCache = new MetadataCache(config.cache);
1473
1490
  this.autoReconnect = config.autoReconnect ?? true;
@@ -1676,9 +1693,35 @@ var ObjectStackAdapter = class {
1676
1693
  /**
1677
1694
  * Create a new record.
1678
1695
  */
1696
+ /**
1697
+ * Notify all mutation subscribers. A throwing listener must not break the
1698
+ * mutation or starve the other subscribers, so each is isolated.
1699
+ */
1700
+ emitMutation(event) {
1701
+ for (const listener of this.mutationListeners) {
1702
+ try {
1703
+ listener(event);
1704
+ } catch (err) {
1705
+ console.warn("ObjectStackAdapter: mutation listener error", err);
1706
+ }
1707
+ }
1708
+ }
1709
+ /**
1710
+ * Subscribe to create/update/delete events on any resource. Returns an
1711
+ * unsubscribe function. Data-bound views use this to auto-refresh after a
1712
+ * mutation (e.g. inline-edit "Save All", which writes through `update` and
1713
+ * must repaint the list without a manual reload).
1714
+ */
1715
+ onMutation(callback) {
1716
+ this.mutationListeners.add(callback);
1717
+ return () => {
1718
+ this.mutationListeners.delete(callback);
1719
+ };
1720
+ }
1679
1721
  async create(resource, data) {
1680
1722
  await this.connect();
1681
1723
  const result = await this.client.data.create(resource, data);
1724
+ this.emitMutation({ type: "create", resource, record: { ...result.record } });
1682
1725
  return result.record;
1683
1726
  }
1684
1727
  /**
@@ -1701,6 +1744,7 @@ var ObjectStackAdapter = class {
1701
1744
  data,
1702
1745
  opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
1703
1746
  );
1747
+ this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
1704
1748
  return result.record;
1705
1749
  } catch (err) {
1706
1750
  throw normaliseClientError(err);
@@ -1721,6 +1765,9 @@ var ObjectStackAdapter = class {
1721
1765
  String(id),
1722
1766
  opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
1723
1767
  );
1768
+ if (result.deleted) {
1769
+ this.emitMutation({ type: "delete", resource, id });
1770
+ }
1724
1771
  return result.deleted;
1725
1772
  } catch (err) {
1726
1773
  throw normaliseClientError(err);
@@ -1745,15 +1792,19 @@ var ObjectStackAdapter = class {
1745
1792
  await this.connect();
1746
1793
  if (!ids || ids.length === 0) return 0;
1747
1794
  const records = ids.map((id) => ({ id: String(id), data: patch }));
1795
+ const emitBulk = (count) => {
1796
+ if (count > 0) this.emitMutation({ type: "update", resource });
1797
+ return count;
1798
+ };
1748
1799
  const updateMany = this.client.data.updateMany;
1749
1800
  if (typeof updateMany === "function") {
1750
1801
  try {
1751
1802
  const res = await updateMany(resource, records, { continueOnError: true });
1752
1803
  if (res && typeof res === "object" && typeof res.succeeded === "number") {
1753
- return res.succeeded;
1804
+ return emitBulk(res.succeeded);
1754
1805
  }
1755
- if (Array.isArray(res)) return res.length;
1756
- return ids.length;
1806
+ if (Array.isArray(res)) return emitBulk(res.length);
1807
+ return emitBulk(ids.length);
1757
1808
  } catch (err) {
1758
1809
  throw normaliseClientError(err);
1759
1810
  }
@@ -1766,7 +1817,7 @@ var ObjectStackAdapter = class {
1766
1817
  } catch {
1767
1818
  }
1768
1819
  }
1769
- return succeeded;
1820
+ return emitBulk(succeeded);
1770
1821
  }
1771
1822
  /**
1772
1823
  * Single-call bulk delete. Mirrors the bulkUpdate contract: prefers
@@ -1779,15 +1830,19 @@ var ObjectStackAdapter = class {
1779
1830
  await this.connect();
1780
1831
  if (!ids || ids.length === 0) return 0;
1781
1832
  const strIds = ids.map((id) => String(id));
1833
+ const emitBulk = (count) => {
1834
+ if (count > 0) this.emitMutation({ type: "delete", resource });
1835
+ return count;
1836
+ };
1782
1837
  const deleteMany = this.client.data.deleteMany;
1783
1838
  if (typeof deleteMany === "function") {
1784
1839
  try {
1785
1840
  const res = await deleteMany(resource, strIds, { continueOnError: true });
1786
1841
  if (res && typeof res === "object" && typeof res.succeeded === "number") {
1787
- return res.succeeded;
1842
+ return emitBulk(res.succeeded);
1788
1843
  }
1789
- if (Array.isArray(res)) return res.length;
1790
- return strIds.length;
1844
+ if (Array.isArray(res)) return emitBulk(res.length);
1845
+ return emitBulk(strIds.length);
1791
1846
  } catch (err) {
1792
1847
  throw normaliseClientError(err);
1793
1848
  }
@@ -1800,7 +1855,7 @@ var ObjectStackAdapter = class {
1800
1855
  } catch {
1801
1856
  }
1802
1857
  }
1803
- return succeeded;
1858
+ return emitBulk(succeeded);
1804
1859
  }
1805
1860
  /**
1806
1861
  * Bulk operations with optimized batch processing and error handling.
@@ -1960,6 +2015,157 @@ var ObjectStackAdapter = class {
1960
2015
  );
1961
2016
  }
1962
2017
  }
2018
+ /**
2019
+ * Bulk-import raw spreadsheet rows in a single server round-trip via
2020
+ * `POST /api/v1/data/:object/import`. The server performs all value coercion
2021
+ * (booleans, numbers, dates→ISO, select label→code, lookup name→id) from the
2022
+ * object's field metadata, so this method forwards the request verbatim and
2023
+ * returns the aggregate + per-row result untouched.
2024
+ *
2025
+ * Requires `@objectstack/client` with `data.import` (server `/import` route).
2026
+ * Callers should feature-detect (`typeof dataSource.importRecords`) and fall
2027
+ * back to a per-row `create` loop when unavailable.
2028
+ */
2029
+ async importRecords(resource, request) {
2030
+ await this.connect();
2031
+ const importFn = this.client.data.import;
2032
+ if (typeof importFn !== "function") {
2033
+ throw new ObjectStackError(
2034
+ "The connected @objectstack/client does not support data.import(). Upgrade the client, or import via a per-row create fallback.",
2035
+ "UNSUPPORTED_OPERATION",
2036
+ 400
2037
+ );
2038
+ }
2039
+ try {
2040
+ const result = await importFn.call(this.client.data, resource, request);
2041
+ return result;
2042
+ } catch (err) {
2043
+ throw normaliseClientError(err);
2044
+ }
2045
+ }
2046
+ /**
2047
+ * Feature-detect the async import-job API on the connected client. Older
2048
+ * clients/servers lack these routes; callers fall back to {@link importRecords}.
2049
+ */
2050
+ importJobApi() {
2051
+ const d = this.client.data;
2052
+ if (typeof d.createImportJob !== "function") return void 0;
2053
+ return d;
2054
+ }
2055
+ /**
2056
+ * Start an asynchronous import job — the large-file counterpart to
2057
+ * {@link importRecords}. Posts the whole payload once; the server processes
2058
+ * rows in the background. Requires an `@objectstack/client` new enough to
2059
+ * expose `data.createImportJob` (server `/import/jobs` route). Callers should
2060
+ * feature-detect (`typeof dataSource.createImportJob`) and fall back to the
2061
+ * synchronous path when unavailable.
2062
+ */
2063
+ async createImportJob(resource, request) {
2064
+ await this.connect();
2065
+ const api = this.importJobApi();
2066
+ if (!api) {
2067
+ throw new ObjectStackError(
2068
+ "The connected @objectstack/client does not support async import jobs (data.createImportJob). Upgrade the client, or use the synchronous importRecords() path.",
2069
+ "UNSUPPORTED_OPERATION",
2070
+ 400
2071
+ );
2072
+ }
2073
+ try {
2074
+ return await api.createImportJob.call(this.client.data, resource, request);
2075
+ } catch (err) {
2076
+ throw normaliseClientError(err);
2077
+ }
2078
+ }
2079
+ /** Poll an import job's progress. Requires {@link createImportJob} support. */
2080
+ async getImportJobProgress(jobId) {
2081
+ await this.connect();
2082
+ const api = this.importJobApi();
2083
+ if (!api) {
2084
+ throw new ObjectStackError(
2085
+ "The connected @objectstack/client does not support async import jobs.",
2086
+ "UNSUPPORTED_OPERATION",
2087
+ 400
2088
+ );
2089
+ }
2090
+ try {
2091
+ return await api.getImportJobProgress.call(this.client.data, jobId);
2092
+ } catch (err) {
2093
+ throw normaliseClientError(err);
2094
+ }
2095
+ }
2096
+ /** Fetch an import job's capped per-row results. */
2097
+ async getImportJobResults(jobId) {
2098
+ await this.connect();
2099
+ const api = this.importJobApi();
2100
+ if (!api) {
2101
+ throw new ObjectStackError(
2102
+ "The connected @objectstack/client does not support async import jobs.",
2103
+ "UNSUPPORTED_OPERATION",
2104
+ 400
2105
+ );
2106
+ }
2107
+ try {
2108
+ return await api.getImportJobResults.call(this.client.data, jobId);
2109
+ } catch (err) {
2110
+ throw normaliseClientError(err);
2111
+ }
2112
+ }
2113
+ /** List recent import jobs (history), newest first. */
2114
+ async listImportJobs(options = {}) {
2115
+ await this.connect();
2116
+ const api = this.importJobApi();
2117
+ if (!api) {
2118
+ throw new ObjectStackError(
2119
+ "The connected @objectstack/client does not support async import jobs.",
2120
+ "UNSUPPORTED_OPERATION",
2121
+ 400
2122
+ );
2123
+ }
2124
+ try {
2125
+ return await api.listImportJobs.call(this.client.data, options);
2126
+ } catch (err) {
2127
+ throw normaliseClientError(err);
2128
+ }
2129
+ }
2130
+ /** Cancel a pending/running import job (cooperative). */
2131
+ async cancelImportJob(jobId) {
2132
+ await this.connect();
2133
+ const api = this.importJobApi();
2134
+ if (!api) {
2135
+ throw new ObjectStackError(
2136
+ "The connected @objectstack/client does not support async import jobs.",
2137
+ "UNSUPPORTED_OPERATION",
2138
+ 400
2139
+ );
2140
+ }
2141
+ try {
2142
+ await api.cancelImportJob.call(this.client.data, jobId);
2143
+ } catch (err) {
2144
+ throw normaliseClientError(err);
2145
+ }
2146
+ }
2147
+ /**
2148
+ * Logically roll back a finished import job — delete the records it created
2149
+ * and restore the records it updated to their pre-import values. Requires an
2150
+ * `@objectstack/client` new enough to expose `data.undoImportJob`, and a job
2151
+ * the server captured an undo log for (see {@link ImportJobProgressInfo.undoable}).
2152
+ */
2153
+ async undoImportJob(jobId) {
2154
+ await this.connect();
2155
+ const api = this.importJobApi();
2156
+ if (!api || typeof api.undoImportJob !== "function") {
2157
+ throw new ObjectStackError(
2158
+ "The connected @objectstack/client does not support undoing import jobs (data.undoImportJob).",
2159
+ "UNSUPPORTED_OPERATION",
2160
+ 400
2161
+ );
2162
+ }
2163
+ try {
2164
+ return await api.undoImportJob.call(this.client.data, jobId);
2165
+ } catch (err) {
2166
+ throw normaliseClientError(err);
2167
+ }
2168
+ }
1963
2169
  /**
1964
2170
  * Normalize the result from data.find() or data.query() into a consistent QueryResult.
1965
2171
  */
@@ -2057,6 +2263,53 @@ var ObjectStackAdapter = class {
2057
2263
  }
2058
2264
  return body;
2059
2265
  }
2266
+ /**
2267
+ * Synchronously download a server-streamed export (csv / json / xlsx).
2268
+ *
2269
+ * Hits `GET /api/v1/data/:object/export`, which streams matching rows in the
2270
+ * requested format, formats values for readability (lookup → name, select →
2271
+ * label, boolean → 是/否, dates formatted) and enforces permissions. The
2272
+ * filter / sort are translated the same way as `rawFindWithPopulate` so the
2273
+ * exported file mirrors the active list view. Returns the file as a Blob;
2274
+ * the caller triggers the browser download.
2275
+ */
2276
+ async exportDownload(resource, request = {}) {
2277
+ const queryParams = new URLSearchParams();
2278
+ const format = request.format === "xlsx" ? "xlsx" : request.format === "json" ? "json" : "csv";
2279
+ queryParams.set("format", format);
2280
+ if (request.fields && request.fields.length > 0) {
2281
+ queryParams.set("fields", request.fields.join(","));
2282
+ }
2283
+ if (request.limit && request.limit > 0) {
2284
+ queryParams.set("limit", String(request.limit));
2285
+ }
2286
+ if (request.includeHeaders === false) {
2287
+ queryParams.set("header", "false");
2288
+ }
2289
+ if (request.sort && request.sort.length > 0) {
2290
+ const orderby = request.sort.filter((s) => s && s.field).map((s) => `${s.field}:${s.direction === "desc" ? "desc" : "asc"}`).join(",");
2291
+ if (orderby) queryParams.set("orderby", orderby);
2292
+ }
2293
+ if (request.filter !== void 0 && request.filter !== null) {
2294
+ const translated = translateFilterToAST(request.filter);
2295
+ if (translated !== void 0) {
2296
+ queryParams.set("filter", JSON.stringify(translated));
2297
+ }
2298
+ }
2299
+ const baseUrl = this.baseUrl.replace(/\/$/, "");
2300
+ const hasApiVersionSuffix = /\/api\/v\d+$/i.test(baseUrl);
2301
+ const dataPath = hasApiVersionSuffix ? "/data" : "/api/v1/data";
2302
+ const url = `${baseUrl}${dataPath}/${encodeURIComponent(resource)}/export?${queryParams.toString()}`;
2303
+ const headers = { ...this.getAuthHeaders() };
2304
+ const res = await this.fetchImpl(url, { method: "GET", headers, credentials: "include" });
2305
+ if (!res.ok) {
2306
+ const errorBody = await res.json().catch(() => ({ message: res.statusText }));
2307
+ const err = new Error(errorBody?.error?.message || errorBody?.message || res.statusText);
2308
+ err.status = res.status;
2309
+ throw err;
2310
+ }
2311
+ return await res.blob();
2312
+ }
2060
2313
  /**
2061
2314
  * Convert ObjectUI QueryParams to ObjectStack QueryOptions.
2062
2315
  * Maps OData-style conventions to ObjectStack conventions.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/data-objectstack",
3
- "version": "11.2.0",
3
+ "version": "11.4.0",
4
4
  "description": "ObjectStack Data Adapter for Object UI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -22,9 +22,9 @@
22
22
  "LICENSE"
23
23
  ],
24
24
  "dependencies": {
25
- "@objectstack/client": "^11.0.0",
26
- "@object-ui/core": "11.2.0",
27
- "@object-ui/types": "11.2.0"
25
+ "@objectstack/client": "^11.5.0",
26
+ "@object-ui/core": "11.4.0",
27
+ "@object-ui/types": "11.4.0"
28
28
  },
29
29
  "devDependencies": {
30
30
  "tsup": "^8.5.1",