@avytrix/nestjs-drizzle-crud 3.7.2 → 3.8.1
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,15 @@ 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");
|
|
14
|
+
let fingerprintTag = 0;
|
|
13
15
|
const ROLLBACK = Symbol("rollback");
|
|
16
|
+
class DeferredDiagnosis {
|
|
17
|
+
settle;
|
|
18
|
+
constructor(settle) {
|
|
19
|
+
this.settle = settle;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
14
22
|
const BIND_LIMITS = {
|
|
15
23
|
postgresql: { limit: 65_535, name: "PostgreSQL" },
|
|
16
24
|
mysql: { limit: 65_535, name: "MySQL" },
|
|
@@ -185,9 +193,27 @@ class SqlBaseCrudService {
|
|
|
185
193
|
const scope = this.config.scope;
|
|
186
194
|
if (!scope || options?.skipScope)
|
|
187
195
|
return null;
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
196
|
+
const context = options?.context ?? crud_context_1.CrudContext.current();
|
|
197
|
+
const memo = options?.[SCOPE_MEMO];
|
|
198
|
+
const mine = memo?.get(this);
|
|
199
|
+
if (mine?.has(context))
|
|
200
|
+
return mine.get(context);
|
|
201
|
+
const values = typeof scope === "function" ? scope(context ?? {}) : scope;
|
|
202
|
+
const checked = this.checkScope(values);
|
|
203
|
+
if (mine)
|
|
204
|
+
mine.set(context, checked);
|
|
205
|
+
else
|
|
206
|
+
memo?.set(this, new Map([[context, checked]]));
|
|
207
|
+
return checked;
|
|
208
|
+
}
|
|
209
|
+
withScopeMemo(options) {
|
|
210
|
+
if (typeof this.config.scope !== "function" ||
|
|
211
|
+
options?.[SCOPE_MEMO]) {
|
|
212
|
+
return options;
|
|
213
|
+
}
|
|
214
|
+
return { ...options, [SCOPE_MEMO]: new Map() };
|
|
215
|
+
}
|
|
216
|
+
checkScope(values) {
|
|
191
217
|
if (values === null)
|
|
192
218
|
return null;
|
|
193
219
|
if (!(0, merge_config_1.isPlainObject)(values)) {
|
|
@@ -422,6 +448,10 @@ class SqlBaseCrudService {
|
|
|
422
448
|
return relation.type;
|
|
423
449
|
return "belongsTo";
|
|
424
450
|
}
|
|
451
|
+
relationNamed(name) {
|
|
452
|
+
const relations = this.config.relations || {};
|
|
453
|
+
return Object.hasOwn(relations, name) ? relations[name] : undefined;
|
|
454
|
+
}
|
|
425
455
|
isCollection(relation) {
|
|
426
456
|
return this.relationType(relation) !== "belongsTo";
|
|
427
457
|
}
|
|
@@ -612,23 +642,19 @@ class SqlBaseCrudService {
|
|
|
612
642
|
const referenced = target[referencedName];
|
|
613
643
|
if (!referenced)
|
|
614
644
|
return;
|
|
615
|
-
const ids =
|
|
616
|
-
if (ids.length === 0)
|
|
617
|
-
for (const host of hosts)
|
|
618
|
-
host[node.name] = null;
|
|
645
|
+
const ids = this.loaderKeys(hosts, localKey, node);
|
|
646
|
+
if (ids.length === 0)
|
|
619
647
|
return;
|
|
620
|
-
}
|
|
621
648
|
const fields = this.relationProjection(target, select.base, [
|
|
622
649
|
referencedName,
|
|
623
650
|
...this.keysForChildren(node.children, target),
|
|
624
651
|
]);
|
|
625
|
-
const
|
|
626
|
-
|
|
627
|
-
const related = await (fields ? db.select(fields) : db.select())
|
|
652
|
+
const scope = this.relatedRowScope(target, options);
|
|
653
|
+
const related = await this.inChunks(db, ids, (chunk) => (fields ? db.select(fields) : db.select())
|
|
628
654
|
.from(target)
|
|
629
|
-
.where((0, drizzle_orm_1.and)(...
|
|
655
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(referenced, chunk), ...scope)));
|
|
630
656
|
const byKey = new Map();
|
|
631
|
-
for (const row of related
|
|
657
|
+
for (const row of related)
|
|
632
658
|
byKey.set(row?.[referencedName], row);
|
|
633
659
|
for (const host of hosts) {
|
|
634
660
|
host[node.name] = byKey.get(host[localKey]) ?? null;
|
|
@@ -643,27 +669,43 @@ class SqlBaseCrudService {
|
|
|
643
669
|
if (!foreign)
|
|
644
670
|
return;
|
|
645
671
|
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] = [];
|
|
672
|
+
const ids = this.loaderKeys(hosts, parentKey, node);
|
|
673
|
+
if (ids.length === 0)
|
|
650
674
|
return;
|
|
651
|
-
}
|
|
652
675
|
const fields = this.relationProjection(target, select.base, [
|
|
653
676
|
foreignKey,
|
|
654
677
|
...this.keysForChildren(node.children, target),
|
|
655
678
|
]);
|
|
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;
|
|
679
|
+
const children = await this.loadCollection(db, node, ids, options, () => fields ? db.select(fields).from(target) : db.select().from(target));
|
|
665
680
|
this.groupCollection(hosts, node.name, parentKey, children, foreignKey);
|
|
666
681
|
}
|
|
682
|
+
loaderKeys(hosts, key, node) {
|
|
683
|
+
const ids = Array.from(new Set(hosts.map((host) => host[key]).filter((v) => v != null)));
|
|
684
|
+
if (ids.length === 0) {
|
|
685
|
+
const collection = this.isCollection(node.config);
|
|
686
|
+
for (const host of hosts)
|
|
687
|
+
host[node.name] = collection ? [] : null;
|
|
688
|
+
}
|
|
689
|
+
return ids;
|
|
690
|
+
}
|
|
691
|
+
loadCollection(db, node, ids, options, source) {
|
|
692
|
+
const relation = node.config;
|
|
693
|
+
const key = this.relationType(relation) === "manyToMany"
|
|
694
|
+
? relation.through[relation.throughLocalKey]
|
|
695
|
+
: relation.table[relation.foreignKey];
|
|
696
|
+
const scope = this.relatedRowScope(relation.table, options);
|
|
697
|
+
const orderBy = this.collectionOrderBy(relation);
|
|
698
|
+
return this.inChunks(db, ids, (chunk) => {
|
|
699
|
+
const query = source().where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(key, chunk), ...scope));
|
|
700
|
+
return orderBy.length > 0 ? query.orderBy(...orderBy) : query;
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
selectThrough(db, fields, relation, target = relation.table, through = relation.through) {
|
|
704
|
+
return db
|
|
705
|
+
.select(fields)
|
|
706
|
+
.from(through)
|
|
707
|
+
.innerJoin(target, (0, drizzle_orm_1.eq)(through[relation.throughForeignKey], target[relation.references ?? "id"]));
|
|
708
|
+
}
|
|
667
709
|
async loadManyToMany(db, hosts, node, select, hostTable = this.config.table, options) {
|
|
668
710
|
const { through, throughLocalKey, throughForeignKey, references } = node.config;
|
|
669
711
|
if (!through || !throughLocalKey || !throughForeignKey)
|
|
@@ -676,12 +718,9 @@ class SqlBaseCrudService {
|
|
|
676
718
|
if (!referenced || !joinLocal || !joinForeign)
|
|
677
719
|
return;
|
|
678
720
|
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] = [];
|
|
721
|
+
const ids = this.loaderKeys(hosts, parentKey, node);
|
|
722
|
+
if (ids.length === 0)
|
|
683
723
|
return;
|
|
684
|
-
}
|
|
685
724
|
const columns = (0, drizzle_orm_1.getTableColumns)(target);
|
|
686
725
|
const wanted = select.base.length > 0
|
|
687
726
|
? [...select.base, ...this.keysForChildren(node.children, target)]
|
|
@@ -691,19 +730,25 @@ class SqlBaseCrudService {
|
|
|
691
730
|
if (columns[name])
|
|
692
731
|
fields[name] = columns[name];
|
|
693
732
|
}
|
|
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;
|
|
733
|
+
const rows = await this.loadCollection(db, node, ids, options, () => this.selectThrough(db, fields, node.config));
|
|
705
734
|
this.groupCollection(hosts, node.name, parentKey, rows, JOIN_KEY, true);
|
|
706
735
|
}
|
|
736
|
+
relationChunkSize(db) {
|
|
737
|
+
const { limit } = this.bindLimit(db);
|
|
738
|
+
return Math.max(1, limit - Math.min(1_000, Math.floor(limit / 10)));
|
|
739
|
+
}
|
|
740
|
+
async inChunks(db, ids, query) {
|
|
741
|
+
const size = this.relationChunkSize(db);
|
|
742
|
+
if (ids.length <= size)
|
|
743
|
+
return (await query(ids)) ?? [];
|
|
744
|
+
const out = [];
|
|
745
|
+
for (let start = 0; start < ids.length; start += size) {
|
|
746
|
+
for (const row of (await query(ids.slice(start, start + size))) ?? []) {
|
|
747
|
+
out.push(row);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
return out;
|
|
751
|
+
}
|
|
707
752
|
groupCollection(hosts, name, parentKey, rows, groupBy, stripGroupKey = false) {
|
|
708
753
|
const grouped = new Map();
|
|
709
754
|
for (const row of rows ?? []) {
|
|
@@ -839,10 +884,7 @@ class SqlBaseCrudService {
|
|
|
839
884
|
return { key, column, order };
|
|
840
885
|
}
|
|
841
886
|
const name = key.slice(0, dot);
|
|
842
|
-
const
|
|
843
|
-
const relation = Object.hasOwn(relations, name)
|
|
844
|
-
? relations[name]
|
|
845
|
-
: undefined;
|
|
887
|
+
const relation = this.relationNamed(name);
|
|
846
888
|
const column = relation
|
|
847
889
|
? this.columnOf(relation.table, key.slice(dot + 1))
|
|
848
890
|
: undefined;
|
|
@@ -1033,11 +1075,22 @@ class SqlBaseCrudService {
|
|
|
1033
1075
|
throw new common_1.BadRequestException(`Invalid full-text query mode: ${String(mode)} (expected 'plain' or 'websearch')`);
|
|
1034
1076
|
}
|
|
1035
1077
|
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
|
-
|
|
1078
|
+
const vector = drizzle_orm_1.sql.join(columns.map((column) => {
|
|
1079
|
+
if (this.isTsVector(column)) {
|
|
1080
|
+
return columns.length > 1
|
|
1081
|
+
? (0, drizzle_orm_1.sql) `coalesce(${column}, ''::tsvector)`
|
|
1082
|
+
: (0, drizzle_orm_1.sql) `${column}`;
|
|
1083
|
+
}
|
|
1084
|
+
return columns.length > 1
|
|
1085
|
+
? (0, drizzle_orm_1.sql) `to_tsvector(${config}, coalesce(${column}, ''))`
|
|
1086
|
+
: (0, drizzle_orm_1.sql) `to_tsvector(${config}, ${column})`;
|
|
1087
|
+
}), (0, drizzle_orm_1.sql) ` || `);
|
|
1039
1088
|
return { vector, query: (0, drizzle_orm_1.sql) `${parse}(${config}, ${term})` };
|
|
1040
1089
|
}
|
|
1090
|
+
isTsVector(column) {
|
|
1091
|
+
const type = typeof column?.getSQLType === "function" ? column.getSQLType() : "";
|
|
1092
|
+
return String(type).trim().toLowerCase() === "tsvector";
|
|
1093
|
+
}
|
|
1041
1094
|
buildSearchCondition(search) {
|
|
1042
1095
|
if (!search?.term || !search.columns?.length)
|
|
1043
1096
|
return undefined;
|
|
@@ -1129,6 +1182,29 @@ class SqlBaseCrudService {
|
|
|
1129
1182
|
}
|
|
1130
1183
|
return row;
|
|
1131
1184
|
}
|
|
1185
|
+
async guardRow(id, options) {
|
|
1186
|
+
const row = await this.findForGuard(id, options);
|
|
1187
|
+
if (!row)
|
|
1188
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1189
|
+
}
|
|
1190
|
+
skipsGuard(options, ...methods) {
|
|
1191
|
+
if (!this.config.sql?.useReturning)
|
|
1192
|
+
return false;
|
|
1193
|
+
if (this.config.events && !options?.skipEvents)
|
|
1194
|
+
return false;
|
|
1195
|
+
const base = SqlBaseCrudService.prototype;
|
|
1196
|
+
return ["emitEvent", ...methods].every((name) => this[name] === base[name]);
|
|
1197
|
+
}
|
|
1198
|
+
async guardOnError(guarded, id, options, run) {
|
|
1199
|
+
try {
|
|
1200
|
+
return await run();
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
if (!guarded)
|
|
1204
|
+
await this.guardRow(id, options);
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1132
1208
|
assertVersion(id, actual, expected) {
|
|
1133
1209
|
if (expected === undefined || expected === null)
|
|
1134
1210
|
return;
|
|
@@ -1171,14 +1247,21 @@ class SqlBaseCrudService {
|
|
|
1171
1247
|
};
|
|
1172
1248
|
}
|
|
1173
1249
|
async find(id, options) {
|
|
1250
|
+
options = this.withScopeMemo(options);
|
|
1174
1251
|
this.checkReadOptions(options);
|
|
1175
1252
|
return this.findFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], options);
|
|
1176
1253
|
}
|
|
1177
1254
|
async findOne(where, options) {
|
|
1255
|
+
options = this.withScopeMemo(options);
|
|
1178
1256
|
this.checkReadOptions(options);
|
|
1179
1257
|
return this.findFirst(this.buildWhereConditionsFromPartial(where), options);
|
|
1180
1258
|
}
|
|
1181
1259
|
async findFirst(conditions, options) {
|
|
1260
|
+
const first = await this.readFirst(conditions, options);
|
|
1261
|
+
await first.hydrate();
|
|
1262
|
+
return first.row;
|
|
1263
|
+
}
|
|
1264
|
+
async readFirst(conditions, options) {
|
|
1182
1265
|
const { transaction, relations = [], select = [] } = options || {};
|
|
1183
1266
|
const db = this.dbFor(transaction);
|
|
1184
1267
|
const nodes = this.relationTree(relations);
|
|
@@ -1193,11 +1276,16 @@ class SqlBaseCrudService {
|
|
|
1193
1276
|
query = this.applyLock(query, options);
|
|
1194
1277
|
const result = await this.runRead(query);
|
|
1195
1278
|
const rows = this.normalizeRelations(result, joined.map((node) => node.name));
|
|
1279
|
+
return {
|
|
1280
|
+
row: rows[0] || null,
|
|
1281
|
+
hydrate: () => this.hydrate(db, rows, nodes, selection, options),
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
async hydrate(db, rows, nodes, selection, options) {
|
|
1196
1285
|
await Promise.all([
|
|
1197
1286
|
this.loadRelations(db, rows, nodes, selection, undefined, options),
|
|
1198
1287
|
this.loadCounts(db, rows, options?.counts, options),
|
|
1199
1288
|
]);
|
|
1200
|
-
return rows[0] || null;
|
|
1201
1289
|
}
|
|
1202
1290
|
planList(filters, sortBy, sortOrder, options) {
|
|
1203
1291
|
this.checkReadOptions(options);
|
|
@@ -1234,6 +1322,9 @@ class SqlBaseCrudService {
|
|
|
1234
1322
|
built.conditions.push(...this.rowScope(options));
|
|
1235
1323
|
return built;
|
|
1236
1324
|
}
|
|
1325
|
+
parseCount(result) {
|
|
1326
|
+
return parseInt(result?.[0]?.count?.toString() || "0", 10);
|
|
1327
|
+
}
|
|
1237
1328
|
buildCountQuery(db, conditions, filterRelations, options) {
|
|
1238
1329
|
let query = db
|
|
1239
1330
|
.select({ count: (0, drizzle_orm_1.sql) `count(*)` })
|
|
@@ -1244,6 +1335,7 @@ class SqlBaseCrudService {
|
|
|
1244
1335
|
return query;
|
|
1245
1336
|
}
|
|
1246
1337
|
async findAll(filters, pagination, options) {
|
|
1338
|
+
options = this.withScopeMemo(options);
|
|
1247
1339
|
const { sortBy, sortOrder = "asc" } = pagination || {};
|
|
1248
1340
|
const withCount = pagination?.withCount !== false;
|
|
1249
1341
|
const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
@@ -1266,10 +1358,7 @@ class SqlBaseCrudService {
|
|
|
1266
1358
|
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
1267
1359
|
const data = fetched.slice(0, safeLimit);
|
|
1268
1360
|
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
|
-
]);
|
|
1361
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1273
1362
|
if (!withCount) {
|
|
1274
1363
|
return {
|
|
1275
1364
|
data: rows,
|
|
@@ -1281,12 +1370,13 @@ class SqlBaseCrudService {
|
|
|
1281
1370
|
}
|
|
1282
1371
|
return {
|
|
1283
1372
|
data: rows,
|
|
1284
|
-
total:
|
|
1373
|
+
total: this.parseCount(totalResult),
|
|
1285
1374
|
page,
|
|
1286
1375
|
limit: safeLimit,
|
|
1287
1376
|
};
|
|
1288
1377
|
}
|
|
1289
1378
|
async findByCursor(filters, pagination, options) {
|
|
1379
|
+
options = this.withScopeMemo(options);
|
|
1290
1380
|
const { sortBy, sortOrder = "asc", count = false } = pagination || {};
|
|
1291
1381
|
const { limit } = this.resolvePagination(1, pagination?.limit);
|
|
1292
1382
|
const plan = this.planList(filters, sortBy, sortOrder, options);
|
|
@@ -1303,9 +1393,17 @@ class SqlBaseCrudService {
|
|
|
1303
1393
|
conditions.push(this.keysetCondition(plan.sort, values));
|
|
1304
1394
|
}
|
|
1305
1395
|
const fields = this.buildSelection(plan.baseSelect, plan.joined, plan.selection);
|
|
1306
|
-
plan.sort.
|
|
1307
|
-
|
|
1396
|
+
const cursorKeys = plan.sort.map((spec, i) => {
|
|
1397
|
+
const field = this.cursorField(spec.column);
|
|
1398
|
+
const projected = field === spec.column && !spec.relation
|
|
1399
|
+
? Object.keys(fields).find((key) => fields[key] === spec.column)
|
|
1400
|
+
: undefined;
|
|
1401
|
+
if (projected)
|
|
1402
|
+
return projected;
|
|
1403
|
+
fields[`${CURSOR_KEY}${i}`] = field;
|
|
1404
|
+
return `${CURSOR_KEY}${i}`;
|
|
1308
1405
|
});
|
|
1406
|
+
const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
|
|
1309
1407
|
let dataQuery = plan.db.select(fields).from(this.config.table);
|
|
1310
1408
|
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1311
1409
|
if (conditions.length > 0)
|
|
@@ -1323,21 +1421,17 @@ class SqlBaseCrudService {
|
|
|
1323
1421
|
const page = data.slice(0, limit);
|
|
1324
1422
|
const last = page[page.length - 1];
|
|
1325
1423
|
const nextCursor = data.length > limit && last
|
|
1326
|
-
? (0, cursor_1.encodeCursor)(
|
|
1424
|
+
? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
|
|
1327
1425
|
: null;
|
|
1328
1426
|
for (const row of page) {
|
|
1329
|
-
|
|
1330
|
-
delete row[
|
|
1331
|
-
});
|
|
1427
|
+
for (const key of hidden)
|
|
1428
|
+
delete row[key];
|
|
1332
1429
|
}
|
|
1333
1430
|
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
|
-
]);
|
|
1431
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1338
1432
|
const out = { data: rows, limit, nextCursor };
|
|
1339
1433
|
if (count) {
|
|
1340
|
-
out.total =
|
|
1434
|
+
out.total = this.parseCount(totalResult);
|
|
1341
1435
|
}
|
|
1342
1436
|
return out;
|
|
1343
1437
|
}
|
|
@@ -1356,9 +1450,16 @@ class SqlBaseCrudService {
|
|
|
1356
1450
|
return (0, drizzle_orm_1.gt)(column, value);
|
|
1357
1451
|
if (op === "<")
|
|
1358
1452
|
return (0, drizzle_orm_1.lt)(column, value);
|
|
1453
|
+
if (op === ">=")
|
|
1454
|
+
return (0, drizzle_orm_1.gte)(column, value);
|
|
1455
|
+
if (op === "<=")
|
|
1456
|
+
return (0, drizzle_orm_1.lte)(column, value);
|
|
1359
1457
|
return (0, drizzle_orm_1.eq)(column, value);
|
|
1360
1458
|
}
|
|
1361
1459
|
keysetCondition(specs, values) {
|
|
1460
|
+
const row = this.keysetRowComparison(specs, values);
|
|
1461
|
+
if (row)
|
|
1462
|
+
return row;
|
|
1362
1463
|
const branches = [];
|
|
1363
1464
|
specs.forEach((spec, i) => {
|
|
1364
1465
|
const after = this.afterCursor(spec, values[i]);
|
|
@@ -1371,21 +1472,64 @@ class SqlBaseCrudService {
|
|
|
1371
1472
|
: this.compareCursor(previous.column, "=", values[j]));
|
|
1372
1473
|
branches.push(ties.length > 0 ? (0, drizzle_orm_1.and)(...ties, after) : after);
|
|
1373
1474
|
});
|
|
1374
|
-
|
|
1475
|
+
if (branches.length === 0)
|
|
1476
|
+
return (0, drizzle_orm_1.sql) `false`;
|
|
1477
|
+
const after = (0, drizzle_orm_1.or)(...branches);
|
|
1478
|
+
const bound = this.keysetBound(specs, values);
|
|
1479
|
+
return bound ? (0, drizzle_orm_1.and)(bound, after) : after;
|
|
1480
|
+
}
|
|
1481
|
+
keysetBound(specs, values) {
|
|
1482
|
+
const [first] = specs;
|
|
1483
|
+
if (!first || specs.length < 2 || values[0] === null)
|
|
1484
|
+
return undefined;
|
|
1485
|
+
if (this.keysetNullable(first) && !this.nullsFirst(first)) {
|
|
1486
|
+
return undefined;
|
|
1487
|
+
}
|
|
1488
|
+
return this.compareCursor(first.column, first.order === "asc" ? ">=" : "<=", values[0]);
|
|
1489
|
+
}
|
|
1490
|
+
keysetRowComparison(specs, values) {
|
|
1491
|
+
if (this.config.dialect !== "postgresql" || specs.length < 2) {
|
|
1492
|
+
return undefined;
|
|
1493
|
+
}
|
|
1494
|
+
const order = specs[0].order;
|
|
1495
|
+
const fits = specs.every((spec, i) => spec.order === order &&
|
|
1496
|
+
values[i] !== null &&
|
|
1497
|
+
!this.keysetNullable(spec));
|
|
1498
|
+
if (!fits)
|
|
1499
|
+
return undefined;
|
|
1500
|
+
const columns = drizzle_orm_1.sql.join(specs.map((spec) => spec.column), (0, drizzle_orm_1.sql) `, `);
|
|
1501
|
+
const params = drizzle_orm_1.sql.join(specs.map((spec, i) => spec.column?.dataType === "date"
|
|
1502
|
+
? (0, drizzle_orm_1.sql) `${values[i]}`
|
|
1503
|
+
: drizzle_orm_1.sql.param(values[i], spec.column)), (0, drizzle_orm_1.sql) `, `);
|
|
1504
|
+
return (0, drizzle_orm_1.sql) `(${columns}) ${drizzle_orm_1.sql.raw(order === "asc" ? ">" : "<")} (${params})`;
|
|
1505
|
+
}
|
|
1506
|
+
keysetNullable(spec) {
|
|
1507
|
+
if (spec.relation)
|
|
1508
|
+
return true;
|
|
1509
|
+
return !(spec.column?.notNull || spec.column?.primary);
|
|
1510
|
+
}
|
|
1511
|
+
nullsFirst(spec) {
|
|
1512
|
+
return this.config.dialect === "postgresql"
|
|
1513
|
+
? spec.order === "desc"
|
|
1514
|
+
: spec.order === "asc";
|
|
1375
1515
|
}
|
|
1376
1516
|
afterCursor(spec, value) {
|
|
1377
|
-
const
|
|
1378
|
-
const nullsFirst = postgres ? spec.order === "desc" : spec.order === "asc";
|
|
1517
|
+
const nullsFirst = this.nullsFirst(spec);
|
|
1379
1518
|
if (value === null) {
|
|
1380
1519
|
return nullsFirst ? (0, drizzle_orm_1.isNotNull)(spec.column) : undefined;
|
|
1381
1520
|
}
|
|
1382
1521
|
const past = this.compareCursor(spec.column, spec.order === "asc" ? ">" : "<", value);
|
|
1383
|
-
return nullsFirst
|
|
1522
|
+
return nullsFirst || !this.keysetNullable(spec)
|
|
1523
|
+
? past
|
|
1524
|
+
: (0, drizzle_orm_1.or)(past, (0, drizzle_orm_1.isNull)(spec.column));
|
|
1384
1525
|
}
|
|
1385
1526
|
async emitEvent(phase, operation, bulk, payload, options) {
|
|
1386
1527
|
const bus = this.config.events;
|
|
1387
1528
|
if (!bus || options?.skipEvents)
|
|
1388
1529
|
return;
|
|
1530
|
+
if (payload.transaction && typeof payload.transaction === "object") {
|
|
1531
|
+
this.ownedTransactions.delete(payload.transaction);
|
|
1532
|
+
}
|
|
1389
1533
|
await bus.emit({
|
|
1390
1534
|
entity: this.getEntityName(),
|
|
1391
1535
|
operation,
|
|
@@ -1407,10 +1551,26 @@ class SqlBaseCrudService {
|
|
|
1407
1551
|
return;
|
|
1408
1552
|
}
|
|
1409
1553
|
const hosts = rows.filter(Boolean);
|
|
1410
|
-
await
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1554
|
+
await this.hydrate(db, hosts, nodes, undefined, options);
|
|
1555
|
+
}
|
|
1556
|
+
loadsAfterCommit(operation, options) {
|
|
1557
|
+
if (operation === "create" || options?.transaction)
|
|
1558
|
+
return false;
|
|
1559
|
+
try {
|
|
1560
|
+
return this.countPlan(options?.counts).every((count) => !count.filter);
|
|
1561
|
+
}
|
|
1562
|
+
catch {
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
async writeThenLoad(operation, options, write) {
|
|
1567
|
+
const late = this.loadsAfterCommit(operation, options);
|
|
1568
|
+
const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written) => late
|
|
1569
|
+
? Promise.resolve()
|
|
1570
|
+
: this.loadWriteRelations(inTx, written, options)), options?.transaction);
|
|
1571
|
+
if (late)
|
|
1572
|
+
await this.loadWriteRelations(this.dbFor(), rows, options);
|
|
1573
|
+
return rows;
|
|
1414
1574
|
}
|
|
1415
1575
|
expectedVersionOf(data, options) {
|
|
1416
1576
|
const column = this.config.versionColumn;
|
|
@@ -1466,6 +1626,7 @@ class SqlBaseCrudService {
|
|
|
1466
1626
|
return createdEntity;
|
|
1467
1627
|
}
|
|
1468
1628
|
async create(data, options) {
|
|
1629
|
+
options = this.withScopeMemo(options);
|
|
1469
1630
|
const { transaction, hooks = {} } = options || {};
|
|
1470
1631
|
this.checkReadOptions(options);
|
|
1471
1632
|
const entityData = await this.prepareCreate(data, options);
|
|
@@ -1526,9 +1687,11 @@ class SqlBaseCrudService {
|
|
|
1526
1687
|
if (!(0, merge_config_1.isPlainObject)(value)) {
|
|
1527
1688
|
throw new crud_exceptions_1.ValidationFailedException(`the relation "${name}" takes an object to create`);
|
|
1528
1689
|
}
|
|
1690
|
+
const parentRow = this.relatedRow(relation.table, value, options);
|
|
1691
|
+
await this.assertNestedKeysInScope(tx, relation, [parentRow], options);
|
|
1529
1692
|
const [parent] = await tx
|
|
1530
1693
|
.insert(relation.table)
|
|
1531
|
-
.values(
|
|
1694
|
+
.values(parentRow)
|
|
1532
1695
|
.returning();
|
|
1533
1696
|
row[relation.localKey] = parent[relation.references ?? "id"];
|
|
1534
1697
|
}
|
|
@@ -1550,6 +1713,7 @@ class SqlBaseCrudService {
|
|
|
1550
1713
|
[relation.foreignKey]: parentValue,
|
|
1551
1714
|
};
|
|
1552
1715
|
});
|
|
1716
|
+
await this.assertNestedKeysInScope(tx, relation, children, options);
|
|
1553
1717
|
if (children.length > 0) {
|
|
1554
1718
|
await tx.insert(relation.table).values(children);
|
|
1555
1719
|
}
|
|
@@ -1558,13 +1722,15 @@ class SqlBaseCrudService {
|
|
|
1558
1722
|
const ids = value.filter((item) => !(0, merge_config_1.isPlainObject)(item));
|
|
1559
1723
|
const fresh = value.filter((item) => (0, merge_config_1.isPlainObject)(item));
|
|
1560
1724
|
if (fresh.length > 0) {
|
|
1725
|
+
const freshRows = fresh.map((item) => this.relatedRow(relation.table, item, options));
|
|
1726
|
+
await this.assertNestedKeysInScope(tx, relation, freshRows, options);
|
|
1561
1727
|
const inserted = await tx
|
|
1562
1728
|
.insert(relation.table)
|
|
1563
|
-
.values(
|
|
1729
|
+
.values(freshRows)
|
|
1564
1730
|
.returning();
|
|
1565
1731
|
ids.push(...inserted.map((r) => r[relation.references ?? "id"]));
|
|
1566
1732
|
}
|
|
1567
|
-
await this.linkTargets(tx, relation, parentValue, ids, options);
|
|
1733
|
+
await this.linkTargets(tx, relation, parentValue, ids, options, true);
|
|
1568
1734
|
}
|
|
1569
1735
|
return created;
|
|
1570
1736
|
}
|
|
@@ -1596,11 +1762,15 @@ class SqlBaseCrudService {
|
|
|
1596
1762
|
}
|
|
1597
1763
|
return out;
|
|
1598
1764
|
}
|
|
1599
|
-
async keysOutsideScope(db, rows, options, skip = []
|
|
1765
|
+
async keysOutsideScope(db, rows, options, skip = [], owner = {
|
|
1766
|
+
table: this.config.table,
|
|
1767
|
+
relations: this.config.relations,
|
|
1768
|
+
}) {
|
|
1600
1769
|
const failures = new Map();
|
|
1601
1770
|
if (!this.resolveScope(options))
|
|
1602
1771
|
return failures;
|
|
1603
|
-
|
|
1772
|
+
const checks = [];
|
|
1773
|
+
for (const [name, relation] of Object.entries(owner.relations ?? {})) {
|
|
1604
1774
|
const local = relation.localKey;
|
|
1605
1775
|
if (!local || this.isCollection(relation) || skip.includes(name)) {
|
|
1606
1776
|
continue;
|
|
@@ -1609,23 +1779,31 @@ class SqlBaseCrudService {
|
|
|
1609
1779
|
const referenced = relation.table[relation.references ?? "id"];
|
|
1610
1780
|
if (scope.length === 0 || !referenced)
|
|
1611
1781
|
continue;
|
|
1612
|
-
const keys = this.uniqueValues(rows.map((row) => row[local]).filter((value) => value != null));
|
|
1782
|
+
const keys = this.uniqueValues(rows.map((row) => row[local]).filter((value) => value != null), referenced);
|
|
1613
1783
|
if (keys.length === 0)
|
|
1614
1784
|
continue;
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1785
|
+
checks.push({
|
|
1786
|
+
local,
|
|
1787
|
+
referenced,
|
|
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)),
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
const found = await Promise.all(checks.map((check) => check.query));
|
|
1795
|
+
checks.forEach(({ local, referenced }, i) => {
|
|
1796
|
+
const present = new Set(found[i].map((row) => this.valueKey(row.id, referenced)));
|
|
1620
1797
|
rows.forEach((row, index) => {
|
|
1621
1798
|
const value = row[local];
|
|
1622
|
-
if (value == null || present.has(this.valueKey(value)))
|
|
1799
|
+
if (value == null || present.has(this.valueKey(value, referenced))) {
|
|
1623
1800
|
return;
|
|
1801
|
+
}
|
|
1624
1802
|
if (!failures.has(index)) {
|
|
1625
|
-
failures.set(index, this.missingParentException(local));
|
|
1803
|
+
failures.set(index, this.missingParentException(local, owner.table));
|
|
1626
1804
|
}
|
|
1627
1805
|
});
|
|
1628
|
-
}
|
|
1806
|
+
});
|
|
1629
1807
|
return failures;
|
|
1630
1808
|
}
|
|
1631
1809
|
async assertBatchKeysInScope(db, rows, refs, message, options) {
|
|
@@ -1639,18 +1817,30 @@ class SqlBaseCrudService {
|
|
|
1639
1817
|
if (failure)
|
|
1640
1818
|
throw failure;
|
|
1641
1819
|
}
|
|
1642
|
-
|
|
1820
|
+
async assertNestedKeysInScope(tx, relation, rows, options) {
|
|
1821
|
+
if (!relation.relations || rows.length === 0)
|
|
1822
|
+
return;
|
|
1823
|
+
const failures = await this.keysOutsideScope(tx, rows, options, [], {
|
|
1824
|
+
table: relation.table,
|
|
1825
|
+
relations: relation.relations,
|
|
1826
|
+
});
|
|
1827
|
+
if (failures.size === 0)
|
|
1828
|
+
return;
|
|
1829
|
+
const first = [...failures.keys()].reduce((a, b) => Math.min(a, b));
|
|
1830
|
+
throw failures.get(first);
|
|
1831
|
+
}
|
|
1832
|
+
missingParentException(localKey, table = this.config.table) {
|
|
1643
1833
|
const field = this.config.dialect === "postgresql"
|
|
1644
|
-
? (this.columnOf(
|
|
1834
|
+
? (this.columnOf(table, localKey)?.name ?? localKey)
|
|
1645
1835
|
: "reference";
|
|
1646
1836
|
return new crud_exceptions_1.ForeignKeyViolationException(this.getEntityName(), field);
|
|
1647
1837
|
}
|
|
1648
|
-
async linkTargets(tx, relation, parentValue, ids, options) {
|
|
1649
|
-
const wanted = this.uniqueValues(ids);
|
|
1650
|
-
if (wanted.length === 0)
|
|
1651
|
-
return;
|
|
1838
|
+
async linkTargets(tx, relation, parentValue, ids, options, fresh = false) {
|
|
1652
1839
|
const target = relation.table;
|
|
1653
1840
|
const referenced = target[relation.references ?? "id"];
|
|
1841
|
+
const wanted = this.uniqueValues(ids, referenced);
|
|
1842
|
+
if (wanted.length === 0)
|
|
1843
|
+
return;
|
|
1654
1844
|
const through = relation.through;
|
|
1655
1845
|
const local = through[relation.throughLocalKey];
|
|
1656
1846
|
const foreign = through[relation.throughForeignKey];
|
|
@@ -1660,8 +1850,9 @@ class SqlBaseCrudService {
|
|
|
1660
1850
|
.select({ id: referenced })
|
|
1661
1851
|
.from(target)
|
|
1662
1852
|
.where((0, drizzle_orm_1.and)(...conditions));
|
|
1663
|
-
const
|
|
1664
|
-
const
|
|
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)));
|
|
1665
1856
|
if (missing.length > 0) {
|
|
1666
1857
|
const name = this.tableName(target);
|
|
1667
1858
|
throw new crud_exceptions_1.BulkOperationException("Unknown related ids", missing.map((id) => ({
|
|
@@ -1669,12 +1860,14 @@ class SqlBaseCrudService {
|
|
|
1669
1860
|
error: new crud_exceptions_1.EntityNotFoundException(name, id),
|
|
1670
1861
|
})));
|
|
1671
1862
|
}
|
|
1672
|
-
const linked =
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1863
|
+
const linked = fresh
|
|
1864
|
+
? []
|
|
1865
|
+
: await tx
|
|
1866
|
+
.select({ id: foreign })
|
|
1867
|
+
.from(through)
|
|
1868
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(local, parentValue), (0, drizzle_orm_1.inArray)(foreign, wanted)));
|
|
1869
|
+
const already = new Set(linked.map((row) => key(row.id)));
|
|
1870
|
+
const add = wanted.filter((id) => !already.has(key(id)));
|
|
1678
1871
|
if (add.length > 0) {
|
|
1679
1872
|
await tx.insert(through).values(add.map((id) => ({
|
|
1680
1873
|
[relation.throughLocalKey]: parentValue,
|
|
@@ -1683,19 +1876,19 @@ class SqlBaseCrudService {
|
|
|
1683
1876
|
}
|
|
1684
1877
|
}
|
|
1685
1878
|
async attach(id, relation, relatedIds, options) {
|
|
1879
|
+
options = this.withScopeMemo(options);
|
|
1686
1880
|
return this.changeLinks("attach", id, relation, relatedIds, options);
|
|
1687
1881
|
}
|
|
1688
1882
|
async detach(id, relation, relatedIds, options) {
|
|
1883
|
+
options = this.withScopeMemo(options);
|
|
1689
1884
|
return this.changeLinks("detach", id, relation, relatedIds, options);
|
|
1690
1885
|
}
|
|
1691
1886
|
async sync(id, relation, relatedIds, options) {
|
|
1887
|
+
options = this.withScopeMemo(options);
|
|
1692
1888
|
return this.changeLinks("sync", id, relation, relatedIds, options);
|
|
1693
1889
|
}
|
|
1694
1890
|
async changeLinks(operation, id, name, relatedIds, options) {
|
|
1695
|
-
const
|
|
1696
|
-
const relation = Object.hasOwn(relations, name)
|
|
1697
|
-
? relations[name]
|
|
1698
|
-
: undefined;
|
|
1891
|
+
const relation = this.relationNamed(name);
|
|
1699
1892
|
if (!relation || this.relationType(relation) !== "manyToMany") {
|
|
1700
1893
|
throw new common_1.BadRequestException(`"${name}" is not a many-to-many relation of ${this.getEntityName()}`);
|
|
1701
1894
|
}
|
|
@@ -1712,17 +1905,17 @@ class SqlBaseCrudService {
|
|
|
1712
1905
|
throw new common_1.BadRequestException(`${operation} takes an array of ids`);
|
|
1713
1906
|
}
|
|
1714
1907
|
this.checkReadOptions(options);
|
|
1715
|
-
const ids = this.uniqueValues(relatedIds ?? []);
|
|
1908
|
+
const ids = this.uniqueValues(relatedIds ?? [], relation.table[relation.references ?? "id"]);
|
|
1716
1909
|
const parentKey = this.parentKeyOf(relation);
|
|
1717
1910
|
return this.executeSqlTransaction(async (tx) => {
|
|
1718
1911
|
const opts = { ...options, transaction: tx };
|
|
1719
|
-
const
|
|
1912
|
+
const read = {
|
|
1720
1913
|
...opts,
|
|
1721
|
-
relations: [],
|
|
1722
|
-
counts: undefined,
|
|
1723
|
-
select: [this.config.primaryKey, parentKey],
|
|
1914
|
+
relations: Array.from(new Set([name, ...(options?.relations ?? [])])),
|
|
1724
1915
|
skipAllowlists: true,
|
|
1725
|
-
}
|
|
1916
|
+
};
|
|
1917
|
+
this.checkReadOptions(read);
|
|
1918
|
+
const { row: parent, hydrate } = await this.readFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], read);
|
|
1726
1919
|
if (!parent)
|
|
1727
1920
|
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1728
1921
|
await this.emitEvent("before", operation, false, { ids: [id], input: relatedIds, transaction: tx }, options);
|
|
@@ -1750,22 +1943,22 @@ class SqlBaseCrudService {
|
|
|
1750
1943
|
: (0, drizzle_orm_1.eq)(local, parentValue));
|
|
1751
1944
|
}
|
|
1752
1945
|
}
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
relations: Array.from(new Set([name, ...(options?.relations ?? [])])),
|
|
1756
|
-
skipAllowlists: true,
|
|
1757
|
-
}));
|
|
1946
|
+
await hydrate();
|
|
1947
|
+
const row = parent;
|
|
1758
1948
|
await this.emitEvent("after", operation, false, { ids: [id], input: relatedIds, rows: [row], transaction: tx }, options);
|
|
1759
1949
|
return row;
|
|
1760
1950
|
}, options?.transaction);
|
|
1761
1951
|
}
|
|
1762
|
-
valueKey(value) {
|
|
1763
|
-
|
|
1952
|
+
valueKey(value, column) {
|
|
1953
|
+
const key = String(value);
|
|
1954
|
+
return column?.columnType === "PgUUID" || this.config.dialect === "mysql"
|
|
1955
|
+
? key.toLowerCase()
|
|
1956
|
+
: key;
|
|
1764
1957
|
}
|
|
1765
|
-
uniqueValues(values) {
|
|
1958
|
+
uniqueValues(values, column) {
|
|
1766
1959
|
const seen = new Set();
|
|
1767
1960
|
return values.filter((value) => {
|
|
1768
|
-
const key = this.valueKey(value);
|
|
1961
|
+
const key = this.valueKey(value, column);
|
|
1769
1962
|
if (seen.has(key))
|
|
1770
1963
|
return false;
|
|
1771
1964
|
seen.add(key);
|
|
@@ -1796,27 +1989,31 @@ class SqlBaseCrudService {
|
|
|
1796
1989
|
return entityData;
|
|
1797
1990
|
}
|
|
1798
1991
|
async update(id, data, options) {
|
|
1992
|
+
options = this.withScopeMemo(options);
|
|
1799
1993
|
const { transaction, hooks = {} } = options || {};
|
|
1800
1994
|
this.checkReadOptions(options);
|
|
1801
1995
|
const expected = this.expectedVersionOf(data, options);
|
|
1802
1996
|
const opts = { ...options, expectedVersion: expected };
|
|
1803
|
-
const
|
|
1804
|
-
if (
|
|
1805
|
-
|
|
1806
|
-
await this.
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1997
|
+
const guarded = !this.skipsGuard(options, "validateUpdate", "mapUpdateDtoToEntity", "prepareUpdate", ...(hooks.skipBefore ? [] : ["beforeUpdate"]));
|
|
1998
|
+
if (guarded)
|
|
1999
|
+
await this.guardRow(id, opts);
|
|
2000
|
+
const entityData = await this.guardOnError(guarded, id, opts, async () => {
|
|
2001
|
+
await this.validateUpdate(id, data);
|
|
2002
|
+
const processedData = hooks.skipBefore
|
|
2003
|
+
? data
|
|
2004
|
+
: await this.beforeUpdate(id, data);
|
|
2005
|
+
return this.prepareUpdate(processedData, opts);
|
|
2006
|
+
});
|
|
1811
2007
|
if (this.config.timestamps?.updatedAt) {
|
|
1812
2008
|
entityData[this.config.timestamps.updatedAt] = new Date();
|
|
1813
2009
|
}
|
|
1814
2010
|
const db = this.dbFor(transaction);
|
|
1815
2011
|
await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
|
|
1816
2012
|
try {
|
|
1817
|
-
await this.assertKeysInScope(db, entityData, opts);
|
|
1818
|
-
const [updatedEntity] = await this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts);
|
|
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)));
|
|
1819
2015
|
if (!updatedEntity) {
|
|
2016
|
+
await this.guardRow(id, opts);
|
|
1820
2017
|
if (expected !== undefined) {
|
|
1821
2018
|
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1822
2019
|
}
|
|
@@ -1832,11 +2029,12 @@ class SqlBaseCrudService {
|
|
|
1832
2029
|
throw this.toClientError(error, "write");
|
|
1833
2030
|
}
|
|
1834
2031
|
}
|
|
1835
|
-
async updateWhereIn(db, set, ids, options) {
|
|
2032
|
+
async updateWhereIn(db, set, ids, options, extra) {
|
|
2033
|
+
const where = this.writeWhere(ids, options);
|
|
1836
2034
|
const query = db
|
|
1837
2035
|
.update(this.config.table)
|
|
1838
2036
|
.set(set)
|
|
1839
|
-
.where(
|
|
2037
|
+
.where(extra ? (0, drizzle_orm_1.and)(where, extra) : where);
|
|
1840
2038
|
if (this.config.sql?.useReturning) {
|
|
1841
2039
|
return this.inIdOrder(await query.returning(), ids);
|
|
1842
2040
|
}
|
|
@@ -1862,39 +2060,65 @@ class SqlBaseCrudService {
|
|
|
1862
2060
|
.map((id) => byKey.get(this.keyOf(id)))
|
|
1863
2061
|
.filter((row) => !!row);
|
|
1864
2062
|
}
|
|
1865
|
-
async
|
|
1866
|
-
if (!this.config.softDelete?.enabled) {
|
|
1867
|
-
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1868
|
-
}
|
|
2063
|
+
async guardedWrite(id, options, step) {
|
|
1869
2064
|
const { transaction, hooks = {} } = options || {};
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
onlyDeleted: false,
|
|
1874
|
-
});
|
|
1875
|
-
if (!existing)
|
|
1876
|
-
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
2065
|
+
if (!this.skipsGuard(options, ...(hooks.skipBefore ? [] : [step.hook]))) {
|
|
2066
|
+
await this.guardRow(id, step.state);
|
|
2067
|
+
}
|
|
1877
2068
|
if (!hooks.skipBefore)
|
|
1878
|
-
await
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
.
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
2069
|
+
await step.before();
|
|
2070
|
+
const event = { ids: [id], transaction };
|
|
2071
|
+
await this.emitEvent("before", step.operation, false, event, options);
|
|
2072
|
+
const finish = async () => {
|
|
2073
|
+
const result = await step.write(this.dbFor(transaction));
|
|
2074
|
+
if (result === undefined) {
|
|
2075
|
+
await this.guardRow(id, step.state);
|
|
2076
|
+
return step.miss();
|
|
2077
|
+
}
|
|
2078
|
+
if (!hooks.skipAfter)
|
|
2079
|
+
await step.after(result);
|
|
2080
|
+
const rows = step.rows?.(result);
|
|
2081
|
+
await this.emitEvent("after", step.operation, false, { ...event, rows }, options);
|
|
2082
|
+
return result;
|
|
2083
|
+
};
|
|
2084
|
+
if (!step.errors)
|
|
2085
|
+
return finish();
|
|
2086
|
+
try {
|
|
2087
|
+
return await finish();
|
|
2088
|
+
}
|
|
2089
|
+
catch (error) {
|
|
2090
|
+
throw this.toClientError(error, step.errors);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
staleOrFalse(id, options) {
|
|
2094
|
+
if (options?.expectedVersion != null) {
|
|
1890
2095
|
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1891
2096
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
2097
|
+
return false;
|
|
2098
|
+
}
|
|
2099
|
+
async softDelete(id, options) {
|
|
2100
|
+
options = this.withScopeMemo(options);
|
|
2101
|
+
if (!this.config.softDelete?.enabled) {
|
|
2102
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1896
2103
|
}
|
|
1897
|
-
|
|
2104
|
+
const live = { ...options, withDeleted: false, onlyDeleted: false };
|
|
2105
|
+
return this.guardedWrite(id, options, {
|
|
2106
|
+
operation: "softDelete",
|
|
2107
|
+
state: live,
|
|
2108
|
+
hook: "beforeSoftDelete",
|
|
2109
|
+
before: () => this.beforeSoftDelete(id),
|
|
2110
|
+
write: async (db) => {
|
|
2111
|
+
let query = db
|
|
2112
|
+
.update(this.config.table)
|
|
2113
|
+
.set(this.softDeleteSet(new Date()))
|
|
2114
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(live)));
|
|
2115
|
+
if (this.config.sql?.useReturning)
|
|
2116
|
+
query = query.returning();
|
|
2117
|
+
return this.wasAffected(await this.runRead(query)) || undefined;
|
|
2118
|
+
},
|
|
2119
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2120
|
+
after: () => this.afterSoftDelete(id),
|
|
2121
|
+
});
|
|
1898
2122
|
}
|
|
1899
2123
|
softDeleteSet(deletedAt) {
|
|
1900
2124
|
return this.withVersionBump({
|
|
@@ -1905,62 +2129,43 @@ class SqlBaseCrudService {
|
|
|
1905
2129
|
});
|
|
1906
2130
|
}
|
|
1907
2131
|
async restore(id, options) {
|
|
2132
|
+
options = this.withScopeMemo(options);
|
|
1908
2133
|
if (!this.config.softDelete?.enabled) {
|
|
1909
2134
|
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1910
2135
|
}
|
|
1911
|
-
const
|
|
1912
|
-
const
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
2136
|
+
const trashed = { ...options, withDeleted: false, onlyDeleted: true };
|
|
2137
|
+
const restored = await this.guardedWrite(id, options, {
|
|
2138
|
+
operation: "restore",
|
|
2139
|
+
state: trashed,
|
|
2140
|
+
hook: "beforeRestore",
|
|
2141
|
+
before: () => this.beforeRestore(id),
|
|
2142
|
+
write: async (db) => (await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options, this.softDeleteScope(trashed))))[0],
|
|
2143
|
+
miss: () => {
|
|
2144
|
+
if (options?.expectedVersion != null && this.config.versionColumn) {
|
|
2145
|
+
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
2146
|
+
}
|
|
2147
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
2148
|
+
},
|
|
2149
|
+
after: (entity) => this.afterRestore(entity),
|
|
2150
|
+
rows: (entity) => [entity],
|
|
1916
2151
|
});
|
|
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;
|
|
2152
|
+
await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
|
|
2153
|
+
return restored;
|
|
1935
2154
|
}
|
|
1936
2155
|
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
|
-
}
|
|
2156
|
+
options = this.withScopeMemo(options);
|
|
2157
|
+
return this.guardedWrite(id, options, {
|
|
2158
|
+
operation: "delete",
|
|
2159
|
+
state: options,
|
|
2160
|
+
hook: "beforeDelete",
|
|
2161
|
+
before: () => this.beforeDelete(id),
|
|
2162
|
+
write: async (db) => this.wasAffected(await db
|
|
2163
|
+
.delete(this.config.table)
|
|
2164
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(options)))) || undefined,
|
|
2165
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2166
|
+
after: () => this.afterDelete(id),
|
|
2167
|
+
errors: "delete",
|
|
2168
|
+
});
|
|
1964
2169
|
}
|
|
1965
2170
|
async forceDelete(id, options) {
|
|
1966
2171
|
return this.delete(id, {
|
|
@@ -1970,6 +2175,7 @@ class SqlBaseCrudService {
|
|
|
1970
2175
|
});
|
|
1971
2176
|
}
|
|
1972
2177
|
async upsert(data, upsert = {}, options) {
|
|
2178
|
+
options = this.withScopeMemo(options);
|
|
1973
2179
|
this.checkReadOptions(options);
|
|
1974
2180
|
const entityData = await this.prepareCreate(data, options);
|
|
1975
2181
|
const db = this.dbFor(options?.transaction);
|
|
@@ -1995,43 +2201,42 @@ class SqlBaseCrudService {
|
|
|
1995
2201
|
return row;
|
|
1996
2202
|
}
|
|
1997
2203
|
async massUpsert(data, upsert = {}, options) {
|
|
1998
|
-
this.
|
|
2204
|
+
options = this.withScopeMemo(options);
|
|
1999
2205
|
const message = "Mass upsert errors";
|
|
2000
|
-
return this.
|
|
2001
|
-
|
|
2002
|
-
|
|
2206
|
+
return this.massWrite({
|
|
2207
|
+
operation: "upsert",
|
|
2208
|
+
message,
|
|
2209
|
+
options,
|
|
2210
|
+
input: data,
|
|
2211
|
+
returnsRows: true,
|
|
2212
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
2003
2213
|
index,
|
|
2004
2214
|
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);
|
|
2215
|
+
})),
|
|
2216
|
+
keys: true,
|
|
2217
|
+
write: (tx, rows, opts) => this.upsertSet(tx, rows, upsert, opts, message),
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
async upsertSet(tx, rows, upsert, opts, message) {
|
|
2221
|
+
const written = await this.runSet(tx, message, (sp) => this.upsertRows(sp, rows, upsert, opts), rows.map((row, index) => ({
|
|
2222
|
+
index,
|
|
2223
|
+
run: (sp) => this.upsertRows(sp, [row], upsert, opts),
|
|
2224
|
+
})), "write");
|
|
2225
|
+
if (written.length >= rows.length)
|
|
2033
2226
|
return written;
|
|
2034
|
-
|
|
2227
|
+
const keys = this.conflictKeys(upsert);
|
|
2228
|
+
const keyOf = (row) => keys
|
|
2229
|
+
.map((k) => this.valueKey(row[k], this.columnOf(this.config.table, k)))
|
|
2230
|
+
.join("|");
|
|
2231
|
+
const seen = new Set(written.map(keyOf));
|
|
2232
|
+
throw new crud_exceptions_1.BulkOperationException(message, rows.flatMap((row, index) => seen.has(keyOf(row))
|
|
2233
|
+
? []
|
|
2234
|
+
: [
|
|
2235
|
+
{
|
|
2236
|
+
index,
|
|
2237
|
+
error: new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(row, upsert)),
|
|
2238
|
+
},
|
|
2239
|
+
]));
|
|
2035
2240
|
}
|
|
2036
2241
|
conflictKeys(upsert) {
|
|
2037
2242
|
return upsert.conflictTarget === undefined
|
|
@@ -2126,38 +2331,78 @@ class SqlBaseCrudService {
|
|
|
2126
2331
|
? (0, drizzle_orm_1.sql) `values(${drizzle_orm_1.sql.identifier(column.name)})`
|
|
2127
2332
|
: (0, drizzle_orm_1.sql) `excluded.${drizzle_orm_1.sql.identifier(column.name)}`;
|
|
2128
2333
|
}
|
|
2334
|
+
async massWrite(spec) {
|
|
2335
|
+
const { options, message, ids } = spec;
|
|
2336
|
+
if (spec.returnsRows)
|
|
2337
|
+
this.checkReadOptions(options);
|
|
2338
|
+
const body = async (tx, load) => {
|
|
2339
|
+
const opts = { ...options, transaction: tx, ...spec.state };
|
|
2340
|
+
const unique = ids ? this.uniqueIds(ids) : [];
|
|
2341
|
+
if (ids) {
|
|
2342
|
+
if (unique.length === 0)
|
|
2343
|
+
return [];
|
|
2344
|
+
await this.assertAllExist(tx, unique, opts, message);
|
|
2345
|
+
}
|
|
2346
|
+
const items = spec.prepare?.(opts, unique);
|
|
2347
|
+
const prepared = items ? await this.eachItem(items, message) : [];
|
|
2348
|
+
if (!ids && prepared.length === 0)
|
|
2349
|
+
return [];
|
|
2350
|
+
const event = {
|
|
2351
|
+
ids: ids ? unique : undefined,
|
|
2352
|
+
input: spec.input,
|
|
2353
|
+
transaction: tx,
|
|
2354
|
+
};
|
|
2355
|
+
await this.emitEvent("before", spec.operation, true, event, options);
|
|
2356
|
+
if (spec.keys) {
|
|
2357
|
+
await this.assertBatchKeysInScope(tx, prepared, ids
|
|
2358
|
+
? unique.map((id) => ({ id }))
|
|
2359
|
+
: prepared.map((_, index) => ({ index })), message, opts);
|
|
2360
|
+
}
|
|
2361
|
+
const written = await spec.write(tx, prepared, opts, unique);
|
|
2362
|
+
if (ids)
|
|
2363
|
+
this.assertAllWritten(unique, written, message, options);
|
|
2364
|
+
if (spec.after && !options?.hooks?.skipAfter) {
|
|
2365
|
+
await spec.after(written, unique);
|
|
2366
|
+
}
|
|
2367
|
+
await this.emitEvent("after", spec.operation, true, {
|
|
2368
|
+
...event,
|
|
2369
|
+
ids: ids ? unique : this.idsOf(written),
|
|
2370
|
+
rows: spec.returnsRows ? written : undefined,
|
|
2371
|
+
}, options);
|
|
2372
|
+
if (spec.returnsRows)
|
|
2373
|
+
await load(tx, written);
|
|
2374
|
+
return ids ? this.inIdOrder(written, ids) : written;
|
|
2375
|
+
};
|
|
2376
|
+
return spec.returnsRows
|
|
2377
|
+
? this.writeThenLoad(spec.operation, options, body)
|
|
2378
|
+
: this.executeSqlTransaction((tx) => body(tx, async () => { }), options?.transaction);
|
|
2379
|
+
}
|
|
2129
2380
|
async massCreate(data, options) {
|
|
2381
|
+
options = this.withScopeMemo(options);
|
|
2130
2382
|
if (options?.bulkMode === "perItem") {
|
|
2131
|
-
return this.
|
|
2383
|
+
return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
|
|
2132
2384
|
}
|
|
2133
|
-
|
|
2134
|
-
return this.
|
|
2135
|
-
|
|
2136
|
-
|
|
2385
|
+
const message = "Mass create errors";
|
|
2386
|
+
return this.massWrite({
|
|
2387
|
+
operation: "create",
|
|
2388
|
+
message,
|
|
2389
|
+
options,
|
|
2390
|
+
input: data,
|
|
2391
|
+
returnsRows: true,
|
|
2392
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
2137
2393
|
index,
|
|
2138
2394
|
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) => ({
|
|
2395
|
+
})),
|
|
2396
|
+
keys: true,
|
|
2397
|
+
write: (tx, rows, opts) => this.runSet(tx, message, (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
|
|
2145
2398
|
index,
|
|
2146
2399
|
run: (sp) => this.insertOne(sp, row, opts),
|
|
2147
|
-
})), "write")
|
|
2148
|
-
|
|
2400
|
+
})), "write"),
|
|
2401
|
+
after: async (created) => {
|
|
2149
2402
|
for (const entity of created)
|
|
2150
2403
|
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);
|
|
2404
|
+
},
|
|
2405
|
+
});
|
|
2161
2406
|
}
|
|
2162
2407
|
async insertMany(db, rows, options) {
|
|
2163
2408
|
if (this.config.sql?.useReturning) {
|
|
@@ -2168,9 +2413,28 @@ class SqlBaseCrudService {
|
|
|
2168
2413
|
await db.insert(this.config.table).values(rows);
|
|
2169
2414
|
return this.fetchByIds(db, rows.map((row) => row[pk]), options);
|
|
2170
2415
|
}
|
|
2171
|
-
const
|
|
2172
|
-
for (const row of rows)
|
|
2173
|
-
|
|
2416
|
+
const ids = [];
|
|
2417
|
+
for (const row of rows) {
|
|
2418
|
+
const result = await db.insert(this.config.table).values(row);
|
|
2419
|
+
ids.push(row[pk] ?? result?.[0]?.insertId);
|
|
2420
|
+
}
|
|
2421
|
+
return this.readBackCreated(db, ids, options);
|
|
2422
|
+
}
|
|
2423
|
+
async readBackCreated(db, ids, options) {
|
|
2424
|
+
const pk = this.config.primaryKey;
|
|
2425
|
+
const base = this.parseSelect(options?.select).base;
|
|
2426
|
+
const fields = this.buildSelectFields(base.length > 0 ? Array.from(new Set([...base, pk])) : base);
|
|
2427
|
+
const column = this.config.table[pk];
|
|
2428
|
+
const rows = await (fields ? db.select(fields) : db.select())
|
|
2429
|
+
.from(this.config.table)
|
|
2430
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(column, ids), ...this.rowScope(options)));
|
|
2431
|
+
const created = this.inIdOrder(rows, ids);
|
|
2432
|
+
if (created.length < ids.length)
|
|
2433
|
+
throw new Error("Failed to create entity");
|
|
2434
|
+
if (fields && !base.includes(pk)) {
|
|
2435
|
+
for (const row of created)
|
|
2436
|
+
delete row[pk];
|
|
2437
|
+
}
|
|
2174
2438
|
return created;
|
|
2175
2439
|
}
|
|
2176
2440
|
assertAllWritten(ids, written, message, options) {
|
|
@@ -2187,59 +2451,62 @@ class SqlBaseCrudService {
|
|
|
2187
2451
|
})));
|
|
2188
2452
|
}
|
|
2189
2453
|
async massUpdate(ids, data, options) {
|
|
2454
|
+
options = this.withScopeMemo(options);
|
|
2190
2455
|
if (options?.bulkMode === "perItem") {
|
|
2191
|
-
return this.
|
|
2456
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass update errors", options, ({ id }, opts) => this.update(id, data, opts));
|
|
2192
2457
|
}
|
|
2193
|
-
this.checkReadOptions(options);
|
|
2194
2458
|
const message = "Mass update errors";
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2459
|
+
const skipBefore = options?.hooks?.skipBefore;
|
|
2460
|
+
return this.massWrite({
|
|
2461
|
+
operation: "update",
|
|
2462
|
+
message,
|
|
2463
|
+
options,
|
|
2464
|
+
ids,
|
|
2465
|
+
input: data,
|
|
2466
|
+
returnsRows: true,
|
|
2467
|
+
prepare: (opts, unique) => unique.map((id) => ({
|
|
2203
2468
|
id,
|
|
2204
2469
|
run: async () => {
|
|
2205
2470
|
await this.validateUpdate(id, data);
|
|
2206
|
-
const processed =
|
|
2471
|
+
const processed = skipBefore
|
|
2207
2472
|
? data
|
|
2208
2473
|
: await this.beforeUpdate(id, data);
|
|
2209
2474
|
return this.prepareUpdate(processed, opts);
|
|
2210
2475
|
},
|
|
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) {
|
|
2476
|
+
})),
|
|
2477
|
+
keys: true,
|
|
2478
|
+
write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
|
|
2479
|
+
after: async (updated) => {
|
|
2236
2480
|
for (const entity of updated)
|
|
2237
2481
|
await this.afterUpdate(entity);
|
|
2482
|
+
},
|
|
2483
|
+
});
|
|
2484
|
+
}
|
|
2485
|
+
async updateSets(tx, sets, unique, opts, message) {
|
|
2486
|
+
const now = new Date();
|
|
2487
|
+
const stamp = (set) => this.withVersionBump(this.config.timestamps?.updatedAt
|
|
2488
|
+
? { ...set, [this.config.timestamps.updatedAt]: now }
|
|
2489
|
+
: set);
|
|
2490
|
+
const groups = new Map();
|
|
2491
|
+
unique.forEach((id, i) => {
|
|
2492
|
+
const key = this.fingerprint(sets[i]);
|
|
2493
|
+
const group = groups.get(key);
|
|
2494
|
+
if (group)
|
|
2495
|
+
group.ids.push(id);
|
|
2496
|
+
else
|
|
2497
|
+
groups.set(key, { set: stamp(sets[i]), ids: [id] });
|
|
2498
|
+
});
|
|
2499
|
+
const written = await this.runSet(tx, message, async (sp) => {
|
|
2500
|
+
const rows = [];
|
|
2501
|
+
for (const group of groups.values()) {
|
|
2502
|
+
rows.push(...(await this.updateWhereIn(sp, group.set, group.ids, opts)));
|
|
2238
2503
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2504
|
+
return rows;
|
|
2505
|
+
}, unique.map((id, i) => ({
|
|
2506
|
+
id,
|
|
2507
|
+
run: (sp) => this.updateWhereIn(sp, stamp(sets[i]), [id], opts),
|
|
2508
|
+
})), "write");
|
|
2509
|
+
return groups.size > 1 ? this.inIdOrder(written, unique) : written;
|
|
2243
2510
|
}
|
|
2244
2511
|
async runKeyed(statement, ids) {
|
|
2245
2512
|
if (this.config.sql?.useReturning) {
|
|
@@ -2253,115 +2520,99 @@ class SqlBaseCrudService {
|
|
|
2253
2520
|
: [];
|
|
2254
2521
|
}
|
|
2255
2522
|
async massSoftDelete(ids, options) {
|
|
2523
|
+
options = this.withScopeMemo(options);
|
|
2256
2524
|
if (!this.config.softDelete?.enabled)
|
|
2257
2525
|
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2258
2526
|
if (options?.bulkMode === "perItem") {
|
|
2259
|
-
|
|
2527
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass soft delete errors", options, ({ id }, opts) => this.softDelete(id, opts));
|
|
2528
|
+
return true;
|
|
2260
2529
|
}
|
|
2261
2530
|
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
|
|
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
|
|
2285
2544
|
.update(this.config.table)
|
|
2286
2545
|
.set(set)
|
|
2287
|
-
.where(this.writeWhere(
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
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) => {
|
|
2291
2550
|
for (const id of unique)
|
|
2292
2551
|
await this.afterSoftDelete(id);
|
|
2293
|
-
}
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
}, options?.transaction);
|
|
2552
|
+
},
|
|
2553
|
+
});
|
|
2554
|
+
return true;
|
|
2297
2555
|
}
|
|
2298
2556
|
async massRestore(ids, options) {
|
|
2557
|
+
options = this.withScopeMemo(options);
|
|
2299
2558
|
if (!this.config.softDelete?.enabled)
|
|
2300
2559
|
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2301
2560
|
if (options?.bulkMode === "perItem") {
|
|
2302
|
-
return this.
|
|
2561
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass restore errors", options, ({ id }, opts) => this.restore(id, opts));
|
|
2303
2562
|
}
|
|
2304
|
-
this.checkReadOptions(options);
|
|
2305
2563
|
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
|
-
|
|
2325
|
-
run: (sp) => this.updateWhereIn(sp, set, [id], opts),
|
|
2326
|
-
})), "write");
|
|
2327
|
-
this.assertAllWritten(unique, restored, message, options);
|
|
2328
|
-
if (!hooks.skipAfter) {
|
|
2564
|
+
return this.massWrite({
|
|
2565
|
+
operation: "restore",
|
|
2566
|
+
message,
|
|
2567
|
+
options,
|
|
2568
|
+
ids,
|
|
2569
|
+
state: { withDeleted: true, onlyDeleted: false },
|
|
2570
|
+
returnsRows: true,
|
|
2571
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2572
|
+
? undefined
|
|
2573
|
+
: unique.map((id) => ({ id, run: () => this.beforeRestore(id) })),
|
|
2574
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2575
|
+
const set = this.softDeleteSet(null);
|
|
2576
|
+
const state = this.softDeleteScope(opts);
|
|
2577
|
+
return this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts, state), unique.map((id) => ({
|
|
2578
|
+
id,
|
|
2579
|
+
run: (sp) => this.updateWhereIn(sp, set, [id], opts, state),
|
|
2580
|
+
})), "write");
|
|
2581
|
+
},
|
|
2582
|
+
after: async (restored) => {
|
|
2329
2583
|
for (const entity of restored)
|
|
2330
2584
|
await this.afterRestore(entity);
|
|
2331
|
-
}
|
|
2332
|
-
|
|
2333
|
-
await this.loadWriteRelations(tx, restored, options);
|
|
2334
|
-
return this.inIdOrder(restored, ids);
|
|
2335
|
-
}, options?.transaction);
|
|
2585
|
+
},
|
|
2586
|
+
});
|
|
2336
2587
|
}
|
|
2337
2588
|
async massDelete(ids, options) {
|
|
2589
|
+
options = this.withScopeMemo(options);
|
|
2338
2590
|
if (options?.bulkMode === "perItem") {
|
|
2339
|
-
|
|
2591
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass delete errors", options, ({ id }, opts) => this.delete(id, opts), "delete");
|
|
2592
|
+
return true;
|
|
2340
2593
|
}
|
|
2341
2594
|
const message = "Mass delete errors";
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
|
|
2355
|
-
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
if (!hooks.skipAfter) {
|
|
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) => {
|
|
2359
2611
|
for (const id of unique)
|
|
2360
2612
|
await this.afterDelete(id);
|
|
2361
|
-
}
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
}, options?.transaction);
|
|
2613
|
+
},
|
|
2614
|
+
});
|
|
2615
|
+
return true;
|
|
2365
2616
|
}
|
|
2366
2617
|
async massForceDelete(ids, options) {
|
|
2367
2618
|
return this.massDelete(ids, {
|
|
@@ -2381,7 +2632,21 @@ class SqlBaseCrudService {
|
|
|
2381
2632
|
});
|
|
2382
2633
|
}
|
|
2383
2634
|
fingerprint(value) {
|
|
2384
|
-
return JSON.stringify(value, (_key, v) =>
|
|
2635
|
+
return JSON.stringify(value, (_key, v) => {
|
|
2636
|
+
if (typeof v === "string")
|
|
2637
|
+
return `s:${v}`;
|
|
2638
|
+
if (typeof v === "bigint")
|
|
2639
|
+
return `bigint:${v}`;
|
|
2640
|
+
if (typeof v === "number" && !Number.isFinite(v))
|
|
2641
|
+
return `number:${v}`;
|
|
2642
|
+
if (v !== null && typeof v === "object" && !Array.isArray(v)) {
|
|
2643
|
+
const proto = Object.getPrototypeOf(v);
|
|
2644
|
+
if (proto !== Object.prototype && proto !== null) {
|
|
2645
|
+
return `opaque:${++fingerprintTag}`;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
return v;
|
|
2649
|
+
});
|
|
2385
2650
|
}
|
|
2386
2651
|
async assertAllExist(tx, ids, options, message) {
|
|
2387
2652
|
const table = this.config.table;
|
|
@@ -2446,12 +2711,16 @@ class SqlBaseCrudService {
|
|
|
2446
2711
|
return item.index !== undefined ? { index: item.index } : { id: item.id };
|
|
2447
2712
|
}
|
|
2448
2713
|
async runSet(tx, message, statement, items, operation) {
|
|
2714
|
+
const owned = this.ownedTransactions.has(tx);
|
|
2449
2715
|
try {
|
|
2450
|
-
return await this.savepoint(tx, statement);
|
|
2716
|
+
return await (owned ? statement(tx) : this.savepoint(tx, statement));
|
|
2451
2717
|
}
|
|
2452
2718
|
catch (error) {
|
|
2453
2719
|
if (error instanceof BindLimitException)
|
|
2454
2720
|
throw error;
|
|
2721
|
+
if (owned) {
|
|
2722
|
+
throw new DeferredDiagnosis(() => this.settleSet(message, error, items, operation));
|
|
2723
|
+
}
|
|
2455
2724
|
const failures = await this.diagnose(tx, items, operation);
|
|
2456
2725
|
if (failures.length > 0) {
|
|
2457
2726
|
throw new crud_exceptions_1.BulkOperationException(message, failures);
|
|
@@ -2459,6 +2728,23 @@ class SqlBaseCrudService {
|
|
|
2459
2728
|
throw this.toClientError(error, operation);
|
|
2460
2729
|
}
|
|
2461
2730
|
}
|
|
2731
|
+
async settleSet(message, error, items, operation) {
|
|
2732
|
+
let failures = [];
|
|
2733
|
+
try {
|
|
2734
|
+
await this.config.db.transaction(async (fresh) => {
|
|
2735
|
+
failures = await this.diagnose(this.withBindLimit(fresh), items, operation);
|
|
2736
|
+
throw ROLLBACK;
|
|
2737
|
+
});
|
|
2738
|
+
}
|
|
2739
|
+
catch (thrown) {
|
|
2740
|
+
if (thrown !== ROLLBACK)
|
|
2741
|
+
throw thrown;
|
|
2742
|
+
}
|
|
2743
|
+
if (failures.length > 0) {
|
|
2744
|
+
throw new crud_exceptions_1.BulkOperationException(message, failures);
|
|
2745
|
+
}
|
|
2746
|
+
throw this.toClientError(error, operation);
|
|
2747
|
+
}
|
|
2462
2748
|
async diagnose(tx, items, operation) {
|
|
2463
2749
|
const failures = [];
|
|
2464
2750
|
try {
|
|
@@ -2483,37 +2769,11 @@ class SqlBaseCrudService {
|
|
|
2483
2769
|
}
|
|
2484
2770
|
return failures;
|
|
2485
2771
|
}
|
|
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;
|
|
2772
|
+
eachInSavepoint(entries, message, options, call, operation = "write") {
|
|
2773
|
+
return this.executeSqlTransaction((tx) => this.eachItem(entries.map((entry) => ({
|
|
2774
|
+
...entry,
|
|
2775
|
+
run: () => this.savepoint(tx, (sp) => call(entry, { ...options, transaction: sp })),
|
|
2776
|
+
})), message, operation), options?.transaction);
|
|
2517
2777
|
}
|
|
2518
2778
|
savepoint(tx, run) {
|
|
2519
2779
|
return typeof tx?.transaction === "function"
|
|
@@ -2521,20 +2781,26 @@ class SqlBaseCrudService {
|
|
|
2521
2781
|
: run(tx);
|
|
2522
2782
|
}
|
|
2523
2783
|
async exists(id, options) {
|
|
2784
|
+
options = this.withScopeMemo(options);
|
|
2785
|
+
this.checkReadOptions({ ...options, select: undefined });
|
|
2524
2786
|
const entity = await this.find(id, {
|
|
2525
2787
|
...options,
|
|
2788
|
+
relations: [],
|
|
2789
|
+
counts: undefined,
|
|
2526
2790
|
select: [this.config.primaryKey],
|
|
2527
2791
|
});
|
|
2528
2792
|
return !!entity;
|
|
2529
2793
|
}
|
|
2530
2794
|
async count(filters, options) {
|
|
2795
|
+
options = this.withScopeMemo(options);
|
|
2531
2796
|
this.checkReadOptions(options);
|
|
2532
2797
|
const db = this.dbFor(options?.transaction);
|
|
2533
2798
|
const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
|
|
2534
2799
|
const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
|
|
2535
|
-
return
|
|
2800
|
+
return this.parseCount(result);
|
|
2536
2801
|
}
|
|
2537
2802
|
async countBy(column, filters, options) {
|
|
2803
|
+
options = this.withScopeMemo(options);
|
|
2538
2804
|
this.checkReadOptions(options);
|
|
2539
2805
|
const db = this.dbFor(options?.transaction);
|
|
2540
2806
|
const spec = this.resolveGroupColumn(column, options);
|
|
@@ -2567,10 +2833,7 @@ class SqlBaseCrudService {
|
|
|
2567
2833
|
}
|
|
2568
2834
|
else {
|
|
2569
2835
|
const name = key.slice(0, dot);
|
|
2570
|
-
const
|
|
2571
|
-
const relation = Object.hasOwn(relations, name)
|
|
2572
|
-
? relations[name]
|
|
2573
|
-
: undefined;
|
|
2836
|
+
const relation = this.relationNamed(name);
|
|
2574
2837
|
const column = relation && !this.isCollection(relation) && relation.localKey
|
|
2575
2838
|
? this.columnOf(relation.table, key.slice(dot + 1))
|
|
2576
2839
|
: undefined;
|
|
@@ -2603,27 +2866,9 @@ class SqlBaseCrudService {
|
|
|
2603
2866
|
return keys.length > 0 ? Array.from(new Set([...base, ...keys])) : base;
|
|
2604
2867
|
}
|
|
2605
2868
|
async loadCounts(db, rows, counts, options) {
|
|
2606
|
-
|
|
2607
|
-
if (names.length === 0 || !Array.isArray(rows))
|
|
2869
|
+
if (this.countNames(counts).length === 0 || !Array.isArray(rows))
|
|
2608
2870
|
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
|
-
});
|
|
2871
|
+
const plan = this.countPlan(counts);
|
|
2627
2872
|
const hosts = rows.filter((row) => row && typeof row === "object");
|
|
2628
2873
|
for (const host of hosts)
|
|
2629
2874
|
host._count = {};
|
|
@@ -2634,7 +2879,7 @@ class SqlBaseCrudService {
|
|
|
2634
2879
|
const ids = Array.from(new Set(hosts.map((host) => host[parentKey]).filter((v) => v != null)));
|
|
2635
2880
|
const totals = new Map();
|
|
2636
2881
|
if (ids.length > 0) {
|
|
2637
|
-
const result = await this.runRead(this.countQuery(db, relation,
|
|
2882
|
+
const result = await this.inChunks(db, ids, (chunk) => this.runRead(this.countQuery(db, relation, chunk, name, filter, options)));
|
|
2638
2883
|
for (const row of result) {
|
|
2639
2884
|
totals.set(String(row[COUNT_KEY]), Number(row.count));
|
|
2640
2885
|
}
|
|
@@ -2644,6 +2889,23 @@ class SqlBaseCrudService {
|
|
|
2644
2889
|
}
|
|
2645
2890
|
}));
|
|
2646
2891
|
}
|
|
2892
|
+
countPlan(counts) {
|
|
2893
|
+
return this.countNames(counts).map((name) => {
|
|
2894
|
+
const relation = this.relationNamed(name);
|
|
2895
|
+
if (!relation) {
|
|
2896
|
+
throw new common_1.BadRequestException(`Unknown relation to count: ${name}`);
|
|
2897
|
+
}
|
|
2898
|
+
if (!this.isCollection(relation)) {
|
|
2899
|
+
throw new common_1.BadRequestException(`Cannot count "${name}": only a has-many or many-to-many relation has a count`);
|
|
2900
|
+
}
|
|
2901
|
+
const filter = !Array.isArray(counts) && (0, merge_config_1.isPlainObject)(counts?.[name])
|
|
2902
|
+
? counts?.[name]
|
|
2903
|
+
: undefined;
|
|
2904
|
+
if (filter)
|
|
2905
|
+
(0, query_parser_1.assertFilterDepth)(filter);
|
|
2906
|
+
return { name, relation, filter };
|
|
2907
|
+
});
|
|
2908
|
+
}
|
|
2647
2909
|
countQuery(db, relation, ids, name, filter, options) {
|
|
2648
2910
|
const target = relation.table;
|
|
2649
2911
|
const conditions = [];
|
|
@@ -2660,12 +2922,7 @@ class SqlBaseCrudService {
|
|
|
2660
2922
|
const total = (0, drizzle_orm_1.sql) `count(*)`;
|
|
2661
2923
|
if (this.relationType(relation) === "manyToMany") {
|
|
2662
2924
|
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))
|
|
2925
|
+
return this.selectThrough(db, { [COUNT_KEY]: joinLocal, count: total }, relation)
|
|
2669
2926
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(joinLocal, ids), ...conditions))
|
|
2670
2927
|
.groupBy(joinLocal);
|
|
2671
2928
|
}
|
|
@@ -2718,11 +2975,7 @@ class SqlBaseCrudService {
|
|
|
2718
2975
|
const joinForeign = relation.through?.[relation.throughForeignKey ?? ""];
|
|
2719
2976
|
if (!referenced || !joinForeign)
|
|
2720
2977
|
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));
|
|
2978
|
+
subquery = this.selectThrough(db, { value: key }, relation).where((0, drizzle_orm_1.and)(...conditions));
|
|
2726
2979
|
}
|
|
2727
2980
|
else {
|
|
2728
2981
|
subquery = db
|
|
@@ -2750,11 +3003,7 @@ class SqlBaseCrudService {
|
|
|
2750
3003
|
let subquery;
|
|
2751
3004
|
if (this.relationType(relation) === "manyToMany") {
|
|
2752
3005
|
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));
|
|
3006
|
+
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
3007
|
}
|
|
2759
3008
|
else {
|
|
2760
3009
|
subquery = db
|
|
@@ -3069,8 +3318,21 @@ class SqlBaseCrudService {
|
|
|
3069
3318
|
const run = (tx) => operation(this.withBindLimit(tx));
|
|
3070
3319
|
if (existingTransaction)
|
|
3071
3320
|
return run(existingTransaction);
|
|
3072
|
-
|
|
3321
|
+
try {
|
|
3322
|
+
return await this.config.db.transaction((tx) => {
|
|
3323
|
+
const view = this.withBindLimit(tx);
|
|
3324
|
+
if (view && typeof view === "object")
|
|
3325
|
+
this.ownedTransactions.add(view);
|
|
3326
|
+
return operation(view);
|
|
3327
|
+
});
|
|
3328
|
+
}
|
|
3329
|
+
catch (error) {
|
|
3330
|
+
if (error instanceof DeferredDiagnosis)
|
|
3331
|
+
return error.settle();
|
|
3332
|
+
throw error;
|
|
3333
|
+
}
|
|
3073
3334
|
}
|
|
3335
|
+
ownedTransactions = new WeakSet();
|
|
3074
3336
|
assertTransactions(db) {
|
|
3075
3337
|
if (this.config.dialect !== "sqlite")
|
|
3076
3338
|
return;
|
|
@@ -3090,6 +3352,7 @@ class SqlBaseCrudService {
|
|
|
3090
3352
|
}
|
|
3091
3353
|
}
|
|
3092
3354
|
async fullTextSearch(searchTerm, searchColumns, pagination, options) {
|
|
3355
|
+
options = this.withScopeMemo(options);
|
|
3093
3356
|
if (this.config.dialect !== "postgresql") {
|
|
3094
3357
|
throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
|
|
3095
3358
|
}
|
|
@@ -3128,12 +3391,9 @@ class SqlBaseCrudService {
|
|
|
3128
3391
|
.offset(offset);
|
|
3129
3392
|
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
3130
3393
|
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
3131
|
-
const total =
|
|
3394
|
+
const total = this.parseCount(totalResult);
|
|
3132
3395
|
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
|
-
]);
|
|
3396
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
|
|
3137
3397
|
return { data: rows, total, page, limit };
|
|
3138
3398
|
}
|
|
3139
3399
|
}
|