@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
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]
|
|
@@ -2857,6 +2970,30 @@ function indexMorphToRelation(children, parentsByType, relation) {
|
|
|
2857
2970
|
return result;
|
|
2858
2971
|
}
|
|
2859
2972
|
|
|
2973
|
+
// ../../src/core/database/pluck.ts
|
|
2974
|
+
function uniqueColumnSelect(table, columns) {
|
|
2975
|
+
const seen = new Set;
|
|
2976
|
+
const select = [];
|
|
2977
|
+
for (const column of columns) {
|
|
2978
|
+
if (seen.has(column)) {
|
|
2979
|
+
continue;
|
|
2980
|
+
}
|
|
2981
|
+
seen.add(column);
|
|
2982
|
+
select.push({ kind: "column", table, column, as: column });
|
|
2983
|
+
}
|
|
2984
|
+
return select;
|
|
2985
|
+
}
|
|
2986
|
+
function projectPluck(rows, column, keyBy) {
|
|
2987
|
+
if (keyBy === undefined) {
|
|
2988
|
+
return rows.map((row) => row[column]);
|
|
2989
|
+
}
|
|
2990
|
+
const keyed = new Map;
|
|
2991
|
+
for (const row of rows) {
|
|
2992
|
+
keyed.set(row[keyBy], row[column]);
|
|
2993
|
+
}
|
|
2994
|
+
return keyed;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2860
2997
|
// ../../src/core/database/whereBuilder.ts
|
|
2861
2998
|
class WhereBuilder {
|
|
2862
2999
|
nodes = [];
|
|
@@ -2934,6 +3071,22 @@ class RepositoryQuery {
|
|
|
2934
3071
|
whereNotNull(column) {
|
|
2935
3072
|
return this.where({ [column]: { isNull: false } });
|
|
2936
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
|
+
}
|
|
2937
3090
|
whereIn(column, values) {
|
|
2938
3091
|
return this.where({ [column]: values });
|
|
2939
3092
|
}
|
|
@@ -3054,6 +3207,28 @@ class RepositoryQuery {
|
|
|
3054
3207
|
async count() {
|
|
3055
3208
|
return await this.repository.count(this.buildOptions());
|
|
3056
3209
|
}
|
|
3210
|
+
async pluck(column, keyBy) {
|
|
3211
|
+
const rows = await this.repository.findAll({
|
|
3212
|
+
...this.buildOptions(),
|
|
3213
|
+
select: uniqueColumnSelect(this.repository.getTable().name, keyBy === undefined || keyBy === column ? [column] : [column, keyBy])
|
|
3214
|
+
});
|
|
3215
|
+
if (keyBy === undefined) {
|
|
3216
|
+
return projectPluck(rows, column);
|
|
3217
|
+
}
|
|
3218
|
+
return projectPluck(rows, column, keyBy);
|
|
3219
|
+
}
|
|
3220
|
+
async value(column) {
|
|
3221
|
+
const rows = await this.repository.findAll({
|
|
3222
|
+
...this.buildOptions(),
|
|
3223
|
+
select: uniqueColumnSelect(this.repository.getTable().name, [column]),
|
|
3224
|
+
limit: 1
|
|
3225
|
+
});
|
|
3226
|
+
const row = rows[0];
|
|
3227
|
+
if (row === undefined) {
|
|
3228
|
+
return null;
|
|
3229
|
+
}
|
|
3230
|
+
return row[column];
|
|
3231
|
+
}
|
|
3057
3232
|
async attachToRows(rows) {
|
|
3058
3233
|
return await this.attach(rows);
|
|
3059
3234
|
}
|
|
@@ -3220,6 +3395,29 @@ class BaseRepository {
|
|
|
3220
3395
|
offset += count;
|
|
3221
3396
|
}
|
|
3222
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
|
+
}
|
|
3223
3421
|
async cursorPaginate(options) {
|
|
3224
3422
|
const {
|
|
3225
3423
|
perPage,
|
|
@@ -3283,6 +3481,23 @@ class BaseRepository {
|
|
|
3283
3481
|
const [record] = await this.findAll({ ...options, where, limit: 1 });
|
|
3284
3482
|
return record ?? null;
|
|
3285
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
|
+
}
|
|
3286
3501
|
async create(values) {
|
|
3287
3502
|
return await withDatabaseErrorHandling(async () => {
|
|
3288
3503
|
const { text, params } = buildInsertQuery(this.table, values);
|
|
@@ -3385,7 +3600,23 @@ class BaseRepository {
|
|
|
3385
3600
|
async averageExpression(expression, alias, where = {}) {
|
|
3386
3601
|
const { text, params } = buildProjectionQuery(this.table, expression, alias, { where });
|
|
3387
3602
|
const [row] = await this.connection.unsafe(text, params);
|
|
3388
|
-
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);
|
|
3389
3620
|
}
|
|
3390
3621
|
async pluckNumberValues(expression, alias, options = {}) {
|
|
3391
3622
|
const { text, params } = buildProjectionQuery(this.table, expression, alias, options);
|
|
@@ -3882,6 +4113,12 @@ function ownerId(owner, ownerKey) {
|
|
|
3882
4113
|
function thenGet(get, onfulfilled, onrejected) {
|
|
3883
4114
|
return get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
3884
4115
|
}
|
|
4116
|
+
function pluckFromQuery(query, column, keyBy) {
|
|
4117
|
+
if (!query) {
|
|
4118
|
+
return Promise.resolve(keyBy === undefined ? [] : new Map);
|
|
4119
|
+
}
|
|
4120
|
+
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
4121
|
+
}
|
|
3885
4122
|
|
|
3886
4123
|
class HasManyRelationQuery {
|
|
3887
4124
|
parent;
|
|
@@ -3947,6 +4184,12 @@ class HasManyRelationQuery {
|
|
|
3947
4184
|
async count() {
|
|
3948
4185
|
return this.scopedQuery().count();
|
|
3949
4186
|
}
|
|
4187
|
+
async pluck(column, keyBy) {
|
|
4188
|
+
return pluckFromQuery(this.scopedQuery(), column, keyBy);
|
|
4189
|
+
}
|
|
4190
|
+
async value(column) {
|
|
4191
|
+
return this.scopedQuery().value(column);
|
|
4192
|
+
}
|
|
3950
4193
|
then(onfulfilled, onrejected) {
|
|
3951
4194
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
3952
4195
|
}
|
|
@@ -4012,6 +4255,13 @@ class HasOneRelationQuery {
|
|
|
4012
4255
|
async count() {
|
|
4013
4256
|
return this.inner.count();
|
|
4014
4257
|
}
|
|
4258
|
+
async pluck(column, keyBy) {
|
|
4259
|
+
const query = this.inner.limit(1);
|
|
4260
|
+
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
4261
|
+
}
|
|
4262
|
+
async value(column) {
|
|
4263
|
+
return this.inner.limit(1).value(column);
|
|
4264
|
+
}
|
|
4015
4265
|
then(onfulfilled, onrejected) {
|
|
4016
4266
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
4017
4267
|
}
|
|
@@ -4058,6 +4308,23 @@ class BelongsToRelationQuery {
|
|
|
4058
4308
|
return { sql, params: extra.params };
|
|
4059
4309
|
}
|
|
4060
4310
|
async get() {
|
|
4311
|
+
const query = this.relatedQuery();
|
|
4312
|
+
if (!query) {
|
|
4313
|
+
return null;
|
|
4314
|
+
}
|
|
4315
|
+
const row = await query.first();
|
|
4316
|
+
return row ? this.related.newFromRecord(row) : null;
|
|
4317
|
+
}
|
|
4318
|
+
async first() {
|
|
4319
|
+
return this.get();
|
|
4320
|
+
}
|
|
4321
|
+
async pluck(column, keyBy) {
|
|
4322
|
+
return pluckFromQuery(this.relatedQuery(), column, keyBy);
|
|
4323
|
+
}
|
|
4324
|
+
async value(column) {
|
|
4325
|
+
return this.relatedQuery()?.value(column) ?? null;
|
|
4326
|
+
}
|
|
4327
|
+
relatedQuery() {
|
|
4061
4328
|
const foreign = this.parent.get(this.relation.foreignKey);
|
|
4062
4329
|
if (foreign === null || foreign === undefined) {
|
|
4063
4330
|
return null;
|
|
@@ -4067,11 +4334,7 @@ class BelongsToRelationQuery {
|
|
|
4067
4334
|
if (this.extraOptions.orderBy) {
|
|
4068
4335
|
query = query.orderBy(this.extraOptions.orderBy);
|
|
4069
4336
|
}
|
|
4070
|
-
|
|
4071
|
-
return row ? this.related.newFromRecord(row) : null;
|
|
4072
|
-
}
|
|
4073
|
-
async first() {
|
|
4074
|
-
return this.get();
|
|
4337
|
+
return query;
|
|
4075
4338
|
}
|
|
4076
4339
|
then(onfulfilled, onrejected) {
|
|
4077
4340
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -4159,6 +4422,31 @@ class BelongsToManyRelationQuery {
|
|
|
4159
4422
|
const rows = await this.connection().unsafe(`SELECT COUNT(*) AS count FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
4160
4423
|
return Number(rows[0]?.count ?? 0);
|
|
4161
4424
|
}
|
|
4425
|
+
async pluck(column, keyBy) {
|
|
4426
|
+
return pluckFromQuery(await this.relatedQuery(), column, keyBy);
|
|
4427
|
+
}
|
|
4428
|
+
async value(column) {
|
|
4429
|
+
const query = await this.relatedQuery();
|
|
4430
|
+
return query ? query.value(column) : null;
|
|
4431
|
+
}
|
|
4432
|
+
async relatedQuery() {
|
|
4433
|
+
const parentId = this.parent.get(this.relation.parentKey);
|
|
4434
|
+
const pivotRows = await this.connection().unsafe(`SELECT * FROM ${this.relation.pivotTable} WHERE ${String(this.relation.foreignPivotKey)} = $1`, [parentId]);
|
|
4435
|
+
if (pivotRows.length === 0) {
|
|
4436
|
+
return null;
|
|
4437
|
+
}
|
|
4438
|
+
const relatedIds = [
|
|
4439
|
+
...new Set(pivotRows.map((row) => row[this.relation.relatedPivotKey]))
|
|
4440
|
+
];
|
|
4441
|
+
let query = this.related.repository().withConnection(this.connection()).query(asWhere({
|
|
4442
|
+
[this.relation.relatedKey]: relatedIds,
|
|
4443
|
+
...this.extraWhere
|
|
4444
|
+
}));
|
|
4445
|
+
if (this.extraOptions.orderBy) {
|
|
4446
|
+
query = query.orderBy(this.extraOptions.orderBy);
|
|
4447
|
+
}
|
|
4448
|
+
return query;
|
|
4449
|
+
}
|
|
4162
4450
|
then(onfulfilled, onrejected) {
|
|
4163
4451
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
4164
4452
|
}
|
|
@@ -4242,13 +4530,15 @@ class MorphManyRelationQuery {
|
|
|
4242
4530
|
const sql = `SELECT 1 FROM ${quoteIdentifier(childTable)} WHERE ${qualifyColumn(childTable, this.relation.morphIdKey)} = ${qualifyColumn(parentTable, this.relation.localKey)}${extraSql ? ` AND ${extraSql}` : ""}`;
|
|
4243
4531
|
return { sql, params: extra.params };
|
|
4244
4532
|
}
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
const rows = await repository.query(asWhere({
|
|
4533
|
+
scopedQuery() {
|
|
4534
|
+
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({
|
|
4248
4535
|
[this.relation.morphTypeKey]: this.relation.morphType,
|
|
4249
4536
|
[this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
|
|
4250
4537
|
...this.extraWhere
|
|
4251
|
-
}))
|
|
4538
|
+
}));
|
|
4539
|
+
}
|
|
4540
|
+
async get() {
|
|
4541
|
+
const rows = await this.scopedQuery().get();
|
|
4252
4542
|
return rows.map((row) => this.related.newFromRecord(row));
|
|
4253
4543
|
}
|
|
4254
4544
|
async first() {
|
|
@@ -4256,12 +4546,13 @@ class MorphManyRelationQuery {
|
|
|
4256
4546
|
return rows[0] ?? null;
|
|
4257
4547
|
}
|
|
4258
4548
|
async count() {
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4549
|
+
return this.scopedQuery().count();
|
|
4550
|
+
}
|
|
4551
|
+
async pluck(column, keyBy) {
|
|
4552
|
+
return pluckFromQuery(this.scopedQuery(), column, keyBy);
|
|
4553
|
+
}
|
|
4554
|
+
async value(column) {
|
|
4555
|
+
return this.scopedQuery().value(column);
|
|
4265
4556
|
}
|
|
4266
4557
|
then(onfulfilled, onrejected) {
|
|
4267
4558
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -4312,6 +4603,12 @@ class MorphOneRelationQuery {
|
|
|
4312
4603
|
async count() {
|
|
4313
4604
|
return this.inner.count();
|
|
4314
4605
|
}
|
|
4606
|
+
async pluck(column, keyBy) {
|
|
4607
|
+
return keyBy === undefined ? this.inner.pluck(column) : this.inner.pluck(column, keyBy);
|
|
4608
|
+
}
|
|
4609
|
+
async value(column) {
|
|
4610
|
+
return this.inner.value(column);
|
|
4611
|
+
}
|
|
4315
4612
|
then(onfulfilled, onrejected) {
|
|
4316
4613
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
4317
4614
|
}
|
|
@@ -4360,15 +4657,31 @@ class MorphToRelationQuery {
|
|
|
4360
4657
|
};
|
|
4361
4658
|
}
|
|
4362
4659
|
async get() {
|
|
4660
|
+
const query = this.relatedQuery();
|
|
4661
|
+
if (!query) {
|
|
4662
|
+
return null;
|
|
4663
|
+
}
|
|
4664
|
+
const row = await query.first();
|
|
4665
|
+
return row ? this.relatedForCurrentType()?.newFromRecord(row) ?? null : null;
|
|
4666
|
+
}
|
|
4667
|
+
async pluck(column, keyBy) {
|
|
4668
|
+
return pluckFromQuery(this.relatedQuery(), column, keyBy);
|
|
4669
|
+
}
|
|
4670
|
+
async value(column) {
|
|
4671
|
+
return this.relatedQuery()?.value(column) ?? null;
|
|
4672
|
+
}
|
|
4673
|
+
relatedForCurrentType() {
|
|
4363
4674
|
const type = String(this.parent.get(this.relation.morphTypeKey) ?? "");
|
|
4675
|
+
return this.relatedByType[type];
|
|
4676
|
+
}
|
|
4677
|
+
relatedQuery() {
|
|
4364
4678
|
const id = this.parent.get(this.relation.morphIdKey);
|
|
4365
|
-
const related = this.
|
|
4679
|
+
const related = this.relatedForCurrentType();
|
|
4366
4680
|
if (!related || id === null || id === undefined) {
|
|
4367
4681
|
return null;
|
|
4368
4682
|
}
|
|
4369
4683
|
const table = related.repository().getTable();
|
|
4370
|
-
|
|
4371
|
-
return row ? related.newFromRecord(row) : null;
|
|
4684
|
+
return related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere }));
|
|
4372
4685
|
}
|
|
4373
4686
|
then(onfulfilled, onrejected) {
|
|
4374
4687
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
@@ -4415,10 +4728,7 @@ class HasManyThroughRelationQuery {
|
|
|
4415
4728
|
return { sql, params: extra.params };
|
|
4416
4729
|
}
|
|
4417
4730
|
async get() {
|
|
4418
|
-
const rows = await this.
|
|
4419
|
-
...this.extraOptions,
|
|
4420
|
-
where: this.extraWhere
|
|
4421
|
-
});
|
|
4731
|
+
const rows = await this.farRows();
|
|
4422
4732
|
return rows.map((row) => this.related.newFromRecord(row));
|
|
4423
4733
|
}
|
|
4424
4734
|
async first() {
|
|
@@ -4429,6 +4739,20 @@ class HasManyThroughRelationQuery {
|
|
|
4429
4739
|
const rows = await this.get();
|
|
4430
4740
|
return rows.length;
|
|
4431
4741
|
}
|
|
4742
|
+
async pluck(column, keyBy) {
|
|
4743
|
+
const rows = await this.farRows();
|
|
4744
|
+
return keyBy === undefined ? projectPluck(rows, column) : projectPluck(rows, column, keyBy);
|
|
4745
|
+
}
|
|
4746
|
+
async value(column) {
|
|
4747
|
+
const values = await this.limit(1).pluck(column);
|
|
4748
|
+
return values[0] ?? null;
|
|
4749
|
+
}
|
|
4750
|
+
async farRows() {
|
|
4751
|
+
return this.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
|
|
4752
|
+
...this.extraOptions,
|
|
4753
|
+
where: this.extraWhere
|
|
4754
|
+
});
|
|
4755
|
+
}
|
|
4432
4756
|
then(onfulfilled, onrejected) {
|
|
4433
4757
|
return thenGet(() => this.get(), onfulfilled, onrejected);
|
|
4434
4758
|
}
|
|
@@ -4572,6 +4896,17 @@ function ensureBooted(model) {
|
|
|
4572
4896
|
function getGlobalScopes(model) {
|
|
4573
4897
|
return modelGlobalScopes.get(model) ?? [];
|
|
4574
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
|
+
}
|
|
4575
4910
|
function hydrateValue(value, cast) {
|
|
4576
4911
|
if (value === null || value === undefined) {
|
|
4577
4912
|
return value;
|
|
@@ -4611,12 +4946,15 @@ function dehydrateValue(value, cast) {
|
|
|
4611
4946
|
case "int":
|
|
4612
4947
|
return value === "" ? null : Number(value);
|
|
4613
4948
|
case "hashed":
|
|
4614
|
-
return value;
|
|
4949
|
+
return hashCastValue(value);
|
|
4615
4950
|
default:
|
|
4616
4951
|
return value;
|
|
4617
4952
|
}
|
|
4618
4953
|
}
|
|
4619
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
|
+
}
|
|
4620
4958
|
const resolvedGuarded = guarded ?? true;
|
|
4621
4959
|
if (fillable && fillable.length > 0) {
|
|
4622
4960
|
const allowed = new Set(fillable);
|
|
@@ -4641,6 +4979,10 @@ function applyCasts(values, casts, direction) {
|
|
|
4641
4979
|
}
|
|
4642
4980
|
return result;
|
|
4643
4981
|
}
|
|
4982
|
+
function castPluckedValue(modelClass, column, value) {
|
|
4983
|
+
const cast = modelStatics(modelClass).$casts?.[column];
|
|
4984
|
+
return cast ? hydrateValue(value, cast) : value;
|
|
4985
|
+
}
|
|
4644
4986
|
function applyTimestampsOnCreate(columns, values, enabled) {
|
|
4645
4987
|
if (!enabled) {
|
|
4646
4988
|
return values;
|
|
@@ -4714,10 +5056,38 @@ class ModelQuery {
|
|
|
4714
5056
|
this.query.whereNull(column);
|
|
4715
5057
|
return this;
|
|
4716
5058
|
}
|
|
5059
|
+
whereNotNull(column) {
|
|
5060
|
+
this.query.whereNotNull(column);
|
|
5061
|
+
return this;
|
|
5062
|
+
}
|
|
4717
5063
|
whereIn(column, values) {
|
|
4718
5064
|
this.query.whereIn(column, values);
|
|
4719
5065
|
return this;
|
|
4720
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
|
+
}
|
|
4721
5091
|
whereExists(sql, params = []) {
|
|
4722
5092
|
this.query.whereExists(sql, params);
|
|
4723
5093
|
return this;
|
|
@@ -4775,8 +5145,10 @@ class ModelQuery {
|
|
|
4775
5145
|
return this;
|
|
4776
5146
|
}
|
|
4777
5147
|
async get() {
|
|
5148
|
+
return await this.hydrateRows(await this.query.get());
|
|
5149
|
+
}
|
|
5150
|
+
async hydrateRows(rows) {
|
|
4778
5151
|
const statics = modelStatics(this.modelClass);
|
|
4779
|
-
const rows = await this.query.get();
|
|
4780
5152
|
const models = [];
|
|
4781
5153
|
for (const row of rows) {
|
|
4782
5154
|
const model = statics.newFromRecord(row, true);
|
|
@@ -4795,6 +5167,25 @@ class ModelQuery {
|
|
|
4795
5167
|
const models = await this.get();
|
|
4796
5168
|
return models[0] ?? null;
|
|
4797
5169
|
}
|
|
5170
|
+
async count() {
|
|
5171
|
+
return this.query.count();
|
|
5172
|
+
}
|
|
5173
|
+
async pluck(column, keyBy) {
|
|
5174
|
+
if (keyBy === undefined) {
|
|
5175
|
+
const values = await this.query.pluck(column);
|
|
5176
|
+
return values.map((value) => castPluckedValue(this.modelClass, column, value));
|
|
5177
|
+
}
|
|
5178
|
+
const keyed = await this.query.pluck(column, keyBy);
|
|
5179
|
+
const result = new Map;
|
|
5180
|
+
for (const [key, value] of keyed) {
|
|
5181
|
+
result.set(castPluckedValue(this.modelClass, keyBy, key), castPluckedValue(this.modelClass, column, value));
|
|
5182
|
+
}
|
|
5183
|
+
return result;
|
|
5184
|
+
}
|
|
5185
|
+
async value(column) {
|
|
5186
|
+
const value = await this.query.value(column);
|
|
5187
|
+
return castPluckedValue(this.modelClass, column, value);
|
|
5188
|
+
}
|
|
4798
5189
|
async find(id) {
|
|
4799
5190
|
const primaryKey = resolveModelRepository(this.modelClass).getTable().primaryKey;
|
|
4800
5191
|
return this.where({ [primaryKey]: id }).first();
|
|
@@ -4809,6 +5200,23 @@ class ModelQuery {
|
|
|
4809
5200
|
then(onfulfilled, onrejected) {
|
|
4810
5201
|
return this.get().then(onfulfilled ?? undefined, onrejected ?? undefined);
|
|
4811
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
|
+
}
|
|
4812
5220
|
constrainExists(name, constrain, not) {
|
|
4813
5221
|
const statics = modelStatics(this.modelClass);
|
|
4814
5222
|
ensureBooted(this.modelClass);
|
|
@@ -5042,6 +5450,16 @@ class Model {
|
|
|
5042
5450
|
static where(where) {
|
|
5043
5451
|
return Model.query.call(this).where(where);
|
|
5044
5452
|
}
|
|
5453
|
+
static async count() {
|
|
5454
|
+
return Model.query.call(this).count();
|
|
5455
|
+
}
|
|
5456
|
+
static pluck(column, keyBy) {
|
|
5457
|
+
const query = Model.query.call(this);
|
|
5458
|
+
return keyBy === undefined ? query.pluck(column) : query.pluck(column, keyBy);
|
|
5459
|
+
}
|
|
5460
|
+
static value(column) {
|
|
5461
|
+
return Model.query.call(this).value(column);
|
|
5462
|
+
}
|
|
5045
5463
|
static async firstWhere(where, options = {}) {
|
|
5046
5464
|
let query = Model.query.call(this).where(where);
|
|
5047
5465
|
if (options.orderBy) {
|
|
@@ -5057,11 +5475,23 @@ class Model {
|
|
|
5057
5475
|
return modelStatics(this).newFromRecord({ ...where, ...values }, false);
|
|
5058
5476
|
}
|
|
5059
5477
|
static async firstOrCreate(where, values = {}) {
|
|
5060
|
-
const
|
|
5478
|
+
const findExisting = () => Model.firstWhere.call(this, where);
|
|
5479
|
+
const existing = await findExisting();
|
|
5061
5480
|
if (existing) {
|
|
5062
5481
|
return existing;
|
|
5063
5482
|
}
|
|
5064
|
-
|
|
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
|
+
}
|
|
5065
5495
|
}
|
|
5066
5496
|
static async updateOrCreate(where, values = {}) {
|
|
5067
5497
|
const existing = await Model.firstWhere.call(this, where);
|
|
@@ -7431,7 +7861,7 @@ function normalizeMimeType(mimeType) {
|
|
|
7431
7861
|
function isAllowedMimeType(mimeType) {
|
|
7432
7862
|
const normalized = normalizeMimeType(mimeType);
|
|
7433
7863
|
if (!normalized || normalized === "application/octet-stream") {
|
|
7434
|
-
return
|
|
7864
|
+
return envFlagEnabled(process.env.UPLOAD_ALLOW_UNKNOWN_MIME);
|
|
7435
7865
|
}
|
|
7436
7866
|
return ALLOWED_UPLOAD_MIME_TYPES.has(normalized);
|
|
7437
7867
|
}
|
|
@@ -10183,6 +10613,7 @@ export {
|
|
|
10183
10613
|
parseQualifiedColumn,
|
|
10184
10614
|
pivotTableName,
|
|
10185
10615
|
policyGate,
|
|
10616
|
+
projectPluck,
|
|
10186
10617
|
prometheusRegistry,
|
|
10187
10618
|
qualifyColumn,
|
|
10188
10619
|
queue,
|
|
@@ -10300,6 +10731,7 @@ export {
|
|
|
10300
10731
|
toResourceCollection,
|
|
10301
10732
|
traceContextStorage,
|
|
10302
10733
|
trustForwardedFor,
|
|
10734
|
+
uniqueColumnSelect,
|
|
10303
10735
|
unregisterNamedConnection,
|
|
10304
10736
|
useSqlDialect,
|
|
10305
10737
|
validateObject,
|