@getstrata/core 1.0.6 → 1.0.8
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/model.d.ts +15 -0
- package/dist/core/database/query.d.ts +9 -1
- package/dist/core/database/repositoryQuery.d.ts +2 -0
- package/dist/core/database/types.d.ts +7 -0
- package/dist/entries/database/model.js +158 -8
- package/dist/entries/database/query.js +82 -3
- package/dist/entries/database/repositoryQuery.js +96 -3
- package/dist/entries/database/schema.js +80 -3
- package/dist/entries/http/parseMultipartUpload.js +21 -1
- package/dist/index.js +267 -9
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.8
|
|
4
|
+
|
|
5
|
+
- Container image starts and ships production dependencies only.
|
|
6
|
+
|
|
7
|
+
## 1.0.7
|
|
8
|
+
|
|
3
9
|
## 1.0.6
|
|
4
10
|
|
|
5
11
|
- Query `pluck` and `value` on `RepositoryQuery`, `ModelQuery`, and relation queries. `pluck(column)` returns `T[]`; `pluck(column, keyBy)` returns a `Map`. Models apply `$casts` and skip hydration/observers. Generated apps and lockstep packages move to `^1.0.6`.
|
|
@@ -28,6 +28,7 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
|
|
|
28
28
|
perPage: number;
|
|
29
29
|
} & Omit<ExtendedQueryOptions<TEntity>, "limit" | "offset">): Promise<PaginatedResult<TEntity>>;
|
|
30
30
|
chunk(count: number, callback: (rows: TEntity[]) => Promise<boolean | void>, options?: Omit<ExtendedQueryOptions<TEntity>, "limit" | "offset">): Promise<void>;
|
|
31
|
+
chunkById(count: number, callback: (rows: TEntity[]) => Promise<boolean | undefined>, options?: Omit<ExtendedQueryOptions<TEntity>, "limit" | "offset" | "orderBy">): Promise<void>;
|
|
31
32
|
cursorPaginate(options: {
|
|
32
33
|
perPage: number;
|
|
33
34
|
cursor?: TEntity[PrimaryKey];
|
|
@@ -38,6 +39,9 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
|
|
|
38
39
|
findByIdOrThrow(id: TEntity[PrimaryKey], errorFactory?: ErrorFactory<TEntity[PrimaryKey]>): Promise<TEntity>;
|
|
39
40
|
findByIds(ids: readonly TEntity[PrimaryKey][]): Promise<TEntity[]>;
|
|
40
41
|
firstOrNull(where: QueryWhere<TEntity>, options?: Omit<QueryOptions<TEntity>, "where" | "limit">): Promise<TEntity | null>;
|
|
42
|
+
upsert(values: MutationValues<TEntity>, conflictColumns: readonly (keyof TEntity & string)[], updateColumns?: readonly (keyof TEntity & string)[]): Promise<TEntity | null>;
|
|
43
|
+
incrementById(id: TEntity[PrimaryKey], column: keyof TEntity & string, amount?: number, extra?: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
|
|
44
|
+
decrementById(id: TEntity[PrimaryKey], column: keyof TEntity & string, amount?: number, extra?: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
|
|
41
45
|
create(values: MutationValues<TEntity>): Promise<TEntity>;
|
|
42
46
|
updateById(id: TEntity[PrimaryKey], changes: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
|
|
43
47
|
updateByIdOrThrow(id: TEntity[PrimaryKey], changes: UpdateValues<TEntity, PrimaryKey>, errorFactory?: ErrorFactory<TEntity[PrimaryKey]>): Promise<TEntity>;
|
|
@@ -53,6 +57,11 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
|
|
|
53
57
|
protected countWhere(where?: QueryWhere<TEntity>, options?: Pick<QueryOptions<TEntity>, "withTrashed" | "onlyTrashed" | "joins" | "groupBy">, whereNodes?: readonly WhereNode<TEntity>[]): Promise<number>;
|
|
54
58
|
protected averageColumn(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
|
|
55
59
|
protected averageExpression(expression: string, alias: string, where?: QueryWhere<TEntity>): Promise<number>;
|
|
60
|
+
sum(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
|
|
61
|
+
avg(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
|
|
62
|
+
min(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
|
|
63
|
+
max(column: keyof TEntity & string, where?: QueryWhere<TEntity>): Promise<number>;
|
|
64
|
+
private aggregateColumn;
|
|
56
65
|
protected pluckNumberValues(expression: string, alias: string, options?: QueryOptions<TEntity>): Promise<number[]>;
|
|
57
66
|
protected countGroupedBy<K extends keyof TEntity & string>(column: K, where?: QueryWhere<TEntity>): Promise<Array<{
|
|
58
67
|
value: TEntity[K] | null;
|
|
@@ -10,6 +10,8 @@ interface SqlDialect {
|
|
|
10
10
|
ilikeOperator(): "ILIKE" | "LIKE";
|
|
11
11
|
nullsLastSuffix(): string;
|
|
12
12
|
castToText(expression: string): string;
|
|
13
|
+
/** Conflict handling appended to an INSERT. Empty updates mean "do nothing". */
|
|
14
|
+
upsertSuffix(conflictColumns: readonly string[], updateColumns: readonly string[]): string;
|
|
13
15
|
}
|
|
14
16
|
declare function dialectFor(driver: DatabaseDriver): SqlDialect;
|
|
15
17
|
declare function currentSqlDialect(): SqlDialect;
|
|
@@ -43,7 +43,20 @@ declare class ModelQuery {
|
|
|
43
43
|
limit(limit: number): this;
|
|
44
44
|
offset(offset: number): this;
|
|
45
45
|
whereNull(column: string): this;
|
|
46
|
+
whereNotNull(column: string): this;
|
|
46
47
|
whereIn(column: string, values: readonly unknown[]): this;
|
|
48
|
+
whereNotIn(column: string, values: readonly unknown[]): this;
|
|
49
|
+
groupBy(groupBy: QueryOptions<object>["groupBy"]): this;
|
|
50
|
+
having(having: QueryWhere<object>): this;
|
|
51
|
+
join(left: `${string}.${string}`, right: `${string}.${string}`): this;
|
|
52
|
+
leftJoin(left: `${string}.${string}`, right: `${string}.${string}`): this;
|
|
53
|
+
paginate(options: {
|
|
54
|
+
page: number;
|
|
55
|
+
perPage: number;
|
|
56
|
+
}): Promise<{
|
|
57
|
+
data: Array<Model<Record<string, unknown>, "id">>;
|
|
58
|
+
meta: Awaited<ReturnType<RepositoryQuery<Record<string, unknown>, "id">["paginate"]>>["meta"];
|
|
59
|
+
}>;
|
|
47
60
|
whereExists(sql: string, params?: readonly unknown[]): this;
|
|
48
61
|
whereNotExists(sql: string, params?: readonly unknown[]): this;
|
|
49
62
|
whereHas(name: string, constrain?: (query: AnyRelationQuery) => void): this;
|
|
@@ -60,6 +73,7 @@ declare class ModelQuery {
|
|
|
60
73
|
withTrashed(): this;
|
|
61
74
|
onlyTrashed(): this;
|
|
62
75
|
get(): Promise<Array<Model<Record<string, unknown>, "id">>>;
|
|
76
|
+
private hydrateRows;
|
|
63
77
|
first(): Promise<Model<Record<string, unknown>, "id"> | null>;
|
|
64
78
|
count(): Promise<number>;
|
|
65
79
|
pluck(column: string): Promise<unknown[]>;
|
|
@@ -68,6 +82,7 @@ declare class ModelQuery {
|
|
|
68
82
|
find(id: unknown): Promise<Model<Record<string, unknown>, "id"> | null>;
|
|
69
83
|
findOrFail(id: unknown, errorFactory?: (id: unknown) => Error): Promise<Model<Record<string, unknown>, "id">>;
|
|
70
84
|
then(onfulfilled?: ((value: Array<Model<Record<string, unknown>, "id">>) => unknown) | null, onrejected?: ((reason: unknown) => unknown) | null): Promise<unknown>;
|
|
85
|
+
withCount(name: string, alias?: string): this;
|
|
71
86
|
private constrainExists;
|
|
72
87
|
}
|
|
73
88
|
declare class Model<TEntity extends object, PrimaryKey extends keyof TEntity & string> {
|
|
@@ -44,6 +44,14 @@ declare function buildInsertQuery<TEntity extends object, PrimaryKey extends key
|
|
|
44
44
|
text: string;
|
|
45
45
|
params: unknown[];
|
|
46
46
|
};
|
|
47
|
+
declare function buildUpsertQuery<TEntity extends object, PrimaryKey extends keyof TEntity & string>(table: TableDefinition<TEntity, PrimaryKey>, values: MutationValues<TEntity>, conflictColumns: readonly string[], updateColumns?: readonly string[]): {
|
|
48
|
+
text: string;
|
|
49
|
+
params: unknown[];
|
|
50
|
+
};
|
|
51
|
+
declare function buildIncrementQuery<TEntity extends object, PrimaryKey extends keyof TEntity & string>(table: TableDefinition<TEntity, PrimaryKey>, id: TEntity[PrimaryKey], column: keyof TEntity & string, amount: number, extra?: UpdateValues<TEntity, PrimaryKey>): {
|
|
52
|
+
text: string;
|
|
53
|
+
params: unknown[];
|
|
54
|
+
};
|
|
47
55
|
declare function buildUpdateQuery<TEntity extends object, PrimaryKey extends keyof TEntity & string>(table: TableDefinition<TEntity, PrimaryKey>, id: TEntity[PrimaryKey], changes: UpdateValues<TEntity, PrimaryKey>): {
|
|
48
56
|
text: string;
|
|
49
57
|
params: unknown[];
|
|
@@ -60,4 +68,4 @@ declare function buildDeleteByIdQuery<TEntity extends object, PrimaryKey extends
|
|
|
60
68
|
text: string;
|
|
61
69
|
params: unknown[];
|
|
62
70
|
};
|
|
63
|
-
export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
|
|
71
|
+
export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildIncrementQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
|
|
@@ -17,6 +17,8 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
|
|
|
17
17
|
limit(limit: number): this;
|
|
18
18
|
whereNull(column: keyof TEntity & string): this;
|
|
19
19
|
whereNotNull(column: keyof TEntity & string): this;
|
|
20
|
+
whereNotIn(column: keyof TEntity & string, values: readonly unknown[]): this;
|
|
21
|
+
withSubqueryCount(alias: string, sql: string, params?: readonly unknown[]): this;
|
|
20
22
|
whereIn(column: keyof TEntity & string, values: readonly unknown[]): this;
|
|
21
23
|
whereExists(sql: string, params?: readonly unknown[]): this;
|
|
22
24
|
whereNotExists(sql: string, params?: readonly unknown[]): this;
|
|
@@ -2,7 +2,9 @@ type DatabaseComparable = string | number | Date;
|
|
|
2
2
|
type DatabaseScalar = DatabaseComparable | boolean | null;
|
|
3
3
|
type QueryOperator = {
|
|
4
4
|
eq?: DatabaseScalar;
|
|
5
|
+
ne?: DatabaseScalar;
|
|
5
6
|
in?: readonly DatabaseScalar[];
|
|
7
|
+
notIn?: readonly DatabaseScalar[];
|
|
6
8
|
gt?: DatabaseComparable;
|
|
7
9
|
gte?: DatabaseComparable;
|
|
8
10
|
lt?: DatabaseComparable;
|
|
@@ -42,6 +44,11 @@ type QuerySelectItem = {
|
|
|
42
44
|
kind: "literalText";
|
|
43
45
|
value: string;
|
|
44
46
|
as: string;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "subqueryCount";
|
|
49
|
+
sql: string;
|
|
50
|
+
params: readonly unknown[];
|
|
51
|
+
as: string;
|
|
45
52
|
} | {
|
|
46
53
|
kind: "tsRank";
|
|
47
54
|
table: string;
|
|
@@ -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]
|
|
@@ -1524,6 +1601,17 @@ function ensureBooted(model) {
|
|
|
1524
1601
|
function getGlobalScopes(model) {
|
|
1525
1602
|
return modelGlobalScopes.get(model) ?? [];
|
|
1526
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
|
+
}
|
|
1527
1615
|
function hydrateValue(value, cast) {
|
|
1528
1616
|
if (value === null || value === undefined) {
|
|
1529
1617
|
return value;
|
|
@@ -1563,12 +1651,15 @@ function dehydrateValue(value, cast) {
|
|
|
1563
1651
|
case "int":
|
|
1564
1652
|
return value === "" ? null : Number(value);
|
|
1565
1653
|
case "hashed":
|
|
1566
|
-
return value;
|
|
1654
|
+
return hashCastValue(value);
|
|
1567
1655
|
default:
|
|
1568
1656
|
return value;
|
|
1569
1657
|
}
|
|
1570
1658
|
}
|
|
1571
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
|
+
}
|
|
1572
1663
|
const resolvedGuarded = guarded ?? true;
|
|
1573
1664
|
if (fillable && fillable.length > 0) {
|
|
1574
1665
|
const allowed = new Set(fillable);
|
|
@@ -1670,10 +1761,38 @@ class ModelQuery {
|
|
|
1670
1761
|
this.query.whereNull(column);
|
|
1671
1762
|
return this;
|
|
1672
1763
|
}
|
|
1764
|
+
whereNotNull(column) {
|
|
1765
|
+
this.query.whereNotNull(column);
|
|
1766
|
+
return this;
|
|
1767
|
+
}
|
|
1673
1768
|
whereIn(column, values) {
|
|
1674
1769
|
this.query.whereIn(column, values);
|
|
1675
1770
|
return this;
|
|
1676
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
|
+
}
|
|
1677
1796
|
whereExists(sql, params = []) {
|
|
1678
1797
|
this.query.whereExists(sql, params);
|
|
1679
1798
|
return this;
|
|
@@ -1731,8 +1850,10 @@ class ModelQuery {
|
|
|
1731
1850
|
return this;
|
|
1732
1851
|
}
|
|
1733
1852
|
async get() {
|
|
1853
|
+
return await this.hydrateRows(await this.query.get());
|
|
1854
|
+
}
|
|
1855
|
+
async hydrateRows(rows) {
|
|
1734
1856
|
const statics = modelStatics(this.modelClass);
|
|
1735
|
-
const rows = await this.query.get();
|
|
1736
1857
|
const models = [];
|
|
1737
1858
|
for (const row of rows) {
|
|
1738
1859
|
const model = statics.newFromRecord(row, true);
|
|
@@ -1784,6 +1905,23 @@ class ModelQuery {
|
|
|
1784
1905
|
then(onfulfilled, onrejected) {
|
|
1785
1906
|
return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
1786
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
|
+
}
|
|
1787
1925
|
constrainExists(name, constrain, not) {
|
|
1788
1926
|
const statics = modelStatics(this.modelClass);
|
|
1789
1927
|
ensureBooted(this.modelClass);
|
|
@@ -2042,11 +2180,23 @@ class Model {
|
|
|
2042
2180
|
return modelStatics(this).newFromRecord({ ...where, ...values }, false);
|
|
2043
2181
|
}
|
|
2044
2182
|
static async firstOrCreate(where, values = {}) {
|
|
2045
|
-
const
|
|
2183
|
+
const findExisting = () => Model.firstWhere.call(this, where);
|
|
2184
|
+
const existing = await findExisting();
|
|
2046
2185
|
if (existing) {
|
|
2047
2186
|
return existing;
|
|
2048
2187
|
}
|
|
2049
|
-
|
|
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
|
+
}
|
|
2050
2200
|
}
|
|
2051
2201
|
static async updateOrCreate(where, values = {}) {
|
|
2052
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,
|
|
@@ -61,15 +61,32 @@ function pushParam(values, value) {
|
|
|
61
61
|
values.push(value);
|
|
62
62
|
return currentSqlDialect().placeholder(values.length);
|
|
63
63
|
}
|
|
64
|
-
|
|
64
|
+
var SUPPORTED_OPERATORS = new Set([
|
|
65
|
+
"eq",
|
|
66
|
+
"ne",
|
|
67
|
+
"in",
|
|
68
|
+
"notIn",
|
|
69
|
+
"gt",
|
|
70
|
+
"gte",
|
|
71
|
+
"lt",
|
|
72
|
+
"lte",
|
|
73
|
+
"isNull",
|
|
74
|
+
"ilike",
|
|
75
|
+
"tsMatch"
|
|
76
|
+
]);
|
|
77
|
+
function buildInClause(column, values, params, negated = false) {
|
|
65
78
|
if (values.length === 0) {
|
|
66
|
-
return "1 = 0";
|
|
79
|
+
return negated ? "1 = 1" : "1 = 0";
|
|
67
80
|
}
|
|
68
81
|
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
69
|
-
return `${column} IN (${placeholders})`;
|
|
82
|
+
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
70
83
|
}
|
|
71
84
|
function buildOperatorClauses(column, operator, params) {
|
|
72
85
|
const clauses = [];
|
|
86
|
+
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
87
|
+
if (unsupported.length > 0) {
|
|
88
|
+
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
89
|
+
}
|
|
73
90
|
if (operator.isNull === true) {
|
|
74
91
|
clauses.push(`${column} IS NULL`);
|
|
75
92
|
}
|
|
@@ -83,9 +100,19 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
83
100
|
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
84
101
|
}
|
|
85
102
|
}
|
|
103
|
+
if (operator.ne !== undefined) {
|
|
104
|
+
if (operator.ne === null) {
|
|
105
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
106
|
+
} else {
|
|
107
|
+
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
86
110
|
if (operator.in !== undefined) {
|
|
87
111
|
clauses.push(buildInClause(column, operator.in, params));
|
|
88
112
|
}
|
|
113
|
+
if (operator.notIn !== undefined) {
|
|
114
|
+
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
115
|
+
}
|
|
89
116
|
if (operator.gt !== undefined) {
|
|
90
117
|
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
91
118
|
}
|
|
@@ -305,6 +332,10 @@ function buildSelectList(table, select, params = []) {
|
|
|
305
332
|
if (item.kind === "literalText") {
|
|
306
333
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
307
334
|
}
|
|
335
|
+
if (item.kind === "subqueryCount") {
|
|
336
|
+
const body = remapExistsSql(item.sql, item.params, params);
|
|
337
|
+
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
338
|
+
}
|
|
308
339
|
const column = qualifyColumn(item.table, item.column);
|
|
309
340
|
const placeholder = pushParam(params, item.query);
|
|
310
341
|
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
@@ -399,6 +430,52 @@ function buildInsertQuery(table, values) {
|
|
|
399
430
|
params
|
|
400
431
|
};
|
|
401
432
|
}
|
|
433
|
+
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
434
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
435
|
+
if (entries.length === 0) {
|
|
436
|
+
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
437
|
+
}
|
|
438
|
+
if (conflictColumns.length === 0) {
|
|
439
|
+
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
440
|
+
}
|
|
441
|
+
const insertable = new Set(entries.map(([column]) => column));
|
|
442
|
+
const conflict = new Set(conflictColumns);
|
|
443
|
+
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
444
|
+
const params = [];
|
|
445
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
446
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
447
|
+
const returningColumns = buildReturningColumns(table);
|
|
448
|
+
const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
|
|
449
|
+
return {
|
|
450
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
451
|
+
params
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
455
|
+
if (!Number.isFinite(amount)) {
|
|
456
|
+
throw new Error("Increment amount must be a finite number.");
|
|
457
|
+
}
|
|
458
|
+
if (!table.columns.includes(column)) {
|
|
459
|
+
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
460
|
+
}
|
|
461
|
+
const params = [];
|
|
462
|
+
const target = quoteIdentifier(column);
|
|
463
|
+
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
464
|
+
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
465
|
+
exclude: [table.primaryKey, column]
|
|
466
|
+
})) {
|
|
467
|
+
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
468
|
+
}
|
|
469
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
470
|
+
const returningColumns = buildReturningColumns(table);
|
|
471
|
+
const scopeClauses = [];
|
|
472
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
473
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
474
|
+
return {
|
|
475
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
476
|
+
params
|
|
477
|
+
};
|
|
478
|
+
}
|
|
402
479
|
function buildUpdateQuery(table, id, changes) {
|
|
403
480
|
const entries = getDefinedColumnEntries(table, changes, {
|
|
404
481
|
exclude: [table.primaryKey]
|
|
@@ -779,6 +856,22 @@ class RepositoryQuery {
|
|
|
779
856
|
whereNotNull(column) {
|
|
780
857
|
return this.where({ [column]: { isNull: false } });
|
|
781
858
|
}
|
|
859
|
+
whereNotIn(column, values) {
|
|
860
|
+
return this.where({ [column]: { notIn: values } });
|
|
861
|
+
}
|
|
862
|
+
withSubqueryCount(alias, sql, params = []) {
|
|
863
|
+
const table = this.repository.getTable();
|
|
864
|
+
const existing = this.queryOptions.select ?? table.columns.map((column) => ({
|
|
865
|
+
kind: "column",
|
|
866
|
+
table: table.name,
|
|
867
|
+
column
|
|
868
|
+
}));
|
|
869
|
+
this.queryOptions = {
|
|
870
|
+
...this.queryOptions,
|
|
871
|
+
select: [...existing, { kind: "subqueryCount", sql, params, as: alias }]
|
|
872
|
+
};
|
|
873
|
+
return this;
|
|
874
|
+
}
|
|
782
875
|
whereIn(column, values) {
|
|
783
876
|
return this.where({ [column]: values });
|
|
784
877
|
}
|
|
@@ -304,15 +304,32 @@ function pushParam(values, value) {
|
|
|
304
304
|
values.push(value);
|
|
305
305
|
return currentSqlDialect().placeholder(values.length);
|
|
306
306
|
}
|
|
307
|
-
|
|
307
|
+
var SUPPORTED_OPERATORS = new Set([
|
|
308
|
+
"eq",
|
|
309
|
+
"ne",
|
|
310
|
+
"in",
|
|
311
|
+
"notIn",
|
|
312
|
+
"gt",
|
|
313
|
+
"gte",
|
|
314
|
+
"lt",
|
|
315
|
+
"lte",
|
|
316
|
+
"isNull",
|
|
317
|
+
"ilike",
|
|
318
|
+
"tsMatch"
|
|
319
|
+
]);
|
|
320
|
+
function buildInClause(column, values, params, negated = false) {
|
|
308
321
|
if (values.length === 0) {
|
|
309
|
-
return "1 = 0";
|
|
322
|
+
return negated ? "1 = 1" : "1 = 0";
|
|
310
323
|
}
|
|
311
324
|
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
312
|
-
return `${column} IN (${placeholders})`;
|
|
325
|
+
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
313
326
|
}
|
|
314
327
|
function buildOperatorClauses(column, operator, params) {
|
|
315
328
|
const clauses = [];
|
|
329
|
+
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
330
|
+
if (unsupported.length > 0) {
|
|
331
|
+
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
332
|
+
}
|
|
316
333
|
if (operator.isNull === true) {
|
|
317
334
|
clauses.push(`${column} IS NULL`);
|
|
318
335
|
}
|
|
@@ -326,9 +343,19 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
326
343
|
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
327
344
|
}
|
|
328
345
|
}
|
|
346
|
+
if (operator.ne !== undefined) {
|
|
347
|
+
if (operator.ne === null) {
|
|
348
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
349
|
+
} else {
|
|
350
|
+
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
329
353
|
if (operator.in !== undefined) {
|
|
330
354
|
clauses.push(buildInClause(column, operator.in, params));
|
|
331
355
|
}
|
|
356
|
+
if (operator.notIn !== undefined) {
|
|
357
|
+
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
358
|
+
}
|
|
332
359
|
if (operator.gt !== undefined) {
|
|
333
360
|
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
334
361
|
}
|
|
@@ -548,6 +575,10 @@ function buildSelectList(table, select, params = []) {
|
|
|
548
575
|
if (item.kind === "literalText") {
|
|
549
576
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
550
577
|
}
|
|
578
|
+
if (item.kind === "subqueryCount") {
|
|
579
|
+
const body = remapExistsSql(item.sql, item.params, params);
|
|
580
|
+
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
581
|
+
}
|
|
551
582
|
const column = qualifyColumn(item.table, item.column);
|
|
552
583
|
const placeholder = pushParam(params, item.query);
|
|
553
584
|
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
@@ -642,6 +673,52 @@ function buildInsertQuery(table, values) {
|
|
|
642
673
|
params
|
|
643
674
|
};
|
|
644
675
|
}
|
|
676
|
+
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
677
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
678
|
+
if (entries.length === 0) {
|
|
679
|
+
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
680
|
+
}
|
|
681
|
+
if (conflictColumns.length === 0) {
|
|
682
|
+
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
683
|
+
}
|
|
684
|
+
const insertable = new Set(entries.map(([column]) => column));
|
|
685
|
+
const conflict = new Set(conflictColumns);
|
|
686
|
+
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
687
|
+
const params = [];
|
|
688
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
689
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
690
|
+
const returningColumns = buildReturningColumns(table);
|
|
691
|
+
const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
|
|
692
|
+
return {
|
|
693
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
694
|
+
params
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
698
|
+
if (!Number.isFinite(amount)) {
|
|
699
|
+
throw new Error("Increment amount must be a finite number.");
|
|
700
|
+
}
|
|
701
|
+
if (!table.columns.includes(column)) {
|
|
702
|
+
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
703
|
+
}
|
|
704
|
+
const params = [];
|
|
705
|
+
const target = quoteIdentifier(column);
|
|
706
|
+
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
707
|
+
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
708
|
+
exclude: [table.primaryKey, column]
|
|
709
|
+
})) {
|
|
710
|
+
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
711
|
+
}
|
|
712
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
713
|
+
const returningColumns = buildReturningColumns(table);
|
|
714
|
+
const scopeClauses = [];
|
|
715
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
716
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
717
|
+
return {
|
|
718
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
719
|
+
params
|
|
720
|
+
};
|
|
721
|
+
}
|
|
645
722
|
function buildUpdateQuery(table, id, changes) {
|
|
646
723
|
const entries = getDefinedColumnEntries(table, changes, {
|
|
647
724
|
exclude: [table.primaryKey]
|
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
// ../../src/core/http/parseMultipartUpload.ts
|
|
3
3
|
import { BadRequestError, PayloadTooLargeError } from "@getstrata/core/errors/http";
|
|
4
4
|
|
|
5
|
+
// ../../src/core/runtime/appEnv.ts
|
|
6
|
+
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
7
|
+
function normalizeEnvValue(value) {
|
|
8
|
+
return (value ?? "").trim().toLowerCase();
|
|
9
|
+
}
|
|
10
|
+
function isProductionEnv(env = process.env) {
|
|
11
|
+
const appEnv = normalizeEnvValue(env.APP_ENV);
|
|
12
|
+
const nodeEnv = normalizeEnvValue(env.NODE_ENV);
|
|
13
|
+
if (appEnv === "production" || nodeEnv === "production") {
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
if (appEnv === "") {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return !NON_PRODUCTION_APP_ENVS.has(appEnv);
|
|
20
|
+
}
|
|
21
|
+
function envFlagEnabled(value) {
|
|
22
|
+
return value === "true";
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
// ../../src/core/http/uploads.ts
|
|
6
26
|
var DEFAULT_MAX_UPLOAD_BYTES = 5 * 1024 * 1024;
|
|
7
27
|
var ALLOWED_UPLOAD_MIME_TYPES = new Set([
|
|
@@ -33,7 +53,7 @@ function normalizeMimeType(mimeType) {
|
|
|
33
53
|
function isAllowedMimeType(mimeType) {
|
|
34
54
|
const normalized = normalizeMimeType(mimeType);
|
|
35
55
|
if (!normalized || normalized === "application/octet-stream") {
|
|
36
|
-
return
|
|
56
|
+
return envFlagEnabled(process.env.UPLOAD_ALLOW_UNKNOWN_MIME);
|
|
37
57
|
}
|
|
38
58
|
return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
|
|
39
59
|
}
|
package/dist/index.js
CHANGED
|
@@ -2071,6 +2071,9 @@ function assertSafeIdentifier(identifier) {
|
|
|
2071
2071
|
}
|
|
2072
2072
|
return identifier;
|
|
2073
2073
|
}
|
|
2074
|
+
function quoteIdentifierFor(dialect, column) {
|
|
2075
|
+
return dialect.quoteIdentifier(column);
|
|
2076
|
+
}
|
|
2074
2077
|
var postgresDialect = {
|
|
2075
2078
|
driver: "pgsql",
|
|
2076
2079
|
placeholder(index) {
|
|
@@ -2096,6 +2099,17 @@ var postgresDialect = {
|
|
|
2096
2099
|
},
|
|
2097
2100
|
castToText(expression) {
|
|
2098
2101
|
return `${expression}::text`;
|
|
2102
|
+
},
|
|
2103
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
2104
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
2105
|
+
if (updateColumns.length === 0) {
|
|
2106
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
2107
|
+
}
|
|
2108
|
+
const assignments = updateColumns.map((column) => {
|
|
2109
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
2110
|
+
return `${quoted} = excluded.${quoted}`;
|
|
2111
|
+
}).join(", ");
|
|
2112
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
2099
2113
|
}
|
|
2100
2114
|
};
|
|
2101
2115
|
var mysqlDialect = {
|
|
@@ -2123,6 +2137,17 @@ var mysqlDialect = {
|
|
|
2123
2137
|
},
|
|
2124
2138
|
castToText(expression) {
|
|
2125
2139
|
return `CAST(${expression} AS CHAR)`;
|
|
2140
|
+
},
|
|
2141
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
2142
|
+
if (updateColumns.length === 0) {
|
|
2143
|
+
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
2144
|
+
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
2145
|
+
}
|
|
2146
|
+
const assignments = updateColumns.map((column) => {
|
|
2147
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
2148
|
+
return `${quoted} = VALUES(${quoted})`;
|
|
2149
|
+
}).join(", ");
|
|
2150
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
2126
2151
|
}
|
|
2127
2152
|
};
|
|
2128
2153
|
var sqliteDialect = {
|
|
@@ -2150,6 +2175,17 @@ var sqliteDialect = {
|
|
|
2150
2175
|
},
|
|
2151
2176
|
castToText(expression) {
|
|
2152
2177
|
return `CAST(${expression} AS TEXT)`;
|
|
2178
|
+
},
|
|
2179
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
2180
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
2181
|
+
if (updateColumns.length === 0) {
|
|
2182
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
2183
|
+
}
|
|
2184
|
+
const assignments = updateColumns.map((column) => {
|
|
2185
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
2186
|
+
return `${quoted} = excluded.${quoted}`;
|
|
2187
|
+
}).join(", ");
|
|
2188
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
2153
2189
|
}
|
|
2154
2190
|
};
|
|
2155
2191
|
var dialects = {
|
|
@@ -2216,15 +2252,32 @@ function pushParam(values, value) {
|
|
|
2216
2252
|
values.push(value);
|
|
2217
2253
|
return currentSqlDialect().placeholder(values.length);
|
|
2218
2254
|
}
|
|
2219
|
-
|
|
2255
|
+
var SUPPORTED_OPERATORS = new Set([
|
|
2256
|
+
"eq",
|
|
2257
|
+
"ne",
|
|
2258
|
+
"in",
|
|
2259
|
+
"notIn",
|
|
2260
|
+
"gt",
|
|
2261
|
+
"gte",
|
|
2262
|
+
"lt",
|
|
2263
|
+
"lte",
|
|
2264
|
+
"isNull",
|
|
2265
|
+
"ilike",
|
|
2266
|
+
"tsMatch"
|
|
2267
|
+
]);
|
|
2268
|
+
function buildInClause(column, values, params, negated = false) {
|
|
2220
2269
|
if (values.length === 0) {
|
|
2221
|
-
return "1 = 0";
|
|
2270
|
+
return negated ? "1 = 1" : "1 = 0";
|
|
2222
2271
|
}
|
|
2223
2272
|
const placeholders = values.map((value) => pushParam(params, value)).join(", ");
|
|
2224
|
-
return `${column} IN (${placeholders})`;
|
|
2273
|
+
return `${column}${negated ? " NOT" : ""} IN (${placeholders})`;
|
|
2225
2274
|
}
|
|
2226
2275
|
function buildOperatorClauses(column, operator, params) {
|
|
2227
2276
|
const clauses = [];
|
|
2277
|
+
const unsupported = Object.keys(operator).filter((key) => !SUPPORTED_OPERATORS.has(key));
|
|
2278
|
+
if (unsupported.length > 0) {
|
|
2279
|
+
throw new Error(`Unsupported query operator(s) for ${column}: ${unsupported.join(", ")}. Supported: ${[...SUPPORTED_OPERATORS].join(", ")}.`);
|
|
2280
|
+
}
|
|
2228
2281
|
if (operator.isNull === true) {
|
|
2229
2282
|
clauses.push(`${column} IS NULL`);
|
|
2230
2283
|
}
|
|
@@ -2238,9 +2291,19 @@ function buildOperatorClauses(column, operator, params) {
|
|
|
2238
2291
|
clauses.push(`${column} = ${pushParam(params, operator.eq)}`);
|
|
2239
2292
|
}
|
|
2240
2293
|
}
|
|
2294
|
+
if (operator.ne !== undefined) {
|
|
2295
|
+
if (operator.ne === null) {
|
|
2296
|
+
clauses.push(`${column} IS NOT NULL`);
|
|
2297
|
+
} else {
|
|
2298
|
+
clauses.push(`${column} <> ${pushParam(params, operator.ne)}`);
|
|
2299
|
+
}
|
|
2300
|
+
}
|
|
2241
2301
|
if (operator.in !== undefined) {
|
|
2242
2302
|
clauses.push(buildInClause(column, operator.in, params));
|
|
2243
2303
|
}
|
|
2304
|
+
if (operator.notIn !== undefined) {
|
|
2305
|
+
clauses.push(buildInClause(column, operator.notIn, params, true));
|
|
2306
|
+
}
|
|
2244
2307
|
if (operator.gt !== undefined) {
|
|
2245
2308
|
clauses.push(`${column} > ${pushParam(params, operator.gt)}`);
|
|
2246
2309
|
}
|
|
@@ -2460,6 +2523,10 @@ function buildSelectList(table, select, params = []) {
|
|
|
2460
2523
|
if (item.kind === "literalText") {
|
|
2461
2524
|
return `${currentSqlDialect().castToText(pushParam(params, item.value))} AS ${quoteIdentifier(item.as)}`;
|
|
2462
2525
|
}
|
|
2526
|
+
if (item.kind === "subqueryCount") {
|
|
2527
|
+
const body = remapExistsSql(item.sql, item.params, params);
|
|
2528
|
+
return `(${body}) AS ${quoteIdentifier(item.as)}`;
|
|
2529
|
+
}
|
|
2463
2530
|
const column = qualifyColumn(item.table, item.column);
|
|
2464
2531
|
const placeholder = pushParam(params, item.query);
|
|
2465
2532
|
return `ts_rank(${column}, plainto_tsquery('english', ${placeholder})) AS ${quoteIdentifier(item.as)}`;
|
|
@@ -2554,6 +2621,52 @@ function buildInsertQuery(table, values) {
|
|
|
2554
2621
|
params
|
|
2555
2622
|
};
|
|
2556
2623
|
}
|
|
2624
|
+
function buildUpsertQuery(table, values, conflictColumns, updateColumns) {
|
|
2625
|
+
const entries = getDefinedColumnEntries(table, values);
|
|
2626
|
+
if (entries.length === 0) {
|
|
2627
|
+
throw new Error(`Cannot upsert into ${table.name} without any column values.`);
|
|
2628
|
+
}
|
|
2629
|
+
if (conflictColumns.length === 0) {
|
|
2630
|
+
throw new Error(`Cannot upsert into ${table.name} without any conflict columns.`);
|
|
2631
|
+
}
|
|
2632
|
+
const insertable = new Set(entries.map(([column]) => column));
|
|
2633
|
+
const conflict = new Set(conflictColumns);
|
|
2634
|
+
const updatable = (updateColumns ?? entries.map(([column]) => column).filter((c) => !conflict.has(c))).filter((column) => insertable.has(column));
|
|
2635
|
+
const params = [];
|
|
2636
|
+
const columns = entries.map(([column]) => quoteIdentifier(column)).join(", ");
|
|
2637
|
+
const placeholders = entries.map(([, value]) => pushParam(params, value)).join(", ");
|
|
2638
|
+
const returningColumns = buildReturningColumns(table);
|
|
2639
|
+
const suffix = currentSqlDialect().upsertSuffix(conflictColumns, updatable);
|
|
2640
|
+
return {
|
|
2641
|
+
text: `INSERT INTO ${quoteIdentifier(table.name)} (${columns}) VALUES (${placeholders})${suffix}${returningSuffix(returningColumns)}`,
|
|
2642
|
+
params
|
|
2643
|
+
};
|
|
2644
|
+
}
|
|
2645
|
+
function buildIncrementQuery(table, id, column, amount, extra = {}) {
|
|
2646
|
+
if (!Number.isFinite(amount)) {
|
|
2647
|
+
throw new Error("Increment amount must be a finite number.");
|
|
2648
|
+
}
|
|
2649
|
+
if (!table.columns.includes(column)) {
|
|
2650
|
+
throw new Error(`Unknown column ${String(column)} on ${table.name}.`);
|
|
2651
|
+
}
|
|
2652
|
+
const params = [];
|
|
2653
|
+
const target = quoteIdentifier(column);
|
|
2654
|
+
const assignments = [`${target} = ${target} + ${pushParam(params, amount)}`];
|
|
2655
|
+
for (const [name, value] of getDefinedColumnEntries(table, extra, {
|
|
2656
|
+
exclude: [table.primaryKey, column]
|
|
2657
|
+
})) {
|
|
2658
|
+
assignments.push(`${quoteIdentifier(name)} = ${pushParam(params, value)}`);
|
|
2659
|
+
}
|
|
2660
|
+
const primaryKeyPlaceholder = pushParam(params, id);
|
|
2661
|
+
const returningColumns = buildReturningColumns(table);
|
|
2662
|
+
const scopeClauses = [];
|
|
2663
|
+
appendSoftDeleteScope(table, {}, scopeClauses);
|
|
2664
|
+
const scopeSuffix = scopeClauses.length > 0 ? ` AND ${scopeClauses.join(" AND ")}` : "";
|
|
2665
|
+
return {
|
|
2666
|
+
text: `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")} WHERE ${quoteIdentifier(table.primaryKey)} = ${primaryKeyPlaceholder}${scopeSuffix}${returningSuffix(returningColumns)}`,
|
|
2667
|
+
params
|
|
2668
|
+
};
|
|
2669
|
+
}
|
|
2557
2670
|
function buildUpdateQuery(table, id, changes) {
|
|
2558
2671
|
const entries = getDefinedColumnEntries(table, changes, {
|
|
2559
2672
|
exclude: [table.primaryKey]
|
|
@@ -2958,6 +3071,22 @@ class RepositoryQuery {
|
|
|
2958
3071
|
whereNotNull(column) {
|
|
2959
3072
|
return this.where({ [column]: { isNull: false } });
|
|
2960
3073
|
}
|
|
3074
|
+
whereNotIn(column, values) {
|
|
3075
|
+
return this.where({ [column]: { notIn: values } });
|
|
3076
|
+
}
|
|
3077
|
+
withSubqueryCount(alias, sql, params = []) {
|
|
3078
|
+
const table = this.repository.getTable();
|
|
3079
|
+
const existing = this.queryOptions.select ?? table.columns.map((column) => ({
|
|
3080
|
+
kind: "column",
|
|
3081
|
+
table: table.name,
|
|
3082
|
+
column
|
|
3083
|
+
}));
|
|
3084
|
+
this.queryOptions = {
|
|
3085
|
+
...this.queryOptions,
|
|
3086
|
+
select: [...existing, { kind: "subqueryCount", sql, params, as: alias }]
|
|
3087
|
+
};
|
|
3088
|
+
return this;
|
|
3089
|
+
}
|
|
2961
3090
|
whereIn(column, values) {
|
|
2962
3091
|
return this.where({ [column]: values });
|
|
2963
3092
|
}
|
|
@@ -3266,6 +3395,29 @@ class BaseRepository {
|
|
|
3266
3395
|
offset += count;
|
|
3267
3396
|
}
|
|
3268
3397
|
}
|
|
3398
|
+
async chunkById(count, callback, options = {}) {
|
|
3399
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
3400
|
+
throw new Error("Chunk size must be a positive integer.");
|
|
3401
|
+
}
|
|
3402
|
+
let cursor;
|
|
3403
|
+
while (true) {
|
|
3404
|
+
const { data, meta } = await this.cursorPaginate({
|
|
3405
|
+
...options,
|
|
3406
|
+
perPage: count,
|
|
3407
|
+
...cursor === undefined ? {} : { cursor }
|
|
3408
|
+
});
|
|
3409
|
+
if (data.length === 0) {
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
if (await callback(data) === false) {
|
|
3413
|
+
return;
|
|
3414
|
+
}
|
|
3415
|
+
if (!meta.has_more || meta.next_cursor === null) {
|
|
3416
|
+
return;
|
|
3417
|
+
}
|
|
3418
|
+
cursor = meta.next_cursor;
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3269
3421
|
async cursorPaginate(options) {
|
|
3270
3422
|
const {
|
|
3271
3423
|
perPage,
|
|
@@ -3329,6 +3481,23 @@ class BaseRepository {
|
|
|
3329
3481
|
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
3330
3482
|
return record ?? null;
|
|
3331
3483
|
}
|
|
3484
|
+
async upsert(values, conflictColumns, updateColumns) {
|
|
3485
|
+
return await withDatabaseErrorHandling(async () => {
|
|
3486
|
+
const { text, params } = buildUpsertQuery(this.table, values, conflictColumns, updateColumns);
|
|
3487
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
3488
|
+
return record ?? null;
|
|
3489
|
+
});
|
|
3490
|
+
}
|
|
3491
|
+
async incrementById(id, column, amount = 1, extra = {}) {
|
|
3492
|
+
return await withDatabaseErrorHandling(async () => {
|
|
3493
|
+
const { text, params } = buildIncrementQuery(this.table, id, column, amount, extra);
|
|
3494
|
+
const [record] = await this.connection.unsafe(text, params);
|
|
3495
|
+
return record ?? null;
|
|
3496
|
+
});
|
|
3497
|
+
}
|
|
3498
|
+
async decrementById(id, column, amount = 1, extra = {}) {
|
|
3499
|
+
return await this.incrementById(id, column, -amount, extra);
|
|
3500
|
+
}
|
|
3332
3501
|
async create(values) {
|
|
3333
3502
|
return await withDatabaseErrorHandling(async () => {
|
|
3334
3503
|
const { text, params } = buildInsertQuery(this.table, values);
|
|
@@ -3431,7 +3600,23 @@ class BaseRepository {
|
|
|
3431
3600
|
async averageExpression(expression, alias, where = {}) {
|
|
3432
3601
|
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
3433
3602
|
const [row] = await this.connection.unsafe(text, params);
|
|
3434
|
-
return
|
|
3603
|
+
return Number(row?.[alias] ?? 0);
|
|
3604
|
+
}
|
|
3605
|
+
async sum(column, where = {}) {
|
|
3606
|
+
return await this.aggregateColumn("SUM", column, where);
|
|
3607
|
+
}
|
|
3608
|
+
async avg(column, where = {}) {
|
|
3609
|
+
return await this.aggregateColumn("AVG", column, where);
|
|
3610
|
+
}
|
|
3611
|
+
async min(column, where = {}) {
|
|
3612
|
+
return await this.aggregateColumn("MIN", column, where);
|
|
3613
|
+
}
|
|
3614
|
+
async max(column, where = {}) {
|
|
3615
|
+
return await this.aggregateColumn("MAX", column, where);
|
|
3616
|
+
}
|
|
3617
|
+
async aggregateColumn(fn, column, where) {
|
|
3618
|
+
const qualifiedColumn = qualifyColumn(this.table.name, column);
|
|
3619
|
+
return await this.averageExpression(`${fn}(${qualifiedColumn})`, "value", where);
|
|
3435
3620
|
}
|
|
3436
3621
|
async pluckNumberValues(expression, alias, options = {}) {
|
|
3437
3622
|
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
@@ -4711,6 +4896,17 @@ function ensureBooted(model) {
|
|
|
4711
4896
|
function getGlobalScopes(model) {
|
|
4712
4897
|
return modelGlobalScopes.get(model) ?? [];
|
|
4713
4898
|
}
|
|
4899
|
+
var PASSWORD_HASH_PATTERN = /^\$(?:2[aby]?|argon2(?:i|d|id)?)\$/;
|
|
4900
|
+
function isAlreadyHashed(value) {
|
|
4901
|
+
return PASSWORD_HASH_PATTERN.test(value);
|
|
4902
|
+
}
|
|
4903
|
+
function hashCastValue(value) {
|
|
4904
|
+
const plain = String(value);
|
|
4905
|
+
if (isAlreadyHashed(plain)) {
|
|
4906
|
+
return plain;
|
|
4907
|
+
}
|
|
4908
|
+
return Bun.password.hashSync(plain, { algorithm: "bcrypt", cost: 10 });
|
|
4909
|
+
}
|
|
4714
4910
|
function hydrateValue(value, cast) {
|
|
4715
4911
|
if (value === null || value === undefined) {
|
|
4716
4912
|
return value;
|
|
@@ -4750,12 +4946,15 @@ function dehydrateValue(value, cast) {
|
|
|
4750
4946
|
case "int":
|
|
4751
4947
|
return value === "" ? null : Number(value);
|
|
4752
4948
|
case "hashed":
|
|
4753
|
-
return value;
|
|
4949
|
+
return hashCastValue(value);
|
|
4754
4950
|
default:
|
|
4755
4951
|
return value;
|
|
4756
4952
|
}
|
|
4757
4953
|
}
|
|
4758
4954
|
function filterMassAssignable(fillable, guarded, input) {
|
|
4955
|
+
if (fillable === undefined && guarded === undefined && Object.keys(input).length > 0) {
|
|
4956
|
+
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.");
|
|
4957
|
+
}
|
|
4759
4958
|
const resolvedGuarded = guarded ?? true;
|
|
4760
4959
|
if (fillable && fillable.length > 0) {
|
|
4761
4960
|
const allowed = new Set(fillable);
|
|
@@ -4857,10 +5056,38 @@ class ModelQuery {
|
|
|
4857
5056
|
this.query.whereNull(column);
|
|
4858
5057
|
return this;
|
|
4859
5058
|
}
|
|
5059
|
+
whereNotNull(column) {
|
|
5060
|
+
this.query.whereNotNull(column);
|
|
5061
|
+
return this;
|
|
5062
|
+
}
|
|
4860
5063
|
whereIn(column, values) {
|
|
4861
5064
|
this.query.whereIn(column, values);
|
|
4862
5065
|
return this;
|
|
4863
5066
|
}
|
|
5067
|
+
whereNotIn(column, values) {
|
|
5068
|
+
this.query.whereNotIn(column, values);
|
|
5069
|
+
return this;
|
|
5070
|
+
}
|
|
5071
|
+
groupBy(groupBy) {
|
|
5072
|
+
this.query.groupBy(groupBy);
|
|
5073
|
+
return this;
|
|
5074
|
+
}
|
|
5075
|
+
having(having) {
|
|
5076
|
+
this.query.having(having);
|
|
5077
|
+
return this;
|
|
5078
|
+
}
|
|
5079
|
+
join(left, right) {
|
|
5080
|
+
this.query.join(left, right);
|
|
5081
|
+
return this;
|
|
5082
|
+
}
|
|
5083
|
+
leftJoin(left, right) {
|
|
5084
|
+
this.query.leftJoin(left, right);
|
|
5085
|
+
return this;
|
|
5086
|
+
}
|
|
5087
|
+
async paginate(options) {
|
|
5088
|
+
const { data, meta } = await this.query.paginate(options);
|
|
5089
|
+
return { data: await this.hydrateRows(data), meta };
|
|
5090
|
+
}
|
|
4864
5091
|
whereExists(sql, params = []) {
|
|
4865
5092
|
this.query.whereExists(sql, params);
|
|
4866
5093
|
return this;
|
|
@@ -4918,8 +5145,10 @@ class ModelQuery {
|
|
|
4918
5145
|
return this;
|
|
4919
5146
|
}
|
|
4920
5147
|
async get() {
|
|
5148
|
+
return await this.hydrateRows(await this.query.get());
|
|
5149
|
+
}
|
|
5150
|
+
async hydrateRows(rows) {
|
|
4921
5151
|
const statics = modelStatics(this.modelClass);
|
|
4922
|
-
const rows = await this.query.get();
|
|
4923
5152
|
const models = [];
|
|
4924
5153
|
for (const row of rows) {
|
|
4925
5154
|
const model = statics.newFromRecord(row, true);
|
|
@@ -4971,6 +5200,23 @@ class ModelQuery {
|
|
|
4971
5200
|
then(onfulfilled, onrejected) {
|
|
4972
5201
|
return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
4973
5202
|
}
|
|
5203
|
+
withCount(name, alias = `${name}_count`) {
|
|
5204
|
+
const statics = modelStatics(this.modelClass);
|
|
5205
|
+
ensureBooted(this.modelClass);
|
|
5206
|
+
const repository = resolveModelRepository(this.modelClass);
|
|
5207
|
+
const dummy = statics.newFromRecord({});
|
|
5208
|
+
const method = dummy[name];
|
|
5209
|
+
if (typeof method !== "function") {
|
|
5210
|
+
throw new Error(`${this.modelClass.name} has no relation method ${name}().`);
|
|
5211
|
+
}
|
|
5212
|
+
const relationQuery = method.call(dummy);
|
|
5213
|
+
const exists = relationQuery.toExistsClause(repository.getTable().name);
|
|
5214
|
+
if (!exists.sql.startsWith("SELECT 1 ")) {
|
|
5215
|
+
throw new Error(`Cannot count relation ${name}: unexpected subquery shape.`);
|
|
5216
|
+
}
|
|
5217
|
+
this.query.withSubqueryCount(alias, `SELECT COUNT(*) ${exists.sql.slice("SELECT 1 ".length)}`, exists.params);
|
|
5218
|
+
return this;
|
|
5219
|
+
}
|
|
4974
5220
|
constrainExists(name, constrain, not) {
|
|
4975
5221
|
const statics = modelStatics(this.modelClass);
|
|
4976
5222
|
ensureBooted(this.modelClass);
|
|
@@ -5229,11 +5475,23 @@ class Model {
|
|
|
5229
5475
|
return modelStatics(this).newFromRecord({ ...where, ...values }, false);
|
|
5230
5476
|
}
|
|
5231
5477
|
static async firstOrCreate(where, values = {}) {
|
|
5232
|
-
const
|
|
5478
|
+
const findExisting = () => Model.firstWhere.call(this, where);
|
|
5479
|
+
const existing = await findExisting();
|
|
5233
5480
|
if (existing) {
|
|
5234
5481
|
return existing;
|
|
5235
5482
|
}
|
|
5236
|
-
|
|
5483
|
+
try {
|
|
5484
|
+
return await Model.create.call(this, { ...where, ...values });
|
|
5485
|
+
} catch (error) {
|
|
5486
|
+
if (!(error instanceof ConflictError)) {
|
|
5487
|
+
throw error;
|
|
5488
|
+
}
|
|
5489
|
+
const raced = await findExisting();
|
|
5490
|
+
if (!raced) {
|
|
5491
|
+
throw error;
|
|
5492
|
+
}
|
|
5493
|
+
return raced;
|
|
5494
|
+
}
|
|
5237
5495
|
}
|
|
5238
5496
|
static async updateOrCreate(where, values = {}) {
|
|
5239
5497
|
const existing = await Model.firstWhere.call(this, where);
|
|
@@ -7603,7 +7861,7 @@ function normalizeMimeType(mimeType) {
|
|
|
7603
7861
|
function isAllowedMimeType(mimeType) {
|
|
7604
7862
|
const normalized = normalizeMimeType(mimeType);
|
|
7605
7863
|
if (!normalized || normalized === "application/octet-stream") {
|
|
7606
|
-
return
|
|
7864
|
+
return envFlagEnabled(process.env.UPLOAD_ALLOW_UNKNOWN_MIME);
|
|
7607
7865
|
}
|
|
7608
7866
|
return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
|
|
7609
7867
|
}
|