@sigitex/outlaw 1.0.1 → 1.2.0
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/README.md +23 -1
- package/package.json +6 -6
- package/src/api/DatabaseTable.ts +31 -19
- package/src/api/DatabaseView.ts +6 -5
- package/src/api/UnsupportedTransactionError.ts +7 -0
- package/src/api/api.types.ts +46 -25
- package/src/api/createDatabase.ts +31 -6
- package/src/api/index.ts +1 -0
- package/src/bun/bun.ts +120 -7
- package/src/cowboyMigration/CowboyConnection.ts +45 -13
- package/src/queryBuilder/DeleteBuilder.ts +6 -4
- package/src/queryBuilder/UpdateBuilder.ts +6 -4
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ bun add @sigitex/outlaw
|
|
|
8
8
|
|
|
9
9
|
Attempts to mirror Sqlite syntax *very* closely. Currently works with Bun and Cloudflare Functions.
|
|
10
10
|
|
|
11
|
-
Define your schema in code, and Outlaw's [cowboy migrations](#cowboy-migrations) automatically diff and converge your database on startup
|
|
11
|
+
Define your schema in code, and Outlaw's [cowboy migrations](#cowboy-migrations) automatically diff and converge your database on startup.
|
|
12
12
|
|
|
13
13
|
> **Note:** This package exports TypeScript sources directly. A TypeScript-compatible runtime or bundler (Bun, etc.) is required.
|
|
14
14
|
|
|
@@ -155,6 +155,9 @@ Outlaw abstracts over any SQLite connection via the `Connection` interface:
|
|
|
155
155
|
type Connection = {
|
|
156
156
|
query<Row>(sql: string): Promise<Row[]>
|
|
157
157
|
script(statements: string[]): Promise<void>
|
|
158
|
+
transaction?<Result>(
|
|
159
|
+
work: (connection: TransactionalConnection) => Promise<Result>,
|
|
160
|
+
): Promise<Result>
|
|
158
161
|
}
|
|
159
162
|
```
|
|
160
163
|
|
|
@@ -188,6 +191,25 @@ import { createDatabase } from "@sigitex/outlaw"
|
|
|
188
191
|
const db = createDatabase(connection, schema)
|
|
189
192
|
```
|
|
190
193
|
|
|
194
|
+
Every database API exposes `transaction`. Supported connections provide a fresh
|
|
195
|
+
database API bound to the transaction-scoped connection:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
await db.transaction(async (tx) => {
|
|
199
|
+
await tx.users.insert({ name: "Wyatt" }).execute()
|
|
200
|
+
|
|
201
|
+
await tx.transaction(async (nested) => {
|
|
202
|
+
await nested.products.insert({ name: "Hat" }).execute()
|
|
203
|
+
})
|
|
204
|
+
})
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Use only the callback's scoped database API until that callback ends. Bun
|
|
208
|
+
serializes root operations, uses `BEGIN IMMEDIATE`, and implements nested
|
|
209
|
+
callbacks with savepoints. Cloudflare D1 does not support interactive callback
|
|
210
|
+
transactions; `transaction` throws `UnsupportedTransactionError` before the
|
|
211
|
+
callback runs, while `script()` remains available through D1 batch.
|
|
212
|
+
|
|
191
213
|
### Select
|
|
192
214
|
|
|
193
215
|
```ts
|
package/package.json
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
"description": "A trigger-happy sqlite framework.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Sigitex",
|
|
8
|
-
"url": "
|
|
8
|
+
"url": "https://sigitex.com"
|
|
9
9
|
},
|
|
10
10
|
"repository": {
|
|
11
11
|
"type": "git",
|
|
12
|
-
"url": "https://github.com/sigitex/outlaw.git"
|
|
12
|
+
"url": "git+https://github.com/sigitex/outlaw.git"
|
|
13
13
|
},
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"@semantic-release/exec": "^7.0.3",
|
|
29
29
|
"@types/bun": "^1.3.13",
|
|
30
30
|
"@types/fast-json-stable-stringify": "^2.1.2",
|
|
31
|
-
"@typescript/native-preview": "beta",
|
|
32
31
|
"husky": "^9.1.7",
|
|
33
32
|
"multi-semantic-release": "^3.1.0",
|
|
34
33
|
"oxfmt": "^0.47.0",
|
|
35
34
|
"oxlint": "^1.62.0",
|
|
36
|
-
"semantic-release": "^25.0.3"
|
|
35
|
+
"semantic-release": "^25.0.3",
|
|
36
|
+
"typescript": "^7.0.2"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@sigitex/print": "*",
|
|
@@ -43,11 +43,11 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"prepare": "husky",
|
|
45
45
|
"lint": "oxlint",
|
|
46
|
-
"check": "
|
|
46
|
+
"check": "tsc --build",
|
|
47
47
|
"test": "bun test --pass-with-no-tests --tsconfig-override tsconfig.test.json"
|
|
48
48
|
},
|
|
49
49
|
"files": [
|
|
50
50
|
"src"
|
|
51
51
|
],
|
|
52
|
-
"version": "1.0
|
|
52
|
+
"version": "1.2.0"
|
|
53
53
|
}
|
package/src/api/DatabaseTable.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
// oxlint-disable typescript/no-explicit-any
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Delete,
|
|
4
|
+
Connection,
|
|
5
|
+
Insert,
|
|
6
|
+
Select,
|
|
7
|
+
TableApi,
|
|
8
|
+
Update,
|
|
9
|
+
} from "./api.types"
|
|
3
10
|
import type { TableData } from "../schemaBuilder"
|
|
4
11
|
import {
|
|
5
12
|
DeleteBuilder,
|
|
@@ -18,27 +25,32 @@ export class DatabaseTable implements TableApi<any> {
|
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
select(all: "*"): Select<any, any>
|
|
21
|
-
select<Column extends string>(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
select<Column extends string>(
|
|
29
|
+
...columns: Column[]
|
|
30
|
+
): Select<Pick<any, any>, any>
|
|
31
|
+
select(...columns: ("*" | string)[]): Select<any, any> {
|
|
32
|
+
const selected = columns[0] === "*" || columns.length === 0 ? "*" : columns
|
|
33
|
+
return new SelectBuilder(this.connection, this.table, selected)
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
insert(
|
|
29
|
-
insert<
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
insert(
|
|
34
|
-
columnsOrRow: any,
|
|
35
|
-
rows?: any,
|
|
36
|
-
): Insert<any, any, number> | Insert<any, any, number> {
|
|
37
|
-
if (rows) {
|
|
38
|
-
return new InsertBuilder(this.connection, this.table, columnsOrRow, rows)
|
|
36
|
+
insert(...rows: [Partial<any>, ...Partial<any>[]]): Insert<any, any, number>
|
|
37
|
+
insert(...rows: Record<string, unknown>[]): Insert<any, any, number> {
|
|
38
|
+
const first = rows[0]
|
|
39
|
+
if (!first) {
|
|
40
|
+
throw new Error("insert() requires at least one row")
|
|
39
41
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
+
const columns = Object.keys(first)
|
|
43
|
+
const expected = new Set(columns)
|
|
44
|
+
for (const row of rows.slice(1)) {
|
|
45
|
+
const rowColumns = Object.keys(row)
|
|
46
|
+
if (
|
|
47
|
+
rowColumns.length !== columns.length ||
|
|
48
|
+
rowColumns.some((column) => !expected.has(column))
|
|
49
|
+
) {
|
|
50
|
+
throw new Error("insert() rows must use the same columns")
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return new InsertBuilder(this.connection, this.table, columns, rows)
|
|
42
54
|
}
|
|
43
55
|
|
|
44
56
|
update(row: Partial<any>): Update<any, number> {
|
package/src/api/DatabaseView.ts
CHANGED
|
@@ -13,10 +13,11 @@ export class DatabaseView implements ViewApi<any> {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
select(all: "*"): Select<any, any>
|
|
16
|
-
select<Column extends string>(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
select<Column extends string>(
|
|
17
|
+
...columns: Column[]
|
|
18
|
+
): Select<Pick<any, any>, any>
|
|
19
|
+
select(...columns: ("*" | string)[]): Select<any, any> {
|
|
20
|
+
const selected = columns[0] === "*" || columns.length === 0 ? "*" : columns
|
|
21
|
+
return new SelectBuilder(this.connection, this.tableData, selected)
|
|
21
22
|
}
|
|
22
23
|
}
|
package/src/api/api.types.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
// oxlint-disable typescript/consistent-type-definitions
|
|
2
1
|
// oxlint-disable typescript/no-explicit-any
|
|
2
|
+
// oxlint-disable typescript/method-signature-style
|
|
3
|
+
// oxlint-disable typescript/consistent-type-definitions
|
|
3
4
|
import type { BINARY_OPERATORS, UNARY_OPERATORS } from "../queryBuilder"
|
|
4
5
|
import type {
|
|
5
|
-
BuildColumn,
|
|
6
6
|
BuildTable,
|
|
7
7
|
BuildView,
|
|
8
8
|
ColumnRef,
|
|
9
|
+
InferColumn,
|
|
9
10
|
SchemaMembers,
|
|
10
11
|
TablesOf,
|
|
11
12
|
ViewsOf,
|
|
@@ -29,36 +30,54 @@ export type InsertRecord<R> = Partial<R> &
|
|
|
29
30
|
export type ColumnsOf<BT> =
|
|
30
31
|
BT extends BuildTable<infer Columns> ? Columns : never
|
|
31
32
|
|
|
32
|
-
export type ColumnTypeOf<BC> =
|
|
33
|
-
BC extends BuildColumn<infer Type, infer Constraints>
|
|
34
|
-
? Constraints extends "notNull" | "primaryKey"
|
|
35
|
-
? Type
|
|
36
|
-
: Type | null
|
|
37
|
-
: never
|
|
38
|
-
|
|
39
33
|
export type ColumnTypesOf<BCS> = {
|
|
40
|
-
readonly [BCK in keyof BCS]:
|
|
34
|
+
readonly [BCK in keyof BCS]: InferColumn<BCS[BCK]>
|
|
41
35
|
}
|
|
42
36
|
|
|
43
37
|
// API
|
|
44
38
|
|
|
45
39
|
export type DefaultRow = Record<string, unknown>
|
|
46
40
|
|
|
47
|
-
export type
|
|
41
|
+
export type ConnectionOperations = {
|
|
48
42
|
readonly query: <Row = DefaultRow>(sql: string) => Promise<Row[]>
|
|
49
43
|
readonly script: (statements: string[]) => Promise<void>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type TransactionWork<Result> = (
|
|
47
|
+
connection: TransactionalConnection,
|
|
48
|
+
) => Promise<Result>
|
|
49
|
+
|
|
50
|
+
export type Transaction = <Result>(
|
|
51
|
+
work: TransactionWork<Result>,
|
|
52
|
+
) => Promise<Result>
|
|
53
|
+
|
|
54
|
+
export type TransactionalConnection = ConnectionOperations & {
|
|
55
|
+
readonly transaction: Transaction
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type Connection = ConnectionOperations & {
|
|
50
59
|
readonly reset?: () => Promise<void>
|
|
60
|
+
readonly transaction?: Transaction
|
|
51
61
|
}
|
|
52
62
|
|
|
53
|
-
export type DatabaseApi<
|
|
63
|
+
export type DatabaseApi<
|
|
64
|
+
M extends SchemaMembers,
|
|
65
|
+
C extends Connection = Connection,
|
|
66
|
+
> = {
|
|
54
67
|
readonly [K in keyof TablesOf<M>]: TableApi<ColumnsOf<TablesOf<M>[K]>>
|
|
55
68
|
} & {
|
|
56
69
|
readonly [K in keyof ViewsOf<M>]: ViewApi<ColumnsOfView<ViewsOf<M>[K]>>
|
|
57
70
|
} & {
|
|
58
|
-
readonly connection:
|
|
71
|
+
readonly connection: C
|
|
72
|
+
readonly transaction: <Result>(
|
|
73
|
+
work: (
|
|
74
|
+
database: DatabaseApi<M, TransactionalConnection>,
|
|
75
|
+
) => Promise<Result>,
|
|
76
|
+
) => Promise<Result>
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
export type ColumnsOfView<BV> =
|
|
79
|
+
export type ColumnsOfView<BV> =
|
|
80
|
+
BV extends BuildView<infer Columns> ? Columns : never
|
|
62
81
|
|
|
63
82
|
export type ViewApi<Columns> = {
|
|
64
83
|
/** Issue a `SELECT *` query to the API. */
|
|
@@ -78,7 +97,7 @@ export type TableApi<Columns> = {
|
|
|
78
97
|
): Select<Pick<Columns, Column>, Columns>
|
|
79
98
|
/** Issue an `INSERT` statement. */
|
|
80
99
|
insert<InsertColumns extends Partial<ColumnTypesOf<Columns>>>(
|
|
81
|
-
...rows: InsertColumns[]
|
|
100
|
+
...rows: [InsertColumns, ...InsertColumns[]]
|
|
82
101
|
): Insert<InsertColumns, Columns, number>
|
|
83
102
|
/** Issue an `UPDATE` statement. */
|
|
84
103
|
update(row: Partial<ColumnTypesOf<Columns>>): Update<Columns, number>
|
|
@@ -91,7 +110,7 @@ export interface HasWhereClause<Columns> {
|
|
|
91
110
|
/** Add a condition where the given column equals the given value. */
|
|
92
111
|
where<Column extends keyof Columns>(
|
|
93
112
|
column: Column,
|
|
94
|
-
value: Columns[Column]
|
|
113
|
+
value: InferColumn<Columns[Column]>,
|
|
95
114
|
): this
|
|
96
115
|
/** Add a unary `WHERE` condition. */
|
|
97
116
|
where<Column extends keyof Columns>(
|
|
@@ -102,13 +121,15 @@ export interface HasWhereClause<Columns> {
|
|
|
102
121
|
where<Column extends keyof Columns>(
|
|
103
122
|
column: Column,
|
|
104
123
|
operator: BinaryOperator,
|
|
105
|
-
value:
|
|
124
|
+
value: InferColumn<Columns[Column]>,
|
|
106
125
|
): this
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
/** Select query API. */
|
|
110
|
-
export interface Select<
|
|
111
|
-
|
|
129
|
+
export interface Select<
|
|
130
|
+
SelectColumns,
|
|
131
|
+
Columns,
|
|
132
|
+
> extends HasWhereClause<Columns> {
|
|
112
133
|
/** Issue the query, expecting an array of results. */
|
|
113
134
|
fetch(): Promise<ColumnTypesOf<SelectColumns>[]>
|
|
114
135
|
/** Issue the query, returning a single result, or throwing.. */
|
|
@@ -177,11 +198,11 @@ export interface Update<Columns, Returning> extends HasWhereClause<Columns> {
|
|
|
177
198
|
/** Execute the statement. */
|
|
178
199
|
execute(): Promise<Returning>
|
|
179
200
|
/** Specify a `RETURNING *` clause. */
|
|
180
|
-
returning(all: "*"): Update<Columns, Columns[]>
|
|
201
|
+
returning(all: "*"): Update<Columns, ColumnTypesOf<Columns>[]>
|
|
181
202
|
/** Specify a `RETURNING` clause with the chosen columns. */
|
|
182
203
|
returning<Column extends keyof Columns>(
|
|
183
|
-
columns: Column[]
|
|
184
|
-
): Update<Columns, Pick<Columns, Column
|
|
204
|
+
...columns: Column[]
|
|
205
|
+
): Update<Columns, ColumnTypesOf<Pick<Columns, Column>>[]>
|
|
185
206
|
}
|
|
186
207
|
|
|
187
208
|
/** Delete statement API. */
|
|
@@ -189,11 +210,11 @@ export interface Delete<Columns, Returning> extends HasWhereClause<Columns> {
|
|
|
189
210
|
/** Execute the statement. */
|
|
190
211
|
execute(): Promise<Returning>
|
|
191
212
|
/** Specify a `RETURNING *` clause. */
|
|
192
|
-
returning(all: "*"): Delete<Columns, Columns[]>
|
|
213
|
+
returning(all: "*"): Delete<Columns, ColumnTypesOf<Columns>[]>
|
|
193
214
|
/** Specify a `RETURNING` clause with the chosen columns. */
|
|
194
215
|
returning<Column extends keyof Columns>(
|
|
195
|
-
columns: Column[]
|
|
196
|
-
): Delete<Columns, Pick<Columns, Column
|
|
216
|
+
...columns: Column[]
|
|
217
|
+
): Delete<Columns, ColumnTypesOf<Pick<Columns, Column>>[]>
|
|
197
218
|
}
|
|
198
219
|
|
|
199
220
|
export type UnaryOperator = ElementOf<typeof UNARY_OPERATORS>
|
|
@@ -1,13 +1,38 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type {
|
|
2
|
+
BuildTable,
|
|
3
|
+
BuildView,
|
|
4
|
+
SchemaMembers,
|
|
5
|
+
Schema,
|
|
6
|
+
} from "../schemaBuilder"
|
|
7
|
+
import type {
|
|
8
|
+
Connection,
|
|
9
|
+
DatabaseApi,
|
|
10
|
+
TransactionalConnection,
|
|
11
|
+
} from "./api.types"
|
|
3
12
|
import { DatabaseTable } from "./DatabaseTable"
|
|
4
13
|
import { DatabaseView } from "./DatabaseView"
|
|
14
|
+
import { UnsupportedTransactionError } from "./UnsupportedTransactionError"
|
|
5
15
|
|
|
6
|
-
export function createDatabase<M extends SchemaMembers>(
|
|
7
|
-
connection:
|
|
16
|
+
export function createDatabase<M extends SchemaMembers, C extends Connection>(
|
|
17
|
+
connection: C,
|
|
8
18
|
schema: Schema<M>,
|
|
9
19
|
) {
|
|
10
|
-
const api = {
|
|
20
|
+
const api = {
|
|
21
|
+
connection,
|
|
22
|
+
transaction: async <Result>(
|
|
23
|
+
work: (
|
|
24
|
+
database: DatabaseApi<M, TransactionalConnection>,
|
|
25
|
+
) => Promise<Result>,
|
|
26
|
+
) => {
|
|
27
|
+
const transaction = connection.transaction
|
|
28
|
+
if (!transaction) {
|
|
29
|
+
throw new UnsupportedTransactionError()
|
|
30
|
+
}
|
|
31
|
+
return transaction((scopedConnection) =>
|
|
32
|
+
work(createDatabase(scopedConnection, schema)),
|
|
33
|
+
)
|
|
34
|
+
},
|
|
35
|
+
}
|
|
11
36
|
for (const [property, member] of Object.entries(schema.tables)) {
|
|
12
37
|
const table = member as BuildTable<unknown>
|
|
13
38
|
Object.defineProperty(api, property, {
|
|
@@ -20,5 +45,5 @@ export function createDatabase<M extends SchemaMembers>(
|
|
|
20
45
|
value: new DatabaseView(connection, view.$tableData),
|
|
21
46
|
})
|
|
22
47
|
}
|
|
23
|
-
return api as DatabaseApi<M>
|
|
48
|
+
return api as DatabaseApi<M, C>
|
|
24
49
|
}
|
package/src/api/index.ts
CHANGED
package/src/bun/bun.ts
CHANGED
|
@@ -1,27 +1,140 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite"
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
Connection,
|
|
4
|
+
DefaultRow,
|
|
5
|
+
TransactionalConnection,
|
|
6
|
+
TransactionWork,
|
|
7
|
+
} from "../api/api.types"
|
|
8
|
+
|
|
9
|
+
type TransactionState = {
|
|
10
|
+
active: boolean
|
|
11
|
+
nextSavepoint: number
|
|
12
|
+
}
|
|
3
13
|
|
|
4
14
|
export class BunConnection implements Connection {
|
|
5
15
|
private readonly db: Database
|
|
16
|
+
private pending = Promise.resolve()
|
|
6
17
|
|
|
7
18
|
constructor(db: Database) {
|
|
8
19
|
this.db = db
|
|
9
20
|
}
|
|
10
21
|
|
|
11
22
|
async query<Row = DefaultRow>(sql: string) {
|
|
12
|
-
return this.
|
|
23
|
+
return this.exclusive(() => query<Row>(this.db, sql))
|
|
13
24
|
}
|
|
14
25
|
|
|
15
26
|
async script(statements: string[]) {
|
|
16
|
-
this.db
|
|
27
|
+
await this.exclusive(() => script(this.db, statements, "BEGIN"))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async transaction<Result>(work: TransactionWork<Result>) {
|
|
31
|
+
return this.exclusive(async () => {
|
|
32
|
+
await run(this.db, "BEGIN IMMEDIATE")
|
|
33
|
+
const state: TransactionState = { active: true, nextSavepoint: 0 }
|
|
34
|
+
const connection = new BunTransactionConnection(this.db, state)
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const result = await work(connection)
|
|
38
|
+
connection.close()
|
|
39
|
+
await run(this.db, "COMMIT")
|
|
40
|
+
return result
|
|
41
|
+
} catch (error) {
|
|
42
|
+
connection.close()
|
|
43
|
+
await run(this.db, "ROLLBACK")
|
|
44
|
+
throw error
|
|
45
|
+
} finally {
|
|
46
|
+
state.active = false
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private async exclusive<Result>(work: () => Promise<Result>) {
|
|
52
|
+
const previous = this.pending
|
|
53
|
+
let release!: () => void
|
|
54
|
+
this.pending = new Promise<void>((resolve) => {
|
|
55
|
+
release = resolve
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
await previous
|
|
17
59
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
60
|
+
return await work()
|
|
61
|
+
} finally {
|
|
62
|
+
release()
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class BunTransactionConnection implements TransactionalConnection {
|
|
68
|
+
private readonly db: Database
|
|
69
|
+
private readonly state: TransactionState
|
|
70
|
+
private active = true
|
|
71
|
+
|
|
72
|
+
constructor(db: Database, state: TransactionState) {
|
|
73
|
+
this.db = db
|
|
74
|
+
this.state = state
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async query<Row = DefaultRow>(sql: string) {
|
|
78
|
+
this.assertActive()
|
|
79
|
+
return query<Row>(this.db, sql)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async script(statements: string[]) {
|
|
83
|
+
this.assertActive()
|
|
84
|
+
await this.transaction(async (connection) => {
|
|
85
|
+
for (const statement of statements) {
|
|
86
|
+
await connection.query(statement)
|
|
20
87
|
}
|
|
21
|
-
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async transaction<Result>(work: TransactionWork<Result>) {
|
|
92
|
+
this.assertActive()
|
|
93
|
+
const name = `outlaw_transaction_${++this.state.nextSavepoint}`
|
|
94
|
+
await run(this.db, `SAVEPOINT ${name}`)
|
|
95
|
+
const connection = new BunTransactionConnection(this.db, this.state)
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
const result = await work(connection)
|
|
99
|
+
connection.close()
|
|
100
|
+
await run(this.db, `RELEASE SAVEPOINT ${name}`)
|
|
101
|
+
return result
|
|
22
102
|
} catch (error) {
|
|
23
|
-
|
|
103
|
+
connection.close()
|
|
104
|
+
await run(this.db, `ROLLBACK TO SAVEPOINT ${name}`)
|
|
105
|
+
await run(this.db, `RELEASE SAVEPOINT ${name}`)
|
|
24
106
|
throw error
|
|
25
107
|
}
|
|
26
108
|
}
|
|
109
|
+
|
|
110
|
+
close() {
|
|
111
|
+
this.active = false
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private assertActive() {
|
|
115
|
+
if (!this.active || !this.state.active) {
|
|
116
|
+
throw new Error("Transaction connection is no longer active.")
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function query<Row>(db: Database, sql: string) {
|
|
122
|
+
return db.query(sql).all() as Row[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function run(db: Database, sql: string) {
|
|
126
|
+
db.run(sql)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function script(db: Database, statements: string[], begin: "BEGIN") {
|
|
130
|
+
await run(db, begin)
|
|
131
|
+
try {
|
|
132
|
+
for (const statement of statements) {
|
|
133
|
+
await run(db, statement)
|
|
134
|
+
}
|
|
135
|
+
await run(db, "COMMIT")
|
|
136
|
+
} catch (error) {
|
|
137
|
+
await run(db, "ROLLBACK")
|
|
138
|
+
throw error
|
|
139
|
+
}
|
|
27
140
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Fixtures, Schema, SchemaMembers, Seeds } from "../schemaBuilder"
|
|
2
|
-
import type { Connection, DefaultRow } from "../api"
|
|
2
|
+
import type { Connection, DefaultRow, Transaction } from "../api"
|
|
3
3
|
import type { SchemaHack } from "./cowboyMigration.types"
|
|
4
4
|
import { CowboyMigrator } from "./CowboyMigrator"
|
|
5
5
|
import { CowboySeeder } from "./CowboySeeder"
|
|
@@ -15,36 +15,68 @@ export class CowboyConnection implements Connection {
|
|
|
15
15
|
private readonly raw: Connection
|
|
16
16
|
private readonly schema: Schema<SchemaMembers>
|
|
17
17
|
private readonly options: CowboyOptions
|
|
18
|
-
private
|
|
18
|
+
private readiness?: Promise<void>
|
|
19
|
+
readonly transaction?: Transaction
|
|
19
20
|
|
|
20
|
-
constructor(
|
|
21
|
+
constructor(
|
|
22
|
+
raw: Connection,
|
|
23
|
+
schema: Schema<SchemaMembers>,
|
|
24
|
+
options?: CowboyOptions,
|
|
25
|
+
) {
|
|
21
26
|
this.raw = raw
|
|
22
27
|
this.schema = schema
|
|
23
28
|
this.options = options ?? {}
|
|
29
|
+
const transaction = raw.transaction?.bind(raw)
|
|
30
|
+
if (transaction) {
|
|
31
|
+
this.transaction = async (work) => {
|
|
32
|
+
await this.ready()
|
|
33
|
+
return transaction(work)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
async query<Row = DefaultRow>(sql: string) {
|
|
27
|
-
|
|
28
|
-
await this.shotgun()
|
|
29
|
-
}
|
|
39
|
+
await this.ready()
|
|
30
40
|
return this.raw.query(sql) as Promise<Row[]>
|
|
31
41
|
}
|
|
32
42
|
|
|
33
43
|
async script(statements: string[]) {
|
|
34
|
-
|
|
35
|
-
await this.shotgun()
|
|
36
|
-
}
|
|
44
|
+
await this.ready()
|
|
37
45
|
await this.raw.script(statements)
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
async reset() {
|
|
41
|
-
|
|
49
|
+
try {
|
|
50
|
+
await this.readiness
|
|
51
|
+
} catch {
|
|
52
|
+
// A failed initialization is already reset by ready().
|
|
53
|
+
}
|
|
54
|
+
this.readiness = undefined
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private ready() {
|
|
58
|
+
if (this.readiness) {
|
|
59
|
+
return this.readiness
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const readiness = this.shotgun().catch((error: unknown) => {
|
|
63
|
+
if (this.readiness === readiness) {
|
|
64
|
+
this.readiness = undefined
|
|
65
|
+
}
|
|
66
|
+
throw error
|
|
67
|
+
})
|
|
68
|
+
this.readiness = readiness
|
|
69
|
+
return readiness
|
|
42
70
|
}
|
|
43
71
|
|
|
44
72
|
private async shotgun() {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
73
|
+
await new CowboyMigrator(
|
|
74
|
+
this.raw,
|
|
75
|
+
this.schema,
|
|
76
|
+
this.options.hacks ?? [],
|
|
77
|
+
).migrate()
|
|
78
|
+
const hasSeeds =
|
|
79
|
+
this.options.seeds || (this.options.runFixtures && this.options.fixtures)
|
|
48
80
|
if (hasSeeds) {
|
|
49
81
|
await new CowboySeeder(
|
|
50
82
|
this.raw,
|
|
@@ -20,13 +20,15 @@ export class DeleteBuilder implements Delete<any, any> {
|
|
|
20
20
|
|
|
21
21
|
async execute(): Promise<any> {
|
|
22
22
|
const result = await this.connection.query(generateDelete(this.command))
|
|
23
|
-
return this.command.returning
|
|
23
|
+
return this.command.returning
|
|
24
|
+
? Mappings.results(this.table, result)
|
|
25
|
+
: result
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
returning(all: "*"): Delete<any, any[]>
|
|
27
|
-
returning(columns: string[]): Delete<any, Pick<any, any>[]>
|
|
28
|
-
returning(columns:
|
|
29
|
-
this.command.returning = columns === "*" ? ["*"] : columns
|
|
29
|
+
returning(...columns: string[]): Delete<any, Pick<any, any>[]>
|
|
30
|
+
returning(...columns: ("*" | string)[]): Delete<any, any[]> {
|
|
31
|
+
this.command.returning = columns[0] === "*" ? ["*"] : columns
|
|
30
32
|
return this
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -24,13 +24,15 @@ export class UpdateBuilder implements Update<any, any> {
|
|
|
24
24
|
|
|
25
25
|
async execute(): Promise<any> {
|
|
26
26
|
const result = await this.connection.query(generateUpdate(this.command))
|
|
27
|
-
return this.command.returning
|
|
27
|
+
return this.command.returning
|
|
28
|
+
? Mappings.results(this.table, result)
|
|
29
|
+
: result
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
returning(all: "*"): Update<any, any[]>
|
|
31
|
-
returning(columns: string[]): Update<any, Pick<any, any>[]>
|
|
32
|
-
returning(columns:
|
|
33
|
-
this.command.returning = columns === "*" ? ["*"] : columns
|
|
33
|
+
returning(...columns: string[]): Update<any, Pick<any, any>[]>
|
|
34
|
+
returning(...columns: ("*" | string)[]): Update<any, any[]> {
|
|
35
|
+
this.command.returning = columns[0] === "*" ? ["*"] : columns
|
|
34
36
|
return this
|
|
35
37
|
}
|
|
36
38
|
|