@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/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
- function buildInClause(column, values, params) {
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 Math.round(Number(row?.[alias] ?? 0));
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
- const row = await query.first();
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
- async get() {
4246
- const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
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
- })).get();
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
- const repository = this.related.repository().withConnection(this.parent.getRepository().getConnection());
4260
- return repository.query(asWhere({
4261
- [this.relation.morphTypeKey]: this.relation.morphType,
4262
- [this.relation.morphIdKey]: this.parent.get(this.relation.localKey),
4263
- ...this.extraWhere
4264
- })).count();
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.relatedByType[type];
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
- const row = await related.repository().withConnection(this.parent.getRepository().getConnection()).query(asWhere({ [table.primaryKey]: id, ...this.extraWhere })).first();
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.related.repository().withConnection(this.parent.getRepository().getConnection()).findHasManyThrough(this.parent.get(this.relation.localKey), this.relation, {
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 existing = await Model.firstWhere.call(this, where);
5478
+ const findExisting = () => Model.firstWhere.call(this, where);
5479
+ const existing = await findExisting();
5061
5480
  if (existing) {
5062
5481
  return existing;
5063
5482
  }
5064
- return Model.create.call(this, { ...where, ...values });
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 true;
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,