@geniee-inc/sfa-kit 1.10.0 → 1.12.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/GUIDE.md +161 -1
- package/package.json +1 -1
- package/src/index.d.ts +8 -1
- package/src/index.js +4 -0
- package/src/lib/auth/SfaLoginGate.js +2 -2
- package/src/lib/auth/StandalonePreviewContext.d.ts +1 -0
- package/src/lib/auth/embed-protocol.d.ts +1 -0
- package/src/lib/auth/embed-protocol.js +2 -1
- package/src/lib/hooks/splitBulkRows.d.ts +30 -0
- package/src/lib/hooks/splitBulkRows.js +42 -0
- package/src/lib/hooks/useBulkCreateRecords.d.ts +56 -0
- package/src/lib/hooks/useBulkCreateRecords.js +41 -0
- package/src/lib/hooks/useBulkDeleteRecords.d.ts +50 -0
- package/src/lib/hooks/useBulkDeleteRecords.js +36 -0
- package/src/lib/hooks/useBulkUpdateRecords.d.ts +55 -0
- package/src/lib/hooks/useBulkUpdateRecords.js +37 -0
- package/src/lib/hooks/useBulkWrite.d.ts +55 -0
- package/src/lib/hooks/useBulkWrite.js +160 -0
- package/src/lib/hooks/useRecordList.js +12 -6
- package/src/lib/provider/SfaClient.d.ts +69 -3
- package/src/lib/provider/SfaClient.js +377 -29
- package/src/lib/provider/SfaProvider.js +24 -17
- package/src/lib/provider/types.d.ts +86 -2
- package/src/lib/version.d.ts +1 -1
- package/src/lib/version.js +1 -1
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
import { splitBulkRows } from './splitBulkRows';
|
|
3
|
+
import { useBulkWrite } from './useBulkWrite';
|
|
4
|
+
/** Module level so its identity never changes: `run` stays stable. */
|
|
5
|
+
const sendBulkUpdate = (client, collection, rows) => client.bulkUpdate(collection, rows);
|
|
6
|
+
/**
|
|
7
|
+
* @description Imperative hook for updating many records of one object in a
|
|
8
|
+
* single call — the save action of a grid edit.
|
|
9
|
+
*
|
|
10
|
+
* Best-effort per row: the API answers 200 and reports each row's outcome,
|
|
11
|
+
* with no all-or-nothing mode. A stale row is reported as `conflict` and left
|
|
12
|
+
* unchanged while the rest of the batch applies, so per-row outcomes come back
|
|
13
|
+
* as data rather than thrown errors.
|
|
14
|
+
*
|
|
15
|
+
* Retry only the rows in `failed`. A conflict is retried with the version from
|
|
16
|
+
* its `record`, never by re-sending the stale one. Rows in `unresolved` are
|
|
17
|
+
* re-read, not retried.
|
|
18
|
+
*/
|
|
19
|
+
export function useBulkUpdateRecords(collection) {
|
|
20
|
+
const { run, results, isLoading, error, reset } = useBulkWrite(collection, sendBulkUpdate);
|
|
21
|
+
/**
|
|
22
|
+
* NOTE: memoised because a grid passes these into effect and callback
|
|
23
|
+
* dependency lists; fresh arrays every render would re-run that work.
|
|
24
|
+
*/
|
|
25
|
+
const split = useMemo(() => splitBulkRows(results, 'updated', row => row.record), [results]);
|
|
26
|
+
return {
|
|
27
|
+
bulkUpdate: run,
|
|
28
|
+
results,
|
|
29
|
+
updated: split.landed,
|
|
30
|
+
conflicts: split.conflicts,
|
|
31
|
+
failed: split.failed,
|
|
32
|
+
unresolved: split.unresolved,
|
|
33
|
+
isLoading,
|
|
34
|
+
error,
|
|
35
|
+
reset,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { SfaBulkRowResult, SfaClientInterface } from '../provider/types';
|
|
2
|
+
export interface BulkWriteState<TRow> {
|
|
3
|
+
readonly run: (rows: readonly TRow[]) => Promise<readonly SfaBulkRowResult[]>;
|
|
4
|
+
readonly results: readonly SfaBulkRowResult[];
|
|
5
|
+
readonly isLoading: boolean;
|
|
6
|
+
readonly error: Error | null;
|
|
7
|
+
readonly reset: () => void;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* @description The state the three bulk write hooks share: one call in flight,
|
|
11
|
+
* its per-row results, and whether the call as a whole was rejected.
|
|
12
|
+
*
|
|
13
|
+
* `send` takes the client and collection as arguments so it can be defined
|
|
14
|
+
* once per hook at module level. Its identity never changes, which is what
|
|
15
|
+
* keeps `run` stable across renders.
|
|
16
|
+
*
|
|
17
|
+
* A hook instance outlives its arguments: a grid switches object, and the
|
|
18
|
+
* provider builds a new client on a token refresh or an org switch. Outcomes
|
|
19
|
+
* must never be reported as the current object's, and this is settled in two
|
|
20
|
+
* places that answer two different questions.
|
|
21
|
+
*
|
|
22
|
+
* NOTE: what to show is settled during render, by clearing the state as the
|
|
23
|
+
* arguments change. Clearing in an effect instead would leave one render
|
|
24
|
+
* showing the previous object's rows. React discards a render it does not
|
|
25
|
+
* commit — an interrupted transition — along with the state set during it, so
|
|
26
|
+
* a screen that never appears cannot blank the one the user is looking at.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: whether an answer may still be published is settled against the
|
|
29
|
+
* arguments as last **committed**, tracked in a ref written from a commit
|
|
30
|
+
* effect. Effects run only for renders that commit, so a transition React
|
|
31
|
+
* starts and abandons cannot orphan the request the visible screen is waiting
|
|
32
|
+
* for. A ref written during render would: the write survives the abandoned
|
|
33
|
+
* render, and the committed screen's own answer would be taken for a stale
|
|
34
|
+
* one.
|
|
35
|
+
*
|
|
36
|
+
* NOTE: the number itself is assigned during render and kept in state, so a
|
|
37
|
+
* callback knows which arguments it was made for without reading the ref, and
|
|
38
|
+
* the ref is recorded in the insertion phase — before any child's layout
|
|
39
|
+
* effect can call back into this hook. A callback from an earlier render is
|
|
40
|
+
* therefore recognised as stale even inside the commit that superseded it,
|
|
41
|
+
* and the current one is never mistaken for stale.
|
|
42
|
+
*
|
|
43
|
+
* The number advances on every committed change, so returning to an object
|
|
44
|
+
* does not adopt the earlier visit's request: a batch started before the
|
|
45
|
+
* reader left carries a number the ref has moved past.
|
|
46
|
+
*
|
|
47
|
+
* NOTE: read-only is read from that same committed ref, not from the
|
|
48
|
+
* callback's own value. The ref is current from the insertion phase onwards,
|
|
49
|
+
* so it answers for the commit that turns the setting on as well as for a
|
|
50
|
+
* `run` the caller kept from before it — a callback's own copy would be
|
|
51
|
+
* whatever its render saw. Read-only throws before anything is sent, the same
|
|
52
|
+
* as the single-record write hooks: it is a provider setting, not an outcome
|
|
53
|
+
* of the call, so it never reaches `error`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function useBulkWrite<TRow>(collection: string, send: (client: SfaClientInterface, collection: string, rows: readonly TRow[]) => Promise<readonly SfaBulkRowResult[]>): BulkWriteState<TRow>;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { useCallback, useEffect, useInsertionEffect, useRef, useState, } from 'react';
|
|
3
|
+
import { ERR_READ_ONLY } from '../errors';
|
|
4
|
+
import { useSfaContext } from '../provider/useSfaClient';
|
|
5
|
+
/** Stable identity so an unfinished batch never re-renders on a fresh array. */
|
|
6
|
+
const NO_RESULTS = [];
|
|
7
|
+
/**
|
|
8
|
+
* Runs in the insertion phase of the commit, which React finishes for the
|
|
9
|
+
* whole tree before any layout effect runs. A child's layout effect can
|
|
10
|
+
* therefore call this hook during the very commit that changed its arguments
|
|
11
|
+
* and still see them recorded — the ordering is child-insertion,
|
|
12
|
+
* parent-insertion, child-layout, parent-layout. A layout effect here would
|
|
13
|
+
* be too late, since the child's runs first.
|
|
14
|
+
*
|
|
15
|
+
* Insertion effects run only for renders that commit, so an abandoned
|
|
16
|
+
* transition still leaves nothing behind. There is no commit to observe on
|
|
17
|
+
* the server, where it would only warn, so the plain effect stands in.
|
|
18
|
+
*/
|
|
19
|
+
const useCommitEffect = typeof window === 'undefined' ? useEffect : useInsertionEffect;
|
|
20
|
+
const NOTHING_WRITTEN = {
|
|
21
|
+
results: NO_RESULTS,
|
|
22
|
+
isLoading: false,
|
|
23
|
+
error: null,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* @description The state the three bulk write hooks share: one call in flight,
|
|
27
|
+
* its per-row results, and whether the call as a whole was rejected.
|
|
28
|
+
*
|
|
29
|
+
* `send` takes the client and collection as arguments so it can be defined
|
|
30
|
+
* once per hook at module level. Its identity never changes, which is what
|
|
31
|
+
* keeps `run` stable across renders.
|
|
32
|
+
*
|
|
33
|
+
* A hook instance outlives its arguments: a grid switches object, and the
|
|
34
|
+
* provider builds a new client on a token refresh or an org switch. Outcomes
|
|
35
|
+
* must never be reported as the current object's, and this is settled in two
|
|
36
|
+
* places that answer two different questions.
|
|
37
|
+
*
|
|
38
|
+
* NOTE: what to show is settled during render, by clearing the state as the
|
|
39
|
+
* arguments change. Clearing in an effect instead would leave one render
|
|
40
|
+
* showing the previous object's rows. React discards a render it does not
|
|
41
|
+
* commit — an interrupted transition — along with the state set during it, so
|
|
42
|
+
* a screen that never appears cannot blank the one the user is looking at.
|
|
43
|
+
*
|
|
44
|
+
* NOTE: whether an answer may still be published is settled against the
|
|
45
|
+
* arguments as last **committed**, tracked in a ref written from a commit
|
|
46
|
+
* effect. Effects run only for renders that commit, so a transition React
|
|
47
|
+
* starts and abandons cannot orphan the request the visible screen is waiting
|
|
48
|
+
* for. A ref written during render would: the write survives the abandoned
|
|
49
|
+
* render, and the committed screen's own answer would be taken for a stale
|
|
50
|
+
* one.
|
|
51
|
+
*
|
|
52
|
+
* NOTE: the number itself is assigned during render and kept in state, so a
|
|
53
|
+
* callback knows which arguments it was made for without reading the ref, and
|
|
54
|
+
* the ref is recorded in the insertion phase — before any child's layout
|
|
55
|
+
* effect can call back into this hook. A callback from an earlier render is
|
|
56
|
+
* therefore recognised as stale even inside the commit that superseded it,
|
|
57
|
+
* and the current one is never mistaken for stale.
|
|
58
|
+
*
|
|
59
|
+
* The number advances on every committed change, so returning to an object
|
|
60
|
+
* does not adopt the earlier visit's request: a batch started before the
|
|
61
|
+
* reader left carries a number the ref has moved past.
|
|
62
|
+
*
|
|
63
|
+
* NOTE: read-only is read from that same committed ref, not from the
|
|
64
|
+
* callback's own value. The ref is current from the insertion phase onwards,
|
|
65
|
+
* so it answers for the commit that turns the setting on as well as for a
|
|
66
|
+
* `run` the caller kept from before it — a callback's own copy would be
|
|
67
|
+
* whatever its render saw. Read-only throws before anything is sent, the same
|
|
68
|
+
* as the single-record write hooks: it is a provider setting, not an outcome
|
|
69
|
+
* of the call, so it never reaches `error`.
|
|
70
|
+
*/
|
|
71
|
+
export function useBulkWrite(collection, send) {
|
|
72
|
+
const { client, readOnly } = useSfaContext();
|
|
73
|
+
const [state, setState] = useState(NOTHING_WRITTEN);
|
|
74
|
+
const [numbered, setNumbered] = useState({
|
|
75
|
+
client,
|
|
76
|
+
collection,
|
|
77
|
+
sequence: 0,
|
|
78
|
+
});
|
|
79
|
+
const callId = useRef(0);
|
|
80
|
+
const committed = useRef({
|
|
81
|
+
readOnly,
|
|
82
|
+
sequence: numbered.sequence,
|
|
83
|
+
});
|
|
84
|
+
// NOTE: the state adjustment React documents for props changes — set during
|
|
85
|
+
// render, so this render already reports nothing, and thrown away with the
|
|
86
|
+
// render if it never commits
|
|
87
|
+
if (numbered.client !== client || numbered.collection !== collection) {
|
|
88
|
+
setNumbered({ client, collection, sequence: numbered.sequence + 1 });
|
|
89
|
+
setState(NOTHING_WRITTEN);
|
|
90
|
+
}
|
|
91
|
+
const sequence = numbered.sequence;
|
|
92
|
+
useCommitEffect(() => {
|
|
93
|
+
committed.current = { readOnly, sequence };
|
|
94
|
+
}, [readOnly, sequence]);
|
|
95
|
+
const run = useCallback((rows) => __awaiter(this, void 0, void 0, function* () {
|
|
96
|
+
// NOTE: both refusals come before anything is claimed. Bumping the call
|
|
97
|
+
// id first would let a refusal disown a batch that is still running, and
|
|
98
|
+
// its answer — the only record of the ids that landed — would be dropped
|
|
99
|
+
if (committed.current.readOnly) {
|
|
100
|
+
throw new Error(ERR_READ_ONLY);
|
|
101
|
+
}
|
|
102
|
+
// NOTE: this callback sends to the client and collection it was made
|
|
103
|
+
// with. Once a later set of arguments has been committed it would write
|
|
104
|
+
// to an object the reader has left, so it refuses. Sending to wherever
|
|
105
|
+
// the hook now points would be worse: the rows were meant for the object
|
|
106
|
+
// the caller asked for.
|
|
107
|
+
//
|
|
108
|
+
// NOTE: strictly later, not merely different. Only a callback the
|
|
109
|
+
// commit record has moved past is stale; one carrying a number the
|
|
110
|
+
// record has not reached could only come from a render that never
|
|
111
|
+
// committed, and no such callback can be invoked.
|
|
112
|
+
if (committed.current.sequence > sequence) {
|
|
113
|
+
throw new Error(`This bulk write was prepared for ${collection} under arguments ` +
|
|
114
|
+
'the hook has moved past — a different object, or a client the ' +
|
|
115
|
+
'provider has replaced. Use the callback it returns now.');
|
|
116
|
+
}
|
|
117
|
+
callId.current += 1;
|
|
118
|
+
const currentCallId = callId.current;
|
|
119
|
+
const stillOwnsTheState = () => currentCallId === callId.current &&
|
|
120
|
+
committed.current.sequence === sequence;
|
|
121
|
+
// NOTE: the previous results stay visible while the next batch runs, so
|
|
122
|
+
// a grid does not blank the outcomes the reader is still acting on
|
|
123
|
+
setState(previous => ({
|
|
124
|
+
results: previous.results,
|
|
125
|
+
isLoading: true,
|
|
126
|
+
error: null,
|
|
127
|
+
}));
|
|
128
|
+
try {
|
|
129
|
+
const rowResults = yield send(client, collection, rows);
|
|
130
|
+
if (stillOwnsTheState()) {
|
|
131
|
+
setState({ results: rowResults, isLoading: false, error: null });
|
|
132
|
+
}
|
|
133
|
+
return rowResults;
|
|
134
|
+
}
|
|
135
|
+
catch (e) {
|
|
136
|
+
// NOTE: the results of the previous call are kept. They are still true
|
|
137
|
+
// about the rows that landed then, and a rejected retry must not be
|
|
138
|
+
// what makes those ids disappear — nothing else records them
|
|
139
|
+
if (stillOwnsTheState()) {
|
|
140
|
+
setState(previous => ({
|
|
141
|
+
results: previous.results,
|
|
142
|
+
isLoading: false,
|
|
143
|
+
error: e instanceof Error ? e : new Error(String(e)),
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
146
|
+
throw e;
|
|
147
|
+
}
|
|
148
|
+
}), [client, collection, send, sequence]);
|
|
149
|
+
const reset = useCallback(() => {
|
|
150
|
+
callId.current += 1;
|
|
151
|
+
setState(NOTHING_WRITTEN);
|
|
152
|
+
}, []);
|
|
153
|
+
return {
|
|
154
|
+
run,
|
|
155
|
+
results: state.results,
|
|
156
|
+
isLoading: state.isLoading,
|
|
157
|
+
error: state.error,
|
|
158
|
+
reset,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { __awaiter } from "tslib";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { useEmbedContext } from '../auth/useEmbedContext';
|
|
3
4
|
import { useSfaClient } from '../provider/useSfaClient';
|
|
4
5
|
/** Stable identity so an unanswered query never re-renders on a fresh array. */
|
|
5
6
|
const NO_ITEMS = [];
|
|
@@ -7,9 +8,11 @@ const NO_ITEMS = [];
|
|
|
7
8
|
* @description Fetches a paginated list of records from the SFA/CRM API.
|
|
8
9
|
*/
|
|
9
10
|
export function useRecordList(collection, params, options) {
|
|
10
|
-
var _a, _b, _c;
|
|
11
|
+
var _a, _b, _c, _d;
|
|
11
12
|
const client = useSfaClient();
|
|
12
|
-
const
|
|
13
|
+
const embedCtx = useEmbedContext();
|
|
14
|
+
const listId = (_a = params === null || params === void 0 ? void 0 : params.listId) !== null && _a !== void 0 ? _a : embedCtx === null || embedCtx === void 0 ? void 0 : embedCtx.list_id;
|
|
15
|
+
const limit = (_b = params === null || params === void 0 ? void 0 : params.limit) !== null && _b !== void 0 ? _b : 20;
|
|
13
16
|
const [items, setItems] = useState([]);
|
|
14
17
|
const [totalCount, setTotalCount] = useState(0);
|
|
15
18
|
const [hasMore, setHasMore] = useState(false);
|
|
@@ -22,11 +25,11 @@ export function useRecordList(collection, params, options) {
|
|
|
22
25
|
* about the *first* page: from there the hook pages on its own, computing
|
|
23
26
|
* offsets from the page number and reusing cursors the server handed back.
|
|
24
27
|
*/
|
|
25
|
-
const initialOffset = (
|
|
28
|
+
const initialOffset = (_c = params === null || params === void 0 ? void 0 : params.offset) !== null && _c !== void 0 ? _c : 0;
|
|
26
29
|
const initialCursor = params === null || params === void 0 ? void 0 : params.cursor;
|
|
27
30
|
const initialPage = limit > 0 ? Math.floor(initialOffset / limit) : 0;
|
|
28
31
|
const [pageIndex, setPageIndex] = useState(initialPage);
|
|
29
|
-
const enabled = (
|
|
32
|
+
const enabled = (_d = options === null || options === void 0 ? void 0 : options.enabled) !== null && _d !== void 0 ? _d : true;
|
|
30
33
|
// NOTE: the seed is never published. Until a fetch settles for the current
|
|
31
34
|
// query the reported status is derived below, so this only matters after the
|
|
32
35
|
// first settle — by which time a fetch has set it.
|
|
@@ -50,6 +53,7 @@ export function useRecordList(collection, params, options) {
|
|
|
50
53
|
filterKey,
|
|
51
54
|
initialOffset,
|
|
52
55
|
initialCursor,
|
|
56
|
+
listId,
|
|
53
57
|
}), [
|
|
54
58
|
collection,
|
|
55
59
|
limit,
|
|
@@ -58,6 +62,7 @@ export function useRecordList(collection, params, options) {
|
|
|
58
62
|
filterKey,
|
|
59
63
|
initialOffset,
|
|
60
64
|
initialCursor,
|
|
65
|
+
listId,
|
|
61
66
|
]);
|
|
62
67
|
/**
|
|
63
68
|
* Which client, query and page the records in state came from, so a failure
|
|
@@ -95,14 +100,14 @@ export function useRecordList(collection, params, options) {
|
|
|
95
100
|
// Every other page is reached with what the server handed back, or with
|
|
96
101
|
// an offset computed from the page number.
|
|
97
102
|
const cursor = (_a = cursorMapRef.current.get(page)) !== null && _a !== void 0 ? _a : (page === initialPage ? initialCursor : undefined);
|
|
98
|
-
const result = yield client.list(collection, Object.assign({ limit,
|
|
103
|
+
const result = yield client.list(collection, Object.assign(Object.assign({ limit,
|
|
99
104
|
// NOTE: offset is a record position, not a page number. Sending the
|
|
100
105
|
// page number asks for records 1..limit for page 1: it overlaps
|
|
101
106
|
// page 0 and comes back reporting pageIndex 0, so the list never
|
|
102
107
|
// advances. SfaClient derives pageIndex from this offset even when
|
|
103
108
|
// the server paginates by cursor, so the two have to agree.
|
|
104
109
|
offset: page === initialPage ? initialOffset : page * limit, sortBy,
|
|
105
|
-
sortOrder, filter: filterRef.current }, (cursor ? { cursor } : {})));
|
|
110
|
+
sortOrder, filter: filterRef.current }, (cursor ? { cursor } : {})), (listId ? { listId } : {})));
|
|
106
111
|
if (currentFetchId === fetchId.current) {
|
|
107
112
|
// NOTE: the refs are written before the setState calls. Outside an
|
|
108
113
|
// event handler React 18 in legacy mode re-renders on every setState
|
|
@@ -179,6 +184,7 @@ export function useRecordList(collection, params, options) {
|
|
|
179
184
|
initialOffset,
|
|
180
185
|
initialCursor,
|
|
181
186
|
initialPage,
|
|
187
|
+
listId,
|
|
182
188
|
]);
|
|
183
189
|
useEffect(() => {
|
|
184
190
|
// Invalidate anything still in flight before deciding what to do next.
|
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
import type { SfaApiMode, SfaClientConfig, SfaClientInterface, SfaCreateOptions, SfaDescribeResult, SfaFieldError, SfaFieldTypeCatalog, SfaListParams, SfaListResponse, SfaObjectCatalog, SfaRecord } from './types';
|
|
1
|
+
import type { SfaApiMode, SfaBulkDeleteRow, SfaBulkRowResult, SfaBulkUpdateRow, SfaClientConfig, SfaClientInterface, SfaCreateOptions, SfaDescribeResult, SfaFieldError, SfaFieldTypeCatalog, SfaListParams, SfaListResponse, SfaObjectCatalog, SfaRecord } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* @description The most rows one bulk call sends.
|
|
4
|
+
*
|
|
5
|
+
* NOTE: the API's own contract is `minItems: 1, maxItems: 100`. 25 is the
|
|
6
|
+
* kit's stricter limit, and it comes from how long the call takes rather than
|
|
7
|
+
* from what the API accepts: Keystone delegates one row at a time, measured at
|
|
8
|
+
* about 1.4 s per row on staging (2026-09-04). 25 rows fits inside the
|
|
9
|
+
* server's own 45 s budget for a bulk write, which in turn fits inside the
|
|
10
|
+
* 60 s ALB idle timeout. Agreed with Kumada and Takeuchi.
|
|
11
|
+
*
|
|
12
|
+
* Do not raise this to match `maxItems` — a call of 100 rows would outlast
|
|
13
|
+
* the server's budget and then the load balancer, and the caller would learn
|
|
14
|
+
* nothing about which rows had been written. It can go up only once a row
|
|
15
|
+
* costs less, or those limits change.
|
|
16
|
+
*
|
|
17
|
+
* NOTE: public because a caller cannot batch without it, and kept next to the
|
|
18
|
+
* check that enforces it so the two cannot disagree.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SFA_BULK_MAX_ROWS = 25;
|
|
2
21
|
/**
|
|
3
22
|
* @description API client for SFA/CRM data operations via keystone.
|
|
4
23
|
* Authenticates with a JWT Bearer token.
|
|
@@ -30,10 +49,57 @@ export declare class SfaClient implements SfaClientInterface {
|
|
|
30
49
|
/** @description Throws if recordId is empty to prevent malformed API requests. */
|
|
31
50
|
private assertRecordId;
|
|
32
51
|
/**
|
|
33
|
-
* @description Creates a new record.
|
|
34
|
-
*
|
|
52
|
+
* @description Creates a new record.
|
|
53
|
+
*
|
|
54
|
+
* NOTE: a retried create writes a second record — nothing de-duplicates it.
|
|
35
55
|
*/
|
|
36
56
|
create(collection: string, fields: Record<string, unknown>, options?: SfaCreateOptions): Promise<SfaRecord>;
|
|
57
|
+
/**
|
|
58
|
+
* @description Creates many records of one object in a single call.
|
|
59
|
+
*
|
|
60
|
+
* The call is best-effort per row: the API answers 200 and reports every
|
|
61
|
+
* row's outcome, so a failed row does not undo the ones that worked. Per-row
|
|
62
|
+
* failures come back as results, not as thrown errors — throwing would
|
|
63
|
+
* discard the ids of the rows that succeeded, which are not recoverable
|
|
64
|
+
* anywhere else. SfaValidationError and the rest still surface when the
|
|
65
|
+
* whole call is rejected.
|
|
66
|
+
*
|
|
67
|
+
* Retry only the failed rows. What makes that safe is that those rows did
|
|
68
|
+
* not apply: nothing on the server de-duplicates a repeated write.
|
|
69
|
+
*/
|
|
70
|
+
bulkCreate(collection: string, rows: readonly Record<string, unknown>[]): Promise<readonly SfaBulkRowResult[]>;
|
|
71
|
+
/**
|
|
72
|
+
* @description Updates many records of one object in a single call.
|
|
73
|
+
*
|
|
74
|
+
* Each row carries its own `version`, so a stale row comes back as
|
|
75
|
+
* `conflict` with the server's current record to reconcile from — the rest
|
|
76
|
+
* of the batch still applies, and nothing is rolled back. Per-row outcomes
|
|
77
|
+
* are results, not thrown errors: a throw would hide which rows landed.
|
|
78
|
+
*/
|
|
79
|
+
bulkUpdate(collection: string, rows: readonly SfaBulkUpdateRow[]): Promise<readonly SfaBulkRowResult[]>;
|
|
80
|
+
/**
|
|
81
|
+
* @description Deletes many records of one object in a single call.
|
|
82
|
+
*
|
|
83
|
+
* Each row carries the `version` last read for it; a stale row comes back as
|
|
84
|
+
* `conflict` and is left in place while the rest of the batch applies.
|
|
85
|
+
*/
|
|
86
|
+
bulkDelete(collection: string, rows: readonly SfaBulkDeleteRow[]): Promise<readonly SfaBulkRowResult[]>;
|
|
87
|
+
/**
|
|
88
|
+
* @description Prepares one row's fields for a bulk write.
|
|
89
|
+
*
|
|
90
|
+
* NOTE: shared by bulk-create and bulk-update so the two steps stay in this
|
|
91
|
+
* order for both. Schema-aware normalisation reads the object's field types
|
|
92
|
+
* — a select written by label, a date written loosely — and the write
|
|
93
|
+
* normalisation that follows only shapes what it produced.
|
|
94
|
+
*/
|
|
95
|
+
private normalizeRowFields;
|
|
96
|
+
/**
|
|
97
|
+
* @description Sends one bulk request and reads its per-row outcomes.
|
|
98
|
+
*
|
|
99
|
+
* NOTE: shared by the three bulk methods so the envelope and the result
|
|
100
|
+
* parsing cannot drift apart between them.
|
|
101
|
+
*/
|
|
102
|
+
private postBulk;
|
|
37
103
|
/**
|
|
38
104
|
* @description Updates a record with optimistic locking. The version token
|
|
39
105
|
* (from a prior GET) is sent as the If-Match header; a 412 response means
|