@ryuzaki13/react-foundation-api 1.1.17 → 1.1.19

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryuzaki13/react-foundation-api",
3
- "version": "1.1.17",
3
+ "version": "1.1.19",
4
4
  "description": "Reusable React and TypeScript foundation API helpers and transport adapters.",
5
5
  "license": "MIT",
6
6
  "author": "Ryuzaki13",
@@ -117,12 +117,12 @@ Parser делает `trim` строковых полей, но не декоди
117
117
  ```ts
118
118
  interface UserTransport {
119
119
  transportNo: string;
120
- description?: string;
121
- owner?: string;
122
- targetSystem?: string;
123
- function?: string;
124
- status?: string;
125
- parentTransportNo?: string;
120
+ description: string | undefined;
121
+ owner: string | undefined;
122
+ targetSystem: string | undefined;
123
+ functionCode: string | undefined;
124
+ statusCode: string | undefined;
125
+ parentTransportNo: string | undefined;
126
126
  }
127
127
  ```
128
128
 
@@ -134,8 +134,8 @@ interface UserTransport {
134
134
  | `AS4TEXT` | `description` | Описание |
135
135
  | `AS4USER` | `owner` | Владелец |
136
136
  | `TARSYSTEM` | `targetSystem` | Целевая система |
137
- | `TRFUNCTION` | `function` | SAP transport function |
138
- | `TRSTATUS` | `status` | SAP status |
137
+ | `TRFUNCTION` | `functionCode` | SAP transport function code |
138
+ | `TRSTATUS` | `statusCode` | SAP status code |
139
139
  | `STRKORR` | `parentTransportNo` | Родительский request/task |
140
140
 
141
141
  Optional-поле может быть `undefined`. Проверяйте его перед отображением:
@@ -146,6 +146,281 @@ const label = transport.description
146
146
  : transport.transportNo;
147
147
  ```
148
148
 
149
+ ## Как устроен ADT response
150
+
151
+ ADT endpoint возвращает не OData JSON, а ABAP XML. Реальный документ обычно содержит namespace и несколько служебных обёрток:
152
+
153
+ ```xml
154
+ <?xml version="1.0" encoding="UTF-8"?>
155
+ <asx:abap xmlns:asx="http://www.sap.com/abapxml" version="1.0">
156
+ <asx:values>
157
+ <DATA>
158
+ <CTS_REQ_HEADER>
159
+ <TRKORR>DEVK900001</TRKORR>
160
+ <AS4TEXT>Исправление отчёта</AS4TEXT>
161
+ <AS4USER>IVANOV</AS4USER>
162
+ <TARSYSTEM>QAS</TARSYSTEM>
163
+ <TRFUNCTION>K</TRFUNCTION>
164
+ <TRSTATUS>D</TRSTATUS>
165
+ <STRKORR>DEVK900000</STRKORR>
166
+ </CTS_REQ_HEADER>
167
+ </DATA>
168
+ </asx:values>
169
+ </asx:abap>
170
+ ```
171
+
172
+ Parser намеренно не зависит от точного положения `CTS_REQ_HEADER`: он рекурсивно обходит objects и arrays. Поэтому дополнительная server wrapper-структура не ломает parsing, пока имена полезных tags остаются прежними.
173
+
174
+ ### Request parameters endpoint
175
+
176
+ ```text
177
+ _action=FIND
178
+ trfunction=K
179
+ ```
180
+
181
+ Они зафиксированы внутри `fetchUserTransports`. Consumer не может изменить owner, function, status или paging через options. Если нужен другой ADT query, создайте отдельный API у владельца package, а не подменяйте поведение этой функции string manipulation-ом.
182
+
183
+ ### Почему используется общий SAP transport
184
+
185
+ Хотя response не OData, `fetchUserTransports` использует `fetchBase` из `/odata`, потому что ему нужны те же infrastructure concerns:
186
+
187
+ - cookies текущей SAP session;
188
+ - `sap-language` и configured `sap-client`;
189
+ - manual redirect detection;
190
+ - automatic SSO recovery;
191
+ - единая HTTP error policy.
192
+
193
+ Base URL передаётся как пустая строка, потому что path уже начинается с `/sap/bc/adt`.
194
+
195
+ ## Подробный алгоритм parser-а
196
+
197
+ ### 1. Пустой документ
198
+
199
+ ```ts
200
+ parseUserTransportsXml("");
201
+ // []
202
+
203
+ parseUserTransportsXml(" \n\t");
204
+ // []
205
+ ```
206
+
207
+ Whitespace-only input не отправляется в XML parser.
208
+
209
+ ### 2. XML остаётся строковым
210
+
211
+ `fast-xml-parser` настроен с `parseTagValue: false`. Значения вроде `0001`, `D` или transport number не превращаются автоматически в numbers/booleans.
212
+
213
+ Это важно:
214
+
215
+ ```xml
216
+ <TRKORR>DEVK900001</TRKORR>
217
+ ```
218
+
219
+ должен остаться exact identifier, а не пройти numeric coercion.
220
+
221
+ ### 3. Поиск всех headers
222
+
223
+ Parser собирает каждый nested value по key `CTS_REQ_HEADER`. Один XML parser может представить:
224
+
225
+ - один header как object;
226
+ - несколько соседних headers как array;
227
+ - headers внутри дополнительных wrappers.
228
+
229
+ Оба случая нормализуются к одному массиву.
230
+
231
+ ### 4. Runtime narrowing
232
+
233
+ Любой найденный value сначала проверяется как record. Primitive/null не превращаются в transport.
234
+
235
+ ### 5. Нормализация text
236
+
237
+ Все SAP fields проходят text normalization:
238
+
239
+ ```text
240
+ " DEVK900001 " -> "DEVK900001"
241
+ " " -> undefined
242
+ null -> undefined
243
+ ```
244
+
245
+ Запись без `transportNo` отбрасывается полностью.
246
+
247
+ ### 6. Дедупликация
248
+
249
+ ```ts
250
+ parseUserTransportsXml(`
251
+ <root>
252
+ <CTS_REQ_HEADER>
253
+ <TRKORR>DEVK900001</TRKORR>
254
+ <AS4TEXT>Первое описание</AS4TEXT>
255
+ </CTS_REQ_HEADER>
256
+ <CTS_REQ_HEADER>
257
+ <TRKORR>DEVK900001</TRKORR>
258
+ <AS4TEXT>Второе описание</AS4TEXT>
259
+ </CTS_REQ_HEADER>
260
+ </root>
261
+ `);
262
+ ```
263
+
264
+ Вернёт первую запись. Parser не объединяет optional fields дубликатов.
265
+
266
+ ### 7. Fallback без `CTS_REQ_HEADER`
267
+
268
+ Некоторые responses содержат только nested `TRKORR`. Тогда parser формирует минимальные records:
269
+
270
+ ```ts
271
+ parseUserTransportsXml(`
272
+ <root>
273
+ <RESULT><TRKORR>DEVK900003</TRKORR></RESULT>
274
+ <RESULT><TRKORR>DEVK900004</TRKORR></RESULT>
275
+ </root>
276
+ `);
277
+ ```
278
+
279
+ ```ts
280
+ [
281
+ {
282
+ transportNo: "DEVK900003",
283
+ description: undefined,
284
+ owner: undefined,
285
+ targetSystem: undefined,
286
+ functionCode: undefined,
287
+ statusCode: undefined,
288
+ parentTransportNo: undefined
289
+ },
290
+ // ...
291
+ ]
292
+ ```
293
+
294
+ Fallback включается только если ни одного корректного header record не найдено.
295
+
296
+ ## Использование с TanStack Query
297
+
298
+ Модуль не навязывает query key/stale policy. Их определяет feature:
299
+
300
+ ```tsx
301
+ import { queryOptions, useQuery } from "@tanstack/react-query";
302
+ import { fetchUserTransports } from "@ryuzaki13/react-foundation-api/adt";
303
+
304
+ const adtTransportKeys = {
305
+ all: ["adt-transports"] as const,
306
+ list: () => [...adtTransportKeys.all, "list"] as const
307
+ };
308
+
309
+ export const adtTransportsQueryOptions = () =>
310
+ queryOptions({
311
+ queryKey: adtTransportKeys.list(),
312
+ queryFn: fetchUserTransports,
313
+ staleTime: 5 * 60 * 1000
314
+ });
315
+
316
+ function TransportSelect() {
317
+ const query = useQuery(adtTransportsQueryOptions());
318
+
319
+ if (query.isPending) return <p>Загрузка транспортов…</p>;
320
+ if (query.isError) return <p>{query.error.message}</p>;
321
+ if (query.data.length === 0) return <p>Доступных транспортов нет</p>;
322
+
323
+ return (
324
+ <select>
325
+ {query.data.map((item) => (
326
+ <option key={item.transportNo} value={item.transportNo}>
327
+ {item.transportNo}
328
+ {item.description ? ` — ${item.description}` : ""}
329
+ </option>
330
+ ))}
331
+ </select>
332
+ );
333
+ }
334
+ ```
335
+
336
+ `fetchUserTransports` не принимает `AbortSignal`, поэтому query cancellation не прерывает underlying fetch через этот API. Не обещайте UI мгновенный transport abort. Если это станет обязательным, контракт должен быть расширен в package.
337
+
338
+ Query key не должен содержать user secrets. Если список зависит от logged-in user, смена user/session должна сопровождаться очисткой auth-sensitive Query cache на project boundary.
339
+
340
+ ## Parent request и task
341
+
342
+ `parentTransportNo` обычно помогает отличить task от верхнеуровневого request:
343
+
344
+ ```ts
345
+ function getTransportLabel(transport: UserTransport) {
346
+ const kind = transport.parentTransportNo ? "Задача" : "Запрос";
347
+ return `${kind}: ${transport.transportNo}`;
348
+ }
349
+ ```
350
+
351
+ Это только presentation inference. Модуль не интерпретирует SAP status/function codes и не гарантирует бизнес-классификацию. Для правил конкретной системы используйте domain mapping приложения.
352
+
353
+ ## Тестирование
354
+
355
+ Parser — pure function, поэтому тестируйте fixtures без network:
356
+
357
+ ```ts
358
+ import { describe, expect, it } from "vitest";
359
+ import { parseUserTransportsXml } from "@ryuzaki13/react-foundation-api/adt";
360
+
361
+ it("сохраняет leading zeros и optional fields", () => {
362
+ const result = parseUserTransportsXml(`
363
+ <root>
364
+ <CTS_REQ_HEADER>
365
+ <TRKORR>DEVK900001</TRKORR>
366
+ <TRSTATUS>D</TRSTATUS>
367
+ </CTS_REQ_HEADER>
368
+ </root>
369
+ `);
370
+
371
+ expect(result).toEqual([
372
+ {
373
+ transportNo: "DEVK900001",
374
+ description: undefined,
375
+ owner: undefined,
376
+ targetSystem: undefined,
377
+ functionCode: undefined,
378
+ statusCode: "D",
379
+ parentTransportNo: undefined
380
+ }
381
+ ]);
382
+ });
383
+ ```
384
+
385
+ Минимальный набор cases:
386
+
387
+ - single header;
388
+ - array headers;
389
+ - nested wrappers/namespaces;
390
+ - duplicate number;
391
+ - whitespace fields;
392
+ - empty XML;
393
+ - fallback `TRKORR`;
394
+ - malformed XML и ожидаемая ошибка.
395
+
396
+ Для transport integration mock-айте `fetch`/MSW и проверяйте Content-Type `text/html`, XML body и HTTP error. Не используйте real SAP credentials в tests.
397
+
398
+ ## Частые ошибки
399
+
400
+ ### Использовать поля `function` и `status`
401
+
402
+ Правильные имена public contract — `functionCode` и `statusCode`.
403
+
404
+ ### Считать status code готовым label
405
+
406
+ `"D"` остаётся строкой `"D"`. Расшифровка зависит от SAP contract проекта и не выполняется package-ом.
407
+
408
+ ### Принимать пустой array за любую ошибку
409
+
410
+ Пустой или whitespace XML даёт `[]`, но HTTP, HTML и malformed XML errors выбрасываются. Обрабатывайте error отдельно от empty state.
411
+
412
+ ### Объединять ADT и UI2 records без mapper-а
413
+
414
+ `/adt` и `/transport` имеют разные source contracts. Если feature показывает их вместе, создайте явную domain model и collision policy.
415
+
416
+ ### Deep import parser-а
417
+
418
+ Используйте только:
419
+
420
+ ```ts
421
+ import { parseUserTransportsXml } from "@ryuzaki13/react-foundation-api/adt";
422
+ ```
423
+
149
424
  ## Ошибки и ограничения
150
425
 
151
426
  - `fast-xml-parser` должен быть установлен в host, использующем этот entrypoint.
@@ -155,6 +430,28 @@ const label = transport.description
155
430
  - HTML-response, HTTP error и XML parse error не превращаются в `[]`.
156
431
  - Дедупликация выполняется только по `transportNo` и сохраняет первую найденную запись.
157
432
 
433
+ ## FAQ
434
+
435
+ ### Можно ли передать другой endpoint?
436
+
437
+ Нет. `fetchUserTransports` — конкретный ADT use case с фиксированным endpoint.
438
+
439
+ ### Можно ли получить только transports конкретного owner?
440
+
441
+ Не через текущий public API. Фильтруйте result локально либо расширьте package contract, если server-side filter является общей потребностью.
442
+
443
+ ### Parser меняет исходную XML string?
444
+
445
+ Нет. Он создаёт новые JS objects.
446
+
447
+ ### Почему optional fields явно содержат `undefined`?
448
+
449
+ Таков стабильный `UserTransport` shape. Consumer может безопасно destructure поля без проверки их наличия как properties.
450
+
451
+ ### Работает ли в Node?
452
+
453
+ Pure parser работает при наличии `fast-xml-parser`. Network function зависит от общего SAP transport, URL/cookies/build constants и в первую очередь рассчитана на browser application.
454
+
158
455
  ## Полный API
159
456
 
160
457
  | Export | Назначение |