@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/server-fn/README.mdx
CHANGED
|
@@ -31,6 +31,31 @@ type ServerFnTransport<TData, TResponse> = (
|
|
|
31
31
|
|
|
32
32
|
Название `server-fn` описывает типичный источник функции. Оно не гарантирует server-only выполнение и не создаёт security boundary.
|
|
33
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
|
+
|
|
34
59
|
## Импорт
|
|
35
60
|
|
|
36
61
|
```ts
|
|
@@ -49,6 +74,16 @@ import type {
|
|
|
49
74
|
|
|
50
75
|
Нужен `@tanstack/react-query`; созданные operations обычно подключаются через `/resource` или `/persisted` descriptor.
|
|
51
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
|
+
|
|
52
87
|
## Query operation
|
|
53
88
|
|
|
54
89
|
Предположим, сгенерированная framework-функция принимает:
|
|
@@ -90,6 +125,87 @@ const searchOrdersOperation = createServerFnQueryOperation({
|
|
|
90
125
|
- optional `staleTime`;
|
|
91
126
|
- optional `gcTime`.
|
|
92
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
|
+
|
|
93
209
|
## Mutation operation
|
|
94
210
|
|
|
95
211
|
```ts
|
|
@@ -113,6 +229,59 @@ const saveOrderOperation = createServerFnMutationOperation({
|
|
|
113
229
|
|
|
114
230
|
Mutation operation не имеет `AbortSignal` в generic resource contract. Custom executor получает `{ client }` без signal.
|
|
115
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
|
+
|
|
116
285
|
## Подключение к resource descriptor
|
|
117
286
|
|
|
118
287
|
```ts
|
|
@@ -135,6 +304,66 @@ const query = useResourceQuery(
|
|
|
135
304
|
|
|
136
305
|
`server-fn` не экспортирует resource hooks повторно. Импортируйте их из `@ryuzaki13/react-foundation-api/resource`.
|
|
137
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
|
+
|
|
138
367
|
## Custom executor
|
|
139
368
|
|
|
140
369
|
По умолчанию adapter вызывает только:
|
|
@@ -164,6 +393,153 @@ const operation = createServerFnQueryOperation({
|
|
|
164
393
|
|
|
165
394
|
Executor получает саму функцию, нормализованный request `{ data }` и `{ client, signal? }`. Он полезен для tracing, тестов и framework adapter-а. Не используйте его для бизнес-логики конкретной entity.
|
|
166
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
|
+
|
|
167
543
|
## Типичные ошибки
|
|
168
544
|
|
|
169
545
|
### Передавать args напрямую
|
|
@@ -182,6 +558,32 @@ Default executor не передаёт signal в функцию, посколь
|
|
|
182
558
|
|
|
183
559
|
Factory возвращает operation, а не готовый React hook. Подключите её к resource/persisted descriptor или вызовите `execute` через корректный orchestration layer.
|
|
184
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
|
+
|
|
185
587
|
## Полный API
|
|
186
588
|
|
|
187
589
|
| Export | Назначение |
|