@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.
- package/AGENT.md +3 -3
- package/CHANGELOG.md +53 -0
- package/README.md +52 -45
- package/dist/controller/crud-controller.factory.d.ts +2 -1
- package/dist/controller/crud-controller.factory.js +43 -15
- package/dist/controller/crud-controller.factory.js.map +1 -1
- package/dist/core/abstract/sql-base-crud.service.d.ts +10 -5
- package/dist/core/abstract/sql-base-crud.service.js +116 -83
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -1
- package/dist/core/utils/query-parser.d.ts +2 -0
- package/dist/core/utils/query-parser.js +36 -2
- package/dist/core/utils/query-parser.js.map +1 -1
- package/package.json +17 -8
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = "
|
|
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 = "
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 = "
|
|
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 = "
|
|
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
|
-
|
|
1395
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1695
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1682
1696
|
}
|
|
1683
1697
|
const { transaction, hooks = {} } = options || {};
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
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
|
|
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
|
-
...(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ...(
|
|
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
|
|
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");
|