@avytrix/nestjs-drizzle-crud 3.8.1 → 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,7 +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
|
-
|
|
14
|
+
const fingerprintTags = new WeakMap();
|
|
15
|
+
let fingerprintCount = 0;
|
|
15
16
|
const ROLLBACK = Symbol("rollback");
|
|
16
17
|
class DeferredDiagnosis {
|
|
17
18
|
settle;
|
|
@@ -128,15 +129,38 @@ class SqlBaseCrudService {
|
|
|
128
129
|
return /(Text|VarChar|Varchar|Char)$/.test(column?.columnType ?? "");
|
|
129
130
|
}
|
|
130
131
|
coerceFilterValue(column, value) {
|
|
131
|
-
if (column?.dataType !== "date"
|
|
132
|
+
if (column?.dataType !== "date")
|
|
132
133
|
return value;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
return date;
|
|
137
|
-
}
|
|
134
|
+
const date = this.toDate(value);
|
|
135
|
+
if (date)
|
|
136
|
+
return date;
|
|
138
137
|
throw new common_1.BadRequestException(`Invalid date filter value: ${value}`);
|
|
139
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
|
+
}
|
|
140
164
|
static ALLOWLIST_LABELS = {
|
|
141
165
|
filterableColumns: "Filtering on",
|
|
142
166
|
sortableColumns: "Sorting by",
|
|
@@ -194,7 +218,8 @@ class SqlBaseCrudService {
|
|
|
194
218
|
if (!scope || options?.skipScope)
|
|
195
219
|
return null;
|
|
196
220
|
const context = options?.context ?? crud_context_1.CrudContext.current();
|
|
197
|
-
const
|
|
221
|
+
const found = options?.[SCOPE_MEMO];
|
|
222
|
+
const memo = found?.ended ? undefined : found;
|
|
198
223
|
const mine = memo?.get(this);
|
|
199
224
|
if (mine?.has(context))
|
|
200
225
|
return mine.get(context);
|
|
@@ -207,12 +232,23 @@ class SqlBaseCrudService {
|
|
|
207
232
|
return checked;
|
|
208
233
|
}
|
|
209
234
|
withScopeMemo(options) {
|
|
210
|
-
|
|
211
|
-
|
|
235
|
+
const memo = options?.[SCOPE_MEMO];
|
|
236
|
+
if (typeof this.config.scope !== "function" || (memo && !memo.ended)) {
|
|
212
237
|
return options;
|
|
213
238
|
}
|
|
214
239
|
return { ...options, [SCOPE_MEMO]: new Map() };
|
|
215
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
|
+
}
|
|
251
|
+
}
|
|
216
252
|
checkScope(values) {
|
|
217
253
|
if (values === null)
|
|
218
254
|
return null;
|
|
@@ -330,8 +366,9 @@ class SqlBaseCrudService {
|
|
|
330
366
|
query = dialect.sqlToQuery(...args);
|
|
331
367
|
}
|
|
332
368
|
catch (error) {
|
|
333
|
-
if (error instanceof RangeError)
|
|
369
|
+
if (error instanceof RangeError && /call stack/i.test(error.message)) {
|
|
334
370
|
throw tooMany(`more than ${limit}`);
|
|
371
|
+
}
|
|
335
372
|
throw error;
|
|
336
373
|
}
|
|
337
374
|
const count = query?.params?.length ?? 0;
|
|
@@ -1247,14 +1284,16 @@ class SqlBaseCrudService {
|
|
|
1247
1284
|
};
|
|
1248
1285
|
}
|
|
1249
1286
|
async find(id, options) {
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
+
});
|
|
1253
1291
|
}
|
|
1254
1292
|
async findOne(where, options) {
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1293
|
+
return this.scopedCall(options, async (options) => {
|
|
1294
|
+
this.checkReadOptions(options);
|
|
1295
|
+
return this.findFirst(this.buildWhereConditionsFromPartial(where), options);
|
|
1296
|
+
});
|
|
1258
1297
|
}
|
|
1259
1298
|
async findFirst(conditions, options) {
|
|
1260
1299
|
const first = await this.readFirst(conditions, options);
|
|
@@ -1335,105 +1374,107 @@ class SqlBaseCrudService {
|
|
|
1335
1374
|
return query;
|
|
1336
1375
|
}
|
|
1337
1376
|
async findAll(filters, pagination, options) {
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
+
}
|
|
1363
1410
|
return {
|
|
1364
1411
|
data: rows,
|
|
1365
|
-
total:
|
|
1412
|
+
total: this.parseCount(totalResult),
|
|
1366
1413
|
page,
|
|
1367
1414
|
limit: safeLimit,
|
|
1368
|
-
hasMore: fetched.length > safeLimit,
|
|
1369
1415
|
};
|
|
1370
|
-
}
|
|
1371
|
-
return {
|
|
1372
|
-
data: rows,
|
|
1373
|
-
total: this.parseCount(totalResult),
|
|
1374
|
-
page,
|
|
1375
|
-
limit: safeLimit,
|
|
1376
|
-
};
|
|
1416
|
+
});
|
|
1377
1417
|
}
|
|
1378
1418
|
async findByCursor(filters, pagination, options) {
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
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;
|
|
1405
1477
|
});
|
|
1406
|
-
const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
|
|
1407
|
-
let dataQuery = plan.db.select(fields).from(this.config.table);
|
|
1408
|
-
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1409
|
-
if (conditions.length > 0)
|
|
1410
|
-
dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
|
|
1411
|
-
dataQuery = dataQuery
|
|
1412
|
-
.orderBy(...plan.sort.map((spec) => this.orderOf(spec)))
|
|
1413
|
-
.limit(limit + 1);
|
|
1414
|
-
dataQuery = this.applyLock(dataQuery, options);
|
|
1415
|
-
const [data, totalResult] = await this.runRead(Promise.all([
|
|
1416
|
-
dataQuery,
|
|
1417
|
-
count
|
|
1418
|
-
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
|
|
1419
|
-
: Promise.resolve(undefined),
|
|
1420
|
-
]));
|
|
1421
|
-
const page = data.slice(0, limit);
|
|
1422
|
-
const last = page[page.length - 1];
|
|
1423
|
-
const nextCursor = data.length > limit && last
|
|
1424
|
-
? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
|
|
1425
|
-
: null;
|
|
1426
|
-
for (const row of page) {
|
|
1427
|
-
for (const key of hidden)
|
|
1428
|
-
delete row[key];
|
|
1429
|
-
}
|
|
1430
|
-
const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
|
|
1431
|
-
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1432
|
-
const out = { data: rows, limit, nextCursor };
|
|
1433
|
-
if (count) {
|
|
1434
|
-
out.total = this.parseCount(totalResult);
|
|
1435
|
-
}
|
|
1436
|
-
return out;
|
|
1437
1478
|
}
|
|
1438
1479
|
cursorField(column) {
|
|
1439
1480
|
if (column?.dataType !== "date")
|
|
@@ -1564,10 +1605,13 @@ class SqlBaseCrudService {
|
|
|
1564
1605
|
}
|
|
1565
1606
|
}
|
|
1566
1607
|
async writeThenLoad(operation, options, write) {
|
|
1567
|
-
|
|
1568
|
-
const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written) =>
|
|
1569
|
-
|
|
1570
|
-
|
|
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);
|
|
1571
1615
|
if (late)
|
|
1572
1616
|
await this.loadWriteRelations(this.dbFor(), rows, options);
|
|
1573
1617
|
return rows;
|
|
@@ -1596,7 +1640,7 @@ class SqlBaseCrudService {
|
|
|
1596
1640
|
const version = this.config.versionColumn;
|
|
1597
1641
|
if (version)
|
|
1598
1642
|
delete mapped[version];
|
|
1599
|
-
const entityData = this.filterWritable(mapped, "create", options);
|
|
1643
|
+
const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "create", options));
|
|
1600
1644
|
const now = new Date();
|
|
1601
1645
|
if (this.config.timestamps?.createdAt)
|
|
1602
1646
|
entityData[this.config.timestamps.createdAt] = now;
|
|
@@ -1626,35 +1670,36 @@ class SqlBaseCrudService {
|
|
|
1626
1670
|
return createdEntity;
|
|
1627
1671
|
}
|
|
1628
1672
|
async create(data, options) {
|
|
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
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
+
});
|
|
1658
1703
|
}
|
|
1659
1704
|
takeNested(entity, options) {
|
|
1660
1705
|
const allowed = this.config.writableRelations;
|
|
@@ -1760,7 +1805,7 @@ class SqlBaseCrudService {
|
|
|
1760
1805
|
if (this.columnOf(table, key))
|
|
1761
1806
|
out[key] = item;
|
|
1762
1807
|
}
|
|
1763
|
-
return out;
|
|
1808
|
+
return this.coerceDates(table, out);
|
|
1764
1809
|
}
|
|
1765
1810
|
async keysOutsideScope(db, rows, options, skip = [], owner = {
|
|
1766
1811
|
table: this.config.table,
|
|
@@ -1779,26 +1824,29 @@ class SqlBaseCrudService {
|
|
|
1779
1824
|
const referenced = relation.table[relation.references ?? "id"];
|
|
1780
1825
|
if (scope.length === 0 || !referenced)
|
|
1781
1826
|
continue;
|
|
1782
|
-
const keys = this.
|
|
1827
|
+
const keys = this.distinct(rows.map((row) => row[local]).filter((value) => value != null));
|
|
1783
1828
|
if (keys.length === 0)
|
|
1784
1829
|
continue;
|
|
1785
1830
|
checks.push({
|
|
1786
1831
|
local,
|
|
1787
|
-
|
|
1788
|
-
query: db
|
|
1789
|
-
.select({ id: referenced })
|
|
1790
|
-
.from(relation.table)
|
|
1791
|
-
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(referenced, keys), ...scope)),
|
|
1832
|
+
keys,
|
|
1833
|
+
query: this.matchKeys(db, relation.table, referenced, keys, scope),
|
|
1792
1834
|
});
|
|
1793
1835
|
}
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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)));
|
|
1797
1846
|
rows.forEach((row, index) => {
|
|
1798
1847
|
const value = row[local];
|
|
1799
|
-
if (value == null || present.has(
|
|
1848
|
+
if (value == null || present.has(value))
|
|
1800
1849
|
return;
|
|
1801
|
-
}
|
|
1802
1850
|
if (!failures.has(index)) {
|
|
1803
1851
|
failures.set(index, this.missingParentException(local, owner.table));
|
|
1804
1852
|
}
|
|
@@ -1838,21 +1886,14 @@ class SqlBaseCrudService {
|
|
|
1838
1886
|
async linkTargets(tx, relation, parentValue, ids, options, fresh = false) {
|
|
1839
1887
|
const target = relation.table;
|
|
1840
1888
|
const referenced = target[relation.references ?? "id"];
|
|
1841
|
-
const wanted = this.
|
|
1889
|
+
const wanted = this.distinct(ids);
|
|
1842
1890
|
if (wanted.length === 0)
|
|
1843
1891
|
return;
|
|
1844
1892
|
const through = relation.through;
|
|
1845
1893
|
const local = through[relation.throughLocalKey];
|
|
1846
1894
|
const foreign = through[relation.throughForeignKey];
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
const found = await tx
|
|
1850
|
-
.select({ id: referenced })
|
|
1851
|
-
.from(target)
|
|
1852
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
1853
|
-
const key = (value) => this.valueKey(value, referenced);
|
|
1854
|
-
const present = new Set(found.map((row) => key(row.id)));
|
|
1855
|
-
const missing = wanted.filter((id) => !present.has(key(id)));
|
|
1895
|
+
const found = await this.matchKeys(tx, target, referenced, wanted, this.relatedRowScope(target, options));
|
|
1896
|
+
const missing = wanted.filter((_, at) => !found.has(at));
|
|
1856
1897
|
if (missing.length > 0) {
|
|
1857
1898
|
const name = this.tableName(target);
|
|
1858
1899
|
throw new crud_exceptions_1.BulkOperationException("Unknown related ids", missing.map((id) => ({
|
|
@@ -1860,14 +1901,13 @@ class SqlBaseCrudService {
|
|
|
1860
1901
|
error: new crud_exceptions_1.EntityNotFoundException(name, id),
|
|
1861
1902
|
})));
|
|
1862
1903
|
}
|
|
1904
|
+
const stored = this.distinct(wanted.map((_, at) => found.get(at)));
|
|
1863
1905
|
const linked = fresh
|
|
1864
|
-
?
|
|
1865
|
-
: await tx
|
|
1866
|
-
.
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
const already = new Set(linked.map((row) => key(row.id)));
|
|
1870
|
-
const add = wanted.filter((id) => !already.has(key(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));
|
|
1871
1911
|
if (add.length > 0) {
|
|
1872
1912
|
await tx.insert(through).values(add.map((id) => ({
|
|
1873
1913
|
[relation.throughLocalKey]: parentValue,
|
|
@@ -1876,16 +1916,19 @@ class SqlBaseCrudService {
|
|
|
1876
1916
|
}
|
|
1877
1917
|
}
|
|
1878
1918
|
async attach(id, relation, relatedIds, options) {
|
|
1879
|
-
|
|
1880
|
-
|
|
1919
|
+
return this.scopedCall(options, async (options) => {
|
|
1920
|
+
return this.changeLinks("attach", id, relation, relatedIds, options);
|
|
1921
|
+
});
|
|
1881
1922
|
}
|
|
1882
1923
|
async detach(id, relation, relatedIds, options) {
|
|
1883
|
-
|
|
1884
|
-
|
|
1924
|
+
return this.scopedCall(options, async (options) => {
|
|
1925
|
+
return this.changeLinks("detach", id, relation, relatedIds, options);
|
|
1926
|
+
});
|
|
1885
1927
|
}
|
|
1886
1928
|
async sync(id, relation, relatedIds, options) {
|
|
1887
|
-
|
|
1888
|
-
|
|
1929
|
+
return this.scopedCall(options, async (options) => {
|
|
1930
|
+
return this.changeLinks("sync", id, relation, relatedIds, options);
|
|
1931
|
+
});
|
|
1889
1932
|
}
|
|
1890
1933
|
async changeLinks(operation, id, name, relatedIds, options) {
|
|
1891
1934
|
const relation = this.relationNamed(name);
|
|
@@ -1905,7 +1948,7 @@ class SqlBaseCrudService {
|
|
|
1905
1948
|
throw new common_1.BadRequestException(`${operation} takes an array of ids`);
|
|
1906
1949
|
}
|
|
1907
1950
|
this.checkReadOptions(options);
|
|
1908
|
-
const ids = this.
|
|
1951
|
+
const ids = this.distinct(relatedIds ?? []);
|
|
1909
1952
|
const parentKey = this.parentKeyOf(relation);
|
|
1910
1953
|
return this.executeSqlTransaction(async (tx) => {
|
|
1911
1954
|
const opts = { ...options, transaction: tx };
|
|
@@ -1955,15 +1998,64 @@ class SqlBaseCrudService {
|
|
|
1955
1998
|
? key.toLowerCase()
|
|
1956
1999
|
: key;
|
|
1957
2000
|
}
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
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);
|
|
1967
2059
|
}
|
|
1968
2060
|
tableName(table) {
|
|
1969
2061
|
try {
|
|
@@ -1980,7 +2072,7 @@ class SqlBaseCrudService {
|
|
|
1980
2072
|
for (const key of Object.keys(this.resolveScope(options) ?? {})) {
|
|
1981
2073
|
delete mapped[key];
|
|
1982
2074
|
}
|
|
1983
|
-
const entityData = this.filterWritable(mapped, "update", options);
|
|
2075
|
+
const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "update", options));
|
|
1984
2076
|
const settable = Object.keys(entityData).filter((key) => entityData[key] !== undefined &&
|
|
1985
2077
|
!!this.columnOf(this.config.table, key));
|
|
1986
2078
|
if (settable.length === 0) {
|
|
@@ -1989,45 +2081,49 @@ class SqlBaseCrudService {
|
|
|
1989
2081
|
return entityData;
|
|
1990
2082
|
}
|
|
1991
2083
|
async update(id, data, options) {
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
const processedData = hooks.skipBefore
|
|
2003
|
-
? data
|
|
2004
|
-
: await this.beforeUpdate(id, data);
|
|
2005
|
-
return this.prepareUpdate(processedData, opts);
|
|
2006
|
-
});
|
|
2007
|
-
if (this.config.timestamps?.updatedAt) {
|
|
2008
|
-
entityData[this.config.timestamps.updatedAt] = new Date();
|
|
2009
|
-
}
|
|
2010
|
-
const db = this.dbFor(transaction);
|
|
2011
|
-
await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
|
|
2012
|
-
try {
|
|
2013
|
-
await this.guardOnError(guarded, id, opts, () => this.assertKeysInScope(db, entityData, opts));
|
|
2014
|
-
const [updatedEntity] = await this.guardOnError(guarded || !!transaction, id, opts, () => this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts, this.softDeleteScope(opts)));
|
|
2015
|
-
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)
|
|
2016
2094
|
await this.guardRow(id, opts);
|
|
2017
|
-
|
|
2018
|
-
|
|
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");
|
|
2019
2116
|
}
|
|
2020
|
-
|
|
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;
|
|
2021
2122
|
}
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
return updatedEntity;
|
|
2027
|
-
}
|
|
2028
|
-
catch (error) {
|
|
2029
|
-
throw this.toClientError(error, "write");
|
|
2030
|
-
}
|
|
2123
|
+
catch (error) {
|
|
2124
|
+
throw this.toClientError(error, "write");
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2031
2127
|
}
|
|
2032
2128
|
async updateWhereIn(db, set, ids, options, extra) {
|
|
2033
2129
|
const where = this.writeWhere(ids, options);
|
|
@@ -2097,27 +2193,28 @@ class SqlBaseCrudService {
|
|
|
2097
2193
|
return false;
|
|
2098
2194
|
}
|
|
2099
2195
|
async softDelete(id, options) {
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
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
|
+
});
|
|
2121
2218
|
});
|
|
2122
2219
|
}
|
|
2123
2220
|
softDeleteSet(deletedAt) {
|
|
@@ -2129,42 +2226,44 @@ class SqlBaseCrudService {
|
|
|
2129
2226
|
});
|
|
2130
2227
|
}
|
|
2131
2228
|
async restore(id, options) {
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
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;
|
|
2151
2251
|
});
|
|
2152
|
-
await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
|
|
2153
|
-
return restored;
|
|
2154
2252
|
}
|
|
2155
2253
|
async delete(id, options) {
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
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
|
+
});
|
|
2168
2267
|
});
|
|
2169
2268
|
}
|
|
2170
2269
|
async forceDelete(id, options) {
|
|
@@ -2175,46 +2274,48 @@ class SqlBaseCrudService {
|
|
|
2175
2274
|
});
|
|
2176
2275
|
}
|
|
2177
2276
|
async upsert(data, upsert = {}, options) {
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
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
|
+
});
|
|
2202
2302
|
}
|
|
2203
2303
|
async massUpsert(data, upsert = {}, options) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
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
|
+
});
|
|
2218
2319
|
});
|
|
2219
2320
|
}
|
|
2220
2321
|
async upsertSet(tx, rows, upsert, opts, message) {
|
|
@@ -2369,8 +2470,9 @@ class SqlBaseCrudService {
|
|
|
2369
2470
|
ids: ids ? unique : this.idsOf(written),
|
|
2370
2471
|
rows: spec.returnsRows ? written : undefined,
|
|
2371
2472
|
}, options);
|
|
2372
|
-
if (spec.returnsRows)
|
|
2373
|
-
await load(tx, written);
|
|
2473
|
+
if (spec.returnsRows) {
|
|
2474
|
+
await load(tx, written, prepared.some((row) => this.holdsSql(row)));
|
|
2475
|
+
}
|
|
2374
2476
|
return ids ? this.inIdOrder(written, ids) : written;
|
|
2375
2477
|
};
|
|
2376
2478
|
return spec.returnsRows
|
|
@@ -2378,30 +2480,31 @@ class SqlBaseCrudService {
|
|
|
2378
2480
|
: this.executeSqlTransaction((tx) => body(tx, async () => { }), options?.transaction);
|
|
2379
2481
|
}
|
|
2380
2482
|
async massCreate(data, options) {
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
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
|
+
});
|
|
2405
2508
|
});
|
|
2406
2509
|
}
|
|
2407
2510
|
async insertMany(db, rows, options) {
|
|
@@ -2451,35 +2554,36 @@ class SqlBaseCrudService {
|
|
|
2451
2554
|
})));
|
|
2452
2555
|
}
|
|
2453
2556
|
async massUpdate(ids, data, options) {
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
|
|
2469
|
-
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
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);
|
|
2475
2585
|
},
|
|
2476
|
-
})
|
|
2477
|
-
keys: true,
|
|
2478
|
-
write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
|
|
2479
|
-
after: async (updated) => {
|
|
2480
|
-
for (const entity of updated)
|
|
2481
|
-
await this.afterUpdate(entity);
|
|
2482
|
-
},
|
|
2586
|
+
});
|
|
2483
2587
|
});
|
|
2484
2588
|
}
|
|
2485
2589
|
async updateSets(tx, sets, unique, opts, message) {
|
|
@@ -2520,99 +2624,105 @@ class SqlBaseCrudService {
|
|
|
2520
2624
|
: [];
|
|
2521
2625
|
}
|
|
2522
2626
|
async massSoftDelete(ids, options) {
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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
|
+
});
|
|
2528
2661
|
return true;
|
|
2529
|
-
}
|
|
2530
|
-
const message = "Mass soft delete errors";
|
|
2531
|
-
await this.massWrite({
|
|
2532
|
-
operation: "softDelete",
|
|
2533
|
-
message,
|
|
2534
|
-
options,
|
|
2535
|
-
ids,
|
|
2536
|
-
state: { withDeleted: false, onlyDeleted: false },
|
|
2537
|
-
returnsRows: false,
|
|
2538
|
-
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2539
|
-
? undefined
|
|
2540
|
-
: unique.map((id) => ({ id, run: () => this.beforeSoftDelete(id) })),
|
|
2541
|
-
write: (tx, _prepared, opts, unique) => {
|
|
2542
|
-
const set = this.softDeleteSet(new Date());
|
|
2543
|
-
const update = (db, keys) => db
|
|
2544
|
-
.update(this.config.table)
|
|
2545
|
-
.set(set)
|
|
2546
|
-
.where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
|
|
2547
|
-
return this.runSet(tx, message, (sp) => this.runKeyed(update(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => update(sp, [id]) })), "write");
|
|
2548
|
-
},
|
|
2549
|
-
after: async (_written, unique) => {
|
|
2550
|
-
for (const id of unique)
|
|
2551
|
-
await this.afterSoftDelete(id);
|
|
2552
|
-
},
|
|
2553
2662
|
});
|
|
2554
|
-
return true;
|
|
2555
2663
|
}
|
|
2556
2664
|
async massRestore(ids, options) {
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
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
|
+
});
|
|
2586
2695
|
});
|
|
2587
2696
|
}
|
|
2588
2697
|
async massDelete(ids, options) {
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
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
|
+
});
|
|
2592
2724
|
return true;
|
|
2593
|
-
}
|
|
2594
|
-
const message = "Mass delete errors";
|
|
2595
|
-
await this.massWrite({
|
|
2596
|
-
operation: "delete",
|
|
2597
|
-
message,
|
|
2598
|
-
options,
|
|
2599
|
-
ids,
|
|
2600
|
-
returnsRows: false,
|
|
2601
|
-
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2602
|
-
? undefined
|
|
2603
|
-
: unique.map((id) => ({ id, run: () => this.beforeDelete(id) })),
|
|
2604
|
-
write: (tx, _prepared, opts, unique) => {
|
|
2605
|
-
const remove = (db, keys) => db
|
|
2606
|
-
.delete(this.config.table)
|
|
2607
|
-
.where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
|
|
2608
|
-
return this.runSet(tx, message, (sp) => this.runKeyed(remove(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => remove(sp, [id]) })), "delete");
|
|
2609
|
-
},
|
|
2610
|
-
after: async (_written, unique) => {
|
|
2611
|
-
for (const id of unique)
|
|
2612
|
-
await this.afterDelete(id);
|
|
2613
|
-
},
|
|
2614
2725
|
});
|
|
2615
|
-
return true;
|
|
2616
2726
|
}
|
|
2617
2727
|
async massForceDelete(ids, options) {
|
|
2618
2728
|
return this.massDelete(ids, {
|
|
@@ -2621,6 +2731,10 @@ class SqlBaseCrudService {
|
|
|
2621
2731
|
onlyDeleted: false,
|
|
2622
2732
|
});
|
|
2623
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
|
+
}
|
|
2624
2738
|
uniqueIds(ids) {
|
|
2625
2739
|
const seen = new Set();
|
|
2626
2740
|
return (ids ?? []).filter((id) => {
|
|
@@ -2632,20 +2746,33 @@ class SqlBaseCrudService {
|
|
|
2632
2746
|
});
|
|
2633
2747
|
}
|
|
2634
2748
|
fingerprint(value) {
|
|
2635
|
-
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
|
+
}
|
|
2636
2757
|
if (typeof v === "string")
|
|
2637
2758
|
return `s:${v}`;
|
|
2638
2759
|
if (typeof v === "bigint")
|
|
2639
2760
|
return `bigint:${v}`;
|
|
2640
|
-
if (typeof v === "number" && !Number.isFinite(v))
|
|
2761
|
+
if (typeof v === "number" && !Number.isFinite(v)) {
|
|
2641
2762
|
return `number:${v}`;
|
|
2763
|
+
}
|
|
2642
2764
|
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
2643
2765
|
const proto = Object.getPrototypeOf(v);
|
|
2644
2766
|
if (proto !== Object.prototype && proto !== null) {
|
|
2645
|
-
|
|
2767
|
+
let tag = fingerprintTags.get(v);
|
|
2768
|
+
if (!tag) {
|
|
2769
|
+
tag = `opaque:${++fingerprintCount}`;
|
|
2770
|
+
fingerprintTags.set(v, tag);
|
|
2771
|
+
}
|
|
2772
|
+
return tag;
|
|
2646
2773
|
}
|
|
2647
2774
|
}
|
|
2648
|
-
return
|
|
2775
|
+
return json;
|
|
2649
2776
|
});
|
|
2650
2777
|
}
|
|
2651
2778
|
async assertAllExist(tx, ids, options, message) {
|
|
@@ -2781,44 +2908,50 @@ class SqlBaseCrudService {
|
|
|
2781
2908
|
: run(tx);
|
|
2782
2909
|
}
|
|
2783
2910
|
async exists(id, options) {
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
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;
|
|
2791
2920
|
});
|
|
2792
|
-
return !!entity;
|
|
2793
2921
|
}
|
|
2794
2922
|
async count(filters, options) {
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
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
|
+
});
|
|
2801
2930
|
}
|
|
2802
2931
|
async countBy(column, filters, options) {
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
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
|
+
});
|
|
2822
2955
|
}
|
|
2823
2956
|
resolveGroupColumn(key, options) {
|
|
2824
2957
|
if (typeof key !== "string" || !key) {
|
|
@@ -3352,49 +3485,50 @@ class SqlBaseCrudService {
|
|
|
3352
3485
|
}
|
|
3353
3486
|
}
|
|
3354
3487
|
async fullTextSearch(searchTerm, searchColumns, pagination, options) {
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
}
|
|
3359
|
-
if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
|
|
3360
|
-
throw new common_1.BadRequestException("Full-text search needs at least one column");
|
|
3361
|
-
}
|
|
3362
|
-
const columns = searchColumns.map((name) => {
|
|
3363
|
-
const column = this.columnOf(this.config.table, name);
|
|
3364
|
-
if (!column) {
|
|
3365
|
-
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");
|
|
3366
3491
|
}
|
|
3367
|
-
|
|
3368
|
-
|
|
3369
|
-
|
|
3370
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
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 };
|
|
3374
3531
|
});
|
|
3375
|
-
const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
3376
|
-
const plan = this.planList(filters, undefined, "asc", readOptions);
|
|
3377
|
-
const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
|
|
3378
|
-
const joins = Array.from(new Set([
|
|
3379
|
-
...plan.joined.map((node) => node.name),
|
|
3380
|
-
...plan.filterRelations,
|
|
3381
|
-
]));
|
|
3382
|
-
const pk = this.columnOf(this.config.table, this.config.primaryKey);
|
|
3383
|
-
const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
|
|
3384
|
-
if (pk)
|
|
3385
|
-
orderBy.push((0, drizzle_orm_1.asc)(pk));
|
|
3386
|
-
let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
|
|
3387
|
-
dataQuery = this.applyJoins(dataQuery, joins, readOptions)
|
|
3388
|
-
.where((0, drizzle_orm_1.and)(...conditions))
|
|
3389
|
-
.orderBy(...orderBy)
|
|
3390
|
-
.limit(limit)
|
|
3391
|
-
.offset(offset);
|
|
3392
|
-
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
3393
|
-
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
3394
|
-
const total = this.parseCount(totalResult);
|
|
3395
|
-
const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
|
|
3396
|
-
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
|
|
3397
|
-
return { data: rows, total, page, limit };
|
|
3398
3532
|
}
|
|
3399
3533
|
}
|
|
3400
3534
|
exports.SqlBaseCrudService = SqlBaseCrudService;
|