@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 +46 -0
- package/dist/index.cjs +299 -46
- package/dist/index.d.cts +76 -2
- package/dist/index.d.ts +76 -2
- package/dist/index.js +299 -46
- package/package.json +4 -4
- package/src/exportDownload.test.ts +93 -0
- package/src/getObjectSchema.test.ts +36 -0
- package/src/importJob.test.ts +71 -0
- package/src/index.ts +312 -9
- package/src/metadata-client.test.ts +30 -0
- package/src/metadata-client.ts +37 -7
- package/src/onMutation.test.ts +125 -0
- package/src/userState.test.ts +53 -0
- package/src/userState.ts +64 -37
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
|
-
|
|
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
|
|
804
|
-
const
|
|
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
|
-
|
|
808
|
-
|
|
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 };
|