@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 = Array.from(new Set(hosts.map((host) => host[localKey]).filter((v) => v != null)));
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 conditions = [(0, drizzle_orm_1.inArray)(referenced, ids)];
626
- conditions.push(...this.relatedRowScope(target, options));
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)(...conditions));
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 = Array.from(new Set(hosts.map((host) => host[parentKey]).filter((v) => v != null)));
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 conditions = [(0, drizzle_orm_1.inArray)(foreign, ids)];
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 = Array.from(new Set(hosts.map((host) => host[parentKey]).filter((v) => v != null)));
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 conditions = [(0, drizzle_orm_1.inArray)(joinLocal, ids)];
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 relations = this.config.relations || {};
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) => columns.length > 1
1037
- ? (0, drizzle_orm_1.sql) `to_tsvector(${config}, coalesce(${column}, ''))`
1038
- : (0, drizzle_orm_1.sql) `to_tsvector(${config}, ${column})`), (0, drizzle_orm_1.sql) ` || `);
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 Promise.all([
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: parseInt(totalResult[0]?.count?.toString() || "0", 10),
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.forEach((spec, i) => {
1307
- fields[`${CURSOR_KEY}${i}`] = this.cursorField(spec.column);
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)(plan.sort.map((_, i) => last[`${CURSOR_KEY}${i}`]), signature)
1421
+ ? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
1327
1422
  : null;
1328
1423
  for (const row of page) {
1329
- plan.sort.forEach((_, i) => {
1330
- delete row[`${CURSOR_KEY}${i}`];
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 Promise.all([
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 = parseInt(totalResult?.[0]?.count?.toString() || "0", 10);
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
- return branches.length > 0 ? (0, drizzle_orm_1.or)(...branches) : (0, drizzle_orm_1.sql) `false`;
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 postgres = this.config.dialect === "postgresql";
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 ? past : (0, drizzle_orm_1.or)(past, (0, drizzle_orm_1.isNull)(spec.column));
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 Promise.all([
1411
- this.loadRelations(db, hosts, nodes, undefined, undefined, options),
1412
- this.loadCounts(db, hosts, options?.counts, options),
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 = await tx
1673
- .select({ id: foreign })
1674
- .from(through)
1675
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(local, parentValue), (0, drizzle_orm_1.inArray)(foreign, wanted)));
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 relations = this.config.relations || {};
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 parent = await this.find(id, {
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
- const row = (await this.find(id, {
1754
- ...opts,
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 existing = await this.findForGuard(id, opts);
1804
- if (!existing)
1805
- throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
1806
- await this.validateUpdate(id, data);
1807
- const processedData = hooks.skipBefore
1808
- ? data
1809
- : await this.beforeUpdate(id, data);
1810
- const entityData = this.prepareUpdate(processedData, opts);
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(this.writeWhere(ids, options));
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 softDelete(id, options) {
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
- const existing = await this.findForGuard(id, {
1871
- ...options,
1872
- withDeleted: false,
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 this.beforeSoftDelete(id);
1879
- await this.emitEvent("before", "softDelete", false, { ids: [id], transaction }, options);
1880
- const db = this.dbFor(transaction);
1881
- let updateQuery = db
1882
- .update(this.config.table)
1883
- .set(this.softDeleteSet(new Date()))
1884
- .where(this.writeWhere([id], options));
1885
- if (this.config.sql?.useReturning)
1886
- updateQuery = updateQuery.returning();
1887
- const result = await this.runRead(updateQuery);
1888
- const success = this.wasAffected(result);
1889
- if (!success && options?.expectedVersion != null) {
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
- if (success && !hooks.skipAfter)
1893
- await this.afterSoftDelete(id);
1894
- if (success) {
1895
- await this.emitEvent("after", "softDelete", false, { ids: [id], transaction }, options);
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
- return success;
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 { transaction, hooks = {} } = options || {};
1912
- const existing = await this.findForGuard(id, {
1913
- ...options,
1914
- withDeleted: false,
1915
- onlyDeleted: true,
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
- if (!existing)
1918
- throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
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
- const { transaction, hooks = {} } = options || {};
1938
- const existing = await this.findForGuard(id, options);
1939
- if (!existing)
1940
- throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
1941
- if (!hooks.skipBefore)
1942
- await this.beforeDelete(id);
1943
- await this.emitEvent("before", "delete", false, { ids: [id], transaction }, options);
1944
- const db = this.dbFor(transaction);
1945
- const deleteQuery = db
1946
- .delete(this.config.table)
1947
- .where(this.writeWhere([id], options));
1948
- try {
1949
- const result = await deleteQuery;
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.checkReadOptions(options);
2168
+ options = this.withScopeMemo(options);
1999
2169
  const message = "Mass upsert errors";
2000
- return this.executeSqlTransaction(async (tx) => {
2001
- const opts = { ...options, transaction: tx };
2002
- const rows = await this.eachItem(data.map((item, index) => ({
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
- })), message);
2006
- if (rows.length === 0)
2007
- return [];
2008
- await this.emitEvent("before", "upsert", true, { input: data, transaction: tx }, options);
2009
- await this.assertBatchKeysInScope(tx, rows, rows.map((_, index) => ({ index })), message, opts);
2010
- const written = await this.runSet(tx, message, (sp) => this.upsertRows(sp, rows, upsert, opts), rows.map((row, index) => ({
2011
- index,
2012
- run: (sp) => this.upsertRows(sp, [row], upsert, opts),
2013
- })), "write");
2014
- if (written.length < rows.length) {
2015
- const keys = this.conflictKeys(upsert);
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
- }, options?.transaction);
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.massCreatePerItem(data, options);
2345
+ return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
2132
2346
  }
2133
- this.checkReadOptions(options);
2134
- return this.executeSqlTransaction(async (tx) => {
2135
- const opts = { ...options, transaction: tx };
2136
- const rows = await this.eachItem(data.map((item, index) => ({
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
- })), "Mass create errors");
2140
- if (rows.length === 0)
2141
- return [];
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
- if (!options?.hooks?.skipAfter) {
2362
+ })), "write"),
2363
+ after: async (created) => {
2149
2364
  for (const entity of created)
2150
2365
  await this.afterCreate(entity);
2151
- }
2152
- await this.emitEvent("after", "create", true, {
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 created = [];
2172
- for (const row of rows)
2173
- created.push(await this.insertOne(db, row, options));
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.massUpdatePerItem(ids, data, options);
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
- return this.executeSqlTransaction(async (tx) => {
2196
- const opts = { ...options, transaction: tx };
2197
- const hooks = options?.hooks ?? {};
2198
- const unique = this.uniqueIds(ids);
2199
- if (unique.length === 0)
2200
- return [];
2201
- await this.assertAllExist(tx, unique, opts, message);
2202
- const sets = await this.eachItem(unique.map((id) => ({
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 = hooks.skipBefore
2433
+ const processed = skipBefore
2207
2434
  ? data
2208
2435
  : await this.beforeUpdate(id, data);
2209
2436
  return this.prepareUpdate(processed, opts);
2210
2437
  },
2211
- })), message);
2212
- await this.emitEvent("before", "update", true, { ids: unique, input: data, transaction: tx }, options);
2213
- await this.assertBatchKeysInScope(tx, sets, unique.map((id) => ({ id })), message, opts);
2214
- const now = new Date();
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
- await this.emitEvent("after", "update", true, { ids: unique, input: data, rows: updated, transaction: tx }, options);
2240
- await this.loadWriteRelations(tx, updated, options);
2241
- return this.inIdOrder(updated, ids);
2242
- }, options?.transaction);
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
- return this.massSoftDeletePerItem(ids, options);
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
- return this.executeSqlTransaction(async (tx) => {
2263
- const opts = {
2264
- ...options,
2265
- transaction: tx,
2266
- withDeleted: false,
2267
- onlyDeleted: false,
2268
- };
2269
- const hooks = options?.hooks ?? {};
2270
- const unique = this.uniqueIds(ids);
2271
- if (unique.length === 0)
2272
- return true;
2273
- await this.assertAllExist(tx, unique, opts, message);
2274
- if (!hooks.skipBefore) {
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([id], opts)),
2288
- })), "write");
2289
- this.assertAllWritten(unique, written, message, options);
2290
- if (!hooks.skipAfter) {
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
- await this.emitEvent("after", "softDelete", true, { ids: unique, transaction: tx }, options);
2295
- return true;
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.massRestorePerItem(ids, options);
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.executeSqlTransaction(async (tx) => {
2307
- const opts = {
2308
- ...options,
2309
- transaction: tx,
2310
- withDeleted: true,
2311
- onlyDeleted: false,
2312
- };
2313
- const hooks = options?.hooks ?? {};
2314
- const unique = this.uniqueIds(ids);
2315
- if (unique.length === 0)
2316
- return [];
2317
- await this.assertAllExist(tx, unique, opts, message);
2318
- if (!hooks.skipBefore) {
2319
- await this.eachItem(unique.map((id) => ({ id, run: () => this.beforeRestore(id) })), message);
2320
- }
2321
- await this.emitEvent("before", "restore", true, { ids: unique, transaction: tx }, options);
2322
- const set = this.softDeleteSet(null);
2323
- const restored = await this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts), unique.map((id) => ({
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
- await this.emitEvent("after", "restore", true, { ids: unique, rows: restored, transaction: tx }, options);
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
- return this.massDeletePerItem(ids, options);
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
- return this.executeSqlTransaction(async (tx) => {
2343
- const opts = { ...options, transaction: tx };
2344
- const hooks = options?.hooks ?? {};
2345
- const unique = this.uniqueIds(ids);
2346
- if (unique.length === 0)
2347
- return true;
2348
- await this.assertAllExist(tx, unique, opts, message);
2349
- if (!hooks.skipBefore) {
2350
- await this.eachItem(unique.map((id) => ({ id, run: () => this.beforeDelete(id) })), message);
2351
- }
2352
- await this.emitEvent("before", "delete", true, { ids: unique, transaction: tx }, options);
2353
- const written = await this.runSet(tx, message, (sp) => this.runKeyed(sp.delete(this.config.table).where(this.writeWhere(unique, opts)), unique), unique.map((id) => ({
2354
- id,
2355
- run: (sp) => sp.delete(this.config.table).where(this.writeWhere([id], opts)),
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
- await this.emitEvent("after", "delete", true, { ids: unique, transaction: tx }, options);
2363
- return true;
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
- async massCreatePerItem(data, options) {
2487
- return this.executeSqlTransaction((tx) => this.eachItem(data.map((item, index) => ({
2488
- index,
2489
- run: () => this.savepoint(tx, (sp) => this.create(item, { ...options, transaction: sp })),
2490
- })), "Mass create errors"), options?.transaction);
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 parseInt(result[0]?.count?.toString() || "0", 10);
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 relations = this.config.relations || {};
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
- const names = this.countNames(counts);
2607
- if (names.length === 0 || !Array.isArray(rows))
2814
+ if (this.countNames(counts).length === 0 || !Array.isArray(rows))
2608
2815
  return;
2609
- const relations = this.config.relations || {};
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, ids, name, filter, options));
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
- const joinForeign = relation.through[relation.throughForeignKey];
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
- return this.config.db.transaction(run);
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 = parseInt(totalResult[0]?.count?.toString() || "0", 10);
3339
+ const total = this.parseCount(totalResult);
3132
3340
  const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
3133
- await Promise.all([
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
  }