@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.
- 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 +461 -0
- package/src/async/README.mdx +628 -0
- package/src/error-report/README.mdx +471 -0
- package/src/foundationApi.mdx +123 -0
- package/src/http/README.mdx +570 -0
- package/src/odata/README.mdx +5142 -0
- package/src/persisted/README.mdx +1080 -0
- package/src/resource/README.mdx +820 -0
- package/src/server-fn/README.mdx +596 -0
- package/src/transport/README.mdx +528 -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
package/src/persisted/README.md
DELETED
|
@@ -1,598 +0,0 @@
|
|
|
1
|
-
# persisted
|
|
2
|
-
|
|
3
|
-
Внутренний infrastructural-модуль для переиспользуемой работы с сохранёнными записями: `variant`, `view-config`, `preset` и похожими сущностями.
|
|
4
|
-
|
|
5
|
-
## Зачем нужен модуль
|
|
6
|
-
|
|
7
|
-
До выноса общей логики разные доменные API независимо решали один и тот же набор задач:
|
|
8
|
-
|
|
9
|
-
- строили `queryKey`;
|
|
10
|
-
- нормализовали `scope`;
|
|
11
|
-
- разбирали JSON payload;
|
|
12
|
-
- подключали `useQuery` и `useMutation`;
|
|
13
|
-
- инвалидировали кэш после сохранения;
|
|
14
|
-
- оборачивали транспортные детали OData, REST или TanStack Start serverFn.
|
|
15
|
-
|
|
16
|
-
`persisted` собирает этот повтор в один слой, но не пытается унифицировать доменную бизнес-логику.
|
|
17
|
-
|
|
18
|
-
Модуль специально не знает, что такое variant, view-config или preset. Он знает только:
|
|
19
|
-
|
|
20
|
-
- у ресурса есть `scope`;
|
|
21
|
-
- ресурс может поддерживать часть стандартных операций;
|
|
22
|
-
- операции могут быть реализованы через разный transport;
|
|
23
|
-
- после мутаций нужно применить переиспользуемую cache policy.
|
|
24
|
-
|
|
25
|
-
## Главная идея
|
|
26
|
-
|
|
27
|
-
Архитектура строится вокруг descriptor ресурса.
|
|
28
|
-
|
|
29
|
-
Descriptor описывает:
|
|
30
|
-
|
|
31
|
-
- идентичность ресурса в кэше;
|
|
32
|
-
- политику нормализации `scope`;
|
|
33
|
-
- проверку, что `scope` вообще пригоден для выполнения операции;
|
|
34
|
-
- набор capability: `list`, `latest`, `history`, `save`, `create`, `delete`;
|
|
35
|
-
- transport-specific реализацию каждой capability.
|
|
36
|
-
|
|
37
|
-
Из descriptor затем строятся:
|
|
38
|
-
|
|
39
|
-
- `usePersistedListQuery`
|
|
40
|
-
- `usePersistedLatestQuery`
|
|
41
|
-
- `usePersistedHistoryQuery`
|
|
42
|
-
- `usePersistedSaveMutation`
|
|
43
|
-
- `usePersistedCreateMutation`
|
|
44
|
-
- `usePersistedDeleteMutation`
|
|
45
|
-
- `getPersistedListData`
|
|
46
|
-
- `getPersistedLatestData`
|
|
47
|
-
- `getPersistedHistoryData`
|
|
48
|
-
|
|
49
|
-
## Слои модуля
|
|
50
|
-
|
|
51
|
-
### `payload.ts`
|
|
52
|
-
|
|
53
|
-
Содержит безопасный JSON codec:
|
|
54
|
-
|
|
55
|
-
- `parsePersistedJson<T>`
|
|
56
|
-
- `stringifyPersistedJson<T>`
|
|
57
|
-
- `createPersistedJsonCodec<T>`
|
|
58
|
-
|
|
59
|
-
Это deliberate-ограничение: parser принимает только строку или `null/undefined`, чтобы нельзя было случайно передать туда весь record.
|
|
60
|
-
|
|
61
|
-
### `keys.ts`
|
|
62
|
-
|
|
63
|
-
Содержит фабрику `createPersistedRecordKeys`.
|
|
64
|
-
|
|
65
|
-
Она строит ключи по схеме:
|
|
66
|
-
|
|
67
|
-
`namespace -> resource -> normalizedScope -> operation -> optionalArgs`
|
|
68
|
-
|
|
69
|
-
Нормализация нужна, чтобы:
|
|
70
|
-
|
|
71
|
-
- trim-ить строки;
|
|
72
|
-
- стабилизировать объекты через сортировку ключей;
|
|
73
|
-
- не плодить разные ключи для эквивалентных scope.
|
|
74
|
-
|
|
75
|
-
### `cache.ts`
|
|
76
|
-
|
|
77
|
-
Содержит стандартные cache strategy:
|
|
78
|
-
|
|
79
|
-
- `createInvalidatePersistedScopeCacheStrategy`
|
|
80
|
-
- `createSetPersistedQueryDataCacheStrategy`
|
|
81
|
-
- `composePersistedCacheStrategies`
|
|
82
|
-
|
|
83
|
-
### `odata.ts`
|
|
84
|
-
|
|
85
|
-
Содержит адаптеры для OData:
|
|
86
|
-
|
|
87
|
-
- `createPersistedODataQueryOperation`
|
|
88
|
-
- `createPersistedODataMutationOperation`
|
|
89
|
-
|
|
90
|
-
### `rest.ts`
|
|
91
|
-
|
|
92
|
-
Содержит адаптеры для обычного REST:
|
|
93
|
-
|
|
94
|
-
- `createPersistedRestQueryOperation`
|
|
95
|
-
- `createPersistedRestMutationOperation`
|
|
96
|
-
|
|
97
|
-
### `resource.ts`
|
|
98
|
-
|
|
99
|
-
Главный orchestration-слой.
|
|
100
|
-
|
|
101
|
-
Тут живут:
|
|
102
|
-
|
|
103
|
-
- `createPersistedResourceDescriptor`
|
|
104
|
-
- query hooks
|
|
105
|
-
- mutation hooks
|
|
106
|
-
- imperative preload helpers
|
|
107
|
-
|
|
108
|
-
Именно этот слой связывает:
|
|
109
|
-
|
|
110
|
-
- descriptor;
|
|
111
|
-
- transport capability;
|
|
112
|
-
- react-query;
|
|
113
|
-
- проверку scope;
|
|
114
|
-
- cache strategy.
|
|
115
|
-
|
|
116
|
-
### `../server-fn`
|
|
117
|
-
|
|
118
|
-
Соседний слой `shared/api/server-fn` содержит переносимые адаптеры для
|
|
119
|
-
TanStack Start server functions:
|
|
120
|
-
|
|
121
|
-
- `createServerFnQueryOperation`
|
|
122
|
-
- `createServerFnMutationOperation`
|
|
123
|
-
|
|
124
|
-
Адаптер не импортирует `@tanstack/react-start`. Он знает только публичную
|
|
125
|
-
форму вызова serverFn: `{ data } -> Promise<response>`. Благодаря этому общий
|
|
126
|
-
shared-слой можно синхронизировать между SAP/OData SPA и SSR-проектом, а
|
|
127
|
-
конкретный проект выбирает transport на уровне descriptor-а ресурса.
|
|
128
|
-
|
|
129
|
-
## Capability model
|
|
130
|
-
|
|
131
|
-
Модуль принципиально не требует полный CRUD.
|
|
132
|
-
|
|
133
|
-
Ресурс может поддерживать только нужные операции.
|
|
134
|
-
|
|
135
|
-
Примеры:
|
|
136
|
-
|
|
137
|
-
- `viewConfig` использует `latest`, `history`, `save`
|
|
138
|
-
- `targetNodePreset` использует `list`, `save`
|
|
139
|
-
- `variantApi` использует `list`, `save`, `create`, `delete`
|
|
140
|
-
|
|
141
|
-
Если попытаться вызвать хук для capability, которой у descriptor нет, общий слой даст раннюю ошибку.
|
|
142
|
-
|
|
143
|
-
## Что оставляем вне общего слоя
|
|
144
|
-
|
|
145
|
-
В `persisted` нельзя складывать доменную бизнес-логику.
|
|
146
|
-
|
|
147
|
-
Снаружи должны оставаться:
|
|
148
|
-
|
|
149
|
-
- выбор default variant;
|
|
150
|
-
- нормализация конкретного snapshot;
|
|
151
|
-
- пользовательские уведомления;
|
|
152
|
-
- специфичные мутации, например `setDefault`;
|
|
153
|
-
- валидация бизнес-правил;
|
|
154
|
-
- подготовка payload, зависящая от доменной модели.
|
|
155
|
-
|
|
156
|
-
Хорошее правило:
|
|
157
|
-
|
|
158
|
-
Если код можно описать как «это одинаково для любого ресурса, который хранит записи», его место здесь.
|
|
159
|
-
|
|
160
|
-
Если код можно описать как «это знание именно про variant/view-config/preset», его место в доменном модуле.
|
|
161
|
-
|
|
162
|
-
## Быстрый рецепт подключения нового ресурса
|
|
163
|
-
|
|
164
|
-
1. Определить `scope`.
|
|
165
|
-
2. Определить, какие capability реально нужны.
|
|
166
|
-
3. Подобрать transport adapter: OData/REST из `shared/api/persisted` или serverFn из `shared/api/server-fn`.
|
|
167
|
-
4. Создать descriptor через `createPersistedResourceDescriptor`.
|
|
168
|
-
5. Экспортировать доменные хуки-обёртки поверх generic-хуков.
|
|
169
|
-
6. Оставить domain-specific transform, notify и validation вне shared-слоя.
|
|
170
|
-
|
|
171
|
-
## Пример 1. OData ресурс с `latest` и `save`
|
|
172
|
-
|
|
173
|
-
Ниже упрощённый пример в стиле `viewConfig`.
|
|
174
|
-
|
|
175
|
-
```ts
|
|
176
|
-
import type { QueryClient } from "@tanstack/react-query";
|
|
177
|
-
|
|
178
|
-
import { BaseURLType } from "@/shared/api";
|
|
179
|
-
import {
|
|
180
|
-
createInvalidatePersistedScopeCacheStrategy,
|
|
181
|
-
createPersistedODataMutationOperation,
|
|
182
|
-
createPersistedODataQueryOperation,
|
|
183
|
-
createPersistedResourceDescriptor,
|
|
184
|
-
getPersistedLatestData,
|
|
185
|
-
parsePersistedJson,
|
|
186
|
-
stringifyPersistedJson,
|
|
187
|
-
usePersistedLatestQuery,
|
|
188
|
-
usePersistedSaveMutation
|
|
189
|
-
} from "@/shared/api/persisted";
|
|
190
|
-
import { createFilterEqual } from "@ryuzaki13/react-foundation-lib/odata-service";
|
|
191
|
-
|
|
192
|
-
type ViewConfigScope = {
|
|
193
|
-
appId: string;
|
|
194
|
-
viewId: string;
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
type ViewConfigPayload = {
|
|
198
|
-
title: string;
|
|
199
|
-
columns: string[];
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
type ViewConfigRaw = {
|
|
203
|
-
id: string;
|
|
204
|
-
appId: string;
|
|
205
|
-
viewId: string;
|
|
206
|
-
payload: string;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
type SaveViewConfigInput = {
|
|
210
|
-
payload: ViewConfigPayload;
|
|
211
|
-
transportRequest: string;
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const SERVICE = "TEXT_CONFIG_SRV";
|
|
215
|
-
const ENTITY = "CONFIG";
|
|
216
|
-
const ENTITY_LATEST = "TEXT_CONFIG_LATEST";
|
|
217
|
-
const SERVICE_BASE_URL: BaseURLType | undefined = __DEV__ ? "odataDp0" : undefined;
|
|
218
|
-
|
|
219
|
-
const viewConfigResource = createPersistedResourceDescriptor({
|
|
220
|
-
namespace: "viewConfig",
|
|
221
|
-
resource: "view",
|
|
222
|
-
normalizeScope: (scope: ViewConfigScope | null | undefined) => ({
|
|
223
|
-
appId: scope?.appId?.trim() ?? "",
|
|
224
|
-
viewId: scope?.viewId?.trim() ?? ""
|
|
225
|
-
}),
|
|
226
|
-
isEnabled: (scope) => Boolean(scope?.appId && scope?.viewId),
|
|
227
|
-
getScopeError: () => "Не удалось выполнить операцию с конфигурацией: scope appId/viewId не задан.",
|
|
228
|
-
transport: {
|
|
229
|
-
latest: createPersistedODataQueryOperation<ViewConfigScope, void, ViewConfigRaw[], ViewConfigPayload | null>({
|
|
230
|
-
odata: { service: SERVICE, target: ENTITY_LATEST },
|
|
231
|
-
baseUrl: SERVICE_BASE_URL,
|
|
232
|
-
staleTime: Infinity,
|
|
233
|
-
gcTime: 1000 * 60 * 60 * 24,
|
|
234
|
-
buildOptions: (scope) => ({
|
|
235
|
-
expression: {
|
|
236
|
-
and: true,
|
|
237
|
-
filters: [
|
|
238
|
-
createFilterEqual("appId", scope.appId),
|
|
239
|
-
createFilterEqual("viewId", scope.viewId)
|
|
240
|
-
]
|
|
241
|
-
}
|
|
242
|
-
}),
|
|
243
|
-
transform: (rows) => parsePersistedJson<ViewConfigPayload>(rows[0]?.payload)
|
|
244
|
-
}),
|
|
245
|
-
save: createPersistedODataMutationOperation<ViewConfigScope, SaveViewConfigInput, unknown, unknown>({
|
|
246
|
-
odata: { service: SERVICE, target: ENTITY },
|
|
247
|
-
baseUrl: SERVICE_BASE_URL,
|
|
248
|
-
method: "POST",
|
|
249
|
-
bodyMapper: (scope, input) => ({
|
|
250
|
-
id: "",
|
|
251
|
-
appId: scope.appId,
|
|
252
|
-
viewId: scope.viewId,
|
|
253
|
-
payload: stringifyPersistedJson(input.payload),
|
|
254
|
-
transportRequest: input.transportRequest
|
|
255
|
-
}),
|
|
256
|
-
cacheStrategy: createInvalidatePersistedScopeCacheStrategy()
|
|
257
|
-
})
|
|
258
|
-
}
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
export function useViewConfigLatestQuery(scope: ViewConfigScope) {
|
|
262
|
-
return usePersistedLatestQuery<ViewConfigScope, ViewConfigPayload | null>(viewConfigResource, scope);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
export function useSaveViewConfigMutation(scope: ViewConfigScope) {
|
|
266
|
-
return usePersistedSaveMutation<ViewConfigScope, SaveViewConfigInput, unknown>(viewConfigResource, scope);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export async function getViewConfigLatestData(scope: ViewConfigScope, queryClient: QueryClient) {
|
|
270
|
-
return await getPersistedLatestData<ViewConfigScope, ViewConfigPayload | null>(viewConfigResource, scope, queryClient);
|
|
271
|
-
}
|
|
272
|
-
```
|
|
273
|
-
|
|
274
|
-
### Что здесь важно
|
|
275
|
-
|
|
276
|
-
- descriptor ничего не знает про UI;
|
|
277
|
-
- payload-parsing остаётся доменным;
|
|
278
|
-
- `latest` возвращает уже доменную модель, а не transport envelope;
|
|
279
|
-
- cache strategy задаётся один раз на уровне ресурса.
|
|
280
|
-
|
|
281
|
-
## Пример 2. SSR ресурс поверх TanStack Start serverFn
|
|
282
|
-
|
|
283
|
-
Ниже упрощённый вариант для проекта с TanStack Start: descriptor остаётся тем же
|
|
284
|
-
generic-слоем, но transport подключается через serverFn вместо OData.
|
|
285
|
-
|
|
286
|
-
```ts
|
|
287
|
-
import type { QueryClient } from "@tanstack/react-query";
|
|
288
|
-
|
|
289
|
-
import {
|
|
290
|
-
createInvalidatePersistedScopeCacheStrategy,
|
|
291
|
-
createPersistedResourceDescriptor,
|
|
292
|
-
getPersistedLatestData,
|
|
293
|
-
usePersistedLatestQuery,
|
|
294
|
-
usePersistedSaveMutation
|
|
295
|
-
} from "@/shared/api/persisted";
|
|
296
|
-
import {
|
|
297
|
-
createServerFnMutationOperation,
|
|
298
|
-
createServerFnQueryOperation
|
|
299
|
-
} from "@/shared/api/server-fn";
|
|
300
|
-
import { getViewConfigServerFn, saveViewConfigServerFn } from "@/server/serverFns";
|
|
301
|
-
|
|
302
|
-
type ViewConfigScope = {
|
|
303
|
-
appId: string;
|
|
304
|
-
viewId: string;
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
type ViewConfigPayload = {
|
|
308
|
-
title: string;
|
|
309
|
-
columns: string[];
|
|
310
|
-
};
|
|
311
|
-
|
|
312
|
-
type SaveViewConfigInput = {
|
|
313
|
-
payload: ViewConfigPayload;
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const viewConfigResource = createPersistedResourceDescriptor({
|
|
317
|
-
namespace: "viewConfig",
|
|
318
|
-
resource: "view",
|
|
319
|
-
normalizeScope: (scope: ViewConfigScope | null | undefined) => ({
|
|
320
|
-
appId: scope?.appId?.trim() ?? "",
|
|
321
|
-
viewId: scope?.viewId?.trim() ?? ""
|
|
322
|
-
}),
|
|
323
|
-
isEnabled: (scope) => Boolean(scope?.appId && scope?.viewId),
|
|
324
|
-
getScopeError: () => "Не удалось выполнить операцию с конфигурацией: scope appId/viewId не задан.",
|
|
325
|
-
transport: {
|
|
326
|
-
latest: createServerFnQueryOperation({
|
|
327
|
-
serverFn: getViewConfigServerFn,
|
|
328
|
-
buildData: (scope) => scope
|
|
329
|
-
}),
|
|
330
|
-
save: createServerFnMutationOperation({
|
|
331
|
-
serverFn: saveViewConfigServerFn,
|
|
332
|
-
buildData: (scope, input) => ({
|
|
333
|
-
...scope,
|
|
334
|
-
payload: input.payload
|
|
335
|
-
}),
|
|
336
|
-
cacheStrategy: createInvalidatePersistedScopeCacheStrategy()
|
|
337
|
-
})
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
export function useViewConfigLatestQuery(scope: ViewConfigScope) {
|
|
342
|
-
return usePersistedLatestQuery<ViewConfigScope, ViewConfigPayload | null>(viewConfigResource, scope);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
export function useSaveViewConfigMutation(scope: ViewConfigScope) {
|
|
346
|
-
return usePersistedSaveMutation<ViewConfigScope, SaveViewConfigInput, void>(viewConfigResource, scope);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export async function getViewConfigLatestData(scope: ViewConfigScope, queryClient: QueryClient) {
|
|
350
|
-
return await getPersistedLatestData<ViewConfigScope, ViewConfigPayload | null>(viewConfigResource, scope, queryClient);
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
### Что здесь важно
|
|
355
|
-
|
|
356
|
-
- shared API не импортирует `@tanstack/react-start`;
|
|
357
|
-
- `{ data }` собирается в `buildData`, а не размазывается по UI/hooks;
|
|
358
|
-
- cache strategy остаётся общей для OData и SSR transport.
|
|
359
|
-
|
|
360
|
-
## Пример 3. REST ресурс с `list` и `save`
|
|
361
|
-
|
|
362
|
-
Ниже упрощённый пример в стиле `targetNodePreset`.
|
|
363
|
-
|
|
364
|
-
```ts
|
|
365
|
-
import {
|
|
366
|
-
createInvalidatePersistedScopeCacheStrategy,
|
|
367
|
-
createPersistedResourceDescriptor,
|
|
368
|
-
createPersistedRestMutationOperation,
|
|
369
|
-
createPersistedRestQueryOperation,
|
|
370
|
-
parsePersistedJson,
|
|
371
|
-
usePersistedListQuery,
|
|
372
|
-
usePersistedSaveMutation
|
|
373
|
-
} from "@/shared/api/persisted";
|
|
374
|
-
|
|
375
|
-
type ConfigPresetScope = {
|
|
376
|
-
userId: string;
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
type TargetNodePayload = {
|
|
380
|
-
name: string;
|
|
381
|
-
description: string;
|
|
382
|
-
type: string;
|
|
383
|
-
config: unknown;
|
|
384
|
-
};
|
|
385
|
-
|
|
386
|
-
type ConfigPresetRecord = {
|
|
387
|
-
recId: string;
|
|
388
|
-
cfgType: string;
|
|
389
|
-
userId: string;
|
|
390
|
-
payload: string;
|
|
391
|
-
crtUtc: string;
|
|
392
|
-
};
|
|
393
|
-
|
|
394
|
-
type ConfigPresetsResponse = {
|
|
395
|
-
data: ConfigPresetRecord[] | null;
|
|
396
|
-
meta: {
|
|
397
|
-
limit: number;
|
|
398
|
-
offset: number;
|
|
399
|
-
};
|
|
400
|
-
};
|
|
401
|
-
|
|
402
|
-
type TargetNodePreset = {
|
|
403
|
-
recId: string;
|
|
404
|
-
cfgType: "target_node_preset";
|
|
405
|
-
userId: string;
|
|
406
|
-
payload: TargetNodePayload;
|
|
407
|
-
crtUtc: string;
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
type TargetNodePresetInput = {
|
|
411
|
-
payload: TargetNodePayload;
|
|
412
|
-
};
|
|
413
|
-
|
|
414
|
-
const BASE = "/api/config-presets";
|
|
415
|
-
const CFG_TYPE = "target_node_preset" as const;
|
|
416
|
-
|
|
417
|
-
const targetNodePresetResource = createPersistedResourceDescriptor({
|
|
418
|
-
namespace: "view-config",
|
|
419
|
-
resource: CFG_TYPE,
|
|
420
|
-
normalizeScope: (scope: ConfigPresetScope | null | undefined) => ({
|
|
421
|
-
userId: scope?.userId?.trim() ?? ""
|
|
422
|
-
}),
|
|
423
|
-
isEnabled: (scope) => Boolean(scope?.userId?.trim()),
|
|
424
|
-
getScopeError: () => "Не удалось выполнить операцию с пресетом узла: userId не задан.",
|
|
425
|
-
transport: {
|
|
426
|
-
list: createPersistedRestQueryOperation<
|
|
427
|
-
ConfigPresetScope,
|
|
428
|
-
void,
|
|
429
|
-
ConfigPresetsResponse,
|
|
430
|
-
TargetNodePreset[]
|
|
431
|
-
>({
|
|
432
|
-
buildUrl: (scope) => `${BASE}/list?cfgType=${CFG_TYPE}&userId=${scope.userId}`,
|
|
433
|
-
transform: (response) =>
|
|
434
|
-
(response.data ?? []).flatMap((record) => {
|
|
435
|
-
const payload = parsePersistedJson<TargetNodePayload>(record.payload);
|
|
436
|
-
return payload
|
|
437
|
-
? [{
|
|
438
|
-
...record,
|
|
439
|
-
cfgType: CFG_TYPE,
|
|
440
|
-
payload
|
|
441
|
-
}]
|
|
442
|
-
: [];
|
|
443
|
-
})
|
|
444
|
-
}),
|
|
445
|
-
save: createPersistedRestMutationOperation<
|
|
446
|
-
ConfigPresetScope,
|
|
447
|
-
TargetNodePresetInput,
|
|
448
|
-
{ data: ConfigPresetRecord | null },
|
|
449
|
-
ConfigPresetRecord | null
|
|
450
|
-
>({
|
|
451
|
-
buildUrl: () => BASE,
|
|
452
|
-
method: "PUT",
|
|
453
|
-
bodyMapper: (scope, input) => ({
|
|
454
|
-
cfgType: CFG_TYPE,
|
|
455
|
-
userId: scope.userId,
|
|
456
|
-
payload: input.payload
|
|
457
|
-
}),
|
|
458
|
-
transform: (response) => response.data,
|
|
459
|
-
cacheStrategy: createInvalidatePersistedScopeCacheStrategy()
|
|
460
|
-
})
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
export function useTargetNodePresetsQuery(scope: ConfigPresetScope | null | undefined) {
|
|
465
|
-
return usePersistedListQuery<ConfigPresetScope, TargetNodePreset[]>(targetNodePresetResource, scope);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
export function useTargetNodePresetMutation(scope: ConfigPresetScope | null | undefined) {
|
|
469
|
-
return usePersistedSaveMutation<ConfigPresetScope, TargetNodePresetInput, ConfigPresetRecord | null>(
|
|
470
|
-
targetNodePresetResource,
|
|
471
|
-
scope
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
```
|
|
475
|
-
|
|
476
|
-
### Что здесь важно
|
|
477
|
-
|
|
478
|
-
- один и тот же orchestration-слой работает и для REST, и для OData;
|
|
479
|
-
- `scope.userId` участвует и в query key, и в `enabled`, и в request body;
|
|
480
|
-
- invalidation остаётся общей политикой, а не копипастой по модулям.
|
|
481
|
-
|
|
482
|
-
## Когда использовать `invalidate`, а когда `setQueryData`
|
|
483
|
-
|
|
484
|
-
### Выбирать `createInvalidatePersistedScopeCacheStrategy`, если:
|
|
485
|
-
|
|
486
|
-
- мутация влияет на несколько query одного scope;
|
|
487
|
-
- backend не возвращает достаточно данных для точного обновления;
|
|
488
|
-
- важнее надёжность, чем микрооптимизация.
|
|
489
|
-
|
|
490
|
-
### Выбирать `createSetPersistedQueryDataCacheStrategy`, если:
|
|
491
|
-
|
|
492
|
-
- нужно быстро обновить один конкретный query;
|
|
493
|
-
- мутация возвращает уже готовую доменную модель;
|
|
494
|
-
- есть уверенность, что не появится рассинхронизация с другими query.
|
|
495
|
-
|
|
496
|
-
### Выбирать `composePersistedCacheStrategies`, если:
|
|
497
|
-
|
|
498
|
-
- нужно совместить адресное обновление и более широкую инвалидацию;
|
|
499
|
-
- ресурс одновременно обновляет `latest` и `history`.
|
|
500
|
-
|
|
501
|
-
## Scope policy
|
|
502
|
-
|
|
503
|
-
Для каждого ресурса важно отдельно определить:
|
|
504
|
-
|
|
505
|
-
- как нормализуется scope;
|
|
506
|
-
- когда scope считается валидным;
|
|
507
|
-
- какое сообщение вернуть при невалидном scope.
|
|
508
|
-
|
|
509
|
-
Рекомендуемый подход:
|
|
510
|
-
|
|
511
|
-
```ts
|
|
512
|
-
normalizeScope: (scope) => ({
|
|
513
|
-
appId: scope?.appId?.trim() ?? "",
|
|
514
|
-
viewId: scope?.viewId?.trim() ?? ""
|
|
515
|
-
}),
|
|
516
|
-
isEnabled: (scope) => Boolean(scope?.appId && scope?.viewId),
|
|
517
|
-
getScopeError: () => "Не удалось выполнить операцию: scope appId/viewId не задан."
|
|
518
|
-
```
|
|
519
|
-
|
|
520
|
-
Это даёт сразу три выигрыша:
|
|
521
|
-
|
|
522
|
-
- одинаковые query key для эквивалентных scope;
|
|
523
|
-
- отсутствие сетевых запросов при неполном scope;
|
|
524
|
-
- понятную ошибку, если мутация всё же была вызвана.
|
|
525
|
-
|
|
526
|
-
## Рекомендации по проектированию
|
|
527
|
-
|
|
528
|
-
### Делайте `transform` доменным
|
|
529
|
-
|
|
530
|
-
Хорошо:
|
|
531
|
-
|
|
532
|
-
- `transform: (rows) => parsePersistedJson<ViewConfigPayload>(rows[0]?.payload)`
|
|
533
|
-
- `transform: (rows) => rows.map(mapVariantRecord)`
|
|
534
|
-
|
|
535
|
-
Плохо:
|
|
536
|
-
|
|
537
|
-
- переносить в shared-слой знание о `variantId`, `isDefault`, `cfgType`, `transportRequest`
|
|
538
|
-
|
|
539
|
-
### Не выносите отдельные бизнес-операции в общий CRUD
|
|
540
|
-
|
|
541
|
-
Например:
|
|
542
|
-
|
|
543
|
-
- `setDefaultVariant`
|
|
544
|
-
- `publishVariant`
|
|
545
|
-
- `duplicatePreset`
|
|
546
|
-
|
|
547
|
-
Если операция не является обычным `save/create/delete`, лучше оставить её доменной мутацией, но при желании переиспользовать `descriptor.keys.scope(...)` и общие cache strategy.
|
|
548
|
-
|
|
549
|
-
### Не заставляйте ресурс поддерживать лишние capability
|
|
550
|
-
|
|
551
|
-
Если backend умеет только `latest` и `save`, так и описывайте.
|
|
552
|
-
|
|
553
|
-
Capability-based модель нужна именно для того, чтобы не строить фальшивый универсальный CRUD.
|
|
554
|
-
|
|
555
|
-
## Тестирование
|
|
556
|
-
|
|
557
|
-
Модуль рассчитан на два уровня тестов:
|
|
558
|
-
|
|
559
|
-
### 1. Тесты shared-слоя
|
|
560
|
-
|
|
561
|
-
Проверяют:
|
|
562
|
-
|
|
563
|
-
- codec;
|
|
564
|
-
- key generation;
|
|
565
|
-
- transport adapters;
|
|
566
|
-
- cache strategies.
|
|
567
|
-
|
|
568
|
-
### 2. Тесты доменного подключения
|
|
569
|
-
|
|
570
|
-
Проверяют:
|
|
571
|
-
|
|
572
|
-
- корректный raw -> domain mapping;
|
|
573
|
-
- защиту от битого payload;
|
|
574
|
-
- специфичные scope policy;
|
|
575
|
-
- контракт возвращаемых данных доменного API.
|
|
576
|
-
|
|
577
|
-
Именно такое разделение сейчас используется в репозитории.
|
|
578
|
-
|
|
579
|
-
## Ограничения модуля
|
|
580
|
-
|
|
581
|
-
- модуль не заменяет domain layer;
|
|
582
|
-
- модуль не решает transport-specific business rules backend;
|
|
583
|
-
- модуль не хранит схемы payload и не валидирует их содержимое;
|
|
584
|
-
- модуль не синтезирует сложные optimistic update сам по себе.
|
|
585
|
-
|
|
586
|
-
Если ресурсу нужна сложная бизнес-валидация или нестандартная синхронизация, это должно остаться в доменном коде поверх shared-слоя.
|
|
587
|
-
|
|
588
|
-
## Чек-лист для нового ресурса
|
|
589
|
-
|
|
590
|
-
- Есть ли у ресурса стабильный `scope`?
|
|
591
|
-
- Нужно ли нормализовать строки и идентификаторы?
|
|
592
|
-
- Какие capability действительно нужны?
|
|
593
|
-
- Подходит OData adapter или пока нужен REST adapter?
|
|
594
|
-
- Можно ли после мутации ограничиться `invalidate(scope)`?
|
|
595
|
-
- Возвращает ли query уже доменную модель без transport envelope?
|
|
596
|
-
- Вынесена ли доменная бизнес-логика за пределы `persisted`?
|
|
597
|
-
|
|
598
|
-
Если на все вопросы есть понятный ответ, ресурс почти наверняка хорошо ложится на этот модуль.
|