@ryuzaki13/react-foundation-api 1.1.16 → 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 +2 -2
  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,790 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation API/OData/OData V2 and SAP Gateway" />
4
+
5
+ # OData V2 и SAP Gateway через `@ryuzaki13/react-foundation-api/odata`
6
+
7
+ Это основной entrypoint пакета для SAP/OData: transport с cookies, SSO и X-CSRF; OData V2 envelope; metadata-aware read/write operations; TanStack Query metadata; справочники; зависимые сегменты и table columns.
8
+
9
+ Документ описывает только публичный subpath. Внутренние файлы `odataFetchFn`, parser-ы и test utils не являются отдельными import paths.
10
+
11
+ ## Содержание
12
+
13
+ - [Что решает модуль](#что-решает-модуль)
14
+ - [Подготовка приложения](#подготовка-приложения)
15
+ - [Project adapter и base URLs](#project-adapter-и-base-urls)
16
+ - [Низкоуровневый transport](#низкоуровневый-transport)
17
+ - [`odataFetch` и query options](#odatafetch-и-query-options)
18
+ - [Metadata-aware operations](#metadata-aware-operations)
19
+ - [TanStack Query и mutations](#tanstack-query-и-mutations)
20
+ - [DEV dry-run helpers](#dev-dry-run-helpers)
21
+ - [Metadata queries](#metadata-queries)
22
+ - [OData collections](#odata-collections)
23
+ - [Зависимые segments](#зависимые-segments)
24
+ - [Table columns](#table-columns)
25
+ - [SSO и X-CSRF lifecycle](#sso-и-x-csrf-lifecycle)
26
+ - [Ошибки и ограничения](#ошибки-и-ограничения)
27
+ - [Полный публичный API](#полный-публичный-api)
28
+
29
+ ## Что решает модуль
30
+
31
+ ```text
32
+ application operation
33
+
34
+
35
+ OData metadata ──► target validation + URL + value parsing
36
+
37
+
38
+ SAP transport ───► cookies + sap-client + SSO + X-CSRF
39
+
40
+
41
+ OData V2 response ─► { data, totalCount? }
42
+ ```
43
+
44
+ Модуль не содержит бизнес-mapping конкретной entity, UI-компоненты и authorization rules backend-а. Фильтры/parameters/metadata types строятся pure helpers из `@ryuzaki13/react-foundation-lib/odata-service`.
45
+
46
+ Для обычного REST без SAP side effects используйте [`@ryuzaki13/react-foundation-api/http`](../http/README.mdx).
47
+
48
+ ## Подготовка приложения
49
+
50
+ ```bash
51
+ npm install @ryuzaki13/react-foundation-api @ryuzaki13/react-foundation-lib @tanstack/react-query react
52
+ ```
53
+
54
+ Для table adapter также нужен совместимый `@tanstack/react-table`.
55
+
56
+ Hooks работают внутри `QueryClientProvider`:
57
+
58
+ ```tsx
59
+ import { QueryClientProvider } from "@tanstack/react-query";
60
+ import { createQueryClient } from "@ryuzaki13/react-foundation-lib/query-client";
61
+
62
+ const queryClient = createQueryClient();
63
+
64
+ export function App() {
65
+ return <QueryClientProvider client={queryClient}>{/* приложение */}</QueryClientProvider>;
66
+ }
67
+ ```
68
+
69
+ Пакет рассчитан на host-сборку, которая определяет используемые compile-time constants (`__DEV__`, `__PREVIEW__`, SAP client, app id/origins/base config URL и build id). Их точная доставка относится к build-конфигурации приложения; это не runtime options одного запроса.
70
+
71
+ ## Project adapter и base URLs
72
+
73
+ До первого OData hook/fetch настройте глобальный adapter один раз при bootstrap:
74
+
75
+ ```ts
76
+ import { configureODataProjectAdapter } from "@ryuzaki13/react-foundation-api/odata";
77
+
78
+ configureODataProjectAdapter({
79
+ devDp0Service: "Z_SPECIAL_SRV",
80
+ resolveSapClient: () => appConfig.sapClient,
81
+ metadataVersion: {
82
+ service: "Z_TECHNICAL_SRV",
83
+ target: "MetadataVersionSet"
84
+ },
85
+ collectionUpdates: {
86
+ service: "Z_TECHNICAL_SRV",
87
+ target: "CollectionUpdatesSet"
88
+ }
89
+ });
90
+ ```
91
+
92
+ ```ts
93
+ type ODataProjectAdapter = {
94
+ devDp0Service?: string;
95
+ resolveSapClient?: () => string | null | undefined;
96
+ metadataVersion?: ODataProjectTechnicalEndpoint;
97
+ collectionUpdates?: ODataProjectTechnicalEndpoint;
98
+ };
99
+ ```
100
+
101
+ | Поле | Назначение |
102
+ | --- | --- |
103
+ | `devDp0Service` | Только этот service в dev/preview переводится на `odataDp0` |
104
+ | `resolveSapClient` | Production/общий runtime SAP client |
105
+ | `metadataVersion` | Technical entity со временем генерации metadata |
106
+ | `collectionUpdates` | Technical entity со справочниками, изменёнными за неделю |
107
+
108
+ Если technical endpoint не задан, соответствующий version/update query возвращает безопасный пустой результат без network request.
109
+
110
+ Adapter хранится в module-level state. Повторный вызов полностью заменяет object; merge не выполняется. `getODataProjectAdapter` возвращает текущий object.
111
+
112
+ ### Base URL types
113
+
114
+ ```ts
115
+ type BaseURLType = "odata" | "odataDp0" | "odataUi2" | "config" | "";
116
+ ```
117
+
118
+ | Type | Base path |
119
+ | --- | --- |
120
+ | `odata` | `/sap/opu/odata/sap` |
121
+ | `odataDp0` | dev/preview DP0 proxy, production обычный SAP OData path |
122
+ | `odataUi2` | `/sap/opu/odata/UI2` или dev/preview proxy |
123
+ | `config` | compile-time app config base URL |
124
+ | `""` | Ничего не добавлять к input path |
125
+
126
+ `resolveODataBaseUrl(service, baseUrl?)` учитывает `devDp0Service`, иначе возвращает явный `baseUrl` или `odata`. Service name нормализуется: suffix после `;` отбрасывается `normalizeODataServiceName`.
127
+
128
+ Низкоуровневые constants доступны как `BaseODataURL`, `BaseODataDp0URL`, `BaseODataUI2URL`, `BaseAppConfigURL`, `BaseUrlMap`. Рассматривайте `BaseUrlMap` как readonly library configuration.
129
+
130
+ ## Низкоуровневый transport
131
+
132
+ ### `fetchBase`
133
+
134
+ ```ts
135
+ const response = await fetchBase(
136
+ "/Z_SERVICE_SRV/Ping",
137
+ { method: "GET" },
138
+ "odata"
139
+ );
140
+ ```
141
+
142
+ Возвращает успешный raw `Response`. Для SAP request transport:
143
+
144
+ - добавляет `sap-language: ru`;
145
+ - добавляет `sap-contextid-accept: header`;
146
+ - добавляет `sap-client`, если client определён;
147
+ - ставит `Accept: application/json`, если consumer его не задал;
148
+ - всегда использует `credentials: include`;
149
+ - для write operation получает X-CSRF token;
150
+ - распознаёт redirect/HTML SSO и пытается восстановить session;
151
+ - при `401/403` write один раз очищает CSRF cache, получает новый token и повторяет запрос;
152
+ - на non-ok строит и бросает Error.
153
+
154
+ `RequestInitType` расширяет `RequestInit` внутренними флагами retry. Не выставляйте `retried`/`ssoRetried` в обычном прикладном коде.
155
+
156
+ ### `fetchODataJson`
157
+
158
+ ```ts
159
+ const response = await fetchODataJson<Order[]>(
160
+ "/Z_ORDER_SRV/OrderSet?$top=20",
161
+ {},
162
+ "odata"
163
+ );
164
+
165
+ console.log(response.data);
166
+ console.log(response.totalCount);
167
+ ```
168
+
169
+ Дополнительно выставляет OData V2 headers `DataServiceVersion: 2.0` и `MaxDataServiceVersion: 2.0`, разбирает JSON и снимает envelope:
170
+
171
+ | Server payload | Result |
172
+ | --- | --- |
173
+ | `{ d: { results: rows, __count } }` | `{ data: rows, totalCount }` |
174
+ | `{ d: record }` | `{ data: record }` |
175
+ | raw JSON | `{ data: raw }` |
176
+
177
+ Поле `__metadata` удаляется из каждого top-level record/одиночного `d`. Вложенные navigation records не очищаются рекурсивно.
178
+
179
+ Для `204/205` data содержит специальный marker. Проверяйте именно `response.data`:
180
+
181
+ ```ts
182
+ if (isNoContentResponse(response.data)) {
183
+ return;
184
+ }
185
+ ```
186
+
187
+ ### `fetchJson`
188
+
189
+ ```ts
190
+ const rows = await fetchJson<Order[]>("/Z_ORDER_SRV/OrderSet");
191
+ ```
192
+
193
+ Возвращает только `data`, поэтому `totalCount` теряется. Generic задаёт ожидаемый compile-time type, но не валидирует server payload.
194
+
195
+ ### `fetchQueryFn`
196
+
197
+ Factory для raw `Response` с обязательным async transform:
198
+
199
+ ```ts
200
+ const loadXml = fetchQueryFn("/sap/bc/example", {
201
+ baseUrl: "",
202
+ init: { headers: { Accept: "application/xml" } },
203
+ transform: (response) => response.text()
204
+ });
205
+
206
+ const xml = await loadXml({ signal });
207
+ ```
208
+
209
+ Переданный query signal используется только если `init.signal` отсутствует. Optional `swCache` добавляет `x-sw-cache`.
210
+
211
+ `fetchJsonQueryFn`, `fetchJsonMutationFn` и `fetchDeleteFn` экспортируются для существующих consumers, но помечены deprecated. Новый generic REST код должен использовать `/http`, metadata-aware OData код — специализированные operations ниже.
212
+
213
+ ## `odataFetch` и query options
214
+
215
+ `odataFetch` строит URL OData query, но не загружает metadata и не проверяет target:
216
+
217
+ ```ts
218
+ const response = await odataFetch<Order, Order[]>(
219
+ "/Z_ORDER_SRV/OrderSet",
220
+ {
221
+ select: ["Id", "Title"],
222
+ top: 50,
223
+ skip: 0,
224
+ sorts: [{ key: "Title" }],
225
+ expression: createFilterEqual("Status", "OPEN"),
226
+ inlinecount: "allpages",
227
+ swCache: "ttl=10m;name=orders"
228
+ },
229
+ { signal }
230
+ );
231
+ ```
232
+
233
+ `entitySetPath` должен содержать начальный `/`. Если в нём уже есть query string, новые options объединяются через `URLSearchParams`.
234
+
235
+ ### `ODataFetchOptions`
236
+
237
+ | Поле | OData option |
238
+ | --- | --- |
239
+ | `expression` | `$filter`, строится `buildODataFilter` |
240
+ | `sorts` | `$orderby`, строится `buildODataOrder` |
241
+ | `select` | `$select`, ключи через запятую |
242
+ | `expand` | `$expand`, ключи через запятую |
243
+ | `top` | `$top` |
244
+ | `skip` | `$skip` |
245
+ | `inlinecount` | `$inlinecount` |
246
+ | `baseUrl` | Выбор base URL |
247
+ | `swCache` | Header `x-sw-cache` |
248
+
249
+ Поле `format?: "json"` присутствует в текущем type, но request builder его не добавляет в URL. Не полагайтесь на него; JSON задаёт transport через Accept/OData parsing.
250
+
251
+ Arrays `select`/`expand` и path values не проходят metadata validation. Для filter values используйте фабрики из `foundation-lib/odata-service`, а не ручную конкатенацию.
252
+
253
+ ## Metadata-aware operations
254
+
255
+ Публичные factory-функции возвращают runner:
256
+
257
+ ```ts
258
+ type Runner<T> = (context: {
259
+ client: QueryClient;
260
+ signal?: AbortSignal;
261
+ }) => Promise<{ data: T; totalCount?: number }>;
262
+ ```
263
+
264
+ Каждый runner:
265
+
266
+ 1. загружает/cache-ит `$metadata` через QueryClient;
267
+ 2. находит target в entities или function imports;
268
+ 3. проверяет совместимость operation;
269
+ 4. строит target path по metadata;
270
+ 5. выполняет transport;
271
+ 6. optional парсит значения по metadata;
272
+ 7. optional вызывает `transform`.
273
+
274
+ ### Выбор operation
275
+
276
+ | Factory | Target | HTTP | Params | Body | Result |
277
+ | --- | --- | --- | --- | --- | --- |
278
+ | `odataQueryFn` | plain/parameterized Entity | GET | optional; у plain Entity игнорируются | нет | array или transform result |
279
+ | `odataReadFn` | non-parameterized Entity | GET | обязательны | нет | single |
280
+ | `odataCreateFn` | Entity | POST | запрещены | обязателен | single |
281
+ | `odataUpdateFn` | Entity | PUT | обязательны | обязателен | single |
282
+ | `odataDeleteFn` | Entity | DELETE | обязательны | нет | single/no-content contract backend-а |
283
+ | `odataFunctionImportFn` | FunctionImport | из metadata | обязательны | нет | single |
284
+
285
+ Body реально сериализуется только для truthy object/value. Для create/update используйте JSON object; `false`, `0`, `""` или `null` не подходят как body текущего helper-а.
286
+
287
+ ### Query списка
288
+
289
+ ```ts
290
+ import { odataQueryFn } from "@ryuzaki13/react-foundation-api/odata";
291
+ import { wrapODataParams } from "@ryuzaki13/react-foundation-lib/odata-service";
292
+
293
+ const loadOrders = odataQueryFn<OrderRaw, Order>({
294
+ odata: { service: "Z_ORDER_SRV", target: "OrderQuery" },
295
+ params: wrapODataParams({
296
+ CompanyId: "1000",
297
+ DateFrom: new Date("2026-01-01")
298
+ }),
299
+ options: {
300
+ top: 100,
301
+ select: ["Id", "Title", "CreatedAt"]
302
+ },
303
+ autoParse: true,
304
+ transform: (rows) => rows.map(mapOrder)
305
+ });
306
+ ```
307
+
308
+ Для parameterized entity metadata определяет suffix `/Set` или `/Results`. Для plain entity `params` игнорируются; в dev выводится warning. Чтение конкретного key делайте `odataReadFn`.
309
+
310
+ `transform` query может вернуть новый array или одно агрегированное значение:
311
+
312
+ ```ts
313
+ const countOrders = odataQueryFn<OrderRaw, number>({
314
+ odata,
315
+ transform: (rows) => rows.length
316
+ });
317
+ ```
318
+
319
+ ### Read, create, update, delete
320
+
321
+ ```ts
322
+ const readOrder = odataReadFn<OrderRaw, Order>({
323
+ odata: { service: "Z_ORDER_SRV", target: "OrderSet" },
324
+ params: wrapODataParams({ Id: "42" }),
325
+ autoParse: true,
326
+ transform: mapOrder
327
+ });
328
+
329
+ const createOrder = odataCreateFn<OrderRaw, Order, CreateOrderBody>({
330
+ odata: { service: "Z_ORDER_SRV", target: "OrderSet" },
331
+ body: { Title: "Новый заказ" },
332
+ transform: mapOrder
333
+ });
334
+
335
+ const updateOrder = odataUpdateFn<OrderRaw, Order, UpdateOrderBody>({
336
+ odata: { service: "Z_ORDER_SRV", target: "OrderSet" },
337
+ params: wrapODataParams({ Id: "42" }),
338
+ body: { Title: "Изменённый заказ" },
339
+ transform: mapOrder
340
+ });
341
+
342
+ const deleteOrder = odataDeleteFn<unknown>({
343
+ odata: { service: "Z_ORDER_SRV", target: "OrderSet" },
344
+ params: wrapODataParams({ Id: "42" })
345
+ });
346
+ ```
347
+
348
+ ### FunctionImport
349
+
350
+ ```ts
351
+ const runAction = odataFunctionImportFn<ActionRaw, ActionResult>({
352
+ odata: { service: "Z_ORDER_SRV", target: "CloseOrder" },
353
+ params: wrapODataParams({ Id: "42" }),
354
+ transform: mapActionResult
355
+ });
356
+ ```
357
+
358
+ Target обязан находиться в metadata как FunctionImport. HTTP method берётся оттуда; отсутствие method приводит к ошибке. Entity нельзя вызвать через `odataFunctionImportFn`, FunctionImport — через entity factory.
359
+
360
+ ### `autoParse`
361
+
362
+ При `autoParse: true` top-level fields преобразуются с помощью metadata columns, например OData date/number/ABAP-boolean-like значения. Для FunctionImport columns берутся из `resultEntity`, если она указана.
363
+
364
+ Parser:
365
+
366
+ - не валидирует наличие всех полей;
367
+ - не делает deep-recursive parsing navigation objects;
368
+ - сохраняет исходный object, если ни одно значение не изменилось;
369
+ - не заменяет domain validation/mapping.
370
+
371
+ ### `options`, `init`, `swCache`
372
+
373
+ Общие fields operation:
374
+
375
+ - `odata: { service, target }`;
376
+ - `options?: ODataFetchOptions`;
377
+ - `init?: RequestInit` без `signal/method/body`;
378
+ - `autoParse?: boolean`;
379
+ - `swCache?: string`;
380
+ - operation-specific `params`/`body`;
381
+ - optional `transform`.
382
+
383
+ Отдельный top-level `swCache` имеет приоритет добавления в итоговые query options. Signal приходит только в runner context.
384
+
385
+ ## TanStack Query и mutations
386
+
387
+ ### Query hook
388
+
389
+ Runner структурно совместим с TanStack Query queryFn:
390
+
391
+ ```tsx
392
+ const ordersQuery = useQuery({
393
+ queryKey: ["orders", companyId],
394
+ queryFn: odataQueryFn<OrderRaw, Order>({
395
+ odata: { service: "Z_ORDER_SRV", target: "OrderSet" },
396
+ options: {
397
+ expression: createFilterEqual("CompanyId", companyId)
398
+ },
399
+ transform: (rows) => rows.map(mapOrder)
400
+ })
401
+ });
402
+
403
+ const orders = ordersQuery.data?.data ?? [];
404
+ ```
405
+
406
+ Query data — object `{ data, totalCount? }`, не сам array. Query key обязан включать все значения, влияющие на params/options/result.
407
+
408
+ ### Mutation hook
409
+
410
+ Runner не является обычной `mutationFn(input)`: ему нужен `QueryClient`. Создайте operation из mutation input и вызовите runner явно:
411
+
412
+ ```tsx
413
+ const queryClient = useQueryClient();
414
+
415
+ const updateMutation = useMutation({
416
+ mutationFn: (input: UpdateOrderInput) =>
417
+ odataUpdateFn<OrderRaw, Order, UpdateOrderBody>({
418
+ odata: { service: "Z_ORDER_SRV", target: "OrderSet" },
419
+ params: wrapODataParams({ Id: input.id }),
420
+ body: mapUpdateBody(input),
421
+ transform: mapOrder
422
+ })({ client: queryClient }),
423
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["orders"] })
424
+ });
425
+ ```
426
+
427
+ Mutation cache не синхронизируется автоматически. Инвалидируйте/обновляйте query по policy ресурса.
428
+
429
+ ## DEV dry-run helpers
430
+
431
+ Экспортируются:
432
+
433
+ - `odataQueryFnDev`;
434
+ - `odataReadFnDev`;
435
+ - `odataCreateFnDev`;
436
+ - `odataUpdateFnDev`;
437
+ - `odataDeleteFnDev`;
438
+ - `odataFunctionImportFnDev`.
439
+
440
+ В `__DEV__` они:
441
+
442
+ 1. реально загружают metadata, если её нет в cache;
443
+ 2. валидируют target/operation;
444
+ 3. строят итоговый URL, headers preview и body;
445
+ 4. показывают warning notification и console group;
446
+ 5. не выполняют целевой data/write request;
447
+ 6. возвращают `{ data: [], totalCount: 0 }` для query или `{ data: undefined }` для single operation;
448
+ 7. не вызывают `transform`, потому что backend response отсутствует.
449
+
450
+ В non-dev build Dev helper делегирует обычной реальной operation. Поэтому это инструмент предварительной проверки запроса, а не mock API и не безопасная замена backend test.
451
+
452
+ Низкоуровневые `odataFetchFn`/`odataFetchFnDev` существуют внутри реализации, но не экспортируются публичным `/odata` entrypoint. Используйте специализированные helpers.
453
+
454
+ ## Metadata queries
455
+
456
+ ### `useODataMetadataQuery`
457
+
458
+ ```tsx
459
+ const metadataQuery = useODataMetadataQuery({
460
+ service: "Z_ORDER_SRV"
461
+ });
462
+ ```
463
+
464
+ Загружает `/<service>/$metadata`, разбирает XML через `foundation-lib/odata-service` и хранит metadata с `staleTime: Infinity`, `gcTime: Infinity`, persisted query meta.
465
+
466
+ Актуальность контролирует отдельный technical version query, если `metadataVersion` настроен в project adapter. Версия сравнивается с `dataUpdatedAt`; более новая server version инвалидирует только metadata нужного service.
467
+
468
+ ### `useODataMetadata`
469
+
470
+ ```tsx
471
+ const { metadata, metadataUpdatedAt, isLoading } = useODataMetadata({
472
+ service: "Z_ORDER_SRV",
473
+ target: "OrderSet"
474
+ });
475
+ ```
476
+
477
+ Возвращает metadata одной Entity. Если target является FunctionImport, после загрузки hook бросит error. Ошибка metadata query также преобразуется в error во время render; используйте подходящий Error Boundary.
478
+
479
+ ### Imperative и options API
480
+
481
+ | Export | Назначение |
482
+ | --- | --- |
483
+ | `fetchMetadata` | Query function raw metadata fetch/parse |
484
+ | `odataMetadataQueryOptions` | Общие TanStack query options |
485
+ | `getODataMetadataData` | `fetchQuery`, optional version sync, metadata result |
486
+ | `useODataMetadataVersionQuery` | Только technical version query |
487
+ | `applyODataMetadataVersion` | Применить уже полученную version к cache |
488
+
489
+ Ошибка optional technical version-check не лишает `getODataMetadataData` уже сохранённых metadata: sync error подавляется, затем metadata читается снова.
490
+
491
+ ## OData collections
492
+
493
+ Collections предназначены для code/text справочников с metadata semantic links.
494
+
495
+ ### Config
496
+
497
+ ```ts
498
+ const organizationOData = {
499
+ service: "Z_REFERENCE_SRV",
500
+ target: "OrganizationSet",
501
+ limitedKeys: ["OrganizationCode"],
502
+ sortByCode: true,
503
+ excludeEmpty: true,
504
+ swCache: "ttl=forever;name=ref"
505
+ } satisfies ODataCollectionConfig;
506
+ ```
507
+
508
+ | Поле | Назначение |
509
+ | --- | --- |
510
+ | `service`, `target` | OData entity |
511
+ | `limitedKeys` | Ограничить выбранные code fields; linked text fields добавляются автоматически |
512
+ | `serverFilter` | `$filter` на сервере |
513
+ | `clientFilter` | Filter после загрузки |
514
+ | `excludeEmpty` | Исключение пустых code values в separated arrays |
515
+ | `sortByCode` | `false` создаёт observer projection по text; cache остаётся canonical по code |
516
+ | `hideCode` | UI contract для consumer controls |
517
+ | `swCache` | SW policy, default `ttl=forever;name=ref` |
518
+
519
+ ### `useODataCollectionQuery`
520
+
521
+ ```tsx
522
+ const collectionQuery = useODataCollectionQuery<OrganizationItem>(organizationOData);
523
+
524
+ const items = collectionQuery.data?.items ?? [];
525
+ const textKey = collectionQuery.data?.keyPairsMap.OrganizationCode;
526
+ ```
527
+
528
+ Результат:
529
+
530
+ ```ts
531
+ interface ODataCollectionResult<T> {
532
+ items: T[];
533
+ keyPairs: CollectionPair[];
534
+ keyPairsMap: Record<string, string>;
535
+ separated: Record<string, T[]>;
536
+ chain: { codeKey: string; count: number }[];
537
+ count: number;
538
+ cacheUpdatedAt: number;
539
+ }
540
+ ```
541
+
542
+ При загрузке служебные top-level fields `ID`, `CNT`, `__metadata`, `__Parameters` удаляются.
543
+
544
+ Критические cache rules:
545
+
546
+ - query key включает `service`, `target`, `limitedKeys`, `serverFilter` и внутреннюю version;
547
+ - `sortByCode` не входит в key намеренно: text ordering — observer `select`;
548
+ - `clientFilter` и `excludeEmpty` тоже не входят в key, хотя влияют на cached snapshot;
549
+ - поэтому для одинакового service/target/limitedKeys/serverFilter все consumers должны использовать совместимые `clientFilter`/`excludeEmpty`; иначе первый fetch может задать общий snapshot с неожиданным составом;
550
+ - `swCache` не является data identity.
551
+
552
+ Внутренняя collection loader сейчас логирует transport/parse error и возвращает пустой array. Поэтому query может иметь `isSuccess === true` и `items.length === 0` даже после ошибки сети. Если UI обязан отличать «пусто» от «ошибка», не используйте collection facade как единственный error signal или добавьте отдельную диагностику владельца.
553
+
554
+ ### SW cache и update list
555
+
556
+ Default policies:
557
+
558
+ ```text
559
+ collection data: ttl=forever;name=ref
560
+ updates list: ttl=4h;name=ref-updates
561
+ bust: bust=forever;name=ref
562
+ ```
563
+
564
+ Technical `collectionUpdates` возвращает entities, изменённые за последние семь дней. Collection сравнивает `lastChangedAt`, metadata update time, coverage window и `cacheUpdatedAt`. Если snapshot устарел, active query инвалидируется, а TTL policy преобразуется в bust.
565
+
566
+ Экспортированы `useODataCollectionUpdatesQuery`, `odataCollectionUpdatesQueryOptions`, `fetchODataCollectionUpdates`, key factory, constants, `resolveODataCollectionSwCachePolicy` и `shouldRefreshODataCollectionCache` для тестов/диагностики.
567
+
568
+ ### `useODataCollectionModel`
569
+
570
+ ```tsx
571
+ const model = useODataCollectionModel({
572
+ codeKey: "OrganizationCode",
573
+ minSearchTextLength: 2,
574
+ maxVisibleItems: 100
575
+ });
576
+ ```
577
+
578
+ Заполняет отсутствующие значения default-ами store из `foundation-lib/odata` и фиксирует snapshot при первом render. Изменение входного model после mount не пересоздаёт normalized model.
579
+
580
+ ### `useODataCollection`
581
+
582
+ ```tsx
583
+ const collection = useODataCollection({
584
+ odata: organizationOData,
585
+ model,
586
+ pageSize: 50
587
+ });
588
+
589
+ const visible = collection.getItems(
590
+ (code, text) => code.includes(search) || text.includes(search),
591
+ selectedItems
592
+ );
593
+ ```
594
+
595
+ Возвращает indexes и UI helpers:
596
+
597
+ - `itemsMap`, `orderedKeys`, `separatedItems`;
598
+ - `getItems(predicate?, selected?)` с `maxVisibleItems`;
599
+ - `getPage(pageIndex)`;
600
+ - `findSourceItemsByKeys(field, keys)`;
601
+ - `setFilteredItems` для dependency filter;
602
+ - `buildSearchIndex(field)` по prefixes длиной 1–5;
603
+ - `debounce(key, fn, delay)`;
604
+ - `codeKey`, `textKey`, `totalCount`, status.
605
+
606
+ Параметр `filter` присутствует в type props, но текущая реализация его не применяет. Не полагайтесь на него; используйте `clientFilter` config или predicate `getItems`. `refetch` и `invalidate` результата deprecated.
607
+
608
+ `useODataCollectionChains` подписывается на QueryCache для набора collection keys и возвращает map chain по `service.target`.
609
+
610
+ ### Query key factories
611
+
612
+ ```ts
613
+ createODataCollectionBaseQueryKey();
614
+ createODataCollectionQueryKey(config);
615
+ ```
616
+
617
+ Используйте их для точной инвалидации вместо ручного копирования array key.
618
+
619
+ ## Зависимые segments
620
+
621
+ Типы `ODataDependentBaseProps`, `ODataDependentSegment`, `ODataDependentSegmentItem` описывают несколько связанных справочников/levels.
622
+
623
+ ```ts
624
+ const items = flattenODataDependentServices(services);
625
+ ```
626
+
627
+ Каждый segment превращается в отдельный item с:
628
+
629
+ - `id` = codeKey;
630
+ - `serviceKey` = `service.target`;
631
+ - исходными `serviceIndex`/`segmentIndex`;
632
+ - normalized model с `codeKey`;
633
+ - `panelVisibility`, default `user`.
634
+
635
+ ```ts
636
+ const ordered = sortODataDependentSegmentItemsByChains(
637
+ items,
638
+ chains,
639
+ userOrderIds
640
+ );
641
+ ```
642
+
643
+ Сначала учитывается service order и server chain, затем optional user override поднимает валидные уникальные IDs вверх, сохраняя относительный порядок остальных.
644
+
645
+ `sortODataDependentServicesByChains` выполняет старую service-level форму и deprecated. Для нового кода используйте flatten + `sortODataDependentSegmentItemsByChains`.
646
+
647
+ ## Table columns
648
+
649
+ `useODataTableColumns` связывает OData entity metadata с table helpers из `foundation-lib/table`.
650
+
651
+ ### Build mode
652
+
653
+ ```tsx
654
+ const { columns, defaultColumnVisibility, isLoading } = useODataTableColumns<Order>({
655
+ mode: "build",
656
+ service: "Z_ORDER_SRV",
657
+ target: "OrderSet",
658
+ resolveHeader: (column) => column.label ?? column.id,
659
+ resolveVisible: (column) => !column.hidden,
660
+ resolveFormatting: (column) => resolveFormatting(column)
661
+ });
662
+ ```
663
+
664
+ Генерирует leaf columns по metadata и стартовую visibility map.
665
+
666
+ ### Enrich mode
667
+
668
+ ```tsx
669
+ const result = useODataTableColumns<Order>({
670
+ mode: "enrich",
671
+ service: "Z_ORDER_SRV",
672
+ target: "OrderSet",
673
+ columns: applicationColumns
674
+ });
675
+ ```
676
+
677
+ Сохраняет существующую структуру columns, добавляет metadata formatting role/type и возвращает visibility только для совпавших stable leaf IDs. Пока metadata грузятся, build mode возвращает `[]`, enrich mode — shallow copy исходных columns.
678
+
679
+ ## SSO и X-CSRF lifecycle
680
+
681
+ ### X-CSRF
682
+
683
+ Для SAP write method (не GET/HEAD) transport:
684
+
685
+ 1. определяет CSRF scope по service/base URL/SAP client;
686
+ 2. переиспользует token из module cache;
687
+ 3. объединяет параллельные token fetch одного scope;
688
+ 4. делает GET с `x-csrf-token: Fetch`;
689
+ 5. добавляет token к write;
690
+ 6. на `401/403` один раз сбрасывает token и повторяет write.
691
+
692
+ Token cache живёт в памяти tab/process. Публичного reset API нет.
693
+
694
+ ### SSO
695
+
696
+ Transport распознаёт opaque redirect или HTML form с `SAMLRequest`/`SAMLResponse`, создаёт `SsoRequiredError` и пытается восстановить session в hidden iframe. Параллельные requests ждут общий recovery gate.
697
+
698
+ ```ts
699
+ try {
700
+ await fetchJson("/Z_SERVICE_SRV/EntitySet");
701
+ } catch (error) {
702
+ if (error instanceof SsoRequiredError) {
703
+ showLoginExpired(error.message);
704
+ }
705
+ }
706
+ ```
707
+
708
+ `SsoRequiredError` содержит `form?`, `recoveryUrl?`, `recoverable`. После неуспешного automatic recovery ошибка становится unrecoverable до успешного общего recovery/session reset flow.
709
+
710
+ Низкоуровневые helpers:
711
+
712
+ - `isSsoForm(form)` проверяет наличие SAML input;
713
+ - `submitSsoForm(form)` вручную submit-ит форму в hidden iframe и возвращает boolean;
714
+ - `recoverSsoSession({ form, recoveryUrl })` дедуплицирует recovery Promise.
715
+
716
+ Они требуют browser `window`/`document`; на server recovery возвращает `false`.
717
+
718
+ ## Ошибки и ограничения
719
+
720
+ ### HTTP/error body
721
+
722
+ - JSON error пытается собрать уникальные `error.code`, `error.message`, `details`, `message`.
723
+ - Непустой text становится Error message.
724
+ - HTML SAML form запускает SSO flow.
725
+ - Неожиданный HTML без SAML form регистрируется как error report и отклоняется.
726
+ - Unsupported non-JSON text/content type отклоняется.
727
+ - Network/CORS/abort errors пробрасываются.
728
+
729
+ ### Практические ограничения
730
+
731
+ - Generic types не проверяют payload; используйте runtime validator на boundary.
732
+ - Metadata cache бесконечный по времени и зависит от optional version endpoint или ручной invalidation.
733
+ - OData transport в первую очередь browser/SAP-oriented: cookies, proxy, compile-time constants и iframe SSO должны быть настроены host-ом.
734
+ - Не помещайте персональные/секретные response в общий SW cache без изоляции.
735
+ - FunctionImport side effects определяются metadata HTTP method; query/mutation cache policy задаёт consumer.
736
+ - `odataFetch` не знает metadata и не заменяет specialized operations для сложных entity paths.
737
+ - Collection facade подавляет loader errors в пустой result — учитывайте это в UX.
738
+ - `submitSsoForm` — низкоуровневый browser side effect; обычный consumer должен позволить transport самому управлять recovery.
739
+
740
+ ## Полный публичный API
741
+
742
+ ### Transport и URL
743
+
744
+ | Группа | Exports |
745
+ | --- | --- |
746
+ | Fetch | `fetchBase`, `fetchODataJson`, `fetchJson`, `fetchQueryFn`, `odataFetch`, `isNoContentResponse` |
747
+ | Deprecated fetch factories | `fetchJsonQueryFn`, `fetchJsonMutationFn`, `fetchDeleteFn` |
748
+ | Base URL | `BaseODataURL`, `BaseODataDp0URL`, `BaseODataUI2URL`, `BaseAppConfigURL`, `BaseUrlMap`, `BaseURLType`, `resolveODataBaseUrl`, `normalizeODataServiceName`, `getInputUrl`, `normalizeRelativePath` |
749
+ | Request types | `RequestInitType`, `FetchErrorReportContext`, `ODataFetchOptions` |
750
+
751
+ ### SSO и project adapter
752
+
753
+ | Группа | Exports |
754
+ | --- | --- |
755
+ | SSO | `SsoRequiredError`, `SsoForm`, `SsoRecoveryArgs`, `SsoRequiredErrorOptions`, `isSsoForm`, `submitSsoForm`, `recoverSsoSession` |
756
+ | Adapter | `configureODataProjectAdapter`, `getODataProjectAdapter`, `resolveODataSapClient`, `ODataProjectAdapter`, `ODataProjectTechnicalEndpoint` |
757
+
758
+ ### Metadata-aware operations
759
+
760
+ | Группа | Exports |
761
+ | --- | --- |
762
+ | Real | `odataQueryFn`, `odataReadFn`, `odataCreateFn`, `odataUpdateFn`, `odataDeleteFn`, `odataFunctionImportFn` |
763
+ | DEV dry-run | `odataQueryFnDev`, `odataReadFnDev`, `odataCreateFnDev`, `odataUpdateFnDev`, `odataDeleteFnDev`, `odataFunctionImportFnDev` |
764
+ | Public request type | `ODataFetchFnRequest` |
765
+
766
+ `odataFetchFn` и `odataFetchFnDev` намеренно не находятся в public index.
767
+
768
+ ### Metadata
769
+
770
+ | Exports | Назначение |
771
+ | --- | --- |
772
+ | `fetchMetadata`, `odataMetadataQueryOptions`, `useODataMetadataQuery`, `getODataMetadataData` | Service metadata lifecycle |
773
+ | `useODataMetadata`, `ODataEntity`, `useODataEntity` | Entity metadata/actions |
774
+ | `useODataMetadataVersionQuery`, `applyODataMetadataVersion` | Technical version integration |
775
+
776
+ `useODataEntity` возвращает actions `getEntityPath`, `getKeys`, `getKeyPairs`, `getKeyPairsMap`; actions нельзя вызывать до окончания metadata loading.
777
+
778
+ ### Collections и dependent UI contracts
779
+
780
+ | Группа | Exports |
781
+ | --- | --- |
782
+ | Query | `useODataCollectionQuery`, `createODataCollectionBaseQueryKey`, `createODataCollectionQueryKey`, `resolveODataCollectionSwCachePolicy`, `shouldRefreshODataCollectionCache` |
783
+ | Model | `useODataCollection`, `useODataCollectionModel`, `useODataCollectionChains` |
784
+ | Updates | `useODataCollectionUpdatesQuery`, `odataCollectionUpdatesQueryOptions`, `fetchODataCollectionUpdates`, `createODataCollectionUpdatesQueryKey`, `ODATA_COLLECTION_UPDATES_LOOKBACK_MS`, `ODATA_COLLECTION_DEFAULT_SW_CACHE`, `ODATA_COLLECTION_BUST_SW_CACHE`, `ODATA_COLLECTION_UPDATES_SW_CACHE` |
785
+ | Dependent | `flattenODataDependentServices`, `sortODataDependentSegmentItemsByChains`, deprecated `sortODataDependentServicesByChains` |
786
+ | Types | `ControlPanelVisibility`, `ODataServiceCollectionConfig`, `ODataCollectionConfig`, `ODataCollectionResult`, `ODataCollectionSegment`, `ODataCollectionModel`, `ODataCollectionProps`, `ODataSelectBaseProps`, `ODataSingleSelectProps`, `ODataDependentSegment`, `ODataDependentSegments`, `ODataDependentBaseProps`, `ODataDependentSegmentItem`, `ODataCollectionUpdateItem`, `ODataCollectionUpdatesResult`, `ODataCollectionUpdatesOptions` |
787
+
788
+ ### Tables
789
+
790
+ `useODataTableColumns`, `UseODataTableColumnsBuildConfig`, `UseODataTableColumnsEnrichConfig`, `UseODataTableColumnsConfig`, `UseODataTableColumnsResult`.