@avytrix/nestjs-drizzle-crud 3.8.1 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,8 @@ const query_parser_1 = require("../utils/query-parser");
11
11
  const JOIN_KEY = "__crudJoinKey";
12
12
  const CURSOR_KEY = "__crudCursor";
13
13
  const SCOPE_MEMO = Symbol("scopeMemo");
14
- let fingerprintTag = 0;
14
+ const fingerprintTags = new WeakMap();
15
+ let fingerprintCount = 0;
15
16
  const ROLLBACK = Symbol("rollback");
16
17
  class DeferredDiagnosis {
17
18
  settle;
@@ -128,15 +129,38 @@ class SqlBaseCrudService {
128
129
  return /(Text|VarChar|Varchar|Char)$/.test(column?.columnType ?? "");
129
130
  }
130
131
  coerceFilterValue(column, value) {
131
- if (column?.dataType !== "date" || value instanceof Date)
132
+ if (column?.dataType !== "date")
132
133
  return value;
133
- if (typeof value === "string" || typeof value === "number") {
134
- const date = new Date(value);
135
- if (!Number.isNaN(date.getTime()))
136
- return date;
137
- }
134
+ const date = this.toDate(value);
135
+ if (date)
136
+ return date;
138
137
  throw new common_1.BadRequestException(`Invalid date filter value: ${value}`);
139
138
  }
139
+ toDate(value) {
140
+ const date = value instanceof Date
141
+ ? value
142
+ : typeof value === "string" || typeof value === "number"
143
+ ? new Date(value)
144
+ : undefined;
145
+ return date && !Number.isNaN(date.getTime()) ? date : undefined;
146
+ }
147
+ coerceDates(table, row) {
148
+ for (const [key, value] of Object.entries(row)) {
149
+ const column = this.columnOf(table, key);
150
+ if (column?.dataType !== "date" ||
151
+ !(typeof value === "string" ||
152
+ typeof value === "number" ||
153
+ value instanceof Date)) {
154
+ continue;
155
+ }
156
+ const date = this.toDate(value);
157
+ if (!date) {
158
+ throw new crud_exceptions_1.ValidationFailedException(`a date or time value has an invalid format: ${column.name ?? key}`);
159
+ }
160
+ row[key] = date;
161
+ }
162
+ return row;
163
+ }
140
164
  static ALLOWLIST_LABELS = {
141
165
  filterableColumns: "Filtering on",
142
166
  sortableColumns: "Sorting by",
@@ -194,7 +218,8 @@ class SqlBaseCrudService {
194
218
  if (!scope || options?.skipScope)
195
219
  return null;
196
220
  const context = options?.context ?? crud_context_1.CrudContext.current();
197
- const memo = options?.[SCOPE_MEMO];
221
+ const found = options?.[SCOPE_MEMO];
222
+ const memo = found?.ended ? undefined : found;
198
223
  const mine = memo?.get(this);
199
224
  if (mine?.has(context))
200
225
  return mine.get(context);
@@ -207,12 +232,23 @@ class SqlBaseCrudService {
207
232
  return checked;
208
233
  }
209
234
  withScopeMemo(options) {
210
- if (typeof this.config.scope !== "function" ||
211
- options?.[SCOPE_MEMO]) {
235
+ const memo = options?.[SCOPE_MEMO];
236
+ if (typeof this.config.scope !== "function" || (memo && !memo.ended)) {
212
237
  return options;
213
238
  }
214
239
  return { ...options, [SCOPE_MEMO]: new Map() };
215
240
  }
241
+ async scopedCall(options, run) {
242
+ const scoped = this.withScopeMemo(options);
243
+ const started = scoped === options ? undefined : scoped[SCOPE_MEMO];
244
+ try {
245
+ return await run(scoped);
246
+ }
247
+ finally {
248
+ if (started)
249
+ started.ended = true;
250
+ }
251
+ }
216
252
  checkScope(values) {
217
253
  if (values === null)
218
254
  return null;
@@ -330,8 +366,9 @@ class SqlBaseCrudService {
330
366
  query = dialect.sqlToQuery(...args);
331
367
  }
332
368
  catch (error) {
333
- if (error instanceof RangeError)
369
+ if (error instanceof RangeError && /call stack/i.test(error.message)) {
334
370
  throw tooMany(`more than ${limit}`);
371
+ }
335
372
  throw error;
336
373
  }
337
374
  const count = query?.params?.length ?? 0;
@@ -1247,14 +1284,16 @@ class SqlBaseCrudService {
1247
1284
  };
1248
1285
  }
1249
1286
  async find(id, options) {
1250
- options = this.withScopeMemo(options);
1251
- this.checkReadOptions(options);
1252
- return this.findFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], options);
1287
+ return this.scopedCall(options, async (options) => {
1288
+ this.checkReadOptions(options);
1289
+ return this.findFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], options);
1290
+ });
1253
1291
  }
1254
1292
  async findOne(where, options) {
1255
- options = this.withScopeMemo(options);
1256
- this.checkReadOptions(options);
1257
- return this.findFirst(this.buildWhereConditionsFromPartial(where), options);
1293
+ return this.scopedCall(options, async (options) => {
1294
+ this.checkReadOptions(options);
1295
+ return this.findFirst(this.buildWhereConditionsFromPartial(where), options);
1296
+ });
1258
1297
  }
1259
1298
  async findFirst(conditions, options) {
1260
1299
  const first = await this.readFirst(conditions, options);
@@ -1335,105 +1374,107 @@ class SqlBaseCrudService {
1335
1374
  return query;
1336
1375
  }
1337
1376
  async findAll(filters, pagination, options) {
1338
- options = this.withScopeMemo(options);
1339
- const { sortBy, sortOrder = "asc" } = pagination || {};
1340
- const withCount = pagination?.withCount !== false;
1341
- const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
1342
- const plan = this.planList(filters, sortBy, sortOrder, options);
1343
- let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, plan.joinRelations.length > 0, plan.selection);
1344
- dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1345
- if (plan.conditions.length > 0) {
1346
- dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...plan.conditions));
1347
- }
1348
- if (plan.sort.length > 0) {
1349
- dataQuery = dataQuery.orderBy(...plan.sort.map((spec) => this.orderOf(spec)));
1350
- }
1351
- dataQuery = dataQuery
1352
- .limit(withCount ? safeLimit : safeLimit + 1)
1353
- .offset(offset);
1354
- dataQuery = this.applyLock(dataQuery, options);
1355
- const countQuery = withCount
1356
- ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1357
- : Promise.resolve(undefined);
1358
- const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
1359
- const data = fetched.slice(0, safeLimit);
1360
- const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
1361
- await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
1362
- if (!withCount) {
1377
+ return this.scopedCall(options, async (options) => {
1378
+ const { sortBy, sortOrder = "asc" } = pagination || {};
1379
+ const withCount = pagination?.withCount !== false;
1380
+ const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
1381
+ const plan = this.planList(filters, sortBy, sortOrder, options);
1382
+ let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, plan.joinRelations.length > 0, plan.selection);
1383
+ dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1384
+ if (plan.conditions.length > 0) {
1385
+ dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...plan.conditions));
1386
+ }
1387
+ if (plan.sort.length > 0) {
1388
+ dataQuery = dataQuery.orderBy(...plan.sort.map((spec) => this.orderOf(spec)));
1389
+ }
1390
+ dataQuery = dataQuery
1391
+ .limit(withCount ? safeLimit : safeLimit + 1)
1392
+ .offset(offset);
1393
+ dataQuery = this.applyLock(dataQuery, options);
1394
+ const countQuery = withCount
1395
+ ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1396
+ : Promise.resolve(undefined);
1397
+ const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
1398
+ const data = fetched.slice(0, safeLimit);
1399
+ const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
1400
+ await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
1401
+ if (!withCount) {
1402
+ return {
1403
+ data: rows,
1404
+ total: null,
1405
+ page,
1406
+ limit: safeLimit,
1407
+ hasMore: fetched.length > safeLimit,
1408
+ };
1409
+ }
1363
1410
  return {
1364
1411
  data: rows,
1365
- total: null,
1412
+ total: this.parseCount(totalResult),
1366
1413
  page,
1367
1414
  limit: safeLimit,
1368
- hasMore: fetched.length > safeLimit,
1369
1415
  };
1370
- }
1371
- return {
1372
- data: rows,
1373
- total: this.parseCount(totalResult),
1374
- page,
1375
- limit: safeLimit,
1376
- };
1416
+ });
1377
1417
  }
1378
1418
  async findByCursor(filters, pagination, options) {
1379
- options = this.withScopeMemo(options);
1380
- const { sortBy, sortOrder = "asc", count = false } = pagination || {};
1381
- const { limit } = this.resolvePagination(1, pagination?.limit);
1382
- const plan = this.planList(filters, sortBy, sortOrder, options);
1383
- const signature = plan.sort
1384
- .map((spec) => `${spec.key}:${spec.order}`)
1385
- .join(",");
1386
- const cursor = pagination?.cursor ?? pagination?.after;
1387
- const conditions = [...plan.conditions];
1388
- if (cursor !== undefined && cursor !== null && cursor !== "") {
1389
- if (typeof cursor !== "string") {
1390
- throw new common_1.BadRequestException("Invalid cursor");
1391
- }
1392
- const values = (0, cursor_1.decodeCursor)(cursor, signature, plan.sort.length);
1393
- conditions.push(this.keysetCondition(plan.sort, values));
1394
- }
1395
- const fields = this.buildSelection(plan.baseSelect, plan.joined, plan.selection);
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}`;
1419
+ return this.scopedCall(options, async (options) => {
1420
+ const { sortBy, sortOrder = "asc", count = false } = pagination || {};
1421
+ const { limit } = this.resolvePagination(1, pagination?.limit);
1422
+ const plan = this.planList(filters, sortBy, sortOrder, options);
1423
+ const signature = plan.sort
1424
+ .map((spec) => `${spec.key}:${spec.order}`)
1425
+ .join(",");
1426
+ const cursor = pagination?.cursor ?? pagination?.after;
1427
+ const conditions = [...plan.conditions];
1428
+ if (cursor !== undefined && cursor !== null && cursor !== "") {
1429
+ if (typeof cursor !== "string") {
1430
+ throw new common_1.BadRequestException("Invalid cursor");
1431
+ }
1432
+ const values = (0, cursor_1.decodeCursor)(cursor, signature, plan.sort.length);
1433
+ conditions.push(this.keysetCondition(plan.sort, values));
1434
+ }
1435
+ const fields = this.buildSelection(plan.baseSelect, plan.joined, plan.selection);
1436
+ const cursorKeys = plan.sort.map((spec, i) => {
1437
+ const field = this.cursorField(spec.column);
1438
+ const projected = field === spec.column && !spec.relation
1439
+ ? Object.keys(fields).find((key) => fields[key] === spec.column)
1440
+ : undefined;
1441
+ if (projected)
1442
+ return projected;
1443
+ fields[`${CURSOR_KEY}${i}`] = field;
1444
+ return `${CURSOR_KEY}${i}`;
1445
+ });
1446
+ const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
1447
+ let dataQuery = plan.db.select(fields).from(this.config.table);
1448
+ dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1449
+ if (conditions.length > 0)
1450
+ dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
1451
+ dataQuery = dataQuery
1452
+ .orderBy(...plan.sort.map((spec) => this.orderOf(spec)))
1453
+ .limit(limit + 1);
1454
+ dataQuery = this.applyLock(dataQuery, options);
1455
+ const [data, totalResult] = await this.runRead(Promise.all([
1456
+ dataQuery,
1457
+ count
1458
+ ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1459
+ : Promise.resolve(undefined),
1460
+ ]));
1461
+ const page = data.slice(0, limit);
1462
+ const last = page[page.length - 1];
1463
+ const nextCursor = data.length > limit && last
1464
+ ? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
1465
+ : null;
1466
+ for (const row of page) {
1467
+ for (const key of hidden)
1468
+ delete row[key];
1469
+ }
1470
+ const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
1471
+ await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
1472
+ const out = { data: rows, limit, nextCursor };
1473
+ if (count) {
1474
+ out.total = this.parseCount(totalResult);
1475
+ }
1476
+ return out;
1405
1477
  });
1406
- const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
1407
- let dataQuery = plan.db.select(fields).from(this.config.table);
1408
- dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1409
- if (conditions.length > 0)
1410
- dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
1411
- dataQuery = dataQuery
1412
- .orderBy(...plan.sort.map((spec) => this.orderOf(spec)))
1413
- .limit(limit + 1);
1414
- dataQuery = this.applyLock(dataQuery, options);
1415
- const [data, totalResult] = await this.runRead(Promise.all([
1416
- dataQuery,
1417
- count
1418
- ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1419
- : Promise.resolve(undefined),
1420
- ]));
1421
- const page = data.slice(0, limit);
1422
- const last = page[page.length - 1];
1423
- const nextCursor = data.length > limit && last
1424
- ? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
1425
- : null;
1426
- for (const row of page) {
1427
- for (const key of hidden)
1428
- delete row[key];
1429
- }
1430
- const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
1431
- await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
1432
- const out = { data: rows, limit, nextCursor };
1433
- if (count) {
1434
- out.total = this.parseCount(totalResult);
1435
- }
1436
- return out;
1437
1478
  }
1438
1479
  cursorField(column) {
1439
1480
  if (column?.dataType !== "date")
@@ -1564,10 +1605,13 @@ class SqlBaseCrudService {
1564
1605
  }
1565
1606
  }
1566
1607
  async writeThenLoad(operation, options, write) {
1567
- 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);
1608
+ let late = this.loadsAfterCommit(operation, options);
1609
+ const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written, inside = false) => {
1610
+ if (late && !inside)
1611
+ return Promise.resolve();
1612
+ late = false;
1613
+ return this.loadWriteRelations(inTx, written, options);
1614
+ }), options?.transaction);
1571
1615
  if (late)
1572
1616
  await this.loadWriteRelations(this.dbFor(), rows, options);
1573
1617
  return rows;
@@ -1596,7 +1640,7 @@ class SqlBaseCrudService {
1596
1640
  const version = this.config.versionColumn;
1597
1641
  if (version)
1598
1642
  delete mapped[version];
1599
- const entityData = this.filterWritable(mapped, "create", options);
1643
+ const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "create", options));
1600
1644
  const now = new Date();
1601
1645
  if (this.config.timestamps?.createdAt)
1602
1646
  entityData[this.config.timestamps.createdAt] = now;
@@ -1626,35 +1670,36 @@ class SqlBaseCrudService {
1626
1670
  return createdEntity;
1627
1671
  }
1628
1672
  async create(data, options) {
1629
- options = this.withScopeMemo(options);
1630
- const { transaction, hooks = {} } = options || {};
1631
- this.checkReadOptions(options);
1632
- const entityData = await this.prepareCreate(data, options);
1633
- const nested = this.takeNested(entityData, options);
1634
- const db = this.dbFor(transaction);
1635
- await this.emitEvent("before", "create", false, { input: data, transaction }, options);
1636
- try {
1637
- await this.assertKeysInScope(db, entityData, options, nested.map((entry) => entry.name));
1638
- const createdEntity = nested.length === 0
1639
- ? await this.insertOne(db, entityData, options)
1640
- : await this.executeSqlTransaction((tx) => this.insertNested(tx, entityData, nested, {
1641
- ...options,
1642
- transaction: tx,
1643
- }), transaction);
1644
- if (!hooks.skipAfter)
1645
- await this.afterCreate(createdEntity);
1646
- await this.emitEvent("after", "create", false, {
1647
- ids: this.idsOf([createdEntity]),
1648
- input: data,
1649
- rows: [createdEntity],
1650
- transaction,
1651
- }, options);
1652
- await this.loadWriteRelations(db, [createdEntity], options);
1653
- return createdEntity;
1654
- }
1655
- catch (error) {
1656
- throw this.toClientError(error, "write");
1657
- }
1673
+ return this.scopedCall(options, async (options) => {
1674
+ const { transaction, hooks = {} } = options || {};
1675
+ this.checkReadOptions(options);
1676
+ const entityData = await this.prepareCreate(data, options);
1677
+ const nested = this.takeNested(entityData, options);
1678
+ const db = this.dbFor(transaction);
1679
+ await this.emitEvent("before", "create", false, { input: data, transaction }, options);
1680
+ try {
1681
+ await this.assertKeysInScope(db, entityData, options, nested.map((entry) => entry.name));
1682
+ const createdEntity = nested.length === 0
1683
+ ? await this.insertOne(db, entityData, options)
1684
+ : await this.executeSqlTransaction((tx) => this.insertNested(tx, entityData, nested, {
1685
+ ...options,
1686
+ transaction: tx,
1687
+ }), transaction);
1688
+ if (!hooks.skipAfter)
1689
+ await this.afterCreate(createdEntity);
1690
+ await this.emitEvent("after", "create", false, {
1691
+ ids: this.idsOf([createdEntity]),
1692
+ input: data,
1693
+ rows: [createdEntity],
1694
+ transaction,
1695
+ }, options);
1696
+ await this.loadWriteRelations(db, [createdEntity], options);
1697
+ return createdEntity;
1698
+ }
1699
+ catch (error) {
1700
+ throw this.toClientError(error, "write");
1701
+ }
1702
+ });
1658
1703
  }
1659
1704
  takeNested(entity, options) {
1660
1705
  const allowed = this.config.writableRelations;
@@ -1760,7 +1805,7 @@ class SqlBaseCrudService {
1760
1805
  if (this.columnOf(table, key))
1761
1806
  out[key] = item;
1762
1807
  }
1763
- return out;
1808
+ return this.coerceDates(table, out);
1764
1809
  }
1765
1810
  async keysOutsideScope(db, rows, options, skip = [], owner = {
1766
1811
  table: this.config.table,
@@ -1779,26 +1824,29 @@ class SqlBaseCrudService {
1779
1824
  const referenced = relation.table[relation.references ?? "id"];
1780
1825
  if (scope.length === 0 || !referenced)
1781
1826
  continue;
1782
- const keys = this.uniqueValues(rows.map((row) => row[local]).filter((value) => value != null), referenced);
1827
+ const keys = this.distinct(rows.map((row) => row[local]).filter((value) => value != null));
1783
1828
  if (keys.length === 0)
1784
1829
  continue;
1785
1830
  checks.push({
1786
1831
  local,
1787
- 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)),
1832
+ keys,
1833
+ query: this.matchKeys(db, relation.table, referenced, keys, scope),
1792
1834
  });
1793
1835
  }
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)));
1836
+ let found;
1837
+ try {
1838
+ found = await Promise.all(checks.map((check) => check.query));
1839
+ }
1840
+ catch (error) {
1841
+ throw this.toClientError(error, "write");
1842
+ }
1843
+ checks.forEach(({ local, keys }, i) => {
1844
+ const matched = found[i];
1845
+ const present = new Set(keys.filter((_, at) => matched.has(at)));
1797
1846
  rows.forEach((row, index) => {
1798
1847
  const value = row[local];
1799
- if (value == null || present.has(this.valueKey(value, referenced))) {
1848
+ if (value == null || present.has(value))
1800
1849
  return;
1801
- }
1802
1850
  if (!failures.has(index)) {
1803
1851
  failures.set(index, this.missingParentException(local, owner.table));
1804
1852
  }
@@ -1838,21 +1886,14 @@ class SqlBaseCrudService {
1838
1886
  async linkTargets(tx, relation, parentValue, ids, options, fresh = false) {
1839
1887
  const target = relation.table;
1840
1888
  const referenced = target[relation.references ?? "id"];
1841
- const wanted = this.uniqueValues(ids, referenced);
1889
+ const wanted = this.distinct(ids);
1842
1890
  if (wanted.length === 0)
1843
1891
  return;
1844
1892
  const through = relation.through;
1845
1893
  const local = through[relation.throughLocalKey];
1846
1894
  const foreign = through[relation.throughForeignKey];
1847
- const conditions = [(0, drizzle_orm_1.inArray)(referenced, wanted)];
1848
- conditions.push(...this.relatedRowScope(target, options));
1849
- const found = await tx
1850
- .select({ id: referenced })
1851
- .from(target)
1852
- .where((0, drizzle_orm_1.and)(...conditions));
1853
- const key = (value) => this.valueKey(value, referenced);
1854
- const present = new Set(found.map((row) => key(row.id)));
1855
- const missing = wanted.filter((id) => !present.has(key(id)));
1895
+ const found = await this.matchKeys(tx, target, referenced, wanted, this.relatedRowScope(target, options));
1896
+ const missing = wanted.filter((_, at) => !found.has(at));
1856
1897
  if (missing.length > 0) {
1857
1898
  const name = this.tableName(target);
1858
1899
  throw new crud_exceptions_1.BulkOperationException("Unknown related ids", missing.map((id) => ({
@@ -1860,14 +1901,13 @@ class SqlBaseCrudService {
1860
1901
  error: new crud_exceptions_1.EntityNotFoundException(name, id),
1861
1902
  })));
1862
1903
  }
1904
+ const stored = this.distinct(wanted.map((_, at) => found.get(at)));
1863
1905
  const linked = fresh
1864
- ? []
1865
- : await tx
1866
- .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)));
1906
+ ? new Map()
1907
+ : await this.matchKeys(tx, through, foreign, stored, [
1908
+ (0, drizzle_orm_1.eq)(local, parentValue),
1909
+ ]);
1910
+ const add = stored.filter((_, at) => !linked.has(at));
1871
1911
  if (add.length > 0) {
1872
1912
  await tx.insert(through).values(add.map((id) => ({
1873
1913
  [relation.throughLocalKey]: parentValue,
@@ -1876,16 +1916,19 @@ class SqlBaseCrudService {
1876
1916
  }
1877
1917
  }
1878
1918
  async attach(id, relation, relatedIds, options) {
1879
- options = this.withScopeMemo(options);
1880
- return this.changeLinks("attach", id, relation, relatedIds, options);
1919
+ return this.scopedCall(options, async (options) => {
1920
+ return this.changeLinks("attach", id, relation, relatedIds, options);
1921
+ });
1881
1922
  }
1882
1923
  async detach(id, relation, relatedIds, options) {
1883
- options = this.withScopeMemo(options);
1884
- return this.changeLinks("detach", id, relation, relatedIds, options);
1924
+ return this.scopedCall(options, async (options) => {
1925
+ return this.changeLinks("detach", id, relation, relatedIds, options);
1926
+ });
1885
1927
  }
1886
1928
  async sync(id, relation, relatedIds, options) {
1887
- options = this.withScopeMemo(options);
1888
- return this.changeLinks("sync", id, relation, relatedIds, options);
1929
+ return this.scopedCall(options, async (options) => {
1930
+ return this.changeLinks("sync", id, relation, relatedIds, options);
1931
+ });
1889
1932
  }
1890
1933
  async changeLinks(operation, id, name, relatedIds, options) {
1891
1934
  const relation = this.relationNamed(name);
@@ -1905,7 +1948,7 @@ class SqlBaseCrudService {
1905
1948
  throw new common_1.BadRequestException(`${operation} takes an array of ids`);
1906
1949
  }
1907
1950
  this.checkReadOptions(options);
1908
- const ids = this.uniqueValues(relatedIds ?? [], relation.table[relation.references ?? "id"]);
1951
+ const ids = this.distinct(relatedIds ?? []);
1909
1952
  const parentKey = this.parentKeyOf(relation);
1910
1953
  return this.executeSqlTransaction(async (tx) => {
1911
1954
  const opts = { ...options, transaction: tx };
@@ -1955,15 +1998,64 @@ class SqlBaseCrudService {
1955
1998
  ? key.toLowerCase()
1956
1999
  : key;
1957
2000
  }
1958
- uniqueValues(values, column) {
1959
- const seen = new Set();
1960
- return values.filter((value) => {
1961
- const key = this.valueKey(value, column);
1962
- if (seen.has(key))
1963
- return false;
1964
- seen.add(key);
1965
- return true;
1966
- });
2001
+ distinct(values) {
2002
+ return Array.from(new Set(values));
2003
+ }
2004
+ async matchKeys(db, table, column, values, conditions) {
2005
+ const input = this.keyInput(column, values);
2006
+ const rows = await db
2007
+ .select({ at: input.at, key: column })
2008
+ .from(table)
2009
+ .innerJoin(input.list, (0, drizzle_orm_1.eq)(column, input.value))
2010
+ .where((0, drizzle_orm_1.and)(...conditions));
2011
+ return new Map(rows.map((row) => [Number(row.at), row.key]));
2012
+ }
2013
+ keyInput(column, values) {
2014
+ const params = values.map((value) => drizzle_orm_1.sql.param(value, column));
2015
+ if (this.config.dialect === "mysql") {
2016
+ const rows = values.map((_, at) => (0, drizzle_orm_1.sql) `row(${drizzle_orm_1.sql.raw(String(at))})`);
2017
+ const picked = (0, drizzle_orm_1.sql) `elt(input.column_0 + 1, ${drizzle_orm_1.sql.join(params, (0, drizzle_orm_1.sql) `, `)})`;
2018
+ const cast = this.mysqlKeyCast(column);
2019
+ return {
2020
+ list: (0, drizzle_orm_1.sql) `(values ${drizzle_orm_1.sql.join(rows, (0, drizzle_orm_1.sql) `, `)}) as input`,
2021
+ at: (0, drizzle_orm_1.sql) `input.column_0`,
2022
+ value: cast ? (0, drizzle_orm_1.sql) `cast(${picked} as ${drizzle_orm_1.sql.raw(cast)})` : picked,
2023
+ };
2024
+ }
2025
+ const type = this.config.dialect === "postgresql" && this.pgKeyType(column);
2026
+ const rows = params.map((param, at) => (0, drizzle_orm_1.sql) `(${drizzle_orm_1.sql.raw(String(at))}, ${type ? (0, drizzle_orm_1.sql) `cast(${param} as ${type})` : param})`);
2027
+ return {
2028
+ list: (0, drizzle_orm_1.sql) `(values ${drizzle_orm_1.sql.join(rows, (0, drizzle_orm_1.sql) `, `)}) as input`,
2029
+ at: (0, drizzle_orm_1.sql) `input.column1`,
2030
+ value: (0, drizzle_orm_1.sql) `input.column2`,
2031
+ };
2032
+ }
2033
+ mysqlKeyCast(column) {
2034
+ const type = String(column?.getSQLType?.() ?? "").toLowerCase();
2035
+ if (type === "serial")
2036
+ return "unsigned";
2037
+ if (/^(tiny|small|medium|big)?int\b/.test(type)) {
2038
+ return type.includes("unsigned") ? "unsigned" : "signed";
2039
+ }
2040
+ const decimal = type.match(/^decimal(\(\d+(?:,\d+)?\))?/);
2041
+ if (decimal)
2042
+ return `decimal${decimal[1] ?? ""}`;
2043
+ return undefined;
2044
+ }
2045
+ pgKeyType(column) {
2046
+ const pgEnum = column.enum;
2047
+ if (pgEnum?.enumName) {
2048
+ return pgEnum.schema
2049
+ ? (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(pgEnum.schema)}.${drizzle_orm_1.sql.identifier(pgEnum.enumName)}`
2050
+ : (0, drizzle_orm_1.sql) `${drizzle_orm_1.sql.identifier(pgEnum.enumName)}`;
2051
+ }
2052
+ const type = column.getSQLType();
2053
+ const serials = {
2054
+ serial: "integer",
2055
+ bigserial: "bigint",
2056
+ smallserial: "smallint",
2057
+ };
2058
+ return drizzle_orm_1.sql.raw(serials[type] ?? type);
1967
2059
  }
1968
2060
  tableName(table) {
1969
2061
  try {
@@ -1980,7 +2072,7 @@ class SqlBaseCrudService {
1980
2072
  for (const key of Object.keys(this.resolveScope(options) ?? {})) {
1981
2073
  delete mapped[key];
1982
2074
  }
1983
- const entityData = this.filterWritable(mapped, "update", options);
2075
+ const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "update", options));
1984
2076
  const settable = Object.keys(entityData).filter((key) => entityData[key] !== undefined &&
1985
2077
  !!this.columnOf(this.config.table, key));
1986
2078
  if (settable.length === 0) {
@@ -1989,45 +2081,49 @@ class SqlBaseCrudService {
1989
2081
  return entityData;
1990
2082
  }
1991
2083
  async update(id, data, options) {
1992
- options = this.withScopeMemo(options);
1993
- const { transaction, hooks = {} } = options || {};
1994
- this.checkReadOptions(options);
1995
- const expected = this.expectedVersionOf(data, options);
1996
- const opts = { ...options, expectedVersion: expected };
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
- });
2007
- if (this.config.timestamps?.updatedAt) {
2008
- entityData[this.config.timestamps.updatedAt] = new Date();
2009
- }
2010
- const db = this.dbFor(transaction);
2011
- await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
2012
- try {
2013
- await this.guardOnError(guarded, id, opts, () => this.assertKeysInScope(db, entityData, opts));
2014
- const [updatedEntity] = await this.guardOnError(guarded || !!transaction, id, opts, () => this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts, this.softDeleteScope(opts)));
2015
- if (!updatedEntity) {
2084
+ return this.scopedCall(options, async (options) => {
2085
+ const { transaction, hooks = {} } = options || {};
2086
+ this.checkReadOptions(options);
2087
+ const expected = this.expectedVersionOf(data, options);
2088
+ const opts = {
2089
+ ...options,
2090
+ expectedVersion: expected,
2091
+ };
2092
+ const guarded = !this.skipsGuard(options, "validateUpdate", "mapUpdateDtoToEntity", "prepareUpdate", ...(hooks.skipBefore ? [] : ["beforeUpdate"]));
2093
+ if (guarded)
2016
2094
  await this.guardRow(id, opts);
2017
- if (expected !== undefined) {
2018
- throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
2095
+ const entityData = await this.guardOnError(guarded, id, opts, async () => {
2096
+ await this.validateUpdate(id, data);
2097
+ const processedData = hooks.skipBefore
2098
+ ? data
2099
+ : await this.beforeUpdate(id, data);
2100
+ return this.prepareUpdate(processedData, opts);
2101
+ });
2102
+ if (this.config.timestamps?.updatedAt) {
2103
+ entityData[this.config.timestamps.updatedAt] = new Date();
2104
+ }
2105
+ const db = this.dbFor(transaction);
2106
+ await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
2107
+ try {
2108
+ await this.guardOnError(guarded || !!transaction, id, opts, () => this.assertKeysInScope(db, entityData, opts));
2109
+ const [updatedEntity] = await this.guardOnError(guarded || !!transaction, id, opts, () => this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts, this.softDeleteScope(opts)));
2110
+ if (!updatedEntity) {
2111
+ await this.guardRow(id, opts);
2112
+ if (expected !== undefined) {
2113
+ throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
2114
+ }
2115
+ throw new Error("Failed to update entity");
2019
2116
  }
2020
- throw new Error("Failed to update entity");
2117
+ if (!hooks.skipAfter)
2118
+ await this.afterUpdate(updatedEntity);
2119
+ await this.emitEvent("after", "update", false, { ids: [id], input: data, rows: [updatedEntity], transaction }, options);
2120
+ await this.loadWriteRelations(db, [updatedEntity], options);
2121
+ return updatedEntity;
2021
2122
  }
2022
- if (!hooks.skipAfter)
2023
- await this.afterUpdate(updatedEntity);
2024
- await this.emitEvent("after", "update", false, { ids: [id], input: data, rows: [updatedEntity], transaction }, options);
2025
- await this.loadWriteRelations(db, [updatedEntity], options);
2026
- return updatedEntity;
2027
- }
2028
- catch (error) {
2029
- throw this.toClientError(error, "write");
2030
- }
2123
+ catch (error) {
2124
+ throw this.toClientError(error, "write");
2125
+ }
2126
+ });
2031
2127
  }
2032
2128
  async updateWhereIn(db, set, ids, options, extra) {
2033
2129
  const where = this.writeWhere(ids, options);
@@ -2097,27 +2193,28 @@ class SqlBaseCrudService {
2097
2193
  return false;
2098
2194
  }
2099
2195
  async softDelete(id, options) {
2100
- options = this.withScopeMemo(options);
2101
- if (!this.config.softDelete?.enabled) {
2102
- throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
2103
- }
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),
2196
+ return this.scopedCall(options, async (options) => {
2197
+ if (!this.config.softDelete?.enabled) {
2198
+ throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
2199
+ }
2200
+ const live = { ...options, withDeleted: false, onlyDeleted: false };
2201
+ return this.guardedWrite(id, options, {
2202
+ operation: "softDelete",
2203
+ state: live,
2204
+ hook: "beforeSoftDelete",
2205
+ before: () => this.beforeSoftDelete(id),
2206
+ write: async (db) => {
2207
+ let query = db
2208
+ .update(this.config.table)
2209
+ .set(this.softDeleteSet(new Date()))
2210
+ .where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(live)));
2211
+ if (this.config.sql?.useReturning)
2212
+ query = query.returning();
2213
+ return this.wasAffected(await this.runRead(query)) || undefined;
2214
+ },
2215
+ miss: () => this.staleOrFalse(id, options),
2216
+ after: () => this.afterSoftDelete(id),
2217
+ });
2121
2218
  });
2122
2219
  }
2123
2220
  softDeleteSet(deletedAt) {
@@ -2129,42 +2226,44 @@ class SqlBaseCrudService {
2129
2226
  });
2130
2227
  }
2131
2228
  async restore(id, options) {
2132
- options = this.withScopeMemo(options);
2133
- if (!this.config.softDelete?.enabled) {
2134
- throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
2135
- }
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],
2229
+ return this.scopedCall(options, async (options) => {
2230
+ if (!this.config.softDelete?.enabled) {
2231
+ throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
2232
+ }
2233
+ const trashed = { ...options, withDeleted: false, onlyDeleted: true };
2234
+ const restored = await this.guardedWrite(id, options, {
2235
+ operation: "restore",
2236
+ state: trashed,
2237
+ hook: "beforeRestore",
2238
+ before: () => this.beforeRestore(id),
2239
+ write: async (db) => (await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options, this.softDeleteScope(trashed))))[0],
2240
+ miss: () => {
2241
+ if (options?.expectedVersion != null && this.config.versionColumn) {
2242
+ throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
2243
+ }
2244
+ throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
2245
+ },
2246
+ after: (entity) => this.afterRestore(entity),
2247
+ rows: (entity) => [entity],
2248
+ });
2249
+ await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
2250
+ return restored;
2151
2251
  });
2152
- await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
2153
- return restored;
2154
2252
  }
2155
2253
  async delete(id, options) {
2156
- 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",
2254
+ return this.scopedCall(options, async (options) => {
2255
+ return this.guardedWrite(id, options, {
2256
+ operation: "delete",
2257
+ state: options,
2258
+ hook: "beforeDelete",
2259
+ before: () => this.beforeDelete(id),
2260
+ write: async (db) => this.wasAffected(await db
2261
+ .delete(this.config.table)
2262
+ .where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(options)))) || undefined,
2263
+ miss: () => this.staleOrFalse(id, options),
2264
+ after: () => this.afterDelete(id),
2265
+ errors: "delete",
2266
+ });
2168
2267
  });
2169
2268
  }
2170
2269
  async forceDelete(id, options) {
@@ -2175,46 +2274,48 @@ class SqlBaseCrudService {
2175
2274
  });
2176
2275
  }
2177
2276
  async upsert(data, upsert = {}, options) {
2178
- options = this.withScopeMemo(options);
2179
- this.checkReadOptions(options);
2180
- const entityData = await this.prepareCreate(data, options);
2181
- const db = this.dbFor(options?.transaction);
2182
- await this.emitEvent("before", "upsert", false, { input: data, transaction: options?.transaction }, options);
2183
- let row;
2184
- try {
2185
- await this.assertKeysInScope(db, entityData, options);
2186
- [row] = await this.upsertRows(db, [entityData], upsert, options);
2187
- }
2188
- catch (error) {
2189
- throw this.toClientError(error, "write");
2190
- }
2191
- if (!row) {
2192
- throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(entityData, upsert));
2193
- }
2194
- await this.emitEvent("after", "upsert", false, {
2195
- ids: this.idsOf([row]),
2196
- input: data,
2197
- rows: [row],
2198
- transaction: options?.transaction,
2199
- }, options);
2200
- await this.loadWriteRelations(db, [row], options);
2201
- return row;
2277
+ return this.scopedCall(options, async (options) => {
2278
+ this.checkReadOptions(options);
2279
+ const entityData = await this.prepareCreate(data, options);
2280
+ const db = this.dbFor(options?.transaction);
2281
+ await this.emitEvent("before", "upsert", false, { input: data, transaction: options?.transaction }, options);
2282
+ let row;
2283
+ try {
2284
+ await this.assertKeysInScope(db, entityData, options);
2285
+ [row] = await this.upsertRows(db, [entityData], upsert, options);
2286
+ }
2287
+ catch (error) {
2288
+ throw this.toClientError(error, "write");
2289
+ }
2290
+ if (!row) {
2291
+ throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(entityData, upsert));
2292
+ }
2293
+ await this.emitEvent("after", "upsert", false, {
2294
+ ids: this.idsOf([row]),
2295
+ input: data,
2296
+ rows: [row],
2297
+ transaction: options?.transaction,
2298
+ }, options);
2299
+ await this.loadWriteRelations(db, [row], options);
2300
+ return row;
2301
+ });
2202
2302
  }
2203
2303
  async massUpsert(data, upsert = {}, options) {
2204
- options = this.withScopeMemo(options);
2205
- const message = "Mass upsert errors";
2206
- return this.massWrite({
2207
- operation: "upsert",
2208
- message,
2209
- options,
2210
- input: data,
2211
- returnsRows: true,
2212
- prepare: (opts) => data.map((item, index) => ({
2213
- index,
2214
- run: () => this.prepareCreate(item, opts),
2215
- })),
2216
- keys: true,
2217
- write: (tx, rows, opts) => this.upsertSet(tx, rows, upsert, opts, message),
2304
+ return this.scopedCall(options, async (options) => {
2305
+ const message = "Mass upsert errors";
2306
+ return this.massWrite({
2307
+ operation: "upsert",
2308
+ message,
2309
+ options,
2310
+ input: data,
2311
+ returnsRows: true,
2312
+ prepare: (opts) => data.map((item, index) => ({
2313
+ index,
2314
+ run: () => this.prepareCreate(item, opts),
2315
+ })),
2316
+ keys: true,
2317
+ write: (tx, rows, opts) => this.upsertSet(tx, rows, upsert, opts, message),
2318
+ });
2218
2319
  });
2219
2320
  }
2220
2321
  async upsertSet(tx, rows, upsert, opts, message) {
@@ -2369,8 +2470,9 @@ class SqlBaseCrudService {
2369
2470
  ids: ids ? unique : this.idsOf(written),
2370
2471
  rows: spec.returnsRows ? written : undefined,
2371
2472
  }, options);
2372
- if (spec.returnsRows)
2373
- await load(tx, written);
2473
+ if (spec.returnsRows) {
2474
+ await load(tx, written, prepared.some((row) => this.holdsSql(row)));
2475
+ }
2374
2476
  return ids ? this.inIdOrder(written, ids) : written;
2375
2477
  };
2376
2478
  return spec.returnsRows
@@ -2378,30 +2480,31 @@ class SqlBaseCrudService {
2378
2480
  : this.executeSqlTransaction((tx) => body(tx, async () => { }), options?.transaction);
2379
2481
  }
2380
2482
  async massCreate(data, options) {
2381
- options = this.withScopeMemo(options);
2382
- if (options?.bulkMode === "perItem") {
2383
- return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
2384
- }
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) => ({
2393
- index,
2394
- run: () => this.prepareCreate(item, opts),
2395
- })),
2396
- keys: true,
2397
- write: (tx, rows, opts) => this.runSet(tx, message, (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
2398
- index,
2399
- run: (sp) => this.insertOne(sp, row, opts),
2400
- })), "write"),
2401
- after: async (created) => {
2402
- for (const entity of created)
2403
- await this.afterCreate(entity);
2404
- },
2483
+ return this.scopedCall(options, async (options) => {
2484
+ if (options?.bulkMode === "perItem") {
2485
+ return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
2486
+ }
2487
+ const message = "Mass create errors";
2488
+ return this.massWrite({
2489
+ operation: "create",
2490
+ message,
2491
+ options,
2492
+ input: data,
2493
+ returnsRows: true,
2494
+ prepare: (opts) => data.map((item, index) => ({
2495
+ index,
2496
+ run: () => this.prepareCreate(item, opts),
2497
+ })),
2498
+ keys: true,
2499
+ write: (tx, rows, opts) => this.runSet(tx, message, (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
2500
+ index,
2501
+ run: (sp) => this.insertOne(sp, row, opts),
2502
+ })), "write"),
2503
+ after: async (created) => {
2504
+ for (const entity of created)
2505
+ await this.afterCreate(entity);
2506
+ },
2507
+ });
2405
2508
  });
2406
2509
  }
2407
2510
  async insertMany(db, rows, options) {
@@ -2451,35 +2554,36 @@ class SqlBaseCrudService {
2451
2554
  })));
2452
2555
  }
2453
2556
  async massUpdate(ids, data, options) {
2454
- options = this.withScopeMemo(options);
2455
- if (options?.bulkMode === "perItem") {
2456
- return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass update errors", options, ({ id }, opts) => this.update(id, data, opts));
2457
- }
2458
- const message = "Mass update errors";
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) => ({
2468
- id,
2469
- run: async () => {
2470
- await this.validateUpdate(id, data);
2471
- const processed = skipBefore
2472
- ? data
2473
- : await this.beforeUpdate(id, data);
2474
- return this.prepareUpdate(processed, opts);
2557
+ return this.scopedCall(options, async (options) => {
2558
+ if (options?.bulkMode === "perItem") {
2559
+ return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass update errors", options, ({ id }, opts) => this.update(id, data, opts));
2560
+ }
2561
+ const message = "Mass update errors";
2562
+ const skipBefore = options?.hooks?.skipBefore;
2563
+ return this.massWrite({
2564
+ operation: "update",
2565
+ message,
2566
+ options,
2567
+ ids,
2568
+ input: data,
2569
+ returnsRows: true,
2570
+ prepare: (opts, unique) => unique.map((id) => ({
2571
+ id,
2572
+ run: async () => {
2573
+ await this.validateUpdate(id, data);
2574
+ const processed = skipBefore
2575
+ ? data
2576
+ : await this.beforeUpdate(id, data);
2577
+ return this.prepareUpdate(processed, opts);
2578
+ },
2579
+ })),
2580
+ keys: true,
2581
+ write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
2582
+ after: async (updated) => {
2583
+ for (const entity of updated)
2584
+ await this.afterUpdate(entity);
2475
2585
  },
2476
- })),
2477
- keys: true,
2478
- write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
2479
- after: async (updated) => {
2480
- for (const entity of updated)
2481
- await this.afterUpdate(entity);
2482
- },
2586
+ });
2483
2587
  });
2484
2588
  }
2485
2589
  async updateSets(tx, sets, unique, opts, message) {
@@ -2520,99 +2624,105 @@ class SqlBaseCrudService {
2520
2624
  : [];
2521
2625
  }
2522
2626
  async massSoftDelete(ids, options) {
2523
- options = this.withScopeMemo(options);
2524
- if (!this.config.softDelete?.enabled)
2525
- throw new common_1.BadRequestException("Soft delete not enabled");
2526
- if (options?.bulkMode === "perItem") {
2527
- await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass soft delete errors", options, ({ id }, opts) => this.softDelete(id, opts));
2627
+ return this.scopedCall(options, async (options) => {
2628
+ if (!this.config.softDelete?.enabled)
2629
+ throw new common_1.BadRequestException("Soft delete not enabled");
2630
+ if (options?.bulkMode === "perItem") {
2631
+ await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass soft delete errors", options, ({ id }, opts) => this.softDelete(id, opts));
2632
+ return true;
2633
+ }
2634
+ const message = "Mass soft delete errors";
2635
+ await this.massWrite({
2636
+ operation: "softDelete",
2637
+ message,
2638
+ options,
2639
+ ids,
2640
+ state: { withDeleted: false, onlyDeleted: false },
2641
+ returnsRows: false,
2642
+ prepare: (_opts, unique) => options?.hooks?.skipBefore
2643
+ ? undefined
2644
+ : unique.map((id) => ({
2645
+ id,
2646
+ run: () => this.beforeSoftDelete(id),
2647
+ })),
2648
+ write: (tx, _prepared, opts, unique) => {
2649
+ const set = this.softDeleteSet(new Date());
2650
+ const update = (db, keys) => db
2651
+ .update(this.config.table)
2652
+ .set(set)
2653
+ .where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
2654
+ return this.runSet(tx, message, (sp) => this.runKeyed(update(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => update(sp, [id]) })), "write");
2655
+ },
2656
+ after: async (_written, unique) => {
2657
+ for (const id of unique)
2658
+ await this.afterSoftDelete(id);
2659
+ },
2660
+ });
2528
2661
  return true;
2529
- }
2530
- const message = "Mass soft delete errors";
2531
- await this.massWrite({
2532
- operation: "softDelete",
2533
- message,
2534
- options,
2535
- ids,
2536
- state: { withDeleted: false, onlyDeleted: false },
2537
- returnsRows: false,
2538
- prepare: (_opts, unique) => options?.hooks?.skipBefore
2539
- ? undefined
2540
- : unique.map((id) => ({ id, run: () => this.beforeSoftDelete(id) })),
2541
- write: (tx, _prepared, opts, unique) => {
2542
- const set = this.softDeleteSet(new Date());
2543
- const update = (db, keys) => db
2544
- .update(this.config.table)
2545
- .set(set)
2546
- .where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
2547
- return this.runSet(tx, message, (sp) => this.runKeyed(update(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => update(sp, [id]) })), "write");
2548
- },
2549
- after: async (_written, unique) => {
2550
- for (const id of unique)
2551
- await this.afterSoftDelete(id);
2552
- },
2553
2662
  });
2554
- return true;
2555
2663
  }
2556
2664
  async massRestore(ids, options) {
2557
- options = this.withScopeMemo(options);
2558
- if (!this.config.softDelete?.enabled)
2559
- throw new common_1.BadRequestException("Soft delete not enabled");
2560
- if (options?.bulkMode === "perItem") {
2561
- return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass restore errors", options, ({ id }, opts) => this.restore(id, opts));
2562
- }
2563
- const message = "Mass restore errors";
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) => {
2583
- for (const entity of restored)
2584
- await this.afterRestore(entity);
2585
- },
2665
+ return this.scopedCall(options, async (options) => {
2666
+ if (!this.config.softDelete?.enabled)
2667
+ throw new common_1.BadRequestException("Soft delete not enabled");
2668
+ if (options?.bulkMode === "perItem") {
2669
+ return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass restore errors", options, ({ id }, opts) => this.restore(id, opts));
2670
+ }
2671
+ const message = "Mass restore errors";
2672
+ return this.massWrite({
2673
+ operation: "restore",
2674
+ message,
2675
+ options,
2676
+ ids,
2677
+ state: { withDeleted: true, onlyDeleted: false },
2678
+ returnsRows: true,
2679
+ prepare: (_opts, unique) => options?.hooks?.skipBefore
2680
+ ? undefined
2681
+ : unique.map((id) => ({ id, run: () => this.beforeRestore(id) })),
2682
+ write: (tx, _prepared, opts, unique) => {
2683
+ const set = this.softDeleteSet(null);
2684
+ const state = this.softDeleteScope(opts);
2685
+ return this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts, state), unique.map((id) => ({
2686
+ id,
2687
+ run: (sp) => this.updateWhereIn(sp, set, [id], opts, state),
2688
+ })), "write");
2689
+ },
2690
+ after: async (restored) => {
2691
+ for (const entity of restored)
2692
+ await this.afterRestore(entity);
2693
+ },
2694
+ });
2586
2695
  });
2587
2696
  }
2588
2697
  async massDelete(ids, options) {
2589
- options = this.withScopeMemo(options);
2590
- if (options?.bulkMode === "perItem") {
2591
- await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass delete errors", options, ({ id }, opts) => this.delete(id, opts), "delete");
2698
+ return this.scopedCall(options, async (options) => {
2699
+ if (options?.bulkMode === "perItem") {
2700
+ await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass delete errors", options, ({ id }, opts) => this.delete(id, opts), "delete");
2701
+ return true;
2702
+ }
2703
+ const message = "Mass delete errors";
2704
+ await this.massWrite({
2705
+ operation: "delete",
2706
+ message,
2707
+ options,
2708
+ ids,
2709
+ returnsRows: false,
2710
+ prepare: (_opts, unique) => options?.hooks?.skipBefore
2711
+ ? undefined
2712
+ : unique.map((id) => ({ id, run: () => this.beforeDelete(id) })),
2713
+ write: (tx, _prepared, opts, unique) => {
2714
+ const remove = (db, keys) => db
2715
+ .delete(this.config.table)
2716
+ .where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
2717
+ return this.runSet(tx, message, (sp) => this.runKeyed(remove(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => remove(sp, [id]) })), "delete");
2718
+ },
2719
+ after: async (_written, unique) => {
2720
+ for (const id of unique)
2721
+ await this.afterDelete(id);
2722
+ },
2723
+ });
2592
2724
  return true;
2593
- }
2594
- const message = "Mass delete errors";
2595
- await this.massWrite({
2596
- operation: "delete",
2597
- message,
2598
- options,
2599
- ids,
2600
- returnsRows: false,
2601
- prepare: (_opts, unique) => options?.hooks?.skipBefore
2602
- ? undefined
2603
- : unique.map((id) => ({ id, run: () => this.beforeDelete(id) })),
2604
- write: (tx, _prepared, opts, unique) => {
2605
- const remove = (db, keys) => db
2606
- .delete(this.config.table)
2607
- .where((0, drizzle_orm_1.and)(this.writeWhere(keys, opts), this.softDeleteScope(opts)));
2608
- return this.runSet(tx, message, (sp) => this.runKeyed(remove(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => remove(sp, [id]) })), "delete");
2609
- },
2610
- after: async (_written, unique) => {
2611
- for (const id of unique)
2612
- await this.afterDelete(id);
2613
- },
2614
2725
  });
2615
- return true;
2616
2726
  }
2617
2727
  async massForceDelete(ids, options) {
2618
2728
  return this.massDelete(ids, {
@@ -2621,6 +2731,10 @@ class SqlBaseCrudService {
2621
2731
  onlyDeleted: false,
2622
2732
  });
2623
2733
  }
2734
+ holdsSql(row) {
2735
+ return ((0, merge_config_1.isPlainObject)(row) &&
2736
+ Object.values(row).some((value) => (0, drizzle_orm_1.is)(value, drizzle_orm_1.SQL)));
2737
+ }
2624
2738
  uniqueIds(ids) {
2625
2739
  const seen = new Set();
2626
2740
  return (ids ?? []).filter((id) => {
@@ -2632,20 +2746,33 @@ class SqlBaseCrudService {
2632
2746
  });
2633
2747
  }
2634
2748
  fingerprint(value) {
2635
- return JSON.stringify(value, (_key, v) => {
2749
+ return JSON.stringify(value, function (key, json) {
2750
+ const v = this[key];
2751
+ if (v instanceof Date)
2752
+ return `date:${v.getTime()}`;
2753
+ if (ArrayBuffer.isView(v)) {
2754
+ const bytes = new Uint8Array(v.buffer, v.byteOffset, v.byteLength);
2755
+ return `bytes:${bytes.join(",")}`;
2756
+ }
2636
2757
  if (typeof v === "string")
2637
2758
  return `s:${v}`;
2638
2759
  if (typeof v === "bigint")
2639
2760
  return `bigint:${v}`;
2640
- if (typeof v === "number" && !Number.isFinite(v))
2761
+ if (typeof v === "number" && !Number.isFinite(v)) {
2641
2762
  return `number:${v}`;
2763
+ }
2642
2764
  if (v !== null && typeof v === "object" && !Array.isArray(v)) {
2643
2765
  const proto = Object.getPrototypeOf(v);
2644
2766
  if (proto !== Object.prototype && proto !== null) {
2645
- return `opaque:${++fingerprintTag}`;
2767
+ let tag = fingerprintTags.get(v);
2768
+ if (!tag) {
2769
+ tag = `opaque:${++fingerprintCount}`;
2770
+ fingerprintTags.set(v, tag);
2771
+ }
2772
+ return tag;
2646
2773
  }
2647
2774
  }
2648
- return v;
2775
+ return json;
2649
2776
  });
2650
2777
  }
2651
2778
  async assertAllExist(tx, ids, options, message) {
@@ -2781,44 +2908,50 @@ class SqlBaseCrudService {
2781
2908
  : run(tx);
2782
2909
  }
2783
2910
  async exists(id, options) {
2784
- options = this.withScopeMemo(options);
2785
- this.checkReadOptions({ ...options, select: undefined });
2786
- const entity = await this.find(id, {
2787
- ...options,
2788
- relations: [],
2789
- counts: undefined,
2790
- select: [this.config.primaryKey],
2911
+ return this.scopedCall(options, async (options) => {
2912
+ this.checkReadOptions({ ...options, select: undefined });
2913
+ const entity = await this.find(id, {
2914
+ ...options,
2915
+ relations: [],
2916
+ counts: undefined,
2917
+ select: [this.config.primaryKey],
2918
+ });
2919
+ return !!entity;
2791
2920
  });
2792
- return !!entity;
2793
2921
  }
2794
2922
  async count(filters, options) {
2795
- options = this.withScopeMemo(options);
2796
- this.checkReadOptions(options);
2797
- const db = this.dbFor(options?.transaction);
2798
- const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
2799
- const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
2800
- return this.parseCount(result);
2923
+ return this.scopedCall(options, async (options) => {
2924
+ this.checkReadOptions(options);
2925
+ const db = this.dbFor(options?.transaction);
2926
+ const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
2927
+ const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
2928
+ return this.parseCount(result);
2929
+ });
2801
2930
  }
2802
2931
  async countBy(column, filters, options) {
2803
- options = this.withScopeMemo(options);
2804
- this.checkReadOptions(options);
2805
- const db = this.dbFor(options?.transaction);
2806
- const spec = this.resolveGroupColumn(column, options);
2807
- const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
2808
- const joins = Array.from(new Set([...filterRelations, ...(spec.relation ? [spec.relation] : [])]));
2809
- const total = (0, drizzle_orm_1.sql) `count(*)`;
2810
- let query = db
2811
- .select({ value: spec.column, count: total })
2812
- .from(this.config.table);
2813
- query = this.applyJoins(query, joins, options);
2814
- if (conditions.length > 0)
2815
- query = query.where((0, drizzle_orm_1.and)(...conditions));
2816
- query = query.groupBy(spec.column).orderBy((0, drizzle_orm_1.desc)(total), (0, drizzle_orm_1.asc)(spec.column));
2817
- const rows = await this.runRead(query);
2818
- return rows.map((row) => ({
2819
- value: row.value,
2820
- count: Number(row.count),
2821
- }));
2932
+ return this.scopedCall(options, async (options) => {
2933
+ this.checkReadOptions(options);
2934
+ const db = this.dbFor(options?.transaction);
2935
+ const spec = this.resolveGroupColumn(column, options);
2936
+ const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
2937
+ const joins = Array.from(new Set([
2938
+ ...filterRelations,
2939
+ ...(spec.relation ? [spec.relation] : []),
2940
+ ]));
2941
+ const total = (0, drizzle_orm_1.sql) `count(*)`;
2942
+ let query = db
2943
+ .select({ value: spec.column, count: total })
2944
+ .from(this.config.table);
2945
+ query = this.applyJoins(query, joins, options);
2946
+ if (conditions.length > 0)
2947
+ query = query.where((0, drizzle_orm_1.and)(...conditions));
2948
+ query = query.groupBy(spec.column).orderBy((0, drizzle_orm_1.desc)(total), (0, drizzle_orm_1.asc)(spec.column));
2949
+ const rows = await this.runRead(query);
2950
+ return rows.map((row) => ({
2951
+ value: row.value,
2952
+ count: Number(row.count),
2953
+ }));
2954
+ });
2822
2955
  }
2823
2956
  resolveGroupColumn(key, options) {
2824
2957
  if (typeof key !== "string" || !key) {
@@ -3352,49 +3485,50 @@ class SqlBaseCrudService {
3352
3485
  }
3353
3486
  }
3354
3487
  async fullTextSearch(searchTerm, searchColumns, pagination, options) {
3355
- options = this.withScopeMemo(options);
3356
- if (this.config.dialect !== "postgresql") {
3357
- throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
3358
- }
3359
- if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
3360
- throw new common_1.BadRequestException("Full-text search needs at least one column");
3361
- }
3362
- const columns = searchColumns.map((name) => {
3363
- const column = this.columnOf(this.config.table, name);
3364
- if (!column) {
3365
- throw new common_1.BadRequestException(`Unknown search column: ${name}`);
3488
+ return this.scopedCall(options, async (options) => {
3489
+ if (this.config.dialect !== "postgresql") {
3490
+ throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
3366
3491
  }
3367
- this.assertAllowed("searchableColumns", name, options);
3368
- return column;
3369
- });
3370
- const { filters, language, queryMode, ...readOptions } = options ?? {};
3371
- const { vector, query } = this.fullTextParts(columns, searchTerm, {
3372
- language,
3373
- queryMode,
3492
+ if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
3493
+ throw new common_1.BadRequestException("Full-text search needs at least one column");
3494
+ }
3495
+ const columns = searchColumns.map((name) => {
3496
+ const column = this.columnOf(this.config.table, name);
3497
+ if (!column) {
3498
+ throw new common_1.BadRequestException(`Unknown search column: ${name}`);
3499
+ }
3500
+ this.assertAllowed("searchableColumns", name, options);
3501
+ return column;
3502
+ });
3503
+ const { filters, language, queryMode, ...readOptions } = options ?? {};
3504
+ const { vector, query } = this.fullTextParts(columns, searchTerm, {
3505
+ language,
3506
+ queryMode,
3507
+ });
3508
+ const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
3509
+ const plan = this.planList(filters, undefined, "asc", readOptions);
3510
+ const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
3511
+ const joins = Array.from(new Set([
3512
+ ...plan.joined.map((node) => node.name),
3513
+ ...plan.filterRelations,
3514
+ ]));
3515
+ const pk = this.columnOf(this.config.table, this.config.primaryKey);
3516
+ const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
3517
+ if (pk)
3518
+ orderBy.push((0, drizzle_orm_1.asc)(pk));
3519
+ let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
3520
+ dataQuery = this.applyJoins(dataQuery, joins, readOptions)
3521
+ .where((0, drizzle_orm_1.and)(...conditions))
3522
+ .orderBy(...orderBy)
3523
+ .limit(limit)
3524
+ .offset(offset);
3525
+ const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
3526
+ const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
3527
+ const total = this.parseCount(totalResult);
3528
+ const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
3529
+ await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
3530
+ return { data: rows, total, page, limit };
3374
3531
  });
3375
- const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
3376
- const plan = this.planList(filters, undefined, "asc", readOptions);
3377
- const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
3378
- const joins = Array.from(new Set([
3379
- ...plan.joined.map((node) => node.name),
3380
- ...plan.filterRelations,
3381
- ]));
3382
- const pk = this.columnOf(this.config.table, this.config.primaryKey);
3383
- const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
3384
- if (pk)
3385
- orderBy.push((0, drizzle_orm_1.asc)(pk));
3386
- let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
3387
- dataQuery = this.applyJoins(dataQuery, joins, readOptions)
3388
- .where((0, drizzle_orm_1.and)(...conditions))
3389
- .orderBy(...orderBy)
3390
- .limit(limit)
3391
- .offset(offset);
3392
- const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
3393
- const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
3394
- const total = this.parseCount(totalResult);
3395
- const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
3396
- await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
3397
- return { data: rows, total, page, limit };
3398
3532
  }
3399
3533
  }
3400
3534
  exports.SqlBaseCrudService = SqlBaseCrudService;