1
- import type { Connection , ConnectionPool } from './connections' ;
2
- import type { ConnectorType } from './connectors' ;
1
+ import {
2
+ SQL ,
3
+ type QueryResult ,
4
+ type QueryResultRow ,
5
+ type SQLCommandOptions ,
6
+ type SQLExecutor ,
7
+ type SQLQueryOptions ,
8
+ } from '../core' ;
9
+ import {
10
+ createConnectionPool ,
11
+ type Connection ,
12
+ type ConnectionPool ,
13
+ } from './connections' ;
14
+ import type { ConnectorType , ConnectorTypeParts } from './connectors' ;
3
15
4
16
export * from './connections' ;
5
17
export * from './connectors' ;
@@ -11,11 +23,267 @@ export * from './serializer';
11
23
export * from './sql' ;
12
24
export * from './tracing' ;
13
25
14
- export type DumboOptions < Connector extends ConnectorType = ConnectorType > = {
15
- connector ?: Connector ;
16
- } ;
17
-
18
26
export type Dumbo <
19
27
Connector extends ConnectorType = ConnectorType ,
20
28
ConnectionType extends Connection < Connector > = Connection < Connector > ,
21
29
> = ConnectionPool < ConnectionType > ;
30
+
31
+ export type PostgreSQLConnectionString =
32
+ | `postgresql://${string } `
33
+ | `postgres://${string } `;
34
+
35
+ export const postgreSQLConnectionString = (
36
+ connectionString : string ,
37
+ ) : PostgreSQLConnectionString => {
38
+ if (
39
+ ! connectionString . startsWith ( 'postgresql://' ) &&
40
+ ! connectionString . startsWith ( 'postgres://' )
41
+ ) {
42
+ throw new Error (
43
+ `Invalid PostgreSQL connection string: ${ connectionString } . It should start with "postgresql://".` ,
44
+ ) ;
45
+ }
46
+ return connectionString as PostgreSQLConnectionString ;
47
+ } ;
48
+
49
+ export type SQLiteConnectionString =
50
+ | `file:${string } `
51
+ | `:memory:`
52
+ | `/${string } `
53
+ | `./${string } `;
54
+
55
+ // Helper type to infer the connector type based on connection string
56
+ export type InferConnector < T extends DatabaseConnectionString > =
57
+ T extends PostgreSQLConnectionString
58
+ ? ConnectorType < 'PostgreSQL' , 'pg' >
59
+ : T extends SQLiteConnectionString
60
+ ? ConnectorType < 'SQLite' , 'sqlite3' >
61
+ : never ;
62
+
63
+ // Helper type to infer the connection type based on connection string
64
+ export type InferConnection < T extends DatabaseConnectionString > =
65
+ T extends PostgreSQLConnectionString
66
+ ? Connection < ConnectorType < 'PostgreSQL' , 'pg' > >
67
+ : T extends SQLiteConnectionString
68
+ ? Connection < ConnectorType < 'SQLite' , 'sqlite3' > >
69
+ : never ;
70
+
71
+ export type DatabaseConnectionString =
72
+ | PostgreSQLConnectionString
73
+ | SQLiteConnectionString ;
74
+
75
+ export type DumboConnectionOptions <
76
+ T extends DatabaseConnectionString = DatabaseConnectionString ,
77
+ > = {
78
+ connectionString : T ;
79
+ } ;
80
+
81
+ export const createLazyExecutor = (
82
+ importExecutor : ( ) => Promise < SQLExecutor > ,
83
+ ) : SQLExecutor => {
84
+ let executor : SQLExecutor | null = null ;
85
+
86
+ const getExecutor = async ( ) : Promise < SQLExecutor > => {
87
+ if ( ! executor ) {
88
+ try {
89
+ executor = await importExecutor ( ) ;
90
+ } catch ( error ) {
91
+ throw new Error (
92
+ `Failed to import SQL executor: ${ error instanceof Error ? error . message : String ( error ) } ` ,
93
+ ) ;
94
+ }
95
+ }
96
+ return executor ;
97
+ } ;
98
+
99
+ return {
100
+ query : async < Result extends QueryResultRow = QueryResultRow > (
101
+ sql : SQL ,
102
+ options ?: SQLQueryOptions ,
103
+ ) : Promise < QueryResult < Result > > => {
104
+ const exec = await getExecutor ( ) ;
105
+ return exec . query < Result > ( sql , options ) ;
106
+ } ,
107
+
108
+ batchQuery : async < Result extends QueryResultRow = QueryResultRow > (
109
+ sqls : SQL [ ] ,
110
+ options ?: SQLQueryOptions ,
111
+ ) : Promise < QueryResult < Result > [ ] > => {
112
+ const exec = await getExecutor ( ) ;
113
+ return exec . batchQuery < Result > ( sqls , options ) ;
114
+ } ,
115
+
116
+ command : async < Result extends QueryResultRow = QueryResultRow > (
117
+ sql : SQL ,
118
+ options ?: SQLCommandOptions ,
119
+ ) : Promise < QueryResult < Result > > => {
120
+ const exec = await getExecutor ( ) ;
121
+ return exec . command < Result > ( sql , options ) ;
122
+ } ,
123
+
124
+ batchCommand : async < Result extends QueryResultRow = QueryResultRow > (
125
+ sqls : SQL [ ] ,
126
+ options ?: SQLCommandOptions ,
127
+ ) : Promise < QueryResult < Result > [ ] > => {
128
+ const exec = await getExecutor ( ) ;
129
+ return exec . batchCommand < Result > ( sqls , options ) ;
130
+ } ,
131
+ } ;
132
+ } ;
133
+
134
+ export const createDeferredConnection = < Connector extends ConnectorType > (
135
+ connector : Connector ,
136
+ importConnection : ( ) => Promise < Connection < Connector > > ,
137
+ ) : Connection < Connector > => {
138
+ const getConnection = importConnection ( ) ;
139
+
140
+ const execute = createLazyExecutor ( async ( ) => {
141
+ const conn = await getConnection ;
142
+ return conn . execute ;
143
+ } ) ;
144
+
145
+ const connection : Connection < Connector > = {
146
+ connector,
147
+ execute,
148
+
149
+ open : async ( ) : Promise < unknown > => {
150
+ const conn = await getConnection ;
151
+ return conn . open ( ) ;
152
+ } ,
153
+
154
+ close : async ( ) : Promise < void > => {
155
+ if ( getConnection ) {
156
+ const conn = await getConnection ;
157
+ await conn . close ( ) ;
158
+ }
159
+ } ,
160
+
161
+ transaction : ( ) => {
162
+ const transaction = getConnection . then ( ( c ) => c . transaction ( ) ) ;
163
+
164
+ return {
165
+ connector,
166
+ connection,
167
+ execute : createLazyExecutor ( async ( ) => ( await transaction ) . execute ) ,
168
+ begin : async ( ) => ( await transaction ) . begin ( ) ,
169
+ commit : async ( ) => ( await transaction ) . commit ( ) ,
170
+ rollback : async ( ) => ( await transaction ) . rollback ( ) ,
171
+ } ;
172
+ } ,
173
+ withTransaction : async ( handle ) => {
174
+ const connection = await getConnection ;
175
+ return connection . withTransaction ( handle ) ;
176
+ } ,
177
+ } ;
178
+
179
+ return connection ;
180
+ } ;
181
+
182
+ export const createDeferredConnectionPool = < Connector extends ConnectorType > (
183
+ connector : Connector ,
184
+ importPool : ( ) => Promise < ConnectionPool < Connection < Connector > > > ,
185
+ ) : ConnectionPool < Connection < Connector > > => {
186
+ let poolPromise : Promise < ConnectionPool < Connection < Connector > > > | null = null ;
187
+
188
+ const getPool = async ( ) : Promise < ConnectionPool < Connection < Connector > > > => {
189
+ if ( ! poolPromise ) {
190
+ try {
191
+ poolPromise = importPool ( ) ;
192
+ } catch ( error ) {
193
+ throw new Error (
194
+ `Failed to import connection pool: ${ error instanceof Error ? error . message : String ( error ) } ` ,
195
+ ) ;
196
+ }
197
+ }
198
+ return poolPromise ;
199
+ } ;
200
+
201
+ return createConnectionPool ( {
202
+ connector,
203
+ close : async ( ) => {
204
+ if ( ! poolPromise ) return ;
205
+ const pool = await poolPromise ;
206
+ await pool . close ( ) ;
207
+ poolPromise = null ;
208
+ } ,
209
+ getConnection : ( ) =>
210
+ createDeferredConnection ( connector , async ( ) =>
211
+ ( await getPool ( ) ) . connection ( ) ,
212
+ ) ,
213
+ } ) ;
214
+ } ;
215
+
216
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
217
+ const importDrivers : Record < string , ( ) => Promise < any > > = {
218
+ 'postgresql:pg' : ( ) => import ( '../storage/postgresql/pg' ) ,
219
+ 'sqlite:sqlite3' : ( ) => import ( '../storage/sqlite/sqlite3' ) ,
220
+ } ;
221
+
222
+ export const parseConnectionString = (
223
+ connectionString : DatabaseConnectionString ,
224
+ ) : ConnectorTypeParts => {
225
+ if (
226
+ connectionString . startsWith ( 'postgresql://' ) ||
227
+ connectionString . startsWith ( 'postgres://' )
228
+ ) {
229
+ return {
230
+ databaseType : 'postgresql' ,
231
+ driverName : 'pg' ,
232
+ } ;
233
+ }
234
+
235
+ if (
236
+ connectionString . startsWith ( 'file:' ) ||
237
+ connectionString === ':memory:' ||
238
+ connectionString . startsWith ( '/' ) ||
239
+ connectionString . startsWith ( './' )
240
+ ) {
241
+ return {
242
+ databaseType : 'sqlite' ,
243
+ driverName : 'sqlite3' ,
244
+ } ;
245
+ }
246
+
247
+ throw new Error (
248
+ `Unsupported database connection string: ${ connectionString } ` ,
249
+ ) ;
250
+ } ;
251
+
252
+ export function dumbo <
253
+ ConnectionString extends DatabaseConnectionString ,
254
+ DatabaseOptions extends DumboConnectionOptions < ConnectionString > ,
255
+ > ( options : DatabaseOptions ) : ConnectionPool < InferConnection < ConnectionString > > {
256
+ const { connectionString } = options ;
257
+
258
+ const { databaseType, driverName } = parseConnectionString ( connectionString ) ;
259
+
260
+ const connector : InferConnection < ConnectionString > [ 'connector' ] =
261
+ `${ databaseType } :${ driverName } ` as InferConnection < ConnectionString > [ 'connector' ] ;
262
+
263
+ const importDriver = importDrivers [ connector ] ;
264
+ if ( ! importDriver ) {
265
+ throw new Error ( `Unsupported connector: ${ connector } ` ) ;
266
+ }
267
+
268
+ const importAndCreatePool = async ( ) => {
269
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
270
+ const module = await importDriver ( ) ;
271
+
272
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
273
+ const poolFactory : ( options : {
274
+ connectionString : string ;
275
+ } ) => ConnectionPool < InferConnection < ConnectionString > > =
276
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
277
+ 'dumbo' in module ? module . dumbo : undefined ;
278
+
279
+ if ( poolFactory === undefined )
280
+ throw new Error ( `No pool factory found for connector: ${ connector } ` ) ;
281
+
282
+ return poolFactory ( { connectionString } ) ;
283
+ } ;
284
+
285
+ return createDeferredConnectionPool (
286
+ connector ,
287
+ importAndCreatePool ,
288
+ ) as ConnectionPool < InferConnection < ConnectionString > > ;
289
+ }
0 commit comments