@avytrix/nestjs-drizzle-crud 3.8.0 → 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,6 +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
+ const fingerprintTags = new WeakMap();
15
+ let fingerprintCount = 0;
14
16
  const ROLLBACK = Symbol("rollback");
15
17
  class DeferredDiagnosis {
16
18
  settle;
@@ -127,15 +129,38 @@ class SqlBaseCrudService {
127
129
  return /(Text|VarChar|Varchar|Char)$/.test(column?.columnType ?? "");
128
130
  }
129
131
  coerceFilterValue(column, value) {
130
- if (column?.dataType !== "date" || value instanceof Date)
132
+ if (column?.dataType !== "date")
131
133
  return value;
132
- if (typeof value === "string" || typeof value === "number") {
133
- const date = new Date(value);
134
- if (!Number.isNaN(date.getTime()))
135
- return date;
136
- }
134
+ const date = this.toDate(value);
135
+ if (date)
136
+ return date;
137
137
  throw new common_1.BadRequestException(`Invalid date filter value: ${value}`);
138
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
+ }
139
164
  static ALLOWLIST_LABELS = {
140
165
  filterableColumns: "Filtering on",
141
166
  sortableColumns: "Sorting by",
@@ -192,23 +217,37 @@ class SqlBaseCrudService {
192
217
  const scope = this.config.scope;
193
218
  if (!scope || options?.skipScope)
194
219
  return null;
195
- const memo = options?.[SCOPE_MEMO];
196
- if (memo && Object.hasOwn(memo, "values"))
197
- return memo.values;
198
- const values = typeof scope === "function"
199
- ? scope(options?.context ?? crud_context_1.CrudContext.current() ?? {})
200
- : scope;
220
+ const context = options?.context ?? crud_context_1.CrudContext.current();
221
+ const found = options?.[SCOPE_MEMO];
222
+ const memo = found?.ended ? undefined : found;
223
+ const mine = memo?.get(this);
224
+ if (mine?.has(context))
225
+ return mine.get(context);
226
+ const values = typeof scope === "function" ? scope(context ?? {}) : scope;
201
227
  const checked = this.checkScope(values);
202
- if (memo)
203
- memo.values = checked;
228
+ if (mine)
229
+ mine.set(context, checked);
230
+ else
231
+ memo?.set(this, new Map([[context, checked]]));
204
232
  return checked;
205
233
  }
206
234
  withScopeMemo(options) {
207
- if (typeof this.config.scope !== "function" ||
208
- options?.[SCOPE_MEMO]) {
235
+ const memo = options?.[SCOPE_MEMO];
236
+ if (typeof this.config.scope !== "function" || (memo && !memo.ended)) {
209
237
  return options;
210
238
  }
211
- return { ...options, [SCOPE_MEMO]: {} };
239
+ return { ...options, [SCOPE_MEMO]: new Map() };
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
+ }
212
251
  }
213
252
  checkScope(values) {
214
253
  if (values === null)
@@ -327,8 +366,9 @@ class SqlBaseCrudService {
327
366
  query = dialect.sqlToQuery(...args);
328
367
  }
329
368
  catch (error) {
330
- if (error instanceof RangeError)
369
+ if (error instanceof RangeError && /call stack/i.test(error.message)) {
331
370
  throw tooMany(`more than ${limit}`);
371
+ }
332
372
  throw error;
333
373
  }
334
374
  const count = query?.params?.length ?? 0;
@@ -1190,7 +1230,7 @@ class SqlBaseCrudService {
1190
1230
  if (this.config.events && !options?.skipEvents)
1191
1231
  return false;
1192
1232
  const base = SqlBaseCrudService.prototype;
1193
- return methods.every((name) => this[name] === base[name]);
1233
+ return ["emitEvent", ...methods].every((name) => this[name] === base[name]);
1194
1234
  }
1195
1235
  async guardOnError(guarded, id, options, run) {
1196
1236
  try {
@@ -1244,14 +1284,16 @@ class SqlBaseCrudService {
1244
1284
  };
1245
1285
  }
1246
1286
  async find(id, options) {
1247
- options = this.withScopeMemo(options);
1248
- this.checkReadOptions(options);
1249
- 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
+ });
1250
1291
  }
1251
1292
  async findOne(where, options) {
1252
- options = this.withScopeMemo(options);
1253
- this.checkReadOptions(options);
1254
- 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
+ });
1255
1297
  }
1256
1298
  async findFirst(conditions, options) {
1257
1299
  const first = await this.readFirst(conditions, options);
@@ -1332,105 +1374,107 @@ class SqlBaseCrudService {
1332
1374
  return query;
1333
1375
  }
1334
1376
  async findAll(filters, pagination, options) {
1335
- options = this.withScopeMemo(options);
1336
- const { sortBy, sortOrder = "asc" } = pagination || {};
1337
- const withCount = pagination?.withCount !== false;
1338
- const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
1339
- const plan = this.planList(filters, sortBy, sortOrder, options);
1340
- let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, plan.joinRelations.length > 0, plan.selection);
1341
- dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1342
- if (plan.conditions.length > 0) {
1343
- dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...plan.conditions));
1344
- }
1345
- if (plan.sort.length > 0) {
1346
- dataQuery = dataQuery.orderBy(...plan.sort.map((spec) => this.orderOf(spec)));
1347
- }
1348
- dataQuery = dataQuery
1349
- .limit(withCount ? safeLimit : safeLimit + 1)
1350
- .offset(offset);
1351
- dataQuery = this.applyLock(dataQuery, options);
1352
- const countQuery = withCount
1353
- ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1354
- : Promise.resolve(undefined);
1355
- const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
1356
- const data = fetched.slice(0, safeLimit);
1357
- const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
1358
- await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
1359
- 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
+ }
1360
1410
  return {
1361
1411
  data: rows,
1362
- total: null,
1412
+ total: this.parseCount(totalResult),
1363
1413
  page,
1364
1414
  limit: safeLimit,
1365
- hasMore: fetched.length > safeLimit,
1366
1415
  };
1367
- }
1368
- return {
1369
- data: rows,
1370
- total: this.parseCount(totalResult),
1371
- page,
1372
- limit: safeLimit,
1373
- };
1416
+ });
1374
1417
  }
1375
1418
  async findByCursor(filters, pagination, options) {
1376
- options = this.withScopeMemo(options);
1377
- const { sortBy, sortOrder = "asc", count = false } = pagination || {};
1378
- const { limit } = this.resolvePagination(1, pagination?.limit);
1379
- const plan = this.planList(filters, sortBy, sortOrder, options);
1380
- const signature = plan.sort
1381
- .map((spec) => `${spec.key}:${spec.order}`)
1382
- .join(",");
1383
- const cursor = pagination?.cursor ?? pagination?.after;
1384
- const conditions = [...plan.conditions];
1385
- if (cursor !== undefined && cursor !== null && cursor !== "") {
1386
- if (typeof cursor !== "string") {
1387
- throw new common_1.BadRequestException("Invalid cursor");
1388
- }
1389
- const values = (0, cursor_1.decodeCursor)(cursor, signature, plan.sort.length);
1390
- conditions.push(this.keysetCondition(plan.sort, values));
1391
- }
1392
- const fields = this.buildSelection(plan.baseSelect, plan.joined, plan.selection);
1393
- const cursorKeys = plan.sort.map((spec, i) => {
1394
- const field = this.cursorField(spec.column);
1395
- const projected = field === spec.column && !spec.relation
1396
- ? Object.keys(fields).find((key) => fields[key] === spec.column)
1397
- : undefined;
1398
- if (projected)
1399
- return projected;
1400
- fields[`${CURSOR_KEY}${i}`] = field;
1401
- return `${CURSOR_KEY}${i}`;
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;
1402
1477
  });
1403
- const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
1404
- let dataQuery = plan.db.select(fields).from(this.config.table);
1405
- dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1406
- if (conditions.length > 0)
1407
- dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
1408
- dataQuery = dataQuery
1409
- .orderBy(...plan.sort.map((spec) => this.orderOf(spec)))
1410
- .limit(limit + 1);
1411
- dataQuery = this.applyLock(dataQuery, options);
1412
- const [data, totalResult] = await this.runRead(Promise.all([
1413
- dataQuery,
1414
- count
1415
- ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1416
- : Promise.resolve(undefined),
1417
- ]));
1418
- const page = data.slice(0, limit);
1419
- const last = page[page.length - 1];
1420
- const nextCursor = data.length > limit && last
1421
- ? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
1422
- : null;
1423
- for (const row of page) {
1424
- for (const key of hidden)
1425
- delete row[key];
1426
- }
1427
- const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
1428
- await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
1429
- const out = { data: rows, limit, nextCursor };
1430
- if (count) {
1431
- out.total = this.parseCount(totalResult);
1432
- }
1433
- return out;
1434
1478
  }
1435
1479
  cursorField(column) {
1436
1480
  if (column?.dataType !== "date")
@@ -1550,8 +1594,8 @@ class SqlBaseCrudService {
1550
1594
  const hosts = rows.filter(Boolean);
1551
1595
  await this.hydrate(db, hosts, nodes, undefined, options);
1552
1596
  }
1553
- loadsAfterCommit(options) {
1554
- if (options?.transaction)
1597
+ loadsAfterCommit(operation, options) {
1598
+ if (operation === "create" || options?.transaction)
1555
1599
  return false;
1556
1600
  try {
1557
1601
  return this.countPlan(options?.counts).every((count) => !count.filter);
@@ -1560,11 +1604,14 @@ class SqlBaseCrudService {
1560
1604
  return false;
1561
1605
  }
1562
1606
  }
1563
- async writeThenLoad(options, write) {
1564
- const late = this.loadsAfterCommit(options);
1565
- const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written) => late
1566
- ? Promise.resolve()
1567
- : this.loadWriteRelations(inTx, written, options)), options?.transaction);
1607
+ async writeThenLoad(operation, options, write) {
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);
1568
1615
  if (late)
1569
1616
  await this.loadWriteRelations(this.dbFor(), rows, options);
1570
1617
  return rows;
@@ -1593,7 +1640,7 @@ class SqlBaseCrudService {
1593
1640
  const version = this.config.versionColumn;
1594
1641
  if (version)
1595
1642
  delete mapped[version];
1596
- const entityData = this.filterWritable(mapped, "create", options);
1643
+ const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "create", options));
1597
1644
  const now = new Date();
1598
1645
  if (this.config.timestamps?.createdAt)
1599
1646
  entityData[this.config.timestamps.createdAt] = now;
@@ -1623,35 +1670,36 @@ class SqlBaseCrudService {
1623
1670
  return createdEntity;
1624
1671
  }
1625
1672
  async create(data, options) {
1626
- options = this.withScopeMemo(options);
1627
- const { transaction, hooks = {} } = options || {};
1628
- this.checkReadOptions(options);
1629
- const entityData = await this.prepareCreate(data, options);
1630
- const nested = this.takeNested(entityData, options);
1631
- const db = this.dbFor(transaction);
1632
- await this.emitEvent("before", "create", false, { input: data, transaction }, options);
1633
- try {
1634
- await this.assertKeysInScope(db, entityData, options, nested.map((entry) => entry.name));
1635
- const createdEntity = nested.length === 0
1636
- ? await this.insertOne(db, entityData, options)
1637
- : await this.executeSqlTransaction((tx) => this.insertNested(tx, entityData, nested, {
1638
- ...options,
1639
- transaction: tx,
1640
- }), transaction);
1641
- if (!hooks.skipAfter)
1642
- await this.afterCreate(createdEntity);
1643
- await this.emitEvent("after", "create", false, {
1644
- ids: this.idsOf([createdEntity]),
1645
- input: data,
1646
- rows: [createdEntity],
1647
- transaction,
1648
- }, options);
1649
- await this.loadWriteRelations(db, [createdEntity], options);
1650
- return createdEntity;
1651
- }
1652
- catch (error) {
1653
- throw this.toClientError(error, "write");
1654
- }
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
+ });
1655
1703
  }
1656
1704
  takeNested(entity, options) {
1657
1705
  const allowed = this.config.writableRelations;
@@ -1684,9 +1732,11 @@ class SqlBaseCrudService {
1684
1732
  if (!(0, merge_config_1.isPlainObject)(value)) {
1685
1733
  throw new crud_exceptions_1.ValidationFailedException(`the relation "${name}" takes an object to create`);
1686
1734
  }
1735
+ const parentRow = this.relatedRow(relation.table, value, options);
1736
+ await this.assertNestedKeysInScope(tx, relation, [parentRow], options);
1687
1737
  const [parent] = await tx
1688
1738
  .insert(relation.table)
1689
- .values(this.relatedRow(relation.table, value, options))
1739
+ .values(parentRow)
1690
1740
  .returning();
1691
1741
  row[relation.localKey] = parent[relation.references ?? "id"];
1692
1742
  }
@@ -1708,6 +1758,7 @@ class SqlBaseCrudService {
1708
1758
  [relation.foreignKey]: parentValue,
1709
1759
  };
1710
1760
  });
1761
+ await this.assertNestedKeysInScope(tx, relation, children, options);
1711
1762
  if (children.length > 0) {
1712
1763
  await tx.insert(relation.table).values(children);
1713
1764
  }
@@ -1716,9 +1767,11 @@ class SqlBaseCrudService {
1716
1767
  const ids = value.filter((item) => !(0, merge_config_1.isPlainObject)(item));
1717
1768
  const fresh = value.filter((item) => (0, merge_config_1.isPlainObject)(item));
1718
1769
  if (fresh.length > 0) {
1770
+ const freshRows = fresh.map((item) => this.relatedRow(relation.table, item, options));
1771
+ await this.assertNestedKeysInScope(tx, relation, freshRows, options);
1719
1772
  const inserted = await tx
1720
1773
  .insert(relation.table)
1721
- .values(fresh.map((item) => this.relatedRow(relation.table, item, options)))
1774
+ .values(freshRows)
1722
1775
  .returning();
1723
1776
  ids.push(...inserted.map((r) => r[relation.references ?? "id"]));
1724
1777
  }
@@ -1752,13 +1805,17 @@ class SqlBaseCrudService {
1752
1805
  if (this.columnOf(table, key))
1753
1806
  out[key] = item;
1754
1807
  }
1755
- return out;
1808
+ return this.coerceDates(table, out);
1756
1809
  }
1757
- async keysOutsideScope(db, rows, options, skip = []) {
1810
+ async keysOutsideScope(db, rows, options, skip = [], owner = {
1811
+ table: this.config.table,
1812
+ relations: this.config.relations,
1813
+ }) {
1758
1814
  const failures = new Map();
1759
1815
  if (!this.resolveScope(options))
1760
1816
  return failures;
1761
- for (const [name, relation] of Object.entries(this.config.relations ?? {})) {
1817
+ const checks = [];
1818
+ for (const [name, relation] of Object.entries(owner.relations ?? {})) {
1762
1819
  const local = relation.localKey;
1763
1820
  if (!local || this.isCollection(relation) || skip.includes(name)) {
1764
1821
  continue;
@@ -1767,23 +1824,34 @@ class SqlBaseCrudService {
1767
1824
  const referenced = relation.table[relation.references ?? "id"];
1768
1825
  if (scope.length === 0 || !referenced)
1769
1826
  continue;
1770
- const keys = this.uniqueValues(rows.map((row) => row[local]).filter((value) => value != null));
1827
+ const keys = this.distinct(rows.map((row) => row[local]).filter((value) => value != null));
1771
1828
  if (keys.length === 0)
1772
1829
  continue;
1773
- const found = await db
1774
- .select({ id: referenced })
1775
- .from(relation.table)
1776
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(referenced, keys), ...scope));
1777
- const present = new Set(found.map((row) => this.valueKey(row.id)));
1830
+ checks.push({
1831
+ local,
1832
+ keys,
1833
+ query: this.matchKeys(db, relation.table, referenced, keys, scope),
1834
+ });
1835
+ }
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)));
1778
1846
  rows.forEach((row, index) => {
1779
1847
  const value = row[local];
1780
- if (value == null || present.has(this.valueKey(value)))
1848
+ if (value == null || present.has(value))
1781
1849
  return;
1782
1850
  if (!failures.has(index)) {
1783
- failures.set(index, this.missingParentException(local));
1851
+ failures.set(index, this.missingParentException(local, owner.table));
1784
1852
  }
1785
1853
  });
1786
- }
1854
+ });
1787
1855
  return failures;
1788
1856
  }
1789
1857
  async assertBatchKeysInScope(db, rows, refs, message, options) {
@@ -1797,29 +1865,35 @@ class SqlBaseCrudService {
1797
1865
  if (failure)
1798
1866
  throw failure;
1799
1867
  }
1800
- missingParentException(localKey) {
1868
+ async assertNestedKeysInScope(tx, relation, rows, options) {
1869
+ if (!relation.relations || rows.length === 0)
1870
+ return;
1871
+ const failures = await this.keysOutsideScope(tx, rows, options, [], {
1872
+ table: relation.table,
1873
+ relations: relation.relations,
1874
+ });
1875
+ if (failures.size === 0)
1876
+ return;
1877
+ const first = [...failures.keys()].reduce((a, b) => Math.min(a, b));
1878
+ throw failures.get(first);
1879
+ }
1880
+ missingParentException(localKey, table = this.config.table) {
1801
1881
  const field = this.config.dialect === "postgresql"
1802
- ? (this.columnOf(this.config.table, localKey)?.name ?? localKey)
1882
+ ? (this.columnOf(table, localKey)?.name ?? localKey)
1803
1883
  : "reference";
1804
1884
  return new crud_exceptions_1.ForeignKeyViolationException(this.getEntityName(), field);
1805
1885
  }
1806
1886
  async linkTargets(tx, relation, parentValue, ids, options, fresh = false) {
1807
- const wanted = this.uniqueValues(ids);
1808
- if (wanted.length === 0)
1809
- return;
1810
1887
  const target = relation.table;
1811
1888
  const referenced = target[relation.references ?? "id"];
1889
+ const wanted = this.distinct(ids);
1890
+ if (wanted.length === 0)
1891
+ return;
1812
1892
  const through = relation.through;
1813
1893
  const local = through[relation.throughLocalKey];
1814
1894
  const foreign = through[relation.throughForeignKey];
1815
- const conditions = [(0, drizzle_orm_1.inArray)(referenced, wanted)];
1816
- conditions.push(...this.relatedRowScope(target, options));
1817
- const found = await tx
1818
- .select({ id: referenced })
1819
- .from(target)
1820
- .where((0, drizzle_orm_1.and)(...conditions));
1821
- const present = new Set(found.map((row) => this.valueKey(row.id)));
1822
- const missing = wanted.filter((id) => !present.has(this.valueKey(id)));
1895
+ const found = await this.matchKeys(tx, target, referenced, wanted, this.relatedRowScope(target, options));
1896
+ const missing = wanted.filter((_, at) => !found.has(at));
1823
1897
  if (missing.length > 0) {
1824
1898
  const name = this.tableName(target);
1825
1899
  throw new crud_exceptions_1.BulkOperationException("Unknown related ids", missing.map((id) => ({
@@ -1827,14 +1901,13 @@ class SqlBaseCrudService {
1827
1901
  error: new crud_exceptions_1.EntityNotFoundException(name, id),
1828
1902
  })));
1829
1903
  }
1904
+ const stored = this.distinct(wanted.map((_, at) => found.get(at)));
1830
1905
  const linked = fresh
1831
- ? []
1832
- : await tx
1833
- .select({ id: foreign })
1834
- .from(through)
1835
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(local, parentValue), (0, drizzle_orm_1.inArray)(foreign, wanted)));
1836
- const already = new Set(linked.map((row) => this.valueKey(row.id)));
1837
- const add = wanted.filter((id) => !already.has(this.valueKey(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));
1838
1911
  if (add.length > 0) {
1839
1912
  await tx.insert(through).values(add.map((id) => ({
1840
1913
  [relation.throughLocalKey]: parentValue,
@@ -1843,16 +1916,19 @@ class SqlBaseCrudService {
1843
1916
  }
1844
1917
  }
1845
1918
  async attach(id, relation, relatedIds, options) {
1846
- options = this.withScopeMemo(options);
1847
- 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
+ });
1848
1922
  }
1849
1923
  async detach(id, relation, relatedIds, options) {
1850
- options = this.withScopeMemo(options);
1851
- 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
+ });
1852
1927
  }
1853
1928
  async sync(id, relation, relatedIds, options) {
1854
- options = this.withScopeMemo(options);
1855
- 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
+ });
1856
1932
  }
1857
1933
  async changeLinks(operation, id, name, relatedIds, options) {
1858
1934
  const relation = this.relationNamed(name);
@@ -1872,7 +1948,7 @@ class SqlBaseCrudService {
1872
1948
  throw new common_1.BadRequestException(`${operation} takes an array of ids`);
1873
1949
  }
1874
1950
  this.checkReadOptions(options);
1875
- const ids = this.uniqueValues(relatedIds ?? []);
1951
+ const ids = this.distinct(relatedIds ?? []);
1876
1952
  const parentKey = this.parentKeyOf(relation);
1877
1953
  return this.executeSqlTransaction(async (tx) => {
1878
1954
  const opts = { ...options, transaction: tx };
@@ -1916,18 +1992,70 @@ class SqlBaseCrudService {
1916
1992
  return row;
1917
1993
  }, options?.transaction);
1918
1994
  }
1919
- valueKey(value) {
1920
- return String(value).toLowerCase();
1995
+ valueKey(value, column) {
1996
+ const key = String(value);
1997
+ return column?.columnType === "PgUUID" || this.config.dialect === "mysql"
1998
+ ? key.toLowerCase()
1999
+ : key;
1921
2000
  }
1922
- uniqueValues(values) {
1923
- const seen = new Set();
1924
- return values.filter((value) => {
1925
- const key = this.valueKey(value);
1926
- if (seen.has(key))
1927
- return false;
1928
- seen.add(key);
1929
- return true;
1930
- });
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);
1931
2059
  }
1932
2060
  tableName(table) {
1933
2061
  try {
@@ -1944,7 +2072,7 @@ class SqlBaseCrudService {
1944
2072
  for (const key of Object.keys(this.resolveScope(options) ?? {})) {
1945
2073
  delete mapped[key];
1946
2074
  }
1947
- const entityData = this.filterWritable(mapped, "update", options);
2075
+ const entityData = this.coerceDates(this.config.table, this.filterWritable(mapped, "update", options));
1948
2076
  const settable = Object.keys(entityData).filter((key) => entityData[key] !== undefined &&
1949
2077
  !!this.columnOf(this.config.table, key));
1950
2078
  if (settable.length === 0) {
@@ -1953,45 +2081,49 @@ class SqlBaseCrudService {
1953
2081
  return entityData;
1954
2082
  }
1955
2083
  async update(id, data, options) {
1956
- options = this.withScopeMemo(options);
1957
- const { transaction, hooks = {} } = options || {};
1958
- this.checkReadOptions(options);
1959
- const expected = this.expectedVersionOf(data, options);
1960
- const opts = { ...options, expectedVersion: expected };
1961
- const guarded = !this.skipsGuard(options, "validateUpdate", "mapUpdateDtoToEntity", ...(hooks.skipBefore ? [] : ["beforeUpdate"]));
1962
- if (guarded)
1963
- await this.guardRow(id, opts);
1964
- const entityData = await this.guardOnError(guarded, id, opts, async () => {
1965
- await this.validateUpdate(id, data);
1966
- const processedData = hooks.skipBefore
1967
- ? data
1968
- : await this.beforeUpdate(id, data);
1969
- return this.prepareUpdate(processedData, opts);
1970
- });
1971
- if (this.config.timestamps?.updatedAt) {
1972
- entityData[this.config.timestamps.updatedAt] = new Date();
1973
- }
1974
- const db = this.dbFor(transaction);
1975
- await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
1976
- try {
1977
- await this.guardOnError(guarded, id, opts, () => this.assertKeysInScope(db, entityData, opts));
1978
- const [updatedEntity] = await this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts, this.softDeleteScope(opts));
1979
- 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)
1980
2094
  await this.guardRow(id, opts);
1981
- if (expected !== undefined) {
1982
- 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");
1983
2116
  }
1984
- 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;
1985
2122
  }
1986
- if (!hooks.skipAfter)
1987
- await this.afterUpdate(updatedEntity);
1988
- await this.emitEvent("after", "update", false, { ids: [id], input: data, rows: [updatedEntity], transaction }, options);
1989
- await this.loadWriteRelations(db, [updatedEntity], options);
1990
- return updatedEntity;
1991
- }
1992
- catch (error) {
1993
- throw this.toClientError(error, "write");
1994
- }
2123
+ catch (error) {
2124
+ throw this.toClientError(error, "write");
2125
+ }
2126
+ });
1995
2127
  }
1996
2128
  async updateWhereIn(db, set, ids, options, extra) {
1997
2129
  const where = this.writeWhere(ids, options);
@@ -2061,27 +2193,28 @@ class SqlBaseCrudService {
2061
2193
  return false;
2062
2194
  }
2063
2195
  async softDelete(id, options) {
2064
- options = this.withScopeMemo(options);
2065
- if (!this.config.softDelete?.enabled) {
2066
- throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
2067
- }
2068
- const live = { ...options, withDeleted: false, onlyDeleted: false };
2069
- return this.guardedWrite(id, options, {
2070
- operation: "softDelete",
2071
- state: live,
2072
- hook: "beforeSoftDelete",
2073
- before: () => this.beforeSoftDelete(id),
2074
- write: async (db) => {
2075
- let query = db
2076
- .update(this.config.table)
2077
- .set(this.softDeleteSet(new Date()))
2078
- .where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(live)));
2079
- if (this.config.sql?.useReturning)
2080
- query = query.returning();
2081
- return this.wasAffected(await this.runRead(query)) || undefined;
2082
- },
2083
- miss: () => this.staleOrFalse(id, options),
2084
- after: () => this.afterSoftDelete(id),
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
+ });
2085
2218
  });
2086
2219
  }
2087
2220
  softDeleteSet(deletedAt) {
@@ -2093,42 +2226,44 @@ class SqlBaseCrudService {
2093
2226
  });
2094
2227
  }
2095
2228
  async restore(id, options) {
2096
- options = this.withScopeMemo(options);
2097
- if (!this.config.softDelete?.enabled) {
2098
- throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
2099
- }
2100
- const trashed = { ...options, withDeleted: false, onlyDeleted: true };
2101
- const restored = await this.guardedWrite(id, options, {
2102
- operation: "restore",
2103
- state: trashed,
2104
- hook: "beforeRestore",
2105
- before: () => this.beforeRestore(id),
2106
- write: async (db) => (await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options, this.softDeleteScope(trashed))))[0],
2107
- miss: () => {
2108
- if (options?.expectedVersion != null && this.config.versionColumn) {
2109
- throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
2110
- }
2111
- throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
2112
- },
2113
- after: (entity) => this.afterRestore(entity),
2114
- rows: (entity) => [entity],
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;
2115
2251
  });
2116
- await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
2117
- return restored;
2118
2252
  }
2119
2253
  async delete(id, options) {
2120
- options = this.withScopeMemo(options);
2121
- return this.guardedWrite(id, options, {
2122
- operation: "delete",
2123
- state: options,
2124
- hook: "beforeDelete",
2125
- before: () => this.beforeDelete(id),
2126
- write: async (db) => this.wasAffected(await db
2127
- .delete(this.config.table)
2128
- .where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(options)))) || undefined,
2129
- miss: () => this.staleOrFalse(id, options),
2130
- after: () => this.afterDelete(id),
2131
- errors: "delete",
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
+ });
2132
2267
  });
2133
2268
  }
2134
2269
  async forceDelete(id, options) {
@@ -2139,46 +2274,48 @@ class SqlBaseCrudService {
2139
2274
  });
2140
2275
  }
2141
2276
  async upsert(data, upsert = {}, options) {
2142
- options = this.withScopeMemo(options);
2143
- this.checkReadOptions(options);
2144
- const entityData = await this.prepareCreate(data, options);
2145
- const db = this.dbFor(options?.transaction);
2146
- await this.emitEvent("before", "upsert", false, { input: data, transaction: options?.transaction }, options);
2147
- let row;
2148
- try {
2149
- await this.assertKeysInScope(db, entityData, options);
2150
- [row] = await this.upsertRows(db, [entityData], upsert, options);
2151
- }
2152
- catch (error) {
2153
- throw this.toClientError(error, "write");
2154
- }
2155
- if (!row) {
2156
- throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(entityData, upsert));
2157
- }
2158
- await this.emitEvent("after", "upsert", false, {
2159
- ids: this.idsOf([row]),
2160
- input: data,
2161
- rows: [row],
2162
- transaction: options?.transaction,
2163
- }, options);
2164
- await this.loadWriteRelations(db, [row], options);
2165
- 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
+ });
2166
2302
  }
2167
2303
  async massUpsert(data, upsert = {}, options) {
2168
- options = this.withScopeMemo(options);
2169
- const message = "Mass upsert errors";
2170
- return this.massWrite({
2171
- operation: "upsert",
2172
- message,
2173
- options,
2174
- input: data,
2175
- returnsRows: true,
2176
- prepare: (opts) => data.map((item, index) => ({
2177
- index,
2178
- run: () => this.prepareCreate(item, opts),
2179
- })),
2180
- keys: true,
2181
- 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
+ });
2182
2319
  });
2183
2320
  }
2184
2321
  async upsertSet(tx, rows, upsert, opts, message) {
@@ -2189,7 +2326,9 @@ class SqlBaseCrudService {
2189
2326
  if (written.length >= rows.length)
2190
2327
  return written;
2191
2328
  const keys = this.conflictKeys(upsert);
2192
- const keyOf = (row) => keys.map((k) => this.valueKey(row[k])).join("|");
2329
+ const keyOf = (row) => keys
2330
+ .map((k) => this.valueKey(row[k], this.columnOf(this.config.table, k)))
2331
+ .join("|");
2193
2332
  const seen = new Set(written.map(keyOf));
2194
2333
  throw new crud_exceptions_1.BulkOperationException(message, rows.flatMap((row, index) => seen.has(keyOf(row))
2195
2334
  ? []
@@ -2331,39 +2470,41 @@ class SqlBaseCrudService {
2331
2470
  ids: ids ? unique : this.idsOf(written),
2332
2471
  rows: spec.returnsRows ? written : undefined,
2333
2472
  }, options);
2334
- if (spec.returnsRows)
2335
- await load(tx, written);
2473
+ if (spec.returnsRows) {
2474
+ await load(tx, written, prepared.some((row) => this.holdsSql(row)));
2475
+ }
2336
2476
  return ids ? this.inIdOrder(written, ids) : written;
2337
2477
  };
2338
2478
  return spec.returnsRows
2339
- ? this.writeThenLoad(options, body)
2479
+ ? this.writeThenLoad(spec.operation, options, body)
2340
2480
  : this.executeSqlTransaction((tx) => body(tx, async () => { }), options?.transaction);
2341
2481
  }
2342
2482
  async massCreate(data, options) {
2343
- options = this.withScopeMemo(options);
2344
- if (options?.bulkMode === "perItem") {
2345
- return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
2346
- }
2347
- const message = "Mass create errors";
2348
- return this.massWrite({
2349
- operation: "create",
2350
- message,
2351
- options,
2352
- input: data,
2353
- returnsRows: true,
2354
- prepare: (opts) => data.map((item, index) => ({
2355
- index,
2356
- run: () => this.prepareCreate(item, opts),
2357
- })),
2358
- keys: true,
2359
- write: (tx, rows, opts) => this.runSet(tx, message, (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
2360
- index,
2361
- run: (sp) => this.insertOne(sp, row, opts),
2362
- })), "write"),
2363
- after: async (created) => {
2364
- for (const entity of created)
2365
- await this.afterCreate(entity);
2366
- },
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
+ });
2367
2508
  });
2368
2509
  }
2369
2510
  async insertMany(db, rows, options) {
@@ -2413,35 +2554,36 @@ class SqlBaseCrudService {
2413
2554
  })));
2414
2555
  }
2415
2556
  async massUpdate(ids, data, options) {
2416
- options = this.withScopeMemo(options);
2417
- if (options?.bulkMode === "perItem") {
2418
- return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass update errors", options, ({ id }, opts) => this.update(id, data, opts));
2419
- }
2420
- const message = "Mass update errors";
2421
- const skipBefore = options?.hooks?.skipBefore;
2422
- return this.massWrite({
2423
- operation: "update",
2424
- message,
2425
- options,
2426
- ids,
2427
- input: data,
2428
- returnsRows: true,
2429
- prepare: (opts, unique) => unique.map((id) => ({
2430
- id,
2431
- run: async () => {
2432
- await this.validateUpdate(id, data);
2433
- const processed = skipBefore
2434
- ? data
2435
- : await this.beforeUpdate(id, data);
2436
- 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);
2437
2585
  },
2438
- })),
2439
- keys: true,
2440
- write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
2441
- after: async (updated) => {
2442
- for (const entity of updated)
2443
- await this.afterUpdate(entity);
2444
- },
2586
+ });
2445
2587
  });
2446
2588
  }
2447
2589
  async updateSets(tx, sets, unique, opts, message) {
@@ -2482,96 +2624,105 @@ class SqlBaseCrudService {
2482
2624
  : [];
2483
2625
  }
2484
2626
  async massSoftDelete(ids, options) {
2485
- options = this.withScopeMemo(options);
2486
- if (!this.config.softDelete?.enabled)
2487
- throw new common_1.BadRequestException("Soft delete not enabled");
2488
- if (options?.bulkMode === "perItem") {
2489
- 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
+ });
2490
2661
  return true;
2491
- }
2492
- const message = "Mass soft delete errors";
2493
- await this.massWrite({
2494
- operation: "softDelete",
2495
- message,
2496
- options,
2497
- ids,
2498
- state: { withDeleted: false, onlyDeleted: false },
2499
- returnsRows: false,
2500
- prepare: (_opts, unique) => options?.hooks?.skipBefore
2501
- ? undefined
2502
- : unique.map((id) => ({ id, run: () => this.beforeSoftDelete(id) })),
2503
- write: (tx, _prepared, opts, unique) => {
2504
- const set = this.softDeleteSet(new Date());
2505
- const update = (db, keys) => db
2506
- .update(this.config.table)
2507
- .set(set)
2508
- .where(this.writeWhere(keys, opts));
2509
- return this.runSet(tx, message, (sp) => this.runKeyed(update(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => update(sp, [id]) })), "write");
2510
- },
2511
- after: async (_written, unique) => {
2512
- for (const id of unique)
2513
- await this.afterSoftDelete(id);
2514
- },
2515
2662
  });
2516
- return true;
2517
2663
  }
2518
2664
  async massRestore(ids, options) {
2519
- options = this.withScopeMemo(options);
2520
- if (!this.config.softDelete?.enabled)
2521
- throw new common_1.BadRequestException("Soft delete not enabled");
2522
- if (options?.bulkMode === "perItem") {
2523
- return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass restore errors", options, ({ id }, opts) => this.restore(id, opts));
2524
- }
2525
- const message = "Mass restore errors";
2526
- return this.massWrite({
2527
- operation: "restore",
2528
- message,
2529
- options,
2530
- ids,
2531
- state: { withDeleted: true, onlyDeleted: false },
2532
- returnsRows: true,
2533
- prepare: (_opts, unique) => options?.hooks?.skipBefore
2534
- ? undefined
2535
- : unique.map((id) => ({ id, run: () => this.beforeRestore(id) })),
2536
- write: (tx, _prepared, opts, unique) => {
2537
- const set = this.softDeleteSet(null);
2538
- return this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts), unique.map((id) => ({
2539
- id,
2540
- run: (sp) => this.updateWhereIn(sp, set, [id], opts),
2541
- })), "write");
2542
- },
2543
- after: async (restored) => {
2544
- for (const entity of restored)
2545
- await this.afterRestore(entity);
2546
- },
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
+ });
2547
2695
  });
2548
2696
  }
2549
2697
  async massDelete(ids, options) {
2550
- options = this.withScopeMemo(options);
2551
- if (options?.bulkMode === "perItem") {
2552
- 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
+ });
2553
2724
  return true;
2554
- }
2555
- const message = "Mass delete errors";
2556
- await this.massWrite({
2557
- operation: "delete",
2558
- message,
2559
- options,
2560
- ids,
2561
- returnsRows: false,
2562
- prepare: (_opts, unique) => options?.hooks?.skipBefore
2563
- ? undefined
2564
- : unique.map((id) => ({ id, run: () => this.beforeDelete(id) })),
2565
- write: (tx, _prepared, opts, unique) => {
2566
- const remove = (db, keys) => db.delete(this.config.table).where(this.writeWhere(keys, opts));
2567
- return this.runSet(tx, message, (sp) => this.runKeyed(remove(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => remove(sp, [id]) })), "delete");
2568
- },
2569
- after: async (_written, unique) => {
2570
- for (const id of unique)
2571
- await this.afterDelete(id);
2572
- },
2573
2725
  });
2574
- return true;
2575
2726
  }
2576
2727
  async massForceDelete(ids, options) {
2577
2728
  return this.massDelete(ids, {
@@ -2580,6 +2731,10 @@ class SqlBaseCrudService {
2580
2731
  onlyDeleted: false,
2581
2732
  });
2582
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
+ }
2583
2738
  uniqueIds(ids) {
2584
2739
  const seen = new Set();
2585
2740
  return (ids ?? []).filter((id) => {
@@ -2591,7 +2746,34 @@ class SqlBaseCrudService {
2591
2746
  });
2592
2747
  }
2593
2748
  fingerprint(value) {
2594
- return JSON.stringify(value, (_key, v) => typeof v === "bigint" ? `${v}n` : 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
+ }
2757
+ if (typeof v === "string")
2758
+ return `s:${v}`;
2759
+ if (typeof v === "bigint")
2760
+ return `bigint:${v}`;
2761
+ if (typeof v === "number" && !Number.isFinite(v)) {
2762
+ return `number:${v}`;
2763
+ }
2764
+ if (v !== null && typeof v === "object" && !Array.isArray(v)) {
2765
+ const proto = Object.getPrototypeOf(v);
2766
+ if (proto !== Object.prototype && proto !== null) {
2767
+ let tag = fingerprintTags.get(v);
2768
+ if (!tag) {
2769
+ tag = `opaque:${++fingerprintCount}`;
2770
+ fingerprintTags.set(v, tag);
2771
+ }
2772
+ return tag;
2773
+ }
2774
+ }
2775
+ return json;
2776
+ });
2595
2777
  }
2596
2778
  async assertAllExist(tx, ids, options, message) {
2597
2779
  const table = this.config.table;
@@ -2726,44 +2908,50 @@ class SqlBaseCrudService {
2726
2908
  : run(tx);
2727
2909
  }
2728
2910
  async exists(id, options) {
2729
- options = this.withScopeMemo(options);
2730
- this.checkReadOptions({ ...options, select: undefined });
2731
- const entity = await this.find(id, {
2732
- ...options,
2733
- relations: [],
2734
- counts: undefined,
2735
- 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;
2736
2920
  });
2737
- return !!entity;
2738
2921
  }
2739
2922
  async count(filters, options) {
2740
- options = this.withScopeMemo(options);
2741
- this.checkReadOptions(options);
2742
- const db = this.dbFor(options?.transaction);
2743
- const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
2744
- const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
2745
- 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
+ });
2746
2930
  }
2747
2931
  async countBy(column, filters, options) {
2748
- options = this.withScopeMemo(options);
2749
- this.checkReadOptions(options);
2750
- const db = this.dbFor(options?.transaction);
2751
- const spec = this.resolveGroupColumn(column, options);
2752
- const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
2753
- const joins = Array.from(new Set([...filterRelations, ...(spec.relation ? [spec.relation] : [])]));
2754
- const total = (0, drizzle_orm_1.sql) `count(*)`;
2755
- let query = db
2756
- .select({ value: spec.column, count: total })
2757
- .from(this.config.table);
2758
- query = this.applyJoins(query, joins, options);
2759
- if (conditions.length > 0)
2760
- query = query.where((0, drizzle_orm_1.and)(...conditions));
2761
- query = query.groupBy(spec.column).orderBy((0, drizzle_orm_1.desc)(total), (0, drizzle_orm_1.asc)(spec.column));
2762
- const rows = await this.runRead(query);
2763
- return rows.map((row) => ({
2764
- value: row.value,
2765
- count: Number(row.count),
2766
- }));
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
+ });
2767
2955
  }
2768
2956
  resolveGroupColumn(key, options) {
2769
2957
  if (typeof key !== "string" || !key) {
@@ -3297,49 +3485,50 @@ class SqlBaseCrudService {
3297
3485
  }
3298
3486
  }
3299
3487
  async fullTextSearch(searchTerm, searchColumns, pagination, options) {
3300
- options = this.withScopeMemo(options);
3301
- if (this.config.dialect !== "postgresql") {
3302
- throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
3303
- }
3304
- if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
3305
- throw new common_1.BadRequestException("Full-text search needs at least one column");
3306
- }
3307
- const columns = searchColumns.map((name) => {
3308
- const column = this.columnOf(this.config.table, name);
3309
- if (!column) {
3310
- 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");
3311
3491
  }
3312
- this.assertAllowed("searchableColumns", name, options);
3313
- return column;
3314
- });
3315
- const { filters, language, queryMode, ...readOptions } = options ?? {};
3316
- const { vector, query } = this.fullTextParts(columns, searchTerm, {
3317
- language,
3318
- 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 };
3319
3531
  });
3320
- const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
3321
- const plan = this.planList(filters, undefined, "asc", readOptions);
3322
- const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
3323
- const joins = Array.from(new Set([
3324
- ...plan.joined.map((node) => node.name),
3325
- ...plan.filterRelations,
3326
- ]));
3327
- const pk = this.columnOf(this.config.table, this.config.primaryKey);
3328
- const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
3329
- if (pk)
3330
- orderBy.push((0, drizzle_orm_1.asc)(pk));
3331
- let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
3332
- dataQuery = this.applyJoins(dataQuery, joins, readOptions)
3333
- .where((0, drizzle_orm_1.and)(...conditions))
3334
- .orderBy(...orderBy)
3335
- .limit(limit)
3336
- .offset(offset);
3337
- const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
3338
- const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
3339
- const total = this.parseCount(totalResult);
3340
- const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
3341
- await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
3342
- return { data: rows, total, page, limit };
3343
3532
  }
3344
3533
  }
3345
3534
  exports.SqlBaseCrudService = SqlBaseCrudService;