@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.
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 +164 -0
  21. package/src/async/README.mdx +253 -0
  22. package/src/error-report/README.mdx +148 -0
  23. package/src/foundationApi.mdx +123 -0
  24. package/src/http/README.mdx +221 -0
  25. package/src/odata/README.mdx +790 -0
  26. package/src/persisted/README.mdx +454 -0
  27. package/src/resource/README.mdx +358 -0
  28. package/src/server-fn/README.mdx +194 -0
  29. package/src/transport/README.mdx +183 -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,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 |