@quark-fw/entity 0.1.7 → 0.1.9
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/dist/client/createEntityContext.d.ts +22 -0
- package/dist/client/createEntityContext.js +291 -0
- package/dist/client/helpers.d.ts +5 -0
- package/dist/client/helpers.js +21 -0
- package/dist/client/types.d.ts +138 -0
- package/dist/client/types.js +1 -0
- package/dist/client.d.ts +2 -153
- package/dist/client.js +4 -294
- package/dist/createEntity.d.ts +216 -0
- package/dist/createEntity.js +287 -0
- package/dist/helpers.d.ts +15 -0
- package/dist/helpers.js +24 -0
- package/dist/index.d.ts +1 -215
- package/dist/index.js +1 -310
- package/package.json +11 -6
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
import type { Serialize } from "@quark-fw/store/src/serialize.js";
|
|
3
|
+
import type { EntitiesContextType, EntityContextType } from "./types.js";
|
|
4
|
+
export declare function createEntityContext<TRow extends {
|
|
5
|
+
id: string;
|
|
6
|
+
}>(entityKey: string): readonly [({ children, filter, limit, sort, }: {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
/** серверный фильтр списка (общий для всех потребителей) */
|
|
9
|
+
filter?: Record<string, unknown>;
|
|
10
|
+
limit?: number;
|
|
11
|
+
sort?: {
|
|
12
|
+
field: string;
|
|
13
|
+
dir?: "asc" | "desc";
|
|
14
|
+
};
|
|
15
|
+
}) => import("react/jsx-runtime").JSX.Element, ({ children, id, }: {
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
id: string;
|
|
18
|
+
}) => import("react/jsx-runtime").JSX.Element, () => EntitiesContextType<Serialize<TRow> & {
|
|
19
|
+
id: string;
|
|
20
|
+
}>, () => EntityContextType<Serialize<TRow> & {
|
|
21
|
+
id: string;
|
|
22
|
+
}>];
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/* eslint-disable react-hooks/rules-of-hooks */
|
|
3
|
+
// Порт createEntityContext из druslee/ugliest под quark:
|
|
4
|
+
// пара React-контекстов на entity с оптимистичным состоянием и откатом.
|
|
5
|
+
// ВАЖНО: клиентский модуль — не импортировать серверный ../index.js.
|
|
6
|
+
import { createContext, startTransition, useEffect, useMemo, useRef, useState, } from "react";
|
|
7
|
+
import { useStore, useTrpc } from "@quark-fw/store";
|
|
8
|
+
import { reportError } from "@quark-fw/store/src/errors.js";
|
|
9
|
+
import { useEntityForm } from "../useEntityForm.js";
|
|
10
|
+
import { uuidv7 } from "../uuidv7.js";
|
|
11
|
+
import { matchesEverything, matchesWhere, useSafeContext } from "./helpers.js";
|
|
12
|
+
export function createEntityContext(entityKey) {
|
|
13
|
+
const EntitiesContext = createContext(null);
|
|
14
|
+
const EntityContext = createContext(null);
|
|
15
|
+
const useEntities = useSafeContext(`${entityKey} entities context was not found in tree`, EntitiesContext);
|
|
16
|
+
const useEntity = useSafeContext(`${entityKey} entity context was not found in tree`, EntityContext);
|
|
17
|
+
const EntitiesProvider = ({ children, filter, limit = 100, sort, }) => {
|
|
18
|
+
const store = useStore();
|
|
19
|
+
// Имя сущности приходит параметром, поэтому ветку роутера нельзя
|
|
20
|
+
// вывести статически — но набор процедур у всех сущностей общий и
|
|
21
|
+
// описан типами выше.
|
|
22
|
+
const tinyCaller = store.tiny.entity[entityKey];
|
|
23
|
+
const trpcCaller = useTrpc().entity[entityKey];
|
|
24
|
+
const [completeList, setCompleteList] = useState(null);
|
|
25
|
+
const [page, setPage] = useState(1);
|
|
26
|
+
// Сколько мутаций сейчас в полёте. Пока хоть одна не завершилась,
|
|
27
|
+
// синхронизация из серверного списка запрещена: иначе первый же
|
|
28
|
+
// эффект после монтирования затирает оптимистичное изменение,
|
|
29
|
+
// сделанное до его выполнения (гонка «создал сразу после загрузки»).
|
|
30
|
+
const mutationsInFlight = useRef(0);
|
|
31
|
+
// Наборы аргументов, с которыми список сейчас читают компоненты.
|
|
32
|
+
// Кэш store обновляем именно по ним — иначе setCache пишет под
|
|
33
|
+
// ключом, который никто не читает, и после ремонта виден старый список.
|
|
34
|
+
const liveArgs = useRef(new Map());
|
|
35
|
+
// Массивы, которые провайдер САМ записал в кэш store. Их эхо приходит
|
|
36
|
+
// назад через fresh, и без пометки эффект синхронизации принимал его
|
|
37
|
+
// за серверные данные: оптимистичный массив, попавший в кэш до отката,
|
|
38
|
+
// перетирал откат (mutationsInFlight к этому моменту уже обнулён).
|
|
39
|
+
const ownCacheWrites = useRef(new WeakSet());
|
|
40
|
+
const setList = (updater) => {
|
|
41
|
+
const syncCache = (arr) => {
|
|
42
|
+
ownCacheWrites.current.add(arr);
|
|
43
|
+
for (const args of liveArgs.current.values()) {
|
|
44
|
+
tinyCaller.list.setCache(args)(arr);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
if (Array.isArray(updater)) {
|
|
48
|
+
setCompleteList(updater);
|
|
49
|
+
syncCache(updater);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
setCompleteList((prev) => {
|
|
53
|
+
const arr = updater(prev);
|
|
54
|
+
// побочный эффект вынесен из апдейтера состояния
|
|
55
|
+
queueMicrotask(() => syncCache(arr));
|
|
56
|
+
return arr;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
/** Помечает мутацию как выполняющуюся, чтобы её не затёрли данными. */
|
|
61
|
+
const withMutation = async (fn) => {
|
|
62
|
+
mutationsInFlight.current += 1;
|
|
63
|
+
try {
|
|
64
|
+
return await fn();
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
mutationsInFlight.current -= 1;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
// при ошибке откатывает список и сообщает через reportError,
|
|
71
|
+
// возвращая null — вызывающему коду не нужен try/catch
|
|
72
|
+
const create = async (data) => {
|
|
73
|
+
const now = new Date().toISOString();
|
|
74
|
+
// клиентский UUIDv7 = финальный id записи
|
|
75
|
+
const item = {
|
|
76
|
+
id: uuidv7(),
|
|
77
|
+
createdAt: now,
|
|
78
|
+
...data,
|
|
79
|
+
};
|
|
80
|
+
const old = completeList;
|
|
81
|
+
setList((prev) => (prev ? [...prev, item] : [item]));
|
|
82
|
+
return withMutation(async () => {
|
|
83
|
+
try {
|
|
84
|
+
return await trpcCaller.create.mutate({ data: item });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
reportError(error, entityKey);
|
|
88
|
+
setList(old ?? []);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
const update = async (id, data, { clientOnly } = {}) => {
|
|
94
|
+
const old = (completeList ?? []).find((e) => e.id === id);
|
|
95
|
+
setList((prev) => {
|
|
96
|
+
const arr = prev ? [...prev] : [];
|
|
97
|
+
const index = arr.findIndex((e) => e.id === id);
|
|
98
|
+
if (index === -1)
|
|
99
|
+
return arr;
|
|
100
|
+
arr[index] = { ...arr[index], ...data };
|
|
101
|
+
return arr;
|
|
102
|
+
});
|
|
103
|
+
await withMutation(async () => {
|
|
104
|
+
try {
|
|
105
|
+
if (!clientOnly) {
|
|
106
|
+
await trpcCaller.update.mutate({ id, data });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
reportError(error, entityKey);
|
|
111
|
+
setList((prev) => {
|
|
112
|
+
if (!prev || !old)
|
|
113
|
+
return prev ?? [];
|
|
114
|
+
const arr = [...prev];
|
|
115
|
+
const index = arr.findIndex((e) => e.id === id);
|
|
116
|
+
if (index !== -1)
|
|
117
|
+
arr[index] = old;
|
|
118
|
+
return arr;
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
const deleteById = async (id) => {
|
|
124
|
+
const old = completeList;
|
|
125
|
+
setList((prev) => (prev ?? []).filter((e) => e.id !== id));
|
|
126
|
+
await withMutation(async () => {
|
|
127
|
+
try {
|
|
128
|
+
await trpcCaller.delete.mutate({ id });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
reportError(error, entityKey);
|
|
132
|
+
setList(old ?? []);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
// Сколько страниц уже добавлено в список. Пока страница одна,
|
|
137
|
+
// провайдер синхронизируется с сервером как обычно; после догрузки
|
|
138
|
+
// синхронизация выключается — иначе первая страница затрёт накопленное.
|
|
139
|
+
const loadedPages = useRef(1);
|
|
140
|
+
/**
|
|
141
|
+
* Принять запись, приехавшую из другого провайдера этой же сущности.
|
|
142
|
+
*
|
|
143
|
+
* Только локальное состояние: запись уже сохранена тем, кто её
|
|
144
|
+
* передал. Если запись с таким id уже есть — обновляем на месте.
|
|
145
|
+
*/
|
|
146
|
+
const adopt = (row) => {
|
|
147
|
+
setList((prev) => {
|
|
148
|
+
const arr = prev ? [...prev] : [];
|
|
149
|
+
const index = arr.findIndex((e) => e.id === row.id);
|
|
150
|
+
if (index === -1)
|
|
151
|
+
arr.push(row);
|
|
152
|
+
else
|
|
153
|
+
arr[index] = row;
|
|
154
|
+
return arr;
|
|
155
|
+
});
|
|
156
|
+
};
|
|
157
|
+
// ЕДИНСТВЕННЫЙ серверный запрос списка на провайдер: состояние тоже
|
|
158
|
+
// одно, поэтому несколько разных запросов рассинхронизировали бы его
|
|
159
|
+
// (колонка, синхронизировавшаяся последней, затирала бы остальные).
|
|
160
|
+
const argsKey = JSON.stringify({
|
|
161
|
+
...(filter ? { where: filter } : {}),
|
|
162
|
+
...(sort ? { sort } : {}),
|
|
163
|
+
limit,
|
|
164
|
+
page,
|
|
165
|
+
});
|
|
166
|
+
// объект аргументов должен сохранять идентичность, пока не изменился
|
|
167
|
+
// их состав: иначе эффект ниже перезапускается на каждый рендер
|
|
168
|
+
const serverArgs = useMemo(() => JSON.parse(argsKey), [argsKey]);
|
|
169
|
+
const fresh = tinyCaller.list.mutate(serverArgs);
|
|
170
|
+
const count = tinyCaller.count.mutate(filter ? { where: filter } : {});
|
|
171
|
+
const loadMore = async () => {
|
|
172
|
+
const nextPage = loadedPages.current + 1;
|
|
173
|
+
const rows = await withMutation(() => trpcCaller.list.mutate({ ...serverArgs, page: nextPage }));
|
|
174
|
+
if (!rows.length)
|
|
175
|
+
return 0;
|
|
176
|
+
loadedPages.current = nextPage;
|
|
177
|
+
setList((prev) => {
|
|
178
|
+
const seen = new Set((prev ?? []).map((row) => row.id));
|
|
179
|
+
return [
|
|
180
|
+
...(prev ?? []),
|
|
181
|
+
...rows.filter((row) => !seen.has(row.id)),
|
|
182
|
+
];
|
|
183
|
+
});
|
|
184
|
+
return rows.length;
|
|
185
|
+
};
|
|
186
|
+
// мутации обновляют кэш store по тем же аргументам, что и чтение
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
// ref читаем до подписки: к моменту очистки .current уже может
|
|
189
|
+
// указывать на другой объект
|
|
190
|
+
const live = liveArgs.current;
|
|
191
|
+
live.set(argsKey, serverArgs);
|
|
192
|
+
return () => {
|
|
193
|
+
live.delete(argsKey);
|
|
194
|
+
};
|
|
195
|
+
}, [argsKey, serverArgs]);
|
|
196
|
+
// с этой порцией серверных данных мы уже синхронизировались
|
|
197
|
+
const syncedFrom = useRef(null);
|
|
198
|
+
useEffect(() => {
|
|
199
|
+
if (syncedFrom.current === fresh)
|
|
200
|
+
return;
|
|
201
|
+
// эхо собственной записи в кэш — это не серверные данные
|
|
202
|
+
if (ownCacheWrites.current.has(fresh)) {
|
|
203
|
+
syncedFrom.current = fresh;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// мутация в полёте — локальное состояние новее серверного
|
|
207
|
+
if (mutationsInFlight.current > 0)
|
|
208
|
+
return;
|
|
209
|
+
// подгруженные страницы: первая страница затёрла бы остальные
|
|
210
|
+
if (loadedPages.current > 1)
|
|
211
|
+
return;
|
|
212
|
+
syncedFrom.current = fresh;
|
|
213
|
+
startTransition(() => {
|
|
214
|
+
setList(fresh);
|
|
215
|
+
});
|
|
216
|
+
// setList намеренно не в зависимостях: он пересоздаётся каждый
|
|
217
|
+
// рендер, а сам пишет состояние — с ним эффект зациклится
|
|
218
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
219
|
+
}, [fresh]);
|
|
220
|
+
const rows = completeList ?? fresh;
|
|
221
|
+
const value = {
|
|
222
|
+
list: ({ filter: extraFilter } = {}) => [
|
|
223
|
+
// срез общего списка под фильтр потребителя
|
|
224
|
+
!extraFilter || matchesEverything(extraFilter)
|
|
225
|
+
? rows
|
|
226
|
+
: rows.filter((row) => matchesWhere(row, extraFilter)),
|
|
227
|
+
{ page, setPage, count: count ?? null },
|
|
228
|
+
],
|
|
229
|
+
tiny: tinyCaller,
|
|
230
|
+
forms: {
|
|
231
|
+
// хук вызывается из компонента (как list выше) — порядок
|
|
232
|
+
// вызовов стабилен, пока компонент вызывает forms.create()
|
|
233
|
+
// безусловно в теле рендера
|
|
234
|
+
create: ({ defaultValues, transformSubmitValues, onSuccess, } = {}) => useEntityForm({
|
|
235
|
+
defaultValues,
|
|
236
|
+
onSubmit: async (values) => {
|
|
237
|
+
const data = transformSubmitValues
|
|
238
|
+
? transformSubmitValues(values)
|
|
239
|
+
: values;
|
|
240
|
+
const row = await create(data);
|
|
241
|
+
onSuccess?.(row);
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
},
|
|
245
|
+
create,
|
|
246
|
+
update,
|
|
247
|
+
deleteById,
|
|
248
|
+
adopt,
|
|
249
|
+
loadMore,
|
|
250
|
+
total: count ?? null,
|
|
251
|
+
};
|
|
252
|
+
return (_jsx(EntitiesContext.Provider, { value: value, children: children }));
|
|
253
|
+
};
|
|
254
|
+
const EntityProvider = ({ children, id, }) => {
|
|
255
|
+
if (!id)
|
|
256
|
+
throw new Error(`EntityProvider [${entityKey}] id must be provided`);
|
|
257
|
+
const store = useStore();
|
|
258
|
+
// Имя сущности приходит параметром, поэтому ветку роутера нельзя
|
|
259
|
+
// вывести статически — но набор процедур у всех сущностей общий и
|
|
260
|
+
// описан типами выше.
|
|
261
|
+
const tinyCaller = store.tiny.entity[entityKey];
|
|
262
|
+
const trpcCaller = useTrpc().entity[entityKey];
|
|
263
|
+
const entities = useEntities();
|
|
264
|
+
const controller = new Proxy({}, {
|
|
265
|
+
get(_target, method) {
|
|
266
|
+
return (data) => trpcCaller.controller.mutate({
|
|
267
|
+
id,
|
|
268
|
+
// ключ прокси может быть символом, а по сети уходит
|
|
269
|
+
// имя метода — берём только строковые
|
|
270
|
+
method: String(method),
|
|
271
|
+
data,
|
|
272
|
+
});
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
const value = {
|
|
276
|
+
instance: () => {
|
|
277
|
+
const [list] = entities.list();
|
|
278
|
+
const item = list.find((e) => e.id === id) ??
|
|
279
|
+
tinyCaller.getById.mutate(id);
|
|
280
|
+
if (!item)
|
|
281
|
+
throw new Error(`${entityKey} ${id} not found`);
|
|
282
|
+
return item;
|
|
283
|
+
},
|
|
284
|
+
update: (data) => entities.update(id, data),
|
|
285
|
+
delete: () => entities.deleteById(id),
|
|
286
|
+
controller,
|
|
287
|
+
};
|
|
288
|
+
return (_jsx(EntityContext.Provider, { value: value, children: children }));
|
|
289
|
+
};
|
|
290
|
+
return [EntitiesProvider, EntityProvider, useEntities, useEntity];
|
|
291
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Context } from "react";
|
|
2
|
+
/** Условия, которые можно проверить на клиенте (простые равенства). */
|
|
3
|
+
export declare const matchesEverything: (where: Record<string, unknown>) => boolean;
|
|
4
|
+
export declare const matchesWhere: (row: Record<string, unknown>, where: Record<string, unknown>) => boolean;
|
|
5
|
+
export declare function useSafeContext<T>(errorMessage: string, Ctx: Context<T | null>): () => T & ({} | undefined);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Хелперы клиентской части сущностей.
|
|
2
|
+
import { useContext } from "react";
|
|
3
|
+
/** Условия, которые можно проверить на клиенте (простые равенства). */
|
|
4
|
+
export const matchesEverything = (where) => Object.keys(where).length === 0;
|
|
5
|
+
export const matchesWhere = (row, where) => Object.entries(where).every(([field, expected]) => {
|
|
6
|
+
// операторы вида { in: [...] } на клиенте не разбираем — доверяем серверу
|
|
7
|
+
if (expected && typeof expected === "object")
|
|
8
|
+
return true;
|
|
9
|
+
return row?.[field] === expected;
|
|
10
|
+
});
|
|
11
|
+
export function useSafeContext(errorMessage, Ctx) {
|
|
12
|
+
return () => {
|
|
13
|
+
// хук зовётся из возвращённой функции, которая сама используется как
|
|
14
|
+
// хук потребителем (useEntities/useEntity) — порядок вызовов стабилен
|
|
15
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
16
|
+
const ctx = useContext(Ctx);
|
|
17
|
+
if (ctx === null)
|
|
18
|
+
throw new Error(errorMessage);
|
|
19
|
+
return ctx;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { useEntityForm } from "../useEntityForm.js";
|
|
2
|
+
export type EntitiesListParams = {
|
|
3
|
+
/**
|
|
4
|
+
* Фильтр КОНКРЕТНОГО потребителя (например колонки доски). Применяется
|
|
5
|
+
* на клиенте к общему списку провайдера: состояние одно на провайдер,
|
|
6
|
+
* поэтому серверный запрос задаётся пропсами провайдера (filter/sort/limit),
|
|
7
|
+
* а здесь получается срез этого списка.
|
|
8
|
+
*/
|
|
9
|
+
filter?: Record<string, unknown>;
|
|
10
|
+
};
|
|
11
|
+
/** аргументы серверного list: то, что уходит в процедуру сущности */
|
|
12
|
+
export type EntityListArgs = {
|
|
13
|
+
where?: Record<string, unknown>;
|
|
14
|
+
sort?: {
|
|
15
|
+
field: string;
|
|
16
|
+
dir?: "asc" | "desc";
|
|
17
|
+
};
|
|
18
|
+
limit?: number;
|
|
19
|
+
page?: number;
|
|
20
|
+
/** поиск по подстроке; работает, если у сущности задан searchField */
|
|
21
|
+
search?: string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* tiny-вызовы КОНКРЕТНОЙ сущности.
|
|
25
|
+
*
|
|
26
|
+
* Имя сущности известно только в рантайме (createEntityContext("project")),
|
|
27
|
+
* поэтому вывести его из роутера нельзя — но набор процедур у всех сущностей
|
|
28
|
+
* один и тот же, и он описан здесь.
|
|
29
|
+
*/
|
|
30
|
+
export type EntityTinyCaller<Row> = {
|
|
31
|
+
list: {
|
|
32
|
+
mutate(args: EntityListArgs): Row[];
|
|
33
|
+
setCache(args: EntityListArgs): (rows: Row[]) => void;
|
|
34
|
+
};
|
|
35
|
+
count: {
|
|
36
|
+
mutate(args: {
|
|
37
|
+
where?: Record<string, unknown>;
|
|
38
|
+
}): number;
|
|
39
|
+
};
|
|
40
|
+
getById: {
|
|
41
|
+
mutate(id: string): Row | null;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
/** те же процедуры, но сетевые (не через кэш стора) */
|
|
45
|
+
export type EntityTrpcCaller<Row> = {
|
|
46
|
+
list: {
|
|
47
|
+
mutate(args: EntityListArgs): Promise<Row[]>;
|
|
48
|
+
};
|
|
49
|
+
create: {
|
|
50
|
+
mutate(args: {
|
|
51
|
+
data: Record<string, unknown>;
|
|
52
|
+
}): Promise<Row>;
|
|
53
|
+
};
|
|
54
|
+
update: {
|
|
55
|
+
mutate(args: {
|
|
56
|
+
id: string;
|
|
57
|
+
data: Record<string, unknown>;
|
|
58
|
+
}): Promise<Row>;
|
|
59
|
+
};
|
|
60
|
+
delete: {
|
|
61
|
+
mutate(args: {
|
|
62
|
+
id: string;
|
|
63
|
+
}): Promise<unknown>;
|
|
64
|
+
};
|
|
65
|
+
controller: {
|
|
66
|
+
mutate(args: {
|
|
67
|
+
id: string;
|
|
68
|
+
method: string;
|
|
69
|
+
data?: unknown;
|
|
70
|
+
}): Promise<unknown>;
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export type TinyBranch<Row> = {
|
|
74
|
+
entity: Record<string, EntityTinyCaller<Row>>;
|
|
75
|
+
};
|
|
76
|
+
export type TrpcBranch<Row> = {
|
|
77
|
+
entity: Record<string, EntityTrpcCaller<Row>>;
|
|
78
|
+
};
|
|
79
|
+
export type EntitiesContextType<Row extends {
|
|
80
|
+
id: string;
|
|
81
|
+
}> = {
|
|
82
|
+
list: (params?: EntitiesListParams) => [
|
|
83
|
+
Row[],
|
|
84
|
+
{
|
|
85
|
+
page: number;
|
|
86
|
+
setPage: (page: number) => void;
|
|
87
|
+
count: number | null;
|
|
88
|
+
}
|
|
89
|
+
];
|
|
90
|
+
tiny: EntityTinyCaller<Row>;
|
|
91
|
+
/**
|
|
92
|
+
* Сахар над useEntityForm: форма, создающая запись в этой сущности.
|
|
93
|
+
*
|
|
94
|
+
* Форма значений выводится из defaultValues, поэтому `form.values.name`
|
|
95
|
+
* у потребителя — строка, а не `any`.
|
|
96
|
+
*/
|
|
97
|
+
forms: {
|
|
98
|
+
create: <Shape extends Record<string, unknown>>(args?: {
|
|
99
|
+
defaultValues?: Partial<Shape>;
|
|
100
|
+
/** доработать значения перед отправкой (подставить projectId и т.п.) */
|
|
101
|
+
transformSubmitValues?: (values: Shape) => Record<string, unknown>;
|
|
102
|
+
onSuccess?: (row: Row | null) => void;
|
|
103
|
+
}) => ReturnType<typeof useEntityForm<Shape>>;
|
|
104
|
+
};
|
|
105
|
+
create: (data: Record<string, unknown>) => Promise<Row | null>;
|
|
106
|
+
update: (id: string, data: Partial<Row>, options?: {
|
|
107
|
+
clientOnly?: boolean;
|
|
108
|
+
}) => Promise<void>;
|
|
109
|
+
deleteById: (id: string) => Promise<void>;
|
|
110
|
+
/**
|
|
111
|
+
* Принять запись в локальный список без обращения к серверу.
|
|
112
|
+
*
|
|
113
|
+
* Нужна, когда рядом живут несколько провайдеров одной сущности с разными
|
|
114
|
+
* серверными фильтрами — например колонки доски. Запись, уехавшая из
|
|
115
|
+
* одного фильтра в другой, для второго провайдера просто не существует:
|
|
116
|
+
* его запрос уже выполнен. Сохранение делает вызывающий, здесь только
|
|
117
|
+
* локальное состояние.
|
|
118
|
+
*/
|
|
119
|
+
adopt: (row: Row) => void;
|
|
120
|
+
/**
|
|
121
|
+
* Подгрузить следующую страницу и ДОБАВИТЬ её к списку.
|
|
122
|
+
*
|
|
123
|
+
* Обычная смена страницы список заменяет; здесь он копится — это нужно
|
|
124
|
+
* для догрузки по прокрутке. Возвращает, сколько записей приехало: ноль
|
|
125
|
+
* означает, что дальше ничего нет.
|
|
126
|
+
*/
|
|
127
|
+
loadMore: () => Promise<number>;
|
|
128
|
+
/** сколько записей всего по серверному фильтру провайдера */
|
|
129
|
+
total: number | null;
|
|
130
|
+
};
|
|
131
|
+
export type EntityContextType<Row extends {
|
|
132
|
+
id: string;
|
|
133
|
+
}> = {
|
|
134
|
+
instance: () => Row;
|
|
135
|
+
update: (data: Partial<Row>) => Promise<void>;
|
|
136
|
+
delete: () => Promise<void>;
|
|
137
|
+
controller: Record<string, (data?: unknown) => Promise<unknown>>;
|
|
138
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/client.d.ts
CHANGED
|
@@ -1,153 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { useEntityForm } from "./useEntityForm.js";
|
|
4
|
-
export type EntitiesListParams = {
|
|
5
|
-
/**
|
|
6
|
-
* Фильтр КОНКРЕТНОГО потребителя (например колонки доски). Применяется
|
|
7
|
-
* на клиенте к общему списку провайдера: состояние одно на провайдер,
|
|
8
|
-
* поэтому серверный запрос задаётся пропсами провайдера (filter/sort/limit),
|
|
9
|
-
* а здесь получается срез этого списка.
|
|
10
|
-
*/
|
|
11
|
-
filter?: Record<string, unknown>;
|
|
12
|
-
};
|
|
13
|
-
/** аргументы серверного list: то, что уходит в процедуру сущности */
|
|
14
|
-
export type EntityListArgs = {
|
|
15
|
-
where?: Record<string, unknown>;
|
|
16
|
-
sort?: {
|
|
17
|
-
field: string;
|
|
18
|
-
dir?: "asc" | "desc";
|
|
19
|
-
};
|
|
20
|
-
limit?: number;
|
|
21
|
-
page?: number;
|
|
22
|
-
/** поиск по подстроке; работает, если у сущности задан searchField */
|
|
23
|
-
search?: string;
|
|
24
|
-
};
|
|
25
|
-
/**
|
|
26
|
-
* tiny-вызовы КОНКРЕТНОЙ сущности.
|
|
27
|
-
*
|
|
28
|
-
* Имя сущности известно только в рантайме (createEntityContext("project")),
|
|
29
|
-
* поэтому вывести его из роутера нельзя — но набор процедур у всех сущностей
|
|
30
|
-
* один и тот же, и он описан здесь.
|
|
31
|
-
*/
|
|
32
|
-
export type EntityTinyCaller<Row> = {
|
|
33
|
-
list: {
|
|
34
|
-
mutate(args: EntityListArgs): Row[];
|
|
35
|
-
setCache(args: EntityListArgs): (rows: Row[]) => void;
|
|
36
|
-
};
|
|
37
|
-
count: {
|
|
38
|
-
mutate(args: {
|
|
39
|
-
where?: Record<string, unknown>;
|
|
40
|
-
}): number;
|
|
41
|
-
};
|
|
42
|
-
getById: {
|
|
43
|
-
mutate(id: string): Row | null;
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
/** те же процедуры, но сетевые (не через кэш стора) */
|
|
47
|
-
export type EntityTrpcCaller<Row> = {
|
|
48
|
-
list: {
|
|
49
|
-
mutate(args: EntityListArgs): Promise<Row[]>;
|
|
50
|
-
};
|
|
51
|
-
create: {
|
|
52
|
-
mutate(args: {
|
|
53
|
-
data: Record<string, unknown>;
|
|
54
|
-
}): Promise<Row>;
|
|
55
|
-
};
|
|
56
|
-
update: {
|
|
57
|
-
mutate(args: {
|
|
58
|
-
id: string;
|
|
59
|
-
data: Record<string, unknown>;
|
|
60
|
-
}): Promise<Row>;
|
|
61
|
-
};
|
|
62
|
-
delete: {
|
|
63
|
-
mutate(args: {
|
|
64
|
-
id: string;
|
|
65
|
-
}): Promise<unknown>;
|
|
66
|
-
};
|
|
67
|
-
controller: {
|
|
68
|
-
mutate(args: {
|
|
69
|
-
id: string;
|
|
70
|
-
method: string;
|
|
71
|
-
data?: unknown;
|
|
72
|
-
}): Promise<unknown>;
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
export type EntitiesContextType<Row extends {
|
|
76
|
-
id: string;
|
|
77
|
-
}> = {
|
|
78
|
-
list: (params?: EntitiesListParams) => [
|
|
79
|
-
Row[],
|
|
80
|
-
{
|
|
81
|
-
page: number;
|
|
82
|
-
setPage: (page: number) => void;
|
|
83
|
-
count: number | null;
|
|
84
|
-
}
|
|
85
|
-
];
|
|
86
|
-
tiny: EntityTinyCaller<Row>;
|
|
87
|
-
/**
|
|
88
|
-
* Сахар над useEntityForm: форма, создающая запись в этой сущности.
|
|
89
|
-
*
|
|
90
|
-
* Форма значений выводится из defaultValues, поэтому `form.values.name`
|
|
91
|
-
* у потребителя — строка, а не `any`.
|
|
92
|
-
*/
|
|
93
|
-
forms: {
|
|
94
|
-
create: <Shape extends Record<string, unknown>>(args?: {
|
|
95
|
-
defaultValues?: Partial<Shape>;
|
|
96
|
-
/** доработать значения перед отправкой (подставить projectId и т.п.) */
|
|
97
|
-
transformSubmitValues?: (values: Shape) => Record<string, unknown>;
|
|
98
|
-
onSuccess?: (row: Row | null) => void;
|
|
99
|
-
}) => ReturnType<typeof useEntityForm<Shape>>;
|
|
100
|
-
};
|
|
101
|
-
create: (data: Record<string, unknown>) => Promise<Row | null>;
|
|
102
|
-
update: (id: string, data: Partial<Row>, options?: {
|
|
103
|
-
clientOnly?: boolean;
|
|
104
|
-
}) => Promise<void>;
|
|
105
|
-
deleteById: (id: string) => Promise<void>;
|
|
106
|
-
/**
|
|
107
|
-
* Принять запись в локальный список без обращения к серверу.
|
|
108
|
-
*
|
|
109
|
-
* Нужна, когда рядом живут несколько провайдеров одной сущности с разными
|
|
110
|
-
* серверными фильтрами — например колонки доски. Запись, уехавшая из
|
|
111
|
-
* одного фильтра в другой, для второго провайдера просто не существует:
|
|
112
|
-
* его запрос уже выполнен. Сохранение делает вызывающий, здесь только
|
|
113
|
-
* локальное состояние.
|
|
114
|
-
*/
|
|
115
|
-
adopt: (row: Row) => void;
|
|
116
|
-
/**
|
|
117
|
-
* Подгрузить следующую страницу и ДОБАВИТЬ её к списку.
|
|
118
|
-
*
|
|
119
|
-
* Обычная смена страницы список заменяет; здесь он копится — это нужно
|
|
120
|
-
* для догрузки по прокрутке. Возвращает, сколько записей приехало: ноль
|
|
121
|
-
* означает, что дальше ничего нет.
|
|
122
|
-
*/
|
|
123
|
-
loadMore: () => Promise<number>;
|
|
124
|
-
/** сколько записей всего по серверному фильтру провайдера */
|
|
125
|
-
total: number | null;
|
|
126
|
-
};
|
|
127
|
-
export type EntityContextType<Row extends {
|
|
128
|
-
id: string;
|
|
129
|
-
}> = {
|
|
130
|
-
instance: () => Row;
|
|
131
|
-
update: (data: Partial<Row>) => Promise<void>;
|
|
132
|
-
delete: () => Promise<void>;
|
|
133
|
-
controller: Record<string, (data?: unknown) => Promise<unknown>>;
|
|
134
|
-
};
|
|
135
|
-
export declare function createEntityContext<TRow extends {
|
|
136
|
-
id: string;
|
|
137
|
-
}>(entityKey: string): readonly [({ children, filter, limit, sort, }: {
|
|
138
|
-
children: ReactNode;
|
|
139
|
-
/** серверный фильтр списка (общий для всех потребителей) */
|
|
140
|
-
filter?: Record<string, unknown>;
|
|
141
|
-
limit?: number;
|
|
142
|
-
sort?: {
|
|
143
|
-
field: string;
|
|
144
|
-
dir?: "asc" | "desc";
|
|
145
|
-
};
|
|
146
|
-
}) => import("react/jsx-runtime").JSX.Element, ({ children, id, }: {
|
|
147
|
-
children: ReactNode;
|
|
148
|
-
id: string;
|
|
149
|
-
}) => import("react/jsx-runtime").JSX.Element, () => EntitiesContextType<Serialize<TRow> & {
|
|
150
|
-
id: string;
|
|
151
|
-
}>, () => EntityContextType<Serialize<TRow> & {
|
|
152
|
-
id: string;
|
|
153
|
-
}>];
|
|
1
|
+
export { createEntityContext } from "./client/createEntityContext.js";
|
|
2
|
+
export type { EntitiesContextType, EntitiesListParams, EntityContextType, EntityListArgs, EntityTinyCaller, EntityTrpcCaller, } from "./client/types.js";
|