@ryuzaki13/react-foundation-api 1.1.17 → 1.1.18

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.
@@ -8,6 +8,14 @@ import { Meta } from "@storybook/addon-docs/blocks";
8
8
 
9
9
  > Здесь слово `transport` означает SAP transport request. Это не generic HTTP transport. Для обычного HTTP используйте [`/http`](../http/README.mdx), для SAP/OData transport infrastructure — [`/odata`](../odata/README.mdx).
10
10
 
11
+ Для разработчика без SAP-базы: transport request — контейнер, в котором изменения переносятся между SAP systems. Workbench и customizing — разные категории, поэтому package сохраняет `type` рядом с каждым id.
12
+
13
+ ```text
14
+ SAP UI2 WorkbenchRequests ──► normalize ──┐
15
+ ├─► TransportRequest[]
16
+ SAP UI2 CustomizingRequests ─► normalize ─┘
17
+ ```
18
+
11
19
  ## Импорт
12
20
 
13
21
  ```ts
@@ -49,6 +57,19 @@ type TransportRequestScope = TransportRequestType | "all";
49
57
 
50
58
  Без аргумента загружается только `workbench`.
51
59
 
60
+ ### Как выбрать scope
61
+
62
+ | Нужно UI | Scope |
63
+ | --- | --- |
64
+ | Только workbench request | `workbench` или аргумент не передан |
65
+ | Только customizing request | `customizing` |
66
+ | Единый список обеих категорий | `all` |
67
+
68
+ ```ts
69
+ await fetchTransportRequests();
70
+ // То же, что fetchTransportRequests("workbench")
71
+ ```
72
+
52
73
  ## Fetch functions
53
74
 
54
75
  | Функция | Endpoint | Результат |
@@ -70,6 +91,57 @@ type TransportRequestScope = TransportRequestType | "all";
70
91
 
71
92
  Если один из двух запросов `fetchUserTransportRequests` завершился ошибкой, `Promise.all` отклоняется и частичный object не возвращается.
72
93
 
94
+ ### Точные network paths
95
+
96
+ ```text
97
+ base URL odataUi2
98
+ ├── /TRANSPORT/WorkbenchRequests
99
+ └── /TRANSPORT/CustomizingRequests
100
+ ```
101
+
102
+ В dev/preview `odataUi2` обычно становится:
103
+
104
+ ```text
105
+ /sap-dp0/opu/odata/UI2
106
+ ```
107
+
108
+ В production:
109
+
110
+ ```text
111
+ /sap/opu/odata/UI2
112
+ ```
113
+
114
+ Итоговый URL определяется OData transport build/project configuration. Consumer не должен добавлять этот prefix вручную.
115
+
116
+ ### Что делает каждая function
117
+
118
+ `fetchWorkbenchTransportRequests()`:
119
+
120
+ 1. Выполняет UI2 OData GET.
121
+ 2. Снимает OData V2 envelope общим transport-ом.
122
+ 3. Если payload falsy, возвращает `[]`.
123
+ 4. Нормализует records с type `workbench`.
124
+
125
+ `fetchCustomizingTransportRequests()` делает то же с type `customizing`.
126
+
127
+ `fetchUserTransportRequests()` запускает обе функции параллельно через `Promise.all`, затем строит:
128
+
129
+ ```ts
130
+ {
131
+ workbench,
132
+ customizing,
133
+ all: [...workbench, ...customizing]
134
+ }
135
+ ```
136
+
137
+ `fetchTransportRequests(scope)` — удобный selector над этими calls.
138
+
139
+ ### AbortSignal
140
+
141
+ Public fetch functions не принимают `AbortSignal`. Их можно использовать как TanStack queryFn, но cancellation QueryClient не прерывает underlying request через этот API.
142
+
143
+ Если abort становится обязательным для общей инфраструктуры, расширьте package contract; не создавайте deep import низкоуровневого helper-а.
144
+
73
145
  ## Результат
74
146
 
75
147
  ```ts
@@ -91,6 +163,27 @@ interface UserTransportRequests {
91
163
  }
92
164
  ```
93
165
 
166
+ ### Поля результата
167
+
168
+ | Поле | Всегда есть | Смысл |
169
+ | --- | --- | --- |
170
+ | `id` | да | Нормализованный SAP request number |
171
+ | `type` | да | `workbench` либо `customizing` |
172
+ | `text` | да | Нормализованное description; может быть пустой строкой |
173
+ | `isDefaultRequest` | да | `true` только при strict input `true` |
174
+
175
+ Надёжный UI label:
176
+
177
+ ```ts
178
+ function getRequestLabel(request: TransportRequest) {
179
+ return request.text
180
+ ? `${request.id} — ${request.text}`
181
+ : request.id;
182
+ }
183
+ ```
184
+
185
+ Не используйте `text` как identity: description может измениться или быть пустым.
186
+
94
187
  ## `normalizeTransportRequests`
95
188
 
96
189
  Функция полезна отдельно для тестов или custom transport:
@@ -120,6 +213,58 @@ const normalized = normalizeTransportRequests(
120
213
 
121
214
  Нормализация не изменяет исходный array/objects.
122
215
 
216
+ ### Raw contract
217
+
218
+ ```ts
219
+ interface TransportRequestRaw {
220
+ id?: string | null;
221
+ description?: string | null;
222
+ isDefaultRequest?: boolean | null;
223
+ }
224
+ ```
225
+
226
+ Все поля могут отсутствовать. После normalization `id`, `text`, `type`, `isDefaultRequest` уже обязательны.
227
+
228
+ ### Пошаговый пример normalization
229
+
230
+ ```ts
231
+ const source = [
232
+ {
233
+ id: " DP0K970966 ",
234
+ description: " MIME типы ",
235
+ isDefaultRequest: true
236
+ },
237
+ {
238
+ id: "DP0K970966",
239
+ description: "Дубликат",
240
+ isDefaultRequest: false
241
+ },
242
+ {
243
+ id: null,
244
+ description: "Без id"
245
+ }
246
+ ];
247
+
248
+ const result = normalizeTransportRequests(source, "workbench");
249
+ ```
250
+
251
+ ```ts
252
+ [
253
+ {
254
+ id: "DP0K970966",
255
+ type: "workbench",
256
+ text: "MIME типы",
257
+ isDefaultRequest: true
258
+ }
259
+ ]
260
+ ```
261
+
262
+ Почему остаётся первая запись: функция сразу добавляет нормализованный id в `Set` и пропускает последующие совпадения.
263
+
264
+ ### Дубликаты между типами
265
+
266
+ Normalization выполняется отдельно для каждого endpoint. Одинаковый id может присутствовать один раз в workbench и один раз в customizing. Array `all` сохранит обе записи, потому что их semantic identity различается по `type`.
267
+
123
268
  ## Query keys
124
269
 
125
270
  ```ts
@@ -146,6 +291,60 @@ const query = useQuery({
146
291
 
147
292
  Factory ключей не создаёт query options и не передаёт AbortSignal; lifecycle запроса настраивает consumer.
148
293
 
294
+ ### Reusable query options
295
+
296
+ ```ts
297
+ import { queryOptions } from "@tanstack/react-query";
298
+ import {
299
+ fetchTransportRequests,
300
+ transportRequestKeys
301
+ } from "@ryuzaki13/react-foundation-api/transport";
302
+
303
+ export const transportRequestsQueryOptions = (
304
+ scope: TransportRequestScope = "workbench"
305
+ ) =>
306
+ queryOptions({
307
+ queryKey: transportRequestKeys.list(scope),
308
+ queryFn: () => fetchTransportRequests(scope),
309
+ staleTime: 5 * 60 * 1000
310
+ });
311
+ ```
312
+
313
+ `scope` обязательно входит в key. Иначе workbench/customizing/all начнут делить один cache slot.
314
+
315
+ ### React UI со всеми состояниями
316
+
317
+ ```tsx
318
+ function TransportRequestSelect({
319
+ scope = "workbench",
320
+ value,
321
+ onChange
322
+ }: {
323
+ scope?: TransportRequestScope;
324
+ value?: string;
325
+ onChange: (id: string) => void;
326
+ }) {
327
+ const query = useQuery(transportRequestsQueryOptions(scope));
328
+
329
+ if (query.isPending) return <p>Загрузка transport requests…</p>;
330
+ if (query.isError) return <p role="alert">{query.error.message}</p>;
331
+ if (query.data.length === 0) return <p>Доступных requests нет</p>;
332
+
333
+ return (
334
+ <select value={value ?? ""} onChange={(event) => onChange(event.target.value)}>
335
+ <option value="">Выберите transport request</option>
336
+ {query.data.map((request) => (
337
+ <option key={getTransportRequestKey(request)} value={request.id}>
338
+ {request.text ? `${request.id} — ${request.text}` : request.id}
339
+ </option>
340
+ ))}
341
+ </select>
342
+ );
343
+ }
344
+ ```
345
+
346
+ Если `scope === "all"`, form value только `id` может быть неоднозначным при collision разных types. В таком UI храните `{ type, id }` или stable key и явно преобразуйте его при submit.
347
+
149
348
  ## Stable key отдельной записи
150
349
 
151
350
  ```ts
@@ -155,6 +354,16 @@ getTransportRequestKey({ type: "workbench", id: "DEVK900001" });
155
354
 
156
355
  Включение `type` предотвращает collision одинаковых ID из разных списков. Функция не escaping-ит `:`; SAP transport ID обычно не содержит этот символ.
157
356
 
357
+ Stable key предназначен для React `key`, maps и UI identity:
358
+
359
+ ```ts
360
+ const map = new Map(
361
+ requests.map((request) => [getTransportRequestKey(request), request])
362
+ );
363
+ ```
364
+
365
+ Не отправляйте `workbench:DEVK...` в backend вместо request id, если backend ожидает только `id`.
366
+
158
367
  ## Отличие от `/adt`
159
368
 
160
369
  | Свойство | `/transport` | `/adt` |
@@ -166,6 +375,116 @@ getTransportRequestKey({ type: "workbench", id: "DEVK900001" });
166
375
 
167
376
  Не объединяйте records автоматически: контракты endpoint и значения полей различаются.
168
377
 
378
+ ADT result богаче техническими fields (`owner`, `statusCode`, `parentTransportNo`), но UI2 module явно разделяет workbench/customizing и знает default request. Выбор source зависит от backend use case, а не от того, какой endpoint оказался удобнее.
379
+
380
+ ## Обработка ошибок
381
+
382
+ Network и OData V2 errors пробрасываются caller-у. Function не превращает HTTP error в пустой array.
383
+
384
+ Для `fetchUserTransportRequests` действует fail-fast result contract:
385
+
386
+ ```text
387
+ workbench success + customizing error = Promise rejection
388
+ ```
389
+
390
+ Если product допускает partial UI, orchestrate calls отдельно на feature boundary:
391
+
392
+ ```ts
393
+ const [workbench, customizing] = await Promise.allSettled([
394
+ fetchWorkbenchTransportRequests(),
395
+ fetchCustomizingTransportRequests()
396
+ ]);
397
+ ```
398
+
399
+ После этого feature обязана явно показать partial state; не маскируйте failed category как настоящий пустой список.
400
+
401
+ ## Cache после создания transport-а
402
+
403
+ Этот module только читает lists. Если другой feature создаёт request через OData/ADT, после успеха инвалидируйте подходящий scope:
404
+
405
+ ```ts
406
+ await queryClient.invalidateQueries({
407
+ queryKey: transportRequestKeys.all
408
+ });
409
+ ```
410
+
411
+ Prefix `all` здесь означает «все query keys семейства transport requests», а не только scope `"all"`.
412
+
413
+ Точечно:
414
+
415
+ ```ts
416
+ await queryClient.invalidateQueries({
417
+ queryKey: transportRequestKeys.list("workbench")
418
+ });
419
+ ```
420
+
421
+ ## Тестирование
422
+
423
+ ### Pure normalization
424
+
425
+ ```ts
426
+ import { expect, it } from "vitest";
427
+ import { normalizeTransportRequests } from "@ryuzaki13/react-foundation-api/transport";
428
+
429
+ it("trim-ит поля и сохраняет первый duplicate", () => {
430
+ expect(
431
+ normalizeTransportRequests(
432
+ [
433
+ { id: " A ", description: " Первый ", isDefaultRequest: true },
434
+ { id: "A", description: " Второй " }
435
+ ],
436
+ "workbench"
437
+ )
438
+ ).toEqual([
439
+ {
440
+ id: "A",
441
+ type: "workbench",
442
+ text: "Первый",
443
+ isDefaultRequest: true
444
+ }
445
+ ]);
446
+ });
447
+ ```
448
+
449
+ ### Что проверить
450
+
451
+ - пустой/null/whitespace id удаляется;
452
+ - пустое description становится `""`;
453
+ - только strict `true` даёт default flag;
454
+ - duplicates сохраняют первую запись;
455
+ - workbench/customizing types проставляются argument-ом;
456
+ - `all` сохраняет порядок категорий;
457
+ - одинаковые ids разных types имеют разные stable keys;
458
+ - ошибка одного parallel endpoint отклоняет combined function.
459
+
460
+ Network tests должны mock-ать UI2 endpoints и OData envelope. Не используйте real SAP session в unit tests.
461
+
462
+ ## Частые ошибки
463
+
464
+ ### Использовать module как generic HTTP transport
465
+
466
+ Название относится к SAP transport requests. Для HTTP есть `/http`.
467
+
468
+ ### Не включить scope в query key
469
+
470
+ Каждый scope возвращает разные данные.
471
+
472
+ ### Использовать `id` как React key при scope `all`
473
+
474
+ Используйте `getTransportRequestKey`, чтобы учесть type.
475
+
476
+ ### Считать пустой `text` ошибкой
477
+
478
+ Public normalized contract допускает пустое description.
479
+
480
+ ### Ожидать partial result
481
+
482
+ Combined fetch использует `Promise.all` и fail-fast semantics.
483
+
484
+ ### Deep import OData transport
485
+
486
+ Application импортирует только `@ryuzaki13/react-foundation-api/transport`. Internal calls остаются detail package-а.
487
+
169
488
  ## Ошибки и ограничения
170
489
 
171
490
  - Модуль только читает requests; он не создаёт, не освобождает и не импортирует transport.
@@ -174,6 +493,32 @@ getTransportRequestKey({ type: "workbench", id: "DEVK900001" });
174
493
  - Нет автоматической TanStack Query cache policy — используйте экспортированные keys.
175
494
  - Runtime требует настроенного SAP proxy/base URLs и cookies.
176
495
 
496
+ ## FAQ
497
+
498
+ ### Как получить обе категории?
499
+
500
+ `fetchTransportRequests("all")` для flat array или `fetchUserTransportRequests()` для grouped object.
501
+
502
+ ### Какая категория загружается по умолчанию?
503
+
504
+ `workbench`.
505
+
506
+ ### Почему `all` может содержать одинаковый id дважды?
507
+
508
+ Если он пришёл из разных categories, records имеют разные `type` и считаются разными.
509
+
510
+ ### Можно ли создать transport request?
511
+
512
+ Нет. Модуль read-only.
513
+
514
+ ### Есть ли automatic query cache?
515
+
516
+ Нет. Используйте exported key factory вместе с TanStack Query.
517
+
518
+ ### Работает ли без SAP SSO?
519
+
520
+ Только если endpoint доступен и auth boundary настроена host environment. Package использует общий SAP transport с cookies/SSO policy.
521
+
177
522
  ## Полный API
178
523
 
179
524
  | Группа | Exports |