@avytrix/nestjs-drizzle-crud 3.8.0 → 3.8.2
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.
|
@@ -11,6 +11,8 @@ const query_parser_1 = require("../utils/query-parser");
|
|
|
11
11
|
const JOIN_KEY = "__crudJoinKey";
|
|
12
12
|
const CURSOR_KEY = "__crudCursor";
|
|
13
13
|
const SCOPE_MEMO = Symbol("scopeMemo");
|
|
14
|
+
const fingerprintTags = new WeakMap();
|
|
15
|
+
let fingerprintCount = 0;
|
|
14
16
|
const ROLLBACK = Symbol("rollback");
|
|
15
17
|
class DeferredDiagnosis {
|
|
16
18
|
settle;
|
|
@@ -127,15 +129,38 @@ class SqlBaseCrudService {
|
|
|
127
129
|
return /(Text|VarChar|Varchar|Char)$/.test(column?.columnType ?? "");
|
|
128
130
|
}
|
|
129
131
|
coerceFilterValue(column, value) {
|
|
130
|
-
if (column?.dataType !== "date"
|
|
132
|
+
if (column?.dataType !== "date")
|
|
131
133
|
return value;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return date;
|
|
136
|
-
}
|
|
134
|
+
const date = this.toDate(value);
|
|
135
|
+
if (date)
|
|
136
|
+
return date;
|
|
137
137
|
throw new common_1.BadRequestException(`Invalid date filter value: ${value}`);
|
|
138
138
|
}
|
|
139
|
+
toDate(value) {
|
|
140
|
+
const date = value instanceof Date
|
|
141
|
+
? value
|
|
142
|
+
: typeof value === "string" || typeof value === "number"
|
|
143
|
+
? new Date(value)
|
|
144
|
+
: undefined;
|
|
145
|
+
return date && !Number.isNaN(date.getTime()) ? date : undefined;
|
|
146
|
+
}
|
|
147
|
+
coerceDates(table, row) {
|
|
148
|
+
for (const [key, value] of Object.entries(row)) {
|
|
149
|
+
const column = this.columnOf(table, key);
|
|
150
|
+
if (column?.dataType !== "date" ||
|
|
151
|
+
!(typeof value === "string" ||
|
|
152
|
+
typeof value === "number" ||
|
|
153
|
+
value instanceof Date)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const date = this.toDate(value);
|
|
157
|
+
if (!date) {
|
|
158
|
+
throw new crud_exceptions_1.ValidationFailedException(`a date or time value has an invalid format: ${column.name ?? key}`);
|
|
159
|
+
}
|
|
160
|
+
row[key] = date;
|
|
161
|
+
}
|
|
162
|
+
return row;
|
|
163
|
+
}
|
|
139
164
|
static ALLOWLIST_LABELS = {
|
|
140
165
|
filterableColumns: "Filtering on",
|
|
141
166
|
sortableColumns: "Sorting by",
|
|
@@ -192,23 +217,37 @@ class SqlBaseCrudService {
|
|
|
192
217
|
const scope = this.config.scope;
|
|
193
218
|
if (!scope || options?.skipScope)
|
|
194
219
|
return null;
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
const
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
const context = options?.context ?? crud_context_1.CrudContext.current();
|
|
221
|
+
const found = options?.[SCOPE_MEMO];
|
|
222
|
+
const memo = found?.ended ? undefined : found;
|
|
223
|
+
const mine = memo?.get(this);
|
|
224
|
+
if (mine?.has(context))
|
|
225
|
+
return mine.get(context);
|
|
226
|
+
const values = typeof scope === "function" ? scope(context ?? {}) : scope;
|
|
201
227
|
const checked = this.checkScope(values);
|
|
202
|
-
if (
|
|
203
|
-
|
|
228
|
+
if (mine)
|
|
229
|
+
mine.set(context, checked);
|
|
230
|
+
else
|
|
231
|
+
memo?.set(this, new Map([[context, checked]]));
|
|
204
232
|
return checked;
|
|
205
233
|
}
|
|
206
234
|
withScopeMemo(options) {
|
|
207
|
-
|
|
208
|
-
|
|
235
|
+
const memo = options?.[SCOPE_MEMO];
|
|
236
|
+
if (typeof this.config.scope !== "function" || (memo && !memo.ended)) {
|
|
209
237
|
return options;
|
|
210
238
|
}
|
|
211
|
-
return { ...options, [SCOPE_MEMO]:
|
|
239
|
+
return { ...options, [SCOPE_MEMO]: new Map() };
|
|
240
|
+
}
|
|
241
|
+
async scopedCall(options, run) {
|
|
242
|
+
const scoped = this.withScopeMemo(options);
|
|
243
|
+
const started = scoped === options ? undefined : scoped[SCOPE_MEMO];
|
|
244
|
+
try {
|
|
245
|
+
return await run(scoped);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
if (started)
|
|
249
|
+
started.ended = true;
|
|
250
|
+
}
|
|
212
251
|
}
|
|
213
252
|
checkScope(values) {
|
|
214
253
|
if (values === null)
|
|
@@ -327,8 +366,9 @@ class SqlBaseCrudService {
|
|
|
327
366
|
query = dialect.sqlToQuery(...args);
|
|
328
367
|
}
|
|
329
368
|
catch (error) {
|
|
330
|
-
if (error instanceof RangeError)
|
|
369
|
+
if (error instanceof RangeError && /call stack/i.test(error.message)) {
|
|
331
370
|
throw tooMany(`more than ${limit}`);
|
|
371
|
+
}
|
|
332
372
|
throw error;
|
|
333
373
|
}
|
|
334
374
|
const count = query?.params?.length ?? 0;
|
|
@@ -1190,7 +1230,7 @@ class SqlBaseCrudService {
|
|
|
1190
1230
|
if (this.config.events && !options?.skipEvents)
|
|
1191
1231
|
return false;
|
|
1192
1232
|
const base = SqlBaseCrudService.prototype;
|
|
1193
|
-
return methods.every((name) => this[name] === base[name]);
|
|
1233
|
+
return ["emitEvent", ...methods].every((name) => this[name] === base[name]);
|
|
1194
1234
|
}
|
|
1195
1235
|
async guardOnError(guarded, id, options, run) {
|
|
1196
1236
|
try {
|
|
@@ -1244,14 +1284,16 @@ class SqlBaseCrudService {
|
|
|
1244
1284
|
};
|
|
1245
1285
|
}
|
|
1246
1286
|
async find(id, options) {
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1287
|
+
return this.scopedCall(options, async (options) => {
|
|
1288
|
+
this.checkReadOptions(options);
|
|
1289
|
+
return this.findFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], options);
|
|
1290
|
+
});
|
|
1250
1291
|
}
|
|
1251
1292
|
async findOne(where, options) {
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1293
|
+
return this.scopedCall(options, async (options) => {
|
|
1294
|
+
this.checkReadOptions(options);
|
|
1295
|
+
return this.findFirst(this.buildWhereConditionsFromPartial(where), options);
|
|
1296
|
+
});
|
|
1255
1297
|
}
|
|
1256
1298
|
async findFirst(conditions, options) {
|
|
1257
1299
|
const first = await this.readFirst(conditions, options);
|
|
@@ -1332,105 +1374,107 @@ class SqlBaseCrudService {
|
|
|
1332
1374
|
return query;
|
|
1333
1375
|
}
|
|
1334
1376
|
async findAll(filters, pagination, options) {
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1377
|
+
return this.scopedCall(options, async (options) => {
|
|
1378
|
+
const { sortBy, sortOrder = "asc" } = pagination || {};
|
|
1379
|
+
const withCount = pagination?.withCount !== false;
|
|
1380
|
+
const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
1381
|
+
const plan = this.planList(filters, sortBy, sortOrder, options);
|
|
1382
|
+
let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, plan.joinRelations.length > 0, plan.selection);
|
|
1383
|
+
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1384
|
+
if (plan.conditions.length > 0) {
|
|
1385
|
+
dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...plan.conditions));
|
|
1386
|
+
}
|
|
1387
|
+
if (plan.sort.length > 0) {
|
|
1388
|
+
dataQuery = dataQuery.orderBy(...plan.sort.map((spec) => this.orderOf(spec)));
|
|
1389
|
+
}
|
|
1390
|
+
dataQuery = dataQuery
|
|
1391
|
+
.limit(withCount ? safeLimit : safeLimit + 1)
|
|
1392
|
+
.offset(offset);
|
|
1393
|
+
dataQuery = this.applyLock(dataQuery, options);
|
|
1394
|
+
const countQuery = withCount
|
|
1395
|
+
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
|
|
1396
|
+
: Promise.resolve(undefined);
|
|
1397
|
+
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
1398
|
+
const data = fetched.slice(0, safeLimit);
|
|
1399
|
+
const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
|
|
1400
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1401
|
+
if (!withCount) {
|
|
1402
|
+
return {
|
|
1403
|
+
data: rows,
|
|
1404
|
+
total: null,
|
|
1405
|
+
page,
|
|
1406
|
+
limit: safeLimit,
|
|
1407
|
+
hasMore: fetched.length > safeLimit,
|
|
1408
|
+
};
|
|
1409
|
+
}
|
|
1360
1410
|
return {
|
|
1361
1411
|
data: rows,
|
|
1362
|
-
total:
|
|
1412
|
+
total: this.parseCount(totalResult),
|
|
1363
1413
|
page,
|
|
1364
1414
|
limit: safeLimit,
|
|
1365
|
-
hasMore: fetched.length > safeLimit,
|
|
1366
1415
|
};
|
|
1367
|
-
}
|
|
1368
|
-
return {
|
|
1369
|
-
data: rows,
|
|
1370
|
-
total: this.parseCount(totalResult),
|
|
1371
|
-
page,
|
|
1372
|
-
limit: safeLimit,
|
|
1373
|
-
};
|
|
1416
|
+
});
|
|
1374
1417
|
}
|
|
1375
1418
|
async findByCursor(filters, pagination, options) {
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1419
|
+
return this.scopedCall(options, async (options) => {
|
|
1420
|
+
const { sortBy, sortOrder = "asc", count = false } = pagination || {};
|
|
1421
|
+
const { limit } = this.resolvePagination(1, pagination?.limit);
|
|
1422
|
+
const plan = this.planList(filters, sortBy, sortOrder, options);
|
|
1423
|
+
const signature = plan.sort
|
|
1424
|
+
.map((spec) => `${spec.key}:${spec.order}`)
|
|
1425
|
+
.join(",");
|
|
1426
|
+
const cursor = pagination?.cursor ?? pagination?.after;
|
|
1427
|
+
const conditions = [...plan.conditions];
|
|
1428
|
+
if (cursor !== undefined && cursor !== null && cursor !== "") {
|
|
1429
|
+
if (typeof cursor !== "string") {
|
|
1430
|
+
throw new common_1.BadRequestException("Invalid cursor");
|
|
1431
|
+
}
|
|
1432
|
+
const values = (0, cursor_1.decodeCursor)(cursor, signature, plan.sort.length);
|
|
1433
|
+
conditions.push(this.keysetCondition(plan.sort, values));
|
|
1434
|
+
}
|
|
1435
|
+
const fields = this.buildSelection(plan.baseSelect, plan.joined, plan.selection);
|
|
1436
|
+
const cursorKeys = plan.sort.map((spec, i) => {
|
|
1437
|
+
const field = this.cursorField(spec.column);
|
|
1438
|
+
const projected = field === spec.column && !spec.relation
|
|
1439
|
+
? Object.keys(fields).find((key) => fields[key] === spec.column)
|
|
1440
|
+
: undefined;
|
|
1441
|
+
if (projected)
|
|
1442
|
+
return projected;
|
|
1443
|
+
fields[`${CURSOR_KEY}${i}`] = field;
|
|
1444
|
+
return `${CURSOR_KEY}${i}`;
|
|
1445
|
+
});
|
|
1446
|
+
const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
|
|
1447
|
+
let dataQuery = plan.db.select(fields).from(this.config.table);
|
|
1448
|
+
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1449
|
+
if (conditions.length > 0)
|
|
1450
|
+
dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
|
|
1451
|
+
dataQuery = dataQuery
|
|
1452
|
+
.orderBy(...plan.sort.map((spec) => this.orderOf(spec)))
|
|
1453
|
+
.limit(limit + 1);
|
|
1454
|
+
dataQuery = this.applyLock(dataQuery, options);
|
|
1455
|
+
const [data, totalResult] = await this.runRead(Promise.all([
|
|
1456
|
+
dataQuery,
|
|
1457
|
+
count
|
|
1458
|
+
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
|
|
1459
|
+
: Promise.resolve(undefined),
|
|
1460
|
+
]));
|
|
1461
|
+
const page = data.slice(0, limit);
|
|
1462
|
+
const last = page[page.length - 1];
|
|
1463
|
+
const nextCursor = data.length > limit && last
|
|
1464
|
+
? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
|
|
1465
|
+
: null;
|
|
1466
|
+
for (const row of page) {
|
|
1467
|
+
for (const key of hidden)
|
|
1468
|
+
delete row[key];
|
|
1469
|
+
}
|
|
1470
|
+
const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
|
|
1471
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1472
|
+
const out = { data: rows, limit, nextCursor };
|
|
1473
|
+
if (count) {
|
|
1474
|
+
out.total = this.parseCount(totalResult);
|
|
1475
|
+
}
|
|
1476
|
+
return out;
|
|
1402
1477
|
});
|
|
1403
|
-
const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
|
|
1404
|
-
let dataQuery = plan.db.select(fields).from(this.config.table);
|
|
1405
|
-
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1406
|
-
if (conditions.length > 0)
|
|
1407
|
-
dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
|
|
1408
|
-
dataQuery = dataQuery
|
|
1409
|
-
.orderBy(...plan.sort.map((spec) => this.orderOf(spec)))
|
|
1410
|
-
.limit(limit + 1);
|
|
1411
|
-
dataQuery = this.applyLock(dataQuery, options);
|
|
1412
|
-
const [data, totalResult] = await this.runRead(Promise.all([
|
|
1413
|
-
dataQuery,
|
|
1414
|
-
count
|
|
1415
|
-
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
|
|
1416
|
-
: Promise.resolve(undefined),
|
|
1417
|
-
]));
|
|
1418
|
-
const page = data.slice(0, limit);
|
|
1419
|
-
const last = page[page.length - 1];
|
|
1420
|
-
const nextCursor = data.length > limit && last
|
|
1421
|
-
? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
|
|
1422
|
-
: null;
|
|
1423
|
-
for (const row of page) {
|
|
1424
|
-
for (const key of hidden)
|
|
1425
|
-
delete row[key];
|
|
1426
|
-
}
|
|
1427
|
-
const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
|
|
1428
|
-
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1429
|
-
const out = { data: rows, limit, nextCursor };
|
|
1430
|
-
if (count) {
|
|
1431
|
-
out.total = this.parseCount(totalResult);
|
|
1432
|
-
}
|
|
1433
|
-
return out;
|
|
1434
1478
|
}
|
|
1435
1479
|
cursorField(column) {
|
|
1436
1480
|
if (column?.dataType !== "date")
|
|
@@ -1550,8 +1594,8 @@ class SqlBaseCrudService {
|
|
|
1550
1594
|
const hosts = rows.filter(Boolean);
|
|
1551
1595
|
await this.hydrate(db, hosts, nodes, undefined, options);
|
|
1552
1596
|
}
|
|
1553
|
-
loadsAfterCommit(options) {
|
|
1554
|
-
if (options?.transaction)
|
|
1597
|
+
loadsAfterCommit(operation, options) {
|
|
1598
|
+
if (operation === "create" || options?.transaction)
|
|
1555
1599
|
return false;
|
|
1556
1600
|
try {
|
|
1557
1601
|
return this.countPlan(options?.counts).every((count) => !count.filter);
|
|
@@ -1560,11 +1604,14 @@ class SqlBaseCrudService {
|
|
|
1560
1604
|
return false;
|
|
1561
1605
|
}
|
|
1562
1606
|
}
|
|
1563
|
-
async writeThenLoad(options, write) {
|
|
1564
|
-
|
|
1565
|
-
const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written) =>
|
|
1566
|
-
|
|
1567
|
-
|
|
1607
|
+
async writeThenLoad(operation, options, write) {
|
|
1608
|
+
let late = this.loadsAfterCommit(operation, options);
|
|
1609
|
+
const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written, inside = false) => {
|
|
1610
|
+
if (late && !inside)
|
|
1611
|
+
return Promise.resolve();
|
|
1612
|
+
late = false;
|
|
1613
|
+
return this.loadWriteRelations(inTx, written, options);
|
|
1614
|
+
}), options?.transaction);
|
|
1568
1615
|
if (late)
|
|
1569
1616
|
await this.loadWriteRelations(this.dbFor(), rows, options);
|
|
1570
1617
|
return rows;
|
|
@@ -1593,7 +1640,7 @@ class SqlBaseCrudService {
|
|
|
1593
1640
|
const version = this.config.versionColumn;
|
|
1594
1641
|
if (version)
|
|
1595
1642
|
delete mapped[version];
|
|
1596
|
-
const entityData = this.filterWritable(mapped, "create", options);
|
|
1643
|
+
const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "create", options));
|
|
1597
1644
|
const now = new Date();
|
|
1598
1645
|
if (this.config.timestamps?.createdAt)
|
|
1599
1646
|
entityData[this.config.timestamps.createdAt] = now;
|
|
@@ -1623,35 +1670,36 @@ class SqlBaseCrudService {
|
|
|
1623
1670
|
return createdEntity;
|
|
1624
1671
|
}
|
|
1625
1672
|
async create(data, options) {
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1673
|
+
return this.scopedCall(options, async (options) => {
|
|
1674
|
+
const { transaction, hooks = {} } = options || {};
|
|
1675
|
+
this.checkReadOptions(options);
|
|
1676
|
+
const entityData = await this.prepareCreate(data, options);
|
|
1677
|
+
const nested = this.takeNested(entityData, options);
|
|
1678
|
+
const db = this.dbFor(transaction);
|
|
1679
|
+
await this.emitEvent("before", "create", false, { input: data, transaction }, options);
|
|
1680
|
+
try {
|
|
1681
|
+
await this.assertKeysInScope(db, entityData, options, nested.map((entry) => entry.name));
|
|
1682
|
+
const createdEntity = nested.length === 0
|
|
1683
|
+
? await this.insertOne(db, entityData, options)
|
|
1684
|
+
: await this.executeSqlTransaction((tx) => this.insertNested(tx, entityData, nested, {
|
|
1685
|
+
...options,
|
|
1686
|
+
transaction: tx,
|
|
1687
|
+
}), transaction);
|
|
1688
|
+
if (!hooks.skipAfter)
|
|
1689
|
+
await this.afterCreate(createdEntity);
|
|
1690
|
+
await this.emitEvent("after", "create", false, {
|
|
1691
|
+
ids: this.idsOf([createdEntity]),
|
|
1692
|
+
input: data,
|
|
1693
|
+
rows: [createdEntity],
|
|
1694
|
+
transaction,
|
|
1695
|
+
}, options);
|
|
1696
|
+
await this.loadWriteRelations(db, [createdEntity], options);
|
|
1697
|
+
return createdEntity;
|
|
1698
|
+
}
|
|
1699
|
+
catch (error) {
|
|
1700
|
+
throw this.toClientError(error, "write");
|
|
1701
|
+
}
|
|
1702
|
+
});
|
|
1655
1703
|
}
|
|
1656
1704
|
takeNested(entity, options) {
|
|
1657
1705
|
const allowed = this.config.writableRelations;
|
|
@@ -1684,9 +1732,11 @@ class SqlBaseCrudService {
|
|
|
1684
1732
|
if (!(0, merge_config_1.isPlainObject)(value)) {
|
|
1685
1733
|
throw new crud_exceptions_1.ValidationFailedException(`the relation "${name}" takes an object to create`);
|
|
1686
1734
|
}
|
|
1735
|
+
const parentRow = this.relatedRow(relation.table, value, options);
|
|
1736
|
+
await this.assertNestedKeysInScope(tx, relation, [parentRow], options);
|
|
1687
1737
|
const [parent] = await tx
|
|
1688
1738
|
.insert(relation.table)
|
|
1689
|
-
.values(
|
|
1739
|
+
.values(parentRow)
|
|
1690
1740
|
.returning();
|
|
1691
1741
|
row[relation.localKey] = parent[relation.references ?? "id"];
|
|
1692
1742
|
}
|
|
@@ -1708,6 +1758,7 @@ class SqlBaseCrudService {
|
|
|
1708
1758
|
[relation.foreignKey]: parentValue,
|
|
1709
1759
|
};
|
|
1710
1760
|
});
|
|
1761
|
+
await this.assertNestedKeysInScope(tx, relation, children, options);
|
|
1711
1762
|
if (children.length > 0) {
|
|
1712
1763
|
await tx.insert(relation.table).values(children);
|
|
1713
1764
|
}
|
|
@@ -1716,9 +1767,11 @@ class SqlBaseCrudService {
|
|
|
1716
1767
|
const ids = value.filter((item) => !(0, merge_config_1.isPlainObject)(item));
|
|
1717
1768
|
const fresh = value.filter((item) => (0, merge_config_1.isPlainObject)(item));
|
|
1718
1769
|
if (fresh.length > 0) {
|
|
1770
|
+
const freshRows = fresh.map((item) => this.relatedRow(relation.table, item, options));
|
|
1771
|
+
await this.assertNestedKeysInScope(tx, relation, freshRows, options);
|
|
1719
1772
|
const inserted = await tx
|
|
1720
1773
|
.insert(relation.table)
|
|
1721
|
-
.values(
|
|
1774
|
+
.values(freshRows)
|
|
1722
1775
|
.returning();
|
|
1723
1776
|
ids.push(...inserted.map((r) => r[relation.references ?? "id"]));
|
|
1724
1777
|
}
|
|
@@ -1752,13 +1805,17 @@ class SqlBaseCrudService {
|
|
|
1752
1805
|
if (this.columnOf(table, key))
|
|
1753
1806
|
out[key] = item;
|
|
1754
1807
|
}
|
|
1755
|
-
return out;
|
|
1808
|
+
return this.coerceDates(table, out);
|
|
1756
1809
|
}
|
|
1757
|
-
async keysOutsideScope(db, rows, options, skip = []
|
|
1810
|
+
async keysOutsideScope(db, rows, options, skip = [], owner = {
|
|
1811
|
+
table: this.config.table,
|
|
1812
|
+
relations: this.config.relations,
|
|
1813
|
+
}) {
|
|
1758
1814
|
const failures = new Map();
|
|
1759
1815
|
if (!this.resolveScope(options))
|
|
1760
1816
|
return failures;
|
|
1761
|
-
|
|
1817
|
+
const checks = [];
|
|
1818
|
+
for (const [name, relation] of Object.entries(owner.relations ?? {})) {
|
|
1762
1819
|
const local = relation.localKey;
|
|
1763
1820
|
if (!local || this.isCollection(relation) || skip.includes(name)) {
|
|
1764
1821
|
continue;
|
|
@@ -1767,23 +1824,34 @@ class SqlBaseCrudService {
|
|
|
1767
1824
|
const referenced = relation.table[relation.references ?? "id"];
|
|
1768
1825
|
if (scope.length === 0 || !referenced)
|
|
1769
1826
|
continue;
|
|
1770
|
-
const keys = this.
|
|
1827
|
+
const keys = this.distinct(rows.map((row) => row[local]).filter((value) => value != null));
|
|
1771
1828
|
if (keys.length === 0)
|
|
1772
1829
|
continue;
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
.
|
|
1777
|
-
|
|
1830
|
+
checks.push({
|
|
1831
|
+
local,
|
|
1832
|
+
keys,
|
|
1833
|
+
query: this.matchKeys(db, relation.table, referenced, keys, scope),
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
let found;
|
|
1837
|
+
try {
|
|
1838
|
+
found = await Promise.all(checks.map((check) => check.query));
|
|
1839
|
+
}
|
|
1840
|
+
catch (error) {
|
|
1841
|
+
throw this.toClientError(error, "write");
|
|
1842
|
+
}
|
|
1843
|
+
checks.forEach(({ local, keys }, i) => {
|
|
1844
|
+
const matched = found[i];
|
|
1845
|
+
const present = new Set(keys.filter((_, at) => matched.has(at)));
|
|
1778
1846
|
rows.forEach((row, index) => {
|
|
1779
1847
|
const value = row[local];
|
|
1780
|
-
if (value == null || present.has(
|
|
1848
|
+
if (value == null || present.has(value))
|
|
1781
1849
|
return;
|
|
1782
1850
|
if (!failures.has(index)) {
|
|
1783
|
-
failures.set(index, this.missingParentException(local));
|
|
1851
|
+
failures.set(index, this.missingParentException(local, owner.table));
|
|
1784
1852
|
}
|
|
1785
1853
|
});
|
|
1786
|
-
}
|
|
1854
|
+
});
|
|
1787
1855
|
return failures;
|
|
1788
1856
|
}
|
|
1789
1857
|
async assertBatchKeysInScope(db, rows, refs, message, options) {
|
|
@@ -1797,29 +1865,35 @@ class SqlBaseCrudService {
|
|
|
1797
1865
|
if (failure)
|
|
1798
1866
|
throw failure;
|
|
1799
1867
|
}
|
|
1800
|
-
|
|
1868
|
+
async assertNestedKeysInScope(tx, relation, rows, options) {
|
|
1869
|
+
if (!relation.relations || rows.length === 0)
|
|
1870
|
+
return;
|
|
1871
|
+
const failures = await this.keysOutsideScope(tx, rows, options, [], {
|
|
1872
|
+
table: relation.table,
|
|
1873
|
+
relations: relation.relations,
|
|
1874
|
+
});
|
|
1875
|
+
if (failures.size === 0)
|
|
1876
|
+
return;
|
|
1877
|
+
const first = [...failures.keys()].reduce((a, b) => Math.min(a, b));
|
|
1878
|
+
throw failures.get(first);
|
|
1879
|
+
}
|
|
1880
|
+
missingParentException(localKey, table = this.config.table) {
|
|
1801
1881
|
const field = this.config.dialect === "postgresql"
|
|
1802
|
-
? (this.columnOf(
|
|
1882
|
+
? (this.columnOf(table, localKey)?.name ?? localKey)
|
|
1803
1883
|
: "reference";
|
|
1804
1884
|
return new crud_exceptions_1.ForeignKeyViolationException(this.getEntityName(), field);
|
|
1805
1885
|
}
|
|
1806
1886
|
async linkTargets(tx, relation, parentValue, ids, options, fresh = false) {
|
|
1807
|
-
const wanted = this.uniqueValues(ids);
|
|
1808
|
-
if (wanted.length === 0)
|
|
1809
|
-
return;
|
|
1810
1887
|
const target = relation.table;
|
|
1811
1888
|
const referenced = target[relation.references ?? "id"];
|
|
1889
|
+
const wanted = this.distinct(ids);
|
|
1890
|
+
if (wanted.length === 0)
|
|
1891
|
+
return;
|
|
1812
1892
|
const through = relation.through;
|
|
1813
1893
|
const local = through[relation.throughLocalKey];
|
|
1814
1894
|
const foreign = through[relation.throughForeignKey];
|
|
1815
|
-
const
|
|
1816
|
-
|
|
1817
|
-
const found = await tx
|
|
1818
|
-
.select({ id: referenced })
|
|
1819
|
-
.from(target)
|
|
1820
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
1821
|
-
const present = new Set(found.map((row) => this.valueKey(row.id)));
|
|
1822
|
-
const missing = wanted.filter((id) => !present.has(this.valueKey(id)));
|
|
1895
|
+
const found = await this.matchKeys(tx, target, referenced, wanted, this.relatedRowScope(target, options));
|
|
1896
|
+
const missing = wanted.filter((_, at) => !found.has(at));
|
|
1823
1897
|
if (missing.length > 0) {
|
|
1824
1898
|
const name = this.tableName(target);
|
|
1825
1899
|
throw new crud_exceptions_1.BulkOperationException("Unknown related ids", missing.map((id) => ({
|
|
@@ -1827,14 +1901,13 @@ class SqlBaseCrudService {
|
|
|
1827
1901
|
error: new crud_exceptions_1.EntityNotFoundException(name, id),
|
|
1828
1902
|
})));
|
|
1829
1903
|
}
|
|
1904
|
+
const stored = this.distinct(wanted.map((_, at) => found.get(at)));
|
|
1830
1905
|
const linked = fresh
|
|
1831
|
-
?
|
|
1832
|
-
: await tx
|
|
1833
|
-
.
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const already = new Set(linked.map((row) => this.valueKey(row.id)));
|
|
1837
|
-
const add = wanted.filter((id) => !already.has(this.valueKey(id)));
|
|
1906
|
+
? new Map()
|
|
1907
|
+
: await this.matchKeys(tx, through, foreign, stored, [
|
|
1908
|
+
(0, drizzle_orm_1.eq)(local, parentValue),
|
|
1909
|
+
]);
|
|
1910
|
+
const add = stored.filter((_, at) => !linked.has(at));
|
|
1838
1911
|
if (add.length > 0) {
|
|
1839
1912
|
await tx.insert(through).values(add.map((id) => ({
|
|
1840
1913
|
[relation.throughLocalKey]: parentValue,
|
|
@@ -1843,16 +1916,19 @@ class SqlBaseCrudService {
|
|
|
1843
1916
|
}
|
|
1844
1917
|
}
|
|
1845
1918
|
async attach(id, relation, relatedIds, options) {
|
|
1846
|
-
|
|
1847
|
-
|
|
1919
|
+
return this.scopedCall(options, async (options) => {
|
|
1920
|
+
return this.changeLinks("attach", id, relation, relatedIds, options);
|
|
1921
|
+
});
|
|
1848
1922
|
}
|
|
1849
1923
|
async detach(id, relation, relatedIds, options) {
|
|
1850
|
-
|
|
1851
|
-
|
|
1924
|
+
return this.scopedCall(options, async (options) => {
|
|
1925
|
+
return this.changeLinks("detach", id, relation, relatedIds, options);
|
|
1926
|
+
});
|
|
1852
1927
|
}
|
|
1853
1928
|
async sync(id, relation, relatedIds, options) {
|
|
1854
|
-
|
|
1855
|
-
|
|
1929
|
+
return this.scopedCall(options, async (options) => {
|
|
1930
|
+
return this.changeLinks("sync", id, relation, relatedIds, options);
|
|
1931
|
+
});
|
|
1856
1932
|
}
|
|
1857
1933
|
async changeLinks(operation, id, name, relatedIds, options) {
|
|
1858
1934
|
const relation = this.relationNamed(name);
|
|
@@ -1872,7 +1948,7 @@ class SqlBaseCrudService {
|
|
|
1872
1948
|
throw new common_1.BadRequestException(`${operation} takes an array of ids`);
|
|
1873
1949
|
}
|
|
1874
1950
|
this.checkReadOptions(options);
|
|
1875
|
-
const ids = this.
|
|
1951
|
+
const ids = this.distinct(relatedIds ?? []);
|
|
1876
1952
|
const parentKey = this.parentKeyOf(relation);
|
|
1877
1953
|
return this.executeSqlTransaction(async (tx) => {
|
|
1878
1954
|
const opts = { ...options, transaction: tx };
|
|
@@ -1916,18 +1992,70 @@ class SqlBaseCrudService {
|
|
|
1916
1992
|
return row;
|
|
1917
1993
|
}, options?.transaction);
|
|
1918
1994
|
}
|
|
1919
|
-
valueKey(value) {
|
|
1920
|
-
|
|
1995
|
+
valueKey(value, column) {
|
|
1996
|
+
const key = String(value);
|
|
1997
|
+
return column?.columnType === "PgUUID" || this.config.dialect === "mysql"
|
|
1998
|
+
? key.toLowerCase()
|
|
1999
|
+
: key;
|
|
1921
2000
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2001
|
+
distinct(values) {
|
|
2002
|
+
return Array.from(new Set(values));
|
|
2003
|
+
}
|
|
2004
|
+
async matchKeys(db, table, column, values, conditions) {
|
|
2005
|
+
const input = this.keyInput(column, values);
|
|
2006
|
+
const rows = await db
|
|
2007
|
+
.select({ at: input.at, key: column })
|
|
2008
|
+
.from(table)
|
|
2009
|
+
.innerJoin(input.list, (0, drizzle_orm_1.eq)(column, input.value))
|
|
2010
|
+
.where((0, drizzle_orm_1.and)(...conditions));
|
|
2011
|
+
return new Map(rows.map((row) => [Number(row.at), row.key]));
|
|
2012
|
+
}
|
|
2013
|
+
keyInput(column, values) {
|
|
2014
|
+
const params = values.map((value) => drizzle_orm_1.sql.param(value, column));
|
|
2015
|
+
if (this.config.dialect === "mysql") {
|
|
2016
|
+
const rows = values.map((_, at) => (0, drizzle_orm_1.sql) `row(${drizzle_orm_1.sql.raw(String(at))})`);
|
|
2017
|
+
const picked = (0, drizzle_orm_1.sql) `elt(input.column_0 + 1, ${drizzle_orm_1.sql.join(params, (0, drizzle_orm_1.sql) `, `)})`;
|
|
2018
|
+
const cast = this.mysqlKeyCast(column);
|
|
2019
|
+
return {
|
|
2020
|
+
list: (0, drizzle_orm_1.sql) `(values ${drizzle_orm_1.sql.join(rows, (0, drizzle_orm_1.sql) `, `)}) as input`,
|
|
2021
|
+
at: (0, drizzle_orm_1.sql) `input.column_0`,
|
|
2022
|
+
value: cast ? (0, drizzle_orm_1.sql) `cast(${picked} as ${drizzle_orm_1.sql.raw(cast)})` : picked,
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
const type = this.config.dialect === "postgresql" && this.pgKeyType(column);
|
|
2026
|
+
const rows = params.map((param, at) => (0, drizzle_orm_1.sql) `(${drizzle_orm_1.sql.raw(String(at))}, ${type ? (0, drizzle_orm_1.sql) `cast(${param} as ${type})` : param})`);
|
|
2027
|
+
return {
|
|
2028
|
+
list: (0, drizzle_orm_1.sql) `(values ${drizzle_orm_1.sql.join(rows, (0, drizzle_orm_1.sql) `, `)}) as input`,
|
|
2029
|
+
at: (0, drizzle_orm_1.sql) `input.column1`,
|
|
2030
|
+
value: (0, drizzle_orm_1.sql) `input.column2`,
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
mysqlKeyCast(column) {
|
|
2034
|
+
const type = String(column?.getSQLType?.() ?? "").toLowerCase();
|
|
2035
|
+
if (type === "serial")
|
|
2036
|
+
return "unsigned";
|
|
2037
|
+
if (/^(tiny|small|medium|big)?int\b/.test(type)) {
|
|
2038
|
+
return type.includes("unsigned") ? "unsigned" : "signed";
|
|
2039
|
+
}
|
|
2040
|
+
const decimal = type.match(/^decimal(\(\d+(?:,\d+)?\))?/);
|
|
2041
|
+
if (decimal)
|
|
2042
|
+
return `decimal${decimal[1] ?? ""}`;
|
|
2043
|
+
return undefined;
|
|
2044
|
+
}
|
|
2045
|
+
pgKeyType(column) {
|
|
2046
|
+
const pgEnum = column.enum;
|
|
2047
|
+
if (pgEnum?.enumName) {
|
|
2048
|
+
return pgEnum.schema
|
|
2049
|
+
? (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(pgEnum.schema)}.${drizzle_orm_1.sql.identifier(pgEnum.enumName)}`
|
|
2050
|
+
: (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(pgEnum.enumName)}`;
|
|
2051
|
+
}
|
|
2052
|
+
const type = column.getSQLType();
|
|
2053
|
+
const serials = {
|
|
2054
|
+
serial: "integer",
|
|
2055
|
+
bigserial: "bigint",
|
|
2056
|
+
smallserial: "smallint",
|
|
2057
|
+
};
|
|
2058
|
+
return drizzle_orm_1.sql.raw(serials[type] ?? type);
|
|
1931
2059
|
}
|
|
1932
2060
|
tableName(table) {
|
|
1933
2061
|
try {
|
|
@@ -1944,7 +2072,7 @@ class SqlBaseCrudService {
|
|
|
1944
2072
|
for (const key of Object.keys(this.resolveScope(options) ?? {})) {
|
|
1945
2073
|
delete mapped[key];
|
|
1946
2074
|
}
|
|
1947
|
-
const entityData = this.filterWritable(mapped, "update", options);
|
|
2075
|
+
const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "update", options));
|
|
1948
2076
|
const settable = Object.keys(entityData).filter((key) => entityData[key] !== undefined &&
|
|
1949
2077
|
!!this.columnOf(this.config.table, key));
|
|
1950
2078
|
if (settable.length === 0) {
|
|
@@ -1953,45 +2081,49 @@ class SqlBaseCrudService {
|
|
|
1953
2081
|
return entityData;
|
|
1954
2082
|
}
|
|
1955
2083
|
async update(id, data, options) {
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
const processedData = hooks.skipBefore
|
|
1967
|
-
? data
|
|
1968
|
-
: await this.beforeUpdate(id, data);
|
|
1969
|
-
return this.prepareUpdate(processedData, opts);
|
|
1970
|
-
});
|
|
1971
|
-
if (this.config.timestamps?.updatedAt) {
|
|
1972
|
-
entityData[this.config.timestamps.updatedAt] = new Date();
|
|
1973
|
-
}
|
|
1974
|
-
const db = this.dbFor(transaction);
|
|
1975
|
-
await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
|
|
1976
|
-
try {
|
|
1977
|
-
await this.guardOnError(guarded, id, opts, () => this.assertKeysInScope(db, entityData, opts));
|
|
1978
|
-
const [updatedEntity] = await this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts, this.softDeleteScope(opts));
|
|
1979
|
-
if (!updatedEntity) {
|
|
2084
|
+
return this.scopedCall(options, async (options) => {
|
|
2085
|
+
const { transaction, hooks = {} } = options || {};
|
|
2086
|
+
this.checkReadOptions(options);
|
|
2087
|
+
const expected = this.expectedVersionOf(data, options);
|
|
2088
|
+
const opts = {
|
|
2089
|
+
...options,
|
|
2090
|
+
expectedVersion: expected,
|
|
2091
|
+
};
|
|
2092
|
+
const guarded = !this.skipsGuard(options, "validateUpdate", "mapUpdateDtoToEntity", "prepareUpdate", ...(hooks.skipBefore ? [] : ["beforeUpdate"]));
|
|
2093
|
+
if (guarded)
|
|
1980
2094
|
await this.guardRow(id, opts);
|
|
1981
|
-
|
|
1982
|
-
|
|
2095
|
+
const entityData = await this.guardOnError(guarded, id, opts, async () => {
|
|
2096
|
+
await this.validateUpdate(id, data);
|
|
2097
|
+
const processedData = hooks.skipBefore
|
|
2098
|
+
? data
|
|
2099
|
+
: await this.beforeUpdate(id, data);
|
|
2100
|
+
return this.prepareUpdate(processedData, opts);
|
|
2101
|
+
});
|
|
2102
|
+
if (this.config.timestamps?.updatedAt) {
|
|
2103
|
+
entityData[this.config.timestamps.updatedAt] = new Date();
|
|
2104
|
+
}
|
|
2105
|
+
const db = this.dbFor(transaction);
|
|
2106
|
+
await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
|
|
2107
|
+
try {
|
|
2108
|
+
await this.guardOnError(guarded || !!transaction, id, opts, () => this.assertKeysInScope(db, entityData, opts));
|
|
2109
|
+
const [updatedEntity] = await this.guardOnError(guarded || !!transaction, id, opts, () => this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts, this.softDeleteScope(opts)));
|
|
2110
|
+
if (!updatedEntity) {
|
|
2111
|
+
await this.guardRow(id, opts);
|
|
2112
|
+
if (expected !== undefined) {
|
|
2113
|
+
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
2114
|
+
}
|
|
2115
|
+
throw new Error("Failed to update entity");
|
|
1983
2116
|
}
|
|
1984
|
-
|
|
2117
|
+
if (!hooks.skipAfter)
|
|
2118
|
+
await this.afterUpdate(updatedEntity);
|
|
2119
|
+
await this.emitEvent("after", "update", false, { ids: [id], input: data, rows: [updatedEntity], transaction }, options);
|
|
2120
|
+
await this.loadWriteRelations(db, [updatedEntity], options);
|
|
2121
|
+
return updatedEntity;
|
|
1985
2122
|
}
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
return updatedEntity;
|
|
1991
|
-
}
|
|
1992
|
-
catch (error) {
|
|
1993
|
-
throw this.toClientError(error, "write");
|
|
1994
|
-
}
|
|
2123
|
+
catch (error) {
|
|
2124
|
+
throw this.toClientError(error, "write");
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
1995
2127
|
}
|
|
1996
2128
|
async updateWhereIn(db, set, ids, options, extra) {
|
|
1997
2129
|
const where = this.writeWhere(ids, options);
|
|
@@ -2061,27 +2193,28 @@ class SqlBaseCrudService {
|
|
|
2061
2193
|
return false;
|
|
2062
2194
|
}
|
|
2063
2195
|
async softDelete(id, options) {
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2196
|
+
return this.scopedCall(options, async (options) => {
|
|
2197
|
+
if (!this.config.softDelete?.enabled) {
|
|
2198
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
2199
|
+
}
|
|
2200
|
+
const live = { ...options, withDeleted: false, onlyDeleted: false };
|
|
2201
|
+
return this.guardedWrite(id, options, {
|
|
2202
|
+
operation: "softDelete",
|
|
2203
|
+
state: live,
|
|
2204
|
+
hook: "beforeSoftDelete",
|
|
2205
|
+
before: () => this.beforeSoftDelete(id),
|
|
2206
|
+
write: async (db) => {
|
|
2207
|
+
let query = db
|
|
2208
|
+
.update(this.config.table)
|
|
2209
|
+
.set(this.softDeleteSet(new Date()))
|
|
2210
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(live)));
|
|
2211
|
+
if (this.config.sql?.useReturning)
|
|
2212
|
+
query = query.returning();
|
|
2213
|
+
return this.wasAffected(await this.runRead(query)) || undefined;
|
|
2214
|
+
},
|
|
2215
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2216
|
+
after: () => this.afterSoftDelete(id),
|
|
2217
|
+
});
|
|
2085
2218
|
});
|
|
2086
2219
|
}
|
|
2087
2220
|
softDeleteSet(deletedAt) {
|
|
@@ -2093,42 +2226,44 @@ class SqlBaseCrudService {
|
|
|
2093
2226
|
});
|
|
2094
2227
|
}
|
|
2095
2228
|
async restore(id, options) {
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2229
|
+
return this.scopedCall(options, async (options) => {
|
|
2230
|
+
if (!this.config.softDelete?.enabled) {
|
|
2231
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
2232
|
+
}
|
|
2233
|
+
const trashed = { ...options, withDeleted: false, onlyDeleted: true };
|
|
2234
|
+
const restored = await this.guardedWrite(id, options, {
|
|
2235
|
+
operation: "restore",
|
|
2236
|
+
state: trashed,
|
|
2237
|
+
hook: "beforeRestore",
|
|
2238
|
+
before: () => this.beforeRestore(id),
|
|
2239
|
+
write: async (db) => (await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options, this.softDeleteScope(trashed))))[0],
|
|
2240
|
+
miss: () => {
|
|
2241
|
+
if (options?.expectedVersion != null && this.config.versionColumn) {
|
|
2242
|
+
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
2243
|
+
}
|
|
2244
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
2245
|
+
},
|
|
2246
|
+
after: (entity) => this.afterRestore(entity),
|
|
2247
|
+
rows: (entity) => [entity],
|
|
2248
|
+
});
|
|
2249
|
+
await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
|
|
2250
|
+
return restored;
|
|
2115
2251
|
});
|
|
2116
|
-
await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
|
|
2117
|
-
return restored;
|
|
2118
2252
|
}
|
|
2119
2253
|
async delete(id, options) {
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2254
|
+
return this.scopedCall(options, async (options) => {
|
|
2255
|
+
return this.guardedWrite(id, options, {
|
|
2256
|
+
operation: "delete",
|
|
2257
|
+
state: options,
|
|
2258
|
+
hook: "beforeDelete",
|
|
2259
|
+
before: () => this.beforeDelete(id),
|
|
2260
|
+
write: async (db) => this.wasAffected(await db
|
|
2261
|
+
.delete(this.config.table)
|
|
2262
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(options)))) || undefined,
|
|
2263
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2264
|
+
after: () => this.afterDelete(id),
|
|
2265
|
+
errors: "delete",
|
|
2266
|
+
});
|
|
2132
2267
|
});
|
|
2133
2268
|
}
|
|
2134
2269
|
async forceDelete(id, options) {
|
|
@@ -2139,46 +2274,48 @@ class SqlBaseCrudService {
|
|
|
2139
2274
|
});
|
|
2140
2275
|
}
|
|
2141
2276
|
async upsert(data, upsert = {}, options) {
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2277
|
+
return this.scopedCall(options, async (options) => {
|
|
2278
|
+
this.checkReadOptions(options);
|
|
2279
|
+
const entityData = await this.prepareCreate(data, options);
|
|
2280
|
+
const db = this.dbFor(options?.transaction);
|
|
2281
|
+
await this.emitEvent("before", "upsert", false, { input: data, transaction: options?.transaction }, options);
|
|
2282
|
+
let row;
|
|
2283
|
+
try {
|
|
2284
|
+
await this.assertKeysInScope(db, entityData, options);
|
|
2285
|
+
[row] = await this.upsertRows(db, [entityData], upsert, options);
|
|
2286
|
+
}
|
|
2287
|
+
catch (error) {
|
|
2288
|
+
throw this.toClientError(error, "write");
|
|
2289
|
+
}
|
|
2290
|
+
if (!row) {
|
|
2291
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(entityData, upsert));
|
|
2292
|
+
}
|
|
2293
|
+
await this.emitEvent("after", "upsert", false, {
|
|
2294
|
+
ids: this.idsOf([row]),
|
|
2295
|
+
input: data,
|
|
2296
|
+
rows: [row],
|
|
2297
|
+
transaction: options?.transaction,
|
|
2298
|
+
}, options);
|
|
2299
|
+
await this.loadWriteRelations(db, [row], options);
|
|
2300
|
+
return row;
|
|
2301
|
+
});
|
|
2166
2302
|
}
|
|
2167
2303
|
async massUpsert(data, upsert = {}, options) {
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2304
|
+
return this.scopedCall(options, async (options) => {
|
|
2305
|
+
const message = "Mass upsert errors";
|
|
2306
|
+
return this.massWrite({
|
|
2307
|
+
operation: "upsert",
|
|
2308
|
+
message,
|
|
2309
|
+
options,
|
|
2310
|
+
input: data,
|
|
2311
|
+
returnsRows: true,
|
|
2312
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
2313
|
+
index,
|
|
2314
|
+
run: () => this.prepareCreate(item, opts),
|
|
2315
|
+
})),
|
|
2316
|
+
keys: true,
|
|
2317
|
+
write: (tx, rows, opts) => this.upsertSet(tx, rows, upsert, opts, message),
|
|
2318
|
+
});
|
|
2182
2319
|
});
|
|
2183
2320
|
}
|
|
2184
2321
|
async upsertSet(tx, rows, upsert, opts, message) {
|
|
@@ -2189,7 +2326,9 @@ class SqlBaseCrudService {
|
|
|
2189
2326
|
if (written.length >= rows.length)
|
|
2190
2327
|
return written;
|
|
2191
2328
|
const keys = this.conflictKeys(upsert);
|
|
2192
|
-
const keyOf = (row) => keys
|
|
2329
|
+
const keyOf = (row) => keys
|
|
2330
|
+
.map((k) => this.valueKey(row[k], this.columnOf(this.config.table, k)))
|
|
2331
|
+
.join("|");
|
|
2193
2332
|
const seen = new Set(written.map(keyOf));
|
|
2194
2333
|
throw new crud_exceptions_1.BulkOperationException(message, rows.flatMap((row, index) => seen.has(keyOf(row))
|
|
2195
2334
|
? []
|
|
@@ -2331,39 +2470,41 @@ class SqlBaseCrudService {
|
|
|
2331
2470
|
ids: ids ? unique : this.idsOf(written),
|
|
2332
2471
|
rows: spec.returnsRows ? written : undefined,
|
|
2333
2472
|
}, options);
|
|
2334
|
-
if (spec.returnsRows)
|
|
2335
|
-
await load(tx, written);
|
|
2473
|
+
if (spec.returnsRows) {
|
|
2474
|
+
await load(tx, written, prepared.some((row) => this.holdsSql(row)));
|
|
2475
|
+
}
|
|
2336
2476
|
return ids ? this.inIdOrder(written, ids) : written;
|
|
2337
2477
|
};
|
|
2338
2478
|
return spec.returnsRows
|
|
2339
|
-
? this.writeThenLoad(options, body)
|
|
2479
|
+
? this.writeThenLoad(spec.operation, options, body)
|
|
2340
2480
|
: this.executeSqlTransaction((tx) => body(tx, async () => { }), options?.transaction);
|
|
2341
2481
|
}
|
|
2342
2482
|
async massCreate(data, options) {
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2483
|
+
return this.scopedCall(options, async (options) => {
|
|
2484
|
+
if (options?.bulkMode === "perItem") {
|
|
2485
|
+
return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
|
|
2486
|
+
}
|
|
2487
|
+
const message = "Mass create errors";
|
|
2488
|
+
return this.massWrite({
|
|
2489
|
+
operation: "create",
|
|
2490
|
+
message,
|
|
2491
|
+
options,
|
|
2492
|
+
input: data,
|
|
2493
|
+
returnsRows: true,
|
|
2494
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
2495
|
+
index,
|
|
2496
|
+
run: () => this.prepareCreate(item, opts),
|
|
2497
|
+
})),
|
|
2498
|
+
keys: true,
|
|
2499
|
+
write: (tx, rows, opts) => this.runSet(tx, message, (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
|
|
2500
|
+
index,
|
|
2501
|
+
run: (sp) => this.insertOne(sp, row, opts),
|
|
2502
|
+
})), "write"),
|
|
2503
|
+
after: async (created) => {
|
|
2504
|
+
for (const entity of created)
|
|
2505
|
+
await this.afterCreate(entity);
|
|
2506
|
+
},
|
|
2507
|
+
});
|
|
2367
2508
|
});
|
|
2368
2509
|
}
|
|
2369
2510
|
async insertMany(db, rows, options) {
|
|
@@ -2413,35 +2554,36 @@ class SqlBaseCrudService {
|
|
|
2413
2554
|
})));
|
|
2414
2555
|
}
|
|
2415
2556
|
async massUpdate(ids, data, options) {
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2557
|
+
return this.scopedCall(options, async (options) => {
|
|
2558
|
+
if (options?.bulkMode === "perItem") {
|
|
2559
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass update errors", options, ({ id }, opts) => this.update(id, data, opts));
|
|
2560
|
+
}
|
|
2561
|
+
const message = "Mass update errors";
|
|
2562
|
+
const skipBefore = options?.hooks?.skipBefore;
|
|
2563
|
+
return this.massWrite({
|
|
2564
|
+
operation: "update",
|
|
2565
|
+
message,
|
|
2566
|
+
options,
|
|
2567
|
+
ids,
|
|
2568
|
+
input: data,
|
|
2569
|
+
returnsRows: true,
|
|
2570
|
+
prepare: (opts, unique) => unique.map((id) => ({
|
|
2571
|
+
id,
|
|
2572
|
+
run: async () => {
|
|
2573
|
+
await this.validateUpdate(id, data);
|
|
2574
|
+
const processed = skipBefore
|
|
2575
|
+
? data
|
|
2576
|
+
: await this.beforeUpdate(id, data);
|
|
2577
|
+
return this.prepareUpdate(processed, opts);
|
|
2578
|
+
},
|
|
2579
|
+
})),
|
|
2580
|
+
keys: true,
|
|
2581
|
+
write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
|
|
2582
|
+
after: async (updated) => {
|
|
2583
|
+
for (const entity of updated)
|
|
2584
|
+
await this.afterUpdate(entity);
|
|
2437
2585
|
},
|
|
2438
|
-
})
|
|
2439
|
-
keys: true,
|
|
2440
|
-
write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
|
|
2441
|
-
after: async (updated) => {
|
|
2442
|
-
for (const entity of updated)
|
|
2443
|
-
await this.afterUpdate(entity);
|
|
2444
|
-
},
|
|
2586
|
+
});
|
|
2445
2587
|
});
|
|
2446
2588
|
}
|
|
2447
2589
|
async updateSets(tx, sets, unique, opts, message) {
|
|
@@ -2482,96 +2624,105 @@ class SqlBaseCrudService {
|
|
|
2482
2624
|
: [];
|
|
2483
2625
|
}
|
|
2484
2626
|
async massSoftDelete(ids, options) {
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2627
|
+
return this.scopedCall(options, async (options) => {
|
|
2628
|
+
if (!this.config.softDelete?.enabled)
|
|
2629
|
+
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2630
|
+
if (options?.bulkMode === "perItem") {
|
|
2631
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass soft delete errors", options, ({ id }, opts) => this.softDelete(id, opts));
|
|
2632
|
+
return true;
|
|
2633
|
+
}
|
|
2634
|
+
const message = "Mass soft delete errors";
|
|
2635
|
+
await this.massWrite({
|
|
2636
|
+
operation: "softDelete",
|
|
2637
|
+
message,
|
|
2638
|
+
options,
|
|
2639
|
+
ids,
|
|
2640
|
+
state: { withDeleted: false, onlyDeleted: false },
|
|
2641
|
+
returnsRows: false,
|
|
2642
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2643
|
+
? undefined
|
|
2644
|
+
: unique.map((id) => ({
|
|
2645
|
+
id,
|
|
2646
|
+
run: () => this.beforeSoftDelete(id),
|
|
2647
|
+
})),
|
|
2648
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2649
|
+
const set = this.softDeleteSet(new Date());
|
|
2650
|
+
const update = (db, keys) => db
|
|
2651
|
+
.update(this.config.table)
|
|
2652
|
+
.set(set)
|
|
2653
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
|
|
2654
|
+
return this.runSet(tx, message, (sp) => this.runKeyed(update(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => update(sp, [id]) })), "write");
|
|
2655
|
+
},
|
|
2656
|
+
after: async (_written, unique) => {
|
|
2657
|
+
for (const id of unique)
|
|
2658
|
+
await this.afterSoftDelete(id);
|
|
2659
|
+
},
|
|
2660
|
+
});
|
|
2490
2661
|
return true;
|
|
2491
|
-
}
|
|
2492
|
-
const message = "Mass soft delete errors";
|
|
2493
|
-
await this.massWrite({
|
|
2494
|
-
operation: "softDelete",
|
|
2495
|
-
message,
|
|
2496
|
-
options,
|
|
2497
|
-
ids,
|
|
2498
|
-
state: { withDeleted: false, onlyDeleted: false },
|
|
2499
|
-
returnsRows: false,
|
|
2500
|
-
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2501
|
-
? undefined
|
|
2502
|
-
: unique.map((id) => ({ id, run: () => this.beforeSoftDelete(id) })),
|
|
2503
|
-
write: (tx, _prepared, opts, unique) => {
|
|
2504
|
-
const set = this.softDeleteSet(new Date());
|
|
2505
|
-
const update = (db, keys) => db
|
|
2506
|
-
.update(this.config.table)
|
|
2507
|
-
.set(set)
|
|
2508
|
-
.where(this.writeWhere(keys, opts));
|
|
2509
|
-
return this.runSet(tx, message, (sp) => this.runKeyed(update(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => update(sp, [id]) })), "write");
|
|
2510
|
-
},
|
|
2511
|
-
after: async (_written, unique) => {
|
|
2512
|
-
for (const id of unique)
|
|
2513
|
-
await this.afterSoftDelete(id);
|
|
2514
|
-
},
|
|
2515
2662
|
});
|
|
2516
|
-
return true;
|
|
2517
2663
|
}
|
|
2518
2664
|
async massRestore(ids, options) {
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
id
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2665
|
+
return this.scopedCall(options, async (options) => {
|
|
2666
|
+
if (!this.config.softDelete?.enabled)
|
|
2667
|
+
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2668
|
+
if (options?.bulkMode === "perItem") {
|
|
2669
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass restore errors", options, ({ id }, opts) => this.restore(id, opts));
|
|
2670
|
+
}
|
|
2671
|
+
const message = "Mass restore errors";
|
|
2672
|
+
return this.massWrite({
|
|
2673
|
+
operation: "restore",
|
|
2674
|
+
message,
|
|
2675
|
+
options,
|
|
2676
|
+
ids,
|
|
2677
|
+
state: { withDeleted: true, onlyDeleted: false },
|
|
2678
|
+
returnsRows: true,
|
|
2679
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2680
|
+
? undefined
|
|
2681
|
+
: unique.map((id) => ({ id, run: () => this.beforeRestore(id) })),
|
|
2682
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2683
|
+
const set = this.softDeleteSet(null);
|
|
2684
|
+
const state = this.softDeleteScope(opts);
|
|
2685
|
+
return this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts, state), unique.map((id) => ({
|
|
2686
|
+
id,
|
|
2687
|
+
run: (sp) => this.updateWhereIn(sp, set, [id], opts, state),
|
|
2688
|
+
})), "write");
|
|
2689
|
+
},
|
|
2690
|
+
after: async (restored) => {
|
|
2691
|
+
for (const entity of restored)
|
|
2692
|
+
await this.afterRestore(entity);
|
|
2693
|
+
},
|
|
2694
|
+
});
|
|
2547
2695
|
});
|
|
2548
2696
|
}
|
|
2549
2697
|
async massDelete(ids, options) {
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2698
|
+
return this.scopedCall(options, async (options) => {
|
|
2699
|
+
if (options?.bulkMode === "perItem") {
|
|
2700
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass delete errors", options, ({ id }, opts) => this.delete(id, opts), "delete");
|
|
2701
|
+
return true;
|
|
2702
|
+
}
|
|
2703
|
+
const message = "Mass delete errors";
|
|
2704
|
+
await this.massWrite({
|
|
2705
|
+
operation: "delete",
|
|
2706
|
+
message,
|
|
2707
|
+
options,
|
|
2708
|
+
ids,
|
|
2709
|
+
returnsRows: false,
|
|
2710
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2711
|
+
? undefined
|
|
2712
|
+
: unique.map((id) => ({ id, run: () => this.beforeDelete(id) })),
|
|
2713
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2714
|
+
const remove = (db, keys) => db
|
|
2715
|
+
.delete(this.config.table)
|
|
2716
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
|
|
2717
|
+
return this.runSet(tx, message, (sp) => this.runKeyed(remove(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => remove(sp, [id]) })), "delete");
|
|
2718
|
+
},
|
|
2719
|
+
after: async (_written, unique) => {
|
|
2720
|
+
for (const id of unique)
|
|
2721
|
+
await this.afterDelete(id);
|
|
2722
|
+
},
|
|
2723
|
+
});
|
|
2553
2724
|
return true;
|
|
2554
|
-
}
|
|
2555
|
-
const message = "Mass delete errors";
|
|
2556
|
-
await this.massWrite({
|
|
2557
|
-
operation: "delete",
|
|
2558
|
-
message,
|
|
2559
|
-
options,
|
|
2560
|
-
ids,
|
|
2561
|
-
returnsRows: false,
|
|
2562
|
-
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2563
|
-
? undefined
|
|
2564
|
-
: unique.map((id) => ({ id, run: () => this.beforeDelete(id) })),
|
|
2565
|
-
write: (tx, _prepared, opts, unique) => {
|
|
2566
|
-
const remove = (db, keys) => db.delete(this.config.table).where(this.writeWhere(keys, opts));
|
|
2567
|
-
return this.runSet(tx, message, (sp) => this.runKeyed(remove(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => remove(sp, [id]) })), "delete");
|
|
2568
|
-
},
|
|
2569
|
-
after: async (_written, unique) => {
|
|
2570
|
-
for (const id of unique)
|
|
2571
|
-
await this.afterDelete(id);
|
|
2572
|
-
},
|
|
2573
2725
|
});
|
|
2574
|
-
return true;
|
|
2575
2726
|
}
|
|
2576
2727
|
async massForceDelete(ids, options) {
|
|
2577
2728
|
return this.massDelete(ids, {
|
|
@@ -2580,6 +2731,10 @@ class SqlBaseCrudService {
|
|
|
2580
2731
|
onlyDeleted: false,
|
|
2581
2732
|
});
|
|
2582
2733
|
}
|
|
2734
|
+
holdsSql(row) {
|
|
2735
|
+
return ((0, merge_config_1.isPlainObject)(row) &&
|
|
2736
|
+
Object.values(row).some((value) => (0, drizzle_orm_1.is)(value, drizzle_orm_1.SQL)));
|
|
2737
|
+
}
|
|
2583
2738
|
uniqueIds(ids) {
|
|
2584
2739
|
const seen = new Set();
|
|
2585
2740
|
return (ids ?? []).filter((id) => {
|
|
@@ -2591,7 +2746,34 @@ class SqlBaseCrudService {
|
|
|
2591
2746
|
});
|
|
2592
2747
|
}
|
|
2593
2748
|
fingerprint(value) {
|
|
2594
|
-
return JSON.stringify(value, (
|
|
2749
|
+
return JSON.stringify(value, function (key, json) {
|
|
2750
|
+
const v = this[key];
|
|
2751
|
+
if (v instanceof Date)
|
|
2752
|
+
return `date:${v.getTime()}`;
|
|
2753
|
+
if (ArrayBuffer.isView(v)) {
|
|
2754
|
+
const bytes = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
|
|
2755
|
+
return `bytes:${bytes.join(",")}`;
|
|
2756
|
+
}
|
|
2757
|
+
if (typeof v === "string")
|
|
2758
|
+
return `s:${v}`;
|
|
2759
|
+
if (typeof v === "bigint")
|
|
2760
|
+
return `bigint:${v}`;
|
|
2761
|
+
if (typeof v === "number" && !Number.isFinite(v)) {
|
|
2762
|
+
return `number:${v}`;
|
|
2763
|
+
}
|
|
2764
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
2765
|
+
const proto = Object.getPrototypeOf(v);
|
|
2766
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
2767
|
+
let tag = fingerprintTags.get(v);
|
|
2768
|
+
if (!tag) {
|
|
2769
|
+
tag = `opaque:${++fingerprintCount}`;
|
|
2770
|
+
fingerprintTags.set(v, tag);
|
|
2771
|
+
}
|
|
2772
|
+
return tag;
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
return json;
|
|
2776
|
+
});
|
|
2595
2777
|
}
|
|
2596
2778
|
async assertAllExist(tx, ids, options, message) {
|
|
2597
2779
|
const table = this.config.table;
|
|
@@ -2726,44 +2908,50 @@ class SqlBaseCrudService {
|
|
|
2726
2908
|
: run(tx);
|
|
2727
2909
|
}
|
|
2728
2910
|
async exists(id, options) {
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2911
|
+
return this.scopedCall(options, async (options) => {
|
|
2912
|
+
this.checkReadOptions({ ...options, select: undefined });
|
|
2913
|
+
const entity = await this.find(id, {
|
|
2914
|
+
...options,
|
|
2915
|
+
relations: [],
|
|
2916
|
+
counts: undefined,
|
|
2917
|
+
select: [this.config.primaryKey],
|
|
2918
|
+
});
|
|
2919
|
+
return !!entity;
|
|
2736
2920
|
});
|
|
2737
|
-
return !!entity;
|
|
2738
2921
|
}
|
|
2739
2922
|
async count(filters, options) {
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2923
|
+
return this.scopedCall(options, async (options) => {
|
|
2924
|
+
this.checkReadOptions(options);
|
|
2925
|
+
const db = this.dbFor(options?.transaction);
|
|
2926
|
+
const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
|
|
2927
|
+
const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
|
|
2928
|
+
return this.parseCount(result);
|
|
2929
|
+
});
|
|
2746
2930
|
}
|
|
2747
2931
|
async countBy(column, filters, options) {
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2932
|
+
return this.scopedCall(options, async (options) => {
|
|
2933
|
+
this.checkReadOptions(options);
|
|
2934
|
+
const db = this.dbFor(options?.transaction);
|
|
2935
|
+
const spec = this.resolveGroupColumn(column, options);
|
|
2936
|
+
const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
|
|
2937
|
+
const joins = Array.from(new Set([
|
|
2938
|
+
...filterRelations,
|
|
2939
|
+
...(spec.relation ? [spec.relation] : []),
|
|
2940
|
+
]));
|
|
2941
|
+
const total = (0, drizzle_orm_1.sql) `count(*)`;
|
|
2942
|
+
let query = db
|
|
2943
|
+
.select({ value: spec.column, count: total })
|
|
2944
|
+
.from(this.config.table);
|
|
2945
|
+
query = this.applyJoins(query, joins, options);
|
|
2946
|
+
if (conditions.length > 0)
|
|
2947
|
+
query = query.where((0, drizzle_orm_1.and)(...conditions));
|
|
2948
|
+
query = query.groupBy(spec.column).orderBy((0, drizzle_orm_1.desc)(total), (0, drizzle_orm_1.asc)(spec.column));
|
|
2949
|
+
const rows = await this.runRead(query);
|
|
2950
|
+
return rows.map((row) => ({
|
|
2951
|
+
value: row.value,
|
|
2952
|
+
count: Number(row.count),
|
|
2953
|
+
}));
|
|
2954
|
+
});
|
|
2767
2955
|
}
|
|
2768
2956
|
resolveGroupColumn(key, options) {
|
|
2769
2957
|
if (typeof key !== "string" || !key) {
|
|
@@ -3297,49 +3485,50 @@ class SqlBaseCrudService {
|
|
|
3297
3485
|
}
|
|
3298
3486
|
}
|
|
3299
3487
|
async fullTextSearch(searchTerm, searchColumns, pagination, options) {
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
}
|
|
3304
|
-
if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
|
|
3305
|
-
throw new common_1.BadRequestException("Full-text search needs at least one column");
|
|
3306
|
-
}
|
|
3307
|
-
const columns = searchColumns.map((name) => {
|
|
3308
|
-
const column = this.columnOf(this.config.table, name);
|
|
3309
|
-
if (!column) {
|
|
3310
|
-
throw new common_1.BadRequestException(`Unknown search column: ${name}`);
|
|
3488
|
+
return this.scopedCall(options, async (options) => {
|
|
3489
|
+
if (this.config.dialect !== "postgresql") {
|
|
3490
|
+
throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
|
|
3311
3491
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
|
|
3492
|
+
if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
|
|
3493
|
+
throw new common_1.BadRequestException("Full-text search needs at least one column");
|
|
3494
|
+
}
|
|
3495
|
+
const columns = searchColumns.map((name) => {
|
|
3496
|
+
const column = this.columnOf(this.config.table, name);
|
|
3497
|
+
if (!column) {
|
|
3498
|
+
throw new common_1.BadRequestException(`Unknown search column: ${name}`);
|
|
3499
|
+
}
|
|
3500
|
+
this.assertAllowed("searchableColumns", name, options);
|
|
3501
|
+
return column;
|
|
3502
|
+
});
|
|
3503
|
+
const { filters, language, queryMode, ...readOptions } = options ?? {};
|
|
3504
|
+
const { vector, query } = this.fullTextParts(columns, searchTerm, {
|
|
3505
|
+
language,
|
|
3506
|
+
queryMode,
|
|
3507
|
+
});
|
|
3508
|
+
const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
3509
|
+
const plan = this.planList(filters, undefined, "asc", readOptions);
|
|
3510
|
+
const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
|
|
3511
|
+
const joins = Array.from(new Set([
|
|
3512
|
+
...plan.joined.map((node) => node.name),
|
|
3513
|
+
...plan.filterRelations,
|
|
3514
|
+
]));
|
|
3515
|
+
const pk = this.columnOf(this.config.table, this.config.primaryKey);
|
|
3516
|
+
const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
|
|
3517
|
+
if (pk)
|
|
3518
|
+
orderBy.push((0, drizzle_orm_1.asc)(pk));
|
|
3519
|
+
let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
|
|
3520
|
+
dataQuery = this.applyJoins(dataQuery, joins, readOptions)
|
|
3521
|
+
.where((0, drizzle_orm_1.and)(...conditions))
|
|
3522
|
+
.orderBy(...orderBy)
|
|
3523
|
+
.limit(limit)
|
|
3524
|
+
.offset(offset);
|
|
3525
|
+
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
3526
|
+
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
3527
|
+
const total = this.parseCount(totalResult);
|
|
3528
|
+
const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
|
|
3529
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
|
|
3530
|
+
return { data: rows, total, page, limit };
|
|
3319
3531
|
});
|
|
3320
|
-
const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
3321
|
-
const plan = this.planList(filters, undefined, "asc", readOptions);
|
|
3322
|
-
const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
|
|
3323
|
-
const joins = Array.from(new Set([
|
|
3324
|
-
...plan.joined.map((node) => node.name),
|
|
3325
|
-
...plan.filterRelations,
|
|
3326
|
-
]));
|
|
3327
|
-
const pk = this.columnOf(this.config.table, this.config.primaryKey);
|
|
3328
|
-
const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
|
|
3329
|
-
if (pk)
|
|
3330
|
-
orderBy.push((0, drizzle_orm_1.asc)(pk));
|
|
3331
|
-
let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
|
|
3332
|
-
dataQuery = this.applyJoins(dataQuery, joins, readOptions)
|
|
3333
|
-
.where((0, drizzle_orm_1.and)(...conditions))
|
|
3334
|
-
.orderBy(...orderBy)
|
|
3335
|
-
.limit(limit)
|
|
3336
|
-
.offset(offset);
|
|
3337
|
-
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
3338
|
-
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
3339
|
-
const total = this.parseCount(totalResult);
|
|
3340
|
-
const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
|
|
3341
|
-
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
|
|
3342
|
-
return { data: rows, total, page, limit };
|
|
3343
3532
|
}
|
|
3344
3533
|
}
|
|
3345
3534
|
exports.SqlBaseCrudService = SqlBaseCrudService;
|