@ryuzaki13/react-foundation-api 1.1.16 → 1.1.17
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/README.md +32 -43
- package/dist/chunks/{odataFetchFn-vnAXC-c0.js → odataFetchFn-B9wSQpUS.js} +11 -11
- package/dist/chunks/{odataFetchFn-vnAXC-c0.js.map → odataFetchFn-B9wSQpUS.js.map} +1 -1
- package/dist/odata/fetchCollectionData.d.ts +1 -1
- package/dist/odata/fetchCollectionData.d.ts.map +1 -1
- package/dist/odata/index.js +111 -112
- package/dist/odata/index.js.map +1 -1
- package/dist/odata/projectODataCollectionSort.d.ts +1 -1
- package/dist/odata/projectODataCollectionSort.d.ts.map +1 -1
- package/dist/odata/types.d.ts +1 -2
- package/dist/odata/types.d.ts.map +1 -1
- package/dist/odata/useODataCollection.d.ts +1 -1
- package/dist/odata/useODataCollection.d.ts.map +1 -1
- package/dist/odata/useODataCollectionQuery.d.ts +1 -1
- package/dist/odata/useODataCollectionQuery.d.ts.map +1 -1
- package/dist/odata/useODataEntity.d.ts +1 -1
- package/dist/odata/useODataEntity.d.ts.map +1 -1
- package/dist/persisted/index.js +1 -1
- package/package.json +2 -2
- package/src/adt/README.mdx +164 -0
- package/src/async/README.mdx +253 -0
- package/src/error-report/README.mdx +148 -0
- package/src/foundationApi.mdx +123 -0
- package/src/http/README.mdx +221 -0
- package/src/odata/README.mdx +790 -0
- package/src/persisted/README.mdx +454 -0
- package/src/resource/README.mdx +358 -0
- package/src/server-fn/README.mdx +194 -0
- package/src/transport/README.mdx +183 -0
- package/src/README.md +0 -937
- package/src/async/README.md +0 -623
- package/src/async/async.mdx +0 -6
- package/src/odata/README.md +0 -761
- package/src/odata/odataFetchFn.mdx +0 -6
- package/src/persisted/README.md +0 -598
- package/src/persisted/persisted.mdx +0 -6
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
import { Meta } from "@storybook/addon-docs/blocks";
|
|
2
|
+
|
|
3
|
+
<Meta title="Foundation API/Query/Generic Resource" />
|
|
4
|
+
|
|
5
|
+
# Generic resources через `@ryuzaki13/react-foundation-api/resource`
|
|
6
|
+
|
|
7
|
+
Модуль описывает transport-agnostic ресурс поверх TanStack Query: стабильные query keys, проверку scope, произвольные read/write operations и cache strategies после mutation.
|
|
8
|
+
|
|
9
|
+
## Для кого этот документ
|
|
10
|
+
|
|
11
|
+
Здесь термины используются так:
|
|
12
|
+
|
|
13
|
+
- **resource** — технически связанная группа данных, например `orders`;
|
|
14
|
+
- **scope** — контекст, внутри которого данные уникальны, например `{ companyId }`;
|
|
15
|
+
- **query/read operation** — чтение без изменения server state;
|
|
16
|
+
- **mutation/write operation** — создание/изменение/удаление;
|
|
17
|
+
- **descriptor** — объект, объединяющий identity, operations и правила доступности;
|
|
18
|
+
- **query key** — сериализуемый identity записи в TanStack Query cache.
|
|
19
|
+
|
|
20
|
+
Модуль не выполняет HTTP/OData сам. Transport находится внутри `operation.execute` или создаётся adapter-ом `/server-fn`.
|
|
21
|
+
|
|
22
|
+
## Когда использовать
|
|
23
|
+
|
|
24
|
+
Используйте `/resource`, когда названия операций произвольны: `search`, `detail`, `archive`, `recalculate`.
|
|
25
|
+
|
|
26
|
+
Для стандартной модели сохранённых записей `list/latest/history/save/create/delete` удобнее [`/persisted`](../persisted/README.mdx).
|
|
27
|
+
|
|
28
|
+
## Установка и provider
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @ryuzaki13/react-foundation-api @ryuzaki13/react-foundation-lib @tanstack/react-query react
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
React hooks работают только внутри `QueryClientProvider`:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
38
|
+
|
|
39
|
+
const queryClient = new QueryClient();
|
|
40
|
+
|
|
41
|
+
export function App() {
|
|
42
|
+
return <QueryClientProvider client={queryClient}>{/* routes */}</QueryClientProvider>;
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Создайте client один раз, а не внутри render.
|
|
47
|
+
|
|
48
|
+
## Импорт
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import {
|
|
52
|
+
applyResourceCacheStrategy,
|
|
53
|
+
buildResourceQueryOptions,
|
|
54
|
+
composeResourceCacheStrategies,
|
|
55
|
+
createInvalidateResourceScopeCacheStrategy,
|
|
56
|
+
createResourceDescriptor,
|
|
57
|
+
createResourceKeys,
|
|
58
|
+
createResourceMutationOperation,
|
|
59
|
+
createResourceQueryOperation,
|
|
60
|
+
createSetResourceQueryDataCacheStrategy,
|
|
61
|
+
getResourceQueryData,
|
|
62
|
+
normalizeResourceKeyValue,
|
|
63
|
+
useResourceMutation,
|
|
64
|
+
useResourceQuery
|
|
65
|
+
} from "@ryuzaki13/react-foundation-api/resource";
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Типы экспортируются из того же subpath.
|
|
69
|
+
|
|
70
|
+
## Пошаговый пример
|
|
71
|
+
|
|
72
|
+
### 1. Описать scope и данные
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
type OrdersScope = {
|
|
76
|
+
companyId: string;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
type SearchOrdersArgs = {
|
|
80
|
+
search: string;
|
|
81
|
+
limit: number;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
type Order = {
|
|
85
|
+
id: string;
|
|
86
|
+
title: string;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type SaveOrderInput = {
|
|
90
|
+
id: string;
|
|
91
|
+
title: string;
|
|
92
|
+
};
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 2. Создать operations
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
const searchOperation = createResourceQueryOperation<
|
|
99
|
+
OrdersScope,
|
|
100
|
+
SearchOrdersArgs,
|
|
101
|
+
Order[]
|
|
102
|
+
>({
|
|
103
|
+
async execute({ scope, args, signal }) {
|
|
104
|
+
const query = new URLSearchParams({
|
|
105
|
+
companyId: scope.companyId,
|
|
106
|
+
search: args.search,
|
|
107
|
+
limit: String(args.limit)
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const response = await fetch(`/api/orders?${query}`, { signal });
|
|
111
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
112
|
+
return response.json() as Promise<Order[]>;
|
|
113
|
+
},
|
|
114
|
+
isEnabled: (scope, args) => Boolean(scope?.companyId && args.limit > 0),
|
|
115
|
+
staleTime: 60_000,
|
|
116
|
+
gcTime: 5 * 60_000
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const saveOperation = createResourceMutationOperation<
|
|
120
|
+
OrdersScope,
|
|
121
|
+
SaveOrderInput,
|
|
122
|
+
Order,
|
|
123
|
+
unknown
|
|
124
|
+
>({
|
|
125
|
+
async execute({ scope, input }) {
|
|
126
|
+
const response = await fetch(`/api/companies/${scope.companyId}/orders/${input.id}`, {
|
|
127
|
+
method: "PUT",
|
|
128
|
+
headers: { "Content-Type": "application/json" },
|
|
129
|
+
body: JSON.stringify(input)
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
133
|
+
return response.json() as Promise<Order>;
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Factories `createResourceQueryOperation` и `createResourceMutationOperation` не изменяют объект. Они помогают TypeScript сохранить generic types.
|
|
139
|
+
|
|
140
|
+
### 3. Создать descriptor
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
const ordersResource = createResourceDescriptor({
|
|
144
|
+
namespace: "sales",
|
|
145
|
+
resource: "orders",
|
|
146
|
+
normalizeScope: (scope: OrdersScope | null | undefined) => ({
|
|
147
|
+
companyId: scope?.companyId.trim() ?? ""
|
|
148
|
+
}),
|
|
149
|
+
operations: {
|
|
150
|
+
queries: { search: searchOperation },
|
|
151
|
+
mutations: { save: saveOperation }
|
|
152
|
+
},
|
|
153
|
+
isEnabled: (scope) => Boolean(scope?.companyId),
|
|
154
|
+
getScopeError: () => "Не выбрана компания"
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
`namespace + resource` должны быть устойчивыми и уникальными. Не включайте локализованные labels или случайные значения.
|
|
159
|
+
|
|
160
|
+
`normalizeScope` влияет только на query key. В `execute` приходит исходный literal scope. Например, key получит trimmed `companyId`, но operation увидит строку ровно в переданном object. Это позволяет нормализовать identity, не переписывая draft/input.
|
|
161
|
+
|
|
162
|
+
### 4. Использовать query hook
|
|
163
|
+
|
|
164
|
+
```tsx
|
|
165
|
+
function OrdersList({ companyId, search }: Props) {
|
|
166
|
+
const query = useResourceQuery(
|
|
167
|
+
ordersResource,
|
|
168
|
+
"search",
|
|
169
|
+
{ companyId },
|
|
170
|
+
{ search, limit: 50 }
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
if (query.isPending) return <p>Загрузка…</p>;
|
|
174
|
+
if (query.isError) return <p>Ошибка загрузки</p>;
|
|
175
|
+
|
|
176
|
+
return query.data.map((order) => <div key={order.id}>{order.title}</div>);
|
|
177
|
+
}
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Rules of Hooks применяются полностью: hook нельзя вызывать условно или в callback.
|
|
181
|
+
|
|
182
|
+
### 5. Использовать mutation hook
|
|
183
|
+
|
|
184
|
+
```tsx
|
|
185
|
+
const mutation = useResourceMutation(
|
|
186
|
+
ordersResource,
|
|
187
|
+
"save",
|
|
188
|
+
{ companyId },
|
|
189
|
+
{
|
|
190
|
+
onSuccess(savedOrder) {
|
|
191
|
+
showSuccess(`Сохранён ${savedOrder.title}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
mutation.mutate({ id: "42", title: "Новый заголовок" });
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Mutation key содержит resource/scope/operation, но не input. Input передаётся только в `mutationFn`.
|
|
200
|
+
|
|
201
|
+
## Query lifecycle
|
|
202
|
+
|
|
203
|
+
`buildResourceQueryOptions(descriptor, name, scope, args)` создаёт TanStack `queryOptions`:
|
|
204
|
+
|
|
205
|
+
- key из `descriptor.keys.operation`;
|
|
206
|
+
- query function с `{ scope, args, client, signal }`;
|
|
207
|
+
- `enabled` из descriptor и operation;
|
|
208
|
+
- `staleTime`/`gcTime` из operation.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const options = buildResourceQueryOptions(
|
|
212
|
+
ordersResource,
|
|
213
|
+
"search",
|
|
214
|
+
{ companyId: "1000" },
|
|
215
|
+
{ search: "", limit: 50 }
|
|
216
|
+
);
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
`enabled` сначала проверяет `descriptor.isEnabled(scope)`, затем `operation.isEnabled(scope, args)`. Если одна проверка вернула `false`, React query не стартует автоматически.
|
|
220
|
+
|
|
221
|
+
Перед фактическим `execute` scope обязан быть не `null`/`undefined` и пройти descriptor policy. Иначе выбрасывается `getScopeError` или стандартная ошибка.
|
|
222
|
+
|
|
223
|
+
## Императивная загрузка
|
|
224
|
+
|
|
225
|
+
```ts
|
|
226
|
+
const orders = await getResourceQueryData(
|
|
227
|
+
ordersResource,
|
|
228
|
+
"search",
|
|
229
|
+
{ companyId: "1000" },
|
|
230
|
+
{ search: "", limit: 50 },
|
|
231
|
+
queryClient
|
|
232
|
+
);
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Функция использует `queryClient.fetchQuery`, поэтому переиспользует query cache. Передавайте только валидный scope: imperative `fetchQuery` нельзя воспринимать как disabled React observer; query function всё равно защищает scope и может бросить ошибку.
|
|
236
|
+
|
|
237
|
+
## Query keys
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
const keys = createResourceKeys<OrdersScope, "search" | "save">({
|
|
241
|
+
namespace: "sales",
|
|
242
|
+
resource: "orders",
|
|
243
|
+
normalizeScope: (scope) => ({ companyId: scope?.companyId ?? "" })
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
keys.all;
|
|
247
|
+
// ["sales", "orders"]
|
|
248
|
+
|
|
249
|
+
keys.scope({ companyId: "1000" });
|
|
250
|
+
// ["sales", "orders", { companyId: "1000" }]
|
|
251
|
+
|
|
252
|
+
keys.operation("search", { companyId: "1000" }, { limit: 50 });
|
|
253
|
+
// ["sales", "orders", { companyId: "1000" }, "search", { limit: 50 }]
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`args === undefined` не добавляется в key. `args === null` добавляется как `null`.
|
|
257
|
+
|
|
258
|
+
### `normalizeResourceKeyValue`
|
|
259
|
+
|
|
260
|
+
Нормализация рекурсивна:
|
|
261
|
+
|
|
262
|
+
- `null` и `undefined` → `null`;
|
|
263
|
+
- string → `trim()`;
|
|
264
|
+
- number/boolean остаются без изменения;
|
|
265
|
+
- array нормализуется по элементам, порядок сохраняется;
|
|
266
|
+
- object keys сортируются, значения нормализуются;
|
|
267
|
+
- другие значения превращаются через `String(value)`.
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
normalizeResourceKeyValue({ b: " x ", a: undefined });
|
|
271
|
+
// { a: null, b: "x" }
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
Ограничения:
|
|
275
|
+
|
|
276
|
+
- `Date`, `Map`, `Set` и class instances не имеют специальной сериализации; обычный `Date` превратится в `{}`;
|
|
277
|
+
- cyclic object приведёт к рекурсивной ошибке;
|
|
278
|
+
- `NaN`/`Infinity` остаются number, но их cache semantics могут быть неочевидны;
|
|
279
|
+
- function/symbol/bigint превращаются в строки и могут collision-иться.
|
|
280
|
+
|
|
281
|
+
Нормализуйте специальные значения сами, например Date → ISO string. Query key не должен содержать secrets, DOM nodes или огромные payload.
|
|
282
|
+
|
|
283
|
+
## Cache strategies после mutation
|
|
284
|
+
|
|
285
|
+
### Инвалидация scope
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
const invalidateScope = createInvalidateResourceScopeCacheStrategy();
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Стратегия вызывает `client.invalidateQueries` по `descriptor.keys.scope(scope)`. Все query этого resource/scope становятся stale и активные observers могут refetch.
|
|
292
|
+
|
|
293
|
+
### Точечный `setQueryData`
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
const updateDetail = createSetResourceQueryDataCacheStrategy<
|
|
297
|
+
OrdersScope,
|
|
298
|
+
SaveOrderInput,
|
|
299
|
+
Order,
|
|
300
|
+
Order[],
|
|
301
|
+
typeof ordersResource
|
|
302
|
+
>({
|
|
303
|
+
getQueryKey: ({ descriptor, scope }) =>
|
|
304
|
+
descriptor.keys.operation("search", scope, { search: "", limit: 50 }),
|
|
305
|
+
update: (current = [], { result }) =>
|
|
306
|
+
current.map((order) => (order.id === result.id ? result : order))
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`update` получает текущее cache data или `undefined`, а также descriptor/scope/input/result/client. Оно должно возвращать новое значение и не мутировать старое.
|
|
311
|
+
|
|
312
|
+
### Композиция
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
const strategy = composeResourceCacheStrategies(
|
|
316
|
+
updateDetail,
|
|
317
|
+
invalidateScope
|
|
318
|
+
);
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Strategies выполняются последовательно в переданном порядке. Если одна бросила ошибку, следующие и custom `onSuccess` mutation hook не выполнятся; сама server mutation при этом уже могла успешно завершиться.
|
|
322
|
+
|
|
323
|
+
### Override в hook
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
useResourceMutation(descriptor, "save", scope, {
|
|
327
|
+
cacheStrategy: undefined // Использовать strategy операции.
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
useResourceMutation(descriptor, "save", scope, {
|
|
331
|
+
cacheStrategy: null // Полностью отключить strategy операции.
|
|
332
|
+
});
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Переданная strategy заменяет operation strategy, а не дополняет её. Для объединения используйте `composeResourceCacheStrategies`.
|
|
336
|
+
|
|
337
|
+
`applyResourceCacheStrategy(strategy, context)` — низкоуровневый helper для ручного применения optional strategy.
|
|
338
|
+
|
|
339
|
+
## Ошибки и границы
|
|
340
|
+
|
|
341
|
+
- Отсутствующая operation даёт раннюю ошибку с именем resource/operation.
|
|
342
|
+
- Невалидный scope блокирует hook через `enabled`, но ручное выполнение бросает ошибку.
|
|
343
|
+
- Error transport-а сохраняется TanStack Query без автоматического преобразования.
|
|
344
|
+
- Descriptor лучше создавать на уровне module, чтобы ссылка не менялась на каждом render.
|
|
345
|
+
- Args/scope должны быть сериализуемыми и описывать все данные, влияющие на query result.
|
|
346
|
+
- Не используйте query cache как authoritative persistent storage.
|
|
347
|
+
- Cache strategy выполняется только после успешной mutation.
|
|
348
|
+
|
|
349
|
+
## Полный API
|
|
350
|
+
|
|
351
|
+
| Группа | Exports |
|
|
352
|
+
| --- | --- |
|
|
353
|
+
| Descriptor/operations | `createResourceDescriptor`, `createResourceQueryOperation`, `createResourceMutationOperation` |
|
|
354
|
+
| Query/mutation | `buildResourceQueryOptions`, `useResourceQuery`, `getResourceQueryData`, `useResourceMutation` |
|
|
355
|
+
| Keys | `normalizeResourceKeyValue`, `createResourceKeys`, `ResourceKeyValue`, `ResourceKeys` |
|
|
356
|
+
| Cache | `applyResourceCacheStrategy`, `createInvalidateResourceScopeCacheStrategy`, `createSetResourceQueryDataCacheStrategy`, `composeResourceCacheStrategies` |
|
|
357
|
+
| Contracts | `ResourceQueryOperationContext`, `ResourceMutationOperationContext`, `ResourceQueryOperation`, `ResourceMutationOperation`, `ResourceDescriptor`, `CreateResourceDescriptorOptions`, `ResourceCacheStrategyContext`, `ResourceCacheStrategy`, `UseResourceMutationOptions`, `ResourceSetQueryDataStrategyOptions` |
|
|
358
|
+
| Type extraction | `ResourceQueryArgs`, `ResourceQueryResult`, `ResourceMutationInput`, `ResourceMutationResult` |
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
```ts
|
|
37
|
+
import {
|
|
38
|
+
createServerFnMutationOperation,
|
|
39
|
+
createServerFnQueryOperation
|
|
40
|
+
} from "@ryuzaki13/react-foundation-api/server-fn";
|
|
41
|
+
|
|
42
|
+
import type {
|
|
43
|
+
ServerFnTransport,
|
|
44
|
+
ServerFnTransportExecutor,
|
|
45
|
+
ServerFnTransportExecutorContext,
|
|
46
|
+
ServerFnTransportRequest
|
|
47
|
+
} from "@ryuzaki13/react-foundation-api/server-fn";
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Нужен `@tanstack/react-query`; созданные operations обычно подключаются через `/resource` или `/persisted` descriptor.
|
|
51
|
+
|
|
52
|
+
## Query operation
|
|
53
|
+
|
|
54
|
+
Предположим, сгенерированная framework-функция принимает:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
type LoadOrdersData = {
|
|
58
|
+
companyId: string;
|
|
59
|
+
search: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
declare const loadOrdersServerFn: ServerFnTransport<
|
|
63
|
+
LoadOrdersData,
|
|
64
|
+
{ items: OrderDto[] }
|
|
65
|
+
>;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Adapter:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const searchOrdersOperation = createServerFnQueryOperation({
|
|
72
|
+
serverFn: loadOrdersServerFn,
|
|
73
|
+
buildData: (companyId: string, args: { search: string }) => ({
|
|
74
|
+
companyId,
|
|
75
|
+
search: args.search
|
|
76
|
+
}),
|
|
77
|
+
transform: (response) => response.items.map(mapOrderDto),
|
|
78
|
+
staleTime: 60_000,
|
|
79
|
+
gcTime: 5 * 60_000,
|
|
80
|
+
isEnabled: (companyId, args) => Boolean(companyId && args.search.trim())
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
`buildData` получает валидный scope и query args. `transform` получает response и `{ scope, args }`; без transform result равен response.
|
|
85
|
+
|
|
86
|
+
Созданный объект соответствует `ResourceQueryOperation`:
|
|
87
|
+
|
|
88
|
+
- `execute({ scope, args, client, signal })`;
|
|
89
|
+
- optional `isEnabled`;
|
|
90
|
+
- optional `staleTime`;
|
|
91
|
+
- optional `gcTime`.
|
|
92
|
+
|
|
93
|
+
## Mutation operation
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
declare const saveOrderServerFn: ServerFnTransport<
|
|
97
|
+
{ companyId: string; order: SaveOrderDto },
|
|
98
|
+
{ order: OrderDto }
|
|
99
|
+
>;
|
|
100
|
+
|
|
101
|
+
const saveOrderOperation = createServerFnMutationOperation({
|
|
102
|
+
serverFn: saveOrderServerFn,
|
|
103
|
+
buildData: (companyId: string, input: SaveOrderInput) => ({
|
|
104
|
+
companyId,
|
|
105
|
+
order: mapSaveOrder(input)
|
|
106
|
+
}),
|
|
107
|
+
transform: (response) => mapOrderDto(response.order),
|
|
108
|
+
cacheStrategy: createInvalidateResourceScopeCacheStrategy()
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`transform` получает `{ scope, input }`. `cacheStrategy` прикрепляется к operation и выполняется владельцем resource mutation после успешного ответа.
|
|
113
|
+
|
|
114
|
+
Mutation operation не имеет `AbortSignal` в generic resource contract. Custom executor получает `{ client }` без signal.
|
|
115
|
+
|
|
116
|
+
## Подключение к resource descriptor
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
const ordersResource = createResourceDescriptor({
|
|
120
|
+
namespace: "sales",
|
|
121
|
+
resource: "orders",
|
|
122
|
+
operations: {
|
|
123
|
+
queries: { search: searchOrdersOperation },
|
|
124
|
+
mutations: { save: saveOrderOperation }
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const query = useResourceQuery(
|
|
129
|
+
ordersResource,
|
|
130
|
+
"search",
|
|
131
|
+
companyId,
|
|
132
|
+
{ search }
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`server-fn` не экспортирует resource hooks повторно. Импортируйте их из `@ryuzaki13/react-foundation-api/resource`.
|
|
137
|
+
|
|
138
|
+
## Custom executor
|
|
139
|
+
|
|
140
|
+
По умолчанию adapter вызывает только:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
serverFn({ data })
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
`QueryClient` и `AbortSignal` не передаются внутрь `serverFn`. Если transport/framework умеет использовать дополнительный context, задайте executor:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
const executor: ServerFnTransportExecutor<Input, Output> = async (
|
|
150
|
+
serverFn,
|
|
151
|
+
request,
|
|
152
|
+
{ signal }
|
|
153
|
+
) => {
|
|
154
|
+
if (signal?.aborted) throw signal.reason;
|
|
155
|
+
return serverFn(request);
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const operation = createServerFnQueryOperation({
|
|
159
|
+
serverFn,
|
|
160
|
+
buildData,
|
|
161
|
+
executor
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Executor получает саму функцию, нормализованный request `{ data }` и `{ client, signal? }`. Он полезен для tracing, тестов и framework adapter-а. Не используйте его для бизнес-логики конкретной entity.
|
|
166
|
+
|
|
167
|
+
## Типичные ошибки
|
|
168
|
+
|
|
169
|
+
### Передавать args напрямую
|
|
170
|
+
|
|
171
|
+
Неверно предполагать, что query args автоматически становятся data. Их явно преобразует `buildData`, потому что scope/args и transport DTO — разные контракты.
|
|
172
|
+
|
|
173
|
+
### Ожидать автоматическую отмену
|
|
174
|
+
|
|
175
|
+
Default executor не передаёт signal в функцию, поскольку её публичный contract содержит только `{ data }`. Отмена TanStack Query остановит дальнейшее использование результата на уровне query, но transport должен поддерживать abort отдельно.
|
|
176
|
+
|
|
177
|
+
### Считать функцию доверенной
|
|
178
|
+
|
|
179
|
+
Если функция вызывается из browser bundle, её input контролирует пользователь. Backend обязан повторно проверить authentication, authorization и payload schema.
|
|
180
|
+
|
|
181
|
+
### Использовать без descriptor-а
|
|
182
|
+
|
|
183
|
+
Factory возвращает operation, а не готовый React hook. Подключите её к resource/persisted descriptor или вызовите `execute` через корректный orchestration layer.
|
|
184
|
+
|
|
185
|
+
## Полный API
|
|
186
|
+
|
|
187
|
+
| Export | Назначение |
|
|
188
|
+
| --- | --- |
|
|
189
|
+
| `createServerFnQueryOperation` | Создаёт read operation, optional transform/lifetime/isEnabled |
|
|
190
|
+
| `createServerFnMutationOperation` | Создаёт write operation, optional transform/cache strategy |
|
|
191
|
+
| `ServerFnTransport` | Контракт функции `(request) => Promise` |
|
|
192
|
+
| `ServerFnTransportRequest` | Обёртка `{ data }` |
|
|
193
|
+
| `ServerFnTransportExecutor` | Контракт custom execution |
|
|
194
|
+
| `ServerFnTransportExecutorContext` | `QueryClient` и optional signal |
|