@object-ui/data-objectstack 11.3.0 → 11.5.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 +51 -0
- package/dist/index.cjs +230 -13
- package/dist/index.d.cts +74 -2
- package/dist/index.d.ts +74 -2
- package/dist/index.js +230 -13
- package/package.json +4 -4
- package/src/importJob.test.ts +71 -0
- package/src/index.ts +274 -9
- package/src/metadata-client.test.ts +30 -0
- package/src/metadata-client.ts +37 -7
- package/src/onMutation.test.ts +125 -0
package/dist/index.js
CHANGED
|
@@ -760,13 +760,17 @@ async function parseError(res) {
|
|
|
760
760
|
} catch {
|
|
761
761
|
body = await res.text().catch(() => void 0);
|
|
762
762
|
}
|
|
763
|
-
const
|
|
764
|
-
const
|
|
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));
|
|
765
767
|
err.status = res.status;
|
|
766
768
|
err.body = body;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
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);
|
|
770
774
|
return err;
|
|
771
775
|
}
|
|
772
776
|
var MetadataClient = class _MetadataClient {
|
|
@@ -1476,6 +1480,11 @@ var ObjectStackAdapter = class {
|
|
|
1476
1480
|
// so optional collections like sys_presence don't hammer the server with
|
|
1477
1481
|
// failing requests on every record open / panel render.
|
|
1478
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());
|
|
1479
1488
|
this.client = new ObjectStackClient({ ...config, logger: createQuietHttpLogger() });
|
|
1480
1489
|
this.metadataCache = new MetadataCache(config.cache);
|
|
1481
1490
|
this.autoReconnect = config.autoReconnect ?? true;
|
|
@@ -1684,9 +1693,35 @@ var ObjectStackAdapter = class {
|
|
|
1684
1693
|
/**
|
|
1685
1694
|
* Create a new record.
|
|
1686
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
|
+
}
|
|
1687
1721
|
async create(resource, data) {
|
|
1688
1722
|
await this.connect();
|
|
1689
1723
|
const result = await this.client.data.create(resource, data);
|
|
1724
|
+
this.emitMutation({ type: "create", resource, record: { ...result.record } });
|
|
1690
1725
|
return result.record;
|
|
1691
1726
|
}
|
|
1692
1727
|
/**
|
|
@@ -1709,6 +1744,7 @@ var ObjectStackAdapter = class {
|
|
|
1709
1744
|
data,
|
|
1710
1745
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
1711
1746
|
);
|
|
1747
|
+
this.emitMutation({ type: "update", resource, id, record: { ...result.record } });
|
|
1712
1748
|
return result.record;
|
|
1713
1749
|
} catch (err) {
|
|
1714
1750
|
throw normaliseClientError(err);
|
|
@@ -1729,6 +1765,9 @@ var ObjectStackAdapter = class {
|
|
|
1729
1765
|
String(id),
|
|
1730
1766
|
opts?.ifMatch ? { ifMatch: opts.ifMatch } : void 0
|
|
1731
1767
|
);
|
|
1768
|
+
if (result.deleted) {
|
|
1769
|
+
this.emitMutation({ type: "delete", resource, id });
|
|
1770
|
+
}
|
|
1732
1771
|
return result.deleted;
|
|
1733
1772
|
} catch (err) {
|
|
1734
1773
|
throw normaliseClientError(err);
|
|
@@ -1753,15 +1792,19 @@ var ObjectStackAdapter = class {
|
|
|
1753
1792
|
await this.connect();
|
|
1754
1793
|
if (!ids || ids.length === 0) return 0;
|
|
1755
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
|
+
};
|
|
1756
1799
|
const updateMany = this.client.data.updateMany;
|
|
1757
1800
|
if (typeof updateMany === "function") {
|
|
1758
1801
|
try {
|
|
1759
1802
|
const res = await updateMany(resource, records, { continueOnError: true });
|
|
1760
1803
|
if (res && typeof res === "object" && typeof res.succeeded === "number") {
|
|
1761
|
-
return res.succeeded;
|
|
1804
|
+
return emitBulk(res.succeeded);
|
|
1762
1805
|
}
|
|
1763
|
-
if (Array.isArray(res)) return res.length;
|
|
1764
|
-
return ids.length;
|
|
1806
|
+
if (Array.isArray(res)) return emitBulk(res.length);
|
|
1807
|
+
return emitBulk(ids.length);
|
|
1765
1808
|
} catch (err) {
|
|
1766
1809
|
throw normaliseClientError(err);
|
|
1767
1810
|
}
|
|
@@ -1774,7 +1817,7 @@ var ObjectStackAdapter = class {
|
|
|
1774
1817
|
} catch {
|
|
1775
1818
|
}
|
|
1776
1819
|
}
|
|
1777
|
-
return succeeded;
|
|
1820
|
+
return emitBulk(succeeded);
|
|
1778
1821
|
}
|
|
1779
1822
|
/**
|
|
1780
1823
|
* Single-call bulk delete. Mirrors the bulkUpdate contract: prefers
|
|
@@ -1787,15 +1830,19 @@ var ObjectStackAdapter = class {
|
|
|
1787
1830
|
await this.connect();
|
|
1788
1831
|
if (!ids || ids.length === 0) return 0;
|
|
1789
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
|
+
};
|
|
1790
1837
|
const deleteMany = this.client.data.deleteMany;
|
|
1791
1838
|
if (typeof deleteMany === "function") {
|
|
1792
1839
|
try {
|
|
1793
1840
|
const res = await deleteMany(resource, strIds, { continueOnError: true });
|
|
1794
1841
|
if (res && typeof res === "object" && typeof res.succeeded === "number") {
|
|
1795
|
-
return res.succeeded;
|
|
1842
|
+
return emitBulk(res.succeeded);
|
|
1796
1843
|
}
|
|
1797
|
-
if (Array.isArray(res)) return res.length;
|
|
1798
|
-
return strIds.length;
|
|
1844
|
+
if (Array.isArray(res)) return emitBulk(res.length);
|
|
1845
|
+
return emitBulk(strIds.length);
|
|
1799
1846
|
} catch (err) {
|
|
1800
1847
|
throw normaliseClientError(err);
|
|
1801
1848
|
}
|
|
@@ -1808,7 +1855,7 @@ var ObjectStackAdapter = class {
|
|
|
1808
1855
|
} catch {
|
|
1809
1856
|
}
|
|
1810
1857
|
}
|
|
1811
|
-
return succeeded;
|
|
1858
|
+
return emitBulk(succeeded);
|
|
1812
1859
|
}
|
|
1813
1860
|
/**
|
|
1814
1861
|
* Bulk operations with optimized batch processing and error handling.
|
|
@@ -1968,6 +2015,157 @@ var ObjectStackAdapter = class {
|
|
|
1968
2015
|
);
|
|
1969
2016
|
}
|
|
1970
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
|
+
}
|
|
1971
2169
|
/**
|
|
1972
2170
|
* Normalize the result from data.find() or data.query() into a consistent QueryResult.
|
|
1973
2171
|
*/
|
|
@@ -2399,6 +2597,25 @@ var ObjectStackAdapter = class {
|
|
|
2399
2597
|
return [];
|
|
2400
2598
|
}
|
|
2401
2599
|
}
|
|
2600
|
+
/**
|
|
2601
|
+
* List registered import `mapping` artifacts targeting a given object
|
|
2602
|
+
* (framework #2611). Reads the `mapping` metadata kind via the overlay API
|
|
2603
|
+
* and filters by `targetObject` client-side (the metadata index is
|
|
2604
|
+
* name-only). Feeds the import wizard's "saved mapping" selector; a failure
|
|
2605
|
+
* (older server without the `mapping` kind) degrades to an empty list, so
|
|
2606
|
+
* the selector simply doesn't appear.
|
|
2607
|
+
*/
|
|
2608
|
+
async listImportMappings(objectName) {
|
|
2609
|
+
await this.connect();
|
|
2610
|
+
try {
|
|
2611
|
+
const result = await this.client.meta.getItems("mapping");
|
|
2612
|
+
const items = Array.isArray(result?.items) ? result.items : Array.isArray(result) ? result : [];
|
|
2613
|
+
return items.filter((m) => m && m.targetObject === objectName);
|
|
2614
|
+
} catch (err) {
|
|
2615
|
+
console.warn("[OBJECTSTACKDataSource] listImportMappings failed:", err);
|
|
2616
|
+
return [];
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2402
2619
|
/**
|
|
2403
2620
|
* Create a new overlay view for an object. The view's `name` is the
|
|
2404
2621
|
* stable identifier — must be unique within the project scope. Returns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/data-objectstack",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.5.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.
|
|
26
|
-
"@object-ui/core": "11.
|
|
27
|
-
"@object-ui/types": "11.
|
|
25
|
+
"@objectstack/client": "^11.5.0",
|
|
26
|
+
"@object-ui/core": "11.5.0",
|
|
27
|
+
"@object-ui/types": "11.5.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"tsup": "^8.5.1",
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ObjectUI
|
|
3
|
+
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
+
*
|
|
5
|
+
* This source code is licensed under the MIT license found in the
|
|
6
|
+
* LICENSE file in the root directory of this source tree.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { ObjectStackAdapter } from './index';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Adapter over a mock client `data` namespace. The async import-job methods are
|
|
14
|
+
* thin pass-throughs to the client SDK, so we assert delegation + argument
|
|
15
|
+
* shaping, plus graceful degradation when the client lacks the job API.
|
|
16
|
+
*/
|
|
17
|
+
function makeDS(stub: Record<string, any>) {
|
|
18
|
+
const ds: any = new ObjectStackAdapter({
|
|
19
|
+
baseUrl: 'http://test.local',
|
|
20
|
+
fetch: vi.fn(async () => new Response(JSON.stringify({ success: true, data: { capabilities: {}, routes: {} } }), {
|
|
21
|
+
status: 200,
|
|
22
|
+
headers: { 'Content-Type': 'application/json' },
|
|
23
|
+
})),
|
|
24
|
+
});
|
|
25
|
+
ds.connected = true;
|
|
26
|
+
ds.connectionState = 'connected';
|
|
27
|
+
ds.client = { data: stub };
|
|
28
|
+
return ds;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('ObjectStackAdapter async import jobs', () => {
|
|
32
|
+
it('createImportJob delegates to client.data.createImportJob', async () => {
|
|
33
|
+
const createImportJob = vi.fn().mockResolvedValue({ jobId: 'imp_1', object: 'task', status: 'pending', total: 2 });
|
|
34
|
+
const ds = makeDS({ createImportJob });
|
|
35
|
+
const req = { format: 'json' as const, rows: [{ a: 1 }, { a: 2 }], writeMode: 'upsert' as const, matchFields: ['a'] };
|
|
36
|
+
const res = await ds.createImportJob('task', req);
|
|
37
|
+
expect(createImportJob).toHaveBeenCalledTimes(1);
|
|
38
|
+
expect(createImportJob.mock.calls[0]).toEqual(['task', req]);
|
|
39
|
+
expect(res).toMatchObject({ jobId: 'imp_1', status: 'pending', total: 2 });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('getImportJobProgress / getImportJobResults / cancelImportJob delegate by jobId', async () => {
|
|
43
|
+
const getImportJobProgress = vi.fn().mockResolvedValue({ jobId: 'imp_1', status: 'running', percentComplete: 50 });
|
|
44
|
+
const getImportJobResults = vi.fn().mockResolvedValue({ jobId: 'imp_1', status: 'succeeded', results: [], resultsTruncated: false });
|
|
45
|
+
const cancelImportJob = vi.fn().mockResolvedValue({ success: true });
|
|
46
|
+
const ds = makeDS({ createImportJob: vi.fn(), getImportJobProgress, getImportJobResults, cancelImportJob });
|
|
47
|
+
|
|
48
|
+
expect((await ds.getImportJobProgress('imp_1')).percentComplete).toBe(50);
|
|
49
|
+
expect(getImportJobProgress).toHaveBeenCalledWith('imp_1');
|
|
50
|
+
|
|
51
|
+
expect((await ds.getImportJobResults('imp_1')).resultsTruncated).toBe(false);
|
|
52
|
+
expect(getImportJobResults).toHaveBeenCalledWith('imp_1');
|
|
53
|
+
|
|
54
|
+
await ds.cancelImportJob('imp_1');
|
|
55
|
+
expect(cancelImportJob).toHaveBeenCalledWith('imp_1');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('listImportJobs forwards filters and returns the jobs array', async () => {
|
|
59
|
+
const listImportJobs = vi.fn().mockResolvedValue([{ jobId: 'imp_1', object: 'task', status: 'succeeded' }]);
|
|
60
|
+
const ds = makeDS({ createImportJob: vi.fn(), listImportJobs });
|
|
61
|
+
const jobs = await ds.listImportJobs({ object: 'task', status: 'succeeded', limit: 10 });
|
|
62
|
+
expect(listImportJobs).toHaveBeenCalledWith({ object: 'task', status: 'succeeded', limit: 10 });
|
|
63
|
+
expect(jobs).toHaveLength(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('throws UNSUPPORTED_OPERATION when the client lacks the job API', async () => {
|
|
67
|
+
const ds = makeDS({ import: vi.fn() }); // sync import only, no createImportJob
|
|
68
|
+
await expect(ds.createImportJob('task', { format: 'json', rows: [] })).rejects.toMatchObject({ code: 'UNSUPPORTED_OPERATION' });
|
|
69
|
+
await expect(ds.getImportJobProgress('imp_1')).rejects.toMatchObject({ code: 'UNSUPPORTED_OPERATION' });
|
|
70
|
+
});
|
|
71
|
+
});
|