@goplusvn/core 0.1.60 → 0.1.61
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/package.json +1 -1
- package/src/crud/lib/mutation-builder.ts +105 -0
- package/src/crud/lib/query-builder.ts +119 -0
- package/src/crud/server-service.ts +35 -163
package/package.json
CHANGED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { EntityConfig } from "../../types";
|
|
2
|
+
import type { ServerCrudLogger } from "../server-service";
|
|
3
|
+
|
|
4
|
+
export interface PrepareMutationDataOptions {
|
|
5
|
+
data: any;
|
|
6
|
+
config?: EntityConfig;
|
|
7
|
+
mode: "create" | "update";
|
|
8
|
+
systemFields: string[];
|
|
9
|
+
relationFieldSkip?: string[];
|
|
10
|
+
logger: ServerCrudLogger;
|
|
11
|
+
touchUpdatedAtOnCreate?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function prepareMutationData(options: PrepareMutationDataOptions) {
|
|
15
|
+
const { data, config, mode, systemFields, relationFieldSkip, logger, touchUpdatedAtOnCreate } = options;
|
|
16
|
+
|
|
17
|
+
let filtered = data;
|
|
18
|
+
|
|
19
|
+
if (config) {
|
|
20
|
+
// 1. Filter valid fields
|
|
21
|
+
const valid = new Set(config.fields.filter((f) => !(f as any).isDisplayOnly).map((f) => f.name));
|
|
22
|
+
for (const f of systemFields) valid.add(f);
|
|
23
|
+
filtered = {};
|
|
24
|
+
for (const [k, v] of Object.entries(data)) {
|
|
25
|
+
if (valid.has(k)) {
|
|
26
|
+
filtered[k] = v;
|
|
27
|
+
} else {
|
|
28
|
+
logger.warn(`Filtering out invalid field "${k}" for entity "${config.name}"`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 2. Cast field values (boolean, number)
|
|
33
|
+
for (const field of config.fields) {
|
|
34
|
+
const value = filtered[field.name];
|
|
35
|
+
if (value === undefined || value === null) continue;
|
|
36
|
+
|
|
37
|
+
if (field.type === "boolean" || field.type === "switch") {
|
|
38
|
+
let isTrue: boolean;
|
|
39
|
+
if (typeof value === "string") {
|
|
40
|
+
const lv = value.toLowerCase();
|
|
41
|
+
isTrue = lv === "true" || lv === "active" || value === "1" || value === "on";
|
|
42
|
+
} else {
|
|
43
|
+
isTrue = Boolean(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (field.type === "switch" && field.options && field.options.length >= 2) {
|
|
47
|
+
filtered[field.name] = isTrue ? (field.options[0] as any).value : (field.options[1] as any).value;
|
|
48
|
+
} else {
|
|
49
|
+
filtered[field.name] = isTrue;
|
|
50
|
+
}
|
|
51
|
+
} else if (field.type === "number" || (field.type as string) === "integer") {
|
|
52
|
+
if (typeof value === "string") {
|
|
53
|
+
if (value.trim() === "") {
|
|
54
|
+
filtered[field.name] = null;
|
|
55
|
+
} else {
|
|
56
|
+
const num = Number(value);
|
|
57
|
+
if (!isNaN(num)) filtered[field.name] = num;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. System defaults for create
|
|
65
|
+
if (mode === "create") {
|
|
66
|
+
if (!filtered.id) {
|
|
67
|
+
filtered.id = crypto.randomUUID();
|
|
68
|
+
}
|
|
69
|
+
if (touchUpdatedAtOnCreate && !filtered.updatedAt) {
|
|
70
|
+
filtered.updatedAt = new Date();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Transform relation fields (*Id -> connect/disconnect)
|
|
75
|
+
const skip = new Set([
|
|
76
|
+
"id",
|
|
77
|
+
"createdBy",
|
|
78
|
+
"updatedBy",
|
|
79
|
+
"citizenId",
|
|
80
|
+
"targetId",
|
|
81
|
+
...(relationFieldSkip ?? []),
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
const prismaData = { ...filtered };
|
|
85
|
+
for (const key of Object.keys(prismaData)) {
|
|
86
|
+
if (skip.has(key)) continue;
|
|
87
|
+
|
|
88
|
+
if (key.endsWith("Id") && key.length > 2) {
|
|
89
|
+
const rel = key.slice(0, -2);
|
|
90
|
+
const value = prismaData[key];
|
|
91
|
+
|
|
92
|
+
if (value && typeof value === "string" && value.trim() !== "") {
|
|
93
|
+
prismaData[rel] = { connect: { id: value } };
|
|
94
|
+
delete prismaData[key];
|
|
95
|
+
} else if (value === null || value === undefined || value === "") {
|
|
96
|
+
if (mode === "update") {
|
|
97
|
+
prismaData[rel] = { disconnect: true };
|
|
98
|
+
}
|
|
99
|
+
delete prismaData[key];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return prismaData;
|
|
105
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { compileFilterTree } from "./filter-tree";
|
|
2
|
+
import type { CrudQueryParams, EntityConfig } from "../../types";
|
|
3
|
+
|
|
4
|
+
export interface BuildListQueryOptions {
|
|
5
|
+
entity: string;
|
|
6
|
+
config: EntityConfig;
|
|
7
|
+
params: CrudQueryParams;
|
|
8
|
+
scopeWhere?: Record<string, unknown>;
|
|
9
|
+
onDisallowedFilter?: (field: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function buildListQuery({
|
|
13
|
+
entity,
|
|
14
|
+
config,
|
|
15
|
+
params,
|
|
16
|
+
scopeWhere,
|
|
17
|
+
onDisallowedFilter,
|
|
18
|
+
}: BuildListQueryOptions) {
|
|
19
|
+
const { page = 1, pageSize = 10, search, sort, filters, filterTree } = params;
|
|
20
|
+
const MAX_PAGE_SIZE = 200;
|
|
21
|
+
|
|
22
|
+
const safePage = Math.max(1, Number(page) || 1);
|
|
23
|
+
const safePageSize = Math.min(Math.max(1, Number(pageSize) || 10), MAX_PAGE_SIZE);
|
|
24
|
+
const skip = (safePage - 1) * safePageSize;
|
|
25
|
+
const take = safePageSize;
|
|
26
|
+
|
|
27
|
+
const allowedFields = new Set<string>([
|
|
28
|
+
...config.fields.map((f) => f.name),
|
|
29
|
+
"id", "createdAt", "updatedAt", "createdBy", "updatedBy", config.idField || "id",
|
|
30
|
+
]);
|
|
31
|
+
const allowedRelations = new Set<string>(config.include || []);
|
|
32
|
+
const isAllowed = (name: string) => {
|
|
33
|
+
if (!name) return false;
|
|
34
|
+
if (name.includes(".")) return allowedRelations.has(name.split(".")[0]);
|
|
35
|
+
return allowedFields.has(name);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const where: any = { ...(scopeWhere ?? {}) };
|
|
39
|
+
if (search && search.trim()) {
|
|
40
|
+
const term = search.trim();
|
|
41
|
+
const searchFields = config.fields.filter((f) => f.filterable && f.type === "text").map((f) => f.name);
|
|
42
|
+
if (searchFields.length) where.OR = searchFields.map((f) => ({ [f]: { contains: term, mode: "insensitive" } }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (filters && filters.length) {
|
|
46
|
+
for (const filter of filters) {
|
|
47
|
+
const { name, value, operator } = filter as any;
|
|
48
|
+
if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) continue;
|
|
49
|
+
if (!isAllowed(name)) {
|
|
50
|
+
if (onDisallowedFilter) onDisallowedFilter(name);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let target = where;
|
|
54
|
+
let key = name;
|
|
55
|
+
if (name.includes(".")) {
|
|
56
|
+
const parts = name.split(".");
|
|
57
|
+
key = parts.pop()!;
|
|
58
|
+
for (const p of parts) { if (!target[p]) target[p] = {}; target = target[p]; }
|
|
59
|
+
}
|
|
60
|
+
const op = operator as string;
|
|
61
|
+
if (op === "contains") target[key] = { contains: value, mode: "insensitive" };
|
|
62
|
+
else if (op === "in") target[key] = { in: value };
|
|
63
|
+
else if (op === "notIn") target[key] = { notIn: value };
|
|
64
|
+
else if (op === "eq") target[key] = value;
|
|
65
|
+
else if (op === "ne") target[key] = { not: value };
|
|
66
|
+
else if (op === "gt") target[key] = { gt: value };
|
|
67
|
+
else if (op === "gte") target[key] = { gte: value };
|
|
68
|
+
else if (op === "lt") target[key] = { lt: value };
|
|
69
|
+
else if (op === "lte") target[key] = { lte: value };
|
|
70
|
+
else if (op === "startsWith") target[key] = { startsWith: value, mode: "insensitive" };
|
|
71
|
+
else if (op === "endsWith") target[key] = { endsWith: value, mode: "insensitive" };
|
|
72
|
+
else if (op === "isNull") target[key] = null;
|
|
73
|
+
else if (op === "isNotNull") target[key] = { not: null };
|
|
74
|
+
else target[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (filterTree) {
|
|
79
|
+
const compiled = compileFilterTree(filterTree, {
|
|
80
|
+
isAllowed,
|
|
81
|
+
onDisallowed: (field) => {
|
|
82
|
+
if (onDisallowedFilter) onDisallowedFilter(field);
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
if (compiled) {
|
|
86
|
+
where.AND = [...(Array.isArray(where.AND) ? where.AND : where.AND ? [where.AND] : []), compiled];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const orderBy: any = {};
|
|
91
|
+
const applySort = (field: string, direction: any) => {
|
|
92
|
+
if (field.includes(".")) {
|
|
93
|
+
const parts = field.split(".");
|
|
94
|
+
const leaf = parts.pop()!;
|
|
95
|
+
let t = orderBy;
|
|
96
|
+
for (const p of parts) { t[p] = t[p] || {}; t = t[p]; }
|
|
97
|
+
t[leaf] = direction;
|
|
98
|
+
} else orderBy[field] = direction;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (sort && isAllowed(sort.field)) applySort(sort.field, sort.direction);
|
|
102
|
+
else if (config.defaultSort) applySort(config.defaultSort.field, config.defaultSort.direction);
|
|
103
|
+
else if (config.fields.some((f) => f.name === "createdAt")) orderBy.createdAt = "desc";
|
|
104
|
+
else orderBy[config.idField || "id"] = "desc";
|
|
105
|
+
|
|
106
|
+
const include: any = {};
|
|
107
|
+
if (config.include?.length) config.include.forEach((inc) => (include[inc] = true));
|
|
108
|
+
const includeOption = Object.keys(include).length ? { include } : {};
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
where,
|
|
112
|
+
orderBy,
|
|
113
|
+
includeOption,
|
|
114
|
+
skip,
|
|
115
|
+
take,
|
|
116
|
+
safePage,
|
|
117
|
+
safePageSize,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// getModelName: (e) => getModelName(e, MODEL_MAP),
|
|
16
16
|
// });
|
|
17
17
|
|
|
18
|
-
import {
|
|
18
|
+
import { buildListQuery } from "./lib/query-builder";
|
|
19
|
+
import { prepareMutationData } from "./lib/mutation-builder";
|
|
19
20
|
import type { CrudQueryParams, CrudResponse, EntityConfig } from "../types";
|
|
20
21
|
import { serializeDecimalFields } from "../utils/serialize";
|
|
21
22
|
|
|
@@ -136,163 +137,27 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
|
|
|
136
137
|
return m;
|
|
137
138
|
};
|
|
138
139
|
|
|
139
|
-
const filterValidFields = (data: any, config: EntityConfig) => {
|
|
140
|
-
const valid = new Set(config.fields.filter((f) => !(f as any).isDisplayOnly).map((f) => f.name));
|
|
141
|
-
for (const f of systemFields) valid.add(f);
|
|
142
|
-
const out: Record<string, unknown> = {};
|
|
143
|
-
for (const [k, v] of Object.entries(data)) {
|
|
144
|
-
if (valid.has(k)) out[k] = v;
|
|
145
|
-
else log.warn(`Filtering out invalid field "${k}" for entity "${config.name}"`);
|
|
146
|
-
}
|
|
147
|
-
return out;
|
|
148
|
-
};
|
|
149
140
|
|
|
150
|
-
const castFieldValues = (data: any, config: EntityConfig) => {
|
|
151
|
-
const out = { ...data };
|
|
152
|
-
for (const field of config.fields) {
|
|
153
|
-
const value = out[field.name];
|
|
154
|
-
if (value === undefined || value === null) continue;
|
|
155
|
-
if (field.type === "boolean" || field.type === "switch") {
|
|
156
|
-
let isTrue: boolean;
|
|
157
|
-
if (typeof value === "string") {
|
|
158
|
-
const lv = value.toLowerCase();
|
|
159
|
-
isTrue = lv === "true" || lv === "active" || value === "1" || value === "on";
|
|
160
|
-
} else isTrue = Boolean(value);
|
|
161
|
-
if (field.type === "switch" && field.options && field.options.length >= 2) {
|
|
162
|
-
out[field.name] = isTrue ? (field.options[0] as any).value : (field.options[1] as any).value;
|
|
163
|
-
} else out[field.name] = isTrue;
|
|
164
|
-
} else if (field.type === "number" || (field.type as string) === "integer") {
|
|
165
|
-
if (typeof value === "string") {
|
|
166
|
-
if (value.trim() === "") out[field.name] = null;
|
|
167
|
-
else {
|
|
168
|
-
const num = Number(value);
|
|
169
|
-
if (!isNaN(num)) out[field.name] = num;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return out;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const transformRelationFields = (data: any, mode: "create" | "update") => {
|
|
178
|
-
const out = { ...data };
|
|
179
|
-
// citizenId/targetId là legacy-default từ consumer đầu tiên — app mới khai
|
|
180
|
-
// cột scalar *Id của mình qua deps.relationFieldSkip thay vì sửa core.
|
|
181
|
-
const skip = new Set([
|
|
182
|
-
"id",
|
|
183
|
-
"createdBy",
|
|
184
|
-
"updatedBy",
|
|
185
|
-
"citizenId",
|
|
186
|
-
"targetId",
|
|
187
|
-
...(deps.relationFieldSkip ?? []),
|
|
188
|
-
]);
|
|
189
|
-
for (const key of Object.keys(out)) {
|
|
190
|
-
if (skip.has(key)) continue;
|
|
191
|
-
if (key.endsWith("Id") && key.length > 2) {
|
|
192
|
-
const rel = key.slice(0, -2);
|
|
193
|
-
const value = out[key];
|
|
194
|
-
if (value && typeof value === "string" && value.trim() !== "") {
|
|
195
|
-
out[rel] = { connect: { id: value } };
|
|
196
|
-
delete out[key];
|
|
197
|
-
} else if (value === null || value === undefined || value === "") {
|
|
198
|
-
if (mode === "update") out[rel] = { disconnect: true };
|
|
199
|
-
delete out[key];
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
return out;
|
|
204
|
-
};
|
|
205
141
|
|
|
206
142
|
return {
|
|
207
143
|
async list(entity, config, params) {
|
|
208
144
|
const prismaModel = model(entity);
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
const where: any = { ...(deps.scopeWhere?.(entity, config) ?? {}) };
|
|
227
|
-
if (search && search.trim()) {
|
|
228
|
-
const term = search.trim();
|
|
229
|
-
const searchFields = config.fields.filter((f) => f.filterable && f.type === "text").map((f) => f.name);
|
|
230
|
-
if (searchFields.length) where.OR = searchFields.map((f) => ({ [f]: { contains: term, mode: "insensitive" } }));
|
|
231
|
-
}
|
|
232
|
-
if (filters && filters.length) {
|
|
233
|
-
for (const filter of filters) {
|
|
234
|
-
const { name, value, operator } = filter as any;
|
|
235
|
-
if (value === undefined || value === null || (Array.isArray(value) && value.length === 0)) continue;
|
|
236
|
-
if (!isAllowed(name)) {
|
|
237
|
-
log.warn(`Ignoring filter on disallowed field "${name}" for entity "${entity}"`);
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
let target = where;
|
|
241
|
-
let key = name;
|
|
242
|
-
if (name.includes(".")) {
|
|
243
|
-
const parts = name.split(".");
|
|
244
|
-
key = parts.pop()!;
|
|
245
|
-
for (const p of parts) { if (!target[p]) target[p] = {}; target = target[p]; }
|
|
246
|
-
}
|
|
247
|
-
const op = operator as string;
|
|
248
|
-
if (op === "contains") target[key] = { contains: value, mode: "insensitive" };
|
|
249
|
-
else if (op === "in") target[key] = { in: value };
|
|
250
|
-
else if (op === "notIn") target[key] = { notIn: value };
|
|
251
|
-
else if (op === "eq") target[key] = value;
|
|
252
|
-
else if (op === "ne") target[key] = { not: value };
|
|
253
|
-
else if (op === "gt") target[key] = { gt: value };
|
|
254
|
-
else if (op === "gte") target[key] = { gte: value };
|
|
255
|
-
else if (op === "lt") target[key] = { lt: value };
|
|
256
|
-
else if (op === "lte") target[key] = { lte: value };
|
|
257
|
-
else if (op === "startsWith") target[key] = { startsWith: value, mode: "insensitive" };
|
|
258
|
-
else if (op === "endsWith") target[key] = { endsWith: value, mode: "insensitive" };
|
|
259
|
-
else if (op === "isNull") target[key] = null;
|
|
260
|
-
else if (op === "isNotNull") target[key] = { not: null };
|
|
261
|
-
else target[key] = value;
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// Bộ lọc nâng cao dạng cây ($and/$or, nhiều điều kiện cùng field) —
|
|
265
|
-
// cùng guard isAllowed với filter phẳng; AND vào where (search dùng OR
|
|
266
|
-
// nên không đụng nhau). Cây sai cấu trúc → throw (route trả lỗi 4xx).
|
|
267
|
-
if (params.filterTree) {
|
|
268
|
-
const compiled = compileFilterTree(params.filterTree, {
|
|
269
|
-
isAllowed,
|
|
270
|
-
onDisallowed: (field) =>
|
|
271
|
-
log.warn(`Ignoring filterTree condition on disallowed field "${field}" for entity "${entity}"`),
|
|
272
|
-
});
|
|
273
|
-
if (compiled) {
|
|
274
|
-
where.AND = [...(Array.isArray(where.AND) ? where.AND : where.AND ? [where.AND] : []), compiled];
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const orderBy: any = {};
|
|
279
|
-
const applySort = (field: string, direction: any) => {
|
|
280
|
-
if (field.includes(".")) {
|
|
281
|
-
const parts = field.split(".");
|
|
282
|
-
const leaf = parts.pop()!;
|
|
283
|
-
let t = orderBy;
|
|
284
|
-
for (const p of parts) { t[p] = t[p] || {}; t = t[p]; }
|
|
285
|
-
t[leaf] = direction;
|
|
286
|
-
} else orderBy[field] = direction;
|
|
287
|
-
};
|
|
288
|
-
if (sort && isAllowed(sort.field)) applySort(sort.field, sort.direction);
|
|
289
|
-
else if (config.defaultSort) applySort(config.defaultSort.field, config.defaultSort.direction);
|
|
290
|
-
else if (config.fields.some((f) => f.name === "createdAt")) orderBy.createdAt = "desc";
|
|
291
|
-
else orderBy[config.idField || "id"] = "desc";
|
|
292
|
-
|
|
293
|
-
const include: any = {};
|
|
294
|
-
if (config.include?.length) config.include.forEach((inc) => (include[inc] = true));
|
|
295
|
-
const includeOption = Object.keys(include).length ? { include } : {};
|
|
145
|
+
|
|
146
|
+
const {
|
|
147
|
+
where,
|
|
148
|
+
orderBy,
|
|
149
|
+
includeOption,
|
|
150
|
+
skip,
|
|
151
|
+
take,
|
|
152
|
+
safePage,
|
|
153
|
+
safePageSize,
|
|
154
|
+
} = buildListQuery({
|
|
155
|
+
entity,
|
|
156
|
+
config,
|
|
157
|
+
params,
|
|
158
|
+
scopeWhere: deps.scopeWhere?.(entity, config) as Record<string, unknown> | undefined,
|
|
159
|
+
onDisallowedFilter: (field) => log.warn(`Ignoring filter on disallowed field "${field}" for entity "${entity}"`),
|
|
160
|
+
});
|
|
296
161
|
|
|
297
162
|
try {
|
|
298
163
|
const [total, data] = await Promise.all([
|
|
@@ -324,13 +189,15 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
|
|
|
324
189
|
async create(entity, data, config, tx) {
|
|
325
190
|
const prismaModel = model(entity, tx ?? prisma);
|
|
326
191
|
try {
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
192
|
+
const prismaData = prepareMutationData({
|
|
193
|
+
data,
|
|
194
|
+
config,
|
|
195
|
+
mode: "create",
|
|
196
|
+
systemFields,
|
|
197
|
+
relationFieldSkip: deps.relationFieldSkip,
|
|
198
|
+
logger: log,
|
|
199
|
+
touchUpdatedAtOnCreate: deps.touchUpdatedAtOnCreate,
|
|
200
|
+
});
|
|
334
201
|
return serializeDecimalFields(await prismaModel.create({ data: prismaData }));
|
|
335
202
|
} catch (error) {
|
|
336
203
|
log.error(`Error creating ${entity}:`, error);
|
|
@@ -341,9 +208,14 @@ export function createServerCrudService(deps: ServerCrudDeps): ServerCrudService
|
|
|
341
208
|
async update(entity, id, data, config, tx) {
|
|
342
209
|
const prismaModel = model(entity, tx ?? prisma);
|
|
343
210
|
try {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
211
|
+
const prismaData = prepareMutationData({
|
|
212
|
+
data,
|
|
213
|
+
config,
|
|
214
|
+
mode: "update",
|
|
215
|
+
systemFields,
|
|
216
|
+
relationFieldSkip: deps.relationFieldSkip,
|
|
217
|
+
logger: log,
|
|
218
|
+
});
|
|
347
219
|
return serializeDecimalFields(await prismaModel.update({ where: { id }, data: prismaData }));
|
|
348
220
|
} catch (error) {
|
|
349
221
|
log.error(`Error updating ${entity}:`, error);
|