@avytrix/nestjs-drizzle-crud 3.5.1 → 3.7.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 +20 -12
- package/CHANGELOG.md +100 -1
- package/README.md +102 -59
- 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 +23 -7
- package/dist/core/abstract/sql-base-crud.service.js +258 -119
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -1
- package/dist/core/interfaces/drizzle-crud-config.interface.d.ts +16 -0
- package/dist/core/interfaces/sql-crud-config.interface.d.ts +9 -1
- package/dist/core/types/sql.types.d.ts +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/dist/index.d.ts +2 -2
- package/dist/index.js.map +1 -1
- package/dist/modules/drizzle-connection.d.ts +3 -1
- package/dist/modules/drizzle-connection.js +49 -2
- package/dist/modules/drizzle-connection.js.map +1 -1
- package/dist/modules/drizzle-crud.module.d.ts +2 -6
- package/dist/modules/drizzle-crud.module.js +28 -6
- package/dist/modules/drizzle-crud.module.js.map +1 -1
- package/package.json +25 -8
|
@@ -66,6 +66,10 @@ class SqlBaseCrudService {
|
|
|
66
66
|
return result.rowsAffected > 0;
|
|
67
67
|
if (typeof result.affectedRows === "number")
|
|
68
68
|
return result.affectedRows > 0;
|
|
69
|
+
if (typeof result.changes === "number")
|
|
70
|
+
return result.changes > 0;
|
|
71
|
+
if (typeof result.meta?.changes === "number")
|
|
72
|
+
return result.meta.changes > 0;
|
|
69
73
|
return false;
|
|
70
74
|
}
|
|
71
75
|
buildWhereConditionsFromPartial(where) {
|
|
@@ -196,6 +200,18 @@ class SqlBaseCrudService {
|
|
|
196
200
|
return [];
|
|
197
201
|
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
202
|
}
|
|
203
|
+
relatedRowScope(target, options) {
|
|
204
|
+
const out = this.relatedScopeConditions(target, options);
|
|
205
|
+
const softDeleted = this.softDeleteCondition(target);
|
|
206
|
+
if (softDeleted)
|
|
207
|
+
out.push(softDeleted);
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
relatedScopeConditions(target, options) {
|
|
211
|
+
return Object.entries(this.resolveScope(options) ?? {})
|
|
212
|
+
.filter(([key]) => this.columnOf(target, key))
|
|
213
|
+
.map(([key, value]) => value === null ? (0, drizzle_orm_1.isNull)(target[key]) : (0, drizzle_orm_1.eq)(target[key], value));
|
|
214
|
+
}
|
|
199
215
|
applyScopeToRow(row, options) {
|
|
200
216
|
const values = this.resolveScope(options);
|
|
201
217
|
if (values)
|
|
@@ -497,7 +513,7 @@ class SqlBaseCrudService {
|
|
|
497
513
|
}
|
|
498
514
|
return Object.keys(fields).length > 0 ? fields : undefined;
|
|
499
515
|
}
|
|
500
|
-
async loadRelations(db, hosts, nodes, select = { base: [], relations: {} }, hostTable = this.config.table) {
|
|
516
|
+
async loadRelations(db, hosts, nodes, select = { base: [], relations: {} }, hostTable = this.config.table, options) {
|
|
501
517
|
if (!nodes.length || !Array.isArray(hosts))
|
|
502
518
|
return;
|
|
503
519
|
const rows = hosts.filter((row) => row && typeof row === "object");
|
|
@@ -508,13 +524,13 @@ class SqlBaseCrudService {
|
|
|
508
524
|
if (!node.joined) {
|
|
509
525
|
switch (this.relationType(node.config)) {
|
|
510
526
|
case "hasMany":
|
|
511
|
-
await this.loadHasMany(db, rows, node, childSelect, hostTable);
|
|
527
|
+
await this.loadHasMany(db, rows, node, childSelect, hostTable, options);
|
|
512
528
|
break;
|
|
513
529
|
case "manyToMany":
|
|
514
|
-
await this.loadManyToMany(db, rows, node, childSelect, hostTable);
|
|
530
|
+
await this.loadManyToMany(db, rows, node, childSelect, hostTable, options);
|
|
515
531
|
break;
|
|
516
532
|
default:
|
|
517
|
-
await this.loadBelongsTo(db, rows, node, childSelect);
|
|
533
|
+
await this.loadBelongsTo(db, rows, node, childSelect, options);
|
|
518
534
|
}
|
|
519
535
|
}
|
|
520
536
|
if (node.children.length === 0)
|
|
@@ -522,10 +538,10 @@ class SqlBaseCrudService {
|
|
|
522
538
|
const nextHosts = this.isCollection(node.config)
|
|
523
539
|
? rows.flatMap((row) => row[node.name] ?? [])
|
|
524
540
|
: rows.map((row) => row[node.name]);
|
|
525
|
-
await this.loadRelations(db, nextHosts, node.children, childSelect, node.config.table);
|
|
541
|
+
await this.loadRelations(db, nextHosts, node.children, childSelect, node.config.table, options);
|
|
526
542
|
}));
|
|
527
543
|
}
|
|
528
|
-
async loadBelongsTo(db, hosts, node, select) {
|
|
544
|
+
async loadBelongsTo(db, hosts, node, select, options) {
|
|
529
545
|
const { localKey, references } = node.config;
|
|
530
546
|
if (!localKey)
|
|
531
547
|
return;
|
|
@@ -545,9 +561,7 @@ class SqlBaseCrudService {
|
|
|
545
561
|
...this.keysForChildren(node.children, target),
|
|
546
562
|
]);
|
|
547
563
|
const conditions = [(0, drizzle_orm_1.inArray)(referenced, ids)];
|
|
548
|
-
|
|
549
|
-
if (softDeleted)
|
|
550
|
-
conditions.push(softDeleted);
|
|
564
|
+
conditions.push(...this.relatedRowScope(target, options));
|
|
551
565
|
const related = await (fields ? db.select(fields) : db.select())
|
|
552
566
|
.from(target)
|
|
553
567
|
.where((0, drizzle_orm_1.and)(...conditions));
|
|
@@ -558,7 +572,7 @@ class SqlBaseCrudService {
|
|
|
558
572
|
host[node.name] = byKey.get(host[localKey]) ?? null;
|
|
559
573
|
}
|
|
560
574
|
}
|
|
561
|
-
async loadHasMany(db, hosts, node, select, hostTable = this.config.table) {
|
|
575
|
+
async loadHasMany(db, hosts, node, select, hostTable = this.config.table, options) {
|
|
562
576
|
const { foreignKey } = node.config;
|
|
563
577
|
if (!foreignKey)
|
|
564
578
|
return;
|
|
@@ -578,9 +592,7 @@ class SqlBaseCrudService {
|
|
|
578
592
|
...this.keysForChildren(node.children, target),
|
|
579
593
|
]);
|
|
580
594
|
const conditions = [(0, drizzle_orm_1.inArray)(foreign, ids)];
|
|
581
|
-
|
|
582
|
-
if (softDeleted)
|
|
583
|
-
conditions.push(softDeleted);
|
|
595
|
+
conditions.push(...this.relatedRowScope(target, options));
|
|
584
596
|
let query = (fields ? db.select(fields) : db.select())
|
|
585
597
|
.from(target)
|
|
586
598
|
.where((0, drizzle_orm_1.and)(...conditions));
|
|
@@ -590,7 +602,7 @@ class SqlBaseCrudService {
|
|
|
590
602
|
const children = await query;
|
|
591
603
|
this.groupCollection(hosts, node.name, parentKey, children, foreignKey);
|
|
592
604
|
}
|
|
593
|
-
async loadManyToMany(db, hosts, node, select, hostTable = this.config.table) {
|
|
605
|
+
async loadManyToMany(db, hosts, node, select, hostTable = this.config.table, options) {
|
|
594
606
|
const { through, throughLocalKey, throughForeignKey, references } = node.config;
|
|
595
607
|
if (!through || !throughLocalKey || !throughForeignKey)
|
|
596
608
|
return;
|
|
@@ -618,9 +630,7 @@ class SqlBaseCrudService {
|
|
|
618
630
|
fields[name] = columns[name];
|
|
619
631
|
}
|
|
620
632
|
const conditions = [(0, drizzle_orm_1.inArray)(joinLocal, ids)];
|
|
621
|
-
|
|
622
|
-
if (softDeleted)
|
|
623
|
-
conditions.push(softDeleted);
|
|
633
|
+
conditions.push(...this.relatedRowScope(target, options));
|
|
624
634
|
let query = db
|
|
625
635
|
.select(fields)
|
|
626
636
|
.from(through)
|
|
@@ -650,7 +660,7 @@ class SqlBaseCrudService {
|
|
|
650
660
|
host[name] = grouped.get(host[parentKey]) ?? [];
|
|
651
661
|
}
|
|
652
662
|
}
|
|
653
|
-
applyJoins(query, relationNames) {
|
|
663
|
+
applyJoins(query, relationNames, options) {
|
|
654
664
|
const configured = this.config.relations || {};
|
|
655
665
|
let q = query;
|
|
656
666
|
for (const name of relationNames) {
|
|
@@ -658,8 +668,7 @@ class SqlBaseCrudService {
|
|
|
658
668
|
if (!rel?.localKey || this.isCollection(rel))
|
|
659
669
|
continue;
|
|
660
670
|
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);
|
|
671
|
+
q = q.leftJoin(rel.table, (0, drizzle_orm_1.and)(on, ...this.relatedRowScope(rel.table, options)));
|
|
663
672
|
}
|
|
664
673
|
return q;
|
|
665
674
|
}
|
|
@@ -681,13 +690,13 @@ class SqlBaseCrudService {
|
|
|
681
690
|
return out;
|
|
682
691
|
});
|
|
683
692
|
}
|
|
684
|
-
buildOrderBy(sortBy, sortOrder = "
|
|
693
|
+
buildOrderBy(sortBy, sortOrder = "asc") {
|
|
685
694
|
return this.resolveSort(sortBy, sortOrder).map((spec) => this.orderOf(spec));
|
|
686
695
|
}
|
|
687
696
|
orderOf(spec) {
|
|
688
697
|
return spec.order === "desc" ? (0, drizzle_orm_1.desc)(spec.column) : (0, drizzle_orm_1.asc)(spec.column);
|
|
689
698
|
}
|
|
690
|
-
resolveSort(sortBy, sortOrder = "
|
|
699
|
+
resolveSort(sortBy, sortOrder = "asc", options) {
|
|
691
700
|
if (sortOrder !== "asc" && sortOrder !== "desc") {
|
|
692
701
|
throw new common_1.BadRequestException(`Invalid sortOrder: ${sortOrder} (expected 'asc' or 'desc')`);
|
|
693
702
|
}
|
|
@@ -800,14 +809,37 @@ class SqlBaseCrudService {
|
|
|
800
809
|
}
|
|
801
810
|
isUniqueViolation(error) {
|
|
802
811
|
const code = error?.code ?? error?.cause?.code;
|
|
803
|
-
|
|
812
|
+
const errno = error?.errno ?? error?.cause?.errno;
|
|
813
|
+
const sqlite = this.sqliteConstraint(error);
|
|
814
|
+
return (code === "23505" ||
|
|
815
|
+
code === "ER_DUP_ENTRY" ||
|
|
816
|
+
errno === 1062 ||
|
|
817
|
+
sqlite === "UNIQUE" ||
|
|
818
|
+
sqlite === "PRIMARYKEY");
|
|
819
|
+
}
|
|
820
|
+
sqliteConstraint(error) {
|
|
821
|
+
if (this.config.dialect !== "sqlite")
|
|
822
|
+
return undefined;
|
|
823
|
+
for (const e of [error, error?.cause]) {
|
|
824
|
+
const code = e?.extendedCode ?? e?.code;
|
|
825
|
+
if (typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT_")) {
|
|
826
|
+
return code.slice("SQLITE_CONSTRAINT_".length);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
const message = `${error?.cause?.message ?? ""} ${error?.message ?? ""}`;
|
|
830
|
+
const m = message.match(/(UNIQUE|PRIMARY KEY|FOREIGN KEY|NOT NULL|CHECK) constraint failed/);
|
|
831
|
+
return m?.[1]?.replace(" ", "");
|
|
804
832
|
}
|
|
805
833
|
toDuplicateException(error) {
|
|
806
834
|
const detail = error?.detail ?? error?.cause?.detail ?? "";
|
|
807
|
-
const
|
|
835
|
+
const message = error?.cause?.message ?? error?.message ?? "";
|
|
836
|
+
const m = detail.match(/Key \((?<field>.+?)\)=\((?<value>.+?)\)/) ??
|
|
837
|
+
message.match(/Duplicate entry '(?<value>.*)' for key '(?:[^']*\.)?(?<field>[^']+)'/) ??
|
|
838
|
+
message.match(/UNIQUE constraint failed: (?:[^.,\s]+\.)?(?<field>[^,\s]+)/);
|
|
808
839
|
return new crud_exceptions_1.DuplicateEntityException(this.getEntityName(), m?.groups?.field ?? "unique field", m?.groups?.value ?? "");
|
|
809
840
|
}
|
|
810
841
|
static DATA_ERRORS = {
|
|
842
|
+
"23502": "a required value is missing",
|
|
811
843
|
"22001": "a value is too long for its column",
|
|
812
844
|
"23514": "a value violates a column check constraint",
|
|
813
845
|
"22003": "a numeric value is out of range",
|
|
@@ -816,13 +848,54 @@ class SqlBaseCrudService {
|
|
|
816
848
|
"22008": "a date or time value is out of range",
|
|
817
849
|
"21000": "the same key appears more than once in one upsert",
|
|
818
850
|
};
|
|
819
|
-
|
|
851
|
+
static MYSQL_DATA_ERRORS = {
|
|
852
|
+
1048: "a required value is missing",
|
|
853
|
+
1364: "a required value is missing",
|
|
854
|
+
1406: "a value is too long for its column",
|
|
855
|
+
3819: "a value violates a column check constraint",
|
|
856
|
+
1264: "a numeric value is out of range",
|
|
857
|
+
1265: "a value does not match its column type",
|
|
858
|
+
1366: "a value does not match its column type",
|
|
859
|
+
1292: "a date or time value has an invalid format",
|
|
860
|
+
};
|
|
861
|
+
static SQLITE_DATA_ERRORS = {
|
|
862
|
+
NOTNULL: "a required value is missing",
|
|
863
|
+
CHECK: "a value violates a column check constraint",
|
|
864
|
+
};
|
|
865
|
+
dataErrorMessage(error) {
|
|
820
866
|
const code = error?.code ?? error?.cause?.code;
|
|
821
|
-
|
|
867
|
+
if (Object.hasOwn(SqlBaseCrudService.DATA_ERRORS, code ?? "")) {
|
|
868
|
+
return SqlBaseCrudService.DATA_ERRORS[code];
|
|
869
|
+
}
|
|
870
|
+
if (this.config.dialect === "mysql") {
|
|
871
|
+
const errno = error?.errno ?? error?.cause?.errno;
|
|
872
|
+
return Object.hasOwn(SqlBaseCrudService.MYSQL_DATA_ERRORS, errno ?? "")
|
|
873
|
+
? SqlBaseCrudService.MYSQL_DATA_ERRORS[errno]
|
|
874
|
+
: undefined;
|
|
875
|
+
}
|
|
876
|
+
const sqlite = this.sqliteConstraint(error) ?? "";
|
|
877
|
+
return Object.hasOwn(SqlBaseCrudService.SQLITE_DATA_ERRORS, sqlite)
|
|
878
|
+
? SqlBaseCrudService.SQLITE_DATA_ERRORS[sqlite]
|
|
879
|
+
: undefined;
|
|
880
|
+
}
|
|
881
|
+
isDataException(error) {
|
|
882
|
+
return this.dataErrorMessage(error) !== undefined;
|
|
822
883
|
}
|
|
823
884
|
toDataException(error) {
|
|
824
|
-
const
|
|
825
|
-
|
|
885
|
+
const message = this.dataErrorMessage(error) ?? "invalid value";
|
|
886
|
+
const cause = error?.cause ?? error;
|
|
887
|
+
const text = String(cause?.message ?? "");
|
|
888
|
+
const column = error?.column ??
|
|
889
|
+
cause?.column ??
|
|
890
|
+
error?.column_name ??
|
|
891
|
+
cause?.column_name ??
|
|
892
|
+
(this.config.dialect === "mysql"
|
|
893
|
+
? text.match(/column '([^']+)'/i)?.[1]
|
|
894
|
+
: undefined) ??
|
|
895
|
+
(this.config.dialect === "sqlite"
|
|
896
|
+
? text.match(/NOT NULL constraint failed: (?:[^.\s]+\.)?(\S+)/)?.[1]
|
|
897
|
+
: undefined);
|
|
898
|
+
return new crud_exceptions_1.ValidationFailedException(column ? `${message}: ${column}` : message);
|
|
826
899
|
}
|
|
827
900
|
toClientError(error, operation = "write") {
|
|
828
901
|
if (this.isUniqueViolation(error))
|
|
@@ -846,7 +919,11 @@ class SqlBaseCrudService {
|
|
|
846
919
|
}
|
|
847
920
|
isForeignKeyViolation(error) {
|
|
848
921
|
const code = error?.code ?? error?.cause?.code;
|
|
849
|
-
|
|
922
|
+
const errno = error?.errno ?? error?.cause?.errno;
|
|
923
|
+
return (code === "23503" ||
|
|
924
|
+
errno === 1451 ||
|
|
925
|
+
errno === 1452 ||
|
|
926
|
+
this.sqliteConstraint(error) === "FOREIGNKEY");
|
|
850
927
|
}
|
|
851
928
|
toForeignKeyException(error) {
|
|
852
929
|
const detail = error?.detail ?? error?.cause?.detail ?? "";
|
|
@@ -854,14 +931,37 @@ class SqlBaseCrudService {
|
|
|
854
931
|
return new crud_exceptions_1.ForeignKeyViolationException(this.getEntityName(), m?.groups?.field ?? "reference");
|
|
855
932
|
}
|
|
856
933
|
applyLock(query, options) {
|
|
857
|
-
|
|
934
|
+
const strength = options?.forNoKeyUpdate
|
|
935
|
+
? "no key update"
|
|
936
|
+
: options?.lock && options.lock !== "none"
|
|
937
|
+
? options.lock
|
|
938
|
+
: undefined;
|
|
939
|
+
if (!strength)
|
|
858
940
|
return query;
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
if (
|
|
863
|
-
return query.for(
|
|
864
|
-
|
|
941
|
+
if (this.config.dialect === "postgresql") {
|
|
942
|
+
return query.for(strength, { of: this.config.table });
|
|
943
|
+
}
|
|
944
|
+
if (this.config.dialect === "mysql") {
|
|
945
|
+
return query.for(strength === "share" ? "share" : "update");
|
|
946
|
+
}
|
|
947
|
+
throw new common_1.BadRequestException(`Row locks (lock, forNoKeyUpdate) are not supported on ${this.config.dialect}`);
|
|
948
|
+
}
|
|
949
|
+
textSearchConfig(language = "english") {
|
|
950
|
+
if (typeof language !== "string" ||
|
|
951
|
+
!/^[A-Za-z_][A-Za-z0-9_]{0,62}(\.[A-Za-z_][A-Za-z0-9_]{0,62})?$/.test(language)) {
|
|
952
|
+
throw new common_1.BadRequestException(`Invalid full-text search language: ${String(language)}`);
|
|
953
|
+
}
|
|
954
|
+
return drizzle_orm_1.sql.raw(`'${language}'`);
|
|
955
|
+
}
|
|
956
|
+
fullTextParts(columns, term, options = {}) {
|
|
957
|
+
const config = this.textSearchConfig(options.language);
|
|
958
|
+
const mode = options.queryMode ?? "plain";
|
|
959
|
+
if (mode !== "plain" && mode !== "websearch") {
|
|
960
|
+
throw new common_1.BadRequestException(`Invalid full-text query mode: ${String(mode)} (expected 'plain' or 'websearch')`);
|
|
961
|
+
}
|
|
962
|
+
const parse = drizzle_orm_1.sql.raw(mode === "websearch" ? "websearch_to_tsquery" : "plainto_tsquery");
|
|
963
|
+
const vector = drizzle_orm_1.sql.join(columns.map((column) => (0, drizzle_orm_1.sql) `to_tsvector(${config}, ${column})`), (0, drizzle_orm_1.sql) ` || `);
|
|
964
|
+
return { vector, query: (0, drizzle_orm_1.sql) `${parse}(${config}, ${term})` };
|
|
865
965
|
}
|
|
866
966
|
buildSearchCondition(search) {
|
|
867
967
|
if (!search?.term || !search.columns?.length)
|
|
@@ -876,20 +976,24 @@ class SqlBaseCrudService {
|
|
|
876
976
|
return undefined;
|
|
877
977
|
if (search.mode === "fullText") {
|
|
878
978
|
if (this.config.dialect !== "postgresql") {
|
|
879
|
-
throw new
|
|
979
|
+
throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
|
|
880
980
|
}
|
|
881
|
-
const
|
|
882
|
-
|
|
883
|
-
return (0, drizzle_orm_1.sql) `${tsVector} @@ ${tsQuery}`;
|
|
981
|
+
const { vector, query } = this.fullTextParts(columns, term, search);
|
|
982
|
+
return (0, drizzle_orm_1.sql) `${vector} @@ ${query}`;
|
|
884
983
|
}
|
|
885
984
|
const pattern = `%${term.replace(/[\\%_]/g, "\\$&")}%`;
|
|
886
985
|
const conditions = columns.map((column) => this.caseInsensitiveLike(column, pattern));
|
|
887
986
|
return conditions.length === 1 ? conditions[0] : (0, drizzle_orm_1.or)(...conditions);
|
|
888
987
|
}
|
|
889
988
|
caseInsensitiveLike(column, pattern) {
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
989
|
+
if (this.config.dialect === "postgresql")
|
|
990
|
+
return (0, drizzle_orm_1.ilike)(column, pattern);
|
|
991
|
+
return this.likePattern(column, pattern);
|
|
992
|
+
}
|
|
993
|
+
likePattern(column, pattern) {
|
|
994
|
+
return this.config.dialect === "sqlite"
|
|
995
|
+
? (0, drizzle_orm_1.sql) `${column} like ${pattern} escape '\\'`
|
|
996
|
+
: (0, drizzle_orm_1.like)(column, pattern);
|
|
893
997
|
}
|
|
894
998
|
parseQuery(query, options = {}) {
|
|
895
999
|
const config = this.config;
|
|
@@ -1008,14 +1112,14 @@ class SqlBaseCrudService {
|
|
|
1008
1112
|
const baseSelect = this.withCountKeys(this.withRequiredKeys(selection.base, nodes), options?.counts);
|
|
1009
1113
|
conditions.push(...this.rowScope(options));
|
|
1010
1114
|
let query = this.startSelect(db, baseSelect, joined, false, selection);
|
|
1011
|
-
query = this.applyJoins(query, joined.map((node) => node.name))
|
|
1115
|
+
query = this.applyJoins(query, joined.map((node) => node.name), options)
|
|
1012
1116
|
.where((0, drizzle_orm_1.and)(...conditions))
|
|
1013
1117
|
.limit(1);
|
|
1014
1118
|
query = this.applyLock(query, options);
|
|
1015
1119
|
const result = await this.runRead(query);
|
|
1016
1120
|
const rows = this.normalizeRelations(result, joined.map((node) => node.name));
|
|
1017
1121
|
await Promise.all([
|
|
1018
|
-
this.loadRelations(db, rows, nodes, selection),
|
|
1122
|
+
this.loadRelations(db, rows, nodes, selection, undefined, options),
|
|
1019
1123
|
this.loadCounts(db, rows, options?.counts, options),
|
|
1020
1124
|
]);
|
|
1021
1125
|
return rows[0] || null;
|
|
@@ -1055,22 +1159,22 @@ class SqlBaseCrudService {
|
|
|
1055
1159
|
built.conditions.push(...this.rowScope(options));
|
|
1056
1160
|
return built;
|
|
1057
1161
|
}
|
|
1058
|
-
buildCountQuery(db, conditions, filterRelations) {
|
|
1162
|
+
buildCountQuery(db, conditions, filterRelations, options) {
|
|
1059
1163
|
let query = db
|
|
1060
1164
|
.select({ count: (0, drizzle_orm_1.sql) `count(*)` })
|
|
1061
1165
|
.from(this.config.table);
|
|
1062
|
-
query = this.applyJoins(query, filterRelations);
|
|
1166
|
+
query = this.applyJoins(query, filterRelations, options);
|
|
1063
1167
|
if (conditions.length > 0)
|
|
1064
1168
|
query = query.where((0, drizzle_orm_1.and)(...conditions));
|
|
1065
1169
|
return query;
|
|
1066
1170
|
}
|
|
1067
1171
|
async findAll(filters, pagination, options) {
|
|
1068
|
-
const { sortBy, sortOrder = "
|
|
1172
|
+
const { sortBy, sortOrder = "asc" } = pagination || {};
|
|
1069
1173
|
const withCount = pagination?.withCount !== false;
|
|
1070
1174
|
const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
1071
1175
|
const plan = this.planList(filters, sortBy, sortOrder, options);
|
|
1072
1176
|
let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, plan.joinRelations.length > 0, plan.selection);
|
|
1073
|
-
dataQuery = this.applyJoins(dataQuery, plan.joinRelations);
|
|
1177
|
+
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1074
1178
|
if (plan.conditions.length > 0) {
|
|
1075
1179
|
dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...plan.conditions));
|
|
1076
1180
|
}
|
|
@@ -1082,13 +1186,13 @@ class SqlBaseCrudService {
|
|
|
1082
1186
|
.offset(offset);
|
|
1083
1187
|
dataQuery = this.applyLock(dataQuery, options);
|
|
1084
1188
|
const countQuery = withCount
|
|
1085
|
-
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations)
|
|
1189
|
+
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
|
|
1086
1190
|
: Promise.resolve(undefined);
|
|
1087
1191
|
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
1088
1192
|
const data = fetched.slice(0, safeLimit);
|
|
1089
1193
|
const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
|
|
1090
1194
|
await Promise.all([
|
|
1091
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection),
|
|
1195
|
+
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
|
|
1092
1196
|
this.loadCounts(plan.db, rows, options?.counts, options),
|
|
1093
1197
|
]);
|
|
1094
1198
|
if (!withCount) {
|
|
@@ -1108,7 +1212,7 @@ class SqlBaseCrudService {
|
|
|
1108
1212
|
};
|
|
1109
1213
|
}
|
|
1110
1214
|
async findByCursor(filters, pagination, options) {
|
|
1111
|
-
const { sortBy, sortOrder = "
|
|
1215
|
+
const { sortBy, sortOrder = "asc", count = false } = pagination || {};
|
|
1112
1216
|
const { limit } = this.resolvePagination(1, pagination?.limit);
|
|
1113
1217
|
const plan = this.planList(filters, sortBy, sortOrder, options);
|
|
1114
1218
|
const signature = plan.sort
|
|
@@ -1128,7 +1232,7 @@ class SqlBaseCrudService {
|
|
|
1128
1232
|
fields[`${CURSOR_KEY}${i}`] = this.cursorField(spec.column);
|
|
1129
1233
|
});
|
|
1130
1234
|
let dataQuery = plan.db.select(fields).from(this.config.table);
|
|
1131
|
-
dataQuery = this.applyJoins(dataQuery, plan.joinRelations);
|
|
1235
|
+
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1132
1236
|
if (conditions.length > 0)
|
|
1133
1237
|
dataQuery = dataQuery.where((0, drizzle_orm_1.and)(...conditions));
|
|
1134
1238
|
dataQuery = dataQuery
|
|
@@ -1138,7 +1242,7 @@ class SqlBaseCrudService {
|
|
|
1138
1242
|
const [data, totalResult] = await this.runRead(Promise.all([
|
|
1139
1243
|
dataQuery,
|
|
1140
1244
|
count
|
|
1141
|
-
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations)
|
|
1245
|
+
? this.buildCountQuery(plan.db, plan.conditions, plan.filterRelations, options)
|
|
1142
1246
|
: Promise.resolve(undefined),
|
|
1143
1247
|
]));
|
|
1144
1248
|
const page = data.slice(0, limit);
|
|
@@ -1153,7 +1257,7 @@ class SqlBaseCrudService {
|
|
|
1153
1257
|
}
|
|
1154
1258
|
const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
|
|
1155
1259
|
await Promise.all([
|
|
1156
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection),
|
|
1260
|
+
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
|
|
1157
1261
|
this.loadCounts(plan.db, rows, options?.counts, options),
|
|
1158
1262
|
]);
|
|
1159
1263
|
const out = { data: rows, limit, nextCursor };
|
|
@@ -1195,7 +1299,7 @@ class SqlBaseCrudService {
|
|
|
1195
1299
|
return branches.length > 0 ? (0, drizzle_orm_1.or)(...branches) : (0, drizzle_orm_1.sql) `false`;
|
|
1196
1300
|
}
|
|
1197
1301
|
afterCursor(spec, value) {
|
|
1198
|
-
const postgres = this.config.dialect
|
|
1302
|
+
const postgres = this.config.dialect === "postgresql";
|
|
1199
1303
|
const nullsFirst = postgres ? spec.order === "desc" : spec.order === "asc";
|
|
1200
1304
|
if (value === null) {
|
|
1201
1305
|
return nullsFirst ? (0, drizzle_orm_1.isNotNull)(spec.column) : undefined;
|
|
@@ -1229,7 +1333,7 @@ class SqlBaseCrudService {
|
|
|
1229
1333
|
}
|
|
1230
1334
|
const hosts = rows.filter(Boolean);
|
|
1231
1335
|
await Promise.all([
|
|
1232
|
-
this.loadRelations(db, hosts, nodes),
|
|
1336
|
+
this.loadRelations(db, hosts, nodes, undefined, undefined, options),
|
|
1233
1337
|
this.loadCounts(db, hosts, options?.counts, options),
|
|
1234
1338
|
]);
|
|
1235
1339
|
}
|
|
@@ -1391,8 +1495,13 @@ class SqlBaseCrudService {
|
|
|
1391
1495
|
relatedRow(table, value, options) {
|
|
1392
1496
|
const out = {};
|
|
1393
1497
|
for (const [key, item] of Object.entries(value)) {
|
|
1394
|
-
|
|
1395
|
-
|
|
1498
|
+
const column = this.columnOf(table, key);
|
|
1499
|
+
if (item === undefined ||
|
|
1500
|
+
!column ||
|
|
1501
|
+
(column.primary && column.hasDefault)) {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
out[key] = item;
|
|
1396
1505
|
}
|
|
1397
1506
|
const softDelete = this.config.softDelete;
|
|
1398
1507
|
if (softDelete?.enabled)
|
|
@@ -1421,14 +1530,7 @@ class SqlBaseCrudService {
|
|
|
1421
1530
|
const local = through[relation.throughLocalKey];
|
|
1422
1531
|
const foreign = through[relation.throughForeignKey];
|
|
1423
1532
|
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
|
-
}
|
|
1533
|
+
conditions.push(...this.relatedRowScope(target, options));
|
|
1432
1534
|
const found = await tx
|
|
1433
1535
|
.select({ id: referenced })
|
|
1434
1536
|
.from(target)
|
|
@@ -1636,7 +1738,7 @@ class SqlBaseCrudService {
|
|
|
1636
1738
|
}
|
|
1637
1739
|
async softDelete(id, options) {
|
|
1638
1740
|
if (!this.config.softDelete?.enabled) {
|
|
1639
|
-
throw new
|
|
1741
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1640
1742
|
}
|
|
1641
1743
|
const { transaction, hooks = {} } = options || {};
|
|
1642
1744
|
const existing = await this.findForGuard(id, {
|
|
@@ -1678,18 +1780,16 @@ class SqlBaseCrudService {
|
|
|
1678
1780
|
}
|
|
1679
1781
|
async restore(id, options) {
|
|
1680
1782
|
if (!this.config.softDelete?.enabled) {
|
|
1681
|
-
throw new
|
|
1783
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1682
1784
|
}
|
|
1683
1785
|
const { transaction, hooks = {} } = options || {};
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1692
|
-
}
|
|
1786
|
+
const existing = await this.findForGuard(id, {
|
|
1787
|
+
...options,
|
|
1788
|
+
withDeleted: false,
|
|
1789
|
+
onlyDeleted: true,
|
|
1790
|
+
});
|
|
1791
|
+
if (!existing)
|
|
1792
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1693
1793
|
if (!hooks.skipBefore)
|
|
1694
1794
|
await this.beforeRestore(id);
|
|
1695
1795
|
await this.emitEvent("before", "restore", false, { ids: [id], transaction }, options);
|
|
@@ -1769,6 +1869,7 @@ class SqlBaseCrudService {
|
|
|
1769
1869
|
}
|
|
1770
1870
|
async massUpsert(data, upsert = {}, options) {
|
|
1771
1871
|
this.checkReadOptions(options);
|
|
1872
|
+
this.assertBatchSize(this.insertParameters(data));
|
|
1772
1873
|
const message = "Mass upsert errors";
|
|
1773
1874
|
return this.executeSqlTransaction(async (tx) => {
|
|
1774
1875
|
const opts = { ...options, transaction: tx };
|
|
@@ -1873,21 +1974,25 @@ class SqlBaseCrudService {
|
|
|
1873
1974
|
statement = insert.onDuplicateKeyUpdate({ set });
|
|
1874
1975
|
}
|
|
1875
1976
|
else {
|
|
1876
|
-
const
|
|
1977
|
+
const guard = [
|
|
1978
|
+
...this.scopeConditions(options),
|
|
1979
|
+
...[this.softDeleteCondition(table)].filter(Boolean),
|
|
1980
|
+
];
|
|
1877
1981
|
statement = insert.onConflictDoUpdate({
|
|
1878
1982
|
target: targets.length === 1 ? targets[0] : targets,
|
|
1879
1983
|
set,
|
|
1880
|
-
...(
|
|
1984
|
+
...(guard.length > 0 && { where: (0, drizzle_orm_1.and)(...guard) }),
|
|
1881
1985
|
});
|
|
1882
1986
|
}
|
|
1883
1987
|
if (!readBack)
|
|
1884
1988
|
return statement.returning();
|
|
1885
1989
|
await statement;
|
|
1886
1990
|
const match = rows.map((row) => (0, drizzle_orm_1.and)(...targetKeys.map((key, i) => (0, drizzle_orm_1.eq)(targets[i], row[key]))));
|
|
1991
|
+
const softDeleted = this.softDeleteCondition(table);
|
|
1887
1992
|
return db
|
|
1888
1993
|
.select()
|
|
1889
1994
|
.from(table)
|
|
1890
|
-
.where(match.length === 1 ? match[0] : (0, drizzle_orm_1.or)(...match));
|
|
1995
|
+
.where((0, drizzle_orm_1.and)(match.length === 1 ? match[0] : (0, drizzle_orm_1.or)(...match), ...this.scopeConditions(options), ...(softDeleted ? [softDeleted] : [])));
|
|
1891
1996
|
}
|
|
1892
1997
|
excludedValue(column) {
|
|
1893
1998
|
return this.config.dialect === "mysql"
|
|
@@ -1899,6 +2004,7 @@ class SqlBaseCrudService {
|
|
|
1899
2004
|
return this.massCreatePerItem(data, options);
|
|
1900
2005
|
}
|
|
1901
2006
|
this.checkReadOptions(options);
|
|
2007
|
+
this.assertBatchSize(this.insertParameters(data));
|
|
1902
2008
|
return this.executeSqlTransaction(async (tx) => {
|
|
1903
2009
|
const opts = { ...options, transaction: tx };
|
|
1904
2010
|
const rows = await this.eachItem(data.map((item, index) => ({
|
|
@@ -1957,6 +2063,7 @@ class SqlBaseCrudService {
|
|
|
1957
2063
|
if (options?.bulkMode === "perItem") {
|
|
1958
2064
|
return this.massUpdatePerItem(ids, data, options);
|
|
1959
2065
|
}
|
|
2066
|
+
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
1960
2067
|
this.checkReadOptions(options);
|
|
1961
2068
|
const message = "Mass update errors";
|
|
1962
2069
|
return this.executeSqlTransaction(async (tx) => {
|
|
@@ -2020,10 +2127,11 @@ class SqlBaseCrudService {
|
|
|
2020
2127
|
}
|
|
2021
2128
|
async massSoftDelete(ids, options) {
|
|
2022
2129
|
if (!this.config.softDelete?.enabled)
|
|
2023
|
-
throw new
|
|
2130
|
+
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2024
2131
|
if (options?.bulkMode === "perItem") {
|
|
2025
2132
|
return this.massSoftDeletePerItem(ids, options);
|
|
2026
2133
|
}
|
|
2134
|
+
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
2027
2135
|
const message = "Mass soft delete errors";
|
|
2028
2136
|
return this.executeSqlTransaction(async (tx) => {
|
|
2029
2137
|
const opts = {
|
|
@@ -2063,10 +2171,11 @@ class SqlBaseCrudService {
|
|
|
2063
2171
|
}
|
|
2064
2172
|
async massRestore(ids, options) {
|
|
2065
2173
|
if (!this.config.softDelete?.enabled)
|
|
2066
|
-
throw new
|
|
2174
|
+
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2067
2175
|
if (options?.bulkMode === "perItem") {
|
|
2068
2176
|
return this.massRestorePerItem(ids, options);
|
|
2069
2177
|
}
|
|
2178
|
+
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
2070
2179
|
this.checkReadOptions(options);
|
|
2071
2180
|
const message = "Mass restore errors";
|
|
2072
2181
|
return this.executeSqlTransaction(async (tx) => {
|
|
@@ -2104,6 +2213,7 @@ class SqlBaseCrudService {
|
|
|
2104
2213
|
if (options?.bulkMode === "perItem") {
|
|
2105
2214
|
return this.massDeletePerItem(ids, options);
|
|
2106
2215
|
}
|
|
2216
|
+
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
2107
2217
|
const message = "Mass delete errors";
|
|
2108
2218
|
return this.executeSqlTransaction(async (tx) => {
|
|
2109
2219
|
const opts = { ...options, transaction: tx };
|
|
@@ -2295,7 +2405,7 @@ class SqlBaseCrudService {
|
|
|
2295
2405
|
this.checkReadOptions(options);
|
|
2296
2406
|
const db = options?.transaction || this.config.db;
|
|
2297
2407
|
const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
|
|
2298
|
-
const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations));
|
|
2408
|
+
const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
|
|
2299
2409
|
return parseInt(result[0]?.count?.toString() || "0", 10);
|
|
2300
2410
|
}
|
|
2301
2411
|
async countBy(column, filters, options) {
|
|
@@ -2308,7 +2418,7 @@ class SqlBaseCrudService {
|
|
|
2308
2418
|
let query = db
|
|
2309
2419
|
.select({ value: spec.column, count: total })
|
|
2310
2420
|
.from(this.config.table);
|
|
2311
|
-
query = this.applyJoins(query, joins);
|
|
2421
|
+
query = this.applyJoins(query, joins, options);
|
|
2312
2422
|
if (conditions.length > 0)
|
|
2313
2423
|
query = query.where((0, drizzle_orm_1.and)(...conditions));
|
|
2314
2424
|
query = query.groupBy(spec.column).orderBy((0, drizzle_orm_1.desc)(total), (0, drizzle_orm_1.asc)(spec.column));
|
|
@@ -2418,9 +2528,7 @@ class SqlBaseCrudService {
|
|
|
2418
2528
|
root: false,
|
|
2419
2529
|
}, filter, db, new Set(), options));
|
|
2420
2530
|
}
|
|
2421
|
-
|
|
2422
|
-
if (softDeleted)
|
|
2423
|
-
conditions.push(softDeleted);
|
|
2531
|
+
conditions.push(...this.relatedRowScope(target, options));
|
|
2424
2532
|
const total = (0, drizzle_orm_1.sql) `count(*)`;
|
|
2425
2533
|
if (this.relationType(relation) === "manyToMany") {
|
|
2426
2534
|
const joinLocal = relation.through[relation.throughLocalKey];
|
|
@@ -2442,6 +2550,7 @@ class SqlBaseCrudService {
|
|
|
2442
2550
|
}
|
|
2443
2551
|
pushColumnCondition(conditions, column, value, caseInsensitive = this.config.sql?.caseSensitive === false) {
|
|
2444
2552
|
if (Array.isArray(value)) {
|
|
2553
|
+
this.assertBatchSize(value.length);
|
|
2445
2554
|
conditions.push((0, drizzle_orm_1.inArray)(column, value.map((item) => this.coerceFilterValue(column, item))));
|
|
2446
2555
|
}
|
|
2447
2556
|
else if ((0, merge_config_1.isPlainObject)(value)) {
|
|
@@ -2463,9 +2572,9 @@ class SqlBaseCrudService {
|
|
|
2463
2572
|
const targetConditions = this.compileFilterAt(this.relationHost(relation, relation.table, name), value, db, new Set(), options);
|
|
2464
2573
|
if (targetConditions.length === 0)
|
|
2465
2574
|
return undefined;
|
|
2466
|
-
return this.collectionCondition(relation, targetConditions, db, this.rootHost());
|
|
2575
|
+
return this.collectionCondition(relation, targetConditions, db, this.rootHost(), options);
|
|
2467
2576
|
}
|
|
2468
|
-
collectionCondition(relation, targetConditions, db, host) {
|
|
2577
|
+
collectionCondition(relation, targetConditions, db, host, options) {
|
|
2469
2578
|
const target = relation.table;
|
|
2470
2579
|
const manyToMany = this.relationType(relation) === "manyToMany";
|
|
2471
2580
|
const joinLocal = manyToMany
|
|
@@ -2475,9 +2584,7 @@ class SqlBaseCrudService {
|
|
|
2475
2584
|
if (!key)
|
|
2476
2585
|
return undefined;
|
|
2477
2586
|
const conditions = [...targetConditions];
|
|
2478
|
-
|
|
2479
|
-
if (softDeleted)
|
|
2480
|
-
conditions.push(softDeleted);
|
|
2587
|
+
conditions.push(...this.relatedRowScope(target, options));
|
|
2481
2588
|
let subquery;
|
|
2482
2589
|
if (manyToMany) {
|
|
2483
2590
|
const referenced = target[relation.references ?? "id"];
|
|
@@ -2506,9 +2613,7 @@ class SqlBaseCrudService {
|
|
|
2506
2613
|
return undefined;
|
|
2507
2614
|
const parentKey = this.parentKeyOf(relation, host.source);
|
|
2508
2615
|
const conditions = [];
|
|
2509
|
-
|
|
2510
|
-
if (softDeleted)
|
|
2511
|
-
conditions.push(softDeleted);
|
|
2616
|
+
conditions.push(...this.relatedRowScope(target, options));
|
|
2512
2617
|
if (compiled.length > 0) {
|
|
2513
2618
|
const matched = (0, drizzle_orm_1.and)(...compiled);
|
|
2514
2619
|
conditions.push(quantifier === "every"
|
|
@@ -2537,11 +2642,10 @@ class SqlBaseCrudService {
|
|
|
2537
2642
|
const compiled = this.compileFilterAt(this.relationHost(relation, target, name), inner, db, new Set(), options);
|
|
2538
2643
|
if (compiled.length === 0)
|
|
2539
2644
|
return undefined;
|
|
2540
|
-
const softDeleted = this.softDeleteCondition(target);
|
|
2541
2645
|
return (0, drizzle_orm_1.exists)(db
|
|
2542
2646
|
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
2543
2647
|
.from(target)
|
|
2544
|
-
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(target[relation.references ?? "id"], host.table[relation.localKey]), ...compiled, ...(
|
|
2648
|
+
.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
2649
|
}
|
|
2546
2650
|
rootHost() {
|
|
2547
2651
|
return {
|
|
@@ -2562,6 +2666,7 @@ class SqlBaseCrudService {
|
|
|
2562
2666
|
};
|
|
2563
2667
|
}
|
|
2564
2668
|
buildFilterConditions(filters, db = this.config.db, options) {
|
|
2669
|
+
(0, query_parser_1.assertFilterDepth)(filters);
|
|
2565
2670
|
const joins = new Set();
|
|
2566
2671
|
const conditions = this.compileFilterAt(this.rootHost(), filters, db, joins, options);
|
|
2567
2672
|
return { conditions, relations: Array.from(joins) };
|
|
@@ -2638,7 +2743,7 @@ class SqlBaseCrudService {
|
|
|
2638
2743
|
const compiled = this.compileFilterAt(this.relationHost(relation, relation.table, path), inner, db, new Set(), options);
|
|
2639
2744
|
if (compiled.length === 0 && !quantified)
|
|
2640
2745
|
continue;
|
|
2641
|
-
const condition = this.collectionCondition(relation, compiled, db, host);
|
|
2746
|
+
const condition = this.collectionCondition(relation, compiled, db, host, options);
|
|
2642
2747
|
if (condition)
|
|
2643
2748
|
parts.push(condition);
|
|
2644
2749
|
}
|
|
@@ -2694,8 +2799,24 @@ class SqlBaseCrudService {
|
|
|
2694
2799
|
if (!Array.isArray(raw)) {
|
|
2695
2800
|
throw new common_1.BadRequestException(`Filter operator "${op}" expects an array`);
|
|
2696
2801
|
}
|
|
2802
|
+
this.assertBatchSize(raw.length);
|
|
2697
2803
|
return raw;
|
|
2698
2804
|
}
|
|
2805
|
+
static MAX_BATCH_PARAMETERS = 60_000;
|
|
2806
|
+
assertBatchSize(parameters) {
|
|
2807
|
+
const max = SqlBaseCrudService.MAX_BATCH_PARAMETERS;
|
|
2808
|
+
if (parameters > max) {
|
|
2809
|
+
throw new common_1.BadRequestException(`Too many values in one call (${parameters}); the limit is ${max}. Split the batch.`);
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
insertParameters(rows) {
|
|
2813
|
+
let widest = 0;
|
|
2814
|
+
for (const row of rows) {
|
|
2815
|
+
if ((0, merge_config_1.isPlainObject)(row))
|
|
2816
|
+
widest = Math.max(widest, Object.keys(row).length);
|
|
2817
|
+
}
|
|
2818
|
+
return rows.length * (widest + 4);
|
|
2819
|
+
}
|
|
2699
2820
|
requirePostgres(op) {
|
|
2700
2821
|
if (this.config.dialect !== "postgresql") {
|
|
2701
2822
|
throw new common_1.BadRequestException(`Filter operator "${op}" is only supported on PostgreSQL`);
|
|
@@ -2718,7 +2839,7 @@ class SqlBaseCrudService {
|
|
|
2718
2839
|
: `%${escaped}%`;
|
|
2719
2840
|
return insensitive || this.config.sql?.caseSensitive === false
|
|
2720
2841
|
? this.caseInsensitiveLike(column, pattern)
|
|
2721
|
-
:
|
|
2842
|
+
: this.likePattern(column, pattern);
|
|
2722
2843
|
}
|
|
2723
2844
|
applyComplexFilter(conditions, column, filterObj) {
|
|
2724
2845
|
const coerce = (value) => this.coerceFilterValue(column, value);
|
|
@@ -2750,7 +2871,7 @@ class SqlBaseCrudService {
|
|
|
2750
2871
|
conditions.push((0, drizzle_orm_1.ne)(column, coerce(raw)));
|
|
2751
2872
|
break;
|
|
2752
2873
|
case "like":
|
|
2753
|
-
conditions.push(
|
|
2874
|
+
conditions.push(this.likePattern(column, raw));
|
|
2754
2875
|
break;
|
|
2755
2876
|
case "ilike":
|
|
2756
2877
|
conditions.push(this.caseInsensitiveLike(column, raw));
|
|
@@ -2808,10 +2929,22 @@ class SqlBaseCrudService {
|
|
|
2808
2929
|
}
|
|
2809
2930
|
}
|
|
2810
2931
|
async executeSqlTransaction(operation, existingTransaction) {
|
|
2932
|
+
this.assertTransactions(existingTransaction ?? this.config.db);
|
|
2811
2933
|
if (existingTransaction)
|
|
2812
2934
|
return operation(existingTransaction);
|
|
2813
2935
|
return this.config.db.transaction(operation);
|
|
2814
2936
|
}
|
|
2937
|
+
assertTransactions(db) {
|
|
2938
|
+
if (this.config.dialect !== "sqlite")
|
|
2939
|
+
return;
|
|
2940
|
+
const kind = db?.constructor?.[drizzle_orm_1.entityKind];
|
|
2941
|
+
if (kind === "D1Database" || kind === "D1Transaction") {
|
|
2942
|
+
throw new crud_exceptions_1.TransactionException("Cloudflare D1 has no interactive transactions, so mass writes, nested writes and attach/detach/sync are not available on it. Use the single-row methods, or db.batch() yourself.");
|
|
2943
|
+
}
|
|
2944
|
+
if (db?.resultKind === "sync") {
|
|
2945
|
+
throw new crud_exceptions_1.TransactionException("This SQLite driver is synchronous (better-sqlite3, bun:sqlite, sql.js), and its transactions cannot wait for async work, so mass writes, nested writes and attach/detach/sync are not available on it. Use an async driver such as libSQL (@libsql/client).");
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2815
2948
|
getEntityName() {
|
|
2816
2949
|
try {
|
|
2817
2950
|
return (0, drizzle_orm_1.getTableName)(this.config.table) || "Entity";
|
|
@@ -2822,7 +2955,7 @@ class SqlBaseCrudService {
|
|
|
2822
2955
|
}
|
|
2823
2956
|
async fullTextSearch(searchTerm, searchColumns, pagination, options) {
|
|
2824
2957
|
if (this.config.dialect !== "postgresql") {
|
|
2825
|
-
throw new
|
|
2958
|
+
throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
|
|
2826
2959
|
}
|
|
2827
2960
|
if (!Array.isArray(searchColumns) || searchColumns.length === 0) {
|
|
2828
2961
|
throw new common_1.BadRequestException("Full-text search needs at least one column");
|
|
@@ -2835,31 +2968,37 @@ class SqlBaseCrudService {
|
|
|
2835
2968
|
this.assertAllowed("searchableColumns", name, options);
|
|
2836
2969
|
return column;
|
|
2837
2970
|
});
|
|
2838
|
-
const {
|
|
2839
|
-
const
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
const whereExpr = (0, drizzle_orm_1.and)(matchExpr, ...this.rowScope(options));
|
|
2971
|
+
const { filters, language, queryMode, ...readOptions } = options ?? {};
|
|
2972
|
+
const { vector, query } = this.fullTextParts(columns, searchTerm, {
|
|
2973
|
+
language,
|
|
2974
|
+
queryMode,
|
|
2975
|
+
});
|
|
2844
2976
|
const { page, limit, offset } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
2977
|
+
const plan = this.planList(filters, undefined, "asc", readOptions);
|
|
2978
|
+
const conditions = [(0, drizzle_orm_1.sql) `${vector} @@ ${query}`, ...plan.conditions];
|
|
2979
|
+
const joins = Array.from(new Set([
|
|
2980
|
+
...plan.joined.map((node) => node.name),
|
|
2981
|
+
...plan.filterRelations,
|
|
2982
|
+
]));
|
|
2845
2983
|
const pk = this.columnOf(this.config.table, this.config.primaryKey);
|
|
2846
|
-
const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${
|
|
2984
|
+
const orderBy = [(0, drizzle_orm_1.sql) `ts_rank(${vector}, ${query}) DESC`];
|
|
2847
2985
|
if (pk)
|
|
2848
2986
|
orderBy.push((0, drizzle_orm_1.asc)(pk));
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
.
|
|
2852
|
-
.where(whereExpr)
|
|
2987
|
+
let dataQuery = this.startSelect(plan.db, plan.baseSelect, plan.joined, joins.length > 0, plan.selection);
|
|
2988
|
+
dataQuery = this.applyJoins(dataQuery, joins, readOptions)
|
|
2989
|
+
.where((0, drizzle_orm_1.and)(...conditions))
|
|
2853
2990
|
.orderBy(...orderBy)
|
|
2854
2991
|
.limit(limit)
|
|
2855
2992
|
.offset(offset);
|
|
2856
|
-
const countQuery = db
|
|
2857
|
-
|
|
2858
|
-
.from(this.config.table)
|
|
2859
|
-
.where(whereExpr);
|
|
2860
|
-
const [data, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
2993
|
+
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
2994
|
+
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
2861
2995
|
const total = parseInt(totalResult[0]?.count?.toString() || "0", 10);
|
|
2862
|
-
|
|
2996
|
+
const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
|
|
2997
|
+
await Promise.all([
|
|
2998
|
+
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, readOptions),
|
|
2999
|
+
this.loadCounts(plan.db, rows, readOptions.counts, readOptions),
|
|
3000
|
+
]);
|
|
3001
|
+
return { data: rows, total, page, limit };
|
|
2863
3002
|
}
|
|
2864
3003
|
}
|
|
2865
3004
|
exports.SqlBaseCrudService = SqlBaseCrudService;
|