@avytrix/nestjs-drizzle-crud 3.7.1 → 3.8.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 +13 -4
- package/CHANGELOG.md +148 -1
- package/README.md +24 -6
- package/dist/core/abstract/sql-base-crud.service.d.ts +43 -5
- package/dist/core/abstract/sql-base-crud.service.js +795 -460
- package/dist/core/abstract/sql-base-crud.service.js.map +1 -1
- package/dist/core/utils/query-parser.js +8 -9
- package/dist/core/utils/query-parser.js.map +1 -1
- package/dist/modules/drizzle-connection.js +1 -0
- package/dist/modules/drizzle-connection.js.map +1 -1
- package/dist/modules/drizzle-crud.module.js +4 -0
- package/dist/modules/drizzle-crud.module.js.map +1 -1
- package/package.json +1 -1
|
@@ -10,7 +10,23 @@ const merge_config_1 = require("../utils/merge-config");
|
|
|
10
10
|
const query_parser_1 = require("../utils/query-parser");
|
|
11
11
|
const JOIN_KEY = "__crudJoinKey";
|
|
12
12
|
const CURSOR_KEY = "__crudCursor";
|
|
13
|
+
const SCOPE_MEMO = Symbol("scopeMemo");
|
|
13
14
|
const ROLLBACK = Symbol("rollback");
|
|
15
|
+
class DeferredDiagnosis {
|
|
16
|
+
settle;
|
|
17
|
+
constructor(settle) {
|
|
18
|
+
this.settle = settle;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const BIND_LIMITS = {
|
|
22
|
+
postgresql: { limit: 65_535, name: "PostgreSQL" },
|
|
23
|
+
mysql: { limit: 65_535, name: "MySQL" },
|
|
24
|
+
sqlite: { limit: 32_766, name: "SQLite" },
|
|
25
|
+
d1: { limit: 100, name: "Cloudflare D1" },
|
|
26
|
+
};
|
|
27
|
+
class BindLimitException extends common_1.BadRequestException {
|
|
28
|
+
}
|
|
29
|
+
const CHECKED_DIALECTS = new WeakSet();
|
|
14
30
|
const GROUP_KEYS = new Set(["and", "or", "not"]);
|
|
15
31
|
const QUANTIFIERS = new Set(["some", "none", "every"]);
|
|
16
32
|
const COUNT_KEY = "__crudCountKey";
|
|
@@ -176,9 +192,25 @@ class SqlBaseCrudService {
|
|
|
176
192
|
const scope = this.config.scope;
|
|
177
193
|
if (!scope || options?.skipScope)
|
|
178
194
|
return null;
|
|
195
|
+
const memo = options?.[SCOPE_MEMO];
|
|
196
|
+
if (memo && Object.hasOwn(memo, "values"))
|
|
197
|
+
return memo.values;
|
|
179
198
|
const values = typeof scope === "function"
|
|
180
199
|
? scope(options?.context ?? crud_context_1.CrudContext.current() ?? {})
|
|
181
200
|
: scope;
|
|
201
|
+
const checked = this.checkScope(values);
|
|
202
|
+
if (memo)
|
|
203
|
+
memo.values = checked;
|
|
204
|
+
return checked;
|
|
205
|
+
}
|
|
206
|
+
withScopeMemo(options) {
|
|
207
|
+
if (typeof this.config.scope !== "function" ||
|
|
208
|
+
options?.[SCOPE_MEMO]) {
|
|
209
|
+
return options;
|
|
210
|
+
}
|
|
211
|
+
return { ...options, [SCOPE_MEMO]: {} };
|
|
212
|
+
}
|
|
213
|
+
checkScope(values) {
|
|
182
214
|
if (values === null)
|
|
183
215
|
return null;
|
|
184
216
|
if (!(0, merge_config_1.isPlainObject)(values)) {
|
|
@@ -273,6 +305,53 @@ class SqlBaseCrudService {
|
|
|
273
305
|
this.config = (0, merge_config_1.mergeCrudConfig)(this.defaultConfig, config);
|
|
274
306
|
this.validateConfiguration();
|
|
275
307
|
}
|
|
308
|
+
boundDbs = new WeakMap();
|
|
309
|
+
dbFor(transaction) {
|
|
310
|
+
return this.withBindLimit(transaction || this.config.db);
|
|
311
|
+
}
|
|
312
|
+
withBindLimit(db) {
|
|
313
|
+
const dialect = db?.dialect;
|
|
314
|
+
if (typeof dialect?.sqlToQuery !== "function" ||
|
|
315
|
+
CHECKED_DIALECTS.has(dialect)) {
|
|
316
|
+
return db;
|
|
317
|
+
}
|
|
318
|
+
const cached = this.boundDbs.get(db);
|
|
319
|
+
if (cached)
|
|
320
|
+
return cached;
|
|
321
|
+
const { limit, name } = this.bindLimit(db);
|
|
322
|
+
const checked = Object.create(dialect);
|
|
323
|
+
const tooMany = (count) => new BindLimitException(`Too many values in one statement: ${count} bound parameters, and ${name} takes at most ${limit}. Split the batch.`);
|
|
324
|
+
checked.sqlToQuery = (...args) => {
|
|
325
|
+
let query;
|
|
326
|
+
try {
|
|
327
|
+
query = dialect.sqlToQuery(...args);
|
|
328
|
+
}
|
|
329
|
+
catch (error) {
|
|
330
|
+
if (error instanceof RangeError)
|
|
331
|
+
throw tooMany(`more than ${limit}`);
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
const count = query?.params?.length ?? 0;
|
|
335
|
+
if (count > limit)
|
|
336
|
+
throw tooMany(String(count));
|
|
337
|
+
return query;
|
|
338
|
+
};
|
|
339
|
+
CHECKED_DIALECTS.add(checked);
|
|
340
|
+
const view = Object.create(db, {
|
|
341
|
+
dialect: { value: checked, writable: true, configurable: true },
|
|
342
|
+
});
|
|
343
|
+
this.boundDbs.set(db, view);
|
|
344
|
+
return view;
|
|
345
|
+
}
|
|
346
|
+
bindLimit(db) {
|
|
347
|
+
if (this.isD1(db))
|
|
348
|
+
return BIND_LIMITS.d1;
|
|
349
|
+
return BIND_LIMITS[this.config.dialect] ?? BIND_LIMITS.postgresql;
|
|
350
|
+
}
|
|
351
|
+
isD1(db) {
|
|
352
|
+
const kind = db?.constructor?.[drizzle_orm_1.entityKind];
|
|
353
|
+
return kind === "D1Database" || kind === "D1Transaction";
|
|
354
|
+
}
|
|
276
355
|
validateConfiguration() {
|
|
277
356
|
if (!this.config.db)
|
|
278
357
|
throw new Error("Database instance is required");
|
|
@@ -366,6 +445,10 @@ class SqlBaseCrudService {
|
|
|
366
445
|
return relation.type;
|
|
367
446
|
return "belongsTo";
|
|
368
447
|
}
|
|
448
|
+
relationNamed(name) {
|
|
449
|
+
const relations = this.config.relations || {};
|
|
450
|
+
return Object.hasOwn(relations, name) ? relations[name] : undefined;
|
|
451
|
+
}
|
|
369
452
|
isCollection(relation) {
|
|
370
453
|
return this.relationType(relation) !== "belongsTo";
|
|
371
454
|
}
|
|
@@ -556,23 +639,19 @@ class SqlBaseCrudService {
|
|
|
556
639
|
const referenced = target[referencedName];
|
|
557
640
|
if (!referenced)
|
|
558
641
|
return;
|
|
559
|
-
const ids =
|
|
560
|
-
if (ids.length === 0)
|
|
561
|
-
for (const host of hosts)
|
|
562
|
-
host[node.name] = null;
|
|
642
|
+
const ids = this.loaderKeys(hosts, localKey, node);
|
|
643
|
+
if (ids.length === 0)
|
|
563
644
|
return;
|
|
564
|
-
}
|
|
565
645
|
const fields = this.relationProjection(target, select.base, [
|
|
566
646
|
referencedName,
|
|
567
647
|
...this.keysForChildren(node.children, target),
|
|
568
648
|
]);
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
const related = await (fields ? db.select(fields) : db.select())
|
|
649
|
+
const scope = this.relatedRowScope(target, options);
|
|
650
|
+
const related = await this.inChunks(db, ids, (chunk) => (fields ? db.select(fields) : db.select())
|
|
572
651
|
.from(target)
|
|
573
|
-
.where((0, drizzle_orm_1.and)(...
|
|
652
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(referenced, chunk), ...scope)));
|
|
574
653
|
const byKey = new Map();
|
|
575
|
-
for (const row of related
|
|
654
|
+
for (const row of related)
|
|
576
655
|
byKey.set(row?.[referencedName], row);
|
|
577
656
|
for (const host of hosts) {
|
|
578
657
|
host[node.name] = byKey.get(host[localKey]) ?? null;
|
|
@@ -587,27 +666,43 @@ class SqlBaseCrudService {
|
|
|
587
666
|
if (!foreign)
|
|
588
667
|
return;
|
|
589
668
|
const parentKey = this.parentKeyOf(node.config, hostTable);
|
|
590
|
-
const ids =
|
|
591
|
-
if (ids.length === 0)
|
|
592
|
-
for (const host of hosts)
|
|
593
|
-
host[node.name] = [];
|
|
669
|
+
const ids = this.loaderKeys(hosts, parentKey, node);
|
|
670
|
+
if (ids.length === 0)
|
|
594
671
|
return;
|
|
595
|
-
}
|
|
596
672
|
const fields = this.relationProjection(target, select.base, [
|
|
597
673
|
foreignKey,
|
|
598
674
|
...this.keysForChildren(node.children, target),
|
|
599
675
|
]);
|
|
600
|
-
const
|
|
601
|
-
conditions.push(...this.relatedRowScope(target, options));
|
|
602
|
-
let query = (fields ? db.select(fields) : db.select())
|
|
603
|
-
.from(target)
|
|
604
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
605
|
-
const orderBy = this.collectionOrderBy(node.config);
|
|
606
|
-
if (orderBy.length > 0)
|
|
607
|
-
query = query.orderBy(...orderBy);
|
|
608
|
-
const children = await query;
|
|
676
|
+
const children = await this.loadCollection(db, node, ids, options, () => fields ? db.select(fields).from(target) : db.select().from(target));
|
|
609
677
|
this.groupCollection(hosts, node.name, parentKey, children, foreignKey);
|
|
610
678
|
}
|
|
679
|
+
loaderKeys(hosts, key, node) {
|
|
680
|
+
const ids = Array.from(new Set(hosts.map((host) => host[key]).filter((v) => v != null)));
|
|
681
|
+
if (ids.length === 0) {
|
|
682
|
+
const collection = this.isCollection(node.config);
|
|
683
|
+
for (const host of hosts)
|
|
684
|
+
host[node.name] = collection ? [] : null;
|
|
685
|
+
}
|
|
686
|
+
return ids;
|
|
687
|
+
}
|
|
688
|
+
loadCollection(db, node, ids, options, source) {
|
|
689
|
+
const relation = node.config;
|
|
690
|
+
const key = this.relationType(relation) === "manyToMany"
|
|
691
|
+
? relation.through[relation.throughLocalKey]
|
|
692
|
+
: relation.table[relation.foreignKey];
|
|
693
|
+
const scope = this.relatedRowScope(relation.table, options);
|
|
694
|
+
const orderBy = this.collectionOrderBy(relation);
|
|
695
|
+
return this.inChunks(db, ids, (chunk) => {
|
|
696
|
+
const query = source().where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(key, chunk), ...scope));
|
|
697
|
+
return orderBy.length > 0 ? query.orderBy(...orderBy) : query;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
selectThrough(db, fields, relation, target = relation.table, through = relation.through) {
|
|
701
|
+
return db
|
|
702
|
+
.select(fields)
|
|
703
|
+
.from(through)
|
|
704
|
+
.innerJoin(target, (0, drizzle_orm_1.eq)(through[relation.throughForeignKey], target[relation.references ?? "id"]));
|
|
705
|
+
}
|
|
611
706
|
async loadManyToMany(db, hosts, node, select, hostTable = this.config.table, options) {
|
|
612
707
|
const { through, throughLocalKey, throughForeignKey, references } = node.config;
|
|
613
708
|
if (!through || !throughLocalKey || !throughForeignKey)
|
|
@@ -620,12 +715,9 @@ class SqlBaseCrudService {
|
|
|
620
715
|
if (!referenced || !joinLocal || !joinForeign)
|
|
621
716
|
return;
|
|
622
717
|
const parentKey = this.parentKeyOf(node.config, hostTable);
|
|
623
|
-
const ids =
|
|
624
|
-
if (ids.length === 0)
|
|
625
|
-
for (const host of hosts)
|
|
626
|
-
host[node.name] = [];
|
|
718
|
+
const ids = this.loaderKeys(hosts, parentKey, node);
|
|
719
|
+
if (ids.length === 0)
|
|
627
720
|
return;
|
|
628
|
-
}
|
|
629
721
|
const columns = (0, drizzle_orm_1.getTableColumns)(target);
|
|
630
722
|
const wanted = select.base.length > 0
|
|
631
723
|
? [...select.base, ...this.keysForChildren(node.children, target)]
|
|
@@ -635,19 +727,25 @@ class SqlBaseCrudService {
|
|
|
635
727
|
if (columns[name])
|
|
636
728
|
fields[name] = columns[name];
|
|
637
729
|
}
|
|
638
|
-
const
|
|
639
|
-
conditions.push(...this.relatedRowScope(target, options));
|
|
640
|
-
let query = db
|
|
641
|
-
.select(fields)
|
|
642
|
-
.from(through)
|
|
643
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(joinForeign, referenced))
|
|
644
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
645
|
-
const orderBy = this.collectionOrderBy(node.config);
|
|
646
|
-
if (orderBy.length > 0)
|
|
647
|
-
query = query.orderBy(...orderBy);
|
|
648
|
-
const rows = await query;
|
|
730
|
+
const rows = await this.loadCollection(db, node, ids, options, () => this.selectThrough(db, fields, node.config));
|
|
649
731
|
this.groupCollection(hosts, node.name, parentKey, rows, JOIN_KEY, true);
|
|
650
732
|
}
|
|
733
|
+
relationChunkSize(db) {
|
|
734
|
+
const { limit } = this.bindLimit(db);
|
|
735
|
+
return Math.max(1, limit - Math.min(1_000, Math.floor(limit / 10)));
|
|
736
|
+
}
|
|
737
|
+
async inChunks(db, ids, query) {
|
|
738
|
+
const size = this.relationChunkSize(db);
|
|
739
|
+
if (ids.length <= size)
|
|
740
|
+
return (await query(ids)) ?? [];
|
|
741
|
+
const out = [];
|
|
742
|
+
for (let start = 0; start < ids.length; start += size) {
|
|
743
|
+
for (const row of (await query(ids.slice(start, start + size))) ?? []) {
|
|
744
|
+
out.push(row);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return out;
|
|
748
|
+
}
|
|
651
749
|
groupCollection(hosts, name, parentKey, rows, groupBy, stripGroupKey = false) {
|
|
652
750
|
const grouped = new Map();
|
|
653
751
|
for (const row of rows ?? []) {
|
|
@@ -783,10 +881,7 @@ class SqlBaseCrudService {
|
|
|
783
881
|
return { key, column, order };
|
|
784
882
|
}
|
|
785
883
|
const name = key.slice(0, dot);
|
|
786
|
-
const
|
|
787
|
-
const relation = Object.hasOwn(relations, name)
|
|
788
|
-
? relations[name]
|
|
789
|
-
: undefined;
|
|
884
|
+
const relation = this.relationNamed(name);
|
|
790
885
|
const column = relation
|
|
791
886
|
? this.columnOf(relation.table, key.slice(dot + 1))
|
|
792
887
|
: undefined;
|
|
@@ -831,8 +926,17 @@ class SqlBaseCrudService {
|
|
|
831
926
|
if (typeof code === "string" && code.startsWith("SQLITE_CONSTRAINT_")) {
|
|
832
927
|
return code.slice("SQLITE_CONSTRAINT_".length);
|
|
833
928
|
}
|
|
929
|
+
if (code === "UNKNOWN_SQLITE_ERROR_3091" || e?.rawCode === 3091) {
|
|
930
|
+
return "DATATYPE";
|
|
931
|
+
}
|
|
932
|
+
if (code === "SQLITE_MISMATCH")
|
|
933
|
+
return "MISMATCH";
|
|
834
934
|
}
|
|
835
935
|
const message = `${error?.cause?.message ?? ""} ${error?.message ?? ""}`;
|
|
936
|
+
if (/cannot store \S+ value in \S+ column/.test(message))
|
|
937
|
+
return "DATATYPE";
|
|
938
|
+
if (/datatype mismatch/.test(message))
|
|
939
|
+
return "MISMATCH";
|
|
836
940
|
const m = message.match(/(UNIQUE|PRIMARY KEY|FOREIGN KEY|NOT NULL|CHECK) constraint failed/);
|
|
837
941
|
return m?.[1]?.replace(" ", "");
|
|
838
942
|
}
|
|
@@ -867,6 +971,8 @@ class SqlBaseCrudService {
|
|
|
867
971
|
static SQLITE_DATA_ERRORS = {
|
|
868
972
|
NOTNULL: "a required value is missing",
|
|
869
973
|
CHECK: "a value violates a column check constraint",
|
|
974
|
+
DATATYPE: "a value does not match its column type",
|
|
975
|
+
MISMATCH: "a value does not match its column type",
|
|
870
976
|
};
|
|
871
977
|
dataErrorMessage(error) {
|
|
872
978
|
const code = error?.code ?? error?.cause?.code;
|
|
@@ -899,7 +1005,7 @@ class SqlBaseCrudService {
|
|
|
899
1005
|
? text.match(/column '([^']+)'/i)?.[1]
|
|
900
1006
|
: undefined) ??
|
|
901
1007
|
(this.config.dialect === "sqlite"
|
|
902
|
-
? text.match(/NOT NULL constraint failed
|
|
1008
|
+
? text.match(/(?:NOT NULL constraint failed:|value in \S+ column) (?:[^.\s]+\.)?(\S+)/)?.[1]
|
|
903
1009
|
: undefined);
|
|
904
1010
|
return new crud_exceptions_1.ValidationFailedException(column ? `${message}: ${column}` : message);
|
|
905
1011
|
}
|
|
@@ -966,9 +1072,22 @@ class SqlBaseCrudService {
|
|
|
966
1072
|
throw new common_1.BadRequestException(`Invalid full-text query mode: ${String(mode)} (expected 'plain' or 'websearch')`);
|
|
967
1073
|
}
|
|
968
1074
|
const parse = drizzle_orm_1.sql.raw(mode === "websearch" ? "websearch_to_tsquery" : "plainto_tsquery");
|
|
969
|
-
const vector = drizzle_orm_1.sql.join(columns.map((column) =>
|
|
1075
|
+
const vector = drizzle_orm_1.sql.join(columns.map((column) => {
|
|
1076
|
+
if (this.isTsVector(column)) {
|
|
1077
|
+
return columns.length > 1
|
|
1078
|
+
? (0, drizzle_orm_1.sql) `coalesce(${column}, ''::tsvector)`
|
|
1079
|
+
: (0, drizzle_orm_1.sql) `${column}`;
|
|
1080
|
+
}
|
|
1081
|
+
return columns.length > 1
|
|
1082
|
+
? (0, drizzle_orm_1.sql) `to_tsvector(${config}, coalesce(${column}, ''))`
|
|
1083
|
+
: (0, drizzle_orm_1.sql) `to_tsvector(${config}, ${column})`;
|
|
1084
|
+
}), (0, drizzle_orm_1.sql) ` || `);
|
|
970
1085
|
return { vector, query: (0, drizzle_orm_1.sql) `${parse}(${config}, ${term})` };
|
|
971
1086
|
}
|
|
1087
|
+
isTsVector(column) {
|
|
1088
|
+
const type = typeof column?.getSQLType === "function" ? column.getSQLType() : "";
|
|
1089
|
+
return String(type).trim().toLowerCase() === "tsvector";
|
|
1090
|
+
}
|
|
972
1091
|
buildSearchCondition(search) {
|
|
973
1092
|
if (!search?.term || !search.columns?.length)
|
|
974
1093
|
return undefined;
|
|
@@ -1060,6 +1179,29 @@ class SqlBaseCrudService {
|
|
|
1060
1179
|
}
|
|
1061
1180
|
return row;
|
|
1062
1181
|
}
|
|
1182
|
+
async guardRow(id, options) {
|
|
1183
|
+
const row = await this.findForGuard(id, options);
|
|
1184
|
+
if (!row)
|
|
1185
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1186
|
+
}
|
|
1187
|
+
skipsGuard(options, ...methods) {
|
|
1188
|
+
if (!this.config.sql?.useReturning)
|
|
1189
|
+
return false;
|
|
1190
|
+
if (this.config.events && !options?.skipEvents)
|
|
1191
|
+
return false;
|
|
1192
|
+
const base = SqlBaseCrudService.prototype;
|
|
1193
|
+
return methods.every((name) => this[name] === base[name]);
|
|
1194
|
+
}
|
|
1195
|
+
async guardOnError(guarded, id, options, run) {
|
|
1196
|
+
try {
|
|
1197
|
+
return await run();
|
|
1198
|
+
}
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
if (!guarded)
|
|
1201
|
+
await this.guardRow(id, options);
|
|
1202
|
+
throw error;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1063
1205
|
assertVersion(id, actual, expected) {
|
|
1064
1206
|
if (expected === undefined || expected === null)
|
|
1065
1207
|
return;
|
|
@@ -1102,16 +1244,23 @@ class SqlBaseCrudService {
|
|
|
1102
1244
|
};
|
|
1103
1245
|
}
|
|
1104
1246
|
async find(id, options) {
|
|
1247
|
+
options = this.withScopeMemo(options);
|
|
1105
1248
|
this.checkReadOptions(options);
|
|
1106
1249
|
return this.findFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], options);
|
|
1107
1250
|
}
|
|
1108
1251
|
async findOne(where, options) {
|
|
1252
|
+
options = this.withScopeMemo(options);
|
|
1109
1253
|
this.checkReadOptions(options);
|
|
1110
1254
|
return this.findFirst(this.buildWhereConditionsFromPartial(where), options);
|
|
1111
1255
|
}
|
|
1112
1256
|
async findFirst(conditions, options) {
|
|
1257
|
+
const first = await this.readFirst(conditions, options);
|
|
1258
|
+
await first.hydrate();
|
|
1259
|
+
return first.row;
|
|
1260
|
+
}
|
|
1261
|
+
async readFirst(conditions, options) {
|
|
1113
1262
|
const { transaction, relations = [], select = [] } = options || {};
|
|
1114
|
-
const db =
|
|
1263
|
+
const db = this.dbFor(transaction);
|
|
1115
1264
|
const nodes = this.relationTree(relations);
|
|
1116
1265
|
const joined = this.planJoins(nodes);
|
|
1117
1266
|
const selection = this.parseSelect(select);
|
|
@@ -1124,16 +1273,21 @@ class SqlBaseCrudService {
|
|
|
1124
1273
|
query = this.applyLock(query, options);
|
|
1125
1274
|
const result = await this.runRead(query);
|
|
1126
1275
|
const rows = this.normalizeRelations(result, joined.map((node) => node.name));
|
|
1276
|
+
return {
|
|
1277
|
+
row: rows[0] || null,
|
|
1278
|
+
hydrate: () => this.hydrate(db, rows, nodes, selection, options),
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
async hydrate(db, rows, nodes, selection, options) {
|
|
1127
1282
|
await Promise.all([
|
|
1128
1283
|
this.loadRelations(db, rows, nodes, selection, undefined, options),
|
|
1129
1284
|
this.loadCounts(db, rows, options?.counts, options),
|
|
1130
1285
|
]);
|
|
1131
|
-
return rows[0] || null;
|
|
1132
1286
|
}
|
|
1133
1287
|
planList(filters, sortBy, sortOrder, options) {
|
|
1134
1288
|
this.checkReadOptions(options);
|
|
1135
1289
|
const { transaction, relations = [], select = [] } = options || {};
|
|
1136
|
-
const db =
|
|
1290
|
+
const db = this.dbFor(transaction);
|
|
1137
1291
|
const nodes = this.relationTree(relations);
|
|
1138
1292
|
const joined = this.planJoins(nodes);
|
|
1139
1293
|
const selection = this.parseSelect(select);
|
|
@@ -1165,6 +1319,9 @@ class SqlBaseCrudService {
|
|
|
1165
1319
|
built.conditions.push(...this.rowScope(options));
|
|
1166
1320
|
return built;
|
|
1167
1321
|
}
|
|
1322
|
+
parseCount(result) {
|
|
1323
|
+
return parseInt(result?.[0]?.count?.toString() || "0", 10);
|
|
1324
|
+
}
|
|
1168
1325
|
buildCountQuery(db, conditions, filterRelations, options) {
|
|
1169
1326
|
let query = db
|
|
1170
1327
|
.select({ count: (0, drizzle_orm_1.sql) `count(*)` })
|
|
@@ -1175,6 +1332,7 @@ class SqlBaseCrudService {
|
|
|
1175
1332
|
return query;
|
|
1176
1333
|
}
|
|
1177
1334
|
async findAll(filters, pagination, options) {
|
|
1335
|
+
options = this.withScopeMemo(options);
|
|
1178
1336
|
const { sortBy, sortOrder = "asc" } = pagination || {};
|
|
1179
1337
|
const withCount = pagination?.withCount !== false;
|
|
1180
1338
|
const { page, limit: safeLimit, offset, } = this.resolvePagination(pagination?.page, pagination?.limit);
|
|
@@ -1197,10 +1355,7 @@ class SqlBaseCrudService {
|
|
|
1197
1355
|
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
1198
1356
|
const data = fetched.slice(0, safeLimit);
|
|
1199
1357
|
const rows = this.normalizeRelations(data, plan.joined.map((node) => node.name));
|
|
1200
|
-
await
|
|
1201
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
|
|
1202
|
-
this.loadCounts(plan.db, rows, options?.counts, options),
|
|
1203
|
-
]);
|
|
1358
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1204
1359
|
if (!withCount) {
|
|
1205
1360
|
return {
|
|
1206
1361
|
data: rows,
|
|
@@ -1212,12 +1367,13 @@ class SqlBaseCrudService {
|
|
|
1212
1367
|
}
|
|
1213
1368
|
return {
|
|
1214
1369
|
data: rows,
|
|
1215
|
-
total:
|
|
1370
|
+
total: this.parseCount(totalResult),
|
|
1216
1371
|
page,
|
|
1217
1372
|
limit: safeLimit,
|
|
1218
1373
|
};
|
|
1219
1374
|
}
|
|
1220
1375
|
async findByCursor(filters, pagination, options) {
|
|
1376
|
+
options = this.withScopeMemo(options);
|
|
1221
1377
|
const { sortBy, sortOrder = "asc", count = false } = pagination || {};
|
|
1222
1378
|
const { limit } = this.resolvePagination(1, pagination?.limit);
|
|
1223
1379
|
const plan = this.planList(filters, sortBy, sortOrder, options);
|
|
@@ -1234,9 +1390,17 @@ class SqlBaseCrudService {
|
|
|
1234
1390
|
conditions.push(this.keysetCondition(plan.sort, values));
|
|
1235
1391
|
}
|
|
1236
1392
|
const fields = this.buildSelection(plan.baseSelect, plan.joined, plan.selection);
|
|
1237
|
-
plan.sort.
|
|
1238
|
-
|
|
1393
|
+
const cursorKeys = plan.sort.map((spec, i) => {
|
|
1394
|
+
const field = this.cursorField(spec.column);
|
|
1395
|
+
const projected = field === spec.column && !spec.relation
|
|
1396
|
+
? Object.keys(fields).find((key) => fields[key] === spec.column)
|
|
1397
|
+
: undefined;
|
|
1398
|
+
if (projected)
|
|
1399
|
+
return projected;
|
|
1400
|
+
fields[`${CURSOR_KEY}${i}`] = field;
|
|
1401
|
+
return `${CURSOR_KEY}${i}`;
|
|
1239
1402
|
});
|
|
1403
|
+
const hidden = cursorKeys.filter((key) => key.startsWith(CURSOR_KEY));
|
|
1240
1404
|
let dataQuery = plan.db.select(fields).from(this.config.table);
|
|
1241
1405
|
dataQuery = this.applyJoins(dataQuery, plan.joinRelations, options);
|
|
1242
1406
|
if (conditions.length > 0)
|
|
@@ -1254,21 +1418,17 @@ class SqlBaseCrudService {
|
|
|
1254
1418
|
const page = data.slice(0, limit);
|
|
1255
1419
|
const last = page[page.length - 1];
|
|
1256
1420
|
const nextCursor = data.length > limit && last
|
|
1257
|
-
? (0, cursor_1.encodeCursor)(
|
|
1421
|
+
? (0, cursor_1.encodeCursor)(cursorKeys.map((key) => last[key]), signature)
|
|
1258
1422
|
: null;
|
|
1259
1423
|
for (const row of page) {
|
|
1260
|
-
|
|
1261
|
-
delete row[
|
|
1262
|
-
});
|
|
1424
|
+
for (const key of hidden)
|
|
1425
|
+
delete row[key];
|
|
1263
1426
|
}
|
|
1264
1427
|
const rows = this.normalizeRelations(page, plan.joined.map((node) => node.name));
|
|
1265
|
-
await
|
|
1266
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, options),
|
|
1267
|
-
this.loadCounts(plan.db, rows, options?.counts, options),
|
|
1268
|
-
]);
|
|
1428
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, options);
|
|
1269
1429
|
const out = { data: rows, limit, nextCursor };
|
|
1270
1430
|
if (count) {
|
|
1271
|
-
out.total =
|
|
1431
|
+
out.total = this.parseCount(totalResult);
|
|
1272
1432
|
}
|
|
1273
1433
|
return out;
|
|
1274
1434
|
}
|
|
@@ -1287,9 +1447,16 @@ class SqlBaseCrudService {
|
|
|
1287
1447
|
return (0, drizzle_orm_1.gt)(column, value);
|
|
1288
1448
|
if (op === "<")
|
|
1289
1449
|
return (0, drizzle_orm_1.lt)(column, value);
|
|
1450
|
+
if (op === ">=")
|
|
1451
|
+
return (0, drizzle_orm_1.gte)(column, value);
|
|
1452
|
+
if (op === "<=")
|
|
1453
|
+
return (0, drizzle_orm_1.lte)(column, value);
|
|
1290
1454
|
return (0, drizzle_orm_1.eq)(column, value);
|
|
1291
1455
|
}
|
|
1292
1456
|
keysetCondition(specs, values) {
|
|
1457
|
+
const row = this.keysetRowComparison(specs, values);
|
|
1458
|
+
if (row)
|
|
1459
|
+
return row;
|
|
1293
1460
|
const branches = [];
|
|
1294
1461
|
specs.forEach((spec, i) => {
|
|
1295
1462
|
const after = this.afterCursor(spec, values[i]);
|
|
@@ -1302,21 +1469,64 @@ class SqlBaseCrudService {
|
|
|
1302
1469
|
: this.compareCursor(previous.column, "=", values[j]));
|
|
1303
1470
|
branches.push(ties.length > 0 ? (0, drizzle_orm_1.and)(...ties, after) : after);
|
|
1304
1471
|
});
|
|
1305
|
-
|
|
1472
|
+
if (branches.length === 0)
|
|
1473
|
+
return (0, drizzle_orm_1.sql) `false`;
|
|
1474
|
+
const after = (0, drizzle_orm_1.or)(...branches);
|
|
1475
|
+
const bound = this.keysetBound(specs, values);
|
|
1476
|
+
return bound ? (0, drizzle_orm_1.and)(bound, after) : after;
|
|
1477
|
+
}
|
|
1478
|
+
keysetBound(specs, values) {
|
|
1479
|
+
const [first] = specs;
|
|
1480
|
+
if (!first || specs.length < 2 || values[0] === null)
|
|
1481
|
+
return undefined;
|
|
1482
|
+
if (this.keysetNullable(first) && !this.nullsFirst(first)) {
|
|
1483
|
+
return undefined;
|
|
1484
|
+
}
|
|
1485
|
+
return this.compareCursor(first.column, first.order === "asc" ? ">=" : "<=", values[0]);
|
|
1486
|
+
}
|
|
1487
|
+
keysetRowComparison(specs, values) {
|
|
1488
|
+
if (this.config.dialect !== "postgresql" || specs.length < 2) {
|
|
1489
|
+
return undefined;
|
|
1490
|
+
}
|
|
1491
|
+
const order = specs[0].order;
|
|
1492
|
+
const fits = specs.every((spec, i) => spec.order === order &&
|
|
1493
|
+
values[i] !== null &&
|
|
1494
|
+
!this.keysetNullable(spec));
|
|
1495
|
+
if (!fits)
|
|
1496
|
+
return undefined;
|
|
1497
|
+
const columns = drizzle_orm_1.sql.join(specs.map((spec) => spec.column), (0, drizzle_orm_1.sql) `, `);
|
|
1498
|
+
const params = drizzle_orm_1.sql.join(specs.map((spec, i) => spec.column?.dataType === "date"
|
|
1499
|
+
? (0, drizzle_orm_1.sql) `${values[i]}`
|
|
1500
|
+
: drizzle_orm_1.sql.param(values[i], spec.column)), (0, drizzle_orm_1.sql) `, `);
|
|
1501
|
+
return (0, drizzle_orm_1.sql) `(${columns}) ${drizzle_orm_1.sql.raw(order === "asc" ? ">" : "<")} (${params})`;
|
|
1502
|
+
}
|
|
1503
|
+
keysetNullable(spec) {
|
|
1504
|
+
if (spec.relation)
|
|
1505
|
+
return true;
|
|
1506
|
+
return !(spec.column?.notNull || spec.column?.primary);
|
|
1507
|
+
}
|
|
1508
|
+
nullsFirst(spec) {
|
|
1509
|
+
return this.config.dialect === "postgresql"
|
|
1510
|
+
? spec.order === "desc"
|
|
1511
|
+
: spec.order === "asc";
|
|
1306
1512
|
}
|
|
1307
1513
|
afterCursor(spec, value) {
|
|
1308
|
-
const
|
|
1309
|
-
const nullsFirst = postgres ? spec.order === "desc" : spec.order === "asc";
|
|
1514
|
+
const nullsFirst = this.nullsFirst(spec);
|
|
1310
1515
|
if (value === null) {
|
|
1311
1516
|
return nullsFirst ? (0, drizzle_orm_1.isNotNull)(spec.column) : undefined;
|
|
1312
1517
|
}
|
|
1313
1518
|
const past = this.compareCursor(spec.column, spec.order === "asc" ? ">" : "<", value);
|
|
1314
|
-
return nullsFirst
|
|
1519
|
+
return nullsFirst || !this.keysetNullable(spec)
|
|
1520
|
+
? past
|
|
1521
|
+
: (0, drizzle_orm_1.or)(past, (0, drizzle_orm_1.isNull)(spec.column));
|
|
1315
1522
|
}
|
|
1316
1523
|
async emitEvent(phase, operation, bulk, payload, options) {
|
|
1317
1524
|
const bus = this.config.events;
|
|
1318
1525
|
if (!bus || options?.skipEvents)
|
|
1319
1526
|
return;
|
|
1527
|
+
if (payload.transaction && typeof payload.transaction === "object") {
|
|
1528
|
+
this.ownedTransactions.delete(payload.transaction);
|
|
1529
|
+
}
|
|
1320
1530
|
await bus.emit({
|
|
1321
1531
|
entity: this.getEntityName(),
|
|
1322
1532
|
operation,
|
|
@@ -1338,10 +1548,26 @@ class SqlBaseCrudService {
|
|
|
1338
1548
|
return;
|
|
1339
1549
|
}
|
|
1340
1550
|
const hosts = rows.filter(Boolean);
|
|
1341
|
-
await
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1551
|
+
await this.hydrate(db, hosts, nodes, undefined, options);
|
|
1552
|
+
}
|
|
1553
|
+
loadsAfterCommit(options) {
|
|
1554
|
+
if (options?.transaction)
|
|
1555
|
+
return false;
|
|
1556
|
+
try {
|
|
1557
|
+
return this.countPlan(options?.counts).every((count) => !count.filter);
|
|
1558
|
+
}
|
|
1559
|
+
catch {
|
|
1560
|
+
return false;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
async writeThenLoad(options, write) {
|
|
1564
|
+
const late = this.loadsAfterCommit(options);
|
|
1565
|
+
const rows = await this.executeSqlTransaction((tx) => write(tx, (inTx, written) => late
|
|
1566
|
+
? Promise.resolve()
|
|
1567
|
+
: this.loadWriteRelations(inTx, written, options)), options?.transaction);
|
|
1568
|
+
if (late)
|
|
1569
|
+
await this.loadWriteRelations(this.dbFor(), rows, options);
|
|
1570
|
+
return rows;
|
|
1345
1571
|
}
|
|
1346
1572
|
expectedVersionOf(data, options) {
|
|
1347
1573
|
const column = this.config.versionColumn;
|
|
@@ -1397,13 +1623,15 @@ class SqlBaseCrudService {
|
|
|
1397
1623
|
return createdEntity;
|
|
1398
1624
|
}
|
|
1399
1625
|
async create(data, options) {
|
|
1626
|
+
options = this.withScopeMemo(options);
|
|
1400
1627
|
const { transaction, hooks = {} } = options || {};
|
|
1401
1628
|
this.checkReadOptions(options);
|
|
1402
1629
|
const entityData = await this.prepareCreate(data, options);
|
|
1403
1630
|
const nested = this.takeNested(entityData, options);
|
|
1404
|
-
const db =
|
|
1631
|
+
const db = this.dbFor(transaction);
|
|
1405
1632
|
await this.emitEvent("before", "create", false, { input: data, transaction }, options);
|
|
1406
1633
|
try {
|
|
1634
|
+
await this.assertKeysInScope(db, entityData, options, nested.map((entry) => entry.name));
|
|
1407
1635
|
const createdEntity = nested.length === 0
|
|
1408
1636
|
? await this.insertOne(db, entityData, options)
|
|
1409
1637
|
: await this.executeSqlTransaction((tx) => this.insertNested(tx, entityData, nested, {
|
|
@@ -1494,7 +1722,7 @@ class SqlBaseCrudService {
|
|
|
1494
1722
|
.returning();
|
|
1495
1723
|
ids.push(...inserted.map((r) => r[relation.references ?? "id"]));
|
|
1496
1724
|
}
|
|
1497
|
-
await this.linkTargets(tx, relation, parentValue, ids, options);
|
|
1725
|
+
await this.linkTargets(tx, relation, parentValue, ids, options, true);
|
|
1498
1726
|
}
|
|
1499
1727
|
return created;
|
|
1500
1728
|
}
|
|
@@ -1526,7 +1754,56 @@ class SqlBaseCrudService {
|
|
|
1526
1754
|
}
|
|
1527
1755
|
return out;
|
|
1528
1756
|
}
|
|
1529
|
-
async
|
|
1757
|
+
async keysOutsideScope(db, rows, options, skip = []) {
|
|
1758
|
+
const failures = new Map();
|
|
1759
|
+
if (!this.resolveScope(options))
|
|
1760
|
+
return failures;
|
|
1761
|
+
for (const [name, relation] of Object.entries(this.config.relations ?? {})) {
|
|
1762
|
+
const local = relation.localKey;
|
|
1763
|
+
if (!local || this.isCollection(relation) || skip.includes(name)) {
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1766
|
+
const scope = this.relatedScopeConditions(relation.table, options);
|
|
1767
|
+
const referenced = relation.table[relation.references ?? "id"];
|
|
1768
|
+
if (scope.length === 0 || !referenced)
|
|
1769
|
+
continue;
|
|
1770
|
+
const keys = this.uniqueValues(rows.map((row) => row[local]).filter((value) => value != null));
|
|
1771
|
+
if (keys.length === 0)
|
|
1772
|
+
continue;
|
|
1773
|
+
const found = await db
|
|
1774
|
+
.select({ id: referenced })
|
|
1775
|
+
.from(relation.table)
|
|
1776
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(referenced, keys), ...scope));
|
|
1777
|
+
const present = new Set(found.map((row) => this.valueKey(row.id)));
|
|
1778
|
+
rows.forEach((row, index) => {
|
|
1779
|
+
const value = row[local];
|
|
1780
|
+
if (value == null || present.has(this.valueKey(value)))
|
|
1781
|
+
return;
|
|
1782
|
+
if (!failures.has(index)) {
|
|
1783
|
+
failures.set(index, this.missingParentException(local));
|
|
1784
|
+
}
|
|
1785
|
+
});
|
|
1786
|
+
}
|
|
1787
|
+
return failures;
|
|
1788
|
+
}
|
|
1789
|
+
async assertBatchKeysInScope(db, rows, refs, message, options) {
|
|
1790
|
+
const failures = await this.keysOutsideScope(db, rows, options);
|
|
1791
|
+
if (failures.size === 0)
|
|
1792
|
+
return;
|
|
1793
|
+
throw new crud_exceptions_1.BulkOperationException(message, Array.from(failures, ([index, error]) => ({ ...refs[index], error })));
|
|
1794
|
+
}
|
|
1795
|
+
async assertKeysInScope(db, row, options, skip = []) {
|
|
1796
|
+
const failure = (await this.keysOutsideScope(db, [row], options, skip)).get(0);
|
|
1797
|
+
if (failure)
|
|
1798
|
+
throw failure;
|
|
1799
|
+
}
|
|
1800
|
+
missingParentException(localKey) {
|
|
1801
|
+
const field = this.config.dialect === "postgresql"
|
|
1802
|
+
? (this.columnOf(this.config.table, localKey)?.name ?? localKey)
|
|
1803
|
+
: "reference";
|
|
1804
|
+
return new crud_exceptions_1.ForeignKeyViolationException(this.getEntityName(), field);
|
|
1805
|
+
}
|
|
1806
|
+
async linkTargets(tx, relation, parentValue, ids, options, fresh = false) {
|
|
1530
1807
|
const wanted = this.uniqueValues(ids);
|
|
1531
1808
|
if (wanted.length === 0)
|
|
1532
1809
|
return;
|
|
@@ -1550,10 +1827,12 @@ class SqlBaseCrudService {
|
|
|
1550
1827
|
error: new crud_exceptions_1.EntityNotFoundException(name, id),
|
|
1551
1828
|
})));
|
|
1552
1829
|
}
|
|
1553
|
-
const linked =
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1830
|
+
const linked = fresh
|
|
1831
|
+
? []
|
|
1832
|
+
: await tx
|
|
1833
|
+
.select({ id: foreign })
|
|
1834
|
+
.from(through)
|
|
1835
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(local, parentValue), (0, drizzle_orm_1.inArray)(foreign, wanted)));
|
|
1557
1836
|
const already = new Set(linked.map((row) => this.valueKey(row.id)));
|
|
1558
1837
|
const add = wanted.filter((id) => !already.has(this.valueKey(id)));
|
|
1559
1838
|
if (add.length > 0) {
|
|
@@ -1564,19 +1843,19 @@ class SqlBaseCrudService {
|
|
|
1564
1843
|
}
|
|
1565
1844
|
}
|
|
1566
1845
|
async attach(id, relation, relatedIds, options) {
|
|
1846
|
+
options = this.withScopeMemo(options);
|
|
1567
1847
|
return this.changeLinks("attach", id, relation, relatedIds, options);
|
|
1568
1848
|
}
|
|
1569
1849
|
async detach(id, relation, relatedIds, options) {
|
|
1850
|
+
options = this.withScopeMemo(options);
|
|
1570
1851
|
return this.changeLinks("detach", id, relation, relatedIds, options);
|
|
1571
1852
|
}
|
|
1572
1853
|
async sync(id, relation, relatedIds, options) {
|
|
1854
|
+
options = this.withScopeMemo(options);
|
|
1573
1855
|
return this.changeLinks("sync", id, relation, relatedIds, options);
|
|
1574
1856
|
}
|
|
1575
1857
|
async changeLinks(operation, id, name, relatedIds, options) {
|
|
1576
|
-
const
|
|
1577
|
-
const relation = Object.hasOwn(relations, name)
|
|
1578
|
-
? relations[name]
|
|
1579
|
-
: undefined;
|
|
1858
|
+
const relation = this.relationNamed(name);
|
|
1580
1859
|
if (!relation || this.relationType(relation) !== "manyToMany") {
|
|
1581
1860
|
throw new common_1.BadRequestException(`"${name}" is not a many-to-many relation of ${this.getEntityName()}`);
|
|
1582
1861
|
}
|
|
@@ -1597,13 +1876,13 @@ class SqlBaseCrudService {
|
|
|
1597
1876
|
const parentKey = this.parentKeyOf(relation);
|
|
1598
1877
|
return this.executeSqlTransaction(async (tx) => {
|
|
1599
1878
|
const opts = { ...options, transaction: tx };
|
|
1600
|
-
const
|
|
1879
|
+
const read = {
|
|
1601
1880
|
...opts,
|
|
1602
|
-
relations: [],
|
|
1603
|
-
counts: undefined,
|
|
1604
|
-
select: [this.config.primaryKey, parentKey],
|
|
1881
|
+
relations: Array.from(new Set([name, ...(options?.relations ?? [])])),
|
|
1605
1882
|
skipAllowlists: true,
|
|
1606
|
-
}
|
|
1883
|
+
};
|
|
1884
|
+
this.checkReadOptions(read);
|
|
1885
|
+
const { row: parent, hydrate } = await this.readFirst([(0, drizzle_orm_1.eq)(this.config.table[this.config.primaryKey], id)], read);
|
|
1607
1886
|
if (!parent)
|
|
1608
1887
|
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1609
1888
|
await this.emitEvent("before", operation, false, { ids: [id], input: relatedIds, transaction: tx }, options);
|
|
@@ -1631,11 +1910,8 @@ class SqlBaseCrudService {
|
|
|
1631
1910
|
: (0, drizzle_orm_1.eq)(local, parentValue));
|
|
1632
1911
|
}
|
|
1633
1912
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
relations: Array.from(new Set([name, ...(options?.relations ?? [])])),
|
|
1637
|
-
skipAllowlists: true,
|
|
1638
|
-
}));
|
|
1913
|
+
await hydrate();
|
|
1914
|
+
const row = parent;
|
|
1639
1915
|
await this.emitEvent("after", operation, false, { ids: [id], input: relatedIds, rows: [row], transaction: tx }, options);
|
|
1640
1916
|
return row;
|
|
1641
1917
|
}, options?.transaction);
|
|
@@ -1677,26 +1953,31 @@ class SqlBaseCrudService {
|
|
|
1677
1953
|
return entityData;
|
|
1678
1954
|
}
|
|
1679
1955
|
async update(id, data, options) {
|
|
1956
|
+
options = this.withScopeMemo(options);
|
|
1680
1957
|
const { transaction, hooks = {} } = options || {};
|
|
1681
1958
|
this.checkReadOptions(options);
|
|
1682
1959
|
const expected = this.expectedVersionOf(data, options);
|
|
1683
1960
|
const opts = { ...options, expectedVersion: expected };
|
|
1684
|
-
const
|
|
1685
|
-
if (
|
|
1686
|
-
|
|
1687
|
-
await this.
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1961
|
+
const guarded = !this.skipsGuard(options, "validateUpdate", "mapUpdateDtoToEntity", ...(hooks.skipBefore ? [] : ["beforeUpdate"]));
|
|
1962
|
+
if (guarded)
|
|
1963
|
+
await this.guardRow(id, opts);
|
|
1964
|
+
const entityData = await this.guardOnError(guarded, id, opts, async () => {
|
|
1965
|
+
await this.validateUpdate(id, data);
|
|
1966
|
+
const processedData = hooks.skipBefore
|
|
1967
|
+
? data
|
|
1968
|
+
: await this.beforeUpdate(id, data);
|
|
1969
|
+
return this.prepareUpdate(processedData, opts);
|
|
1970
|
+
});
|
|
1692
1971
|
if (this.config.timestamps?.updatedAt) {
|
|
1693
1972
|
entityData[this.config.timestamps.updatedAt] = new Date();
|
|
1694
1973
|
}
|
|
1695
|
-
const db =
|
|
1974
|
+
const db = this.dbFor(transaction);
|
|
1696
1975
|
await this.emitEvent("before", "update", false, { ids: [id], input: data, transaction }, options);
|
|
1697
1976
|
try {
|
|
1698
|
-
|
|
1977
|
+
await this.guardOnError(guarded, id, opts, () => this.assertKeysInScope(db, entityData, opts));
|
|
1978
|
+
const [updatedEntity] = await this.updateWhereIn(db, this.withVersionBump(entityData), [id], opts, this.softDeleteScope(opts));
|
|
1699
1979
|
if (!updatedEntity) {
|
|
1980
|
+
await this.guardRow(id, opts);
|
|
1700
1981
|
if (expected !== undefined) {
|
|
1701
1982
|
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1702
1983
|
}
|
|
@@ -1712,11 +1993,12 @@ class SqlBaseCrudService {
|
|
|
1712
1993
|
throw this.toClientError(error, "write");
|
|
1713
1994
|
}
|
|
1714
1995
|
}
|
|
1715
|
-
async updateWhereIn(db, set, ids, options) {
|
|
1996
|
+
async updateWhereIn(db, set, ids, options, extra) {
|
|
1997
|
+
const where = this.writeWhere(ids, options);
|
|
1716
1998
|
const query = db
|
|
1717
1999
|
.update(this.config.table)
|
|
1718
2000
|
.set(set)
|
|
1719
|
-
.where(
|
|
2001
|
+
.where(extra ? (0, drizzle_orm_1.and)(where, extra) : where);
|
|
1720
2002
|
if (this.config.sql?.useReturning) {
|
|
1721
2003
|
return this.inIdOrder(await query.returning(), ids);
|
|
1722
2004
|
}
|
|
@@ -1742,39 +2024,65 @@ class SqlBaseCrudService {
|
|
|
1742
2024
|
.map((id) => byKey.get(this.keyOf(id)))
|
|
1743
2025
|
.filter((row) => !!row);
|
|
1744
2026
|
}
|
|
1745
|
-
async
|
|
1746
|
-
if (!this.config.softDelete?.enabled) {
|
|
1747
|
-
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1748
|
-
}
|
|
2027
|
+
async guardedWrite(id, options, step) {
|
|
1749
2028
|
const { transaction, hooks = {} } = options || {};
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
onlyDeleted: false,
|
|
1754
|
-
});
|
|
1755
|
-
if (!existing)
|
|
1756
|
-
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
2029
|
+
if (!this.skipsGuard(options, ...(hooks.skipBefore ? [] : [step.hook]))) {
|
|
2030
|
+
await this.guardRow(id, step.state);
|
|
2031
|
+
}
|
|
1757
2032
|
if (!hooks.skipBefore)
|
|
1758
|
-
await
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
.
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
2033
|
+
await step.before();
|
|
2034
|
+
const event = { ids: [id], transaction };
|
|
2035
|
+
await this.emitEvent("before", step.operation, false, event, options);
|
|
2036
|
+
const finish = async () => {
|
|
2037
|
+
const result = await step.write(this.dbFor(transaction));
|
|
2038
|
+
if (result === undefined) {
|
|
2039
|
+
await this.guardRow(id, step.state);
|
|
2040
|
+
return step.miss();
|
|
2041
|
+
}
|
|
2042
|
+
if (!hooks.skipAfter)
|
|
2043
|
+
await step.after(result);
|
|
2044
|
+
const rows = step.rows?.(result);
|
|
2045
|
+
await this.emitEvent("after", step.operation, false, { ...event, rows }, options);
|
|
2046
|
+
return result;
|
|
2047
|
+
};
|
|
2048
|
+
if (!step.errors)
|
|
2049
|
+
return finish();
|
|
2050
|
+
try {
|
|
2051
|
+
return await finish();
|
|
2052
|
+
}
|
|
2053
|
+
catch (error) {
|
|
2054
|
+
throw this.toClientError(error, step.errors);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
staleOrFalse(id, options) {
|
|
2058
|
+
if (options?.expectedVersion != null) {
|
|
1770
2059
|
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1771
2060
|
}
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2061
|
+
return false;
|
|
2062
|
+
}
|
|
2063
|
+
async softDelete(id, options) {
|
|
2064
|
+
options = this.withScopeMemo(options);
|
|
2065
|
+
if (!this.config.softDelete?.enabled) {
|
|
2066
|
+
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1776
2067
|
}
|
|
1777
|
-
|
|
2068
|
+
const live = { ...options, withDeleted: false, onlyDeleted: false };
|
|
2069
|
+
return this.guardedWrite(id, options, {
|
|
2070
|
+
operation: "softDelete",
|
|
2071
|
+
state: live,
|
|
2072
|
+
hook: "beforeSoftDelete",
|
|
2073
|
+
before: () => this.beforeSoftDelete(id),
|
|
2074
|
+
write: async (db) => {
|
|
2075
|
+
let query = db
|
|
2076
|
+
.update(this.config.table)
|
|
2077
|
+
.set(this.softDeleteSet(new Date()))
|
|
2078
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(live)));
|
|
2079
|
+
if (this.config.sql?.useReturning)
|
|
2080
|
+
query = query.returning();
|
|
2081
|
+
return this.wasAffected(await this.runRead(query)) || undefined;
|
|
2082
|
+
},
|
|
2083
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2084
|
+
after: () => this.afterSoftDelete(id),
|
|
2085
|
+
});
|
|
1778
2086
|
}
|
|
1779
2087
|
softDeleteSet(deletedAt) {
|
|
1780
2088
|
return this.withVersionBump({
|
|
@@ -1785,62 +2093,43 @@ class SqlBaseCrudService {
|
|
|
1785
2093
|
});
|
|
1786
2094
|
}
|
|
1787
2095
|
async restore(id, options) {
|
|
2096
|
+
options = this.withScopeMemo(options);
|
|
1788
2097
|
if (!this.config.softDelete?.enabled) {
|
|
1789
2098
|
throw new common_1.BadRequestException("Soft delete is not enabled for this entity");
|
|
1790
2099
|
}
|
|
1791
|
-
const
|
|
1792
|
-
const
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
2100
|
+
const trashed = { ...options, withDeleted: false, onlyDeleted: true };
|
|
2101
|
+
const restored = await this.guardedWrite(id, options, {
|
|
2102
|
+
operation: "restore",
|
|
2103
|
+
state: trashed,
|
|
2104
|
+
hook: "beforeRestore",
|
|
2105
|
+
before: () => this.beforeRestore(id),
|
|
2106
|
+
write: async (db) => (await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options, this.softDeleteScope(trashed))))[0],
|
|
2107
|
+
miss: () => {
|
|
2108
|
+
if (options?.expectedVersion != null && this.config.versionColumn) {
|
|
2109
|
+
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
2110
|
+
}
|
|
2111
|
+
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
2112
|
+
},
|
|
2113
|
+
after: (entity) => this.afterRestore(entity),
|
|
2114
|
+
rows: (entity) => [entity],
|
|
1796
2115
|
});
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
if (!hooks.skipBefore)
|
|
1800
|
-
await this.beforeRestore(id);
|
|
1801
|
-
await this.emitEvent("before", "restore", false, { ids: [id], transaction }, options);
|
|
1802
|
-
const db = transaction || this.config.db;
|
|
1803
|
-
const [restoredEntity] = await this.runRead(this.updateWhereIn(db, this.softDeleteSet(null), [id], options));
|
|
1804
|
-
if (!restoredEntity) {
|
|
1805
|
-
if (options?.expectedVersion != null && this.config.versionColumn) {
|
|
1806
|
-
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1807
|
-
}
|
|
1808
|
-
throw new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), id);
|
|
1809
|
-
}
|
|
1810
|
-
if (!hooks.skipAfter)
|
|
1811
|
-
await this.afterRestore(restoredEntity);
|
|
1812
|
-
await this.emitEvent("after", "restore", false, { ids: [id], rows: [restoredEntity], transaction }, options);
|
|
1813
|
-
await this.loadWriteRelations(db, [restoredEntity], options);
|
|
1814
|
-
return restoredEntity;
|
|
2116
|
+
await this.loadWriteRelations(this.dbFor(options?.transaction), [restored], options);
|
|
2117
|
+
return restored;
|
|
1815
2118
|
}
|
|
1816
2119
|
async delete(id, options) {
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
const success = this.wasAffected(result);
|
|
1831
|
-
if (!success && options?.expectedVersion != null) {
|
|
1832
|
-
throw new crud_exceptions_1.StaleVersionException(this.getEntityName(), id);
|
|
1833
|
-
}
|
|
1834
|
-
if (success && !hooks.skipAfter)
|
|
1835
|
-
await this.afterDelete(id);
|
|
1836
|
-
if (success) {
|
|
1837
|
-
await this.emitEvent("after", "delete", false, { ids: [id], transaction }, options);
|
|
1838
|
-
}
|
|
1839
|
-
return success;
|
|
1840
|
-
}
|
|
1841
|
-
catch (error) {
|
|
1842
|
-
throw this.toClientError(error, "delete");
|
|
1843
|
-
}
|
|
2120
|
+
options = this.withScopeMemo(options);
|
|
2121
|
+
return this.guardedWrite(id, options, {
|
|
2122
|
+
operation: "delete",
|
|
2123
|
+
state: options,
|
|
2124
|
+
hook: "beforeDelete",
|
|
2125
|
+
before: () => this.beforeDelete(id),
|
|
2126
|
+
write: async (db) => this.wasAffected(await db
|
|
2127
|
+
.delete(this.config.table)
|
|
2128
|
+
.where((0, drizzle_orm_1.and)(this.writeWhere([id], options), this.softDeleteScope(options)))) || undefined,
|
|
2129
|
+
miss: () => this.staleOrFalse(id, options),
|
|
2130
|
+
after: () => this.afterDelete(id),
|
|
2131
|
+
errors: "delete",
|
|
2132
|
+
});
|
|
1844
2133
|
}
|
|
1845
2134
|
async forceDelete(id, options) {
|
|
1846
2135
|
return this.delete(id, {
|
|
@@ -1850,12 +2139,14 @@ class SqlBaseCrudService {
|
|
|
1850
2139
|
});
|
|
1851
2140
|
}
|
|
1852
2141
|
async upsert(data, upsert = {}, options) {
|
|
2142
|
+
options = this.withScopeMemo(options);
|
|
1853
2143
|
this.checkReadOptions(options);
|
|
1854
2144
|
const entityData = await this.prepareCreate(data, options);
|
|
1855
|
-
const db = options?.transaction
|
|
2145
|
+
const db = this.dbFor(options?.transaction);
|
|
1856
2146
|
await this.emitEvent("before", "upsert", false, { input: data, transaction: options?.transaction }, options);
|
|
1857
2147
|
let row;
|
|
1858
2148
|
try {
|
|
2149
|
+
await this.assertKeysInScope(db, entityData, options);
|
|
1859
2150
|
[row] = await this.upsertRows(db, [entityData], upsert, options);
|
|
1860
2151
|
}
|
|
1861
2152
|
catch (error) {
|
|
@@ -1874,43 +2165,40 @@ class SqlBaseCrudService {
|
|
|
1874
2165
|
return row;
|
|
1875
2166
|
}
|
|
1876
2167
|
async massUpsert(data, upsert = {}, options) {
|
|
1877
|
-
this.
|
|
1878
|
-
this.assertBatchSize(this.insertParameters(data));
|
|
2168
|
+
options = this.withScopeMemo(options);
|
|
1879
2169
|
const message = "Mass upsert errors";
|
|
1880
|
-
return this.
|
|
1881
|
-
|
|
1882
|
-
|
|
2170
|
+
return this.massWrite({
|
|
2171
|
+
operation: "upsert",
|
|
2172
|
+
message,
|
|
2173
|
+
options,
|
|
2174
|
+
input: data,
|
|
2175
|
+
returnsRows: true,
|
|
2176
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
1883
2177
|
index,
|
|
1884
2178
|
run: () => this.prepareCreate(item, opts),
|
|
1885
|
-
})),
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
throw new crud_exceptions_1.BulkOperationException(message, rows.flatMap((row, index) => seen.has(keys.map((k) => this.valueKey(row[k])).join("|"))
|
|
1897
|
-
? []
|
|
1898
|
-
: [
|
|
1899
|
-
{
|
|
1900
|
-
index,
|
|
1901
|
-
error: new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(row, upsert)),
|
|
1902
|
-
},
|
|
1903
|
-
]));
|
|
1904
|
-
}
|
|
1905
|
-
await this.emitEvent("after", "upsert", true, {
|
|
1906
|
-
ids: this.idsOf(written),
|
|
1907
|
-
input: data,
|
|
1908
|
-
rows: written,
|
|
1909
|
-
transaction: tx,
|
|
1910
|
-
}, options);
|
|
1911
|
-
await this.loadWriteRelations(tx, written, options);
|
|
2179
|
+
})),
|
|
2180
|
+
keys: true,
|
|
2181
|
+
write: (tx, rows, opts) => this.upsertSet(tx, rows, upsert, opts, message),
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
async upsertSet(tx, rows, upsert, opts, message) {
|
|
2185
|
+
const written = await this.runSet(tx, message, (sp) => this.upsertRows(sp, rows, upsert, opts), rows.map((row, index) => ({
|
|
2186
|
+
index,
|
|
2187
|
+
run: (sp) => this.upsertRows(sp, [row], upsert, opts),
|
|
2188
|
+
})), "write");
|
|
2189
|
+
if (written.length >= rows.length)
|
|
1912
2190
|
return written;
|
|
1913
|
-
|
|
2191
|
+
const keys = this.conflictKeys(upsert);
|
|
2192
|
+
const keyOf = (row) => keys.map((k) => this.valueKey(row[k])).join("|");
|
|
2193
|
+
const seen = new Set(written.map(keyOf));
|
|
2194
|
+
throw new crud_exceptions_1.BulkOperationException(message, rows.flatMap((row, index) => seen.has(keyOf(row))
|
|
2195
|
+
? []
|
|
2196
|
+
: [
|
|
2197
|
+
{
|
|
2198
|
+
index,
|
|
2199
|
+
error: new crud_exceptions_1.EntityNotFoundException(this.getEntityName(), this.conflictValue(row, upsert)),
|
|
2200
|
+
},
|
|
2201
|
+
]));
|
|
1914
2202
|
}
|
|
1915
2203
|
conflictKeys(upsert) {
|
|
1916
2204
|
return upsert.conflictTarget === undefined
|
|
@@ -2005,38 +2293,78 @@ class SqlBaseCrudService {
|
|
|
2005
2293
|
? (0, drizzle_orm_1.sql) `values(${drizzle_orm_1.sql.identifier(column.name)})`
|
|
2006
2294
|
: (0, drizzle_orm_1.sql) `excluded.${drizzle_orm_1.sql.identifier(column.name)}`;
|
|
2007
2295
|
}
|
|
2296
|
+
async massWrite(spec) {
|
|
2297
|
+
const { options, message, ids } = spec;
|
|
2298
|
+
if (spec.returnsRows)
|
|
2299
|
+
this.checkReadOptions(options);
|
|
2300
|
+
const body = async (tx, load) => {
|
|
2301
|
+
const opts = { ...options, transaction: tx, ...spec.state };
|
|
2302
|
+
const unique = ids ? this.uniqueIds(ids) : [];
|
|
2303
|
+
if (ids) {
|
|
2304
|
+
if (unique.length === 0)
|
|
2305
|
+
return [];
|
|
2306
|
+
await this.assertAllExist(tx, unique, opts, message);
|
|
2307
|
+
}
|
|
2308
|
+
const items = spec.prepare?.(opts, unique);
|
|
2309
|
+
const prepared = items ? await this.eachItem(items, message) : [];
|
|
2310
|
+
if (!ids && prepared.length === 0)
|
|
2311
|
+
return [];
|
|
2312
|
+
const event = {
|
|
2313
|
+
ids: ids ? unique : undefined,
|
|
2314
|
+
input: spec.input,
|
|
2315
|
+
transaction: tx,
|
|
2316
|
+
};
|
|
2317
|
+
await this.emitEvent("before", spec.operation, true, event, options);
|
|
2318
|
+
if (spec.keys) {
|
|
2319
|
+
await this.assertBatchKeysInScope(tx, prepared, ids
|
|
2320
|
+
? unique.map((id) => ({ id }))
|
|
2321
|
+
: prepared.map((_, index) => ({ index })), message, opts);
|
|
2322
|
+
}
|
|
2323
|
+
const written = await spec.write(tx, prepared, opts, unique);
|
|
2324
|
+
if (ids)
|
|
2325
|
+
this.assertAllWritten(unique, written, message, options);
|
|
2326
|
+
if (spec.after && !options?.hooks?.skipAfter) {
|
|
2327
|
+
await spec.after(written, unique);
|
|
2328
|
+
}
|
|
2329
|
+
await this.emitEvent("after", spec.operation, true, {
|
|
2330
|
+
...event,
|
|
2331
|
+
ids: ids ? unique : this.idsOf(written),
|
|
2332
|
+
rows: spec.returnsRows ? written : undefined,
|
|
2333
|
+
}, options);
|
|
2334
|
+
if (spec.returnsRows)
|
|
2335
|
+
await load(tx, written);
|
|
2336
|
+
return ids ? this.inIdOrder(written, ids) : written;
|
|
2337
|
+
};
|
|
2338
|
+
return spec.returnsRows
|
|
2339
|
+
? this.writeThenLoad(options, body)
|
|
2340
|
+
: this.executeSqlTransaction((tx) => body(tx, async () => { }), options?.transaction);
|
|
2341
|
+
}
|
|
2008
2342
|
async massCreate(data, options) {
|
|
2343
|
+
options = this.withScopeMemo(options);
|
|
2009
2344
|
if (options?.bulkMode === "perItem") {
|
|
2010
|
-
return this.
|
|
2345
|
+
return this.eachInSavepoint(data.map((item, index) => ({ index, item })), "Mass create errors", options, ({ item }, opts) => this.create(item, opts));
|
|
2011
2346
|
}
|
|
2012
|
-
|
|
2013
|
-
this.
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2347
|
+
const message = "Mass create errors";
|
|
2348
|
+
return this.massWrite({
|
|
2349
|
+
operation: "create",
|
|
2350
|
+
message,
|
|
2351
|
+
options,
|
|
2352
|
+
input: data,
|
|
2353
|
+
returnsRows: true,
|
|
2354
|
+
prepare: (opts) => data.map((item, index) => ({
|
|
2017
2355
|
index,
|
|
2018
2356
|
run: () => this.prepareCreate(item, opts),
|
|
2019
|
-
})),
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
await this.emitEvent("before", "create", true, { input: data, transaction: tx }, options);
|
|
2023
|
-
const created = await this.runSet(tx, "Mass create errors", (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
|
|
2357
|
+
})),
|
|
2358
|
+
keys: true,
|
|
2359
|
+
write: (tx, rows, opts) => this.runSet(tx, message, (sp) => this.insertMany(sp, rows, opts), rows.map((row, index) => ({
|
|
2024
2360
|
index,
|
|
2025
2361
|
run: (sp) => this.insertOne(sp, row, opts),
|
|
2026
|
-
})), "write")
|
|
2027
|
-
|
|
2362
|
+
})), "write"),
|
|
2363
|
+
after: async (created) => {
|
|
2028
2364
|
for (const entity of created)
|
|
2029
2365
|
await this.afterCreate(entity);
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
ids: this.idsOf(created),
|
|
2033
|
-
input: data,
|
|
2034
|
-
rows: created,
|
|
2035
|
-
transaction: tx,
|
|
2036
|
-
}, options);
|
|
2037
|
-
await this.loadWriteRelations(tx, created, options);
|
|
2038
|
-
return created;
|
|
2039
|
-
}, options?.transaction);
|
|
2366
|
+
},
|
|
2367
|
+
});
|
|
2040
2368
|
}
|
|
2041
2369
|
async insertMany(db, rows, options) {
|
|
2042
2370
|
if (this.config.sql?.useReturning) {
|
|
@@ -2047,9 +2375,28 @@ class SqlBaseCrudService {
|
|
|
2047
2375
|
await db.insert(this.config.table).values(rows);
|
|
2048
2376
|
return this.fetchByIds(db, rows.map((row) => row[pk]), options);
|
|
2049
2377
|
}
|
|
2050
|
-
const
|
|
2051
|
-
for (const row of rows)
|
|
2052
|
-
|
|
2378
|
+
const ids = [];
|
|
2379
|
+
for (const row of rows) {
|
|
2380
|
+
const result = await db.insert(this.config.table).values(row);
|
|
2381
|
+
ids.push(row[pk] ?? result?.[0]?.insertId);
|
|
2382
|
+
}
|
|
2383
|
+
return this.readBackCreated(db, ids, options);
|
|
2384
|
+
}
|
|
2385
|
+
async readBackCreated(db, ids, options) {
|
|
2386
|
+
const pk = this.config.primaryKey;
|
|
2387
|
+
const base = this.parseSelect(options?.select).base;
|
|
2388
|
+
const fields = this.buildSelectFields(base.length > 0 ? Array.from(new Set([...base, pk])) : base);
|
|
2389
|
+
const column = this.config.table[pk];
|
|
2390
|
+
const rows = await (fields ? db.select(fields) : db.select())
|
|
2391
|
+
.from(this.config.table)
|
|
2392
|
+
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(column, ids), ...this.rowScope(options)));
|
|
2393
|
+
const created = this.inIdOrder(rows, ids);
|
|
2394
|
+
if (created.length < ids.length)
|
|
2395
|
+
throw new Error("Failed to create entity");
|
|
2396
|
+
if (fields && !base.includes(pk)) {
|
|
2397
|
+
for (const row of created)
|
|
2398
|
+
delete row[pk];
|
|
2399
|
+
}
|
|
2053
2400
|
return created;
|
|
2054
2401
|
}
|
|
2055
2402
|
assertAllWritten(ids, written, message, options) {
|
|
@@ -2066,59 +2413,62 @@ class SqlBaseCrudService {
|
|
|
2066
2413
|
})));
|
|
2067
2414
|
}
|
|
2068
2415
|
async massUpdate(ids, data, options) {
|
|
2416
|
+
options = this.withScopeMemo(options);
|
|
2069
2417
|
if (options?.bulkMode === "perItem") {
|
|
2070
|
-
return this.
|
|
2418
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass update errors", options, ({ id }, opts) => this.update(id, data, opts));
|
|
2071
2419
|
}
|
|
2072
|
-
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
2073
|
-
this.checkReadOptions(options);
|
|
2074
2420
|
const message = "Mass update errors";
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2421
|
+
const skipBefore = options?.hooks?.skipBefore;
|
|
2422
|
+
return this.massWrite({
|
|
2423
|
+
operation: "update",
|
|
2424
|
+
message,
|
|
2425
|
+
options,
|
|
2426
|
+
ids,
|
|
2427
|
+
input: data,
|
|
2428
|
+
returnsRows: true,
|
|
2429
|
+
prepare: (opts, unique) => unique.map((id) => ({
|
|
2083
2430
|
id,
|
|
2084
2431
|
run: async () => {
|
|
2085
2432
|
await this.validateUpdate(id, data);
|
|
2086
|
-
const processed =
|
|
2433
|
+
const processed = skipBefore
|
|
2087
2434
|
? data
|
|
2088
2435
|
: await this.beforeUpdate(id, data);
|
|
2089
2436
|
return this.prepareUpdate(processed, opts);
|
|
2090
2437
|
},
|
|
2091
|
-
})),
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
? { ...set, [this.config.timestamps.updatedAt]: now }
|
|
2096
|
-
: set);
|
|
2097
|
-
let updated;
|
|
2098
|
-
const first = this.fingerprint(sets[0]);
|
|
2099
|
-
if (sets.every((set) => this.fingerprint(set) === first)) {
|
|
2100
|
-
const set = stamp(sets[0]);
|
|
2101
|
-
updated = await this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts), unique.map((id) => ({
|
|
2102
|
-
id,
|
|
2103
|
-
run: (sp) => this.updateWhereIn(sp, set, [id], opts),
|
|
2104
|
-
})), "write");
|
|
2105
|
-
}
|
|
2106
|
-
else {
|
|
2107
|
-
const rows = await this.eachItem(unique.map((id, i) => ({
|
|
2108
|
-
id,
|
|
2109
|
-
run: () => this.savepoint(tx, (sp) => this.updateWhereIn(sp, stamp(sets[i]), [id], opts)),
|
|
2110
|
-
})), message, "write");
|
|
2111
|
-
updated = rows.flat();
|
|
2112
|
-
}
|
|
2113
|
-
this.assertAllWritten(unique, updated, message, options);
|
|
2114
|
-
if (!hooks.skipAfter) {
|
|
2438
|
+
})),
|
|
2439
|
+
keys: true,
|
|
2440
|
+
write: (tx, sets, opts, unique) => this.updateSets(tx, sets, unique, opts, message),
|
|
2441
|
+
after: async (updated) => {
|
|
2115
2442
|
for (const entity of updated)
|
|
2116
2443
|
await this.afterUpdate(entity);
|
|
2444
|
+
},
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
async updateSets(tx, sets, unique, opts, message) {
|
|
2448
|
+
const now = new Date();
|
|
2449
|
+
const stamp = (set) => this.withVersionBump(this.config.timestamps?.updatedAt
|
|
2450
|
+
? { ...set, [this.config.timestamps.updatedAt]: now }
|
|
2451
|
+
: set);
|
|
2452
|
+
const groups = new Map();
|
|
2453
|
+
unique.forEach((id, i) => {
|
|
2454
|
+
const key = this.fingerprint(sets[i]);
|
|
2455
|
+
const group = groups.get(key);
|
|
2456
|
+
if (group)
|
|
2457
|
+
group.ids.push(id);
|
|
2458
|
+
else
|
|
2459
|
+
groups.set(key, { set: stamp(sets[i]), ids: [id] });
|
|
2460
|
+
});
|
|
2461
|
+
const written = await this.runSet(tx, message, async (sp) => {
|
|
2462
|
+
const rows = [];
|
|
2463
|
+
for (const group of groups.values()) {
|
|
2464
|
+
rows.push(...(await this.updateWhereIn(sp, group.set, group.ids, opts)));
|
|
2117
2465
|
}
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2466
|
+
return rows;
|
|
2467
|
+
}, unique.map((id, i) => ({
|
|
2468
|
+
id,
|
|
2469
|
+
run: (sp) => this.updateWhereIn(sp, stamp(sets[i]), [id], opts),
|
|
2470
|
+
})), "write");
|
|
2471
|
+
return groups.size > 1 ? this.inIdOrder(written, unique) : written;
|
|
2122
2472
|
}
|
|
2123
2473
|
async runKeyed(statement, ids) {
|
|
2124
2474
|
if (this.config.sql?.useReturning) {
|
|
@@ -2132,118 +2482,96 @@ class SqlBaseCrudService {
|
|
|
2132
2482
|
: [];
|
|
2133
2483
|
}
|
|
2134
2484
|
async massSoftDelete(ids, options) {
|
|
2485
|
+
options = this.withScopeMemo(options);
|
|
2135
2486
|
if (!this.config.softDelete?.enabled)
|
|
2136
2487
|
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2137
2488
|
if (options?.bulkMode === "perItem") {
|
|
2138
|
-
|
|
2489
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass soft delete errors", options, ({ id }, opts) => this.softDelete(id, opts));
|
|
2490
|
+
return true;
|
|
2139
2491
|
}
|
|
2140
|
-
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
2141
2492
|
const message = "Mass soft delete errors";
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
await this.eachItem(unique.map((id) => ({ id, run: () => this.beforeSoftDelete(id) })), message);
|
|
2156
|
-
}
|
|
2157
|
-
await this.emitEvent("before", "softDelete", true, { ids: unique, transaction: tx }, options);
|
|
2158
|
-
const set = this.softDeleteSet(new Date());
|
|
2159
|
-
const written = await this.runSet(tx, message, (sp) => this.runKeyed(sp
|
|
2160
|
-
.update(this.config.table)
|
|
2161
|
-
.set(set)
|
|
2162
|
-
.where(this.writeWhere(unique, opts)), unique), unique.map((id) => ({
|
|
2163
|
-
id,
|
|
2164
|
-
run: (sp) => sp
|
|
2493
|
+
await this.massWrite({
|
|
2494
|
+
operation: "softDelete",
|
|
2495
|
+
message,
|
|
2496
|
+
options,
|
|
2497
|
+
ids,
|
|
2498
|
+
state: { withDeleted: false, onlyDeleted: false },
|
|
2499
|
+
returnsRows: false,
|
|
2500
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2501
|
+
? undefined
|
|
2502
|
+
: unique.map((id) => ({ id, run: () => this.beforeSoftDelete(id) })),
|
|
2503
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2504
|
+
const set = this.softDeleteSet(new Date());
|
|
2505
|
+
const update = (db, keys) => db
|
|
2165
2506
|
.update(this.config.table)
|
|
2166
2507
|
.set(set)
|
|
2167
|
-
.where(this.writeWhere(
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2508
|
+
.where(this.writeWhere(keys, opts));
|
|
2509
|
+
return this.runSet(tx, message, (sp) => this.runKeyed(update(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => update(sp, [id]) })), "write");
|
|
2510
|
+
},
|
|
2511
|
+
after: async (_written, unique) => {
|
|
2171
2512
|
for (const id of unique)
|
|
2172
2513
|
await this.afterSoftDelete(id);
|
|
2173
|
-
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
}, options?.transaction);
|
|
2514
|
+
},
|
|
2515
|
+
});
|
|
2516
|
+
return true;
|
|
2177
2517
|
}
|
|
2178
2518
|
async massRestore(ids, options) {
|
|
2519
|
+
options = this.withScopeMemo(options);
|
|
2179
2520
|
if (!this.config.softDelete?.enabled)
|
|
2180
2521
|
throw new common_1.BadRequestException("Soft delete not enabled");
|
|
2181
2522
|
if (options?.bulkMode === "perItem") {
|
|
2182
|
-
return this.
|
|
2523
|
+
return this.eachInSavepoint(ids.map((id) => ({ id })), "Mass restore errors", options, ({ id }, opts) => this.restore(id, opts));
|
|
2183
2524
|
}
|
|
2184
|
-
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
2185
|
-
this.checkReadOptions(options);
|
|
2186
2525
|
const message = "Mass restore errors";
|
|
2187
|
-
return this.
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
id,
|
|
2206
|
-
run: (sp) => this.updateWhereIn(sp, set, [id], opts),
|
|
2207
|
-
})), "write");
|
|
2208
|
-
this.assertAllWritten(unique, restored, message, options);
|
|
2209
|
-
if (!hooks.skipAfter) {
|
|
2526
|
+
return this.massWrite({
|
|
2527
|
+
operation: "restore",
|
|
2528
|
+
message,
|
|
2529
|
+
options,
|
|
2530
|
+
ids,
|
|
2531
|
+
state: { withDeleted: true, onlyDeleted: false },
|
|
2532
|
+
returnsRows: true,
|
|
2533
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2534
|
+
? undefined
|
|
2535
|
+
: unique.map((id) => ({ id, run: () => this.beforeRestore(id) })),
|
|
2536
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2537
|
+
const set = this.softDeleteSet(null);
|
|
2538
|
+
return this.runSet(tx, message, (sp) => this.updateWhereIn(sp, set, unique, opts), unique.map((id) => ({
|
|
2539
|
+
id,
|
|
2540
|
+
run: (sp) => this.updateWhereIn(sp, set, [id], opts),
|
|
2541
|
+
})), "write");
|
|
2542
|
+
},
|
|
2543
|
+
after: async (restored) => {
|
|
2210
2544
|
for (const entity of restored)
|
|
2211
2545
|
await this.afterRestore(entity);
|
|
2212
|
-
}
|
|
2213
|
-
|
|
2214
|
-
await this.loadWriteRelations(tx, restored, options);
|
|
2215
|
-
return this.inIdOrder(restored, ids);
|
|
2216
|
-
}, options?.transaction);
|
|
2546
|
+
},
|
|
2547
|
+
});
|
|
2217
2548
|
}
|
|
2218
2549
|
async massDelete(ids, options) {
|
|
2550
|
+
options = this.withScopeMemo(options);
|
|
2219
2551
|
if (options?.bulkMode === "perItem") {
|
|
2220
|
-
|
|
2552
|
+
await this.eachInSavepoint(ids.map((id) => ({ id })), "Mass delete errors", options, ({ id }, opts) => this.delete(id, opts), "delete");
|
|
2553
|
+
return true;
|
|
2221
2554
|
}
|
|
2222
|
-
this.assertBatchSize(Array.isArray(ids) ? ids.length : 0);
|
|
2223
2555
|
const message = "Mass delete errors";
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
})), "delete");
|
|
2239
|
-
this.assertAllWritten(unique, written, message, options);
|
|
2240
|
-
if (!hooks.skipAfter) {
|
|
2556
|
+
await this.massWrite({
|
|
2557
|
+
operation: "delete",
|
|
2558
|
+
message,
|
|
2559
|
+
options,
|
|
2560
|
+
ids,
|
|
2561
|
+
returnsRows: false,
|
|
2562
|
+
prepare: (_opts, unique) => options?.hooks?.skipBefore
|
|
2563
|
+
? undefined
|
|
2564
|
+
: unique.map((id) => ({ id, run: () => this.beforeDelete(id) })),
|
|
2565
|
+
write: (tx, _prepared, opts, unique) => {
|
|
2566
|
+
const remove = (db, keys) => db.delete(this.config.table).where(this.writeWhere(keys, opts));
|
|
2567
|
+
return this.runSet(tx, message, (sp) => this.runKeyed(remove(sp, unique), unique), unique.map((id) => ({ id, run: (sp) => remove(sp, [id]) })), "delete");
|
|
2568
|
+
},
|
|
2569
|
+
after: async (_written, unique) => {
|
|
2241
2570
|
for (const id of unique)
|
|
2242
2571
|
await this.afterDelete(id);
|
|
2243
|
-
}
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
}, options?.transaction);
|
|
2572
|
+
},
|
|
2573
|
+
});
|
|
2574
|
+
return true;
|
|
2247
2575
|
}
|
|
2248
2576
|
async massForceDelete(ids, options) {
|
|
2249
2577
|
return this.massDelete(ids, {
|
|
@@ -2328,10 +2656,16 @@ class SqlBaseCrudService {
|
|
|
2328
2656
|
return item.index !== undefined ? { index: item.index } : { id: item.id };
|
|
2329
2657
|
}
|
|
2330
2658
|
async runSet(tx, message, statement, items, operation) {
|
|
2659
|
+
const owned = this.ownedTransactions.has(tx);
|
|
2331
2660
|
try {
|
|
2332
|
-
return await this.savepoint(tx, statement);
|
|
2661
|
+
return await (owned ? statement(tx) : this.savepoint(tx, statement));
|
|
2333
2662
|
}
|
|
2334
2663
|
catch (error) {
|
|
2664
|
+
if (error instanceof BindLimitException)
|
|
2665
|
+
throw error;
|
|
2666
|
+
if (owned) {
|
|
2667
|
+
throw new DeferredDiagnosis(() => this.settleSet(message, error, items, operation));
|
|
2668
|
+
}
|
|
2335
2669
|
const failures = await this.diagnose(tx, items, operation);
|
|
2336
2670
|
if (failures.length > 0) {
|
|
2337
2671
|
throw new crud_exceptions_1.BulkOperationException(message, failures);
|
|
@@ -2339,6 +2673,23 @@ class SqlBaseCrudService {
|
|
|
2339
2673
|
throw this.toClientError(error, operation);
|
|
2340
2674
|
}
|
|
2341
2675
|
}
|
|
2676
|
+
async settleSet(message, error, items, operation) {
|
|
2677
|
+
let failures = [];
|
|
2678
|
+
try {
|
|
2679
|
+
await this.config.db.transaction(async (fresh) => {
|
|
2680
|
+
failures = await this.diagnose(this.withBindLimit(fresh), items, operation);
|
|
2681
|
+
throw ROLLBACK;
|
|
2682
|
+
});
|
|
2683
|
+
}
|
|
2684
|
+
catch (thrown) {
|
|
2685
|
+
if (thrown !== ROLLBACK)
|
|
2686
|
+
throw thrown;
|
|
2687
|
+
}
|
|
2688
|
+
if (failures.length > 0) {
|
|
2689
|
+
throw new crud_exceptions_1.BulkOperationException(message, failures);
|
|
2690
|
+
}
|
|
2691
|
+
throw this.toClientError(error, operation);
|
|
2692
|
+
}
|
|
2342
2693
|
async diagnose(tx, items, operation) {
|
|
2343
2694
|
const failures = [];
|
|
2344
2695
|
try {
|
|
@@ -2363,60 +2714,40 @@ class SqlBaseCrudService {
|
|
|
2363
2714
|
}
|
|
2364
2715
|
return failures;
|
|
2365
2716
|
}
|
|
2366
|
-
|
|
2367
|
-
return this.executeSqlTransaction((tx) => this.eachItem(
|
|
2368
|
-
|
|
2369
|
-
run: () => this.savepoint(tx, (sp) =>
|
|
2370
|
-
})),
|
|
2371
|
-
}
|
|
2372
|
-
async massUpdatePerItem(ids, data, options) {
|
|
2373
|
-
return this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2374
|
-
id,
|
|
2375
|
-
run: () => this.savepoint(tx, (sp) => this.update(id, data, { ...options, transaction: sp })),
|
|
2376
|
-
})), "Mass update errors"), options?.transaction);
|
|
2377
|
-
}
|
|
2378
|
-
async massSoftDeletePerItem(ids, options) {
|
|
2379
|
-
await this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2380
|
-
id,
|
|
2381
|
-
run: () => this.savepoint(tx, (sp) => this.softDelete(id, { ...options, transaction: sp })),
|
|
2382
|
-
})), "Mass soft delete errors"), options?.transaction);
|
|
2383
|
-
return true;
|
|
2384
|
-
}
|
|
2385
|
-
async massRestorePerItem(ids, options) {
|
|
2386
|
-
return this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2387
|
-
id,
|
|
2388
|
-
run: () => this.savepoint(tx, (sp) => this.restore(id, { ...options, transaction: sp })),
|
|
2389
|
-
})), "Mass restore errors"), options?.transaction);
|
|
2390
|
-
}
|
|
2391
|
-
async massDeletePerItem(ids, options) {
|
|
2392
|
-
await this.executeSqlTransaction((tx) => this.eachItem(ids.map((id) => ({
|
|
2393
|
-
id,
|
|
2394
|
-
run: () => this.savepoint(tx, (sp) => this.delete(id, { ...options, transaction: sp })),
|
|
2395
|
-
})), "Mass delete errors", "delete"), options?.transaction);
|
|
2396
|
-
return true;
|
|
2717
|
+
eachInSavepoint(entries, message, options, call, operation = "write") {
|
|
2718
|
+
return this.executeSqlTransaction((tx) => this.eachItem(entries.map((entry) => ({
|
|
2719
|
+
...entry,
|
|
2720
|
+
run: () => this.savepoint(tx, (sp) => call(entry, { ...options, transaction: sp })),
|
|
2721
|
+
})), message, operation), options?.transaction);
|
|
2397
2722
|
}
|
|
2398
2723
|
savepoint(tx, run) {
|
|
2399
2724
|
return typeof tx?.transaction === "function"
|
|
2400
|
-
? tx.transaction(run)
|
|
2725
|
+
? tx.transaction((sp) => run(this.withBindLimit(sp)))
|
|
2401
2726
|
: run(tx);
|
|
2402
2727
|
}
|
|
2403
2728
|
async exists(id, options) {
|
|
2729
|
+
options = this.withScopeMemo(options);
|
|
2730
|
+
this.checkReadOptions({ ...options, select: undefined });
|
|
2404
2731
|
const entity = await this.find(id, {
|
|
2405
2732
|
...options,
|
|
2733
|
+
relations: [],
|
|
2734
|
+
counts: undefined,
|
|
2406
2735
|
select: [this.config.primaryKey],
|
|
2407
2736
|
});
|
|
2408
2737
|
return !!entity;
|
|
2409
2738
|
}
|
|
2410
2739
|
async count(filters, options) {
|
|
2740
|
+
options = this.withScopeMemo(options);
|
|
2411
2741
|
this.checkReadOptions(options);
|
|
2412
|
-
const db = options?.transaction
|
|
2742
|
+
const db = this.dbFor(options?.transaction);
|
|
2413
2743
|
const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
|
|
2414
2744
|
const result = await this.runRead(this.buildCountQuery(db, conditions, filterRelations, options));
|
|
2415
|
-
return
|
|
2745
|
+
return this.parseCount(result);
|
|
2416
2746
|
}
|
|
2417
2747
|
async countBy(column, filters, options) {
|
|
2748
|
+
options = this.withScopeMemo(options);
|
|
2418
2749
|
this.checkReadOptions(options);
|
|
2419
|
-
const db = options?.transaction
|
|
2750
|
+
const db = this.dbFor(options?.transaction);
|
|
2420
2751
|
const spec = this.resolveGroupColumn(column, options);
|
|
2421
2752
|
const { conditions, relations: filterRelations } = this.listConditions(filters, db, options);
|
|
2422
2753
|
const joins = Array.from(new Set([...filterRelations, ...(spec.relation ? [spec.relation] : [])]));
|
|
@@ -2447,10 +2778,7 @@ class SqlBaseCrudService {
|
|
|
2447
2778
|
}
|
|
2448
2779
|
else {
|
|
2449
2780
|
const name = key.slice(0, dot);
|
|
2450
|
-
const
|
|
2451
|
-
const relation = Object.hasOwn(relations, name)
|
|
2452
|
-
? relations[name]
|
|
2453
|
-
: undefined;
|
|
2781
|
+
const relation = this.relationNamed(name);
|
|
2454
2782
|
const column = relation && !this.isCollection(relation) && relation.localKey
|
|
2455
2783
|
? this.columnOf(relation.table, key.slice(dot + 1))
|
|
2456
2784
|
: undefined;
|
|
@@ -2483,25 +2811,9 @@ class SqlBaseCrudService {
|
|
|
2483
2811
|
return keys.length > 0 ? Array.from(new Set([...base, ...keys])) : base;
|
|
2484
2812
|
}
|
|
2485
2813
|
async loadCounts(db, rows, counts, options) {
|
|
2486
|
-
|
|
2487
|
-
if (names.length === 0 || !Array.isArray(rows))
|
|
2814
|
+
if (this.countNames(counts).length === 0 || !Array.isArray(rows))
|
|
2488
2815
|
return;
|
|
2489
|
-
const
|
|
2490
|
-
const plan = names.map((name) => {
|
|
2491
|
-
const relation = Object.hasOwn(relations, name)
|
|
2492
|
-
? relations[name]
|
|
2493
|
-
: undefined;
|
|
2494
|
-
if (!relation) {
|
|
2495
|
-
throw new common_1.BadRequestException(`Unknown relation to count: ${name}`);
|
|
2496
|
-
}
|
|
2497
|
-
if (!this.isCollection(relation)) {
|
|
2498
|
-
throw new common_1.BadRequestException(`Cannot count "${name}": only a has-many or many-to-many relation has a count`);
|
|
2499
|
-
}
|
|
2500
|
-
const filter = !Array.isArray(counts) && (0, merge_config_1.isPlainObject)(counts?.[name])
|
|
2501
|
-
? counts?.[name]
|
|
2502
|
-
: undefined;
|
|
2503
|
-
return { name, relation, filter };
|
|
2504
|
-
});
|
|
2816
|
+
const plan = this.countPlan(counts);
|
|
2505
2817
|
const hosts = rows.filter((row) => row && typeof row === "object");
|
|
2506
2818
|
for (const host of hosts)
|
|
2507
2819
|
host._count = {};
|
|
@@ -2512,7 +2824,7 @@ class SqlBaseCrudService {
|
|
|
2512
2824
|
const ids = Array.from(new Set(hosts.map((host) => host[parentKey]).filter((v) => v != null)));
|
|
2513
2825
|
const totals = new Map();
|
|
2514
2826
|
if (ids.length > 0) {
|
|
2515
|
-
const result = await this.runRead(this.countQuery(db, relation,
|
|
2827
|
+
const result = await this.inChunks(db, ids, (chunk) => this.runRead(this.countQuery(db, relation, chunk, name, filter, options)));
|
|
2516
2828
|
for (const row of result) {
|
|
2517
2829
|
totals.set(String(row[COUNT_KEY]), Number(row.count));
|
|
2518
2830
|
}
|
|
@@ -2522,6 +2834,23 @@ class SqlBaseCrudService {
|
|
|
2522
2834
|
}
|
|
2523
2835
|
}));
|
|
2524
2836
|
}
|
|
2837
|
+
countPlan(counts) {
|
|
2838
|
+
return this.countNames(counts).map((name) => {
|
|
2839
|
+
const relation = this.relationNamed(name);
|
|
2840
|
+
if (!relation) {
|
|
2841
|
+
throw new common_1.BadRequestException(`Unknown relation to count: ${name}`);
|
|
2842
|
+
}
|
|
2843
|
+
if (!this.isCollection(relation)) {
|
|
2844
|
+
throw new common_1.BadRequestException(`Cannot count "${name}": only a has-many or many-to-many relation has a count`);
|
|
2845
|
+
}
|
|
2846
|
+
const filter = !Array.isArray(counts) && (0, merge_config_1.isPlainObject)(counts?.[name])
|
|
2847
|
+
? counts?.[name]
|
|
2848
|
+
: undefined;
|
|
2849
|
+
if (filter)
|
|
2850
|
+
(0, query_parser_1.assertFilterDepth)(filter);
|
|
2851
|
+
return { name, relation, filter };
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2525
2854
|
countQuery(db, relation, ids, name, filter, options) {
|
|
2526
2855
|
const target = relation.table;
|
|
2527
2856
|
const conditions = [];
|
|
@@ -2538,12 +2867,7 @@ class SqlBaseCrudService {
|
|
|
2538
2867
|
const total = (0, drizzle_orm_1.sql) `count(*)`;
|
|
2539
2868
|
if (this.relationType(relation) === "manyToMany") {
|
|
2540
2869
|
const joinLocal = relation.through[relation.throughLocalKey];
|
|
2541
|
-
|
|
2542
|
-
const referenced = target[relation.references ?? "id"];
|
|
2543
|
-
return db
|
|
2544
|
-
.select({ [COUNT_KEY]: joinLocal, count: total })
|
|
2545
|
-
.from(relation.through)
|
|
2546
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(joinForeign, referenced))
|
|
2870
|
+
return this.selectThrough(db, { [COUNT_KEY]: joinLocal, count: total }, relation)
|
|
2547
2871
|
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.inArray)(joinLocal, ids), ...conditions))
|
|
2548
2872
|
.groupBy(joinLocal);
|
|
2549
2873
|
}
|
|
@@ -2556,7 +2880,6 @@ class SqlBaseCrudService {
|
|
|
2556
2880
|
}
|
|
2557
2881
|
pushColumnCondition(conditions, column, value, caseInsensitive = this.config.sql?.caseSensitive === false) {
|
|
2558
2882
|
if (Array.isArray(value)) {
|
|
2559
|
-
this.assertBatchSize(value.length);
|
|
2560
2883
|
conditions.push((0, drizzle_orm_1.inArray)(column, value.map((item) => this.coerceFilterValue(column, item))));
|
|
2561
2884
|
}
|
|
2562
2885
|
else if ((0, merge_config_1.isPlainObject)(value)) {
|
|
@@ -2597,11 +2920,7 @@ class SqlBaseCrudService {
|
|
|
2597
2920
|
const joinForeign = relation.through?.[relation.throughForeignKey ?? ""];
|
|
2598
2921
|
if (!referenced || !joinForeign)
|
|
2599
2922
|
return undefined;
|
|
2600
|
-
subquery = db
|
|
2601
|
-
.select({ value: key })
|
|
2602
|
-
.from(relation.through)
|
|
2603
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(joinForeign, referenced))
|
|
2604
|
-
.where((0, drizzle_orm_1.and)(...conditions));
|
|
2923
|
+
subquery = this.selectThrough(db, { value: key }, relation).where((0, drizzle_orm_1.and)(...conditions));
|
|
2605
2924
|
}
|
|
2606
2925
|
else {
|
|
2607
2926
|
subquery = db
|
|
@@ -2629,11 +2948,7 @@ class SqlBaseCrudService {
|
|
|
2629
2948
|
let subquery;
|
|
2630
2949
|
if (this.relationType(relation) === "manyToMany") {
|
|
2631
2950
|
const through = subqueryAlias(relation.through);
|
|
2632
|
-
subquery = db
|
|
2633
|
-
.select({ one: (0, drizzle_orm_1.sql) `1` })
|
|
2634
|
-
.from(through)
|
|
2635
|
-
.innerJoin(target, (0, drizzle_orm_1.eq)(through[relation.throughForeignKey], target[relation.references ?? "id"]))
|
|
2636
|
-
.where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(through[relation.throughLocalKey], host.table[parentKey]), ...conditions));
|
|
2951
|
+
subquery = this.selectThrough(db, { one: (0, drizzle_orm_1.sql) `1` }, relation, target, through).where((0, drizzle_orm_1.and)((0, drizzle_orm_1.eq)(through[relation.throughLocalKey], host.table[parentKey]), ...conditions));
|
|
2637
2952
|
}
|
|
2638
2953
|
else {
|
|
2639
2954
|
subquery = db
|
|
@@ -2805,7 +3120,6 @@ class SqlBaseCrudService {
|
|
|
2805
3120
|
if (!Array.isArray(raw)) {
|
|
2806
3121
|
throw new common_1.BadRequestException(`Filter operator "${op}" expects an array`);
|
|
2807
3122
|
}
|
|
2808
|
-
this.assertBatchSize(raw.length);
|
|
2809
3123
|
return raw;
|
|
2810
3124
|
}
|
|
2811
3125
|
static MAX_BATCH_PARAMETERS = 60_000;
|
|
@@ -2843,9 +3157,19 @@ class SqlBaseCrudService {
|
|
|
2843
3157
|
: kind === "endsWith"
|
|
2844
3158
|
? `%${escaped}`
|
|
2845
3159
|
: `%${escaped}%`;
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
3160
|
+
if (insensitive || this.config.sql?.caseSensitive === false) {
|
|
3161
|
+
return this.caseInsensitiveLike(column, pattern);
|
|
3162
|
+
}
|
|
3163
|
+
if (this.config.dialect === "sqlite") {
|
|
3164
|
+
const literal = raw.replace(/[*?[]/g, "[$&]");
|
|
3165
|
+
const glob = kind === "startsWith"
|
|
3166
|
+
? `${literal}*`
|
|
3167
|
+
: kind === "endsWith"
|
|
3168
|
+
? `*${literal}`
|
|
3169
|
+
: `*${literal}*`;
|
|
3170
|
+
return (0, drizzle_orm_1.sql) `${column} glob ${glob}`;
|
|
3171
|
+
}
|
|
3172
|
+
return this.likePattern(column, pattern);
|
|
2849
3173
|
}
|
|
2850
3174
|
applyComplexFilter(conditions, column, filterObj) {
|
|
2851
3175
|
const coerce = (value) => this.coerceFilterValue(column, value);
|
|
@@ -2936,15 +3260,28 @@ class SqlBaseCrudService {
|
|
|
2936
3260
|
}
|
|
2937
3261
|
async executeSqlTransaction(operation, existingTransaction) {
|
|
2938
3262
|
this.assertTransactions(existingTransaction ?? this.config.db);
|
|
3263
|
+
const run = (tx) => operation(this.withBindLimit(tx));
|
|
2939
3264
|
if (existingTransaction)
|
|
2940
|
-
return
|
|
2941
|
-
|
|
3265
|
+
return run(existingTransaction);
|
|
3266
|
+
try {
|
|
3267
|
+
return await this.config.db.transaction((tx) => {
|
|
3268
|
+
const view = this.withBindLimit(tx);
|
|
3269
|
+
if (view && typeof view === "object")
|
|
3270
|
+
this.ownedTransactions.add(view);
|
|
3271
|
+
return operation(view);
|
|
3272
|
+
});
|
|
3273
|
+
}
|
|
3274
|
+
catch (error) {
|
|
3275
|
+
if (error instanceof DeferredDiagnosis)
|
|
3276
|
+
return error.settle();
|
|
3277
|
+
throw error;
|
|
3278
|
+
}
|
|
2942
3279
|
}
|
|
3280
|
+
ownedTransactions = new WeakSet();
|
|
2943
3281
|
assertTransactions(db) {
|
|
2944
3282
|
if (this.config.dialect !== "sqlite")
|
|
2945
3283
|
return;
|
|
2946
|
-
|
|
2947
|
-
if (kind === "D1Database" || kind === "D1Transaction") {
|
|
3284
|
+
if (this.isD1(db)) {
|
|
2948
3285
|
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.");
|
|
2949
3286
|
}
|
|
2950
3287
|
if (db?.resultKind === "sync") {
|
|
@@ -2960,6 +3297,7 @@ class SqlBaseCrudService {
|
|
|
2960
3297
|
}
|
|
2961
3298
|
}
|
|
2962
3299
|
async fullTextSearch(searchTerm, searchColumns, pagination, options) {
|
|
3300
|
+
options = this.withScopeMemo(options);
|
|
2963
3301
|
if (this.config.dialect !== "postgresql") {
|
|
2964
3302
|
throw new common_1.BadRequestException("Full-text search only supported in PostgreSQL");
|
|
2965
3303
|
}
|
|
@@ -2998,12 +3336,9 @@ class SqlBaseCrudService {
|
|
|
2998
3336
|
.offset(offset);
|
|
2999
3337
|
const countQuery = this.buildCountQuery(plan.db, conditions, plan.filterRelations, readOptions);
|
|
3000
3338
|
const [fetched, totalResult] = await this.runRead(Promise.all([dataQuery, countQuery]));
|
|
3001
|
-
const total =
|
|
3339
|
+
const total = this.parseCount(totalResult);
|
|
3002
3340
|
const rows = this.normalizeRelations(fetched, plan.joined.map((node) => node.name));
|
|
3003
|
-
await
|
|
3004
|
-
this.loadRelations(plan.db, rows, plan.nodes, plan.selection, undefined, readOptions),
|
|
3005
|
-
this.loadCounts(plan.db, rows, readOptions.counts, readOptions),
|
|
3006
|
-
]);
|
|
3341
|
+
await this.hydrate(plan.db, rows, plan.nodes, plan.selection, readOptions);
|
|
3007
3342
|
return { data: rows, total, page, limit };
|
|
3008
3343
|
}
|
|
3009
3344
|
}
|