@quark-fw/entity 0.1.6 → 0.1.8
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 +280 -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 +4 -4
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { TRPCError } from "@trpc/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { publicProcedure, router, } from "@quark-fw/plugin-trpc/src/trpc.js";
|
|
4
|
+
import { invoke, modelByName } from "./helpers.js";
|
|
5
|
+
// Сериализуемая сортировка: orderBy в Prisma 8 принимает СЕЛЕКТОР-функцию
|
|
6
|
+
// (f) => f.field.asc(), поэтому по сети передаём {field, dir}, а функцию
|
|
7
|
+
// собираем на сервере. Одно поле: цепочка/массив билдером не поддержаны.
|
|
8
|
+
export const sortSchema = z.object({
|
|
9
|
+
field: z.string(),
|
|
10
|
+
dir: z.enum(["asc", "desc"]).default("asc"),
|
|
11
|
+
});
|
|
12
|
+
const listSchema = z.object({
|
|
13
|
+
where: z.record(z.string(), z.any()).optional(),
|
|
14
|
+
page: z.number().int().min(1).default(1),
|
|
15
|
+
limit: z.number().int().min(1).max(100).default(20),
|
|
16
|
+
sort: sortSchema.optional(),
|
|
17
|
+
/** поиск по подстроке; поле задаётся в createEntity (searchField) */
|
|
18
|
+
search: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
/**
|
|
21
|
+
* Сущность поверх модели контракта.
|
|
22
|
+
*
|
|
23
|
+
* Тип строки берётся из самой модели по её имени, поэтому дженерик передавать
|
|
24
|
+
* не нужно: `createEntity({ model: "Task", ... })`. Имена полей —
|
|
25
|
+
* владельца, сортировки, поиска и связей — проверяются против этой модели.
|
|
26
|
+
*/
|
|
27
|
+
export function createEntity(args) {
|
|
28
|
+
// в конфиге направление необязательно, схема сортировки требует его явно
|
|
29
|
+
const fallbackSort = args.defaultSort && {
|
|
30
|
+
field: args.defaultSort.field,
|
|
31
|
+
dir: args.defaultSort.dir ?? "asc",
|
|
32
|
+
};
|
|
33
|
+
// лениво: quark.db появляется после init prisma-плагина
|
|
34
|
+
const getModel = () => modelByName(args.model);
|
|
35
|
+
const chain = (where) => {
|
|
36
|
+
let q = getModel();
|
|
37
|
+
if (where && Object.keys(where).length)
|
|
38
|
+
q = q.where(where);
|
|
39
|
+
return q;
|
|
40
|
+
};
|
|
41
|
+
// {field, dir} -> селектор Prisma 8. Не все типы полей сортируемы
|
|
42
|
+
// (у boolean, например, нет asc/desc) — отвечаем понятной ошибкой.
|
|
43
|
+
const applySort = (q, sort) => {
|
|
44
|
+
if (!sort)
|
|
45
|
+
return q;
|
|
46
|
+
return q.orderBy((fields) => {
|
|
47
|
+
const direction = fields?.[sort.field]?.[sort.dir];
|
|
48
|
+
if (typeof direction !== "function") {
|
|
49
|
+
throw new TRPCError({
|
|
50
|
+
code: "BAD_REQUEST",
|
|
51
|
+
message: `Field "${sort.field}" of ${args.model} is not sortable`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return direction.call(fields[sort.field]);
|
|
55
|
+
});
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Поиск по подстроке, без учёта регистра (ilike).
|
|
59
|
+
*
|
|
60
|
+
* Условие добавляется отдельным where — они складываются по И, поэтому
|
|
61
|
+
* скоуп владельца и фильтры остаются в силе.
|
|
62
|
+
*/
|
|
63
|
+
const applySearch = (q, search) => {
|
|
64
|
+
const term = search?.trim();
|
|
65
|
+
if (!term || !args.searchField)
|
|
66
|
+
return q;
|
|
67
|
+
// % и _ — служебные символы шаблона; экранируем, чтобы «50%» искалось
|
|
68
|
+
// как текст, а не как «что угодно после 50»
|
|
69
|
+
const escaped = term.replace(/[\\%_]/g, "\\$&");
|
|
70
|
+
return q.where((fields) => {
|
|
71
|
+
const field = fields?.[args.searchField];
|
|
72
|
+
const ilike = field?.ilike;
|
|
73
|
+
if (typeof ilike !== "function") {
|
|
74
|
+
throw new TRPCError({
|
|
75
|
+
code: "BAD_REQUEST",
|
|
76
|
+
message: `Field "${args.searchField}" of ${args.model} is not searchable`,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return ilike.call(field, `%${escaped}%`);
|
|
80
|
+
});
|
|
81
|
+
};
|
|
82
|
+
const baseController = (row) => ({
|
|
83
|
+
update: (data) => chain({ id: row.id }).update(data),
|
|
84
|
+
delete: () => chain({ id: row.id }).delete(),
|
|
85
|
+
});
|
|
86
|
+
const buildController = (row) => ({
|
|
87
|
+
...baseController(row),
|
|
88
|
+
...(args.extendController?.({ entity: row }) ?? {}),
|
|
89
|
+
});
|
|
90
|
+
const withController = (row) => {
|
|
91
|
+
if (!row || typeof row !== "object")
|
|
92
|
+
return row;
|
|
93
|
+
return Object.assign(row, buildController(row));
|
|
94
|
+
};
|
|
95
|
+
const methods = {
|
|
96
|
+
list: async (input) => {
|
|
97
|
+
const { where, page, limit, sort, search } = listSchema.parse(input ?? {});
|
|
98
|
+
const rows = (await applySort(applySearch(chain(where), search), sort ?? fallbackSort)
|
|
99
|
+
.skip(limit * (page - 1))
|
|
100
|
+
.take(limit)
|
|
101
|
+
.all());
|
|
102
|
+
return rows.map(withController);
|
|
103
|
+
},
|
|
104
|
+
getById: async (id) => withController((await chain({ id }).first())),
|
|
105
|
+
/** как getById, но по произвольному where (используется для скоупинга) */
|
|
106
|
+
getOne: async (where) => withController((await chain(where).first())),
|
|
107
|
+
create: async (data) => withController((await getModel().create(data))),
|
|
108
|
+
update: async (id, data) => withController((await chain({ id }).update(data))),
|
|
109
|
+
// Prisma 8 при непопадании во where возвращает null, а не бросает.
|
|
110
|
+
// Молчаливый null скрыл бы и опечатку в id, и попытку тронуть чужую
|
|
111
|
+
// запись — отвечаем NOT_FOUND (существование чужой не подтверждаем).
|
|
112
|
+
updateWhere: async (where, data) => {
|
|
113
|
+
const row = (await chain(where).update(data));
|
|
114
|
+
if (!row) {
|
|
115
|
+
throw new TRPCError({
|
|
116
|
+
code: "NOT_FOUND",
|
|
117
|
+
message: `${args.model} not found`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
return withController(row);
|
|
121
|
+
},
|
|
122
|
+
deleteById: (id) => chain({ id }).delete(),
|
|
123
|
+
deleteWhere: async (where) => {
|
|
124
|
+
const row = await chain(where).delete();
|
|
125
|
+
if (!row) {
|
|
126
|
+
throw new TRPCError({
|
|
127
|
+
code: "NOT_FOUND",
|
|
128
|
+
message: `${args.model} not found`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return row;
|
|
132
|
+
},
|
|
133
|
+
// count() билдера доступен только внутри include-рефайнментов —
|
|
134
|
+
// счёт делается через aggregate
|
|
135
|
+
count: async (where) => {
|
|
136
|
+
const { n } = (await chain(where).aggregate((a) => ({
|
|
137
|
+
n: a.count(),
|
|
138
|
+
})));
|
|
139
|
+
return n;
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
const createRouter = () => {
|
|
143
|
+
const proc = args.procedure ?? publicProcedure;
|
|
144
|
+
// TRPCUser — пустой интерфейс-шов, его поля объявляет плагин
|
|
145
|
+
// авторизации, поэтому id читаем структурно
|
|
146
|
+
const ownerId = (ctx) => ctx.user?.id;
|
|
147
|
+
// where-скоуп владельца: {} если authField не задан
|
|
148
|
+
const scope = (ctx) => args.authField ? { [args.authField]: ownerId(ctx) } : {};
|
|
149
|
+
/**
|
|
150
|
+
* Убирает из присланных данных поля, которые клиент задавать не вправе.
|
|
151
|
+
*
|
|
152
|
+
* Колонка владельца проставляется только из контекста. Иначе владельца
|
|
153
|
+
* можно переписать на чужого: скоуп во where проверяет, что ЭТА запись
|
|
154
|
+
* твоя, но не мешает после этого отдать её другому — так в чужой
|
|
155
|
+
* аккаунт подкладывается запись.
|
|
156
|
+
*
|
|
157
|
+
* id при обновлении тоже не принимаем: подмена первичного ключа рвёт
|
|
158
|
+
* ссылки на запись и не имеет смысла как операция.
|
|
159
|
+
*/
|
|
160
|
+
const stripProtected = (data, also = []) => {
|
|
161
|
+
const clean = { ...data };
|
|
162
|
+
if (args.authField)
|
|
163
|
+
delete clean[args.authField];
|
|
164
|
+
for (const field of also)
|
|
165
|
+
delete clean[field];
|
|
166
|
+
return clean;
|
|
167
|
+
};
|
|
168
|
+
// порт checkRelation: записать ссылку на родителя можно, только если
|
|
169
|
+
// этот родитель принадлежит вызывающему (иначе чужой id «подсунули»)
|
|
170
|
+
const checkRelations = async (data, ctx) => {
|
|
171
|
+
for (const relation of args.relations ?? []) {
|
|
172
|
+
const value = data[relation.field];
|
|
173
|
+
if (value === undefined || value === null)
|
|
174
|
+
continue;
|
|
175
|
+
const parent = await modelByName(relation.model)
|
|
176
|
+
.where({
|
|
177
|
+
[relation.parentField ?? "id"]: value,
|
|
178
|
+
[relation.authField]: ownerId(ctx),
|
|
179
|
+
})
|
|
180
|
+
.first();
|
|
181
|
+
if (!parent) {
|
|
182
|
+
// NOT_FOUND, а не FORBIDDEN: не подтверждаем существование
|
|
183
|
+
// чужой записи
|
|
184
|
+
throw new TRPCError({
|
|
185
|
+
code: "NOT_FOUND",
|
|
186
|
+
message: `${relation.model} ${String(value)} not found`,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const custom = args.extendMethods?.({ model: getModel() }) ?? {};
|
|
192
|
+
const customProcedures = Object.fromEntries(Object.entries(custom).map(([name, m]) => [
|
|
193
|
+
name,
|
|
194
|
+
typeof m === "function"
|
|
195
|
+
? proc
|
|
196
|
+
.input(z.any().optional())
|
|
197
|
+
.mutation(({ input }) => m(input))
|
|
198
|
+
: proc
|
|
199
|
+
.input(m.input)
|
|
200
|
+
.mutation(({ input }) => m.handler(input)),
|
|
201
|
+
]));
|
|
202
|
+
return router({
|
|
203
|
+
...customProcedures,
|
|
204
|
+
list: proc.input(listSchema.optional()).mutation(({ input, ctx }) => methods.list({
|
|
205
|
+
...(input ?? {}),
|
|
206
|
+
where: { ...(input?.where ?? {}), ...scope(ctx) },
|
|
207
|
+
})),
|
|
208
|
+
getById: proc
|
|
209
|
+
.input(z.string())
|
|
210
|
+
.mutation(({ input, ctx }) => methods.getOne({ id: input, ...scope(ctx) })),
|
|
211
|
+
create: proc
|
|
212
|
+
.input(z.object({ data: z.record(z.string(), z.any()) }))
|
|
213
|
+
.mutation(async ({ input, ctx }) => {
|
|
214
|
+
const data = stripProtected(input.data);
|
|
215
|
+
await checkRelations(data, ctx);
|
|
216
|
+
return methods.create({ ...data, ...scope(ctx) });
|
|
217
|
+
}),
|
|
218
|
+
update: proc
|
|
219
|
+
.input(z.object({
|
|
220
|
+
id: z.string(),
|
|
221
|
+
data: z.record(z.string(), z.any()),
|
|
222
|
+
}))
|
|
223
|
+
.mutation(async ({ input, ctx }) => {
|
|
224
|
+
const data = stripProtected(input.data, ["id"]);
|
|
225
|
+
await checkRelations(data, ctx);
|
|
226
|
+
// скоуп во where: чужую запись обновить нельзя
|
|
227
|
+
const where = { id: input.id, ...scope(ctx) };
|
|
228
|
+
// после отсева могло не остаться ни одного поля —
|
|
229
|
+
// отвечаем самой записью, а не ложным NOT_FOUND
|
|
230
|
+
if (!Object.keys(data).length) {
|
|
231
|
+
const row = await methods.getOne(where);
|
|
232
|
+
if (!row) {
|
|
233
|
+
throw new TRPCError({
|
|
234
|
+
code: "NOT_FOUND",
|
|
235
|
+
message: `${args.model} not found`,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
return row;
|
|
239
|
+
}
|
|
240
|
+
return methods.updateWhere(where, data);
|
|
241
|
+
}),
|
|
242
|
+
delete: proc
|
|
243
|
+
.input(z.object({ id: z.string() }))
|
|
244
|
+
.mutation(({ input, ctx }) => methods.deleteWhere({ id: input.id, ...scope(ctx) })),
|
|
245
|
+
count: proc
|
|
246
|
+
.input(z
|
|
247
|
+
.object({
|
|
248
|
+
where: z.record(z.string(), z.any()).optional(),
|
|
249
|
+
})
|
|
250
|
+
.optional())
|
|
251
|
+
.mutation(({ input, ctx }) => methods.count({ ...(input?.where ?? {}), ...scope(ctx) })),
|
|
252
|
+
controller: proc
|
|
253
|
+
.input(z.object({
|
|
254
|
+
id: z.string(),
|
|
255
|
+
method: z.string(),
|
|
256
|
+
data: z.any().optional(),
|
|
257
|
+
}))
|
|
258
|
+
.mutation(async ({ input, ctx }) => {
|
|
259
|
+
const row = (await chain({
|
|
260
|
+
id: input.id,
|
|
261
|
+
...scope(ctx),
|
|
262
|
+
}).first());
|
|
263
|
+
if (!row)
|
|
264
|
+
throw new TRPCError({ code: "NOT_FOUND" });
|
|
265
|
+
const controller = buildController(row);
|
|
266
|
+
// Только собственные методы контроллера: без этой проверки
|
|
267
|
+
// имя вроде "constructor" или "toString" дотягивается до
|
|
268
|
+
// прототипа объекта и вызывается как метод сущности.
|
|
269
|
+
const method = Object.hasOwn(controller, input.method)
|
|
270
|
+
? controller[input.method]
|
|
271
|
+
: undefined;
|
|
272
|
+
if (!method)
|
|
273
|
+
throw new TRPCError({
|
|
274
|
+
code: "BAD_REQUEST",
|
|
275
|
+
message: `Method ${input.method} not found on ${args.model}`,
|
|
276
|
+
});
|
|
277
|
+
return invoke(method, input.data);
|
|
278
|
+
}),
|
|
279
|
+
});
|
|
280
|
+
};
|
|
281
|
+
return {
|
|
282
|
+
modelName: args.model,
|
|
283
|
+
getModel,
|
|
284
|
+
methods,
|
|
285
|
+
createRouter,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CustomMethod, EntityModelHandle } from "./createEntity.js";
|
|
2
|
+
export declare const invoke: (m: CustomMethod, data: unknown) => unknown;
|
|
3
|
+
/** модели контракта по имени: конкретный набор знает только приложение */
|
|
4
|
+
type OrmModels = Record<string, EntityModelHandle | undefined>;
|
|
5
|
+
/**
|
|
6
|
+
* Модели контракта.
|
|
7
|
+
*
|
|
8
|
+
* Тип `quark.db` объявляет само приложение (аугментация Quark), поэтому
|
|
9
|
+
* пакету он не виден — но форма, на которую пакет опирается, известна и
|
|
10
|
+
* описана здесь. Заодно ловим ненастроенный prisma-плагин внятной ошибкой
|
|
11
|
+
* вместо `Cannot read properties of undefined`.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ormModels: () => OrmModels;
|
|
14
|
+
export declare const modelByName: (name: string) => EntityModelHandle;
|
|
15
|
+
export {};
|
package/dist/helpers.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export const invoke = (m, data) => typeof m === "function" ? m(data) : m.handler(m.input.parse(data));
|
|
2
|
+
/**
|
|
3
|
+
* Модели контракта.
|
|
4
|
+
*
|
|
5
|
+
* Тип `quark.db` объявляет само приложение (аугментация Quark), поэтому
|
|
6
|
+
* пакету он не виден — но форма, на которую пакет опирается, известна и
|
|
7
|
+
* описана здесь. Заодно ловим ненастроенный prisma-плагин внятной ошибкой
|
|
8
|
+
* вместо `Cannot read properties of undefined`.
|
|
9
|
+
*/
|
|
10
|
+
export const ormModels = () => {
|
|
11
|
+
const { db } = quark;
|
|
12
|
+
const models = db?.orm?.public;
|
|
13
|
+
if (!models) {
|
|
14
|
+
throw new Error("@quark-fw/entity: quark.db.orm.public недоступен — не подключён @quark/plugin-prisma?");
|
|
15
|
+
}
|
|
16
|
+
return models;
|
|
17
|
+
};
|
|
18
|
+
export const modelByName = (name) => {
|
|
19
|
+
const model = ormModels()[name];
|
|
20
|
+
if (!model) {
|
|
21
|
+
throw new Error(`@quark-fw/entity: модели "${name}" нет в контракте`);
|
|
22
|
+
}
|
|
23
|
+
return model;
|
|
24
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,216 +1,2 @@
|
|
|
1
|
-
import { z, ZodType } from "zod";
|
|
2
|
-
import { publicProcedure, type TRPCContext } from "@quark-fw/plugin-trpc/src/trpc.js";
|
|
3
|
-
import type { EntityField, EntityModelName, EntityRow } from "./types.js";
|
|
4
1
|
export type { EntityModels, EntityModelName, EntityRow, EntityField, } from "./types.js";
|
|
5
|
-
export type CustomMethodFn
|
|
6
|
-
export type CustomMethodValidated<TSchema extends ZodType = ZodType> = {
|
|
7
|
-
input: TSchema;
|
|
8
|
-
handler: (input: z.output<TSchema>) => unknown;
|
|
9
|
-
};
|
|
10
|
-
export type CustomMethod = CustomMethodFn | CustomMethodValidated;
|
|
11
|
-
/**
|
|
12
|
-
* Одно поле модели в селекторах Prisma 8: `f.name.asc()`, `f.name.ilike(...)`.
|
|
13
|
-
* Набор операторов зависит от типа колонки, поэтому проверяется в рантайме.
|
|
14
|
-
*/
|
|
15
|
-
type FieldHandle = {
|
|
16
|
-
[operator: string]: ((...args: string[]) => unknown) | undefined;
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Поля модели в селекторе: `(f) => f.name.asc()`. Конкретный состав известен
|
|
20
|
-
* только сгенерированному контракту приложения, здесь — структура.
|
|
21
|
-
*/
|
|
22
|
-
type SelectorFields = Record<string, FieldHandle | undefined>;
|
|
23
|
-
/** хелперы агрегатов Prisma 8: `(a) => ({ n: a.count() })` */
|
|
24
|
-
type AggregateHelpers = Record<string, () => unknown>;
|
|
25
|
-
export type EntityModelHandle = {
|
|
26
|
-
where(where: Record<string, unknown>): EntityModelHandle;
|
|
27
|
-
where(selector: (fields: SelectorFields) => unknown): EntityModelHandle;
|
|
28
|
-
skip(n: number): EntityModelHandle;
|
|
29
|
-
take(n: number): EntityModelHandle;
|
|
30
|
-
all(): Promise<unknown[]>;
|
|
31
|
-
first(): Promise<unknown | null>;
|
|
32
|
-
orderBy(selector: (fields: SelectorFields) => unknown): EntityModelHandle;
|
|
33
|
-
aggregate(selector: (a: AggregateHelpers) => unknown): Promise<unknown>;
|
|
34
|
-
create(data: unknown): Promise<unknown>;
|
|
35
|
-
update(data: unknown): Promise<unknown>;
|
|
36
|
-
delete(): Promise<unknown>;
|
|
37
|
-
};
|
|
38
|
-
export declare const sortSchema: z.ZodObject<{
|
|
39
|
-
field: z.ZodString;
|
|
40
|
-
dir: z.ZodDefault<z.ZodEnum<{
|
|
41
|
-
asc: "asc";
|
|
42
|
-
desc: "desc";
|
|
43
|
-
}>>;
|
|
44
|
-
}, z.core.$strip>;
|
|
45
|
-
export type EntitySort = z.output<typeof sortSchema>;
|
|
46
|
-
declare const listSchema: z.ZodObject<{
|
|
47
|
-
where: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
48
|
-
page: z.ZodDefault<z.ZodNumber>;
|
|
49
|
-
limit: z.ZodDefault<z.ZodNumber>;
|
|
50
|
-
sort: z.ZodOptional<z.ZodObject<{
|
|
51
|
-
field: z.ZodString;
|
|
52
|
-
dir: z.ZodDefault<z.ZodEnum<{
|
|
53
|
-
asc: "asc";
|
|
54
|
-
desc: "desc";
|
|
55
|
-
}>>;
|
|
56
|
-
}, z.core.$strip>>;
|
|
57
|
-
search: z.ZodOptional<z.ZodString>;
|
|
58
|
-
}, z.core.$strip>;
|
|
59
|
-
/**
|
|
60
|
-
* Билдер, на котором строится роутер сущности.
|
|
61
|
-
*
|
|
62
|
-
* userProcedure — это publicProcedure.use(...), то есть тот же билдер с
|
|
63
|
-
* доуточнённым контекстом; в самих процедурах сущности контекст читается
|
|
64
|
-
* только как `ctx.user?.id`, который есть и в базовом TRPCContext.
|
|
65
|
-
*/
|
|
66
|
-
export type EntityProcedure = typeof publicProcedure;
|
|
67
|
-
/**
|
|
68
|
-
* Связь с родительской моделью.
|
|
69
|
-
*
|
|
70
|
-
* Тип раздаётся по всем моделям контракта, поэтому поля родителя
|
|
71
|
-
* (parentField, authField) проверяются против ИМЕННО той модели, которая
|
|
72
|
-
* указана в model.
|
|
73
|
-
*/
|
|
74
|
-
export type EntityRelation<TModel extends string = EntityModelName> = {
|
|
75
|
-
[TParent in EntityModelName]: {
|
|
76
|
-
/** колонка-ссылка на этой модели, например "projectId" */
|
|
77
|
-
field: EntityField<TModel>;
|
|
78
|
-
/** родительская модель в контракте, например "Project" */
|
|
79
|
-
model: TParent;
|
|
80
|
-
/** колонка родителя, с которой сопоставляется ссылка (по умолчанию id) */
|
|
81
|
-
parentField?: EntityField<TParent>;
|
|
82
|
-
/** колонка владельца на родителе, например "userId" */
|
|
83
|
-
authField: EntityField<TParent>;
|
|
84
|
-
};
|
|
85
|
-
}[EntityModelName];
|
|
86
|
-
export type CreateEntityArgs<TModel extends EntityModelName> = {
|
|
87
|
-
model: TModel;
|
|
88
|
-
authField?: EntityField<TModel>;
|
|
89
|
-
procedure?: EntityProcedure;
|
|
90
|
-
defaultSort?: {
|
|
91
|
-
field: EntityField<TModel>;
|
|
92
|
-
dir?: EntitySort["dir"];
|
|
93
|
-
};
|
|
94
|
-
/**
|
|
95
|
-
* Поле для поиска по подстроке в list({ search }).
|
|
96
|
-
*
|
|
97
|
-
* Одно, а не список: билдер Prisma 8 умеет складывать условия только по И
|
|
98
|
-
* (у полей есть ilike, но ни f.or, ни .or у условия нет), поэтому поиск
|
|
99
|
-
* сразу по нескольким колонкам выражается только отдельными запросами.
|
|
100
|
-
*/
|
|
101
|
-
searchField?: EntityField<TModel>;
|
|
102
|
-
relations?: EntityRelation<TModel>[];
|
|
103
|
-
extendMethods?: (args: {
|
|
104
|
-
model: EntityModelHandle;
|
|
105
|
-
}) => Record<string, CustomMethod>;
|
|
106
|
-
extendController?: (args: {
|
|
107
|
-
entity: EntityRow<TModel>;
|
|
108
|
-
}) => Record<string, CustomMethod>;
|
|
109
|
-
};
|
|
110
|
-
/**
|
|
111
|
-
* Сущность поверх модели контракта.
|
|
112
|
-
*
|
|
113
|
-
* Тип строки берётся из самой модели по её имени, поэтому дженерик передавать
|
|
114
|
-
* не нужно: `createEntity({ model: "Task", ... })`. Имена полей —
|
|
115
|
-
* владельца, сортировки, поиска и связей — проверяются против этой модели.
|
|
116
|
-
*/
|
|
117
|
-
export declare function createEntity<TModel extends EntityModelName>(args: CreateEntityArgs<TModel>): {
|
|
118
|
-
modelName: TModel;
|
|
119
|
-
getModel: () => EntityModelHandle;
|
|
120
|
-
methods: {
|
|
121
|
-
list: (input?: z.input<typeof listSchema> | undefined) => Promise<(EntityRow<TModel> & {
|
|
122
|
-
id: string;
|
|
123
|
-
})[]>;
|
|
124
|
-
getById: (id: string) => Promise<(EntityRow<TModel> & {
|
|
125
|
-
id: string;
|
|
126
|
-
}) | null>;
|
|
127
|
-
/** как getById, но по произвольному where (используется для скоупинга) */
|
|
128
|
-
getOne: (where: Record<string, unknown>) => Promise<(EntityRow<TModel> & {
|
|
129
|
-
id: string;
|
|
130
|
-
}) | null>;
|
|
131
|
-
create: (data: Record<string, unknown>) => Promise<EntityRow<TModel> & {
|
|
132
|
-
id: string;
|
|
133
|
-
}>;
|
|
134
|
-
update: (id: string, data: Record<string, unknown>) => Promise<EntityRow<TModel> & {
|
|
135
|
-
id: string;
|
|
136
|
-
}>;
|
|
137
|
-
updateWhere: (where: Record<string, unknown>, data: Record<string, unknown>) => Promise<EntityRow<TModel> & {
|
|
138
|
-
id: string;
|
|
139
|
-
}>;
|
|
140
|
-
deleteById: (id: string) => Promise<unknown>;
|
|
141
|
-
deleteWhere: (where: Record<string, unknown>) => Promise<{}>;
|
|
142
|
-
count: (where?: Record<string, unknown>) => Promise<number>;
|
|
143
|
-
};
|
|
144
|
-
createRouter: () => import("@trpc/server").TRPCBuiltRouter<{
|
|
145
|
-
ctx: TRPCContext;
|
|
146
|
-
meta: object;
|
|
147
|
-
errorShape: import("@trpc/server").TRPCDefaultErrorShape;
|
|
148
|
-
transformer: false;
|
|
149
|
-
}, import("@trpc/server").TRPCDecorateCreateRouterOptions<{
|
|
150
|
-
list: import("@trpc/server").TRPCMutationProcedure<{
|
|
151
|
-
input: {
|
|
152
|
-
where?: Record<string, any> | undefined;
|
|
153
|
-
page?: number | undefined;
|
|
154
|
-
limit?: number | undefined;
|
|
155
|
-
sort?: {
|
|
156
|
-
field: string;
|
|
157
|
-
dir?: "asc" | "desc" | undefined;
|
|
158
|
-
} | undefined;
|
|
159
|
-
search?: string | undefined;
|
|
160
|
-
} | undefined;
|
|
161
|
-
output: (EntityRow<TModel> & {
|
|
162
|
-
id: string;
|
|
163
|
-
})[];
|
|
164
|
-
meta: object;
|
|
165
|
-
}>;
|
|
166
|
-
getById: import("@trpc/server").TRPCMutationProcedure<{
|
|
167
|
-
input: string;
|
|
168
|
-
output: (EntityRow<TModel> & {
|
|
169
|
-
id: string;
|
|
170
|
-
}) | null;
|
|
171
|
-
meta: object;
|
|
172
|
-
}>;
|
|
173
|
-
create: import("@trpc/server").TRPCMutationProcedure<{
|
|
174
|
-
input: {
|
|
175
|
-
data: Record<string, any>;
|
|
176
|
-
};
|
|
177
|
-
output: EntityRow<TModel> & {
|
|
178
|
-
id: string;
|
|
179
|
-
};
|
|
180
|
-
meta: object;
|
|
181
|
-
}>;
|
|
182
|
-
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
183
|
-
input: {
|
|
184
|
-
id: string;
|
|
185
|
-
data: Record<string, any>;
|
|
186
|
-
};
|
|
187
|
-
output: EntityRow<TModel> & {
|
|
188
|
-
id: string;
|
|
189
|
-
};
|
|
190
|
-
meta: object;
|
|
191
|
-
}>;
|
|
192
|
-
delete: import("@trpc/server").TRPCMutationProcedure<{
|
|
193
|
-
input: {
|
|
194
|
-
id: string;
|
|
195
|
-
};
|
|
196
|
-
output: {};
|
|
197
|
-
meta: object;
|
|
198
|
-
}>;
|
|
199
|
-
count: import("@trpc/server").TRPCMutationProcedure<{
|
|
200
|
-
input: {
|
|
201
|
-
where?: Record<string, any> | undefined;
|
|
202
|
-
} | undefined;
|
|
203
|
-
output: number;
|
|
204
|
-
meta: object;
|
|
205
|
-
}>;
|
|
206
|
-
controller: import("@trpc/server").TRPCMutationProcedure<{
|
|
207
|
-
input: {
|
|
208
|
-
id: string;
|
|
209
|
-
method: string;
|
|
210
|
-
data?: any;
|
|
211
|
-
};
|
|
212
|
-
output: unknown;
|
|
213
|
-
meta: object;
|
|
214
|
-
}>;
|
|
215
|
-
}>>;
|
|
216
|
-
};
|
|
2
|
+
export { createEntity, sortSchema, type CreateEntityArgs, type CustomMethod, type CustomMethodFn, type CustomMethodValidated, type EntityModelHandle, type EntityProcedure, type EntityRelation, type EntitySort, } from "./createEntity.js";
|