@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 values = typeof scope === "function"
189
- ? scope(options?.context ?? crud_context_1.CrudContext.current() ?? {})
190
- : scope;
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 = 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;
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 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())
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)(...conditions));
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 = 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] = [];
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 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;
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 = 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] = [];
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 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;
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 relations = this.config.relations || {};
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) => 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) ` || `);
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 Promise.all([
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: parseInt(totalResult[0]?.count?.toString() || "0", 10),
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.forEach((spec, i) => {
1307
- fields[`${CURSOR_KEY}${i}`] = this.cursorField(spec.column);
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)(plan.sort.map((_, i) => last[`${CURSOR_KEY}${i}`]), signature)
1424
+ ? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
1327
1425
  : null;
1328
1426
  for (const row of page) {
1329
- plan.sort.forEach((_, i) => {
1330
- delete row[`${CURSOR_KEY}${i}`];
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 Promise.all([
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 = parseInt(totalResult?.[0]?.count?.toString() || "0", 10);
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
- return branches.length > 0 ? (0, drizzle_orm_1.or)(...branches) : (0, drizzle_orm_1.sql) `false`;
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 postgres = this.config.dialect === "postgresql";
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 ? past : (0, drizzle_orm_1.or)(past, (0, drizzle_orm_1.isNull)(spec.column));
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 Promise.all([
1411
- this.loadRelations(db, hosts, nodes, undefined, undefined, options),
1412
- this.loadCounts(db, hosts, options?.counts, options),
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(this.relatedRow(relation.table, value, options))
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(fresh.map((item) => this.relatedRow(relation.table, item, options)))
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
- for (const [name, relation] of Object.entries(this.config.relations ?? {})) {
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
- const found = await db
1616
- .select({ id: referenced })
1617
- .from(relation.table)
1618
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(referenced, keys), ...scope));
1619
- const present = new Set(found.map((row) => this.valueKey(row.id)));
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
- missingParentException(localKey) {
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(this.config.table, localKey)?.name ?? localKey)
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 present = new Set(found.map((row) => this.valueKey(row.id)));
1664
- const missing = wanted.filter((id) => !present.has(this.valueKey(id)));
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 = 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)));
1676
- const already = new Set(linked.map((row) => this.valueKey(row.id)));
1677
- const add = wanted.filter((id) => !already.has(this.valueKey(id)));
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 relations = this.config.relations || {};
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 parent = await this.find(id, {
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
- const row = (await this.find(id, {
1754
- ...opts,
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
- return String(value).toLowerCase();
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 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);
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(this.writeWhere(ids, options));
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 softDelete(id, options) {
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
- 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);
2065
+ if (!this.skipsGuard(options, ...(hooks.skipBefore ? [] : [step.hook]))) {
2066
+ await this.guardRow(id, step.state);
2067
+ }
1877
2068
  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) {
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
- if (success && !hooks.skipAfter)
1893
- await this.afterSoftDelete(id);
1894
- if (success) {
1895
- await this.emitEvent("after", "softDelete", false, { ids: [id], transaction }, options);
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
- return success;
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 { transaction, hooks = {} } = options || {};
1912
- const existing = await this.findForGuard(id, {
1913
- ...options,
1914
- withDeleted: false,
1915
- onlyDeleted: true,
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
- 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;
2152
+ await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
2153
+ return restored;
1935
2154
  }
1936
2155
  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
- }
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.checkReadOptions(options);
2204
+ options = this.withScopeMemo(options);
1999
2205
  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) => ({
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
- })), 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);
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
- }, options?.transaction);
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.massCreatePerItem(data, options);
2383
+ return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
2132
2384
  }
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) => ({
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
- })), "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) => ({
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
- if (!options?.hooks?.skipAfter) {
2400
+ })), "write"),
2401
+ after: async (created) => {
2149
2402
  for (const entity of created)
2150
2403
  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);
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 created = [];
2172
- for (const row of rows)
2173
- created.push(await this.insertOne(db, row, options));
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.massUpdatePerItem(ids, data, options);
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
- 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) => ({
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 = hooks.skipBefore
2471
+ const processed = skipBefore
2207
2472
  ? data
2208
2473
  : await this.beforeUpdate(id, data);
2209
2474
  return this.prepareUpdate(processed, opts);
2210
2475
  },
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) {
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
- 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);
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
- return this.massSoftDeletePerItem(ids, options);
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
- 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
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([id], opts)),
2288
- })), "write");
2289
- this.assertAllWritten(unique, written, message, options);
2290
- if (!hooks.skipAfter) {
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
- await this.emitEvent("after", "softDelete", true, { ids: unique, transaction: tx }, options);
2295
- return true;
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.massRestorePerItem(ids, options);
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.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) {
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
- 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);
2585
+ },
2586
+ });
2336
2587
  }
2337
2588
  async massDelete(ids, options) {
2589
+ options = this.withScopeMemo(options);
2338
2590
  if (options?.bulkMode === "perItem") {
2339
- return this.massDeletePerItem(ids, options);
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
- 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) {
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
- await this.emitEvent("after", "delete", true, { ids: unique, transaction: tx }, options);
2363
- return true;
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) => typeof v === "bigint" ? `${v}n` : 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
- 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;
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 parseInt(result[0]?.count?.toString() || "0", 10);
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 relations = this.config.relations || {};
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
- const names = this.countNames(counts);
2607
- if (names.length === 0 || !Array.isArray(rows))
2869
+ if (this.countNames(counts).length === 0 || !Array.isArray(rows))
2608
2870
  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
- });
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, ids, name, filter, options));
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
- 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))
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
- return this.config.db.transaction(run);
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 = parseInt(totalResult[0]?.count?.toString() || "0", 10);
3394
+ const total = this.parseCount(totalResult);
3132
3395
  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
- ]);
3396
+ await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
3137
3397
  return { data: rows, total, page, limit };
3138
3398
  }
3139
3399
  }