@ryuzaki13/react-foundation-api 1.1.15 → 1.1.17
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/README.md +32 -43
- package/dist/chunks/{odataFetchFn-vnAXC-c0.js → odataFetchFn-B9wSQpUS.js} +11 -11
- package/dist/chunks/{odataFetchFn-vnAXC-c0.js.map → odataFetchFn-B9wSQpUS.js.map} +1 -1
- package/dist/odata/fetchCollectionData.d.ts +1 -1
- package/dist/odata/fetchCollectionData.d.ts.map +1 -1
- package/dist/odata/index.js +111 -112
- package/dist/odata/index.js.map +1 -1
- package/dist/odata/projectODataCollectionSort.d.ts +1 -1
- package/dist/odata/projectODataCollectionSort.d.ts.map +1 -1
- package/dist/odata/types.d.ts +1 -2
- package/dist/odata/types.d.ts.map +1 -1
- package/dist/odata/useODataCollection.d.ts +1 -1
- package/dist/odata/useODataCollection.d.ts.map +1 -1
- package/dist/odata/useODataCollectionQuery.d.ts +1 -1
- package/dist/odata/useODataCollectionQuery.d.ts.map +1 -1
- package/dist/odata/useODataEntity.d.ts +1 -1
- package/dist/odata/useODataEntity.d.ts.map +1 -1
- package/dist/persisted/index.js +1 -1
- package/package.json +8 -3
- package/src/adt/README.mdx +164 -0
- package/src/async/README.mdx +253 -0
- package/src/error-report/README.mdx +148 -0
- package/src/foundationApi.mdx +123 -0
- package/src/http/README.mdx +221 -0
- package/src/odata/README.mdx +790 -0
- package/src/persisted/README.mdx +454 -0
- package/src/resource/README.mdx +358 -0
- package/src/server-fn/README.mdx +194 -0
- package/src/transport/README.mdx +183 -0
- package/src/README.md +0 -937
- package/src/async/README.md +0 -623
- package/src/async/async.mdx +0 -6
- package/src/odata/README.md +0 -761
- package/src/odata/odataFetchFn.mdx +0 -6
- package/src/persisted/README.md +0 -598
- package/src/persisted/persisted.mdx +0 -6
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation API/Query/Persisted Records" />
|
|
4
|
+
|
|
5
|
+
# Сохранённые записи через `@ryuzaki13/react-foundation-api/persisted`
|
|
6
|
+
|
|
7
|
+
`persisted` — специализированный слой поверх TanStack Query для данных, которые пользователь сохраняет и позднее восстанавливает: presets, variants, view configs и другие versioned records.
|
|
8
|
+
|
|
9
|
+
## Ментальная модель
|
|
10
|
+
|
|
11
|
+
```text
|
|
12
|
+
PersistedResourceDescriptor
|
|
13
|
+
├─ identity: namespace + resource + normalized scope
|
|
14
|
+
├─ read capability: list | latest | history
|
|
15
|
+
├─ write capability: save | create | delete
|
|
16
|
+
├─ transport operation: REST | OData | server-fn | custom
|
|
17
|
+
└─ cache strategy после успешной mutation
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Capability optional: resource не обязан поддерживать все шесть операций. TypeScript разрешит соответствующий hook только когда operation присутствует в descriptor; runtime также выдаёт понятную ошибку при неправильном объекте.
|
|
21
|
+
|
|
22
|
+
## `persisted` или `resource`
|
|
23
|
+
|
|
24
|
+
| Нужна модель | Использовать |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| Фиксированные сохранённые records: list/latest/history/save/create/delete | `/persisted` |
|
|
27
|
+
| Произвольные названия и semantics операций | [`/resource`](../resource/README.mdx) |
|
|
28
|
+
|
|
29
|
+
`persisted` построен на тех же принципах keys/cache, но намеренно не является универсальным CRUD.
|
|
30
|
+
|
|
31
|
+
## Установка и provider
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @ryuzaki13/react-foundation-api @ryuzaki13/react-foundation-lib @tanstack/react-query react
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Hooks требуют `QueryClientProvider`. Descriptor и operations создавайте на module level, чтобы ссылки были стабильными.
|
|
38
|
+
|
|
39
|
+
## Импорт
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import {
|
|
43
|
+
applyPersistedCacheStrategy,
|
|
44
|
+
composePersistedCacheStrategies,
|
|
45
|
+
createInvalidatePersistedScopeCacheStrategy,
|
|
46
|
+
createPersistedJsonCodec,
|
|
47
|
+
createPersistedODataMutationOperation,
|
|
48
|
+
createPersistedODataQueryOperation,
|
|
49
|
+
createPersistedODataReadOperation,
|
|
50
|
+
createPersistedRecordKeys,
|
|
51
|
+
createPersistedResourceDescriptor,
|
|
52
|
+
createPersistedRestMutationOperation,
|
|
53
|
+
createPersistedRestQueryOperation,
|
|
54
|
+
createSetPersistedQueryDataCacheStrategy,
|
|
55
|
+
getPersistedHistoryData,
|
|
56
|
+
getPersistedLatestData,
|
|
57
|
+
getPersistedListData,
|
|
58
|
+
parsePersistedJson,
|
|
59
|
+
stringifyPersistedJson,
|
|
60
|
+
usePersistedCreateMutation,
|
|
61
|
+
usePersistedDeleteMutation,
|
|
62
|
+
usePersistedHistoryQuery,
|
|
63
|
+
usePersistedLatestQuery,
|
|
64
|
+
usePersistedListQuery,
|
|
65
|
+
usePersistedSaveMutation
|
|
66
|
+
} from "@ryuzaki13/react-foundation-api/persisted";
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Пошаговый REST пример
|
|
70
|
+
|
|
71
|
+
### 1. Контракты
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
type ViewScope = {
|
|
75
|
+
userId: string;
|
|
76
|
+
viewId: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type ViewConfig = {
|
|
80
|
+
columns: string[];
|
|
81
|
+
compact: boolean;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type SavedViewRecord = {
|
|
85
|
+
id: string;
|
|
86
|
+
createdUtc: string;
|
|
87
|
+
payload: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
type SaveViewInput = {
|
|
91
|
+
payload: ViewConfig;
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Внешний record и распарсенный config — разные типы. TypeScript не проверяет network payload автоматически.
|
|
96
|
+
|
|
97
|
+
### 2. Runtime parsers
|
|
98
|
+
|
|
99
|
+
```ts
|
|
100
|
+
function parseRecord(value: unknown): SavedViewRecord {
|
|
101
|
+
if (!isSavedViewRecord(value)) {
|
|
102
|
+
throw new Error("Некорректный SavedViewRecord payload");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseRecordList(value: unknown): SavedViewRecord[] {
|
|
109
|
+
if (!Array.isArray(value)) throw new Error("Ожидался массив records");
|
|
110
|
+
return value.map(parseRecord);
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### 3. Operations
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
const listOperation = createPersistedRestQueryOperation<
|
|
118
|
+
ViewScope,
|
|
119
|
+
void,
|
|
120
|
+
SavedViewRecord[]
|
|
121
|
+
>({
|
|
122
|
+
buildUrl: (scope) =>
|
|
123
|
+
`/api/users/${encodeURIComponent(scope.userId)}/views/${encodeURIComponent(scope.viewId)}`,
|
|
124
|
+
parseResponse: parseRecordList,
|
|
125
|
+
staleTime: 60_000
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const latestOperation = createPersistedRestQueryOperation<
|
|
129
|
+
ViewScope,
|
|
130
|
+
void,
|
|
131
|
+
SavedViewRecord,
|
|
132
|
+
ViewConfig | null
|
|
133
|
+
>({
|
|
134
|
+
buildUrl: (scope) =>
|
|
135
|
+
`/api/users/${encodeURIComponent(scope.userId)}/views/${encodeURIComponent(scope.viewId)}/latest`,
|
|
136
|
+
parseResponse: parseRecord,
|
|
137
|
+
transform: (record) => parsePersistedJson<ViewConfig>(record.payload)
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const saveOperation = createPersistedRestMutationOperation<
|
|
141
|
+
ViewScope,
|
|
142
|
+
SaveViewInput,
|
|
143
|
+
SavedViewRecord
|
|
144
|
+
>({
|
|
145
|
+
buildUrl: (scope) =>
|
|
146
|
+
`/api/users/${encodeURIComponent(scope.userId)}/views/${encodeURIComponent(scope.viewId)}`,
|
|
147
|
+
method: "PUT",
|
|
148
|
+
bodyMapper: (_scope, input) => ({
|
|
149
|
+
payload: stringifyPersistedJson(input.payload)
|
|
150
|
+
}),
|
|
151
|
+
parseResponse: parseRecord,
|
|
152
|
+
cacheStrategy: createInvalidatePersistedScopeCacheStrategy()
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 4. Descriptor
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const viewResource = createPersistedResourceDescriptor({
|
|
160
|
+
namespace: "view-config",
|
|
161
|
+
resource: "view",
|
|
162
|
+
normalizeScope: (scope: ViewScope | null | undefined) => ({
|
|
163
|
+
userId: scope?.userId.trim() ?? "",
|
|
164
|
+
viewId: scope?.viewId.trim() ?? ""
|
|
165
|
+
}),
|
|
166
|
+
isEnabled: (scope) => Boolean(scope?.userId && scope?.viewId),
|
|
167
|
+
getScopeError: () => "Не определён пользователь или представление",
|
|
168
|
+
transport: {
|
|
169
|
+
list: listOperation,
|
|
170
|
+
latest: latestOperation,
|
|
171
|
+
save: saveOperation
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`normalizeScope` изменяет только cache identity. В `buildUrl`/`execute` передаётся исходный scope.
|
|
177
|
+
|
|
178
|
+
### 5. Hooks
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
const scope = { userId, viewId };
|
|
182
|
+
const latestQuery = usePersistedLatestQuery(viewResource, scope);
|
|
183
|
+
const listQuery = usePersistedListQuery(viewResource, scope);
|
|
184
|
+
const saveMutation = usePersistedSaveMutation(viewResource, scope);
|
|
185
|
+
|
|
186
|
+
saveMutation.mutate({
|
|
187
|
+
payload: { columns: ["name", "status"], compact: true }
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Нельзя вызвать `usePersistedHistoryQuery` для этого descriptor, пока capability `history` не добавлена.
|
|
192
|
+
|
|
193
|
+
## Read capabilities
|
|
194
|
+
|
|
195
|
+
| Capability | Hook | Imperative helper | Args |
|
|
196
|
+
| --- | --- | --- | --- |
|
|
197
|
+
| `list` | `usePersistedListQuery` | `getPersistedListData` | нет (`void`) |
|
|
198
|
+
| `latest` | `usePersistedLatestQuery` | `getPersistedLatestData` | нет (`void`) |
|
|
199
|
+
| `history` | `usePersistedHistoryQuery` | `getPersistedHistoryData` | обязательный generic args |
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
const page = await getPersistedHistoryData(
|
|
203
|
+
resourceWithHistory,
|
|
204
|
+
scope,
|
|
205
|
+
{ cursor: "next", limit: 20 },
|
|
206
|
+
queryClient
|
|
207
|
+
);
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Query operation получает `{ scope, args, client, signal }`. `isEnabled` operation и descriptor определяют автоматический запуск React observer; `staleTime`, `gcTime`, `meta` переходят в TanStack Query options.
|
|
211
|
+
|
|
212
|
+
Imperative helpers используют `queryClient.fetchQuery`. Передавайте валидный scope; disabled policy в первую очередь управляет observer, а фактическая query function всё равно проверяет scope.
|
|
213
|
+
|
|
214
|
+
## Write capabilities
|
|
215
|
+
|
|
216
|
+
| Capability | Hook | Типичный смысл |
|
|
217
|
+
| --- | --- | --- |
|
|
218
|
+
| `save` | `usePersistedSaveMutation` | Сохранить/обновить snapshot |
|
|
219
|
+
| `create` | `usePersistedCreateMutation` | Создать новую именованную запись |
|
|
220
|
+
| `delete` | `usePersistedDeleteMutation` | Удалить запись |
|
|
221
|
+
|
|
222
|
+
Mutation operation получает `{ scope, input, client }`. Generic persisted contract не передаёт `AbortSignal` для mutation.
|
|
223
|
+
|
|
224
|
+
После успешного transport:
|
|
225
|
+
|
|
226
|
+
1. выбирается cache strategy;
|
|
227
|
+
2. strategy выполняется и awaited;
|
|
228
|
+
3. вызывается optional hook `onSuccess(result, input)`.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
usePersistedSaveMutation(resource, scope, {
|
|
232
|
+
cacheStrategy: null, // Отключить operation strategy.
|
|
233
|
+
onSuccess(result) {
|
|
234
|
+
showSuccess(`Сохранено ${result.id}`);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
`cacheStrategy: undefined` использует strategy операции; `null` отключает; объект заменяет исходную strategy. Для дополнения применяйте композицию.
|
|
240
|
+
|
|
241
|
+
## Query keys
|
|
242
|
+
|
|
243
|
+
```ts
|
|
244
|
+
const keys = createPersistedRecordKeys<ViewScope>({
|
|
245
|
+
namespace: "view-config",
|
|
246
|
+
resource: "view",
|
|
247
|
+
normalizeScope: (scope) => ({
|
|
248
|
+
userId: scope?.userId ?? "",
|
|
249
|
+
viewId: scope?.viewId ?? ""
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
keys.all;
|
|
254
|
+
keys.scope(scope);
|
|
255
|
+
keys.list(scope);
|
|
256
|
+
keys.latest(scope);
|
|
257
|
+
keys.history(scope, { limit: 20 });
|
|
258
|
+
keys.save(scope);
|
|
259
|
+
keys.create(scope);
|
|
260
|
+
keys.delete(scope);
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Форма:
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
[namespace, resource, normalizedScope, operation, optionalArgs]
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Используется нормализация `/resource`: strings trim, object keys сортируются, `undefined` становится `null`. Date/Map/cyclic object требуют явного нормализатора.
|
|
270
|
+
|
|
271
|
+
## JSON payload codec
|
|
272
|
+
|
|
273
|
+
### Parse
|
|
274
|
+
|
|
275
|
+
```ts
|
|
276
|
+
parsePersistedJson<ViewConfig>(null); // null.
|
|
277
|
+
parsePersistedJson<ViewConfig>(""); // null.
|
|
278
|
+
parsePersistedJson<ViewConfig>("broken"); // null.
|
|
279
|
+
parsePersistedJson<ViewConfig>('{"compact":true,"columns":[]}');
|
|
280
|
+
// Object, но без runtime validation.
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
`parsePersistedJson<T>` только вызывает `JSON.parse` и делает TypeScript cast. Он не проверяет schema. После parse примените domain validator/normalizer на явной restore boundary.
|
|
284
|
+
|
|
285
|
+
### Stringify
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
const payload = stringifyPersistedJson({ compact: false, columns: [] });
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Ошибки `JSON.stringify` пробрасываются. Передавайте только JSON-compatible данные. В частности:
|
|
292
|
+
|
|
293
|
+
- cyclic object и `BigInt` приводят к ошибке;
|
|
294
|
+
- `Date` превращается в ISO string;
|
|
295
|
+
- `undefined` в object-поле удаляется;
|
|
296
|
+
- вызов с корневым `undefined` фактически может вернуть `undefined`, несмотря на заявленный string contract — не передавайте его.
|
|
297
|
+
|
|
298
|
+
`createPersistedJsonCodec<T>()` возвращает `{ parse, stringify }` для передачи единым контрактом.
|
|
299
|
+
|
|
300
|
+
## REST adapters
|
|
301
|
+
|
|
302
|
+
### Query
|
|
303
|
+
|
|
304
|
+
`createPersistedRestQueryOperation` принимает:
|
|
305
|
+
|
|
306
|
+
- `buildUrl(scope, args)` — относительный или абсолютный URL;
|
|
307
|
+
- optional `baseUrl`;
|
|
308
|
+
- optional `buildInit(scope, args)`;
|
|
309
|
+
- `parseResponse(unknown)` или custom `executor`;
|
|
310
|
+
- optional `transform`;
|
|
311
|
+
- `staleTime`, `gcTime`, `isEnabled`.
|
|
312
|
+
|
|
313
|
+
Если нет ни `executor`, ни `parseResponse`, operation бросит `REST persisted operation requires executor or parseResponse.`
|
|
314
|
+
|
|
315
|
+
Read signal всегда записывается поверх `buildInit.signal`, чтобы текущая TanStack Query отменяла именно свой запрос.
|
|
316
|
+
|
|
317
|
+
### Mutation
|
|
318
|
+
|
|
319
|
+
`createPersistedRestMutationOperation` поддерживает method `POST`, `PUT`, `DELETE`.
|
|
320
|
+
|
|
321
|
+
- `bodyMapper(scope, input)` создаёт JSON body;
|
|
322
|
+
- если mapper отсутствует/вернул `undefined`, body и default Content-Type не задаются;
|
|
323
|
+
- иначе body сериализуется и default header равен `application/json`;
|
|
324
|
+
- `buildInit` применяется после default headers и может полностью заменить headers;
|
|
325
|
+
- response обязательно проходит `parseResponse` или custom executor;
|
|
326
|
+
- optional `transform` строит domain result;
|
|
327
|
+
- optional `cacheStrategy` прикрепляется к operation.
|
|
328
|
+
|
|
329
|
+
REST adapter использует нейтральный `/http`: SAP SSO/X-CSRF policy здесь нет.
|
|
330
|
+
|
|
331
|
+
## OData adapters
|
|
332
|
+
|
|
333
|
+
OData operations используют metadata-aware transport из [`/odata`](../odata/README.mdx).
|
|
334
|
+
|
|
335
|
+
### Query collection
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const list = createPersistedODataQueryOperation<
|
|
339
|
+
ViewScope,
|
|
340
|
+
void,
|
|
341
|
+
SavedViewRecord[]
|
|
342
|
+
>({
|
|
343
|
+
odata: { service: "Z_VIEW_CONFIG_SRV", target: "ViewSet" },
|
|
344
|
+
buildOptions: (scope) => ({
|
|
345
|
+
expression: createFilterEqual("UserId", scope.userId)
|
|
346
|
+
}),
|
|
347
|
+
transform: (rows) => rows
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Доступные options factory: `odata`, optional `baseUrl`, `buildParams`, `buildOptions`, `buildInit`, `transform`, `executor`, `staleTime`, `gcTime`, `meta`, `isEnabled`.
|
|
352
|
+
|
|
353
|
+
`createPersistedODataQueryOperation` использует semantic method `query` и возвращает collection response data. `createPersistedODataReadOperation` использует `read` для одной entity по key parameters:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
const latest = createPersistedODataReadOperation({
|
|
357
|
+
odata: { service: "Z_VIEW_CONFIG_SRV", target: "ViewSet" },
|
|
358
|
+
buildParams: (scope: ViewScope) => wrapODataParams({
|
|
359
|
+
UserId: scope.userId,
|
|
360
|
+
ViewId: scope.viewId
|
|
361
|
+
}),
|
|
362
|
+
transform: (record: SavedViewRecord) =>
|
|
363
|
+
parsePersistedJson<ViewConfig>(record.payload)
|
|
364
|
+
});
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
`buildParams` должен возвращать wrapped OData parameters из `foundation-lib/odata-service`.
|
|
368
|
+
|
|
369
|
+
### Mutation
|
|
370
|
+
|
|
371
|
+
```ts
|
|
372
|
+
const save = createPersistedODataMutationOperation({
|
|
373
|
+
odata: { service: "Z_VIEW_CONFIG_SRV", target: "ViewSet" },
|
|
374
|
+
method: "update",
|
|
375
|
+
buildParams: (scope: ViewScope) => wrapODataParams({
|
|
376
|
+
UserId: scope.userId,
|
|
377
|
+
ViewId: scope.viewId
|
|
378
|
+
}),
|
|
379
|
+
bodyMapper: (_scope, input: SaveViewInput) => ({
|
|
380
|
+
Payload: stringifyPersistedJson(input.payload)
|
|
381
|
+
}),
|
|
382
|
+
cacheStrategy: createInvalidatePersistedScopeCacheStrategy()
|
|
383
|
+
});
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
Semantic methods:
|
|
387
|
+
|
|
388
|
+
| Method | Params | Body |
|
|
389
|
+
| --- | --- | --- |
|
|
390
|
+
| `create` | запрещены | обязательный `bodyMapper` |
|
|
391
|
+
| `update` | обязательный `buildParams` | обязательный `bodyMapper` |
|
|
392
|
+
| `delete` | обязательный `buildParams` | запрещён |
|
|
393
|
+
|
|
394
|
+
Это OData operation names, а не raw HTTP strings. Не передавайте `"PUT"`/`"POST"`.
|
|
395
|
+
|
|
396
|
+
OData adapter наследует metadata validation, SAP cookies, SSO recovery и X-CSRF lifecycle. Query/read получают AbortSignal; mutations — нет в generic contract.
|
|
397
|
+
|
|
398
|
+
## Custom и server-function operations
|
|
399
|
+
|
|
400
|
+
Descriptor принимает любой object, соответствующий `PersistedQueryOperation`/`PersistedMutationOperation`. Server-function factories из [`/server-fn`](../server-fn/README.mdx) возвращают совместимые resource operations и могут быть использованы как capabilities, если их generic contract совпадает.
|
|
401
|
+
|
|
402
|
+
## Cache strategies
|
|
403
|
+
|
|
404
|
+
### Инвалидация всего scope
|
|
405
|
+
|
|
406
|
+
```ts
|
|
407
|
+
const invalidate = createInvalidatePersistedScopeCacheStrategy();
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
Инвалидирует prefix `descriptor.keys.scope(scope)`, поэтому затрагивает list/latest/history данного scope.
|
|
411
|
+
|
|
412
|
+
### Точечное обновление
|
|
413
|
+
|
|
414
|
+
```ts
|
|
415
|
+
const setLatest = createSetPersistedQueryDataCacheStrategy({
|
|
416
|
+
getQueryKey: ({ descriptor, scope }) => descriptor.keys.latest(scope),
|
|
417
|
+
update: (_current, { result }) => result
|
|
418
|
+
});
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
### Композиция
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const combined = composePersistedCacheStrategies(setLatest, invalidate);
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Порядок последовательный. Если strategy бросила ошибку, следующие steps и hook `onSuccess` не выполняются, хотя backend mutation уже могла завершиться.
|
|
428
|
+
|
|
429
|
+
`applyPersistedCacheStrategy` — низкоуровневый helper для ручного вызова optional strategy.
|
|
430
|
+
|
|
431
|
+
## Ошибки и безопасность
|
|
432
|
+
|
|
433
|
+
- Scope `null`/`undefined` или rejected `isEnabled` блокирует query и вызывает ошибку при фактическом execution.
|
|
434
|
+
- Network payload валидируйте в `parseResponse`/`transform`; TypeScript generic не является validator-ом.
|
|
435
|
+
- Не храните secrets в payload/query keys/client persistence.
|
|
436
|
+
- Save/create должны быть идемпотентны настолько, насколько это возможно для retry policy.
|
|
437
|
+
- Cache update — локальная проекция, backend остаётся source of truth.
|
|
438
|
+
- `parsePersistedJson` подавляет syntax error и возвращает `null`; различайте «нет payload» и «повреждён payload» на уровне продукта, если это важно.
|
|
439
|
+
|
|
440
|
+
## Полный API
|
|
441
|
+
|
|
442
|
+
| Группа | Exports |
|
|
443
|
+
| --- | --- |
|
|
444
|
+
| Descriptor/keys | `createPersistedResourceDescriptor`, `createPersistedRecordKeys` |
|
|
445
|
+
| React reads | `usePersistedListQuery`, `usePersistedLatestQuery`, `usePersistedHistoryQuery` |
|
|
446
|
+
| Imperative reads | `getPersistedListData`, `getPersistedLatestData`, `getPersistedHistoryData` |
|
|
447
|
+
| Mutations | `usePersistedSaveMutation`, `usePersistedCreateMutation`, `usePersistedDeleteMutation` |
|
|
448
|
+
| Payload | `parsePersistedJson`, `stringifyPersistedJson`, `createPersistedJsonCodec` |
|
|
449
|
+
| REST adapters | `createPersistedRestQueryOperation`, `createPersistedRestMutationOperation` |
|
|
450
|
+
| OData adapters | `createPersistedODataQueryOperation`, `createPersistedODataReadOperation`, `createPersistedODataMutationOperation` |
|
|
451
|
+
| Cache | `applyPersistedCacheStrategy`, `createInvalidatePersistedScopeCacheStrategy`, `createSetPersistedQueryDataCacheStrategy`, `composePersistedCacheStrategies` |
|
|
452
|
+
| Names/types | `PersistedReadOperationName`, `PersistedWriteOperationName`, `PersistedOperationName`, `PersistedPayloadCodec`, `PersistedRecordKeys` |
|
|
453
|
+
| Operation contracts | `PersistedQueryOperationContext`, `PersistedMutationOperationContext`, `PersistedQueryOperation`, `PersistedMutationOperation`, `PersistedOperationCapabilities`, `PersistedTransportAdapter` |
|
|
454
|
+
| Descriptor/cache contracts | `PersistedResourceDescriptor`, `CreatePersistedResourceDescriptorOptions`, `PersistedCacheStrategyContext`, `PersistedCacheStrategy`, `PersistedSetQueryDataStrategyOptions` |
|