@avytrix/nestjs-drizzle-crud 3.5.1 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -196,6 +196,18 @@ class SqlBaseCrudService {
196
196
  return [];
197
197
  return Object.entries(values).map(([key, value]) => value === null ? (0, drizzle_orm_1.isNull)(table[key]) : (0, drizzle_orm_1.eq)(table[key], value));
198
198
  }
199
+ relatedRowScope(target, options) {
200
+ const out = this.relatedScopeConditions(target, options);
201
+ const softDeleted = this.softDeleteCondition(target);
202
+ if (softDeleted)
203
+ out.push(softDeleted);
204
+ return out;
205
+ }
206
+ relatedScopeConditions(target, options) {
207
+ return Object.entries(this.resolveScope(options) ?? {})
208
+ .filter(([key]) => this.columnOf(target, key))
209
+ .map(([key, value]) => value === null ? (0, drizzle_orm_1.isNull)(target[key]) : (0, drizzle_orm_1.eq)(target[key], value));
210
+ }
199
211
  applyScopeToRow(row, options) {
200
212
  const values = this.resolveScope(options);
201
213
  if (values)
@@ -497,7 +509,7 @@ class SqlBaseCrudService {
497
509
  }
498
510
  return Object.keys(fields).length > 0 ? fields : undefined;
499
511
  }
500
- async loadRelations(db, hosts, nodes, select = { base: [], relations: {} }, hostTable = this.config.table) {
512
+ async loadRelations(db, hosts, nodes, select = { base: [], relations: {} }, hostTable = this.config.table, options) {
501
513
  if (!nodes.length || !Array.isArray(hosts))
502
514
  return;
503
515
  const rows = hosts.filter((row) => row && typeof row === "object");
@@ -508,13 +520,13 @@ class SqlBaseCrudService {
508
520
  if (!node.joined) {
509
521
  switch (this.relationType(node.config)) {
510
522
  case "hasMany":
511
- await this.loadHasMany(db, rows, node, childSelect, hostTable);
523
+ await this.loadHasMany(db, rows, node, childSelect, hostTable, options);
512
524
  break;
513
525
  case "manyToMany":
514
- await this.loadManyToMany(db, rows, node, childSelect, hostTable);
526
+ await this.loadManyToMany(db, rows, node, childSelect, hostTable, options);
515
527
  break;
516
528
  default:
517
- await this.loadBelongsTo(db, rows, node, childSelect);
529
+ await this.loadBelongsTo(db, rows, node, childSelect, options);
518
530
  }
519
531
  }
520
532
  if (node.children.length === 0)
@@ -522,10 +534,10 @@ class SqlBaseCrudService {
522
534
  const nextHosts = this.isCollection(node.config)
523
535
  ? rows.flatMap((row) => row[node.name] ?? [])
524
536
  : rows.map((row) => row[node.name]);
525
- await this.loadRelations(db, nextHosts, node.children, childSelect, node.config.table);
537
+ await this.loadRelations(db, nextHosts, node.children, childSelect, node.config.table, options);
526
538
  }));
527
539
  }
528
- async loadBelongsTo(db, hosts, node, select) {
540
+ async loadBelongsTo(db, hosts, node, select, options) {
529
541
  const { localKey, references } = node.config;
530
542
  if (!localKey)
531
543
  return;
@@ -545,9 +557,7 @@ class SqlBaseCrudService {
545
557
  ...this.keysForChildren(node.children, target),
546
558
  ]);
547
559
  const conditions = [(0, drizzle_orm_1.inArray)(referenced, ids)];
548
- const softDeleted = this.softDeleteCondition(target);
549
- if (softDeleted)
550
- conditions.push(softDeleted);
560
+ conditions.push(...this.relatedRowScope(target, options));
551
561
  const related = await (fields ? db.select(fields) : db.select())
552
562
  .from(target)
553
563
  .where((0, drizzle_orm_1.and)(...conditions));
@@ -558,7 +568,7 @@ class SqlBaseCrudService {
558
568
  host[node.name] = byKey.get(host[localKey]) ?? null;
559
569
  }
560
570
  }
561
- async loadHasMany(db, hosts, node, select, hostTable = this.config.table) {
571
+ async loadHasMany(db, hosts, node, select, hostTable = this.config.table, options) {
562
572
  const { foreignKey } = node.config;
563
573
  if (!foreignKey)
564
574
  return;
@@ -578,9 +588,7 @@ class SqlBaseCrudService {
578
588
  ...this.keysForChildren(node.children, target),
579
589
  ]);
580
590
  const conditions = [(0, drizzle_orm_1.inArray)(foreign, ids)];
581
- const softDeleted = this.softDeleteCondition(target);
582
- if (softDeleted)
583
- conditions.push(softDeleted);
591
+ conditions.push(...this.relatedRowScope(target, options));
584
592
  let query = (fields ? db.select(fields) : db.select())
585
593
  .from(target)
586
594
  .where((0, drizzle_orm_1.and)(...conditions));
@@ -590,7 +598,7 @@ class SqlBaseCrudService {
590
598
  const children = await query;
591
599
  this.groupCollection(hosts, node.name, parentKey, children, foreignKey);
592
600
  }
593
- async loadManyToMany(db, hosts, node, select, hostTable = this.config.table) {
601
+ async loadManyToMany(db, hosts, node, select, hostTable = this.config.table, options) {
594
602
  const { through, throughLocalKey, throughForeignKey, references } = node.config;
595
603
  if (!through || !throughLocalKey || !throughForeignKey)
596
604
  return;
@@ -618,9 +626,7 @@ class SqlBaseCrudService {
618
626
  fields[name] = columns[name];
619
627
  }
620
628
  const conditions = [(0, drizzle_orm_1.inArray)(joinLocal, ids)];
621
- const softDeleted = this.softDeleteCondition(target);
622
- if (softDeleted)
623
- conditions.push(softDeleted);
629
+ conditions.push(...this.relatedRowScope(target, options));
624
630
  let query = db
625
631
  .select(fields)
626
632
  .from(through)
@@ -650,7 +656,7 @@ class SqlBaseCrudService {
650
656
  host[name] = grouped.get(host[parentKey]) ?? [];
651
657
  }
652
658
  }
653
- applyJoins(query, relationNames) {
659
+ applyJoins(query, relationNames, options) {
654
660
  const configured = this.config.relations || {};
655
661
  let q = query;
656
662
  for (const name of relationNames) {
@@ -658,8 +664,7 @@ class SqlBaseCrudService {
658
664
  if (!rel?.localKey || this.isCollection(rel))
659
665
  continue;
660
666
  const on = (0, drizzle_orm_1.eq)(this.config.table[rel.localKey], rel.table[rel.references ?? "id"]);
661
- const softDeleted = this.softDeleteCondition(rel.table);
662
- q = q.leftJoin(rel.table, softDeleted ? (0, drizzle_orm_1.and)(on, softDeleted) : on);
667
+ q = q.leftJoin(rel.table, (0, drizzle_orm_1.and)(on, ...this.relatedRowScope(rel.table, options)));
663
668
  }
664
669
  return q;
665
670
  }
@@ -681,13 +686,13 @@ class SqlBaseCrudService {
681
686
  return out;
682
687
  });
683
688
  }
684
- buildOrderBy(sortBy, sortOrder = "desc") {
689
+ buildOrderBy(sortBy, sortOrder = "asc") {
685
690
  return this.resolveSort(sortBy, sortOrder).map((spec) => this.orderOf(spec));
686
691
  }
687
692
  orderOf(spec) {
688
693
  return spec.order === "desc" ? (0, drizzle_orm_1.desc)(spec.column) : (0, drizzle_orm_1.asc)(spec.column);
689
694
  }
690
- resolveSort(sortBy, sortOrder = "desc", options) {
695
+ resolveSort(sortBy, sortOrder = "asc", options) {
691
696
  if (sortOrder !== "asc" && sortOrder !== "desc") {
692
697
  throw new common_1.BadRequestException(`Invalid sortOrder: ${sortOrder} (expected 'asc' or 'desc')`);
693
698
  }
@@ -800,14 +805,18 @@ class SqlBaseCrudService {
800
805
  }
801
806
  isUniqueViolation(error) {
802
807
  const code = error?.code ?? error?.cause?.code;
803
- return code === "23505";
808
+ const errno = error?.errno ?? error?.cause?.errno;
809
+ return code === "23505" || code === "ER_DUP_ENTRY" || errno === 1062;
804
810
  }
805
811
  toDuplicateException(error) {
806
812
  const detail = error?.detail ?? error?.cause?.detail ?? "";
807
- const m = detail.match(/Key \((?<field>.+?)\)=\((?<value>.+?)\)/);
813
+ const message = error?.cause?.message ?? error?.message ?? "";
814
+ const m = detail.match(/Key \((?<field>.+?)\)=\((?<value>.+?)\)/) ??
815
+ message.match(/Duplicate entry '(?<value>.*)' for key '(?:[^']*\.)?(?<field>[^']+)'/);
808
816
  return new crud_exceptions_1.DuplicateEntityException(this.getEntityName(), m?.groups?.field ?? "unique field", m?.groups?.value ?? "");
809
817
  }
810
818
  static DATA_ERRORS = {
819
+ "23502": "a required value is missing",
811
820
  "22001": "a value is too long for its column",
812
821
  "23514": "a value violates a column check constraint",
813
822
  "22003": "a numeric value is out of range",
@@ -822,7 +831,13 @@ class SqlBaseCrudService {
822
831
  }
823
832
  toDataException(error) {
824
833
  const code = error?.code ?? error?.cause?.code;
825
- return new crud_exceptions_1.ValidationFailedException(SqlBaseCrudService.DATA_ERRORS[code] ?? "invalid value");
834
+ const message = SqlBaseCrudService.DATA_ERRORS[code] ?? "invalid value";
835
+ const cause = error?.cause ?? error;
836
+ const column = error?.column ??
837
+ cause?.column ??
838
+ error?.column_name ??
839
+ cause?.column_name;
840
+ return new crud_exceptions_1.ValidationFailedException(column ? `${message}: ${column}` : message);
826
841
  }
827
842
  toClientError(error, operation = "write") {
828
843
  if (this.isUniqueViolation(error))
@@ -846,7 +861,8 @@ class SqlBaseCrudService {
846
861
  }
847
862
  isForeignKeyViolation(error) {
848
863
  const code = error?.code ?? error?.cause?.code;
849
- return code === "23503";
864
+ const errno = error?.errno ?? error?.cause?.errno;
865
+ return code === "23503" || errno === 1451 || errno === 1452;
850
866
  }
851
867
  toForeignKeyException(error) {
852
868
  const detail = error?.detail ?? error?.cause?.detail ?? "";
@@ -876,7 +892,7 @@ class SqlBaseCrudService {
876
892
  return undefined;
877
893
  if (search.mode === "fullText") {
878
894
  if (this.config.dialect !== "postgresql") {
879
- throw new Error("Full-text search only supported in PostgreSQL");
895
+ throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
880
896
  }
881
897
  const tsVector = drizzle_orm_1.sql.join(columns.map((column) => (0, drizzle_orm_1.sql) `to_tsvector('english', ${column})`), (0, drizzle_orm_1.sql) ` || `);
882
898
  const tsQuery = (0, drizzle_orm_1.sql) `plainto_tsquery('english', ${term})`;
@@ -1008,14 +1024,14 @@ class SqlBaseCrudService {
1008
1024
  const baseSelect = this.withCountKeys(this.withRequiredKeys(selection.base, nodes), options?.counts);
1009
1025
  conditions.push(...this.rowScope(options));
1010
1026
  let query = this.startSelect(db, baseSelect, joined, false, selection);
1011
- query = this.applyJoins(query, joined.map((node) => node.name))
1027
+ query = this.applyJoins(query, joined.map((node) => node.name), options)
1012
1028
  .where((0, drizzle_orm_1.and)(...conditions))
1013
1029
  .limit(1);
1014
1030
  query = this.applyLock(query, options);
1015
1031
  const result = await this.runRead(query);
1016
1032
  const rows = this.normalizeRelations(result, joined.map((node) => node.name));
1017
1033
  await Promise.all([
1018
- this.loadRelations(db, rows, nodes, selection),
1034
+ this.loadRelations(db, rows, nodes, selection, undefined, options),
1019
1035
  this.loadCounts(db, rows, options?.counts, options),
1020
1036
  ]);
1021
1037
  return rows[0] || null;
@@ -1055,22 +1071,22 @@ class SqlBaseCrudService {
1055
1071
  built.conditions.push(...this.rowScope(options));
1056
1072
  return built;
1057
1073
  }
1058
- buildCountQuery(db, conditions, filterRelations) {
1074
+ buildCountQuery(db, conditions, filterRelations, options) {
1059
1075
  let query = db
1060
1076
  .select({ count: (0, drizzle_orm_1.sql) `count(*)` })
1061
1077
  .from(this.config.table);
1062
- query = this.applyJoins(query, filterRelations);
1078
+ query = this.applyJoins(query, filterRelations, options);
1063
1079
  if (conditions.length > 0)
1064
1080
  query = query.where((0, drizzle_orm_1.and)(...conditions));
1065
1081
  return query;
1066
1082
  }
1067
1083
  async findAll(filters, pagination, options) {
1068
- const { sortBy, sortOrder = "desc" } = pagination || {};
1084
+ const { sortBy, sortOrder = "asc" } = pagination || {};
1069
1085
  const withCount = pagination?.withCount !== false;
1070
1086
  const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
1071
1087
  const plan = this.planList(filters, sortBy, sortOrder, options);
1072
1088
  let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, plan.joinRelations.length > 0, plan.selection);
1073
- dataQuery = this.applyJoins(dataQuery, plan.joinRelations);
1089
+ dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1074
1090
  if (plan.conditions.length > 0) {
1075
1091
  dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...plan.conditions));
1076
1092
  }
@@ -1082,13 +1098,13 @@ class SqlBaseCrudService {
1082
1098
  .offset(offset);
1083
1099
  dataQuery = this.applyLock(dataQuery, options);
1084
1100
  const countQuery = withCount
1085
- ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations)
1101
+ ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1086
1102
  : Promise.resolve(undefined);
1087
1103
  const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
1088
1104
  const data = fetched.slice(0, safeLimit);
1089
1105
  const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
1090
1106
  await Promise.all([
1091
- this.loadRelations(plan.db, rows, plan.nodes, plan.selection),
1107
+ this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
1092
1108
  this.loadCounts(plan.db, rows, options?.counts, options),
1093
1109
  ]);
1094
1110
  if (!withCount) {
@@ -1108,7 +1124,7 @@ class SqlBaseCrudService {
1108
1124
  };
1109
1125
  }
1110
1126
  async findByCursor(filters, pagination, options) {
1111
- const { sortBy, sortOrder = "desc", count = false } = pagination || {};
1127
+ const { sortBy, sortOrder = "asc", count = false } = pagination || {};
1112
1128
  const { limit } = this.resolvePagination(1, pagination?.limit);
1113
1129
  const plan = this.planList(filters, sortBy, sortOrder, options);
1114
1130
  const signature = plan.sort
@@ -1128,7 +1144,7 @@ class SqlBaseCrudService {
1128
1144
  fields[`${CURSOR_KEY}${i}`] = this.cursorField(spec.column);
1129
1145
  });
1130
1146
  let dataQuery = plan.db.select(fields).from(this.config.table);
1131
- dataQuery = this.applyJoins(dataQuery, plan.joinRelations);
1147
+ dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
1132
1148
  if (conditions.length > 0)
1133
1149
  dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
1134
1150
  dataQuery = dataQuery
@@ -1138,7 +1154,7 @@ class SqlBaseCrudService {
1138
1154
  const [data, totalResult] = await this.runRead(Promise.all([
1139
1155
  dataQuery,
1140
1156
  count
1141
- ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations)
1157
+ ? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
1142
1158
  : Promise.resolve(undefined),
1143
1159
  ]));
1144
1160
  const page = data.slice(0, limit);
@@ -1153,7 +1169,7 @@ class SqlBaseCrudService {
1153
1169
  }
1154
1170
  const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
1155
1171
  await Promise.all([
1156
- this.loadRelations(plan.db, rows, plan.nodes, plan.selection),
1172
+ this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
1157
1173
  this.loadCounts(plan.db, rows, options?.counts, options),
1158
1174
  ]);
1159
1175
  const out = { data: rows, limit, nextCursor };
@@ -1229,7 +1245,7 @@ class SqlBaseCrudService {
1229
1245
  }
1230
1246
  const hosts = rows.filter(Boolean);
1231
1247
  await Promise.all([
1232
- this.loadRelations(db, hosts, nodes),
1248
+ this.loadRelations(db, hosts, nodes, undefined, undefined, options),
1233
1249
  this.loadCounts(db, hosts, options?.counts, options),
1234
1250
  ]);
1235
1251
  }
@@ -1391,8 +1407,13 @@ class SqlBaseCrudService {
1391
1407
  relatedRow(table, value, options) {
1392
1408
  const out = {};
1393
1409
  for (const [key, item] of Object.entries(value)) {
1394
- if (item !== undefined && this.columnOf(table, key))
1395
- out[key] = item;
1410
+ const column = this.columnOf(table, key);
1411
+ if (item === undefined ||
1412
+ !column ||
1413
+ (column.primary && column.hasDefault)) {
1414
+ continue;
1415
+ }
1416
+ out[key] = item;
1396
1417
  }
1397
1418
  const softDelete = this.config.softDelete;
1398
1419
  if (softDelete?.enabled)
@@ -1421,14 +1442,7 @@ class SqlBaseCrudService {
1421
1442
  const local = through[relation.throughLocalKey];
1422
1443
  const foreign = through[relation.throughForeignKey];
1423
1444
  const conditions = [(0, drizzle_orm_1.inArray)(referenced, wanted)];
1424
- const softDeleted = this.softDeleteCondition(target);
1425
- if (softDeleted)
1426
- conditions.push(softDeleted);
1427
- for (const [key, value] of Object.entries(this.resolveScope(options) ?? {})) {
1428
- if (!this.columnOf(target, key))
1429
- continue;
1430
- conditions.push(value === null ? (0, drizzle_orm_1.isNull)(target[key]) : (0, drizzle_orm_1.eq)(target[key], value));
1431
- }
1445
+ conditions.push(...this.relatedRowScope(target, options));
1432
1446
  const found = await tx
1433
1447
  .select({ id: referenced })
1434
1448
  .from(target)
@@ -1636,7 +1650,7 @@ class SqlBaseCrudService {
1636
1650
  }
1637
1651
  async softDelete(id, options) {
1638
1652
  if (!this.config.softDelete?.enabled) {
1639
- throw new Error("Soft delete is not enabled for this entity");
1653
+ throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
1640
1654
  }
1641
1655
  const { transaction, hooks = {} } = options || {};
1642
1656
  const existing = await this.findForGuard(id, {
@@ -1678,18 +1692,16 @@ class SqlBaseCrudService {
1678
1692
  }
1679
1693
  async restore(id, options) {
1680
1694
  if (!this.config.softDelete?.enabled) {
1681
- throw new Error("Soft delete is not enabled for this entity");
1695
+ throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
1682
1696
  }
1683
1697
  const { transaction, hooks = {} } = options || {};
1684
- if (options?.expectedVersion != null && this.config.versionColumn) {
1685
- const existing = await this.findForGuard(id, {
1686
- ...options,
1687
- withDeleted: true,
1688
- onlyDeleted: false,
1689
- });
1690
- if (!existing)
1691
- throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
1692
- }
1698
+ const existing = await this.findForGuard(id, {
1699
+ ...options,
1700
+ withDeleted: false,
1701
+ onlyDeleted: true,
1702
+ });
1703
+ if (!existing)
1704
+ throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
1693
1705
  if (!hooks.skipBefore)
1694
1706
  await this.beforeRestore(id);
1695
1707
  await this.emitEvent("before", "restore", false, { ids: [id], transaction }, options);
@@ -1769,6 +1781,7 @@ class SqlBaseCrudService {
1769
1781
  }
1770
1782
  async massUpsert(data, upsert = {}, options) {
1771
1783
  this.checkReadOptions(options);
1784
+ this.assertBatchSize(this.insertParameters(data));
1772
1785
  const message = "Mass upsert errors";
1773
1786
  return this.executeSqlTransaction(async (tx) => {
1774
1787
  const opts = { ...options, transaction: tx };
@@ -1873,21 +1886,25 @@ class SqlBaseCrudService {
1873
1886
  statement = insert.onDuplicateKeyUpdate({ set });
1874
1887
  }
1875
1888
  else {
1876
- const scoped = this.scopeConditions(options);
1889
+ const guard = [
1890
+ ...this.scopeConditions(options),
1891
+ ...[this.softDeleteCondition(table)].filter(Boolean),
1892
+ ];
1877
1893
  statement = insert.onConflictDoUpdate({
1878
1894
  target: targets.length === 1 ? targets[0] : targets,
1879
1895
  set,
1880
- ...(scoped.length > 0 && { where: (0, drizzle_orm_1.and)(...scoped) }),
1896
+ ...(guard.length > 0 && { where: (0, drizzle_orm_1.and)(...guard) }),
1881
1897
  });
1882
1898
  }
1883
1899
  if (!readBack)
1884
1900
  return statement.returning();
1885
1901
  await statement;
1886
1902
  const match = rows.map((row) => (0, drizzle_orm_1.and)(...targetKeys.map((key, i) => (0, drizzle_orm_1.eq)(targets[i], row[key]))));
1903
+ const softDeleted = this.softDeleteCondition(table);
1887
1904
  return db
1888
1905
  .select()
1889
1906
  .from(table)
1890
- .where(match.length === 1 ? match[0] : (0, drizzle_orm_1.or)(...match));
1907
+ .where((0, drizzle_orm_1.and)(match.length === 1 ? match[0] : (0, drizzle_orm_1.or)(...match), ...this.scopeConditions(options), ...(softDeleted ? [softDeleted] : [])));
1891
1908
  }
1892
1909
  excludedValue(column) {
1893
1910
  return this.config.dialect === "mysql"
@@ -1899,6 +1916,7 @@ class SqlBaseCrudService {
1899
1916
  return this.massCreatePerItem(data, options);
1900
1917
  }
1901
1918
  this.checkReadOptions(options);
1919
+ this.assertBatchSize(this.insertParameters(data));
1902
1920
  return this.executeSqlTransaction(async (tx) => {
1903
1921
  const opts = { ...options, transaction: tx };
1904
1922
  const rows = await this.eachItem(data.map((item, index) => ({
@@ -1957,6 +1975,7 @@ class SqlBaseCrudService {
1957
1975
  if (options?.bulkMode === "perItem") {
1958
1976
  return this.massUpdatePerItem(ids, data, options);
1959
1977
  }
1978
+ this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
1960
1979
  this.checkReadOptions(options);
1961
1980
  const message = "Mass update errors";
1962
1981
  return this.executeSqlTransaction(async (tx) => {
@@ -2020,10 +2039,11 @@ class SqlBaseCrudService {
2020
2039
  }
2021
2040
  async massSoftDelete(ids, options) {
2022
2041
  if (!this.config.softDelete?.enabled)
2023
- throw new Error("Soft delete not enabled");
2042
+ throw new common_1.BadRequestException("Soft delete not enabled");
2024
2043
  if (options?.bulkMode === "perItem") {
2025
2044
  return this.massSoftDeletePerItem(ids, options);
2026
2045
  }
2046
+ this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
2027
2047
  const message = "Mass soft delete errors";
2028
2048
  return this.executeSqlTransaction(async (tx) => {
2029
2049
  const opts = {
@@ -2063,10 +2083,11 @@ class SqlBaseCrudService {
2063
2083
  }
2064
2084
  async massRestore(ids, options) {
2065
2085
  if (!this.config.softDelete?.enabled)
2066
- throw new Error("Soft delete not enabled");
2086
+ throw new common_1.BadRequestException("Soft delete not enabled");
2067
2087
  if (options?.bulkMode === "perItem") {
2068
2088
  return this.massRestorePerItem(ids, options);
2069
2089
  }
2090
+ this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
2070
2091
  this.checkReadOptions(options);
2071
2092
  const message = "Mass restore errors";
2072
2093
  return this.executeSqlTransaction(async (tx) => {
@@ -2104,6 +2125,7 @@ class SqlBaseCrudService {
2104
2125
  if (options?.bulkMode === "perItem") {
2105
2126
  return this.massDeletePerItem(ids, options);
2106
2127
  }
2128
+ this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
2107
2129
  const message = "Mass delete errors";
2108
2130
  return this.executeSqlTransaction(async (tx) => {
2109
2131
  const opts = { ...options, transaction: tx };
@@ -2295,7 +2317,7 @@ class SqlBaseCrudService {
2295
2317
  this.checkReadOptions(options);
2296
2318
  const db = options?.transaction || this.config.db;
2297
2319
  const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
2298
- const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations));
2320
+ const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
2299
2321
  return parseInt(result[0]?.count?.toString() || "0", 10);
2300
2322
  }
2301
2323
  async countBy(column, filters, options) {
@@ -2308,7 +2330,7 @@ class SqlBaseCrudService {
2308
2330
  let query = db
2309
2331
  .select({ value: spec.column, count: total })
2310
2332
  .from(this.config.table);
2311
- query = this.applyJoins(query, joins);
2333
+ query = this.applyJoins(query, joins, options);
2312
2334
  if (conditions.length > 0)
2313
2335
  query = query.where((0, drizzle_orm_1.and)(...conditions));
2314
2336
  query = query.groupBy(spec.column).orderBy((0, drizzle_orm_1.desc)(total), (0, drizzle_orm_1.asc)(spec.column));
@@ -2418,9 +2440,7 @@ class SqlBaseCrudService {
2418
2440
  root: false,
2419
2441
  }, filter, db, new Set(), options));
2420
2442
  }
2421
- const softDeleted = this.softDeleteCondition(target);
2422
- if (softDeleted)
2423
- conditions.push(softDeleted);
2443
+ conditions.push(...this.relatedRowScope(target, options));
2424
2444
  const total = (0, drizzle_orm_1.sql) `count(*)`;
2425
2445
  if (this.relationType(relation) === "manyToMany") {
2426
2446
  const joinLocal = relation.through[relation.throughLocalKey];
@@ -2442,6 +2462,7 @@ class SqlBaseCrudService {
2442
2462
  }
2443
2463
  pushColumnCondition(conditions, column, value, caseInsensitive = this.config.sql?.caseSensitive === false) {
2444
2464
  if (Array.isArray(value)) {
2465
+ this.assertBatchSize(value.length);
2445
2466
  conditions.push((0, drizzle_orm_1.inArray)(column, value.map((item) => this.coerceFilterValue(column, item))));
2446
2467
  }
2447
2468
  else if ((0, merge_config_1.isPlainObject)(value)) {
@@ -2463,9 +2484,9 @@ class SqlBaseCrudService {
2463
2484
  const targetConditions = this.compileFilterAt(this.relationHost(relation, relation.table, name), value, db, new Set(), options);
2464
2485
  if (targetConditions.length === 0)
2465
2486
  return undefined;
2466
- return this.collectionCondition(relation, targetConditions, db, this.rootHost());
2487
+ return this.collectionCondition(relation, targetConditions, db, this.rootHost(), options);
2467
2488
  }
2468
- collectionCondition(relation, targetConditions, db, host) {
2489
+ collectionCondition(relation, targetConditions, db, host, options) {
2469
2490
  const target = relation.table;
2470
2491
  const manyToMany = this.relationType(relation) === "manyToMany";
2471
2492
  const joinLocal = manyToMany
@@ -2475,9 +2496,7 @@ class SqlBaseCrudService {
2475
2496
  if (!key)
2476
2497
  return undefined;
2477
2498
  const conditions = [...targetConditions];
2478
- const softDeleted = this.softDeleteCondition(target);
2479
- if (softDeleted)
2480
- conditions.push(softDeleted);
2499
+ conditions.push(...this.relatedRowScope(target, options));
2481
2500
  let subquery;
2482
2501
  if (manyToMany) {
2483
2502
  const referenced = target[relation.references ?? "id"];
@@ -2506,9 +2525,7 @@ class SqlBaseCrudService {
2506
2525
  return undefined;
2507
2526
  const parentKey = this.parentKeyOf(relation, host.source);
2508
2527
  const conditions = [];
2509
- const softDeleted = this.softDeleteCondition(target);
2510
- if (softDeleted)
2511
- conditions.push(softDeleted);
2528
+ conditions.push(...this.relatedRowScope(target, options));
2512
2529
  if (compiled.length > 0) {
2513
2530
  const matched = (0, drizzle_orm_1.and)(...compiled);
2514
2531
  conditions.push(quantifier === "every"
@@ -2537,11 +2554,10 @@ class SqlBaseCrudService {
2537
2554
  const compiled = this.compileFilterAt(this.relationHost(relation, target, name), inner, db, new Set(), options);
2538
2555
  if (compiled.length === 0)
2539
2556
  return undefined;
2540
- const softDeleted = this.softDeleteCondition(target);
2541
2557
  return (0, drizzle_orm_1.exists)(db
2542
2558
  .select({ one: (0, drizzle_orm_1.sql) `1` })
2543
2559
  .from(target)
2544
- .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(target[relation.references ?? "id"], host.table[relation.localKey]), ...compiled, ...(softDeleted ? [softDeleted] : []))));
2560
+ .where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(target[relation.references ?? "id"], host.table[relation.localKey]), ...compiled, ...this.relatedRowScope(target, options))));
2545
2561
  }
2546
2562
  rootHost() {
2547
2563
  return {
@@ -2562,6 +2578,7 @@ class SqlBaseCrudService {
2562
2578
  };
2563
2579
  }
2564
2580
  buildFilterConditions(filters, db = this.config.db, options) {
2581
+ (0, query_parser_1.assertFilterDepth)(filters);
2565
2582
  const joins = new Set();
2566
2583
  const conditions = this.compileFilterAt(this.rootHost(), filters, db, joins, options);
2567
2584
  return { conditions, relations: Array.from(joins) };
@@ -2638,7 +2655,7 @@ class SqlBaseCrudService {
2638
2655
  const compiled = this.compileFilterAt(this.relationHost(relation, relation.table, path), inner, db, new Set(), options);
2639
2656
  if (compiled.length === 0 && !quantified)
2640
2657
  continue;
2641
- const condition = this.collectionCondition(relation, compiled, db, host);
2658
+ const condition = this.collectionCondition(relation, compiled, db, host, options);
2642
2659
  if (condition)
2643
2660
  parts.push(condition);
2644
2661
  }
@@ -2694,8 +2711,24 @@ class SqlBaseCrudService {
2694
2711
  if (!Array.isArray(raw)) {
2695
2712
  throw new common_1.BadRequestException(`Filter operator "${op}" expects an array`);
2696
2713
  }
2714
+ this.assertBatchSize(raw.length);
2697
2715
  return raw;
2698
2716
  }
2717
+ static MAX_BATCH_PARAMETERS = 60_000;
2718
+ assertBatchSize(parameters) {
2719
+ const max = SqlBaseCrudService.MAX_BATCH_PARAMETERS;
2720
+ if (parameters > max) {
2721
+ throw new common_1.BadRequestException(`Too many values in one call (${parameters}); the limit is ${max}. Split the batch.`);
2722
+ }
2723
+ }
2724
+ insertParameters(rows) {
2725
+ let widest = 0;
2726
+ for (const row of rows) {
2727
+ if ((0, merge_config_1.isPlainObject)(row))
2728
+ widest = Math.max(widest, Object.keys(row).length);
2729
+ }
2730
+ return rows.length * (widest + 4);
2731
+ }
2699
2732
  requirePostgres(op) {
2700
2733
  if (this.config.dialect !== "postgresql") {
2701
2734
  throw new common_1.BadRequestException(`Filter operator "${op}" is only supported on PostgreSQL`);
@@ -2822,7 +2855,7 @@ class SqlBaseCrudService {
2822
2855
  }
2823
2856
  async fullTextSearch(searchTerm, searchColumns, pagination, options) {
2824
2857
  if (this.config.dialect !== "postgresql") {
2825
- throw new Error("Full-text search only supported in PostgreSQL");
2858
+ throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
2826
2859
  }
2827
2860
  if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
2828
2861
  throw new common_1.BadRequestException("Full-text search needs at least one column");