@getstrata/core 2.2.1 → 2.2.2

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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.2
4
+
5
+ - orm improvments
6
+
3
7
  ## 2.2.1
4
8
 
5
9
  F02 — complete queue durability. Queues have leased reservations, recovery and fenced acknowledgement, but still use Redis lists. Persisted retry schedules, execution deadlines, cancellation and the planned Streams migration remain unfinished. Retries currently wait inside the running worker.
@@ -45,6 +45,11 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
45
45
  incrementById(id: TEntity[PrimaryKey], column: keyof TEntity & string, amount?: number, extra?: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
46
46
  decrementById(id: TEntity[PrimaryKey], column: keyof TEntity & string, amount?: number, extra?: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
47
47
  create(values: MutationValues<TEntity>): Promise<TEntity>;
48
+ /** Set-based writes intentionally bypass per-row model observers and write casts. */
49
+ updateWhere(changes: UpdateValues<TEntity, PrimaryKey>, options: QueryOptions<TEntity>): Promise<number>;
50
+ deleteWhere(options: QueryOptions<TEntity>): Promise<number>;
51
+ updateWhereReturning<K extends keyof TEntity & string>(changes: UpdateValues<TEntity, PrimaryKey>, columns: readonly K[], options: QueryOptions<TEntity>): Promise<Pick<TEntity, K>[]>;
52
+ private executeConditionalWrite;
48
53
  updateById(id: TEntity[PrimaryKey], changes: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
49
54
  updateByIdOrThrow(id: TEntity[PrimaryKey], changes: UpdateValues<TEntity, PrimaryKey>, errorFactory?: ErrorFactory<TEntity[PrimaryKey]>): Promise<TEntity>;
50
55
  deleteById(id: TEntity[PrimaryKey]): Promise<boolean>;
@@ -4,7 +4,7 @@ import { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuer
4
4
  import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation } from "./relationships.ts";
5
5
  import type { RepositoryQuery } from "./repositoryQuery.ts";
6
6
  import type { TableDefinition } from "./table.ts";
7
- import type { ModelWhere, ModelWriteValues, QueryOptions, QueryWhere } from "./types.ts";
7
+ import type { ModelWhere, ModelWriteValues, QueryLockOptions, QueryOptions, QueryWhere } from "./types.ts";
8
8
  type CastType = "date" | "datetime" | "json" | "bool" | "boolean" | "integer" | "int" | "hashed";
9
9
  type LoadedAttributes = Record<string, unknown>;
10
10
  type ModelCasts = Partial<Record<string, CastType>>;
@@ -70,6 +70,8 @@ declare class ModelQuery<TModel extends object = AnyModel, TCounts extends strin
70
70
  /** Qualified/dynamic filters retain existing SQL semantics through an explicit escape hatch. */
71
71
  whereDynamic(input: QueryWhere<object>): this;
72
72
  select<K extends keyof ModelAttributes<TModel> & string>(...columns: readonly K[]): ModelProjection<ModelAttributes<TModel>, K>;
73
+ lockForUpdate(options?: QueryLockOptions): this;
74
+ sharedLock(options?: QueryLockOptions): this;
73
75
  orderBy(orderBy: QueryOptions<ModelAttributes<TModel>>["orderBy"]): this;
74
76
  limit(limit: number): this;
75
77
  offset(offset: number): this;
@@ -68,4 +68,9 @@ declare function buildDeleteByIdQuery<TEntity extends object, PrimaryKey extends
68
68
  text: string;
69
69
  params: unknown[];
70
70
  };
71
- export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildIncrementQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
71
+ /** Single-statement writes: predicates and protected scopes stay in the mutation. */
72
+ declare function buildConditionalWriteQuery<TEntity extends object, PrimaryKey extends keyof TEntity & string>(table: TableDefinition<TEntity, PrimaryKey>, operation: "update" | "delete", changes: UpdateValues<TEntity, PrimaryKey>, options: QueryOptions<TEntity>, returning?: readonly (keyof TEntity & string)[]): {
73
+ text: string;
74
+ params: unknown[];
75
+ };
76
+ export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildConditionalWriteQuery, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildIncrementQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
@@ -1,7 +1,7 @@
1
1
  import type { PaginatedResult } from "../pagination/index.ts";
2
2
  import type BaseRepository from "./baseRepository.ts";
3
3
  import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, MorphManyRelation, MorphOneRelation, MorphToRelation } from "./relationships.ts";
4
- import type { QueryOptions, QueryWhere } from "./types.ts";
4
+ import type { QueryLockOptions, QueryOptions, QueryWhere, UpdateValues } from "./types.ts";
5
5
  import { WhereBuilder } from "./whereBuilder.ts";
6
6
  type LoadedRow = Record<string, unknown>;
7
7
  declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof TEntity & string> {
@@ -16,6 +16,8 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
16
16
  /** Preserve completed scope predicates separately from subsequent caller OR conditions. */
17
17
  protectWhere(): this;
18
18
  orWhere(input: QueryWhere<TEntity> | ((builder: WhereBuilder<TEntity>) => void)): this;
19
+ lockForUpdate(options?: QueryLockOptions): this;
20
+ sharedLock(options?: QueryLockOptions): this;
19
21
  orderBy(orderBy: QueryOptions<TEntity>["orderBy"]): this;
20
22
  limit(limit: number): this;
21
23
  whereNull(column: keyof TEntity & string): this;
@@ -39,6 +41,10 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
39
41
  withHasManyThrough<TFar extends object, LocalKey extends keyof TEntity & string, SecondKey extends keyof TFar & string, Alias extends string>(as: Alias, relation: HasManyThroughRelation<TEntity, TFar, LocalKey, string, string, SecondKey>, farRepository: BaseRepository<TFar, keyof TFar & string>, options?: Omit<QueryOptions<TFar>, "where">): this;
40
42
  withTrashed(): this;
41
43
  onlyTrashed(): this;
44
+ update(changes: UpdateValues<TEntity, PrimaryKey>): Promise<number>;
45
+ updateReturning<K extends keyof TEntity & string>(changes: UpdateValues<TEntity, PrimaryKey>, ...columns: K[]): Promise<Pick<TEntity, K>[]>;
46
+ delete(): Promise<number>;
47
+ private assertNoMutationEagerLoads;
42
48
  get(): Promise<Array<TEntity & LoadedRow>>;
43
49
  first(): Promise<(TEntity & LoadedRow) | null>;
44
50
  count(): Promise<number>;
@@ -30,6 +30,8 @@ declare class Blueprint {
30
30
  readonly droppedIndexes: string[];
31
31
  constructor(table: string, action: BlueprintAction);
32
32
  id(name?: string): ColumnDefinition;
33
+ /** Generated 64-bit primary key; does not change existing id() migrations. */
34
+ bigId(name?: string): ColumnDefinition;
33
35
  string(name: string, length?: number): ColumnDefinition;
34
36
  text(name: string): ColumnDefinition;
35
37
  boolean(name: string): ColumnDefinition;
@@ -1,4 +1,4 @@
1
- type ColumnKind = "id" | "string" | "text" | "boolean" | "integer" | "bigInteger" | "timestamp" | "json" | "jsonb" | "foreignId";
1
+ type ColumnKind = "id" | "bigId" | "string" | "text" | "boolean" | "integer" | "bigInteger" | "timestamp" | "json" | "jsonb" | "foreignId";
2
2
  interface ForeignKeyOptions {
3
3
  referencesTable: string;
4
4
  referencesColumn: string;
@@ -78,7 +78,16 @@ type QuerySelectItem = {
78
78
  query: string;
79
79
  as: string;
80
80
  };
81
+ type QueryLockOptions = {
82
+ wait?: "wait" | "nowait" | "skipLocked";
83
+ /** PostgreSQL table names in this query; aliases are not inferred. */
84
+ of?: readonly string[];
85
+ };
86
+ type QueryLock = QueryLockOptions & {
87
+ mode: "update" | "share";
88
+ };
81
89
  interface QueryOptions<TEntity extends object> {
90
+ lock?: QueryLock;
82
91
  whereNodes?: WhereNode<TEntity>[];
83
92
  where?: QueryWhere<TEntity>;
84
93
  orderBy?: QueryOrder<TEntity> | QueryOrder<TEntity>[] | QueryOrderShorthand<TEntity>;
@@ -93,4 +102,4 @@ interface QueryOptions<TEntity extends object> {
93
102
  }
94
103
  type MutationValues<TEntity> = Partial<TEntity>;
95
104
  type UpdateValues<TEntity, PrimaryKey extends keyof TEntity & string = keyof TEntity & string> = Partial<Omit<TEntity, PrimaryKey>>;
96
- export type { DatabaseComparable, DatabaseScalar, ModelWhere, ModelWriteValues, MutationValues, QueryFilterValue, QueryJoin, QueryJoinOn, QueryOperator, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, UpdateValues, };
105
+ export type { DatabaseComparable, DatabaseScalar, ModelWhere, ModelWriteValues, MutationValues, QueryFilterValue, QueryJoin, QueryJoinOn, QueryLock, QueryLockOptions, QueryOperator, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, UpdateValues, };
@@ -331,6 +331,31 @@ function getDefinedColumnEntries(table, values, options = {}) {
331
331
  return [[column, value]];
332
332
  });
333
333
  }
334
+ function buildSelectLock(table, options) {
335
+ const lock = options.lock;
336
+ if (!lock)
337
+ return "";
338
+ const driver = currentSqlDialect().driver;
339
+ if (driver === "sqlite")
340
+ throw new Error("Row locks are not supported on SQLite; use an explicit write transaction.");
341
+ if (lock.mode !== "update" && lock.mode !== "share")
342
+ throw new TypeError("Invalid row lock mode.");
343
+ if (lock.wait !== undefined && !["wait", "nowait", "skipLocked"].includes(lock.wait))
344
+ throw new TypeError("Invalid row lock wait policy.");
345
+ if (options.groupBy || options.having)
346
+ throw new Error("Row locks cannot be combined with grouped/aggregate queries.");
347
+ let target = "";
348
+ if (lock.of !== undefined) {
349
+ if (driver !== "pgsql")
350
+ throw new Error("Row lock targets are supported only on PostgreSQL.");
351
+ const tables = [table.name, ...(options.joins ?? []).map((join) => join.table)];
352
+ if (!lock.of.length || lock.of.some((name) => !tables.includes(name)))
353
+ throw new TypeError("Row lock targets must name tables in this query.");
354
+ target = ` OF ${lock.of.map(quoteIdentifier).join(", ")}`;
355
+ }
356
+ const wait = lock.wait === "nowait" ? " NOWAIT" : lock.wait === "skipLocked" ? " SKIP LOCKED" : "";
357
+ return ` FOR ${lock.mode === "update" ? "UPDATE" : "SHARE"}${target}${wait}`;
358
+ }
334
359
  function buildSelectQuery(table, options = {}, whereNodes = []) {
335
360
  const params = [];
336
361
  const columns = buildSelectList(table, options.select, params);
@@ -341,8 +366,9 @@ function buildSelectQuery(table, options = {}, whereNodes = []) {
341
366
  const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
342
367
  const limit = buildLimitClause(options.limit);
343
368
  const offset = buildOffsetClause(options.offset);
369
+ const lock = buildSelectLock(table, options);
344
370
  return {
345
- text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
371
+ text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}${lock}`,
346
372
  params
347
373
  };
348
374
  }
@@ -511,9 +537,52 @@ function buildDeleteByIdQuery(table, id) {
511
537
  params
512
538
  };
513
539
  }
540
+ function buildConditionalWriteQuery(table, operation, changes, options, returning) {
541
+ for (const key of [
542
+ "joins",
543
+ "groupBy",
544
+ "having",
545
+ "select",
546
+ "orderBy",
547
+ "limit",
548
+ "offset",
549
+ "lock"
550
+ ]) {
551
+ if (options[key] !== undefined)
552
+ throw new Error(`Conditional writes do not support ${key}.`);
553
+ }
554
+ const params = [];
555
+ const predicate = buildAdvancedWhereClause(table.name, options.where ?? {}, options.whereNodes ?? []);
556
+ if (!predicate.clause)
557
+ throw new Error("Conditional writes require an explicit predicate.");
558
+ if (returning !== undefined && currentSqlDialect().driver === "mysql")
559
+ throw new Error("Mutation RETURNING is not supported on MySQL.");
560
+ const columns = returning ?? [table.primaryKey];
561
+ if (!columns.length || columns.some((column) => !table.columns.includes(column)))
562
+ throw new Error("RETURNING columns must be nonempty declared table columns.");
563
+ const deletedAt = resolveSoftDeleteColumn(table);
564
+ const values = operation === "delete" && deletedAt ? { [deletedAt]: new Date } : changes;
565
+ let prefix;
566
+ if (operation === "update" || deletedAt) {
567
+ const entries = Object.entries(values).filter(([, value]) => value !== undefined);
568
+ if (!entries.length)
569
+ throw new Error("Conditional updates require at least one value.");
570
+ if (entries.some(([column]) => column === table.primaryKey || !table.columns.includes(column)))
571
+ throw new Error("Conditional updates require declared non-primary-key columns.");
572
+ const assignments = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`);
573
+ prefix = `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")}`;
574
+ } else
575
+ prefix = `DELETE FROM ${quoteIdentifier(table.name)}`;
576
+ const { clause } = buildQueryWhereClause(table, options, options.whereNodes ?? [], params);
577
+ return {
578
+ text: prefix + clause + returningSuffix(columns.map(quoteIdentifier).join(", ")),
579
+ params
580
+ };
581
+ }
514
582
  export {
515
583
  assertSafeProjectionExpression,
516
584
  buildAdvancedWhereClause,
585
+ buildConditionalWriteQuery,
517
586
  buildCountQuery,
518
587
  buildDeleteByIdQuery,
519
588
  buildGroupedCountQuery,
@@ -355,6 +355,31 @@ function getDefinedColumnEntries(table, values, options = {}) {
355
355
  return [[column, value]];
356
356
  });
357
357
  }
358
+ function buildSelectLock(table, options) {
359
+ const lock = options.lock;
360
+ if (!lock)
361
+ return "";
362
+ const driver = currentSqlDialect().driver;
363
+ if (driver === "sqlite")
364
+ throw new Error("Row locks are not supported on SQLite; use an explicit write transaction.");
365
+ if (lock.mode !== "update" && lock.mode !== "share")
366
+ throw new TypeError("Invalid row lock mode.");
367
+ if (lock.wait !== undefined && !["wait", "nowait", "skipLocked"].includes(lock.wait))
368
+ throw new TypeError("Invalid row lock wait policy.");
369
+ if (options.groupBy || options.having)
370
+ throw new Error("Row locks cannot be combined with grouped/aggregate queries.");
371
+ let target = "";
372
+ if (lock.of !== undefined) {
373
+ if (driver !== "pgsql")
374
+ throw new Error("Row lock targets are supported only on PostgreSQL.");
375
+ const tables = [table.name, ...(options.joins ?? []).map((join) => join.table)];
376
+ if (!lock.of.length || lock.of.some((name) => !tables.includes(name)))
377
+ throw new TypeError("Row lock targets must name tables in this query.");
378
+ target = ` OF ${lock.of.map(quoteIdentifier).join(", ")}`;
379
+ }
380
+ const wait = lock.wait === "nowait" ? " NOWAIT" : lock.wait === "skipLocked" ? " SKIP LOCKED" : "";
381
+ return ` FOR ${lock.mode === "update" ? "UPDATE" : "SHARE"}${target}${wait}`;
382
+ }
358
383
  function buildSelectQuery(table, options = {}, whereNodes = []) {
359
384
  const params = [];
360
385
  const columns = buildSelectList(table, options.select, params);
@@ -365,8 +390,9 @@ function buildSelectQuery(table, options = {}, whereNodes = []) {
365
390
  const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
366
391
  const limit = buildLimitClause(options.limit);
367
392
  const offset = buildOffsetClause(options.offset);
393
+ const lock = buildSelectLock(table, options);
368
394
  return {
369
- text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
395
+ text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}${lock}`,
370
396
  params
371
397
  };
372
398
  }
@@ -535,6 +561,48 @@ function buildDeleteByIdQuery(table, id) {
535
561
  params
536
562
  };
537
563
  }
564
+ function buildConditionalWriteQuery(table, operation, changes, options, returning) {
565
+ for (const key of [
566
+ "joins",
567
+ "groupBy",
568
+ "having",
569
+ "select",
570
+ "orderBy",
571
+ "limit",
572
+ "offset",
573
+ "lock"
574
+ ]) {
575
+ if (options[key] !== undefined)
576
+ throw new Error(`Conditional writes do not support ${key}.`);
577
+ }
578
+ const params = [];
579
+ const predicate = buildAdvancedWhereClause(table.name, options.where ?? {}, options.whereNodes ?? []);
580
+ if (!predicate.clause)
581
+ throw new Error("Conditional writes require an explicit predicate.");
582
+ if (returning !== undefined && currentSqlDialect().driver === "mysql")
583
+ throw new Error("Mutation RETURNING is not supported on MySQL.");
584
+ const columns = returning ?? [table.primaryKey];
585
+ if (!columns.length || columns.some((column) => !table.columns.includes(column)))
586
+ throw new Error("RETURNING columns must be nonempty declared table columns.");
587
+ const deletedAt = resolveSoftDeleteColumn(table);
588
+ const values = operation === "delete" && deletedAt ? { [deletedAt]: new Date } : changes;
589
+ let prefix;
590
+ if (operation === "update" || deletedAt) {
591
+ const entries = Object.entries(values).filter(([, value]) => value !== undefined);
592
+ if (!entries.length)
593
+ throw new Error("Conditional updates require at least one value.");
594
+ if (entries.some(([column]) => column === table.primaryKey || !table.columns.includes(column)))
595
+ throw new Error("Conditional updates require declared non-primary-key columns.");
596
+ const assignments = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`);
597
+ prefix = `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")}`;
598
+ } else
599
+ prefix = `DELETE FROM ${quoteIdentifier(table.name)}`;
600
+ const { clause } = buildQueryWhereClause(table, options, options.whereNodes ?? [], params);
601
+ return {
602
+ text: prefix + clause + returningSuffix(columns.map(quoteIdentifier).join(", ")),
603
+ params
604
+ };
605
+ }
538
606
 
539
607
  // ../../src/core/database/relationships.ts
540
608
  function hasMany(definition) {
@@ -856,6 +924,20 @@ class RepositoryQuery {
856
924
  this.whereNodes.push({ kind: "or", where: input });
857
925
  return this;
858
926
  }
927
+ lockForUpdate(options = {}) {
928
+ this.queryOptions = {
929
+ ...this.queryOptions,
930
+ lock: { ...options, of: options.of ? [...options.of] : undefined, mode: "update" }
931
+ };
932
+ return this;
933
+ }
934
+ sharedLock(options = {}) {
935
+ this.queryOptions = {
936
+ ...this.queryOptions,
937
+ lock: { ...options, of: options.of ? [...options.of] : undefined, mode: "share" }
938
+ };
939
+ return this;
940
+ }
859
941
  orderBy(orderBy) {
860
942
  this.queryOptions = { ...this.queryOptions, orderBy };
861
943
  return this;
@@ -995,6 +1077,22 @@ class RepositoryQuery {
995
1077
  this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
996
1078
  return this;
997
1079
  }
1080
+ async update(changes) {
1081
+ this.assertNoMutationEagerLoads();
1082
+ return this.repository.updateWhere(changes, this.buildOptions());
1083
+ }
1084
+ async updateReturning(changes, ...columns) {
1085
+ this.assertNoMutationEagerLoads();
1086
+ return this.repository.updateWhereReturning(changes, columns, this.buildOptions());
1087
+ }
1088
+ async delete() {
1089
+ this.assertNoMutationEagerLoads();
1090
+ return this.repository.deleteWhere(this.buildOptions());
1091
+ }
1092
+ assertNoMutationEagerLoads() {
1093
+ if (this.eagerLoads.length)
1094
+ throw new Error("Conditional writes do not support eager loading.");
1095
+ }
998
1096
  async get() {
999
1097
  const rows = await this.repository.findAll(this.buildOptions());
1000
1098
  return await this.attach(rows);
@@ -340,6 +340,13 @@ class Blueprint {
340
340
  this.columns.push(column);
341
341
  return column;
342
342
  }
343
+ bigId(name = "id") {
344
+ const column = new ColumnDefinition(name, "bigId");
345
+ column.primary();
346
+ column.autoIncrement = true;
347
+ this.columns.push(column);
348
+ return column;
349
+ }
343
350
  string(name, length) {
344
351
  if (length !== undefined && (!Number.isSafeInteger(length) || length <= 0)) {
345
352
  throw new Error("String length must be a positive safe integer.");
@@ -486,6 +493,8 @@ init_dialect();
486
493
  // ../../src/core/database/schema/grammars/grammar.ts
487
494
  function compileColumnType(driver, column) {
488
495
  switch (column.kind) {
496
+ case "bigId":
497
+ return driver === "pgsql" ? "BIGSERIAL" : compileIdType(driver);
489
498
  case "id":
490
499
  return compileIdType(driver);
491
500
  case "string":
@@ -69,7 +69,7 @@ function createSqliteConnection(filename) {
69
69
  const transactions = new WeakSet;
70
70
  function execute(query, params) {
71
71
  const statement = db.query(query);
72
- const args = [...params];
72
+ const args = params.map((value) => value instanceof Date ? value.toISOString() : value);
73
73
  if (isRowReturning(query))
74
74
  return statement.all(...args);
75
75
  statement.run(...args);
package/dist/index.js CHANGED
@@ -4463,6 +4463,31 @@ function getDefinedColumnEntries(table, values, options = {}) {
4463
4463
  return [[column, value]];
4464
4464
  });
4465
4465
  }
4466
+ function buildSelectLock(table, options) {
4467
+ const lock = options.lock;
4468
+ if (!lock)
4469
+ return "";
4470
+ const driver = currentSqlDialect().driver;
4471
+ if (driver === "sqlite")
4472
+ throw new Error("Row locks are not supported on SQLite; use an explicit write transaction.");
4473
+ if (lock.mode !== "update" && lock.mode !== "share")
4474
+ throw new TypeError("Invalid row lock mode.");
4475
+ if (lock.wait !== undefined && !["wait", "nowait", "skipLocked"].includes(lock.wait))
4476
+ throw new TypeError("Invalid row lock wait policy.");
4477
+ if (options.groupBy || options.having)
4478
+ throw new Error("Row locks cannot be combined with grouped/aggregate queries.");
4479
+ let target = "";
4480
+ if (lock.of !== undefined) {
4481
+ if (driver !== "pgsql")
4482
+ throw new Error("Row lock targets are supported only on PostgreSQL.");
4483
+ const tables = [table.name, ...(options.joins ?? []).map((join) => join.table)];
4484
+ if (!lock.of.length || lock.of.some((name) => !tables.includes(name)))
4485
+ throw new TypeError("Row lock targets must name tables in this query.");
4486
+ target = ` OF ${lock.of.map(quoteIdentifier).join(", ")}`;
4487
+ }
4488
+ const wait = lock.wait === "nowait" ? " NOWAIT" : lock.wait === "skipLocked" ? " SKIP LOCKED" : "";
4489
+ return ` FOR ${lock.mode === "update" ? "UPDATE" : "SHARE"}${target}${wait}`;
4490
+ }
4466
4491
  function buildSelectQuery(table, options = {}, whereNodes = []) {
4467
4492
  const params = [];
4468
4493
  const columns = buildSelectList(table, options.select, params);
@@ -4473,8 +4498,9 @@ function buildSelectQuery(table, options = {}, whereNodes = []) {
4473
4498
  const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
4474
4499
  const limit = buildLimitClause(options.limit);
4475
4500
  const offset = buildOffsetClause(options.offset);
4501
+ const lock = buildSelectLock(table, options);
4476
4502
  return {
4477
- text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
4503
+ text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}${lock}`,
4478
4504
  params
4479
4505
  };
4480
4506
  }
@@ -4643,6 +4669,48 @@ function buildDeleteByIdQuery(table, id) {
4643
4669
  params
4644
4670
  };
4645
4671
  }
4672
+ function buildConditionalWriteQuery(table, operation, changes, options, returning) {
4673
+ for (const key of [
4674
+ "joins",
4675
+ "groupBy",
4676
+ "having",
4677
+ "select",
4678
+ "orderBy",
4679
+ "limit",
4680
+ "offset",
4681
+ "lock"
4682
+ ]) {
4683
+ if (options[key] !== undefined)
4684
+ throw new Error(`Conditional writes do not support ${key}.`);
4685
+ }
4686
+ const params = [];
4687
+ const predicate = buildAdvancedWhereClause(table.name, options.where ?? {}, options.whereNodes ?? []);
4688
+ if (!predicate.clause)
4689
+ throw new Error("Conditional writes require an explicit predicate.");
4690
+ if (returning !== undefined && currentSqlDialect().driver === "mysql")
4691
+ throw new Error("Mutation RETURNING is not supported on MySQL.");
4692
+ const columns = returning ?? [table.primaryKey];
4693
+ if (!columns.length || columns.some((column) => !table.columns.includes(column)))
4694
+ throw new Error("RETURNING columns must be nonempty declared table columns.");
4695
+ const deletedAt = resolveSoftDeleteColumn(table);
4696
+ const values = operation === "delete" && deletedAt ? { [deletedAt]: new Date } : changes;
4697
+ let prefix;
4698
+ if (operation === "update" || deletedAt) {
4699
+ const entries = Object.entries(values).filter(([, value]) => value !== undefined);
4700
+ if (!entries.length)
4701
+ throw new Error("Conditional updates require at least one value.");
4702
+ if (entries.some(([column]) => column === table.primaryKey || !table.columns.includes(column)))
4703
+ throw new Error("Conditional updates require declared non-primary-key columns.");
4704
+ const assignments = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`);
4705
+ prefix = `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")}`;
4706
+ } else
4707
+ prefix = `DELETE FROM ${quoteIdentifier(table.name)}`;
4708
+ const { clause } = buildQueryWhereClause(table, options, options.whereNodes ?? [], params);
4709
+ return {
4710
+ text: prefix + clause + returningSuffix(columns.map(quoteIdentifier).join(", ")),
4711
+ params
4712
+ };
4713
+ }
4646
4714
 
4647
4715
  // ../../src/core/database/relationships.ts
4648
4716
  function hasMany(definition) {
@@ -4991,6 +5059,20 @@ class RepositoryQuery {
4991
5059
  this.whereNodes.push({ kind: "or", where: input });
4992
5060
  return this;
4993
5061
  }
5062
+ lockForUpdate(options = {}) {
5063
+ this.queryOptions = {
5064
+ ...this.queryOptions,
5065
+ lock: { ...options, of: options.of ? [...options.of] : undefined, mode: "update" }
5066
+ };
5067
+ return this;
5068
+ }
5069
+ sharedLock(options = {}) {
5070
+ this.queryOptions = {
5071
+ ...this.queryOptions,
5072
+ lock: { ...options, of: options.of ? [...options.of] : undefined, mode: "share" }
5073
+ };
5074
+ return this;
5075
+ }
4994
5076
  orderBy(orderBy) {
4995
5077
  this.queryOptions = { ...this.queryOptions, orderBy };
4996
5078
  return this;
@@ -5130,6 +5212,22 @@ class RepositoryQuery {
5130
5212
  this.queryOptions = { ...this.queryOptions, onlyTrashed: true };
5131
5213
  return this;
5132
5214
  }
5215
+ async update(changes) {
5216
+ this.assertNoMutationEagerLoads();
5217
+ return this.repository.updateWhere(changes, this.buildOptions());
5218
+ }
5219
+ async updateReturning(changes, ...columns) {
5220
+ this.assertNoMutationEagerLoads();
5221
+ return this.repository.updateWhereReturning(changes, columns, this.buildOptions());
5222
+ }
5223
+ async delete() {
5224
+ this.assertNoMutationEagerLoads();
5225
+ return this.repository.deleteWhere(this.buildOptions());
5226
+ }
5227
+ assertNoMutationEagerLoads() {
5228
+ if (this.eagerLoads.length)
5229
+ throw new Error("Conditional writes do not support eager loading.");
5230
+ }
5133
5231
  async get() {
5134
5232
  const rows = await this.repository.findAll(this.buildOptions());
5135
5233
  return await this.attach(rows);
@@ -5335,11 +5433,16 @@ class BaseRepository {
5335
5433
  return runInTransaction(operation);
5336
5434
  }
5337
5435
  async count(options = {}) {
5436
+ if (options.lock)
5437
+ throw new Error("Row locks require a row selection, not count().");
5338
5438
  const { whereNodes, where, ...rest } = options;
5339
5439
  return await this.countWhere(where ?? {}, rest, whereNodes ?? []);
5340
5440
  }
5341
5441
  async findAll(options = {}) {
5342
5442
  return await withDatabaseErrorHandling(async () => {
5443
+ if (options.lock && (!hasActiveTransaction() || this.connection !== repositoryConnection && this.connection !== getActiveDatabaseConnection(repositoryConnection))) {
5444
+ throw new Error("Row locks require the framework active transaction connection.");
5445
+ }
5343
5446
  const { whereNodes, ...queryOptions } = options;
5344
5447
  const { text, params } = buildSelectQuery(this.table, queryOptions, whereNodes ?? []);
5345
5448
  return await this.connection.unsafe(text, params);
@@ -5509,6 +5612,28 @@ class BaseRepository {
5509
5612
  return entity;
5510
5613
  }));
5511
5614
  }
5615
+ async updateWhere(changes, options) {
5616
+ return this.executeConditionalWrite("update", changes, options);
5617
+ }
5618
+ async deleteWhere(options) {
5619
+ return this.executeConditionalWrite("delete", {}, options);
5620
+ }
5621
+ async updateWhereReturning(changes, columns, options) {
5622
+ const { text, params } = buildConditionalWriteQuery(this.table, "update", changes, options, columns);
5623
+ return withDatabaseErrorHandling(() => this.connection.unsafe(text, params));
5624
+ }
5625
+ async executeConditionalWrite(operation, changes, options) {
5626
+ const { text, params } = buildConditionalWriteQuery(this.table, operation, changes, options);
5627
+ return withDatabaseErrorHandling(async () => {
5628
+ const rows = await this.connection.unsafe(text, params);
5629
+ if (currentSqlDialect().driver !== "mysql")
5630
+ return rows.length;
5631
+ const affected = rows[0]?.affectedRows;
5632
+ if (!Number.isSafeInteger(affected) || affected === undefined || affected < 0)
5633
+ throw new Error("MySQL conditional writes require affectedRows metadata from the framework adapter.");
5634
+ return affected;
5635
+ });
5636
+ }
5512
5637
  async updateById(id, changes) {
5513
5638
  this.assertTransactionalEvent("updated");
5514
5639
  return await this.runModelMutation("updated", () => withDatabaseErrorHandling(async () => {
@@ -7172,6 +7297,14 @@ class ModelQuery {
7172
7297
  return rows.map((row) => applyCasts(row, modelStatics(this.modelClass).$casts, "hydrate"));
7173
7298
  });
7174
7299
  }
7300
+ lockForUpdate(options = {}) {
7301
+ this.query.lockForUpdate(options);
7302
+ return this;
7303
+ }
7304
+ sharedLock(options = {}) {
7305
+ this.query.sharedLock(options);
7306
+ return this;
7307
+ }
7175
7308
  orderBy(orderBy) {
7176
7309
  this.query.orderBy(orderBy);
7177
7310
  return this;
@@ -8200,6 +8333,13 @@ class Blueprint {
8200
8333
  this.columns.push(column);
8201
8334
  return column;
8202
8335
  }
8336
+ bigId(name = "id") {
8337
+ const column = new ColumnDefinition(name, "bigId");
8338
+ column.primary();
8339
+ column.autoIncrement = true;
8340
+ this.columns.push(column);
8341
+ return column;
8342
+ }
8203
8343
  string(name, length) {
8204
8344
  if (length !== undefined && (!Number.isSafeInteger(length) || length <= 0)) {
8205
8345
  throw new Error("String length must be a positive safe integer.");
@@ -8346,6 +8486,8 @@ init_dialect();
8346
8486
  // ../../src/core/database/schema/grammars/grammar.ts
8347
8487
  function compileColumnType(driver, column) {
8348
8488
  switch (column.kind) {
8489
+ case "bigId":
8490
+ return driver === "pgsql" ? "BIGSERIAL" : compileIdType(driver);
8349
8491
  case "id":
8350
8492
  return compileIdType(driver);
8351
8493
  case "string":
@@ -8781,7 +8923,7 @@ function createSqliteConnection(filename) {
8781
8923
  const transactions = new WeakSet;
8782
8924
  function execute(query, params) {
8783
8925
  const statement = db.query(query);
8784
- const args = [...params];
8926
+ const args = params.map((value) => value instanceof Date ? value.toISOString() : value);
8785
8927
  if (isRowReturning(query))
8786
8928
  return statement.all(...args);
8787
8929
  statement.run(...args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.2.1",
3
+ "version": "2.2.2",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",