@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 +4 -0
- package/dist/core/database/baseRepository.d.ts +5 -0
- package/dist/core/database/model.d.ts +3 -1
- package/dist/core/database/query.d.ts +6 -1
- package/dist/core/database/repositoryQuery.d.ts +7 -1
- package/dist/core/database/schema/blueprint.d.ts +2 -0
- package/dist/core/database/schema/columnDefinition.d.ts +1 -1
- package/dist/core/database/types.d.ts +10 -1
- package/dist/entries/database/query.js +70 -1
- package/dist/entries/database/repositoryQuery.js +99 -1
- package/dist/entries/database/schema.js +9 -0
- package/dist/entries/database/sqliteConnection.js +1 -1
- package/dist/index.js +144 -2
- package/package.json +1 -1
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
|
-
|
|
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 =
|
|
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 =
|
|
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);
|