@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.
Files changed (36) hide show
  1. package/README.md +32 -43
  2. package/dist/chunks/{odataFetchFn-vnAXC-c0.js → odataFetchFn-B9wSQpUS.js} +11 -11
  3. package/dist/chunks/{odataFetchFn-vnAXC-c0.js.map → odataFetchFn-B9wSQpUS.js.map} +1 -1
  4. package/dist/odata/fetchCollectionData.d.ts +1 -1
  5. package/dist/odata/fetchCollectionData.d.ts.map +1 -1
  6. package/dist/odata/index.js +111 -112
  7. package/dist/odata/index.js.map +1 -1
  8. package/dist/odata/projectODataCollectionSort.d.ts +1 -1
  9. package/dist/odata/projectODataCollectionSort.d.ts.map +1 -1
  10. package/dist/odata/types.d.ts +1 -2
  11. package/dist/odata/types.d.ts.map +1 -1
  12. package/dist/odata/useODataCollection.d.ts +1 -1
  13. package/dist/odata/useODataCollection.d.ts.map +1 -1
  14. package/dist/odata/useODataCollectionQuery.d.ts +1 -1
  15. package/dist/odata/useODataCollectionQuery.d.ts.map +1 -1
  16. package/dist/odata/useODataEntity.d.ts +1 -1
  17. package/dist/odata/useODataEntity.d.ts.map +1 -1
  18. package/dist/persisted/index.js +1 -1
  19. package/package.json +8 -3
  20. package/src/adt/README.mdx +164 -0
  21. package/src/async/README.mdx +253 -0
  22. package/src/error-report/README.mdx +148 -0
  23. package/src/foundationApi.mdx +123 -0
  24. package/src/http/README.mdx +221 -0
  25. package/src/odata/README.mdx +790 -0
  26. package/src/persisted/README.mdx +454 -0
  27. package/src/resource/README.mdx +358 -0
  28. package/src/server-fn/README.mdx +194 -0
  29. package/src/transport/README.mdx +183 -0
  30. package/src/README.md +0 -937
  31. package/src/async/README.md +0 -623
  32. package/src/async/async.mdx +0 -6
  33. package/src/odata/README.md +0 -761
  34. package/src/odata/odataFetchFn.mdx +0 -6
  35. package/src/persisted/README.md +0 -598
  36. 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` |