@sigitex/outlaw 1.0.1 → 2.0.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 +193 -6
- package/package.json +6 -6
- package/src/api/DatabaseTable.ts +57 -31
- package/src/api/DatabaseView.ts +36 -13
- package/src/api/UnsupportedTransactionError.ts +7 -0
- package/src/api/api.types.ts +73 -100
- 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/framework/Format.ts +4 -0
- package/src/framework/definitions.d.ts +4 -1
- package/src/queryBuilder/DeleteBuilder.ts +6 -4
- package/src/queryBuilder/Mappings.ts +1 -1
- package/src/queryBuilder/Projection.ts +83 -0
- package/src/queryBuilder/QueryScope.ts +272 -0
- package/src/queryBuilder/QuerySource.ts +231 -0
- package/src/queryBuilder/QuerySourceBuilder.ts +76 -0
- package/src/queryBuilder/SelectBuilder.ts +14 -20
- package/src/queryBuilder/SelectQueryBuilder.ts +61 -72
- package/src/queryBuilder/UpdateBuilder.ts +6 -4
- package/src/queryBuilder/index.ts +2 -0
- package/src/queryBuilder/queryBuilders.types.ts +21 -16
- package/src/queryGenerator/Clause.ts +36 -35
- package/src/queryGenerator/generateSelect.ts +38 -73
- package/src/schemaBuilder/ColumnRef.ts +36 -0
- package/src/schemaBuilder/columnBuilders.ts +7 -2
- package/src/schemaBuilder/createTable.ts +33 -26
- package/src/schemaBuilder/createView.ts +24 -12
- package/src/schemaBuilder/index.ts +1 -0
- package/src/schemaBuilder/metadata.ts +12 -6
- package/src/schemaBuilder/schemaBuilder.types.ts +57 -30
package/src/api/api.types.ts
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
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
|
-
|
|
6
|
-
BuildTable,
|
|
7
|
-
BuildView,
|
|
8
|
-
ColumnRef,
|
|
6
|
+
InferColumn,
|
|
9
7
|
SchemaMembers,
|
|
10
8
|
TablesOf,
|
|
11
9
|
ViewsOf,
|
|
12
10
|
} from "../schemaBuilder"
|
|
11
|
+
import type { QueryScope } from "../queryBuilder/QueryScope"
|
|
13
12
|
|
|
14
13
|
// Utilities
|
|
15
14
|
|
|
@@ -26,59 +25,82 @@ export type InsertRecord<R> = Partial<R> &
|
|
|
26
25
|
|
|
27
26
|
// Deconstruction
|
|
28
27
|
|
|
29
|
-
export type ColumnsOf<BT> =
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
28
|
+
export type ColumnsOf<BT> = BT extends { readonly $columns: infer Columns }
|
|
29
|
+
? Columns
|
|
30
|
+
: never
|
|
38
31
|
|
|
39
32
|
export type ColumnTypesOf<BCS> = {
|
|
40
|
-
readonly [BCK in keyof BCS]:
|
|
33
|
+
readonly [BCK in keyof BCS]: InferColumn<BCS[BCK]>
|
|
41
34
|
}
|
|
42
35
|
|
|
43
36
|
// API
|
|
44
37
|
|
|
45
38
|
export type DefaultRow = Record<string, unknown>
|
|
46
39
|
|
|
47
|
-
export type
|
|
40
|
+
export type ConnectionOperations = {
|
|
48
41
|
readonly query: <Row = DefaultRow>(sql: string) => Promise<Row[]>
|
|
49
42
|
readonly script: (statements: string[]) => Promise<void>
|
|
50
|
-
readonly reset?: () => Promise<void>
|
|
51
43
|
}
|
|
52
44
|
|
|
53
|
-
export type
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
45
|
+
export type TransactionWork<Result> = (
|
|
46
|
+
connection: TransactionalConnection,
|
|
47
|
+
) => Promise<Result>
|
|
48
|
+
|
|
49
|
+
export type Transaction = <Result>(
|
|
50
|
+
work: TransactionWork<Result>,
|
|
51
|
+
) => Promise<Result>
|
|
52
|
+
|
|
53
|
+
export type TransactionalConnection = ConnectionOperations & {
|
|
54
|
+
readonly transaction: Transaction
|
|
59
55
|
}
|
|
60
56
|
|
|
61
|
-
export type
|
|
57
|
+
export type Connection = ConnectionOperations & {
|
|
58
|
+
readonly reset?: () => Promise<void>
|
|
59
|
+
readonly transaction?: Transaction
|
|
60
|
+
}
|
|
62
61
|
|
|
63
|
-
export type
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
62
|
+
export type DatabaseApi<
|
|
63
|
+
M extends SchemaMembers,
|
|
64
|
+
C extends Connection = Connection,
|
|
65
|
+
> = {
|
|
66
|
+
readonly [K in keyof TablesOf<M>]: TableApi<
|
|
67
|
+
ColumnsOf<TablesOf<M>[K]>,
|
|
68
|
+
QueryScope.Of<TablesOf<M>[K]>["name"]
|
|
69
|
+
>
|
|
70
|
+
} & {
|
|
71
|
+
readonly [K in keyof ViewsOf<M>]: ViewApi<
|
|
72
|
+
QueryScope.Of<ViewsOf<M>[K]>["columns"],
|
|
73
|
+
QueryScope.Of<ViewsOf<M>[K]>["name"]
|
|
74
|
+
>
|
|
75
|
+
} & {
|
|
76
|
+
readonly connection: C
|
|
77
|
+
readonly transaction: <Result>(
|
|
78
|
+
work: (
|
|
79
|
+
database: DatabaseApi<M, TransactionalConnection>,
|
|
80
|
+
) => Promise<Result>,
|
|
81
|
+
) => Promise<Result>
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
export type
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
84
|
+
export type ColumnsOfView<BV> = QueryScope.Of<BV>["columns"]
|
|
85
|
+
|
|
86
|
+
export type ViewApi<
|
|
87
|
+
Columns extends QueryScope.Columns,
|
|
88
|
+
Name extends string = string,
|
|
89
|
+
> = QueryScope.Composition<
|
|
90
|
+
QueryScope.Initial<QueryScope.State<Name, Columns>>,
|
|
91
|
+
true
|
|
92
|
+
>
|
|
93
|
+
|
|
94
|
+
export type TableApi<
|
|
95
|
+
Columns,
|
|
96
|
+
Name extends string = string,
|
|
97
|
+
> = QueryScope.Composition<
|
|
98
|
+
QueryScope.Initial<QueryScope.State<Name, QueryScope.SchemaColumns<Columns>>>,
|
|
99
|
+
true
|
|
100
|
+
> & {
|
|
79
101
|
/** Issue an `INSERT` statement. */
|
|
80
102
|
insert<InsertColumns extends Partial<ColumnTypesOf<Columns>>>(
|
|
81
|
-
...rows: InsertColumns[]
|
|
103
|
+
...rows: [InsertColumns, ...InsertColumns[]]
|
|
82
104
|
): Insert<InsertColumns, Columns, number>
|
|
83
105
|
/** Issue an `UPDATE` statement. */
|
|
84
106
|
update(row: Partial<ColumnTypesOf<Columns>>): Update<Columns, number>
|
|
@@ -91,7 +113,7 @@ export interface HasWhereClause<Columns> {
|
|
|
91
113
|
/** Add a condition where the given column equals the given value. */
|
|
92
114
|
where<Column extends keyof Columns>(
|
|
93
115
|
column: Column,
|
|
94
|
-
value: Columns[Column]
|
|
116
|
+
value: InferColumn<Columns[Column]>,
|
|
95
117
|
): this
|
|
96
118
|
/** Add a unary `WHERE` condition. */
|
|
97
119
|
where<Column extends keyof Columns>(
|
|
@@ -102,63 +124,14 @@ export interface HasWhereClause<Columns> {
|
|
|
102
124
|
where<Column extends keyof Columns>(
|
|
103
125
|
column: Column,
|
|
104
126
|
operator: BinaryOperator,
|
|
105
|
-
value:
|
|
127
|
+
value: InferColumn<Columns[Column]>,
|
|
106
128
|
): this
|
|
107
129
|
}
|
|
108
130
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
extends
|
|
112
|
-
|
|
113
|
-
fetch(): Promise<ColumnTypesOf<SelectColumns>[]>
|
|
114
|
-
/** Issue the query, returning a single result, or throwing.. */
|
|
115
|
-
first(): Promise<ColumnTypesOf<SelectColumns>>
|
|
116
|
-
/** Add a `LIMIT` clause. */
|
|
117
|
-
limit(n: number): this
|
|
118
|
-
/** Add an `OFFSET` clause. */
|
|
119
|
-
offset(n: number): this
|
|
120
|
-
/** Add an `ORDER BY` sort expression. */
|
|
121
|
-
orderBy(sorts: [keyof Columns, "asc" | "desc"][]): this
|
|
122
|
-
|
|
123
|
-
/** Inner join on a table. */
|
|
124
|
-
join<JC>(
|
|
125
|
-
table: BuildTable<JC>,
|
|
126
|
-
): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
|
|
127
|
-
/** Inner join on a subquery. */
|
|
128
|
-
join<SC>(
|
|
129
|
-
query: Select<SC, any>,
|
|
130
|
-
): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
|
|
131
|
-
|
|
132
|
-
/** Left join on a table — joined columns become nullable. */
|
|
133
|
-
leftJoin<JC>(
|
|
134
|
-
table: BuildTable<JC>,
|
|
135
|
-
): Select<SelectColumns & Partial<ColumnTypesOf<JC>>, Columns & JC>
|
|
136
|
-
/** Left join on a subquery — joined columns become nullable. */
|
|
137
|
-
leftJoin<SC>(
|
|
138
|
-
query: Select<SC, any>,
|
|
139
|
-
): Select<SelectColumns & Partial<ColumnTypesOf<SC>>, Columns & SC>
|
|
140
|
-
|
|
141
|
-
/** Right join on a table. */
|
|
142
|
-
rightJoin<JC>(
|
|
143
|
-
table: BuildTable<JC>,
|
|
144
|
-
): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
|
|
145
|
-
/** Right join on a subquery. */
|
|
146
|
-
rightJoin<SC>(
|
|
147
|
-
query: Select<SC, any>,
|
|
148
|
-
): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
|
|
149
|
-
|
|
150
|
-
/** Cross join on a table. */
|
|
151
|
-
crossJoin<JC>(
|
|
152
|
-
table: BuildTable<JC>,
|
|
153
|
-
): Select<SelectColumns & ColumnTypesOf<JC>, Columns & JC>
|
|
154
|
-
/** Cross join on a subquery. */
|
|
155
|
-
crossJoin<SC>(
|
|
156
|
-
query: Select<SC, any>,
|
|
157
|
-
): Select<SelectColumns & ColumnTypesOf<SC>, Columns & SC>
|
|
158
|
-
|
|
159
|
-
/** Add an ON condition to the most recent join. */
|
|
160
|
-
on(left: ColumnRef, operator: BinaryOperator, right: ColumnRef): this
|
|
161
|
-
}
|
|
131
|
+
export type Select<
|
|
132
|
+
SelectColumns extends QueryScope.Columns,
|
|
133
|
+
Scope extends QueryScope,
|
|
134
|
+
> = QueryScope.Selection<SelectColumns, Scope, true>
|
|
162
135
|
|
|
163
136
|
/** Insert statement API. */
|
|
164
137
|
export interface Insert<InsertColumns, Columns, Returning> {
|
|
@@ -177,11 +150,11 @@ export interface Update<Columns, Returning> extends HasWhereClause<Columns> {
|
|
|
177
150
|
/** Execute the statement. */
|
|
178
151
|
execute(): Promise<Returning>
|
|
179
152
|
/** Specify a `RETURNING *` clause. */
|
|
180
|
-
returning(all: "*"): Update<Columns, Columns[]>
|
|
153
|
+
returning(all: "*"): Update<Columns, ColumnTypesOf<Columns>[]>
|
|
181
154
|
/** Specify a `RETURNING` clause with the chosen columns. */
|
|
182
155
|
returning<Column extends keyof Columns>(
|
|
183
|
-
columns: Column[]
|
|
184
|
-
): Update<Columns, Pick<Columns, Column
|
|
156
|
+
...columns: Column[]
|
|
157
|
+
): Update<Columns, ColumnTypesOf<Pick<Columns, Column>>[]>
|
|
185
158
|
}
|
|
186
159
|
|
|
187
160
|
/** Delete statement API. */
|
|
@@ -189,11 +162,11 @@ export interface Delete<Columns, Returning> extends HasWhereClause<Columns> {
|
|
|
189
162
|
/** Execute the statement. */
|
|
190
163
|
execute(): Promise<Returning>
|
|
191
164
|
/** Specify a `RETURNING *` clause. */
|
|
192
|
-
returning(all: "*"): Delete<Columns, Columns[]>
|
|
165
|
+
returning(all: "*"): Delete<Columns, ColumnTypesOf<Columns>[]>
|
|
193
166
|
/** Specify a `RETURNING` clause with the chosen columns. */
|
|
194
167
|
returning<Column extends keyof Columns>(
|
|
195
|
-
columns: Column[]
|
|
196
|
-
): Delete<Columns, Pick<Columns, Column
|
|
168
|
+
...columns: Column[]
|
|
169
|
+
): Delete<Columns, ColumnTypesOf<Pick<Columns, Column>>[]>
|
|
197
170
|
}
|
|
198
171
|
|
|
199
172
|
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,
|
package/src/framework/Format.ts
CHANGED
|
@@ -11,6 +11,10 @@ export namespace Format {
|
|
|
11
11
|
return SqlString.escapeId(name)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
export function identifier(name: string) {
|
|
15
|
+
return SqlString.escapeId(name, true)
|
|
16
|
+
}
|
|
17
|
+
|
|
14
18
|
export function number(number: number) {
|
|
15
19
|
if (typeof number !== "number" && typeof number !== "bigint") {
|
|
16
20
|
throw new Error("Not a number.")
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
declare module "sqlstring-sqlite" {
|
|
2
2
|
export function escape(text: string | null | undefined): string
|
|
3
|
-
export function escapeId(
|
|
3
|
+
export function escapeId(
|
|
4
|
+
text: string | null | undefined,
|
|
5
|
+
forbidQualified?: boolean,
|
|
6
|
+
): string
|
|
4
7
|
}
|
|
@@ -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
|
|
|
@@ -41,7 +41,7 @@ export namespace Mappings {
|
|
|
41
41
|
return rows.map((row) => {
|
|
42
42
|
const mapped: Record<string, unknown> = { ...row }
|
|
43
43
|
for (const [name, mapping] of mappings) {
|
|
44
|
-
if (name in mapped) {
|
|
44
|
+
if (name in mapped && mapped[name] !== null) {
|
|
45
45
|
mapped[name] = mapping.from(mapped[name])
|
|
46
46
|
}
|
|
47
47
|
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import type { ColumnRef } from "../schemaBuilder/ColumnRef"
|
|
2
|
+
import type { ColumnData } from "../schemaBuilder/metadata"
|
|
3
|
+
import type { ColumnIdentifier, SelectQuery } from "./queryBuilders.types"
|
|
4
|
+
import { QuerySource } from "./QuerySource"
|
|
5
|
+
|
|
6
|
+
export type Projection =
|
|
7
|
+
| "*"
|
|
8
|
+
| { column: ColumnIdentifier; alias?: string }
|
|
9
|
+
| ColumnRef.Wildcard
|
|
10
|
+
|
|
11
|
+
export namespace Projection {
|
|
12
|
+
export type Input =
|
|
13
|
+
| "*"
|
|
14
|
+
| string
|
|
15
|
+
| ColumnRef
|
|
16
|
+
| ColumnRef.Aliased<string, string, string>
|
|
17
|
+
| ColumnRef.Wildcard
|
|
18
|
+
|
|
19
|
+
export function create(scope: QuerySource.Scope, input: Input): Projection {
|
|
20
|
+
if (input === "*") {
|
|
21
|
+
return input
|
|
22
|
+
}
|
|
23
|
+
if (typeof input === "object" && "wildcard" in input) {
|
|
24
|
+
return { ...input }
|
|
25
|
+
}
|
|
26
|
+
const column = QuerySource.identifier(scope, input)
|
|
27
|
+
const alias =
|
|
28
|
+
typeof input === "object" && "alias" in input
|
|
29
|
+
? input.alias
|
|
30
|
+
: column.table === undefined && scope.coalesced.has(column.column)
|
|
31
|
+
? column.column
|
|
32
|
+
: undefined
|
|
33
|
+
return {
|
|
34
|
+
column,
|
|
35
|
+
...(alias !== undefined ? { alias } : {}),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function columns(query: SelectQuery): ColumnData[] {
|
|
40
|
+
const scope = QuerySource.scope(query)
|
|
41
|
+
const selected = query.selected.flatMap((projection) => {
|
|
42
|
+
if (projection === "*") {
|
|
43
|
+
return scope.output
|
|
44
|
+
}
|
|
45
|
+
if ("wildcard" in projection) {
|
|
46
|
+
return scope.sources
|
|
47
|
+
.filter(
|
|
48
|
+
(source) => QuerySource.qualifier(source) === projection.table,
|
|
49
|
+
)
|
|
50
|
+
.flatMap((source) => source.tableData.columns)
|
|
51
|
+
}
|
|
52
|
+
return QuerySource.resolve(scope, projection.column).map((column) =>
|
|
53
|
+
renameColumn(column, projection.alias ?? column.name),
|
|
54
|
+
)
|
|
55
|
+
})
|
|
56
|
+
const outputs = new Map<string, ColumnData[]>()
|
|
57
|
+
for (const column of selected) {
|
|
58
|
+
const existing = outputs.get(column.name)
|
|
59
|
+
if (existing) {
|
|
60
|
+
existing.push(column)
|
|
61
|
+
} else {
|
|
62
|
+
outputs.set(column.name, [column])
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return Array.from(outputs.values(), merge)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function merge(columns: ColumnData[]): ColumnData {
|
|
69
|
+
const first = columns[0]
|
|
70
|
+
return {
|
|
71
|
+
...first,
|
|
72
|
+
notNull: columns.every((column) => column.notNull || !!column.primaryKey),
|
|
73
|
+
primaryKey: undefined,
|
|
74
|
+
mapping: columns.every((column) => column.mapping === first.mapping)
|
|
75
|
+
? first.mapping
|
|
76
|
+
: undefined,
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function renameColumn(column: ColumnData, name: string): ColumnData {
|
|
81
|
+
return { ...column, name }
|
|
82
|
+
}
|
|
83
|
+
}
|