@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.
- package/package.json +1 -1
- package/src/adt/README.mdx +305 -8
- package/src/async/README.mdx +375 -0
- package/src/error-report/README.mdx +323 -0
- package/src/http/README.mdx +349 -0
- package/src/odata/README.mdx +4907 -555
- package/src/persisted/README.mdx +626 -0
- package/src/resource/README.mdx +462 -0
- package/src/server-fn/README.mdx +402 -0
- package/src/transport/README.mdx +345 -0
package/src/transport/README.mdx
CHANGED
|
@@ -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 |
|