@getstrata/core 1.0.5 → 1.0.7
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/CHANGELOG.md +6 -0
- package/dist/core/database/baseRepository.d.ts +9 -0
- package/dist/core/database/dialect.d.ts +2 -0
- package/dist/core/database/index.d.ts +1 -0
- package/dist/core/database/model.d.ts +23 -0
- package/dist/core/database/pluck.d.ts +5 -0
- package/dist/core/database/query.d.ts +9 -1
- package/dist/core/database/relationQuery.d.ts +30 -0
- package/dist/core/database/repositoryQuery.d.ts +6 -0
- package/dist/core/database/types.d.ts +7 -0
- package/dist/entries/database/model.js +330 -30
- package/dist/entries/database/query.js +82 -3
- package/dist/entries/database/repositoryQuery.js +145 -4
- package/dist/entries/database/schema.js +80 -3
- package/dist/entries/http/parseMultipartUpload.js +21 -1
- package/dist/framework/public-api.d.ts +1 -1
- package/dist/index.js +463 -31
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/database/model.ts
|
|
3
|
-
import { NotFoundError } from "@getstrata/core/errors/http";
|
|
3
|
+
import { ConflictError, NotFoundError } from "@getstrata/core/errors/http";
|
|
4
4
|
|
|
5
5
|
// ../../src/core/database/inflection.ts
|
|
6
6
|
function singularize(word) {
|
|
@@ -60,15 +60,32 @@ function pushParam(values, value) {
|
|
|
60
60
|
values.push(value);
|
|
61
61
|
return currentSqlDialect().placeholder(values.length);
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
var SUPPORTED_OPERATORS = new Set([
|
|
64
|
+
"eq",
|
|
65
|
+
"ne",
|
|
66
|
+
"in",
|
|
67
|
+
"notIn",
|
|
68
|
+
"gt",
|
|
69
|
+
"gte",
|
|
70
|
+
"lt",
|
|
71
|
+
"lte",
|
|
72
|
+
"isNull",
|
|
73
|
+
"ilike",
|
|
74
|
+
"tsMatch"
|
|
75
|
+
]);
|
|
76
|
+
function buildInClause(column, values, params, negated = false) {
|
|
64
77
|
if (values.length === 0) {
|
|
65
|
-
return "1 = 0";
|
|
78
|
+
return negated ? "1 = 1" : "1 = 0";
|
|
66
79
|
}
|
|
67
80
|
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
68
|
-
return `${column} IN (${placeholders})`;
|
|
81
|
+
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
69
82
|
}
|
|
70
83
|
function buildOperatorClauses(column, operator, params) {
|
|
71
84
|
const clauses = [];
|
|
85
|
+
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
86
|
+
if (unsupported.length > 0) {
|
|
87
|
+
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
88
|
+
}
|
|
72
89
|
if (operator.isNull === true) {
|
|
73
90
|
clauses.push(`${column} IS NULL`);
|
|
74
91
|
}
|
|
@@ -82,9 +99,19 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
82
99
|
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
83
100
|
}
|
|
84
101
|
}
|
|
102
|
+
if (operator.ne !== undefined) {
|
|
103
|
+
if (operator.ne === null) {
|
|
104
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
105
|
+
} else {
|
|
106
|
+
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
85
109
|
if (operator.in !== undefined) {
|
|
86
110
|
clauses.push(buildInClause(column, operator.in, params));
|
|
87
111
|
}
|
|
112
|
+
if (operator.notIn !== undefined) {
|
|
113
|
+
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
114
|
+
}
|
|
88
115
|
if (operator.gt !== undefined) {
|
|
89
116
|
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
90
117
|
}
|
|
@@ -304,6 +331,10 @@ function buildSelectList(table, select, params = []) {
|
|
|
304
331
|
if (item.kind === "literalText") {
|
|
305
332
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
306
333
|
}
|
|
334
|
+
if (item.kind === "subqueryCount") {
|
|
335
|
+
const body = remapExistsSql(item.sql, item.params, params);
|
|
336
|
+
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
337
|
+
}
|
|
307
338
|
const column = qualifyColumn(item.table, item.column);
|
|
308
339
|
const placeholder = pushParam(params, item.query);
|
|
309
340
|
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
@@ -398,6 +429,52 @@ function buildInsertQuery(table, values) {
|
|
|
398
429
|
params
|
|
399
430
|
};
|
|
400
431
|
}
|
|
432
|
+
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
433
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
434
|
+
if (entries.length === 0) {
|
|
435
|
+
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
436
|
+
}
|
|
437
|
+
if (conflictColumns.length === 0) {
|
|
438
|
+
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
439
|
+
}
|
|
440
|
+
const insertable = new Set(entries.map(([column]) => column));
|
|
441
|
+
const conflict = new Set(conflictColumns);
|
|
442
|
+
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
443
|
+
const params = [];
|
|
444
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
445
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
446
|
+
const returningColumns = buildReturningColumns(table);
|
|
447
|
+
const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
|
|
448
|
+
return {
|
|
449
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
450
|
+
params
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
454
|
+
if (!Number.isFinite(amount)) {
|
|
455
|
+
throw new Error("Increment amount must be a finite number.");
|
|
456
|
+
}
|
|
457
|
+
if (!table.columns.includes(column)) {
|
|
458
|
+
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
459
|
+
}
|
|
460
|
+
const params = [];
|
|
461
|
+
const target = quoteIdentifier(column);
|
|
462
|
+
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
463
|
+
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
464
|
+
exclude: [table.primaryKey, column]
|
|
465
|
+
})) {
|
|
466
|
+
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
467
|
+
}
|
|
468
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
469
|
+
const returningColumns = buildReturningColumns(table);
|
|
470
|
+
const scopeClauses = [];
|
|
471
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
472
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
473
|
+
return {
|
|
474
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
475
|
+
params
|
|
476
|
+
};
|
|
477
|
+
}
|
|
401
478
|
function buildUpdateQuery(table, id, changes) {
|
|
402
479
|
const entries = getDefinedColumnEntries(table, changes, {
|
|
403
480
|
exclude: [table.primaryKey]
|
|
@@ -458,6 +535,30 @@ function buildDeleteByIdQuery(table, id) {
|
|
|
458
535
|
};
|
|
459
536
|
}
|
|
460
537
|
|
|
538
|
+
// ../../src/core/database/pluck.ts
|
|
539
|
+
function uniqueColumnSelect(table, columns) {
|
|
540
|
+
const seen = new Set;
|
|
541
|
+
const select = [];
|
|
542
|
+
for (const column of columns) {
|
|
543
|
+
if (seen.has(column)) {
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
seen.add(column);
|
|
547
|
+
select.push({ kind: "column", table, column, as: column });
|
|
548
|
+
}
|
|
549
|
+
return select;
|
|
550
|
+
}
|
|
551
|
+
function projectPluck(rows, column, keyBy) {
|
|
552
|
+
if (keyBy === undefined) {
|
|
553
|
+
return rows.map((row) => row[column]);
|
|
554
|
+
}
|
|
555
|
+
const keyed = new Map;
|
|
556
|
+
for (const row of rows) {
|
|
557
|
+
keyed.set(row[keyBy], row[column]);
|
|
558
|
+
}
|
|
559
|
+
return keyed;
|
|
560
|
+
}
|
|
561
|
+
|
|
461
562
|
// ../../src/core/database/relationQuery.ts
|
|
462
563
|
function asWhere(where) {
|
|
463
564
|
return where;
|
|
@@ -474,6 +575,12 @@ function ownerId(owner, ownerKey) {
|
|
|
474
575
|
function thenGet(get, onfulfilled, onrejected) {
|
|
475
576
|
return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
476
577
|
}
|
|
578
|
+
function pluckFromQuery(query, column, keyBy) {
|
|
579
|
+
if (!query) {
|
|
580
|
+
return Promise.resolve(keyBy === undefined ? [] : new Map);
|
|
581
|
+
}
|
|
582
|
+
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
583
|
+
}
|
|
477
584
|
|
|
478
585
|
class HasManyRelationQuery {
|
|
479
586
|
parent;
|
|
@@ -539,6 +646,12 @@ class HasManyRelationQuery {
|
|
|
539
646
|
async count() {
|
|
540
647
|
return this.scopedQuery().count();
|
|
541
648
|
}
|
|
649
|
+
async pluck(column, keyBy) {
|
|
650
|
+
return pluckFromQuery(this.scopedQuery(), column, keyBy);
|
|
651
|
+
}
|
|
652
|
+
async value(column) {
|
|
653
|
+
return this.scopedQuery().value(column);
|
|
654
|
+
}
|
|
542
655
|
then(onfulfilled, onrejected) {
|
|
543
656
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
544
657
|
}
|
|
@@ -604,6 +717,13 @@ class HasOneRelationQuery {
|
|
|
604
717
|
async count() {
|
|
605
718
|
return this.inner.count();
|
|
606
719
|
}
|
|
720
|
+
async pluck(column, keyBy) {
|
|
721
|
+
const query = this.inner.limit(1);
|
|
722
|
+
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
723
|
+
}
|
|
724
|
+
async value(column) {
|
|
725
|
+
return this.inner.limit(1).value(column);
|
|
726
|
+
}
|
|
607
727
|
then(onfulfilled, onrejected) {
|
|
608
728
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
609
729
|
}
|
|
@@ -650,6 +770,23 @@ class BelongsToRelationQuery {
|
|
|
650
770
|
return { sql, params: extra.params };
|
|
651
771
|
}
|
|
652
772
|
async get() {
|
|
773
|
+
const query = this.relatedQuery();
|
|
774
|
+
if (!query) {
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
const row = await query.first();
|
|
778
|
+
return row ? this.related.newFromRecord(row) : null;
|
|
779
|
+
}
|
|
780
|
+
async first() {
|
|
781
|
+
return this.get();
|
|
782
|
+
}
|
|
783
|
+
async pluck(column, keyBy) {
|
|
784
|
+
return pluckFromQuery(this.relatedQuery(), column, keyBy);
|
|
785
|
+
}
|
|
786
|
+
async value(column) {
|
|
787
|
+
return this.relatedQuery()?.value(column) ?? null;
|
|
788
|
+
}
|
|
789
|
+
relatedQuery() {
|
|
653
790
|
const foreign = this.parent.get(this.relation.foreignKey);
|
|
654
791
|
if (foreign === null || foreign === undefined) {
|
|
655
792
|
return null;
|
|
@@ -659,11 +796,7 @@ class BelongsToRelationQuery {
|
|
|
659
796
|
if (this.extraOptions.orderBy) {
|
|
660
797
|
query = query.orderBy(this.extraOptions.orderBy);
|
|
661
798
|
}
|
|
662
|
-
|
|
663
|
-
return row ? this.related.newFromRecord(row) : null;
|
|
664
|
-
}
|
|
665
|
-
async first() {
|
|
666
|
-
return this.get();
|
|
799
|
+
return query;
|
|
667
800
|
}
|
|
668
801
|
then(onfulfilled, onrejected) {
|
|
669
802
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -751,6 +884,31 @@ class BelongsToManyRelationQuery {
|
|
|
751
884
|
const rows = await this.connection().unsafe(`SELECT COUNT(*) AS count FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
752
885
|
return Number(rows[0]?.count ?? 0);
|
|
753
886
|
}
|
|
887
|
+
async pluck(column, keyBy) {
|
|
888
|
+
return pluckFromQuery(await this.relatedQuery(), column, keyBy);
|
|
889
|
+
}
|
|
890
|
+
async value(column) {
|
|
891
|
+
const query = await this.relatedQuery();
|
|
892
|
+
return query ? query.value(column) : null;
|
|
893
|
+
}
|
|
894
|
+
async relatedQuery() {
|
|
895
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
896
|
+
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
897
|
+
if (pivotRows.length === 0) {
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
const relatedIds = [
|
|
901
|
+
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
902
|
+
];
|
|
903
|
+
let query = this.related.repository().withConnection(this.connection()).query(asWhere({
|
|
904
|
+
[this.relation.relatedKey]: relatedIds,
|
|
905
|
+
...this.extraWhere
|
|
906
|
+
}));
|
|
907
|
+
if (this.extraOptions.orderBy) {
|
|
908
|
+
query = query.orderBy(this.extraOptions.orderBy);
|
|
909
|
+
}
|
|
910
|
+
return query;
|
|
911
|
+
}
|
|
754
912
|
then(onfulfilled, onrejected) {
|
|
755
913
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
756
914
|
}
|
|
@@ -834,13 +992,15 @@ class MorphManyRelationQuery {
|
|
|
834
992
|
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
835
993
|
return { sql, params: extra.params };
|
|
836
994
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
const rows = await repository.query(asWhere({
|
|
995
|
+
scopedQuery() {
|
|
996
|
+
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({
|
|
840
997
|
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
841
998
|
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
|
|
842
999
|
...this.extraWhere
|
|
843
|
-
}))
|
|
1000
|
+
}));
|
|
1001
|
+
}
|
|
1002
|
+
async get() {
|
|
1003
|
+
const rows = await this.scopedQuery().get();
|
|
844
1004
|
return rows.map((row) => this.related.newFromRecord(row));
|
|
845
1005
|
}
|
|
846
1006
|
async first() {
|
|
@@ -848,12 +1008,13 @@ class MorphManyRelationQuery {
|
|
|
848
1008
|
return rows[0] ?? null;
|
|
849
1009
|
}
|
|
850
1010
|
async count() {
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
1011
|
+
return this.scopedQuery().count();
|
|
1012
|
+
}
|
|
1013
|
+
async pluck(column, keyBy) {
|
|
1014
|
+
return pluckFromQuery(this.scopedQuery(), column, keyBy);
|
|
1015
|
+
}
|
|
1016
|
+
async value(column) {
|
|
1017
|
+
return this.scopedQuery().value(column);
|
|
857
1018
|
}
|
|
858
1019
|
then(onfulfilled, onrejected) {
|
|
859
1020
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -904,6 +1065,12 @@ class MorphOneRelationQuery {
|
|
|
904
1065
|
async count() {
|
|
905
1066
|
return this.inner.count();
|
|
906
1067
|
}
|
|
1068
|
+
async pluck(column, keyBy) {
|
|
1069
|
+
return keyBy === undefined ? this.inner.pluck(column) : this.inner.pluck(column, keyBy);
|
|
1070
|
+
}
|
|
1071
|
+
async value(column) {
|
|
1072
|
+
return this.inner.value(column);
|
|
1073
|
+
}
|
|
907
1074
|
then(onfulfilled, onrejected) {
|
|
908
1075
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
909
1076
|
}
|
|
@@ -952,15 +1119,31 @@ class MorphToRelationQuery {
|
|
|
952
1119
|
};
|
|
953
1120
|
}
|
|
954
1121
|
async get() {
|
|
1122
|
+
const query = this.relatedQuery();
|
|
1123
|
+
if (!query) {
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1126
|
+
const row = await query.first();
|
|
1127
|
+
return row ? this.relatedForCurrentType()?.newFromRecord(row) ?? null : null;
|
|
1128
|
+
}
|
|
1129
|
+
async pluck(column, keyBy) {
|
|
1130
|
+
return pluckFromQuery(this.relatedQuery(), column, keyBy);
|
|
1131
|
+
}
|
|
1132
|
+
async value(column) {
|
|
1133
|
+
return this.relatedQuery()?.value(column) ?? null;
|
|
1134
|
+
}
|
|
1135
|
+
relatedForCurrentType() {
|
|
955
1136
|
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
1137
|
+
return this.relatedByType[type];
|
|
1138
|
+
}
|
|
1139
|
+
relatedQuery() {
|
|
956
1140
|
const id = this.parent.get(this.relation.morphIdKey);
|
|
957
|
-
const related = this.
|
|
1141
|
+
const related = this.relatedForCurrentType();
|
|
958
1142
|
if (!related || id === null || id === undefined) {
|
|
959
1143
|
return null;
|
|
960
1144
|
}
|
|
961
1145
|
const table = related.repository().getTable();
|
|
962
|
-
|
|
963
|
-
return row ? related.newFromRecord(row) : null;
|
|
1146
|
+
return related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere }));
|
|
964
1147
|
}
|
|
965
1148
|
then(onfulfilled, onrejected) {
|
|
966
1149
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -1007,10 +1190,7 @@ class HasManyThroughRelationQuery {
|
|
|
1007
1190
|
return { sql, params: extra.params };
|
|
1008
1191
|
}
|
|
1009
1192
|
async get() {
|
|
1010
|
-
const rows = await this.
|
|
1011
|
-
...this.extraOptions,
|
|
1012
|
-
where: this.extraWhere
|
|
1013
|
-
});
|
|
1193
|
+
const rows = await this.farRows();
|
|
1014
1194
|
return rows.map((row) => this.related.newFromRecord(row));
|
|
1015
1195
|
}
|
|
1016
1196
|
async first() {
|
|
@@ -1021,6 +1201,20 @@ class HasManyThroughRelationQuery {
|
|
|
1021
1201
|
const rows = await this.get();
|
|
1022
1202
|
return rows.length;
|
|
1023
1203
|
}
|
|
1204
|
+
async pluck(column, keyBy) {
|
|
1205
|
+
const rows = await this.farRows();
|
|
1206
|
+
return keyBy === undefined ? projectPluck(rows, column) : projectPluck(rows, column, keyBy);
|
|
1207
|
+
}
|
|
1208
|
+
async value(column) {
|
|
1209
|
+
const values = await this.limit(1).pluck(column);
|
|
1210
|
+
return values[0] ?? null;
|
|
1211
|
+
}
|
|
1212
|
+
async farRows() {
|
|
1213
|
+
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
|
|
1214
|
+
...this.extraOptions,
|
|
1215
|
+
where: this.extraWhere
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1024
1218
|
then(onfulfilled, onrejected) {
|
|
1025
1219
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
1026
1220
|
}
|
|
@@ -1407,6 +1601,17 @@ function ensureBooted(model) {
|
|
|
1407
1601
|
function getGlobalScopes(model) {
|
|
1408
1602
|
return modelGlobalScopes.get(model) ?? [];
|
|
1409
1603
|
}
|
|
1604
|
+
var PASSWORD_HASH_PATTERN = /^\$(?:2[aby]?|argon2(?:i|d|id)?)\$/;
|
|
1605
|
+
function isAlreadyHashed(value) {
|
|
1606
|
+
return PASSWORD_HASH_PATTERN.test(value);
|
|
1607
|
+
}
|
|
1608
|
+
function hashCastValue(value) {
|
|
1609
|
+
const plain = String(value);
|
|
1610
|
+
if (isAlreadyHashed(plain)) {
|
|
1611
|
+
return plain;
|
|
1612
|
+
}
|
|
1613
|
+
return Bun.password.hashSync(plain, { algorithm: "bcrypt", cost: 10 });
|
|
1614
|
+
}
|
|
1410
1615
|
function hydrateValue(value, cast) {
|
|
1411
1616
|
if (value === null || value === undefined) {
|
|
1412
1617
|
return value;
|
|
@@ -1446,12 +1651,15 @@ function dehydrateValue(value, cast) {
|
|
|
1446
1651
|
case "int":
|
|
1447
1652
|
return value === "" ? null : Number(value);
|
|
1448
1653
|
case "hashed":
|
|
1449
|
-
return value;
|
|
1654
|
+
return hashCastValue(value);
|
|
1450
1655
|
default:
|
|
1451
1656
|
return value;
|
|
1452
1657
|
}
|
|
1453
1658
|
}
|
|
1454
1659
|
function filterMassAssignable(fillable, guarded, input) {
|
|
1660
|
+
if (fillable === undefined && guarded === undefined && Object.keys(input).length > 0) {
|
|
1661
|
+
throw new Error("Mass assignment is not configured for this model. Declare static $fillable = [...] to allow specific columns, or static $guarded = [] to allow all of them.");
|
|
1662
|
+
}
|
|
1455
1663
|
const resolvedGuarded = guarded ?? true;
|
|
1456
1664
|
if (fillable && fillable.length > 0) {
|
|
1457
1665
|
const allowed = new Set(fillable);
|
|
@@ -1476,6 +1684,10 @@ function applyCasts(values, casts, direction) {
|
|
|
1476
1684
|
}
|
|
1477
1685
|
return result;
|
|
1478
1686
|
}
|
|
1687
|
+
function castPluckedValue(modelClass, column, value) {
|
|
1688
|
+
const cast = modelStatics(modelClass).$casts?.[column];
|
|
1689
|
+
return cast ? hydrateValue(value, cast) : value;
|
|
1690
|
+
}
|
|
1479
1691
|
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
1480
1692
|
if (!enabled) {
|
|
1481
1693
|
return values;
|
|
@@ -1549,10 +1761,38 @@ class ModelQuery {
|
|
|
1549
1761
|
this.query.whereNull(column);
|
|
1550
1762
|
return this;
|
|
1551
1763
|
}
|
|
1764
|
+
whereNotNull(column) {
|
|
1765
|
+
this.query.whereNotNull(column);
|
|
1766
|
+
return this;
|
|
1767
|
+
}
|
|
1552
1768
|
whereIn(column, values) {
|
|
1553
1769
|
this.query.whereIn(column, values);
|
|
1554
1770
|
return this;
|
|
1555
1771
|
}
|
|
1772
|
+
whereNotIn(column, values) {
|
|
1773
|
+
this.query.whereNotIn(column, values);
|
|
1774
|
+
return this;
|
|
1775
|
+
}
|
|
1776
|
+
groupBy(groupBy) {
|
|
1777
|
+
this.query.groupBy(groupBy);
|
|
1778
|
+
return this;
|
|
1779
|
+
}
|
|
1780
|
+
having(having) {
|
|
1781
|
+
this.query.having(having);
|
|
1782
|
+
return this;
|
|
1783
|
+
}
|
|
1784
|
+
join(left, right) {
|
|
1785
|
+
this.query.join(left, right);
|
|
1786
|
+
return this;
|
|
1787
|
+
}
|
|
1788
|
+
leftJoin(left, right) {
|
|
1789
|
+
this.query.leftJoin(left, right);
|
|
1790
|
+
return this;
|
|
1791
|
+
}
|
|
1792
|
+
async paginate(options) {
|
|
1793
|
+
const { data, meta } = await this.query.paginate(options);
|
|
1794
|
+
return { data: await this.hydrateRows(data), meta };
|
|
1795
|
+
}
|
|
1556
1796
|
whereExists(sql, params = []) {
|
|
1557
1797
|
this.query.whereExists(sql, params);
|
|
1558
1798
|
return this;
|
|
@@ -1610,8 +1850,10 @@ class ModelQuery {
|
|
|
1610
1850
|
return this;
|
|
1611
1851
|
}
|
|
1612
1852
|
async get() {
|
|
1853
|
+
return await this.hydrateRows(await this.query.get());
|
|
1854
|
+
}
|
|
1855
|
+
async hydrateRows(rows) {
|
|
1613
1856
|
const statics = modelStatics(this.modelClass);
|
|
1614
|
-
const rows = await this.query.get();
|
|
1615
1857
|
const models = [];
|
|
1616
1858
|
for (const row of rows) {
|
|
1617
1859
|
const model = statics.newFromRecord(row, true);
|
|
@@ -1630,6 +1872,25 @@ class ModelQuery {
|
|
|
1630
1872
|
const models = await this.get();
|
|
1631
1873
|
return models[0] ?? null;
|
|
1632
1874
|
}
|
|
1875
|
+
async count() {
|
|
1876
|
+
return this.query.count();
|
|
1877
|
+
}
|
|
1878
|
+
async pluck(column, keyBy) {
|
|
1879
|
+
if (keyBy === undefined) {
|
|
1880
|
+
const values = await this.query.pluck(column);
|
|
1881
|
+
return values.map((value) => castPluckedValue(this.modelClass, column, value));
|
|
1882
|
+
}
|
|
1883
|
+
const keyed = await this.query.pluck(column, keyBy);
|
|
1884
|
+
const result = new Map;
|
|
1885
|
+
for (const [key, value] of keyed) {
|
|
1886
|
+
result.set(castPluckedValue(this.modelClass, keyBy, key), castPluckedValue(this.modelClass, column, value));
|
|
1887
|
+
}
|
|
1888
|
+
return result;
|
|
1889
|
+
}
|
|
1890
|
+
async value(column) {
|
|
1891
|
+
const value = await this.query.value(column);
|
|
1892
|
+
return castPluckedValue(this.modelClass, column, value);
|
|
1893
|
+
}
|
|
1633
1894
|
async find(id) {
|
|
1634
1895
|
const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
|
|
1635
1896
|
return this.where({ [primaryKey]: id }).first();
|
|
@@ -1644,6 +1905,23 @@ class ModelQuery {
|
|
|
1644
1905
|
then(onfulfilled, onrejected) {
|
|
1645
1906
|
return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
1646
1907
|
}
|
|
1908
|
+
withCount(name, alias = `${name}_count`) {
|
|
1909
|
+
const statics = modelStatics(this.modelClass);
|
|
1910
|
+
ensureBooted(this.modelClass);
|
|
1911
|
+
const repository = resolveModelRepository(this.modelClass);
|
|
1912
|
+
const dummy = statics.newFromRecord({});
|
|
1913
|
+
const method = dummy[name];
|
|
1914
|
+
if (typeof method !== "function") {
|
|
1915
|
+
throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
|
|
1916
|
+
}
|
|
1917
|
+
const relationQuery = method.call(dummy);
|
|
1918
|
+
const exists = relationQuery.toExistsClause(repository.getTable().name);
|
|
1919
|
+
if (!exists.sql.startsWith("SELECT 1 ")) {
|
|
1920
|
+
throw new Error(`Cannot count relation ${name}: unexpected subquery shape.`);
|
|
1921
|
+
}
|
|
1922
|
+
this.query.withSubqueryCount(alias, `SELECT COUNT(*) ${exists.sql.slice("SELECT 1 ".length)}`, exists.params);
|
|
1923
|
+
return this;
|
|
1924
|
+
}
|
|
1647
1925
|
constrainExists(name, constrain, not) {
|
|
1648
1926
|
const statics = modelStatics(this.modelClass);
|
|
1649
1927
|
ensureBooted(this.modelClass);
|
|
@@ -1877,6 +2155,16 @@ class Model {
|
|
|
1877
2155
|
static where(where) {
|
|
1878
2156
|
return Model.query.call(this).where(where);
|
|
1879
2157
|
}
|
|
2158
|
+
static async count() {
|
|
2159
|
+
return Model.query.call(this).count();
|
|
2160
|
+
}
|
|
2161
|
+
static pluck(column, keyBy) {
|
|
2162
|
+
const query = Model.query.call(this);
|
|
2163
|
+
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
2164
|
+
}
|
|
2165
|
+
static value(column) {
|
|
2166
|
+
return Model.query.call(this).value(column);
|
|
2167
|
+
}
|
|
1880
2168
|
static async firstWhere(where, options = {}) {
|
|
1881
2169
|
let query = Model.query.call(this).where(where);
|
|
1882
2170
|
if (options.orderBy) {
|
|
@@ -1892,11 +2180,23 @@ class Model {
|
|
|
1892
2180
|
return modelStatics(this).newFromRecord({ ...where, ...values }, false);
|
|
1893
2181
|
}
|
|
1894
2182
|
static async firstOrCreate(where, values = {}) {
|
|
1895
|
-
const
|
|
2183
|
+
const findExisting = () => Model.firstWhere.call(this, where);
|
|
2184
|
+
const existing = await findExisting();
|
|
1896
2185
|
if (existing) {
|
|
1897
2186
|
return existing;
|
|
1898
2187
|
}
|
|
1899
|
-
|
|
2188
|
+
try {
|
|
2189
|
+
return await Model.create.call(this, { ...where, ...values });
|
|
2190
|
+
} catch (error) {
|
|
2191
|
+
if (!(error instanceof ConflictError)) {
|
|
2192
|
+
throw error;
|
|
2193
|
+
}
|
|
2194
|
+
const raced = await findExisting();
|
|
2195
|
+
if (!raced) {
|
|
2196
|
+
throw error;
|
|
2197
|
+
}
|
|
2198
|
+
return raced;
|
|
2199
|
+
}
|
|
1900
2200
|
}
|
|
1901
2201
|
static async updateOrCreate(where, values = {}) {
|
|
1902
2202
|
const existing = await Model.firstWhere.call(this, where);
|
|
@@ -37,15 +37,32 @@ function pushParam(values, value) {
|
|
|
37
37
|
values.push(value);
|
|
38
38
|
return currentSqlDialect().placeholder(values.length);
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
var SUPPORTED_OPERATORS = new Set([
|
|
41
|
+
"eq",
|
|
42
|
+
"ne",
|
|
43
|
+
"in",
|
|
44
|
+
"notIn",
|
|
45
|
+
"gt",
|
|
46
|
+
"gte",
|
|
47
|
+
"lt",
|
|
48
|
+
"lte",
|
|
49
|
+
"isNull",
|
|
50
|
+
"ilike",
|
|
51
|
+
"tsMatch"
|
|
52
|
+
]);
|
|
53
|
+
function buildInClause(column, values, params, negated = false) {
|
|
41
54
|
if (values.length === 0) {
|
|
42
|
-
return "1 = 0";
|
|
55
|
+
return negated ? "1 = 1" : "1 = 0";
|
|
43
56
|
}
|
|
44
57
|
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
45
|
-
return `${column} IN (${placeholders})`;
|
|
58
|
+
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
46
59
|
}
|
|
47
60
|
function buildOperatorClauses(column, operator, params) {
|
|
48
61
|
const clauses = [];
|
|
62
|
+
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
63
|
+
if (unsupported.length > 0) {
|
|
64
|
+
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
65
|
+
}
|
|
49
66
|
if (operator.isNull === true) {
|
|
50
67
|
clauses.push(`${column} IS NULL`);
|
|
51
68
|
}
|
|
@@ -59,9 +76,19 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
59
76
|
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
60
77
|
}
|
|
61
78
|
}
|
|
79
|
+
if (operator.ne !== undefined) {
|
|
80
|
+
if (operator.ne === null) {
|
|
81
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
82
|
+
} else {
|
|
83
|
+
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
62
86
|
if (operator.in !== undefined) {
|
|
63
87
|
clauses.push(buildInClause(column, operator.in, params));
|
|
64
88
|
}
|
|
89
|
+
if (operator.notIn !== undefined) {
|
|
90
|
+
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
91
|
+
}
|
|
65
92
|
if (operator.gt !== undefined) {
|
|
66
93
|
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
67
94
|
}
|
|
@@ -281,6 +308,10 @@ function buildSelectList(table, select, params = []) {
|
|
|
281
308
|
if (item.kind === "literalText") {
|
|
282
309
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
283
310
|
}
|
|
311
|
+
if (item.kind === "subqueryCount") {
|
|
312
|
+
const body = remapExistsSql(item.sql, item.params, params);
|
|
313
|
+
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
314
|
+
}
|
|
284
315
|
const column = qualifyColumn(item.table, item.column);
|
|
285
316
|
const placeholder = pushParam(params, item.query);
|
|
286
317
|
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
@@ -375,6 +406,52 @@ function buildInsertQuery(table, values) {
|
|
|
375
406
|
params
|
|
376
407
|
};
|
|
377
408
|
}
|
|
409
|
+
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
410
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
411
|
+
if (entries.length === 0) {
|
|
412
|
+
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
413
|
+
}
|
|
414
|
+
if (conflictColumns.length === 0) {
|
|
415
|
+
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
416
|
+
}
|
|
417
|
+
const insertable = new Set(entries.map(([column]) => column));
|
|
418
|
+
const conflict = new Set(conflictColumns);
|
|
419
|
+
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
420
|
+
const params = [];
|
|
421
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
422
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
423
|
+
const returningColumns = buildReturningColumns(table);
|
|
424
|
+
const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
|
|
425
|
+
return {
|
|
426
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
427
|
+
params
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
431
|
+
if (!Number.isFinite(amount)) {
|
|
432
|
+
throw new Error("Increment amount must be a finite number.");
|
|
433
|
+
}
|
|
434
|
+
if (!table.columns.includes(column)) {
|
|
435
|
+
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
436
|
+
}
|
|
437
|
+
const params = [];
|
|
438
|
+
const target = quoteIdentifier(column);
|
|
439
|
+
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
440
|
+
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
441
|
+
exclude: [table.primaryKey, column]
|
|
442
|
+
})) {
|
|
443
|
+
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
444
|
+
}
|
|
445
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
446
|
+
const returningColumns = buildReturningColumns(table);
|
|
447
|
+
const scopeClauses = [];
|
|
448
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
449
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
450
|
+
return {
|
|
451
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
452
|
+
params
|
|
453
|
+
};
|
|
454
|
+
}
|
|
378
455
|
function buildUpdateQuery(table, id, changes) {
|
|
379
456
|
const entries = getDefinedColumnEntries(table, changes, {
|
|
380
457
|
exclude: [table.primaryKey]
|
|
@@ -440,6 +517,7 @@ export {
|
|
|
440
517
|
buildCountQuery,
|
|
441
518
|
buildDeleteByIdQuery,
|
|
442
519
|
buildGroupedCountQuery,
|
|
520
|
+
buildIncrementQuery,
|
|
443
521
|
buildInsertQuery,
|
|
444
522
|
buildJoinClause,
|
|
445
523
|
buildOrderByClause,
|
|
@@ -449,6 +527,7 @@ export {
|
|
|
449
527
|
buildSelectQuery,
|
|
450
528
|
buildSoftDeleteByIdQuery,
|
|
451
529
|
buildUpdateQuery,
|
|
530
|
+
buildUpsertQuery,
|
|
452
531
|
buildWhereClause,
|
|
453
532
|
parseQualifiedColumn,
|
|
454
533
|
qualifyColumn,
|