@ryuzaki13/react-foundation-api 1.1.16 → 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.
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 +461 -0
  21. package/src/async/README.mdx +628 -0
  22. package/src/error-report/README.mdx +471 -0
  23. package/src/foundationApi.mdx +123 -0
  24. package/src/http/README.mdx +570 -0
  25. package/src/odata/README.mdx +5142 -0
  26. package/src/persisted/README.mdx +1080 -0
  27. package/src/resource/README.mdx +820 -0
  28. package/src/server-fn/README.mdx +596 -0
  29. package/src/transport/README.mdx +528 -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,596 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation API/Adapters/Server Function" />
4
+
5
+ # Server-function adapters через `@ryuzaki13/react-foundation-api/server-fn`
6
+
7
+ Модуль адаптирует функцию с публичной формой `serverFn({ data })` к query/mutation operation из [`/resource`](../resource/README.mdx). Сам пакет не импортирует TanStack Start и не запускает сервер.
8
+
9
+ ## Модель
10
+
11
+ ```text
12
+ scope + args/input
13
+ │ buildData
14
+
15
+ { data }
16
+ │ serverFn или custom executor
17
+
18
+ response
19
+ │ optional transform
20
+
21
+ resource operation result
22
+ ```
23
+
24
+ Подходит любая async-функция такого контракта:
25
+
26
+ ```ts
27
+ type ServerFnTransport<TData, TResponse> = (
28
+ request: { data: TData }
29
+ ) => Promise<TResponse>;
30
+ ```
31
+
32
+ Название `server-fn` описывает типичный источник функции. Оно не гарантирует server-only выполнение и не создаёт security boundary.
33
+
34
+ Для начинающего разработчика:
35
+
36
+ - `serverFn` — transport function framework-а;
37
+ - `buildData` — явный mapper application inputs в transport DTO;
38
+ - `transform` — mapper transport response в application result;
39
+ - resource operation — описание того, как generic `/resource` должен выполнить query/mutation.
40
+
41
+ ```text
42
+ React component
43
+ │ useResourceQuery/useResourceMutation
44
+
45
+ resource descriptor
46
+ │ execute context
47
+
48
+ server-fn adapter
49
+ │ { data }
50
+
51
+ framework server function stub
52
+ │ network/server boundary framework-а
53
+
54
+ server handler
55
+ ```
56
+
57
+ Этот package адаптирует только участок `execute context → { data }`. Он не реализует network protocol framework-а.
58
+
59
+ ## Импорт
60
+
61
+ ```ts
62
+ import {
63
+ createServerFnMutationOperation,
64
+ createServerFnQueryOperation
65
+ } from "@ryuzaki13/react-foundation-api/server-fn";
66
+
67
+ import type {
68
+ ServerFnTransport,
69
+ ServerFnTransportExecutor,
70
+ ServerFnTransportExecutorContext,
71
+ ServerFnTransportRequest
72
+ } from "@ryuzaki13/react-foundation-api/server-fn";
73
+ ```
74
+
75
+ Нужен `@tanstack/react-query`; созданные operations обычно подключаются через `/resource` или `/persisted` descriptor.
76
+
77
+ Пакет не имеет root import. Не импортируйте внутренний файл:
78
+
79
+ ```ts
80
+ // Правильно.
81
+ import { createServerFnQueryOperation } from "@ryuzaki13/react-foundation-api/server-fn";
82
+
83
+ // Неправильно.
84
+ import { createServerFnQueryOperation } from "@ryuzaki13/react-foundation-api/src/server-fn/serverFn";
85
+ ```
86
+
87
+ ## Query operation
88
+
89
+ Предположим, сгенерированная framework-функция принимает:
90
+
91
+ ```ts
92
+ type LoadOrdersData = {
93
+ companyId: string;
94
+ search: string;
95
+ };
96
+
97
+ declare const loadOrdersServerFn: ServerFnTransport<
98
+ LoadOrdersData,
99
+ { items: OrderDto[] }
100
+ >;
101
+ ```
102
+
103
+ Adapter:
104
+
105
+ ```ts
106
+ const searchOrdersOperation = createServerFnQueryOperation({
107
+ serverFn: loadOrdersServerFn,
108
+ buildData: (companyId: string, args: { search: string }) => ({
109
+ companyId,
110
+ search: args.search
111
+ }),
112
+ transform: (response) => response.items.map(mapOrderDto),
113
+ staleTime: 60_000,
114
+ gcTime: 5 * 60_000,
115
+ isEnabled: (companyId, args) => Boolean(companyId && args.search.trim())
116
+ });
117
+ ```
118
+
119
+ `buildData` получает валидный scope и query args. `transform` получает response и `{ scope, args }`; без transform result равен response.
120
+
121
+ Созданный объект соответствует `ResourceQueryOperation`:
122
+
123
+ - `execute({ scope, args, client, signal })`;
124
+ - optional `isEnabled`;
125
+ - optional `staleTime`;
126
+ - optional `gcTime`.
127
+
128
+ ### Полный query lifecycle
129
+
130
+ ```text
131
+ Resource проверяет scope/isEnabled
132
+
133
+
134
+ operation.execute({ scope, args, client, signal })
135
+
136
+
137
+ buildData(scope, args)
138
+
139
+
140
+ executor(serverFn, { data }, { client, signal })
141
+
142
+
143
+ server response
144
+
145
+ ├─ transform задан ──► transform(response, { scope, args })
146
+ └─ transform нет ────► response
147
+ ```
148
+
149
+ `buildData` вызывается на каждом фактическом execute, а не при создании descriptor-а.
150
+
151
+ ### Scope, args и data — разные вещи
152
+
153
+ ```ts
154
+ type Scope = string; // companyId
155
+ type Args = { search: string; page: number };
156
+ type Data = {
157
+ company: string;
158
+ query: string;
159
+ offset: number;
160
+ };
161
+ ```
162
+
163
+ ```ts
164
+ buildData: (companyId, args) => ({
165
+ company: companyId,
166
+ query: args.search.trim(),
167
+ offset: args.page * 50
168
+ })
169
+ ```
170
+
171
+ Не передавайте args как data автоматически: имена/форматы transport DTO могут отличаться, а scope часто должен попасть внутрь request.
172
+
173
+ ### `isEnabled`
174
+
175
+ Callback получает nullable scope, потому что hook может быть вызван до готовности route/form state:
176
+
177
+ ```ts
178
+ isEnabled: (companyId, args) =>
179
+ Boolean(companyId && args.search.trim().length >= 2)
180
+ ```
181
+
182
+ Сам adapter просто сохраняет callback в operation. Проверку выполняет `/resource` при построении query options. Не вызывайте `operation.execute` вручную с invalid scope и не ожидайте, что `isEnabled` внутри остановит его.
183
+
184
+ ### `staleTime` и `gcTime`
185
+
186
+ Это hints TanStack Query lifecycle, а не transport timeout:
187
+
188
+ - `staleTime` — сколько snapshot считается fresh;
189
+ - `gcTime` — когда неактивный snapshot можно удалить;
190
+ - они не прерывают network request;
191
+ - они не определяют server cache.
192
+
193
+ ### Transform и runtime validation
194
+
195
+ ```ts
196
+ transform: (response, { scope, args }) => {
197
+ if (!response || !Array.isArray(response.items)) {
198
+ throw new Error("ServerFn response не содержит items");
199
+ }
200
+
201
+ return response.items.map((item) => mapOrderDto(item, scope, args));
202
+ }
203
+ ```
204
+
205
+ TypeScript generic не проверяет response framework-а. Если server/client могут иметь разные versions или endpoint получает внешние данные, выполняйте runtime validation.
206
+
207
+ Ошибка `buildData`, executor или transform становится query error.
208
+
209
+ ## Mutation operation
210
+
211
+ ```ts
212
+ declare const saveOrderServerFn: ServerFnTransport<
213
+ { companyId: string; order: SaveOrderDto },
214
+ { order: OrderDto }
215
+ >;
216
+
217
+ const saveOrderOperation = createServerFnMutationOperation({
218
+ serverFn: saveOrderServerFn,
219
+ buildData: (companyId: string, input: SaveOrderInput) => ({
220
+ companyId,
221
+ order: mapSaveOrder(input)
222
+ }),
223
+ transform: (response) => mapOrderDto(response.order),
224
+ cacheStrategy: createInvalidateResourceScopeCacheStrategy()
225
+ });
226
+ ```
227
+
228
+ `transform` получает `{ scope, input }`. `cacheStrategy` прикрепляется к operation и выполняется владельцем resource mutation после успешного ответа.
229
+
230
+ Mutation operation не имеет `AbortSignal` в generic resource contract. Custom executor получает `{ client }` без signal.
231
+
232
+ ### Полный mutation lifecycle
233
+
234
+ ```text
235
+ useResourceMutation(...).mutate(input)
236
+
237
+
238
+ operation.execute({ scope, input, client })
239
+
240
+
241
+ buildData(scope, input)
242
+
243
+
244
+ executor(serverFn, { data }, { client })
245
+
246
+
247
+ optional transform(response, { scope, input })
248
+
249
+
250
+ resource применяет cacheStrategy
251
+ ```
252
+
253
+ Cache strategy выполняется только после успешного `execute`. Если `transform` бросил error, mutation считается failed и strategy не должна применяться.
254
+
255
+ ### Body mapping без mutation input leakage
256
+
257
+ ```ts
258
+ type SaveOrderInput = {
259
+ localDraftId: string;
260
+ name: string;
261
+ amount: number;
262
+ };
263
+
264
+ buildData: (companyId, input) => ({
265
+ companyId,
266
+ order: {
267
+ name: input.name.trim(),
268
+ amount: input.amount
269
+ }
270
+ })
271
+ ```
272
+
273
+ `localDraftId` не уходит на server, если он не является частью transport contract.
274
+
275
+ ### Cache strategy выбирается явно
276
+
277
+ ```ts
278
+ cacheStrategy: createInvalidateResourceScopeCacheStrategy()
279
+ ```
280
+
281
+ или точечный `setQueryData` strategy из `/resource`. Adapter только прикрепляет strategy к operation; выполняет её resource mutation layer.
282
+
283
+ Без strategy server mutation может успешно завершиться, но cached lists/details останутся stale.
284
+
285
+ ## Подключение к resource descriptor
286
+
287
+ ```ts
288
+ const ordersResource = createResourceDescriptor({
289
+ namespace: "sales",
290
+ resource: "orders",
291
+ operations: {
292
+ queries: { search: searchOrdersOperation },
293
+ mutations: { save: saveOrderOperation }
294
+ }
295
+ });
296
+
297
+ const query = useResourceQuery(
298
+ ordersResource,
299
+ "search",
300
+ companyId,
301
+ { search }
302
+ );
303
+ ```
304
+
305
+ `server-fn` не экспортирует resource hooks повторно. Импортируйте их из `@ryuzaki13/react-foundation-api/resource`.
306
+
307
+ ### Полный descriptor example
308
+
309
+ ```ts
310
+ import {
311
+ createResourceDescriptor,
312
+ useResourceMutation,
313
+ useResourceQuery
314
+ } from "@ryuzaki13/react-foundation-api/resource";
315
+ import {
316
+ createServerFnMutationOperation,
317
+ createServerFnQueryOperation
318
+ } from "@ryuzaki13/react-foundation-api/server-fn";
319
+
320
+ const ordersResource = createResourceDescriptor({
321
+ namespace: "sales",
322
+ resource: "orders",
323
+ operations: {
324
+ queries: {
325
+ search: createServerFnQueryOperation({
326
+ serverFn: loadOrdersServerFn,
327
+ buildData: (companyId: string, args: { search: string }) => ({
328
+ companyId,
329
+ search: args.search
330
+ }),
331
+ transform: (response) => response.items.map(mapOrderDto),
332
+ isEnabled: (scope, args) => Boolean(scope && args.search.trim())
333
+ })
334
+ },
335
+ mutations: {
336
+ save: createServerFnMutationOperation({
337
+ serverFn: saveOrderServerFn,
338
+ buildData: (companyId: string, input: SaveOrderInput) => ({
339
+ companyId,
340
+ order: mapSaveOrder(input)
341
+ }),
342
+ transform: (response) => mapOrderDto(response.order),
343
+ cacheStrategy: createInvalidateResourceScopeCacheStrategy()
344
+ })
345
+ }
346
+ }
347
+ });
348
+
349
+ function Orders({ companyId, search }: { companyId: string; search: string }) {
350
+ const orders = useResourceQuery(
351
+ ordersResource,
352
+ "search",
353
+ companyId,
354
+ { search }
355
+ );
356
+
357
+ const save = useResourceMutation(
358
+ ordersResource,
359
+ "save",
360
+ companyId
361
+ );
362
+
363
+ // render loading/error/data and invoke save.mutate(input)
364
+ }
365
+ ```
366
+
367
+ ## Custom executor
368
+
369
+ По умолчанию adapter вызывает только:
370
+
371
+ ```ts
372
+ serverFn({ data })
373
+ ```
374
+
375
+ `QueryClient` и `AbortSignal` не передаются внутрь `serverFn`. Если transport/framework умеет использовать дополнительный context, задайте executor:
376
+
377
+ ```ts
378
+ const executor: ServerFnTransportExecutor<Input, Output> = async (
379
+ serverFn,
380
+ request,
381
+ { signal }
382
+ ) => {
383
+ if (signal?.aborted) throw signal.reason;
384
+ return serverFn(request);
385
+ };
386
+
387
+ const operation = createServerFnQueryOperation({
388
+ serverFn,
389
+ buildData,
390
+ executor
391
+ });
392
+ ```
393
+
394
+ Executor получает саму функцию, нормализованный request `{ data }` и `{ client, signal? }`. Он полезен для tracing, тестов и framework adapter-а. Не используйте его для бизнес-логики конкретной entity.
395
+
396
+ ### Default executor
397
+
398
+ Без option `executor` фактически выполняется:
399
+
400
+ ```ts
401
+ await serverFn({ data });
402
+ ```
403
+
404
+ `client` и `signal` доступны adapter layer, но не добавляются в transport data.
405
+
406
+ ### Tracing executor
407
+
408
+ ```ts
409
+ const tracingExecutor: ServerFnTransportExecutor<Input, Output> = async (
410
+ serverFn,
411
+ request,
412
+ context
413
+ ) => {
414
+ const startedAt = performance.now();
415
+
416
+ try {
417
+ if (context.signal?.aborted) {
418
+ throw context.signal.reason;
419
+ }
420
+
421
+ return await serverFn(request);
422
+ } finally {
423
+ console.debug("serverFn duration", performance.now() - startedAt);
424
+ }
425
+ };
426
+ ```
427
+
428
+ Не логируйте `request.data` целиком: он может содержать personal/auth-sensitive fields.
429
+
430
+ ### Test executor
431
+
432
+ Custom executor позволяет проверить request без real framework runtime:
433
+
434
+ ```ts
435
+ const executor = vi.fn(async (_serverFn, request) => ({
436
+ items: [{ id: request.data.companyId }]
437
+ }));
438
+
439
+ const operation = createServerFnQueryOperation({
440
+ serverFn: vi.fn(),
441
+ buildData: (scope: string, args: { search: string }) => ({
442
+ companyId: scope,
443
+ search: args.search
444
+ }),
445
+ executor
446
+ });
447
+ ```
448
+
449
+ ## Security boundary
450
+
451
+ Public client type `{ data }` не означает, что data доверена. Server handler обязан повторно выполнить:
452
+
453
+ - authentication;
454
+ - authorization на scope/entity/action;
455
+ - runtime schema validation;
456
+ - ограничения размера/частоты;
457
+ - sanitization перед DB/HTML/logs;
458
+ - безопасную обработку errors.
459
+
460
+ ```text
461
+ TypeScript type на client ≠ проверенный server input
462
+ ```
463
+
464
+ Не передавайте secrets server-а в `buildData`: этот mapper выполняется на client side operation boundary.
465
+
466
+ ## Query cache и server cache
467
+
468
+ Resource query key владеет identity client snapshot. ServerFn/framework может дополнительно кэшировать server response, но это другой слой.
469
+
470
+ После mutation:
471
+
472
+ - resource cache strategy синхронизирует QueryClient;
473
+ - server-side invalidation управляется framework/backend;
474
+ - одно не заменяет другое.
475
+
476
+ ## Тестирование
477
+
478
+ ### Query operation
479
+
480
+ ```ts
481
+ it("строит { data } и преобразует response", async () => {
482
+ const serverFn = vi.fn(async ({ data }) => ({
483
+ items: [{ id: data.companyId }]
484
+ }));
485
+
486
+ const operation = createServerFnQueryOperation({
487
+ serverFn,
488
+ buildData: (scope: string, args: { search: string }) => ({
489
+ companyId: scope,
490
+ search: args.search
491
+ }),
492
+ transform: (response) => response.items
493
+ });
494
+
495
+ const result = await operation.execute({
496
+ scope: "1000",
497
+ args: { search: "test" },
498
+ client: queryClient,
499
+ signal: new AbortController().signal
500
+ });
501
+
502
+ expect(serverFn).toHaveBeenCalledWith({
503
+ data: { companyId: "1000", search: "test" }
504
+ });
505
+ expect(result).toEqual([{ id: "1000" }]);
506
+ });
507
+ ```
508
+
509
+ Проверяйте:
510
+
511
+ - buildData получает scope и args/input;
512
+ - serverFn получает ровно `{ data }`;
513
+ - transform context правильный;
514
+ - без transform response возвращается как есть;
515
+ - query executor видит signal;
516
+ - mutation executor не получает signal;
517
+ - errors mapper/executor/transform пробрасываются;
518
+ - cache strategy прикреплена к mutation operation;
519
+ - `isEnabled`, `staleTime`, `gcTime` сохранены в query operation.
520
+
521
+ ## Частые ошибки
522
+
523
+ ### Выполнять serverFn при создании operation
524
+
525
+ Factory должна получить функцию, а не Promise/result.
526
+
527
+ ### Смешивать transport DTO и domain model
528
+
529
+ Используйте `buildData` и `transform` как явные boundaries.
530
+
531
+ ### Хранить бизнес-логику в generic executor
532
+
533
+ Executor — infrastructure wrapper. Entity mapping принадлежит operation/owner entity.
534
+
535
+ ### Ожидать cache update без strategy
536
+
537
+ Mutation operation сама не угадывает затронутые queries.
538
+
539
+ ### Передавать signal внутрь `{ data }`
540
+
541
+ AbortSignal не сериализуемый business payload. Используйте executor/framework capability.
542
+
543
+ ## Типичные ошибки
544
+
545
+ ### Передавать args напрямую
546
+
547
+ Неверно предполагать, что query args автоматически становятся data. Их явно преобразует `buildData`, потому что scope/args и transport DTO — разные контракты.
548
+
549
+ ### Ожидать автоматическую отмену
550
+
551
+ Default executor не передаёт signal в функцию, поскольку её публичный contract содержит только `{ data }`. Отмена TanStack Query остановит дальнейшее использование результата на уровне query, но transport должен поддерживать abort отдельно.
552
+
553
+ ### Считать функцию доверенной
554
+
555
+ Если функция вызывается из browser bundle, её input контролирует пользователь. Backend обязан повторно проверить authentication, authorization и payload schema.
556
+
557
+ ### Использовать без descriptor-а
558
+
559
+ Factory возвращает operation, а не готовый React hook. Подключите её к resource/persisted descriptor или вызовите `execute` через корректный orchestration layer.
560
+
561
+ ## FAQ
562
+
563
+ ### Обязательно ли использовать TanStack Start?
564
+
565
+ Нет. Подходит любая async function с contract `({ data }) => Promise<response>`.
566
+
567
+ ### Где выполняется `buildData`?
568
+
569
+ На стороне, где выполняется resource operation, обычно в client bundle.
570
+
571
+ ### Передаётся ли QueryClient в serverFn?
572
+
573
+ Нет. Только custom executor получает QueryClient как infrastructure context.
574
+
575
+ ### Работает ли query cancellation?
576
+
577
+ Signal доходит до executor. Default serverFn contract его не использует; реальный abort зависит от framework/custom executor.
578
+
579
+ ### Почему mutation не получает signal?
580
+
581
+ Таков generic `ResourceMutationOperation` contract. Если нужен abort, проектируйте отдельный controlled transport boundary.
582
+
583
+ ### Можно ли вернуть domain model из transform?
584
+
585
+ Да. Generic result type будет выведен из transform.
586
+
587
+ ## Полный API
588
+
589
+ | Export | Назначение |
590
+ | --- | --- |
591
+ | `createServerFnQueryOperation` | Создаёт read operation, optional transform/lifetime/isEnabled |
592
+ | `createServerFnMutationOperation` | Создаёт write operation, optional transform/cache strategy |
593
+ | `ServerFnTransport` | Контракт функции `(request) => Promise` |
594
+ | `ServerFnTransportRequest` | Обёртка `{ data }` |
595
+ | `ServerFnTransportExecutor` | Контракт custom execution |
596
+ | `ServerFnTransportExecutorContext` | `QueryClient` и optional signal |