@avytrix/nestjs-drizzle-crud 3.7.2 → 3.8.0
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.
|
@@ -10,7 +10,14 @@ const merge_config_1 = require("../utils/merge-config");
|
|
|
10
10
|
const query_parser_1 = require("../utils/query-parser");
|
|
11
11
|
const JOIN_KEY = "__crudJoinKey";
|
|
12
12
|
const CURSOR_KEY = "__crudCursor";
|
|
13
|
+
const SCOPE_MEMO = Symbol("scopeMemo");
|
|
13
14
|
const ROLLBACK = Symbol("rollback");
|
|
15
|
+
class DeferredDiagnosis {
|
|
16
|
+
settle;
|
|
17
|
+
constructor(settle) {
|
|
18
|
+
this.settle = settle;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
14
21
|
const BIND_LIMITS = {
|
|
15
22
|
postgresql: { limit: 65_535, name: "PostgreSQL" },
|
|
16
23
|
mysql: { limit: 65_535, name: "MySQL" },
|
|
@@ -185,9 +192,25 @@ class SqlBaseCrudService {
|
|
|
185
192
|
const scope = this.config.scope;
|
|
186
193
|
if (!scope || options?.skipScope)
|
|
187
194
|
return null;
|
|
195
|
+
const memo = options?.[SCOPE_MEMO];
|
|
196
|
+
if (memo && Object.hasOwn(memo, "values"))
|
|
197
|
+
return memo.values;
|
|
188
198
|
const values = typeof scope === "function"
|
|
189
199
|
? scope(options?.context ?? crud_context_1.CrudContext.current() ?? {})
|
|
190
200
|
: scope;
|
|
201
|
+
const checked = this.checkScope(values);
|
|
202
|
+
if (memo)
|
|
203
|
+
memo.values = checked;
|
|
204
|
+
return checked;
|
|
205
|
+
}
|
|
206
|
+
withScopeMemo(options) {
|
|
207
|
+
if (typeof this.config.scope !== "function" ||
|
|
208
|
+
options?.[SCOPE_MEMO]) {
|
|
209
|
+
return options;
|
|
210
|
+
}
|
|
211
|
+
return { ...options, [SCOPE_MEMO]: {} };
|
|
212
|
+
}
|
|
213
|
+
checkScope(values) {
|
|
191
214
|
if (values === null)
|
|
192
215
|
return null;
|
|
193
216
|
if (!(0, merge_config_1.isPlainObject)(values)) {
|
|
@@ -422,6 +445,10 @@ class SqlBaseCrudService {
|
|
|
422
445
|
return relation.type;
|
|
423
446
|
return "belongsTo";
|
|
424
447
|
}
|
|
448
|
+
relationNamed(name) {
|
|
449
|
+
const relations = this.config.relations || {};
|
|
450
|
+
return Object.hasOwn(relations, name) ? relations[name] : undefined;
|
|
451
|
+
}
|
|
425
452
|
isCollection(relation) {
|
|
426
453
|
return this.relationType(relation) !== "belongsTo";
|
|
427
454
|
}
|
|
@@ -612,23 +639,19 @@ class SqlBaseCrudService {
|
|
|
612
639
|
const referenced = target[referencedName];
|
|
613
640
|
if (!referenced)
|
|
614
641
|
return;
|
|
615
|
-
const ids =
|
|
616
|
-
if (ids.length === 0)
|
|
617
|
-
for (const host of hosts)
|
|
618
|
-
host[node.name] = null;
|
|
642
|
+
const ids = this.loaderKeys(hosts, localKey, node);
|
|
643
|
+
if (ids.length === 0)
|
|
619
644
|
return;
|
|
620
|
-
}
|
|
621
645
|
const fields = this.relationProjection(target, select.base, [
|
|
622
646
|
referencedName,
|
|
623
647
|
...this.keysForChildren(node.children, target),
|
|
624
648
|
]);
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
const related = await (fields ? db.select(fields) : db.select())
|
|
649
|
+
const scope = this.relatedRowScope(target, options);
|
|
650
|
+
const related = await this.inChunks(db, ids, (chunk) => (fields ? db.select(fields) : db.select())
|
|
628
651
|
.from(target)
|
|
629
|
-
.where((0, drizzle_orm_1.and)(...
|
|
652
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(referenced, chunk), ...scope)));
|
|
630
653
|
const byKey = new Map();
|
|
631
|
-
for (const row of related
|
|
654
|
+
for (const row of related)
|
|
632
655
|
byKey.set(row?.[referencedName], row);
|
|
633
656
|
for (const host of hosts) {
|
|
634
657
|
host[node.name] = byKey.get(host[localKey]) ?? null;
|
|
@@ -643,27 +666,43 @@ class SqlBaseCrudService {
|
|
|
643
666
|
if (!foreign)
|
|
644
667
|
return;
|
|
645
668
|
const parentKey = this.parentKeyOf(node.config, hostTable);
|
|
646
|
-
const ids =
|
|
647
|
-
if (ids.length === 0)
|
|
648
|
-
for (const host of hosts)
|
|
649
|
-
host[node.name] = [];
|
|
669
|
+
const ids = this.loaderKeys(hosts, parentKey, node);
|
|
670
|
+
if (ids.length === 0)
|
|
650
671
|
return;
|
|
651
|
-
}
|
|
652
672
|
const fields = this.relationProjection(target, select.base, [
|
|
653
673
|
foreignKey,
|
|
654
674
|
...this.keysForChildren(node.children, target),
|
|
655
675
|
]);
|
|
656
|
-
const
|
|
657
|
-
conditions.push(...this.relatedRowScope(target, options));
|
|
658
|
-
let query = (fields ? db.select(fields) : db.select())
|
|
659
|
-
.from(target)
|
|
660
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
661
|
-
const orderBy = this.collectionOrderBy(node.config);
|
|
662
|
-
if (orderBy.length > 0)
|
|
663
|
-
query = query.orderBy(...orderBy);
|
|
664
|
-
const children = await query;
|
|
676
|
+
const children = await this.loadCollection(db, node, ids, options, () => fields ? db.select(fields).from(target) : db.select().from(target));
|
|
665
677
|
this.groupCollection(hosts, node.name, parentKey, children, foreignKey);
|
|
666
678
|
}
|
|
679
|
+
loaderKeys(hosts, key, node) {
|
|
680
|
+
const ids = Array.from(new Set(hosts.map((host) => host[key]).filter((v) => v != null)));
|
|
681
|
+
if (ids.length === 0) {
|
|
682
|
+
const collection = this.isCollection(node.config);
|
|
683
|
+
for (const host of hosts)
|
|
684
|
+
host[node.name] = collection ? [] : null;
|
|
685
|
+
}
|
|
686
|
+
return ids;
|
|
687
|
+
}
|
|
688
|
+
loadCollection(db, node, ids, options, source) {
|
|
689
|
+
const relation = node.config;
|
|
690
|
+
const key = this.relationType(relation) === "manyToMany"
|
|
691
|
+
? relation.through[relation.throughLocalKey]
|
|
692
|
+
: relation.table[relation.foreignKey];
|
|
693
|
+
const scope = this.relatedRowScope(relation.table, options);
|
|
694
|
+
const orderBy = this.collectionOrderBy(relation);
|
|
695
|
+
return this.inChunks(db, ids, (chunk) => {
|
|
696
|
+
const query = source().where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(key, chunk), ...scope));
|
|
697
|
+
return orderBy.length > 0 ? query.orderBy(...orderBy) : query;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
selectThrough(db, fields, relation, target = relation.table, through = relation.through) {
|
|
701
|
+
return db
|
|
702
|
+
.select(fields)
|
|
703
|
+
.from(through)
|
|
704
|
+
.innerJoin(target, (0, drizzle_orm_1.eq)(through[relation.throughForeignKey], target[relation.references ?? "id"]));
|
|
705
|
+
}
|
|
667
706
|
async loadManyToMany(db, hosts, node, select, hostTable = this.config.table, options) {
|
|
668
707
|
const { through, throughLocalKey, throughForeignKey, references } = node.config;
|
|
669
708
|
if (!through || !throughLocalKey || !throughForeignKey)
|
|
@@ -676,12 +715,9 @@ class SqlBaseCrudService {
|
|
|
676
715
|
if (!referenced || !joinLocal || !joinForeign)
|
|
677
716
|
return;
|
|
678
717
|
const parentKey = this.parentKeyOf(node.config, hostTable);
|
|
679
|
-
const ids =
|
|
680
|
-
if (ids.length === 0)
|
|
681
|
-
for (const host of hosts)
|
|
682
|
-
host[node.name] = [];
|
|
718
|
+
const ids = this.loaderKeys(hosts, parentKey, node);
|
|
719
|
+
if (ids.length === 0)
|
|
683
720
|
return;
|
|
684
|
-
}
|
|
685
721
|
const columns = (0, drizzle_orm_1.getTableColumns)(target);
|
|
686
722
|
const wanted = select.base.length > 0
|
|
687
723
|
? [...select.base, ...this.keysForChildren(node.children, target)]
|
|
@@ -691,19 +727,25 @@ class SqlBaseCrudService {
|
|
|
691
727
|
if (columns[name])
|
|
692
728
|
fields[name] = columns[name];
|
|
693
729
|
}
|
|
694
|
-
const
|
|
695
|
-
conditions.push(...this.relatedRowScope(target, options));
|
|
696
|
-
let query = db
|
|
697
|
-
.select(fields)
|
|
698
|
-
.from(through)
|
|
699
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(joinForeign, referenced))
|
|
700
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
701
|
-
const orderBy = this.collectionOrderBy(node.config);
|
|
702
|
-
if (orderBy.length > 0)
|
|
703
|
-
query = query.orderBy(...orderBy);
|
|
704
|
-
const rows = await query;
|
|
730
|
+
const rows = await this.loadCollection(db, node, ids, options, () => this.selectThrough(db, fields, node.config));
|
|
705
731
|
this.groupCollection(hosts, node.name, parentKey, rows, JOIN_KEY, true);
|
|
706
732
|
}
|
|
733
|
+
relationChunkSize(db) {
|
|
734
|
+
const { limit } = this.bindLimit(db);
|
|
735
|
+
return Math.max(1, limit - Math.min(1_000, Math.floor(limit / 10)));
|
|
736
|
+
}
|
|
737
|
+
async inChunks(db, ids, query) {
|
|
738
|
+
const size = this.relationChunkSize(db);
|
|
739
|
+
if (ids.length <= size)
|
|
740
|
+
return (await query(ids)) ?? [];
|
|
741
|
+
const out = [];
|
|
742
|
+
for (let start = 0; start < ids.length; start += size) {
|
|
743
|
+
for (const row of (await query(ids.slice(start, start + size))) ?? []) {
|
|
744
|
+
out.push(row);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return out;
|
|
748
|
+
}
|
|
707
749
|
groupCollection(hosts, name, parentKey, rows, groupBy, stripGroupKey = false) {
|
|
708
750
|
const grouped = new Map();
|
|
709
751
|
for (const row of rows ?? []) {
|
|
@@ -839,10 +881,7 @@ class SqlBaseCrudService {
|
|
|
839
881
|
return { key, column, order };
|
|
840
882
|
}
|
|
841
883
|
const name = key.slice(0, dot);
|
|
842
|
-
const
|
|
843
|
-
const relation = Object.hasOwn(relations, name)
|
|
844
|
-
? relations[name]
|
|
845
|
-
: undefined;
|
|
884
|
+
const relation = this.relationNamed(name);
|
|
846
885
|
const column = relation
|
|
847
886
|
? this.columnOf(relation.table, key.slice(dot + 1))
|
|
848
887
|
: undefined;
|
|
@@ -1033,11 +1072,22 @@ class SqlBaseCrudService {
|
|
|
1033
1072
|
throw new common_1.BadRequestException(`Invalid full-text query mode: ${String(mode)} (expected 'plain' or 'websearch')`);
|
|
1034
1073
|
}
|
|
1035
1074
|
const parse = drizzle_orm_1.sql.raw(mode === "websearch" ? "websearch_to_tsquery" : "plainto_tsquery");
|
|
1036
|
-
const vector = drizzle_orm_1.sql.join(columns.map((column) =>
|
|
1037
|
-
|
|
1038
|
-
|
|
1075
|
+
const vector = drizzle_orm_1.sql.join(columns.map((column) => {
|
|
1076
|
+
if (this.isTsVector(column)) {
|
|
1077
|
+
return columns.length > 1
|
|
1078
|
+
? (0, drizzle_orm_1.sql) `coalesce(${column}, ''::tsvector)`
|
|
1079
|
+
: (0, drizzle_orm_1.sql) `${column}`;
|
|
1080
|
+
}
|
|
1081
|
+
return columns.length > 1
|
|
1082
|
+
? (0, drizzle_orm_1.sql) `to_tsvector(${config}, coalesce(${column}, ''))`
|
|
1083
|
+
: (0, drizzle_orm_1.sql) `to_tsvector(${config}, ${column})`;
|
|
1084
|
+
}), (0, drizzle_orm_1.sql) ` || `);
|
|
1039
1085
|
return { vector, query: (0, drizzle_orm_1.sql) `${parse}(${config}, ${term})` };
|
|
1040
1086
|
}
|
|
1087
|
+
isTsVector(column) {
|
|
1088
|
+
const type = typeof column?.getSQLType === "function" ? column.getSQLType() : "";
|
|
1089
|
+
return String(type).trim().toLowerCase() === "tsvector";
|
|
1090
|
+
}
|
|
1041
1091
|
buildSearchCondition(search) {
|
|
1042
1092
|
if (!search?.term || !search.columns?.length)
|
|
1043
1093
|
return undefined;
|
|
@@ -1129,6 +1179,29 @@ class SqlBaseCrudService {
|
|
|
1129
1179
|
}
|
|
1130
1180
|
return row;
|
|
1131
1181
|
}
|
|
1182
|
+
async guardRow(id, options) {
|
|
1183
|
+
const row = await this.findForGuard(id, options);
|
|
1184
|
+
if (!row)
|
|
1185
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1186
|
+
}
|
|
1187
|
+
skipsGuard(options, ...methods) {
|
|
1188
|
+
if (!this.config.sql?.useReturning)
|
|
1189
|
+
return false;
|
|
1190
|
+
if (this.config.events && !options?.skipEvents)
|
|
1191
|
+
return false;
|
|
1192
|
+
const base = SqlBaseCrudService.prototype;
|
|
1193
|
+
return methods.every((name) => this[name] === base[name]);
|
|
1194
|
+
}
|
|
1195
|
+
async guardOnError(guarded, id, options, run) {
|
|
1196
|
+
try {
|
|
1197
|
+
return await run();
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
if (!guarded)
|
|
1201
|
+
await this.guardRow(id, options);
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1132
1205
|
assertVersion(id, actual, expected) {
|
|
1133
1206
|
if (expected === undefined || expected === null)
|
|
1134
1207
|
return;
|
|
@@ -1171,14 +1244,21 @@ class SqlBaseCrudService {
|
|
|
1171
1244
|
};
|
|
1172
1245
|
}
|
|
1173
1246
|
async find(id, options) {
|
|
1247
|
+
options = this.withScopeMemo(options);
|
|
1174
1248
|
this.checkReadOptions(options);
|
|
1175
1249
|
return this.findFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], options);
|
|
1176
1250
|
}
|
|
1177
1251
|
async findOne(where, options) {
|
|
1252
|
+
options = this.withScopeMemo(options);
|
|
1178
1253
|
this.checkReadOptions(options);
|
|
1179
1254
|
return this.findFirst(this.buildWhereConditionsFromPartial(where), options);
|
|
1180
1255
|
}
|
|
1181
1256
|
async findFirst(conditions, options) {
|
|
1257
|
+
const first = await this.readFirst(conditions, options);
|
|
1258
|
+
await first.hydrate();
|
|
1259
|
+
return first.row;
|
|
1260
|
+
}
|
|
1261
|
+
async readFirst(conditions, options) {
|
|
1182
1262
|
const { transaction, relations = [], select = [] } = options || {};
|
|
1183
1263
|
const db = this.dbFor(transaction);
|
|
1184
1264
|
const nodes = this.relationTree(relations);
|
|
@@ -1193,11 +1273,16 @@ class SqlBaseCrudService {
|
|
|
1193
1273
|
query = this.applyLock(query, options);
|
|
1194
1274
|
const result = await this.runRead(query);
|
|
1195
1275
|
const rows = this.normalizeRelations(result, joined.map((node) => node.name));
|
|
1276
|
+
return {
|
|
1277
|
+
row: rows[0] || null,
|
|
1278
|
+
hydrate: () => this.hydrate(db, rows, nodes, selection, options),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async hydrate(db, rows, nodes, selection, options) {
|
|
1196
1282
|
await Promise.all([
|
|
1197
1283
|
this.loadRelations(db, rows, nodes, selection, undefined, options),
|
|
1198
1284
|
this.loadCounts(db, rows, options?.counts, options),
|
|
1199
1285
|
]);
|
|
1200
|
-
return rows[0] || null;
|
|
1201
1286
|
}
|
|
1202
1287
|
planList(filters, sortBy, sortOrder, options) {
|
|
1203
1288
|
this.checkReadOptions(options);
|
|
@@ -1234,6 +1319,9 @@ class SqlBaseCrudService {
|
|
|
1234
1319
|
built.conditions.push(...this.rowScope(options));
|
|
1235
1320
|
return built;
|
|
1236
1321
|
}
|
|
1322
|
+
parseCount(result) {
|
|
1323
|
+
return parseInt(result?.[0]?.count?.toString() || "0", 10);
|
|
1324
|
+
}
|
|
1237
1325
|
buildCountQuery(db, conditions, filterRelations, options) {
|
|
1238
1326
|
let query = db
|
|
1239
1327
|
.select({ count: (0, drizzle_orm_1.sql) `count(*)` })
|
|
@@ -1244,6 +1332,7 @@ class SqlBaseCrudService {
|
|
|
1244
1332
|
return query;
|
|
1245
1333
|
}
|
|
1246
1334
|
async findAll(filters, pagination, options) {
|
|
1335
|
+
options = this.withScopeMemo(options);
|
|
1247
1336
|
const { sortBy, sortOrder = "asc" } = pagination || {};
|
|
1248
1337
|
const withCount = pagination?.withCount !== false;
|
|
1249
1338
|
const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
@@ -1266,10 +1355,7 @@ class SqlBaseCrudService {
|
|
|
1266
1355
|
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
1267
1356
|
const data = fetched.slice(0, safeLimit);
|
|
1268
1357
|
const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
|
|
1269
|
-
await
|
|
1270
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
|
|
1271
|
-
this.loadCounts(plan.db, rows, options?.counts, options),
|
|
1272
|
-
]);
|
|
1358
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1273
1359
|
if (!withCount) {
|
|
1274
1360
|
return {
|
|
1275
1361
|
data: rows,
|
|
@@ -1281,12 +1367,13 @@ class SqlBaseCrudService {
|
|
|
1281
1367
|
}
|
|
1282
1368
|
return {
|
|
1283
1369
|
data: rows,
|
|
1284
|
-
total:
|
|
1370
|
+
total: this.parseCount(totalResult),
|
|
1285
1371
|
page,
|
|
1286
1372
|
limit: safeLimit,
|
|
1287
1373
|
};
|
|
1288
1374
|
}
|
|
1289
1375
|
async findByCursor(filters, pagination, options) {
|
|
1376
|
+
options = this.withScopeMemo(options);
|
|
1290
1377
|
const { sortBy, sortOrder = "asc", count = false } = pagination || {};
|
|
1291
1378
|
const { limit } = this.resolvePagination(1, pagination?.limit);
|
|
1292
1379
|
const plan = this.planList(filters, sortBy, sortOrder, options);
|
|
@@ -1303,9 +1390,17 @@ class SqlBaseCrudService {
|
|
|
1303
1390
|
conditions.push(this.keysetCondition(plan.sort, values));
|
|
1304
1391
|
}
|
|
1305
1392
|
const fields = this.buildSelection(plan.baseSelect, plan.joined, plan.selection);
|
|
1306
|
-
plan.sort.
|
|
1307
|
-
|
|
1393
|
+
const cursorKeys = plan.sort.map((spec, i) => {
|
|
1394
|
+
const field = this.cursorField(spec.column);
|
|
1395
|
+
const projected = field === spec.column && !spec.relation
|
|
1396
|
+
? Object.keys(fields).find((key) => fields[key] === spec.column)
|
|
1397
|
+
: undefined;
|
|
1398
|
+
if (projected)
|
|
1399
|
+
return projected;
|
|
1400
|
+
fields[`${CURSOR_KEY}${i}`] = field;
|
|
1401
|
+
return `${CURSOR_KEY}${i}`;
|
|
1308
1402
|
});
|
|
1403
|
+
const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
|
|
1309
1404
|
let dataQuery = plan.db.select(fields).from(this.config.table);
|
|
1310
1405
|
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1311
1406
|
if (conditions.length > 0)
|
|
@@ -1323,21 +1418,17 @@ class SqlBaseCrudService {
|
|
|
1323
1418
|
const page = data.slice(0, limit);
|
|
1324
1419
|
const last = page[page.length - 1];
|
|
1325
1420
|
const nextCursor = data.length > limit && last
|
|
1326
|
-
? (0, cursor_1.encodeCursor)(
|
|
1421
|
+
? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
|
|
1327
1422
|
: null;
|
|
1328
1423
|
for (const row of page) {
|
|
1329
|
-
|
|
1330
|
-
delete row[
|
|
1331
|
-
});
|
|
1424
|
+
for (const key of hidden)
|
|
1425
|
+
delete row[key];
|
|
1332
1426
|
}
|
|
1333
1427
|
const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
|
|
1334
|
-
await
|
|
1335
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
|
|
1336
|
-
this.loadCounts(plan.db, rows, options?.counts, options),
|
|
1337
|
-
]);
|
|
1428
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1338
1429
|
const out = { data: rows, limit, nextCursor };
|
|
1339
1430
|
if (count) {
|
|
1340
|
-
out.total =
|
|
1431
|
+
out.total = this.parseCount(totalResult);
|
|
1341
1432
|
}
|
|
1342
1433
|
return out;
|
|
1343
1434
|
}
|
|
@@ -1356,9 +1447,16 @@ class SqlBaseCrudService {
|
|
|
1356
1447
|
return (0, drizzle_orm_1.gt)(column, value);
|
|
1357
1448
|
if (op === "<")
|
|
1358
1449
|
return (0, drizzle_orm_1.lt)(column, value);
|
|
1450
|
+
if (op === ">=")
|
|
1451
|
+
return (0, drizzle_orm_1.gte)(column, value);
|
|
1452
|
+
if (op === "<=")
|
|
1453
|
+
return (0, drizzle_orm_1.lte)(column, value);
|
|
1359
1454
|
return (0, drizzle_orm_1.eq)(column, value);
|
|
1360
1455
|
}
|
|
1361
1456
|
keysetCondition(specs, values) {
|
|
1457
|
+
const row = this.keysetRowComparison(specs, values);
|
|
1458
|
+
if (row)
|
|
1459
|
+
return row;
|
|
1362
1460
|
const branches = [];
|
|
1363
1461
|
specs.forEach((spec, i) => {
|
|
1364
1462
|
const after = this.afterCursor(spec, values[i]);
|
|
@@ -1371,21 +1469,64 @@ class SqlBaseCrudService {
|
|
|
1371
1469
|
: this.compareCursor(previous.column, "=", values[j]));
|
|
1372
1470
|
branches.push(ties.length > 0 ? (0, drizzle_orm_1.and)(...ties, after) : after);
|
|
1373
1471
|
});
|
|
1374
|
-
|
|
1472
|
+
if (branches.length === 0)
|
|
1473
|
+
return (0, drizzle_orm_1.sql) `false`;
|
|
1474
|
+
const after = (0, drizzle_orm_1.or)(...branches);
|
|
1475
|
+
const bound = this.keysetBound(specs, values);
|
|
1476
|
+
return bound ? (0, drizzle_orm_1.and)(bound, after) : after;
|
|
1477
|
+
}
|
|
1478
|
+
keysetBound(specs, values) {
|
|
1479
|
+
const [first] = specs;
|
|
1480
|
+
if (!first || specs.length < 2 || values[0] === null)
|
|
1481
|
+
return undefined;
|
|
1482
|
+
if (this.keysetNullable(first) && !this.nullsFirst(first)) {
|
|
1483
|
+
return undefined;
|
|
1484
|
+
}
|
|
1485
|
+
return this.compareCursor(first.column, first.order === "asc" ? ">=" : "<=", values[0]);
|
|
1486
|
+
}
|
|
1487
|
+
keysetRowComparison(specs, values) {
|
|
1488
|
+
if (this.config.dialect !== "postgresql" || specs.length < 2) {
|
|
1489
|
+
return undefined;
|
|
1490
|
+
}
|
|
1491
|
+
const order = specs[0].order;
|
|
1492
|
+
const fits = specs.every((spec, i) => spec.order === order &&
|
|
1493
|
+
values[i] !== null &&
|
|
1494
|
+
!this.keysetNullable(spec));
|
|
1495
|
+
if (!fits)
|
|
1496
|
+
return undefined;
|
|
1497
|
+
const columns = drizzle_orm_1.sql.join(specs.map((spec) => spec.column), (0, drizzle_orm_1.sql) `, `);
|
|
1498
|
+
const params = drizzle_orm_1.sql.join(specs.map((spec, i) => spec.column?.dataType === "date"
|
|
1499
|
+
? (0, drizzle_orm_1.sql) `${values[i]}`
|
|
1500
|
+
: drizzle_orm_1.sql.param(values[i], spec.column)), (0, drizzle_orm_1.sql) `, `);
|
|
1501
|
+
return (0, drizzle_orm_1.sql) `(${columns}) ${drizzle_orm_1.sql.raw(order === "asc" ? ">" : "<")} (${params})`;
|
|
1502
|
+
}
|
|
1503
|
+
keysetNullable(spec) {
|
|
1504
|
+
if (spec.relation)
|
|
1505
|
+
return true;
|
|
1506
|
+
return !(spec.column?.notNull || spec.column?.primary);
|
|
1507
|
+
}
|
|
1508
|
+
nullsFirst(spec) {
|
|
1509
|
+
return this.config.dialect === "postgresql"
|
|
1510
|
+
? spec.order === "desc"
|
|
1511
|
+
: spec.order === "asc";
|
|
1375
1512
|
}
|
|
1376
1513
|
afterCursor(spec, value) {
|
|
1377
|
-
const
|
|
1378
|
-
const nullsFirst = postgres ? spec.order === "desc" : spec.order === "asc";
|
|
1514
|
+
const nullsFirst = this.nullsFirst(spec);
|
|
1379
1515
|
if (value === null) {
|
|
1380
1516
|
return nullsFirst ? (0, drizzle_orm_1.isNotNull)(spec.column) : undefined;
|
|
1381
1517
|
}
|
|
1382
1518
|
const past = this.compareCursor(spec.column, spec.order === "asc" ? ">" : "<", value);
|
|
1383
|
-
return nullsFirst
|
|
1519
|
+
return nullsFirst || !this.keysetNullable(spec)
|
|
1520
|
+
? past
|
|
1521
|
+
: (0, drizzle_orm_1.or)(past, (0, drizzle_orm_1.isNull)(spec.column));
|
|
1384
1522
|
}
|
|
1385
1523
|
async emitEvent(phase, operation, bulk, payload, options) {
|
|
1386
1524
|
const bus = this.config.events;
|
|
1387
1525
|
if (!bus || options?.skipEvents)
|
|
1388
1526
|
return;
|
|
1527
|
+
if (payload.transaction && typeof payload.transaction === "object") {
|
|
1528
|
+
this.ownedTransactions.delete(payload.transaction);
|
|
1529
|
+
}
|
|
1389
1530
|
await bus.emit({
|
|
1390
1531
|
entity: this.getEntityName(),
|
|
1391
1532
|
operation,
|
|
@@ -1407,10 +1548,26 @@ class SqlBaseCrudService {
|
|
|
1407
1548
|
return;
|
|
1408
1549
|
}
|
|
1409
1550
|
const hosts = rows.filter(Boolean);
|
|
1410
|
-
await
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1551
|
+
await this.hydrate(db, hosts, nodes, undefined, options);
|
|
1552
|
+
}
|
|
1553
|
+
loadsAfterCommit(options) {
|
|
1554
|
+
if (options?.transaction)
|
|
1555
|
+
return false;
|
|
1556
|
+
try {
|
|
1557
|
+
return this.countPlan(options?.counts).every((count) => !count.filter);
|
|
1558
|
+
}
|
|
1559
|
+
catch {
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async writeThenLoad(options, write) {
|
|
1564
|
+
const late = this.loadsAfterCommit(options);
|
|
1565
|
+
const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written) => late
|
|
1566
|
+
? Promise.resolve()
|
|
1567
|
+
: this.loadWriteRelations(inTx, written, options)), options?.transaction);
|
|
1568
|
+
if (late)
|
|
1569
|
+
await this.loadWriteRelations(this.dbFor(), rows, options);
|
|
1570
|
+
return rows;
|
|
1414
1571
|
}
|
|
1415
1572
|
expectedVersionOf(data, options) {
|
|
1416
1573
|
const column = this.config.versionColumn;
|
|
@@ -1466,6 +1623,7 @@ class SqlBaseCrudService {
|
|
|
1466
1623
|
return createdEntity;
|
|
1467
1624
|
}
|
|
1468
1625
|
async create(data, options) {
|
|
1626
|
+
options = this.withScopeMemo(options);
|
|
1469
1627
|
const { transaction, hooks = {} } = options || {};
|
|
1470
1628
|
this.checkReadOptions(options);
|
|
1471
1629
|
const entityData = await this.prepareCreate(data, options);
|
|
@@ -1564,7 +1722,7 @@ class SqlBaseCrudService {
|
|
|
1564
1722
|
.returning();
|
|
1565
1723
|
ids.push(...inserted.map((r) => r[relation.references ?? "id"]));
|
|
1566
1724
|
}
|
|
1567
|
-
await this.linkTargets(tx, relation, parentValue, ids, options);
|
|
1725
|
+
await this.linkTargets(tx, relation, parentValue, ids, options, true);
|
|
1568
1726
|
}
|
|
1569
1727
|
return created;
|
|
1570
1728
|
}
|
|
@@ -1645,7 +1803,7 @@ class SqlBaseCrudService {
|
|
|
1645
1803
|
: "reference";
|
|
1646
1804
|
return new crud_exceptions_1.ForeignKeyViolationException(this.getEntityName(), field);
|
|
1647
1805
|
}
|
|
1648
|
-
async linkTargets(tx, relation, parentValue, ids, options) {
|
|
1806
|
+
async linkTargets(tx, relation, parentValue, ids, options, fresh = false) {
|
|
1649
1807
|
const wanted = this.uniqueValues(ids);
|
|
1650
1808
|
if (wanted.length === 0)
|
|
1651
1809
|
return;
|
|
@@ -1669,10 +1827,12 @@ class SqlBaseCrudService {
|
|
|
1669
1827
|
error: new crud_exceptions_1.EntityNotFoundException(name, id),
|
|
1670
1828
|
})));
|
|
1671
1829
|
}
|
|
1672
|
-
const linked =
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1830
|
+
const linked = fresh
|
|
1831
|
+
? []
|
|
1832
|
+
: await tx
|
|
1833
|
+
.select({ id: foreign })
|
|
1834
|
+
.from(through)
|
|
1835
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(local, parentValue), (0, drizzle_orm_1.inArray)(foreign, wanted)));
|
|
1676
1836
|
const already = new Set(linked.map((row) => this.valueKey(row.id)));
|
|
1677
1837
|
const add = wanted.filter((id) => !already.has(this.valueKey(id)));
|
|
1678
1838
|
if (add.length > 0) {
|
|
@@ -1683,19 +1843,19 @@ class SqlBaseCrudService {
|
|
|
1683
1843
|
}
|
|
1684
1844
|
}
|
|
1685
1845
|
async attach(id, relation, relatedIds, options) {
|
|
1846
|
+
options = this.withScopeMemo(options);
|
|
1686
1847
|
return this.changeLinks("attach", id, relation, relatedIds, options);
|
|
1687
1848
|
}
|
|
1688
1849
|
async detach(id, relation, relatedIds, options) {
|
|
1850
|
+
options = this.withScopeMemo(options);
|
|
1689
1851
|
return this.changeLinks("detach", id, relation, relatedIds, options);
|
|
1690
1852
|
}
|
|
1691
1853
|
async sync(id, relation, relatedIds, options) {
|
|
1854
|
+
options = this.withScopeMemo(options);
|
|
1692
1855
|
return this.changeLinks("sync", id, relation, relatedIds, options);
|
|
1693
1856
|
}
|
|
1694
1857
|
async changeLinks(operation, id, name, relatedIds, options) {
|
|
1695
|
-
const
|
|
1696
|
-
const relation = Object.hasOwn(relations, name)
|
|
1697
|
-
? relations[name]
|
|
1698
|
-
: undefined;
|
|
1858
|
+
const relation = this.relationNamed(name);
|
|
1699
1859
|
if (!relation || this.relationType(relation) !== "manyToMany") {
|
|
1700
1860
|
throw new common_1.BadRequestException(`"${name}" is not a many-to-many relation of ${this.getEntityName()}`);
|
|
1701
1861
|
}
|
|
@@ -1716,13 +1876,13 @@ class SqlBaseCrudService {
|
|
|
1716
1876
|
const parentKey = this.parentKeyOf(relation);
|
|
1717
1877
|
return this.executeSqlTransaction(async (tx) => {
|
|
1718
1878
|
const opts = { ...options, transaction: tx };
|
|
1719
|
-
const
|
|
1879
|
+
const read = {
|
|
1720
1880
|
...opts,
|
|
1721
|
-
relations: [],
|
|
1722
|
-
counts: undefined,
|
|
1723
|
-
select: [this.config.primaryKey, parentKey],
|
|
1881
|
+
relations: Array.from(new Set([name, ...(options?.relations ?? [])])),
|
|
1724
1882
|
skipAllowlists: true,
|
|
1725
|
-
}
|
|
1883
|
+
};
|
|
1884
|
+
this.checkReadOptions(read);
|
|
1885
|
+
const { row: parent, hydrate } = await this.readFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], read);
|
|
1726
1886
|
if (!parent)
|
|
1727
1887
|
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1728
1888
|
await this.emitEvent("before", operation, false, { ids: [id], input: relatedIds, transaction: tx }, options);
|
|
@@ -1750,11 +1910,8 @@ class SqlBaseCrudService {
|
|
|
1750
1910
|
: (0, drizzle_orm_1.eq)(local, parentValue));
|
|
1751
1911
|
}
|
|
1752
1912
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
relations: Array.from(new Set([name, ...(options?.relations ?? [])])),
|
|
1756
|
-
skipAllowlists: true,
|
|
1757
|
-
}));
|
|
1913
|
+
await hydrate();
|
|
1914
|
+
const row = parent;
|
|
1758
1915
|
await this.emitEvent("after", operation, false, { ids: [id], input: relatedIds, rows: [row], transaction: tx }, options);
|
|
1759
1916
|
return row;
|
|
1760
1917
|
}, options?.transaction);
|
|
@@ -1796,27 +1953,31 @@ class SqlBaseCrudService {
|
|
|
1796
1953
|
return entityData;
|
|
1797
1954
|
}
|
|
1798
1955
|
async update(id, data, options) {
|
|
1956
|
+
options = this.withScopeMemo(options);
|
|
1799
1957
|
const { transaction, hooks = {} } = options || {};
|
|
1800
1958
|
this.checkReadOptions(options);
|
|
1801
1959
|
const expected = this.expectedVersionOf(data, options);
|
|
1802
1960
|
const opts = { ...options, expectedVersion: expected };
|
|
1803
|
-
const
|
|
1804
|
-
if (
|
|
1805
|
-
|
|
1806
|
-
await this.
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1961
|
+
const guarded = !this.skipsGuard(options, "validateUpdate", "mapUpdateDtoToEntity", ...(hooks.skipBefore ? [] : ["beforeUpdate"]));
|
|
1962
|
+
if (guarded)
|
|
1963
|
+
await this.guardRow(id, opts);
|
|
1964
|
+
const entityData = await this.guardOnError(guarded, id, opts, async () => {
|
|
1965
|
+
await this.validateUpdate(id, data);
|
|
1966
|
+
const processedData = hooks.skipBefore
|
|
1967
|
+
? data
|
|
1968
|
+
: await this.beforeUpdate(id, data);
|
|
1969
|
+
return this.prepareUpdate(processedData, opts);
|
|
1970
|
+
});
|
|
1811
1971
|
if (this.config.timestamps?.updatedAt) {
|
|
1812
1972
|
entityData[this.config.timestamps.updatedAt] = new Date();
|
|
1813
1973
|
}
|
|
1814
1974
|
const db = this.dbFor(transaction);
|
|
1815
1975
|
await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
|
|
1816
1976
|
try {
|
|
1817
|
-
await this.assertKeysInScope(db, entityData, opts);
|
|
1818
|
-
const [updatedEntity] = await this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts);
|
|
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));
|
|
1819
1979
|
if (!updatedEntity) {
|
|
1980
|
+
await this.guardRow(id, opts);
|
|
1820
1981
|
if (expected !== undefined) {
|
|
1821
1982
|
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1822
1983
|
}
|
|
@@ -1832,11 +1993,12 @@ class SqlBaseCrudService {
|
|
|
1832
1993
|
throw this.toClientError(error, "write");
|
|
1833
1994
|
}
|
|
1834
1995
|
}
|
|
1835
|
-
async updateWhereIn(db, set, ids, options) {
|
|
1996
|
+
async updateWhereIn(db, set, ids, options, extra) {
|
|
1997
|
+
const where = this.writeWhere(ids, options);
|
|
1836
1998
|
const query = db
|
|
1837
1999
|
.update(this.config.table)
|
|
1838
2000
|
.set(set)
|
|
1839
|
-
.where(
|
|
2001
|
+
.where(extra ? (0, drizzle_orm_1.and)(where, extra) : where);
|
|
1840
2002
|
if (this.config.sql?.useReturning) {
|
|
1841
2003
|
return this.inIdOrder(await query.returning(), ids);
|
|
1842
2004
|
}
|
|
@@ -1862,39 +2024,65 @@ class SqlBaseCrudService {
|
|
|
1862
2024
|
.map((id) => byKey.get(this.keyOf(id)))
|
|
1863
2025
|
.filter((row) => !!row);
|
|
1864
2026
|
}
|
|
1865
|
-
async
|
|
1866
|
-
if (!this.config.softDelete?.enabled) {
|
|
1867
|
-
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1868
|
-
}
|
|
2027
|
+
async guardedWrite(id, options, step) {
|
|
1869
2028
|
const { transaction, hooks = {} } = options || {};
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
onlyDeleted: false,
|
|
1874
|
-
});
|
|
1875
|
-
if (!existing)
|
|
1876
|
-
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
2029
|
+
if (!this.skipsGuard(options, ...(hooks.skipBefore ? [] : [step.hook]))) {
|
|
2030
|
+
await this.guardRow(id, step.state);
|
|
2031
|
+
}
|
|
1877
2032
|
if (!hooks.skipBefore)
|
|
1878
|
-
await
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
.
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2033
|
+
await step.before();
|
|
2034
|
+
const event = { ids: [id], transaction };
|
|
2035
|
+
await this.emitEvent("before", step.operation, false, event, options);
|
|
2036
|
+
const finish = async () => {
|
|
2037
|
+
const result = await step.write(this.dbFor(transaction));
|
|
2038
|
+
if (result === undefined) {
|
|
2039
|
+
await this.guardRow(id, step.state);
|
|
2040
|
+
return step.miss();
|
|
2041
|
+
}
|
|
2042
|
+
if (!hooks.skipAfter)
|
|
2043
|
+
await step.after(result);
|
|
2044
|
+
const rows = step.rows?.(result);
|
|
2045
|
+
await this.emitEvent("after", step.operation, false, { ...event, rows }, options);
|
|
2046
|
+
return result;
|
|
2047
|
+
};
|
|
2048
|
+
if (!step.errors)
|
|
2049
|
+
return finish();
|
|
2050
|
+
try {
|
|
2051
|
+
return await finish();
|
|
2052
|
+
}
|
|
2053
|
+
catch (error) {
|
|
2054
|
+
throw this.toClientError(error, step.errors);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
staleOrFalse(id, options) {
|
|
2058
|
+
if (options?.expectedVersion != null) {
|
|
1890
2059
|
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1891
2060
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
async softDelete(id, options) {
|
|
2064
|
+
options = this.withScopeMemo(options);
|
|
2065
|
+
if (!this.config.softDelete?.enabled) {
|
|
2066
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1896
2067
|
}
|
|
1897
|
-
|
|
2068
|
+
const live = { ...options, withDeleted: false, onlyDeleted: false };
|
|
2069
|
+
return this.guardedWrite(id, options, {
|
|
2070
|
+
operation: "softDelete",
|
|
2071
|
+
state: live,
|
|
2072
|
+
hook: "beforeSoftDelete",
|
|
2073
|
+
before: () => this.beforeSoftDelete(id),
|
|
2074
|
+
write: async (db) => {
|
|
2075
|
+
let query = db
|
|
2076
|
+
.update(this.config.table)
|
|
2077
|
+
.set(this.softDeleteSet(new Date()))
|
|
2078
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(live)));
|
|
2079
|
+
if (this.config.sql?.useReturning)
|
|
2080
|
+
query = query.returning();
|
|
2081
|
+
return this.wasAffected(await this.runRead(query)) || undefined;
|
|
2082
|
+
},
|
|
2083
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2084
|
+
after: () => this.afterSoftDelete(id),
|
|
2085
|
+
});
|
|
1898
2086
|
}
|
|
1899
2087
|
softDeleteSet(deletedAt) {
|
|
1900
2088
|
return this.withVersionBump({
|
|
@@ -1905,62 +2093,43 @@ class SqlBaseCrudService {
|
|
|
1905
2093
|
});
|
|
1906
2094
|
}
|
|
1907
2095
|
async restore(id, options) {
|
|
2096
|
+
options = this.withScopeMemo(options);
|
|
1908
2097
|
if (!this.config.softDelete?.enabled) {
|
|
1909
2098
|
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1910
2099
|
}
|
|
1911
|
-
const
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2100
|
+
const trashed = { ...options, withDeleted: false, onlyDeleted: true };
|
|
2101
|
+
const restored = await this.guardedWrite(id, options, {
|
|
2102
|
+
operation: "restore",
|
|
2103
|
+
state: trashed,
|
|
2104
|
+
hook: "beforeRestore",
|
|
2105
|
+
before: () => this.beforeRestore(id),
|
|
2106
|
+
write: async (db) => (await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options, this.softDeleteScope(trashed))))[0],
|
|
2107
|
+
miss: () => {
|
|
2108
|
+
if (options?.expectedVersion != null && this.config.versionColumn) {
|
|
2109
|
+
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
2110
|
+
}
|
|
2111
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
2112
|
+
},
|
|
2113
|
+
after: (entity) => this.afterRestore(entity),
|
|
2114
|
+
rows: (entity) => [entity],
|
|
1916
2115
|
});
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
if (!hooks.skipBefore)
|
|
1920
|
-
await this.beforeRestore(id);
|
|
1921
|
-
await this.emitEvent("before", "restore", false, { ids: [id], transaction }, options);
|
|
1922
|
-
const db = this.dbFor(transaction);
|
|
1923
|
-
const [restoredEntity] = await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options));
|
|
1924
|
-
if (!restoredEntity) {
|
|
1925
|
-
if (options?.expectedVersion != null && this.config.versionColumn) {
|
|
1926
|
-
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1927
|
-
}
|
|
1928
|
-
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1929
|
-
}
|
|
1930
|
-
if (!hooks.skipAfter)
|
|
1931
|
-
await this.afterRestore(restoredEntity);
|
|
1932
|
-
await this.emitEvent("after", "restore", false, { ids: [id], rows: [restoredEntity], transaction }, options);
|
|
1933
|
-
await this.loadWriteRelations(db, [restoredEntity], options);
|
|
1934
|
-
return restoredEntity;
|
|
2116
|
+
await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
|
|
2117
|
+
return restored;
|
|
1935
2118
|
}
|
|
1936
2119
|
async delete(id, options) {
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
const success = this.wasAffected(result);
|
|
1951
|
-
if (!success && options?.expectedVersion != null) {
|
|
1952
|
-
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1953
|
-
}
|
|
1954
|
-
if (success && !hooks.skipAfter)
|
|
1955
|
-
await this.afterDelete(id);
|
|
1956
|
-
if (success) {
|
|
1957
|
-
await this.emitEvent("after", "delete", false, { ids: [id], transaction }, options);
|
|
1958
|
-
}
|
|
1959
|
-
return success;
|
|
1960
|
-
}
|
|
1961
|
-
catch (error) {
|
|
1962
|
-
throw this.toClientError(error, "delete");
|
|
1963
|
-
}
|
|
2120
|
+
options = this.withScopeMemo(options);
|
|
2121
|
+
return this.guardedWrite(id, options, {
|
|
2122
|
+
operation: "delete",
|
|
2123
|
+
state: options,
|
|
2124
|
+
hook: "beforeDelete",
|
|
2125
|
+
before: () => this.beforeDelete(id),
|
|
2126
|
+
write: async (db) => this.wasAffected(await db
|
|
2127
|
+
.delete(this.config.table)
|
|
2128
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(options)))) || undefined,
|
|
2129
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2130
|
+
after: () => this.afterDelete(id),
|
|
2131
|
+
errors: "delete",
|
|
2132
|
+
});
|
|
1964
2133
|
}
|
|
1965
2134
|
async forceDelete(id, options) {
|
|
1966
2135
|
return this.delete(id, {
|
|
@@ -1970,6 +2139,7 @@ class SqlBaseCrudService {
|
|
|
1970
2139
|
});
|
|
1971
2140
|
}
|
|
1972
2141
|
async upsert(data, upsert = {}, options) {
|
|
2142
|
+
options = this.withScopeMemo(options);
|
|
1973
2143
|
this.checkReadOptions(options);
|
|
1974
2144
|
const entityData = await this.prepareCreate(data, options);
|
|
1975
2145
|
const db = this.dbFor(options?.transaction);
|
|
@@ -1995,43 +2165,40 @@ class SqlBaseCrudService {
|
|
|
1995
2165
|
return row;
|
|
1996
2166
|
}
|
|
1997
2167
|
async massUpsert(data, upsert = {}, options) {
|
|
1998
|
-
this.
|
|
2168
|
+
options = this.withScopeMemo(options);
|
|
1999
2169
|
const message = "Mass upsert errors";
|
|
2000
|
-
return this.
|
|
2001
|
-
|
|
2002
|
-
|
|
2170
|
+
return this.massWrite({
|
|
2171
|
+
operation: "upsert",
|
|
2172
|
+
message,
|
|
2173
|
+
options,
|
|
2174
|
+
input: data,
|
|
2175
|
+
returnsRows: true,
|
|
2176
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
2003
2177
|
index,
|
|
2004
2178
|
run: () => this.prepareCreate(item, opts),
|
|
2005
|
-
})),
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
const seen = new Set(written.map((row) => keys.map((k) => this.valueKey(row[k])).join("|")));
|
|
2017
|
-
throw new crud_exceptions_1.BulkOperationException(message, rows.flatMap((row, index) => seen.has(keys.map((k) => this.valueKey(row[k])).join("|"))
|
|
2018
|
-
? []
|
|
2019
|
-
: [
|
|
2020
|
-
{
|
|
2021
|
-
index,
|
|
2022
|
-
error: new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(row, upsert)),
|
|
2023
|
-
},
|
|
2024
|
-
]));
|
|
2025
|
-
}
|
|
2026
|
-
await this.emitEvent("after", "upsert", true, {
|
|
2027
|
-
ids: this.idsOf(written),
|
|
2028
|
-
input: data,
|
|
2029
|
-
rows: written,
|
|
2030
|
-
transaction: tx,
|
|
2031
|
-
}, options);
|
|
2032
|
-
await this.loadWriteRelations(tx, written, options);
|
|
2179
|
+
})),
|
|
2180
|
+
keys: true,
|
|
2181
|
+
write: (tx, rows, opts) => this.upsertSet(tx, rows, upsert, opts, message),
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
async upsertSet(tx, rows, upsert, opts, message) {
|
|
2185
|
+
const written = await this.runSet(tx, message, (sp) => this.upsertRows(sp, rows, upsert, opts), rows.map((row, index) => ({
|
|
2186
|
+
index,
|
|
2187
|
+
run: (sp) => this.upsertRows(sp, [row], upsert, opts),
|
|
2188
|
+
})), "write");
|
|
2189
|
+
if (written.length >= rows.length)
|
|
2033
2190
|
return written;
|
|
2034
|
-
|
|
2191
|
+
const keys = this.conflictKeys(upsert);
|
|
2192
|
+
const keyOf = (row) => keys.map((k) => this.valueKey(row[k])).join("|");
|
|
2193
|
+
const seen = new Set(written.map(keyOf));
|
|
2194
|
+
throw new crud_exceptions_1.BulkOperationException(message, rows.flatMap((row, index) => seen.has(keyOf(row))
|
|
2195
|
+
? []
|
|
2196
|
+
: [
|
|
2197
|
+
{
|
|
2198
|
+
index,
|
|
2199
|
+
error: new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(row, upsert)),
|
|
2200
|
+
},
|
|
2201
|
+
]));
|
|
2035
2202
|
}
|
|
2036
2203
|
conflictKeys(upsert) {
|
|
2037
2204
|
return upsert.conflictTarget === undefined
|
|
@@ -2126,38 +2293,78 @@ class SqlBaseCrudService {
|
|
|
2126
2293
|
? (0, drizzle_orm_1.sql) `values(${drizzle_orm_1.sql.identifier(column.name)})`
|
|
2127
2294
|
: (0, drizzle_orm_1.sql) `excluded.${drizzle_orm_1.sql.identifier(column.name)}`;
|
|
2128
2295
|
}
|
|
2296
|
+
async massWrite(spec) {
|
|
2297
|
+
const { options, message, ids } = spec;
|
|
2298
|
+
if (spec.returnsRows)
|
|
2299
|
+
this.checkReadOptions(options);
|
|
2300
|
+
const body = async (tx, load) => {
|
|
2301
|
+
const opts = { ...options, transaction: tx, ...spec.state };
|
|
2302
|
+
const unique = ids ? this.uniqueIds(ids) : [];
|
|
2303
|
+
if (ids) {
|
|
2304
|
+
if (unique.length === 0)
|
|
2305
|
+
return [];
|
|
2306
|
+
await this.assertAllExist(tx, unique, opts, message);
|
|
2307
|
+
}
|
|
2308
|
+
const items = spec.prepare?.(opts, unique);
|
|
2309
|
+
const prepared = items ? await this.eachItem(items, message) : [];
|
|
2310
|
+
if (!ids && prepared.length === 0)
|
|
2311
|
+
return [];
|
|
2312
|
+
const event = {
|
|
2313
|
+
ids: ids ? unique : undefined,
|
|
2314
|
+
input: spec.input,
|
|
2315
|
+
transaction: tx,
|
|
2316
|
+
};
|
|
2317
|
+
await this.emitEvent("before", spec.operation, true, event, options);
|
|
2318
|
+
if (spec.keys) {
|
|
2319
|
+
await this.assertBatchKeysInScope(tx, prepared, ids
|
|
2320
|
+
? unique.map((id) => ({ id }))
|
|
2321
|
+
: prepared.map((_, index) => ({ index })), message, opts);
|
|
2322
|
+
}
|
|
2323
|
+
const written = await spec.write(tx, prepared, opts, unique);
|
|
2324
|
+
if (ids)
|
|
2325
|
+
this.assertAllWritten(unique, written, message, options);
|
|
2326
|
+
if (spec.after && !options?.hooks?.skipAfter) {
|
|
2327
|
+
await spec.after(written, unique);
|
|
2328
|
+
}
|
|
2329
|
+
await this.emitEvent("after", spec.operation, true, {
|
|
2330
|
+
...event,
|
|
2331
|
+
ids: ids ? unique : this.idsOf(written),
|
|
2332
|
+
rows: spec.returnsRows ? written : undefined,
|
|
2333
|
+
}, options);
|
|
2334
|
+
if (spec.returnsRows)
|
|
2335
|
+
await load(tx, written);
|
|
2336
|
+
return ids ? this.inIdOrder(written, ids) : written;
|
|
2337
|
+
};
|
|
2338
|
+
return spec.returnsRows
|
|
2339
|
+
? this.writeThenLoad(options, body)
|
|
2340
|
+
: this.executeSqlTransaction((tx) => body(tx, async () => { }), options?.transaction);
|
|
2341
|
+
}
|
|
2129
2342
|
async massCreate(data, options) {
|
|
2343
|
+
options = this.withScopeMemo(options);
|
|
2130
2344
|
if (options?.bulkMode === "perItem") {
|
|
2131
|
-
return this.
|
|
2345
|
+
return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
|
|
2132
2346
|
}
|
|
2133
|
-
|
|
2134
|
-
return this.
|
|
2135
|
-
|
|
2136
|
-
|
|
2347
|
+
const message = "Mass create errors";
|
|
2348
|
+
return this.massWrite({
|
|
2349
|
+
operation: "create",
|
|
2350
|
+
message,
|
|
2351
|
+
options,
|
|
2352
|
+
input: data,
|
|
2353
|
+
returnsRows: true,
|
|
2354
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
2137
2355
|
index,
|
|
2138
2356
|
run: () => this.prepareCreate(item, opts),
|
|
2139
|
-
})),
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
await this.emitEvent("before", "create", true, { input: data, transaction: tx }, options);
|
|
2143
|
-
await this.assertBatchKeysInScope(tx, rows, rows.map((_, index) => ({ index })), "Mass create errors", opts);
|
|
2144
|
-
const created = await this.runSet(tx, "Mass create errors", (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
|
|
2357
|
+
})),
|
|
2358
|
+
keys: true,
|
|
2359
|
+
write: (tx, rows, opts) => this.runSet(tx, message, (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
|
|
2145
2360
|
index,
|
|
2146
2361
|
run: (sp) => this.insertOne(sp, row, opts),
|
|
2147
|
-
})), "write")
|
|
2148
|
-
|
|
2362
|
+
})), "write"),
|
|
2363
|
+
after: async (created) => {
|
|
2149
2364
|
for (const entity of created)
|
|
2150
2365
|
await this.afterCreate(entity);
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
ids: this.idsOf(created),
|
|
2154
|
-
input: data,
|
|
2155
|
-
rows: created,
|
|
2156
|
-
transaction: tx,
|
|
2157
|
-
}, options);
|
|
2158
|
-
await this.loadWriteRelations(tx, created, options);
|
|
2159
|
-
return created;
|
|
2160
|
-
}, options?.transaction);
|
|
2366
|
+
},
|
|
2367
|
+
});
|
|
2161
2368
|
}
|
|
2162
2369
|
async insertMany(db, rows, options) {
|
|
2163
2370
|
if (this.config.sql?.useReturning) {
|
|
@@ -2168,9 +2375,28 @@ class SqlBaseCrudService {
|
|
|
2168
2375
|
await db.insert(this.config.table).values(rows);
|
|
2169
2376
|
return this.fetchByIds(db, rows.map((row) => row[pk]), options);
|
|
2170
2377
|
}
|
|
2171
|
-
const
|
|
2172
|
-
for (const row of rows)
|
|
2173
|
-
|
|
2378
|
+
const ids = [];
|
|
2379
|
+
for (const row of rows) {
|
|
2380
|
+
const result = await db.insert(this.config.table).values(row);
|
|
2381
|
+
ids.push(row[pk] ?? result?.[0]?.insertId);
|
|
2382
|
+
}
|
|
2383
|
+
return this.readBackCreated(db, ids, options);
|
|
2384
|
+
}
|
|
2385
|
+
async readBackCreated(db, ids, options) {
|
|
2386
|
+
const pk = this.config.primaryKey;
|
|
2387
|
+
const base = this.parseSelect(options?.select).base;
|
|
2388
|
+
const fields = this.buildSelectFields(base.length > 0 ? Array.from(new Set([...base, pk])) : base);
|
|
2389
|
+
const column = this.config.table[pk];
|
|
2390
|
+
const rows = await (fields ? db.select(fields) : db.select())
|
|
2391
|
+
.from(this.config.table)
|
|
2392
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(column, ids), ...this.rowScope(options)));
|
|
2393
|
+
const created = this.inIdOrder(rows, ids);
|
|
2394
|
+
if (created.length < ids.length)
|
|
2395
|
+
throw new Error("Failed to create entity");
|
|
2396
|
+
if (fields && !base.includes(pk)) {
|
|
2397
|
+
for (const row of created)
|
|
2398
|
+
delete row[pk];
|
|
2399
|
+
}
|
|
2174
2400
|
return created;
|
|
2175
2401
|
}
|
|
2176
2402
|
assertAllWritten(ids, written, message, options) {
|
|
@@ -2187,59 +2413,62 @@ class SqlBaseCrudService {
|
|
|
2187
2413
|
})));
|
|
2188
2414
|
}
|
|
2189
2415
|
async massUpdate(ids, data, options) {
|
|
2416
|
+
options = this.withScopeMemo(options);
|
|
2190
2417
|
if (options?.bulkMode === "perItem") {
|
|
2191
|
-
return this.
|
|
2418
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass update errors", options, ({ id }, opts) => this.update(id, data, opts));
|
|
2192
2419
|
}
|
|
2193
|
-
this.checkReadOptions(options);
|
|
2194
2420
|
const message = "Mass update errors";
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2421
|
+
const skipBefore = options?.hooks?.skipBefore;
|
|
2422
|
+
return this.massWrite({
|
|
2423
|
+
operation: "update",
|
|
2424
|
+
message,
|
|
2425
|
+
options,
|
|
2426
|
+
ids,
|
|
2427
|
+
input: data,
|
|
2428
|
+
returnsRows: true,
|
|
2429
|
+
prepare: (opts, unique) => unique.map((id) => ({
|
|
2203
2430
|
id,
|
|
2204
2431
|
run: async () => {
|
|
2205
2432
|
await this.validateUpdate(id, data);
|
|
2206
|
-
const processed =
|
|
2433
|
+
const processed = skipBefore
|
|
2207
2434
|
? data
|
|
2208
2435
|
: await this.beforeUpdate(id, data);
|
|
2209
2436
|
return this.prepareUpdate(processed, opts);
|
|
2210
2437
|
},
|
|
2211
|
-
})),
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
const stamp = (set) => this.withVersionBump(this.config.timestamps?.updatedAt
|
|
2216
|
-
? { ...set, [this.config.timestamps.updatedAt]: now }
|
|
2217
|
-
: set);
|
|
2218
|
-
let updated;
|
|
2219
|
-
const first = this.fingerprint(sets[0]);
|
|
2220
|
-
if (sets.every((set) => this.fingerprint(set) === first)) {
|
|
2221
|
-
const set = stamp(sets[0]);
|
|
2222
|
-
updated = await this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts), unique.map((id) => ({
|
|
2223
|
-
id,
|
|
2224
|
-
run: (sp) => this.updateWhereIn(sp, set, [id], opts),
|
|
2225
|
-
})), "write");
|
|
2226
|
-
}
|
|
2227
|
-
else {
|
|
2228
|
-
const rows = await this.eachItem(unique.map((id, i) => ({
|
|
2229
|
-
id,
|
|
2230
|
-
run: () => this.savepoint(tx, (sp) => this.updateWhereIn(sp, stamp(sets[i]), [id], opts)),
|
|
2231
|
-
})), message, "write");
|
|
2232
|
-
updated = rows.flat();
|
|
2233
|
-
}
|
|
2234
|
-
this.assertAllWritten(unique, updated, message, options);
|
|
2235
|
-
if (!hooks.skipAfter) {
|
|
2438
|
+
})),
|
|
2439
|
+
keys: true,
|
|
2440
|
+
write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
|
|
2441
|
+
after: async (updated) => {
|
|
2236
2442
|
for (const entity of updated)
|
|
2237
2443
|
await this.afterUpdate(entity);
|
|
2444
|
+
},
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
async updateSets(tx, sets, unique, opts, message) {
|
|
2448
|
+
const now = new Date();
|
|
2449
|
+
const stamp = (set) => this.withVersionBump(this.config.timestamps?.updatedAt
|
|
2450
|
+
? { ...set, [this.config.timestamps.updatedAt]: now }
|
|
2451
|
+
: set);
|
|
2452
|
+
const groups = new Map();
|
|
2453
|
+
unique.forEach((id, i) => {
|
|
2454
|
+
const key = this.fingerprint(sets[i]);
|
|
2455
|
+
const group = groups.get(key);
|
|
2456
|
+
if (group)
|
|
2457
|
+
group.ids.push(id);
|
|
2458
|
+
else
|
|
2459
|
+
groups.set(key, { set: stamp(sets[i]), ids: [id] });
|
|
2460
|
+
});
|
|
2461
|
+
const written = await this.runSet(tx, message, async (sp) => {
|
|
2462
|
+
const rows = [];
|
|
2463
|
+
for (const group of groups.values()) {
|
|
2464
|
+
rows.push(...(await this.updateWhereIn(sp, group.set, group.ids, opts)));
|
|
2238
2465
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2466
|
+
return rows;
|
|
2467
|
+
}, unique.map((id, i) => ({
|
|
2468
|
+
id,
|
|
2469
|
+
run: (sp) => this.updateWhereIn(sp, stamp(sets[i]), [id], opts),
|
|
2470
|
+
})), "write");
|
|
2471
|
+
return groups.size > 1 ? this.inIdOrder(written, unique) : written;
|
|
2243
2472
|
}
|
|
2244
2473
|
async runKeyed(statement, ids) {
|
|
2245
2474
|
if (this.config.sql?.useReturning) {
|
|
@@ -2253,115 +2482,96 @@ class SqlBaseCrudService {
|
|
|
2253
2482
|
: [];
|
|
2254
2483
|
}
|
|
2255
2484
|
async massSoftDelete(ids, options) {
|
|
2485
|
+
options = this.withScopeMemo(options);
|
|
2256
2486
|
if (!this.config.softDelete?.enabled)
|
|
2257
2487
|
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2258
2488
|
if (options?.bulkMode === "perItem") {
|
|
2259
|
-
|
|
2489
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass soft delete errors", options, ({ id }, opts) => this.softDelete(id, opts));
|
|
2490
|
+
return true;
|
|
2260
2491
|
}
|
|
2261
2492
|
const message = "Mass soft delete errors";
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
await this.eachItem(unique.map((id) => ({ id, run: () => this.beforeSoftDelete(id) })), message);
|
|
2276
|
-
}
|
|
2277
|
-
await this.emitEvent("before", "softDelete", true, { ids: unique, transaction: tx }, options);
|
|
2278
|
-
const set = this.softDeleteSet(new Date());
|
|
2279
|
-
const written = await this.runSet(tx, message, (sp) => this.runKeyed(sp
|
|
2280
|
-
.update(this.config.table)
|
|
2281
|
-
.set(set)
|
|
2282
|
-
.where(this.writeWhere(unique, opts)), unique), unique.map((id) => ({
|
|
2283
|
-
id,
|
|
2284
|
-
run: (sp) => sp
|
|
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
|
|
2285
2506
|
.update(this.config.table)
|
|
2286
2507
|
.set(set)
|
|
2287
|
-
.where(this.writeWhere(
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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) => {
|
|
2291
2512
|
for (const id of unique)
|
|
2292
2513
|
await this.afterSoftDelete(id);
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
}, options?.transaction);
|
|
2514
|
+
},
|
|
2515
|
+
});
|
|
2516
|
+
return true;
|
|
2297
2517
|
}
|
|
2298
2518
|
async massRestore(ids, options) {
|
|
2519
|
+
options = this.withScopeMemo(options);
|
|
2299
2520
|
if (!this.config.softDelete?.enabled)
|
|
2300
2521
|
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2301
2522
|
if (options?.bulkMode === "perItem") {
|
|
2302
|
-
return this.
|
|
2523
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass restore errors", options, ({ id }, opts) => this.restore(id, opts));
|
|
2303
2524
|
}
|
|
2304
|
-
this.checkReadOptions(options);
|
|
2305
2525
|
const message = "Mass restore errors";
|
|
2306
|
-
return this.
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
id,
|
|
2325
|
-
run: (sp) => this.updateWhereIn(sp, set, [id], opts),
|
|
2326
|
-
})), "write");
|
|
2327
|
-
this.assertAllWritten(unique, restored, message, options);
|
|
2328
|
-
if (!hooks.skipAfter) {
|
|
2526
|
+
return this.massWrite({
|
|
2527
|
+
operation: "restore",
|
|
2528
|
+
message,
|
|
2529
|
+
options,
|
|
2530
|
+
ids,
|
|
2531
|
+
state: { withDeleted: true, onlyDeleted: false },
|
|
2532
|
+
returnsRows: true,
|
|
2533
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2534
|
+
? undefined
|
|
2535
|
+
: unique.map((id) => ({ id, run: () => this.beforeRestore(id) })),
|
|
2536
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2537
|
+
const set = this.softDeleteSet(null);
|
|
2538
|
+
return this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts), unique.map((id) => ({
|
|
2539
|
+
id,
|
|
2540
|
+
run: (sp) => this.updateWhereIn(sp, set, [id], opts),
|
|
2541
|
+
})), "write");
|
|
2542
|
+
},
|
|
2543
|
+
after: async (restored) => {
|
|
2329
2544
|
for (const entity of restored)
|
|
2330
2545
|
await this.afterRestore(entity);
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
await this.loadWriteRelations(tx, restored, options);
|
|
2334
|
-
return this.inIdOrder(restored, ids);
|
|
2335
|
-
}, options?.transaction);
|
|
2546
|
+
},
|
|
2547
|
+
});
|
|
2336
2548
|
}
|
|
2337
2549
|
async massDelete(ids, options) {
|
|
2550
|
+
options = this.withScopeMemo(options);
|
|
2338
2551
|
if (options?.bulkMode === "perItem") {
|
|
2339
|
-
|
|
2552
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass delete errors", options, ({ id }, opts) => this.delete(id, opts), "delete");
|
|
2553
|
+
return true;
|
|
2340
2554
|
}
|
|
2341
2555
|
const message = "Mass delete errors";
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
})), "delete");
|
|
2357
|
-
this.assertAllWritten(unique, written, message, options);
|
|
2358
|
-
if (!hooks.skipAfter) {
|
|
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) => {
|
|
2359
2570
|
for (const id of unique)
|
|
2360
2571
|
await this.afterDelete(id);
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
}, options?.transaction);
|
|
2572
|
+
},
|
|
2573
|
+
});
|
|
2574
|
+
return true;
|
|
2365
2575
|
}
|
|
2366
2576
|
async massForceDelete(ids, options) {
|
|
2367
2577
|
return this.massDelete(ids, {
|
|
@@ -2446,12 +2656,16 @@ class SqlBaseCrudService {
|
|
|
2446
2656
|
return item.index !== undefined ? { index: item.index } : { id: item.id };
|
|
2447
2657
|
}
|
|
2448
2658
|
async runSet(tx, message, statement, items, operation) {
|
|
2659
|
+
const owned = this.ownedTransactions.has(tx);
|
|
2449
2660
|
try {
|
|
2450
|
-
return await this.savepoint(tx, statement);
|
|
2661
|
+
return await (owned ? statement(tx) : this.savepoint(tx, statement));
|
|
2451
2662
|
}
|
|
2452
2663
|
catch (error) {
|
|
2453
2664
|
if (error instanceof BindLimitException)
|
|
2454
2665
|
throw error;
|
|
2666
|
+
if (owned) {
|
|
2667
|
+
throw new DeferredDiagnosis(() => this.settleSet(message, error, items, operation));
|
|
2668
|
+
}
|
|
2455
2669
|
const failures = await this.diagnose(tx, items, operation);
|
|
2456
2670
|
if (failures.length > 0) {
|
|
2457
2671
|
throw new crud_exceptions_1.BulkOperationException(message, failures);
|
|
@@ -2459,6 +2673,23 @@ class SqlBaseCrudService {
|
|
|
2459
2673
|
throw this.toClientError(error, operation);
|
|
2460
2674
|
}
|
|
2461
2675
|
}
|
|
2676
|
+
async settleSet(message, error, items, operation) {
|
|
2677
|
+
let failures = [];
|
|
2678
|
+
try {
|
|
2679
|
+
await this.config.db.transaction(async (fresh) => {
|
|
2680
|
+
failures = await this.diagnose(this.withBindLimit(fresh), items, operation);
|
|
2681
|
+
throw ROLLBACK;
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
catch (thrown) {
|
|
2685
|
+
if (thrown !== ROLLBACK)
|
|
2686
|
+
throw thrown;
|
|
2687
|
+
}
|
|
2688
|
+
if (failures.length > 0) {
|
|
2689
|
+
throw new crud_exceptions_1.BulkOperationException(message, failures);
|
|
2690
|
+
}
|
|
2691
|
+
throw this.toClientError(error, operation);
|
|
2692
|
+
}
|
|
2462
2693
|
async diagnose(tx, items, operation) {
|
|
2463
2694
|
const failures = [];
|
|
2464
2695
|
try {
|
|
@@ -2483,37 +2714,11 @@ class SqlBaseCrudService {
|
|
|
2483
2714
|
}
|
|
2484
2715
|
return failures;
|
|
2485
2716
|
}
|
|
2486
|
-
|
|
2487
|
-
return this.executeSqlTransaction((tx) => this.eachItem(
|
|
2488
|
-
|
|
2489
|
-
run: () => this.savepoint(tx, (sp) =>
|
|
2490
|
-
})),
|
|
2491
|
-
}
|
|
2492
|
-
async massUpdatePerItem(ids, data, options) {
|
|
2493
|
-
return this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2494
|
-
id,
|
|
2495
|
-
run: () => this.savepoint(tx, (sp) => this.update(id, data, { ...options, transaction: sp })),
|
|
2496
|
-
})), "Mass update errors"), options?.transaction);
|
|
2497
|
-
}
|
|
2498
|
-
async massSoftDeletePerItem(ids, options) {
|
|
2499
|
-
await this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2500
|
-
id,
|
|
2501
|
-
run: () => this.savepoint(tx, (sp) => this.softDelete(id, { ...options, transaction: sp })),
|
|
2502
|
-
})), "Mass soft delete errors"), options?.transaction);
|
|
2503
|
-
return true;
|
|
2504
|
-
}
|
|
2505
|
-
async massRestorePerItem(ids, options) {
|
|
2506
|
-
return this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2507
|
-
id,
|
|
2508
|
-
run: () => this.savepoint(tx, (sp) => this.restore(id, { ...options, transaction: sp })),
|
|
2509
|
-
})), "Mass restore errors"), options?.transaction);
|
|
2510
|
-
}
|
|
2511
|
-
async massDeletePerItem(ids, options) {
|
|
2512
|
-
await this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2513
|
-
id,
|
|
2514
|
-
run: () => this.savepoint(tx, (sp) => this.delete(id, { ...options, transaction: sp })),
|
|
2515
|
-
})), "Mass delete errors", "delete"), options?.transaction);
|
|
2516
|
-
return true;
|
|
2717
|
+
eachInSavepoint(entries, message, options, call, operation = "write") {
|
|
2718
|
+
return this.executeSqlTransaction((tx) => this.eachItem(entries.map((entry) => ({
|
|
2719
|
+
...entry,
|
|
2720
|
+
run: () => this.savepoint(tx, (sp) => call(entry, { ...options, transaction: sp })),
|
|
2721
|
+
})), message, operation), options?.transaction);
|
|
2517
2722
|
}
|
|
2518
2723
|
savepoint(tx, run) {
|
|
2519
2724
|
return typeof tx?.transaction === "function"
|
|
@@ -2521,20 +2726,26 @@ class SqlBaseCrudService {
|
|
|
2521
2726
|
: run(tx);
|
|
2522
2727
|
}
|
|
2523
2728
|
async exists(id, options) {
|
|
2729
|
+
options = this.withScopeMemo(options);
|
|
2730
|
+
this.checkReadOptions({ ...options, select: undefined });
|
|
2524
2731
|
const entity = await this.find(id, {
|
|
2525
2732
|
...options,
|
|
2733
|
+
relations: [],
|
|
2734
|
+
counts: undefined,
|
|
2526
2735
|
select: [this.config.primaryKey],
|
|
2527
2736
|
});
|
|
2528
2737
|
return !!entity;
|
|
2529
2738
|
}
|
|
2530
2739
|
async count(filters, options) {
|
|
2740
|
+
options = this.withScopeMemo(options);
|
|
2531
2741
|
this.checkReadOptions(options);
|
|
2532
2742
|
const db = this.dbFor(options?.transaction);
|
|
2533
2743
|
const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
|
|
2534
2744
|
const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
|
|
2535
|
-
return
|
|
2745
|
+
return this.parseCount(result);
|
|
2536
2746
|
}
|
|
2537
2747
|
async countBy(column, filters, options) {
|
|
2748
|
+
options = this.withScopeMemo(options);
|
|
2538
2749
|
this.checkReadOptions(options);
|
|
2539
2750
|
const db = this.dbFor(options?.transaction);
|
|
2540
2751
|
const spec = this.resolveGroupColumn(column, options);
|
|
@@ -2567,10 +2778,7 @@ class SqlBaseCrudService {
|
|
|
2567
2778
|
}
|
|
2568
2779
|
else {
|
|
2569
2780
|
const name = key.slice(0, dot);
|
|
2570
|
-
const
|
|
2571
|
-
const relation = Object.hasOwn(relations, name)
|
|
2572
|
-
? relations[name]
|
|
2573
|
-
: undefined;
|
|
2781
|
+
const relation = this.relationNamed(name);
|
|
2574
2782
|
const column = relation && !this.isCollection(relation) && relation.localKey
|
|
2575
2783
|
? this.columnOf(relation.table, key.slice(dot + 1))
|
|
2576
2784
|
: undefined;
|
|
@@ -2603,27 +2811,9 @@ class SqlBaseCrudService {
|
|
|
2603
2811
|
return keys.length > 0 ? Array.from(new Set([...base, ...keys])) : base;
|
|
2604
2812
|
}
|
|
2605
2813
|
async loadCounts(db, rows, counts, options) {
|
|
2606
|
-
|
|
2607
|
-
if (names.length === 0 || !Array.isArray(rows))
|
|
2814
|
+
if (this.countNames(counts).length === 0 || !Array.isArray(rows))
|
|
2608
2815
|
return;
|
|
2609
|
-
const
|
|
2610
|
-
const plan = names.map((name) => {
|
|
2611
|
-
const relation = Object.hasOwn(relations, name)
|
|
2612
|
-
? relations[name]
|
|
2613
|
-
: undefined;
|
|
2614
|
-
if (!relation) {
|
|
2615
|
-
throw new common_1.BadRequestException(`Unknown relation to count: ${name}`);
|
|
2616
|
-
}
|
|
2617
|
-
if (!this.isCollection(relation)) {
|
|
2618
|
-
throw new common_1.BadRequestException(`Cannot count "${name}": only a has-many or many-to-many relation has a count`);
|
|
2619
|
-
}
|
|
2620
|
-
const filter = !Array.isArray(counts) && (0, merge_config_1.isPlainObject)(counts?.[name])
|
|
2621
|
-
? counts?.[name]
|
|
2622
|
-
: undefined;
|
|
2623
|
-
if (filter)
|
|
2624
|
-
(0, query_parser_1.assertFilterDepth)(filter);
|
|
2625
|
-
return { name, relation, filter };
|
|
2626
|
-
});
|
|
2816
|
+
const plan = this.countPlan(counts);
|
|
2627
2817
|
const hosts = rows.filter((row) => row && typeof row === "object");
|
|
2628
2818
|
for (const host of hosts)
|
|
2629
2819
|
host._count = {};
|
|
@@ -2634,7 +2824,7 @@ class SqlBaseCrudService {
|
|
|
2634
2824
|
const ids = Array.from(new Set(hosts.map((host) => host[parentKey]).filter((v) => v != null)));
|
|
2635
2825
|
const totals = new Map();
|
|
2636
2826
|
if (ids.length > 0) {
|
|
2637
|
-
const result = await this.runRead(this.countQuery(db, relation,
|
|
2827
|
+
const result = await this.inChunks(db, ids, (chunk) => this.runRead(this.countQuery(db, relation, chunk, name, filter, options)));
|
|
2638
2828
|
for (const row of result) {
|
|
2639
2829
|
totals.set(String(row[COUNT_KEY]), Number(row.count));
|
|
2640
2830
|
}
|
|
@@ -2644,6 +2834,23 @@ class SqlBaseCrudService {
|
|
|
2644
2834
|
}
|
|
2645
2835
|
}));
|
|
2646
2836
|
}
|
|
2837
|
+
countPlan(counts) {
|
|
2838
|
+
return this.countNames(counts).map((name) => {
|
|
2839
|
+
const relation = this.relationNamed(name);
|
|
2840
|
+
if (!relation) {
|
|
2841
|
+
throw new common_1.BadRequestException(`Unknown relation to count: ${name}`);
|
|
2842
|
+
}
|
|
2843
|
+
if (!this.isCollection(relation)) {
|
|
2844
|
+
throw new common_1.BadRequestException(`Cannot count "${name}": only a has-many or many-to-many relation has a count`);
|
|
2845
|
+
}
|
|
2846
|
+
const filter = !Array.isArray(counts) && (0, merge_config_1.isPlainObject)(counts?.[name])
|
|
2847
|
+
? counts?.[name]
|
|
2848
|
+
: undefined;
|
|
2849
|
+
if (filter)
|
|
2850
|
+
(0, query_parser_1.assertFilterDepth)(filter);
|
|
2851
|
+
return { name, relation, filter };
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2647
2854
|
countQuery(db, relation, ids, name, filter, options) {
|
|
2648
2855
|
const target = relation.table;
|
|
2649
2856
|
const conditions = [];
|
|
@@ -2660,12 +2867,7 @@ class SqlBaseCrudService {
|
|
|
2660
2867
|
const total = (0, drizzle_orm_1.sql) `count(*)`;
|
|
2661
2868
|
if (this.relationType(relation) === "manyToMany") {
|
|
2662
2869
|
const joinLocal = relation.through[relation.throughLocalKey];
|
|
2663
|
-
|
|
2664
|
-
const referenced = target[relation.references ?? "id"];
|
|
2665
|
-
return db
|
|
2666
|
-
.select({ [COUNT_KEY]: joinLocal, count: total })
|
|
2667
|
-
.from(relation.through)
|
|
2668
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(joinForeign, referenced))
|
|
2870
|
+
return this.selectThrough(db, { [COUNT_KEY]: joinLocal, count: total }, relation)
|
|
2669
2871
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(joinLocal, ids), ...conditions))
|
|
2670
2872
|
.groupBy(joinLocal);
|
|
2671
2873
|
}
|
|
@@ -2718,11 +2920,7 @@ class SqlBaseCrudService {
|
|
|
2718
2920
|
const joinForeign = relation.through?.[relation.throughForeignKey ?? ""];
|
|
2719
2921
|
if (!referenced || !joinForeign)
|
|
2720
2922
|
return undefined;
|
|
2721
|
-
subquery = db
|
|
2722
|
-
.select({ value: key })
|
|
2723
|
-
.from(relation.through)
|
|
2724
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(joinForeign, referenced))
|
|
2725
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
2923
|
+
subquery = this.selectThrough(db, { value: key }, relation).where((0, drizzle_orm_1.and)(...conditions));
|
|
2726
2924
|
}
|
|
2727
2925
|
else {
|
|
2728
2926
|
subquery = db
|
|
@@ -2750,11 +2948,7 @@ class SqlBaseCrudService {
|
|
|
2750
2948
|
let subquery;
|
|
2751
2949
|
if (this.relationType(relation) === "manyToMany") {
|
|
2752
2950
|
const through = subqueryAlias(relation.through);
|
|
2753
|
-
subquery = db
|
|
2754
|
-
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
2755
|
-
.from(through)
|
|
2756
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(through[relation.throughForeignKey], target[relation.references ?? "id"]))
|
|
2757
|
-
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(through[relation.throughLocalKey], host.table[parentKey]), ...conditions));
|
|
2951
|
+
subquery = this.selectThrough(db, { one: (0, drizzle_orm_1.sql) `1` }, relation, target, through).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(through[relation.throughLocalKey], host.table[parentKey]), ...conditions));
|
|
2758
2952
|
}
|
|
2759
2953
|
else {
|
|
2760
2954
|
subquery = db
|
|
@@ -3069,8 +3263,21 @@ class SqlBaseCrudService {
|
|
|
3069
3263
|
const run = (tx) => operation(this.withBindLimit(tx));
|
|
3070
3264
|
if (existingTransaction)
|
|
3071
3265
|
return run(existingTransaction);
|
|
3072
|
-
|
|
3266
|
+
try {
|
|
3267
|
+
return await this.config.db.transaction((tx) => {
|
|
3268
|
+
const view = this.withBindLimit(tx);
|
|
3269
|
+
if (view && typeof view === "object")
|
|
3270
|
+
this.ownedTransactions.add(view);
|
|
3271
|
+
return operation(view);
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
catch (error) {
|
|
3275
|
+
if (error instanceof DeferredDiagnosis)
|
|
3276
|
+
return error.settle();
|
|
3277
|
+
throw error;
|
|
3278
|
+
}
|
|
3073
3279
|
}
|
|
3280
|
+
ownedTransactions = new WeakSet();
|
|
3074
3281
|
assertTransactions(db) {
|
|
3075
3282
|
if (this.config.dialect !== "sqlite")
|
|
3076
3283
|
return;
|
|
@@ -3090,6 +3297,7 @@ class SqlBaseCrudService {
|
|
|
3090
3297
|
}
|
|
3091
3298
|
}
|
|
3092
3299
|
async fullTextSearch(searchTerm, searchColumns, pagination, options) {
|
|
3300
|
+
options = this.withScopeMemo(options);
|
|
3093
3301
|
if (this.config.dialect !== "postgresql") {
|
|
3094
3302
|
throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
|
|
3095
3303
|
}
|
|
@@ -3128,12 +3336,9 @@ class SqlBaseCrudService {
|
|
|
3128
3336
|
.offset(offset);
|
|
3129
3337
|
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
3130
3338
|
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
3131
|
-
const total =
|
|
3339
|
+
const total = this.parseCount(totalResult);
|
|
3132
3340
|
const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
|
|
3133
|
-
await
|
|
3134
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, readOptions),
|
|
3135
|
-
this.loadCounts(plan.db, rows, readOptions.counts, readOptions),
|
|
3136
|
-
]);
|
|
3341
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
|
|
3137
3342
|
return { data: rows, total, page, limit };
|
|
3138
3343
|
}
|
|
3139
3344
|
}
|