@geniee-inc/sfa-kit 1.14.1 → 1.16.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 +40 -11
- package/package.json +1 -1
- package/src/lib/auth/ObjectKeyGuard.d.ts +12 -0
- package/src/lib/auth/ObjectKeyGuard.js +61 -0
- package/src/lib/auth/SfaLoginGate.js +5 -4
- package/src/lib/hooks/splitBulkRows.d.ts +8 -4
- package/src/lib/hooks/splitBulkRows.js +17 -4
- package/src/lib/hooks/useBulkCreateRecords.d.ts +8 -3
- package/src/lib/hooks/useBulkCreateRecords.js +5 -2
- package/src/lib/hooks/useBulkDeleteRecords.d.ts +14 -1
- package/src/lib/hooks/useBulkDeleteRecords.js +4 -0
- package/src/lib/hooks/useBulkUpdateRecords.d.ts +8 -2
- package/src/lib/hooks/useBulkUpdateRecords.js +3 -1
- package/src/lib/provider/SfaClient.d.ts +1 -0
- package/src/lib/provider/SfaClient.js +18 -5
- package/src/lib/provider/types.d.ts +19 -3
- package/src/lib/version.d.ts +1 -1
- package/src/lib/version.js +1 -1
package/GUIDE.md
CHANGED
|
@@ -286,9 +286,11 @@ that did land, and those are not recoverable anywhere else.
|
|
|
286
286
|
whole picture leaves it looking neither written nor retryable. Re-read the
|
|
287
287
|
records to find out what landed
|
|
288
288
|
- `failed` — the rows that did not land **and can be sent again**: `conflict`,
|
|
289
|
-
`invalid` and `
|
|
290
|
-
that they did not apply; there is no
|
|
291
|
-
on
|
|
289
|
+
`invalid`, `not_attempted`, `not_found` and `not_permitted`. Retry exactly
|
|
290
|
+
these. What makes that safe is that they did not apply; there is no
|
|
291
|
+
server-side de-duplication to fall back on. Read each row's `status` before
|
|
292
|
+
offering a retry: `not_found` and `not_permitted` are the server's settled
|
|
293
|
+
answer, and resending alone will not change it
|
|
292
294
|
- `unresolved` — the rows that may already be written but cannot be reported
|
|
293
295
|
either way. They keep whatever `id` arrived. **Re-read these; never retry
|
|
294
296
|
them**: a create that already landed would be written twice
|
|
@@ -375,7 +377,9 @@ return (
|
|
|
375
377
|
<ErrorSnackbar
|
|
376
378
|
error={null}
|
|
377
379
|
message={`${results.length} 件中 ${updated.length} 件を保存しました。${failed.length} 件は保存されていません。`}
|
|
378
|
-
onRetry={() =>
|
|
380
|
+
onRetry={() =>
|
|
381
|
+
void bulkUpdate(rowsFor(resendable(failed))).catch(() => undefined)
|
|
382
|
+
}
|
|
379
383
|
onClose={reset}
|
|
380
384
|
/>
|
|
381
385
|
)}
|
|
@@ -385,17 +389,42 @@ return (
|
|
|
385
389
|
|
|
386
390
|
Counts only: which row failed and why belongs in the grid, where the reader
|
|
387
391
|
acts on it — `results` carries each row's `index` and its per-field errors.
|
|
388
|
-
|
|
389
|
-
|
|
392
|
+
`unresolved` rows are re-read instead, so label the action accordingly
|
|
393
|
+
(`retryLabel`).
|
|
394
|
+
|
|
395
|
+
Offer `onRetry` for the `failed` rows a resend can still change — that is what
|
|
396
|
+
`resendable` stands for above:
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
const SETTLED = new Set(['not_found', 'not_permitted']);
|
|
400
|
+
|
|
401
|
+
const resendable = (rows: readonly SfaBulkRowResult[]) =>
|
|
402
|
+
rows.filter(row => !SETTLED.has(row.status));
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Every row in `failed` is safe to send again, but a `not_found` or
|
|
406
|
+
`not_permitted` one comes back refused however often it is sent. Showing those
|
|
407
|
+
under a retry button gives the reader an action that cannot succeed; report
|
|
408
|
+
what they say instead.
|
|
390
409
|
|
|
391
410
|
A row counts as written only when it carries what a write produces: the record
|
|
392
411
|
for a create or an update, the id for a delete. Anything short of that is
|
|
393
412
|
`failed` or `unresolved`, and which one is decided by what the answer says,
|
|
394
|
-
not by elimination.
|
|
395
|
-
may be sent again:
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
413
|
+
not by elimination. Five statuses mean the row did not apply, and only those
|
|
414
|
+
may be sent again:
|
|
415
|
+
|
|
416
|
+
| Status | What it means | What to do |
|
|
417
|
+
| --------------- | -------------------------------------------------- | -------------------------------------------------------------------------- |
|
|
418
|
+
| `conflict` | Another write got there first | Retry with the `version` the row reports, or re-read first if none arrived |
|
|
419
|
+
| `invalid` | The row failed validation | Correct the row from its per-field errors, then resend |
|
|
420
|
+
| `not_attempted` | The call ran out of budget before starting the row | Resend exactly as it was |
|
|
421
|
+
| `not_found` | The record the row named does not exist | Tell the reader; a resend answers the same |
|
|
422
|
+
| `not_permitted` | The caller may not write that record | Tell the reader; a resend answers the same |
|
|
423
|
+
|
|
424
|
+
`not_found` and `not_permitted` are refusals of a row the call did reach, so no
|
|
425
|
+
write happened and resending is safe — it is simply futile until the record or
|
|
426
|
+
the permission changes. Their `error.code` may be an empty string: keystone has
|
|
427
|
+
not minted one for `not_permitted`, so branch on `status` rather than `code`.
|
|
399
428
|
|
|
400
429
|
Everything else is `unresolved`: `indeterminate`, the server saying it cannot
|
|
401
430
|
tell whether the row applied; a claimed success missing what it should carry —
|
package/package.json
CHANGED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
interface ObjectKeyGuardProps {
|
|
3
|
+
readonly objectKey: string;
|
|
4
|
+
readonly children: ReactNode;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* objectKey が組織のオブジェクトカタログに存在することを検証する。
|
|
8
|
+
* 不一致の場合はページ本体の代わりにエラーバナーを表示し、
|
|
9
|
+
* singular / typo のキーがリフレッシュ時に 404 で壊れる前に検出する。
|
|
10
|
+
*/
|
|
11
|
+
export declare function ObjectKeyGuard({ objectKey, children }: ObjectKeyGuardProps): JSX.Element;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useObjectCatalog } from '../hooks/useObjectCatalog';
|
|
3
|
+
import { font, space } from '../theme';
|
|
4
|
+
/**
|
|
5
|
+
* objectKey が組織のオブジェクトカタログに存在することを検証する。
|
|
6
|
+
* 不一致の場合はページ本体の代わりにエラーバナーを表示し、
|
|
7
|
+
* singular / typo のキーがリフレッシュ時に 404 で壊れる前に検出する。
|
|
8
|
+
*/
|
|
9
|
+
export function ObjectKeyGuard({ objectKey, children }) {
|
|
10
|
+
const { objects, isLoading, error } = useObjectCatalog();
|
|
11
|
+
// カタログ取得中やエラー時はブロックせずそのまま表示する。
|
|
12
|
+
// ガードが止めるのは「カタログに無い」と確定した場合のみ。
|
|
13
|
+
if (isLoading || error || objects.length === 0)
|
|
14
|
+
return _jsx(_Fragment, { children: children });
|
|
15
|
+
const knownKeys = objects.map(o => o.key);
|
|
16
|
+
if (knownKeys.includes(objectKey))
|
|
17
|
+
return _jsx(_Fragment, { children: children });
|
|
18
|
+
// 提案のみ大文字小文字を無視する。一致判定は API がキーを厳密比較するため厳密のまま
|
|
19
|
+
const target = objectKey.toLowerCase();
|
|
20
|
+
const suggestion = knownKeys.find(knownKey => {
|
|
21
|
+
const key = knownKey.toLowerCase();
|
|
22
|
+
return key.startsWith(target) || target.startsWith(key);
|
|
23
|
+
});
|
|
24
|
+
return (_jsx("div", { style: {
|
|
25
|
+
padding: space.lg,
|
|
26
|
+
fontFamily: font.family,
|
|
27
|
+
maxWidth: 560,
|
|
28
|
+
margin: '80px auto',
|
|
29
|
+
}, children: _jsxs("div", { style: {
|
|
30
|
+
background: '#FEF2F2',
|
|
31
|
+
border: '1px solid #FECACA',
|
|
32
|
+
borderRadius: 8,
|
|
33
|
+
padding: space.lg,
|
|
34
|
+
}, children: [_jsx("div", { style: {
|
|
35
|
+
fontWeight: 700,
|
|
36
|
+
fontSize: 15,
|
|
37
|
+
color: '#991B1B',
|
|
38
|
+
marginBottom: 8,
|
|
39
|
+
}, children: "Invalid object key" }), _jsxs("p", { style: {
|
|
40
|
+
margin: 0,
|
|
41
|
+
fontSize: 14,
|
|
42
|
+
color: '#7F1D1D',
|
|
43
|
+
lineHeight: 1.5,
|
|
44
|
+
}, children: [_jsx("code", { style: {
|
|
45
|
+
background: '#FEE2E2',
|
|
46
|
+
padding: '2px 6px',
|
|
47
|
+
borderRadius: 4,
|
|
48
|
+
fontSize: 13,
|
|
49
|
+
}, children: objectKey }), ' ', "does not match any object in this organization.", suggestion && (_jsxs(_Fragment, { children: [' ', "Did you mean", ' ', _jsx("code", { style: {
|
|
50
|
+
background: '#DCFCE7',
|
|
51
|
+
padding: '2px 6px',
|
|
52
|
+
borderRadius: 4,
|
|
53
|
+
fontSize: 13,
|
|
54
|
+
color: '#166534',
|
|
55
|
+
}, children: suggestion }), "?"] }))] }), _jsx("p", { style: {
|
|
56
|
+
margin: '12px 0 0',
|
|
57
|
+
fontSize: 12,
|
|
58
|
+
color: '#9CA3AF',
|
|
59
|
+
lineHeight: 1.4,
|
|
60
|
+
}, children: "Object keys are the plural collection name from the object catalogue (e.g. \"business_discussions\", not \"business_discussion\"). Check the objectKey prop on SfaLoginGate and the manifest." })] }) }));
|
|
61
|
+
}
|
|
@@ -6,6 +6,7 @@ import { color, font, radius, space, baseContainerStyle, baseButtonStyle, } from
|
|
|
6
6
|
import { SFA_KIT_VERSION } from '../version';
|
|
7
7
|
import { DevToolbar } from './DevToolbar';
|
|
8
8
|
import { authenticate } from './keystone-auth';
|
|
9
|
+
import { ObjectKeyGuard } from './ObjectKeyGuard';
|
|
9
10
|
import { KEYSTONE_PRODUCTION_API_BASE, resolveApiBase, } from './resolve-api-base';
|
|
10
11
|
import { loadSession, saveSession, clearSession, loadLastOrgId, } from './session-store';
|
|
11
12
|
import { StandalonePreviewCtx } from './StandalonePreviewContext';
|
|
@@ -102,7 +103,7 @@ export function SfaLoginGate({ apiBase, trustedOrigin, defaultOrganizationId, ob
|
|
|
102
103
|
return (_jsx(EmbedStatus, { title: "\u63A5\u7D9A\u3067\u304D\u307E\u305B\u3093", message: "SFA/CRM \u306E\u30DB\u30B9\u30C8\u30AA\u30EA\u30B8\u30F3\u3092\u78BA\u8A8D\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u3053\u306E\u30DA\u30FC\u30B8\u3092\u8868\u793A\u3067\u304D\u307E\u305B\u3093\u3002\u57CB\u3081\u8FBC\u307F\u8A2D\u5B9A\u3092\u7BA1\u7406\u8005\u306B\u3054\u78BA\u8A8D\u304F\u3060\u3055\u3044\u3002" }));
|
|
103
104
|
}
|
|
104
105
|
// Embedded: the SFA/CRM host that framed us decides the environment.
|
|
105
|
-
return (_jsx(EmbedFlow, { trustedOrigin: resolvedOrigin, apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvedOrigin), children: children }));
|
|
106
|
+
return (_jsx(EmbedFlow, { objectKey: objectKey, trustedOrigin: resolvedOrigin, apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvedOrigin), children: children }));
|
|
106
107
|
}
|
|
107
108
|
// Standalone: there is no host, so the page's own origin decides.
|
|
108
109
|
return (_jsx(StandaloneFlow, { apiBase: apiBase !== null && apiBase !== void 0 ? apiBase : resolveApiBase(resolvePageOrigin()), defaultOrganizationId: defaultOrganizationId, objectKey: objectKey, level: level, children: children }));
|
|
@@ -110,7 +111,7 @@ export function SfaLoginGate({ apiBase, trustedOrigin, defaultOrganizationId, ob
|
|
|
110
111
|
// ---------------------------------------------------------------------------
|
|
111
112
|
// Embed flow (iframe + postMessage)
|
|
112
113
|
// ---------------------------------------------------------------------------
|
|
113
|
-
function EmbedFlow({ trustedOrigin, apiBase, children, }) {
|
|
114
|
+
function EmbedFlow({ objectKey, trustedOrigin, apiBase, children, }) {
|
|
114
115
|
const normalizedBase = useMemo(() => apiBase.replace(/\/+$/, ''), [apiBase]);
|
|
115
116
|
const { phase, handleAuthError } = useEmbedSession({
|
|
116
117
|
apiBase: normalizedBase,
|
|
@@ -135,7 +136,7 @@ function EmbedFlow({ trustedOrigin, apiBase, children, }) {
|
|
|
135
136
|
}
|
|
136
137
|
if (!auth)
|
|
137
138
|
return null;
|
|
138
|
-
return (_jsx(EmbedContextValue.Provider, { value: phase.context, children: _jsx(SfaProvider, { apiBase: normalizedBase, auth: auth, mode: "direct", organizationId: phase.session.organizationId, onAuthError: handleAuthError, children: children }) }));
|
|
139
|
+
return (_jsx(EmbedContextValue.Provider, { value: phase.context, children: _jsx(SfaProvider, { apiBase: normalizedBase, auth: auth, mode: "direct", organizationId: phase.session.organizationId, onAuthError: handleAuthError, children: objectKey ? (_jsx(ObjectKeyGuard, { objectKey: objectKey, children: children })) : (children) }) }));
|
|
139
140
|
}
|
|
140
141
|
function EmbedStatus({ title, message, showReload, }) {
|
|
141
142
|
const handleReload = useCallback(() => {
|
|
@@ -241,7 +242,7 @@ function StandaloneFlow({ apiBase, defaultOrganizationId, objectKey, level, chil
|
|
|
241
242
|
if (!session || !auth) {
|
|
242
243
|
return (_jsx(LoginForm, { defaultApiBase: apiBase, defaultOrganizationId: defaultOrganizationId, onSubmit: handleLogin, status: status, error: error }));
|
|
243
244
|
}
|
|
244
|
-
return (_jsx(SfaProvider, { apiBase: activeBase, auth: auth, mode: "direct", organizationId: session.organizationId, onAuthError: handleLogout, children: _jsxs(StandalonePreviewCtx.Provider, { value: previewControls, children: [_jsx(DevToolbar, { organizationId: session.organizationId, preview: previewControls, objectKey: objectKey, level: resolvedLevel, onLogout: handleLogout }), children] }) }));
|
|
245
|
+
return (_jsx(SfaProvider, { apiBase: activeBase, auth: auth, mode: "direct", organizationId: session.organizationId, onAuthError: handleLogout, children: _jsxs(StandalonePreviewCtx.Provider, { value: previewControls, children: [_jsx(DevToolbar, { organizationId: session.organizationId, preview: previewControls, objectKey: objectKey, level: resolvedLevel, onLogout: handleLogout }), objectKey ? (_jsx(ObjectKeyGuard, { objectKey: objectKey, children: children })) : (children)] }) }));
|
|
245
246
|
}
|
|
246
247
|
// ---------------------------------------------------------------------------
|
|
247
248
|
// GENIEE SFA/CRM-branded login form
|
|
@@ -8,7 +8,9 @@ export interface BulkRowSplit<T> {
|
|
|
8
8
|
readonly conflicts: readonly SfaBulkRowResult[];
|
|
9
9
|
/**
|
|
10
10
|
* The rows that did not land and can be sent again. A `conflict` is one of
|
|
11
|
-
* them, and also appears in `conflicts`.
|
|
11
|
+
* them, and also appears in `conflicts`. Read each row's `status` to say why
|
|
12
|
+
* it failed: `not_found` and `not_permitted` are settled refusals that a
|
|
13
|
+
* resend alone will not clear.
|
|
12
14
|
*/
|
|
13
15
|
readonly failed: readonly SfaBulkRowResult[];
|
|
14
16
|
}
|
|
@@ -16,9 +18,11 @@ export interface BulkRowSplit<T> {
|
|
|
16
18
|
* @description Splits per-row outcomes by what the caller may do with them.
|
|
17
19
|
*
|
|
18
20
|
* NOTE: `failed` is decided by what the answer says, never by exclusion.
|
|
19
|
-
* `conflict`, `invalid` and `
|
|
20
|
-
* those are the ones safe to send again — a row
|
|
21
|
-
* unwritten as one the server
|
|
21
|
+
* `conflict`, `invalid`, `not_attempted`, `not_found` and `not_permitted` all
|
|
22
|
+
* mean the row did not apply, so those are the ones safe to send again — a row
|
|
23
|
+
* the call never started is as unwritten as one the server refused. Each keeps
|
|
24
|
+
* its own `status`, so a caller can tell "no permission" from "failed
|
|
25
|
+
* validation" without a bucket per reason. Anything else the kit cannot place —
|
|
22
26
|
* `indeterminate`, no record, no version, a status outside the contract or
|
|
23
27
|
* belonging to another operation — may already be written, and a retry would
|
|
24
28
|
* write it twice.
|
|
@@ -1,16 +1,29 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* The outcomes that mean the row did not apply, so sending it again is safe.
|
|
3
|
+
*
|
|
4
|
+
* NOTE: safe to send again, not sure to succeed. `invalid` needs the row
|
|
5
|
+
* corrected first, `conflict` a fresh `version`, and `not_found` /
|
|
6
|
+
* `not_permitted` answer the same until the record or the permission changes.
|
|
7
|
+
* What the set decides is that no write happened, so the caller may resend
|
|
8
|
+
* without writing a second record — which is the question `unresolved` exists
|
|
9
|
+
* to answer, and the reason these belong in `failed` instead.
|
|
10
|
+
*/
|
|
2
11
|
const RETRYABLE_STATUSES = new Set([
|
|
3
12
|
'conflict',
|
|
4
13
|
'invalid',
|
|
5
14
|
'not_attempted',
|
|
15
|
+
'not_found',
|
|
16
|
+
'not_permitted',
|
|
6
17
|
]);
|
|
7
18
|
/**
|
|
8
19
|
* @description Splits per-row outcomes by what the caller may do with them.
|
|
9
20
|
*
|
|
10
21
|
* NOTE: `failed` is decided by what the answer says, never by exclusion.
|
|
11
|
-
* `conflict`, `invalid` and `
|
|
12
|
-
* those are the ones safe to send again — a row
|
|
13
|
-
* unwritten as one the server
|
|
22
|
+
* `conflict`, `invalid`, `not_attempted`, `not_found` and `not_permitted` all
|
|
23
|
+
* mean the row did not apply, so those are the ones safe to send again — a row
|
|
24
|
+
* the call never started is as unwritten as one the server refused. Each keeps
|
|
25
|
+
* its own `status`, so a caller can tell "no permission" from "failed
|
|
26
|
+
* validation" without a bucket per reason. Anything else the kit cannot place —
|
|
14
27
|
* `indeterminate`, no record, no version, a status outside the contract or
|
|
15
28
|
* belonging to another operation — may already be written, and a retry would
|
|
16
29
|
* write it twice.
|
|
@@ -18,7 +18,9 @@ export interface UseBulkCreateRecordsResult {
|
|
|
18
18
|
/**
|
|
19
19
|
* The rows that did not land and can be sent again, with their `index` and
|
|
20
20
|
* `error`. A batch can be partly successful, so this being non-empty does
|
|
21
|
-
* not mean nothing was written
|
|
21
|
+
* not mean nothing was written. Read each row's `status` before retrying:
|
|
22
|
+
* `not_found` and `not_permitted` are settled refusals that come back the
|
|
23
|
+
* same however often they are resent.
|
|
22
24
|
*/
|
|
23
25
|
readonly failed: readonly SfaBulkRowResult[];
|
|
24
26
|
/**
|
|
@@ -50,7 +52,10 @@ export interface UseBulkCreateRecordsResult {
|
|
|
50
52
|
* the call as a whole.
|
|
51
53
|
*
|
|
52
54
|
* Retry only the rows in `failed`: those did not apply, which is what makes
|
|
53
|
-
* resending them safe
|
|
54
|
-
*
|
|
55
|
+
* resending them safe — safe, not sure to succeed. A `not_found` or
|
|
56
|
+
* `not_permitted` row is refused again until the record or the permission
|
|
57
|
+
* changes, so branch on `status` rather than offering one retry for the whole
|
|
58
|
+
* bucket. Re-read the rows in `unresolved` instead — nothing protects against
|
|
59
|
+
* writing them twice.
|
|
55
60
|
*/
|
|
56
61
|
export declare function useBulkCreateRecords(collection: string): UseBulkCreateRecordsResult;
|
|
@@ -14,8 +14,11 @@ const sendBulkCreate = (client, collection, rows) => client.bulkCreate(collectio
|
|
|
14
14
|
* the call as a whole.
|
|
15
15
|
*
|
|
16
16
|
* Retry only the rows in `failed`: those did not apply, which is what makes
|
|
17
|
-
* resending them safe
|
|
18
|
-
*
|
|
17
|
+
* resending them safe — safe, not sure to succeed. A `not_found` or
|
|
18
|
+
* `not_permitted` row is refused again until the record or the permission
|
|
19
|
+
* changes, so branch on `status` rather than offering one retry for the whole
|
|
20
|
+
* bucket. Re-read the rows in `unresolved` instead — nothing protects against
|
|
21
|
+
* writing them twice.
|
|
19
22
|
*/
|
|
20
23
|
export function useBulkCreateRecords(collection) {
|
|
21
24
|
const { run, results, isLoading, error, reset } = useBulkWrite(collection, sendBulkCreate);
|
|
@@ -20,7 +20,16 @@ export interface UseBulkDeleteRecordsResult {
|
|
|
20
20
|
* whether it still wants the record gone.
|
|
21
21
|
*/
|
|
22
22
|
readonly conflicts: readonly SfaBulkRowResult[];
|
|
23
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* The rows that were not removed by this call, conflicts included.
|
|
25
|
+
*
|
|
26
|
+
* NOTE: not all of them are still in place. A `not_found` row says the
|
|
27
|
+
* record was not there to delete — already gone, or never existing. It is
|
|
28
|
+
* reported here rather than in `deletedIds` because this call removed
|
|
29
|
+
* nothing, and the kit will not claim a deletion the server did not make.
|
|
30
|
+
* A caller whose grid wants the row cleared checks for that `status`;
|
|
31
|
+
* `not_permitted` is the one that leaves the record in place.
|
|
32
|
+
*/
|
|
24
33
|
readonly failed: readonly SfaBulkRowResult[];
|
|
25
34
|
/**
|
|
26
35
|
* The rows that may already be removed but cannot be reported either way.
|
|
@@ -46,5 +55,9 @@ export interface UseBulkDeleteRecordsResult {
|
|
|
46
55
|
* place while the rest of the batch applies. There is no rollback, so a
|
|
47
56
|
* partly failed batch has already removed records — which is why per-row
|
|
48
57
|
* outcomes are returned as data rather than thrown.
|
|
58
|
+
*
|
|
59
|
+
* Retry only the rows in `failed`, and read each row's `status` first: a
|
|
60
|
+
* `not_found` row was not there to delete and a `not_permitted` one may not be
|
|
61
|
+
* deleted, so neither clears on a resend. Rows in `unresolved` are re-read.
|
|
49
62
|
*/
|
|
50
63
|
export declare function useBulkDeleteRecords(collection: string): UseBulkDeleteRecordsResult;
|
|
@@ -11,6 +11,10 @@ const sendBulkDelete = (client, collection, rows) => client.bulkDelete(collectio
|
|
|
11
11
|
* place while the rest of the batch applies. There is no rollback, so a
|
|
12
12
|
* partly failed batch has already removed records — which is why per-row
|
|
13
13
|
* outcomes are returned as data rather than thrown.
|
|
14
|
+
*
|
|
15
|
+
* Retry only the rows in `failed`, and read each row's `status` first: a
|
|
16
|
+
* `not_found` row was not there to delete and a `not_permitted` one may not be
|
|
17
|
+
* deleted, so neither clears on a resend. Rows in `unresolved` are re-read.
|
|
14
18
|
*/
|
|
15
19
|
export function useBulkDeleteRecords(collection) {
|
|
16
20
|
const { run, results, isLoading, error, reset } = useBulkWrite(collection, sendBulkDelete);
|
|
@@ -21,7 +21,11 @@ export interface UseBulkUpdateRecordsResult {
|
|
|
21
21
|
* without a `version`, re-read it first — there is nothing to lock against.
|
|
22
22
|
*/
|
|
23
23
|
readonly conflicts: readonly SfaBulkRowResult[];
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* The rows that did not land and can be sent again, conflicts included.
|
|
26
|
+
* Read each row's `status`: `not_found` and `not_permitted` are settled
|
|
27
|
+
* refusals, and a resend alone will not clear them.
|
|
28
|
+
*/
|
|
25
29
|
readonly failed: readonly SfaBulkRowResult[];
|
|
26
30
|
/**
|
|
27
31
|
* The rows that may already be applied but cannot be reported either way.
|
|
@@ -49,7 +53,9 @@ export interface UseBulkUpdateRecordsResult {
|
|
|
49
53
|
* as data rather than thrown errors.
|
|
50
54
|
*
|
|
51
55
|
* Retry only the rows in `failed`. A conflict is retried with the version from
|
|
52
|
-
* its `record`, never by re-sending the stale one.
|
|
56
|
+
* its `record`, never by re-sending the stale one. A `not_found` or
|
|
57
|
+
* `not_permitted` row is refused again until the record or the permission
|
|
58
|
+
* changes — report it rather than offering a retry. Rows in `unresolved` are
|
|
53
59
|
* re-read, not retried.
|
|
54
60
|
*/
|
|
55
61
|
export declare function useBulkUpdateRecords(collection: string): UseBulkUpdateRecordsResult;
|
|
@@ -13,7 +13,9 @@ const sendBulkUpdate = (client, collection, rows) => client.bulkUpdate(collectio
|
|
|
13
13
|
* as data rather than thrown errors.
|
|
14
14
|
*
|
|
15
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.
|
|
16
|
+
* its `record`, never by re-sending the stale one. A `not_found` or
|
|
17
|
+
* `not_permitted` row is refused again until the record or the permission
|
|
18
|
+
* changes — report it rather than offering a retry. Rows in `unresolved` are
|
|
17
19
|
* re-read, not retried.
|
|
18
20
|
*/
|
|
19
21
|
export function useBulkUpdateRecords(collection) {
|
|
@@ -35,6 +35,7 @@ export declare class SfaClient implements SfaClientInterface {
|
|
|
35
35
|
/** In-memory cache for describe results, keyed by objectKey. Stores Promises
|
|
36
36
|
* so concurrent callers share a single in-flight request. */
|
|
37
37
|
private readonly describeCache;
|
|
38
|
+
private listObjectsCache;
|
|
38
39
|
constructor(config: SfaClientConfig);
|
|
39
40
|
private resolveBase;
|
|
40
41
|
get(collection: string, recordId: string): Promise<SfaRecord>;
|
|
@@ -59,6 +59,7 @@ export class SfaClient {
|
|
|
59
59
|
/** In-memory cache for describe results, keyed by objectKey. Stores Promises
|
|
60
60
|
* so concurrent callers share a single in-flight request. */
|
|
61
61
|
this.describeCache = new Map();
|
|
62
|
+
this.listObjectsCache = null;
|
|
62
63
|
this.apiBase = this.resolveBase(config.apiBase.replace(/\/+$/, ''), config.mode);
|
|
63
64
|
this.auth = config.auth;
|
|
64
65
|
this.mode = config.mode;
|
|
@@ -174,11 +175,21 @@ export class SfaClient {
|
|
|
174
175
|
}
|
|
175
176
|
listObjects() {
|
|
176
177
|
return __awaiter(this, void 0, void 0, function* () {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
if (this.listObjectsCache)
|
|
179
|
+
return this.listObjectsCache;
|
|
180
|
+
const promise = this.fetchGet(this.objectsPath())
|
|
181
|
+
.then(raw => {
|
|
182
|
+
var _a;
|
|
183
|
+
const data = raw;
|
|
184
|
+
const objects = ((_a = data.objects) !== null && _a !== void 0 ? _a : []).map(o => normalizeObjectSummary(o));
|
|
185
|
+
return { objects };
|
|
186
|
+
})
|
|
187
|
+
.catch((err) => {
|
|
188
|
+
this.listObjectsCache = null;
|
|
189
|
+
throw err;
|
|
190
|
+
});
|
|
191
|
+
this.listObjectsCache = promise;
|
|
192
|
+
return promise;
|
|
182
193
|
});
|
|
183
194
|
}
|
|
184
195
|
listFieldTypes() {
|
|
@@ -1040,6 +1051,8 @@ function readBulkRowStatus(raw) {
|
|
|
1040
1051
|
conflict: 'conflict',
|
|
1041
1052
|
invalid: 'invalid',
|
|
1042
1053
|
not_attempted: 'not_attempted',
|
|
1054
|
+
not_found: 'not_found',
|
|
1055
|
+
not_permitted: 'not_permitted',
|
|
1043
1056
|
indeterminate: 'indeterminate',
|
|
1044
1057
|
};
|
|
1045
1058
|
return typeof raw === 'string' ? (_a = known[raw]) !== null && _a !== void 0 ? _a : 'unknown' : 'unknown';
|
|
@@ -103,13 +103,19 @@ export interface SfaFieldError {
|
|
|
103
103
|
* All are reported per row with HTTP 200 — a bulk call is best-effort and has
|
|
104
104
|
* no all-or-nothing mode.
|
|
105
105
|
*
|
|
106
|
+
* `not_found` and `not_permitted` are refusals of a row the call did reach:
|
|
107
|
+
* the record it named does not exist, or the caller may not write it. No write
|
|
108
|
+
* happened either way, so both are settled failures and reach the caller in
|
|
109
|
+
* `failed`. Sending them again is safe, but returns the same answer until the
|
|
110
|
+
* record or the permission changes — the decision belongs to the server.
|
|
111
|
+
*
|
|
106
112
|
* `unknown` is not sent by the API: it is what the kit reads an unrecognised
|
|
107
113
|
* status as, and what a claimed success it cannot make sense of becomes — no
|
|
108
114
|
* record, or no `version` for the next write to lock against. The row keeps
|
|
109
115
|
* its `id`, and reaches the caller in `unresolved`, never in `failed`:
|
|
110
116
|
* retrying a create that already landed writes a second record.
|
|
111
117
|
*/
|
|
112
|
-
export type SfaBulkRowStatus = 'created' | 'updated' | 'deleted' | 'conflict' | 'invalid' | 'not_attempted' | 'indeterminate' | 'unknown';
|
|
118
|
+
export type SfaBulkRowStatus = 'created' | 'updated' | 'deleted' | 'conflict' | 'invalid' | 'not_attempted' | 'not_found' | 'not_permitted' | 'indeterminate' | 'unknown';
|
|
113
119
|
/** @description One row of a bulk update: what to change and from which version. */
|
|
114
120
|
export interface SfaBulkUpdateRow {
|
|
115
121
|
readonly id: string;
|
|
@@ -146,12 +152,22 @@ export interface SfaBulkRowResult {
|
|
|
146
152
|
* against.
|
|
147
153
|
*/
|
|
148
154
|
readonly record: SfaRecord | null;
|
|
149
|
-
/**
|
|
155
|
+
/**
|
|
156
|
+
* The problem detail for this row. Set on the statuses that report one:
|
|
157
|
+
* `conflict`, `invalid`, `not_found` and `not_permitted`.
|
|
158
|
+
*/
|
|
150
159
|
readonly error: SfaBulkRowError | null;
|
|
151
160
|
}
|
|
152
161
|
/** @description Why one row failed, in the same shape single writes report. */
|
|
153
162
|
export interface SfaBulkRowError {
|
|
154
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Machine-readable problem code, e.g. `version_mismatch`.
|
|
165
|
+
*
|
|
166
|
+
* NOTE: empty when the response carried none. Keystone has not minted a code
|
|
167
|
+
* for `not_permitted` and widened that field of its contract to any string,
|
|
168
|
+
* so a caller that branches on `code` has to treat `''` as "not stated" and
|
|
169
|
+
* fall back to `status`, rather than assume every failure names itself.
|
|
170
|
+
*/
|
|
155
171
|
readonly code: string;
|
|
156
172
|
readonly message: string;
|
|
157
173
|
/** Per-field errors when the row failed validation. */
|
package/src/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SFA_KIT_VERSION = "1.
|
|
1
|
+
export declare const SFA_KIT_VERSION = "1.16.0";
|
package/src/lib/version.js
CHANGED