bunigniter 0.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/LICENSE +21 -0
- package/README.md +229 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +229 -0
- package/dist/base/controller.ts +324 -0
- package/dist/base/index.ts +5 -0
- package/dist/base/service.ts +21 -0
- package/dist/cli/index.ts +318 -0
- package/dist/cli/list-routes.ts +72 -0
- package/dist/cli/repl.ts +461 -0
- package/dist/cli/templates.ts +283 -0
- package/dist/client/index.ts +159 -0
- package/dist/db/drizzle.ts +550 -0
- package/dist/db/validators.ts +229 -0
- package/dist/edge-builder.ts +120 -0
- package/dist/edge.ts +69 -0
- package/dist/helpers/cache.ts +173 -0
- package/dist/helpers/cors.ts +103 -0
- package/dist/helpers/csrf.ts +155 -0
- package/dist/helpers/debug.ts +158 -0
- package/dist/helpers/env.ts +147 -0
- package/dist/helpers/handler.ts +158 -0
- package/dist/helpers/http.ts +194 -0
- package/dist/helpers/image.ts +217 -0
- package/dist/helpers/jwt.ts +147 -0
- package/dist/helpers/logger.ts +96 -0
- package/dist/helpers/mail.ts +272 -0
- package/dist/helpers/middleware-loader.ts +116 -0
- package/dist/helpers/middleware.ts +57 -0
- package/dist/helpers/modules.ts +115 -0
- package/dist/helpers/openapi.ts +140 -0
- package/dist/helpers/pagination.ts +159 -0
- package/dist/helpers/queue.ts +186 -0
- package/dist/helpers/request-context.ts +13 -0
- package/dist/helpers/request.ts +376 -0
- package/dist/helpers/schedule.ts +173 -0
- package/dist/helpers/session-middleware.ts +89 -0
- package/dist/helpers/session.ts +286 -0
- package/dist/helpers/sse.ts +90 -0
- package/dist/helpers/throttle.ts +156 -0
- package/dist/helpers/upload.ts +417 -0
- package/dist/helpers/validator.ts +287 -0
- package/dist/helpers/ws.ts +123 -0
- package/dist/index.ts +221 -0
- package/dist/package.json +70 -0
- package/dist/router/file-router.ts +541 -0
- package/dist/router/server-router.ts +103 -0
- package/dist/view/page.ts +96 -0
- package/dist/view/renderer.tsx +390 -0
- package/dist/view/view-response.ts +10 -0
- package/package.json +70 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle wrapper — CodeIgniter-style database interface.
|
|
3
|
+
*
|
|
4
|
+
* Provides `db.query('SQL', [params])` for raw SQL with parameter binding,
|
|
5
|
+
* plus full Drizzle ORM access for type-safe queries.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* // Raw SQL (CodeIgniter style)
|
|
10
|
+
* const users = await db.query('SELECT * FROM users WHERE id = ?', [1])
|
|
11
|
+
*
|
|
12
|
+
* // Drizzle ORM (type-safe)
|
|
13
|
+
* const rows = await db.select().from(users).all()
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { type ExtractTablesWithRelations } from 'drizzle-orm'
|
|
17
|
+
import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core'
|
|
18
|
+
import type { BunSQLiteDatabase } from 'drizzle-orm/bun-sqlite'
|
|
19
|
+
|
|
20
|
+
/** Dialect types supported. */
|
|
21
|
+
export type Dialect = 'postgres' | 'mysql' | 'sqlite' | 'bun-sqlite' | 'd1'
|
|
22
|
+
|
|
23
|
+
/** Database configuration. */
|
|
24
|
+
export interface DbConfig {
|
|
25
|
+
dialect: Dialect
|
|
26
|
+
connection: Record<string, any>
|
|
27
|
+
logging?: boolean
|
|
28
|
+
autoMigrate?: boolean
|
|
29
|
+
migrationsFolder?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Query result from raw SQL. */
|
|
33
|
+
export interface QueryResult<T = any> {
|
|
34
|
+
rows: T[]
|
|
35
|
+
affectedRows: number
|
|
36
|
+
insertId?: number | string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Thin wrapper around Drizzle ORM.
|
|
41
|
+
*
|
|
42
|
+
* Wraps a Drizzle database client and exposes:
|
|
43
|
+
* - `query(sql, params)` — CodeIgniter-style raw SQL
|
|
44
|
+
* - `select()` / `insert(table)` / `update(table)` / `delete(table)` — Drizzle builders
|
|
45
|
+
* - `transaction(fn)` — ACID transactions
|
|
46
|
+
*/
|
|
47
|
+
export class DbClient {
|
|
48
|
+
private client: any
|
|
49
|
+
private dialect: Dialect
|
|
50
|
+
private rawExecutor: RawExecutor | null = null
|
|
51
|
+
private opened = false
|
|
52
|
+
private config: DbConfig
|
|
53
|
+
|
|
54
|
+
constructor(config: DbConfig) {
|
|
55
|
+
this.config = config
|
|
56
|
+
this.dialect = config.dialect
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Initialize the database connection. */
|
|
60
|
+
async open(): Promise<void> {
|
|
61
|
+
if (this.opened) return
|
|
62
|
+
const drv = await resolveDriver(this.dialect, this.config)
|
|
63
|
+
this.client = drv.db
|
|
64
|
+
this.rawExecutor = drv.rawExecutor ?? null
|
|
65
|
+
this.opened = true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The raw Drizzle client for type-safe queries. */
|
|
69
|
+
get drizzle(): any {
|
|
70
|
+
this.assertOpen()
|
|
71
|
+
return this.client
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Database dialect name. */
|
|
75
|
+
get dialectName(): Dialect {
|
|
76
|
+
return this.dialect
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Raw SQL (CodeIgniter style) ─────────────────────────────
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Execute a parameterized SQL query.
|
|
83
|
+
*
|
|
84
|
+
* @example
|
|
85
|
+
* ```ts
|
|
86
|
+
* const users = await db.query('SELECT * FROM users WHERE id = ?', [1])
|
|
87
|
+
* const result = await db.query('INSERT INTO users (name) VALUES (?)', ['Alice'])
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
/**
|
|
91
|
+
* Tagged template SQL — Drizzle-style `sql\`...\``.
|
|
92
|
+
* Inline parameters, no need for separate params array.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```ts
|
|
96
|
+
* await db.sql\`SELECT * FROM users WHERE id = ${id}\`
|
|
97
|
+
* await db.sql\`UPDATE posts SET title = ${title} WHERE id = ${id}\`
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
async sql(strings: TemplateStringsArray, ...values: unknown[]): Promise<QueryResult<any>> {
|
|
101
|
+
let s = strings[0] ?? ''
|
|
102
|
+
for (let i = 1; i < strings.length; i++) s += '?' + strings[i]
|
|
103
|
+
return this.query(s, values)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ─── CodeIgniter-style Active Record ─────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Insert a record.
|
|
110
|
+
*
|
|
111
|
+
* @example await db.insert('users', { name: 'Alice', email: 'a@b.com' })
|
|
112
|
+
*/
|
|
113
|
+
async insert(table: string, data: Record<string, any>): Promise<QueryResult> {
|
|
114
|
+
const keys = Object.keys(data)
|
|
115
|
+
const vals = Object.values(data)
|
|
116
|
+
return this.query(
|
|
117
|
+
`INSERT INTO ${table} (${keys.join(', ')}) VALUES (${keys.map(() => '?').join(', ')})`,
|
|
118
|
+
vals
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Update records.
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* await db.update('users', { name: 'Bob' }, { id: 1 })
|
|
127
|
+
* await db.update('posts', { views: 0 }, { views: ['<', 0] }) // views < 0
|
|
128
|
+
*/
|
|
129
|
+
async update(table: string, data: Record<string, any>, where: Record<string, any>): Promise<QueryResult> {
|
|
130
|
+
const setCols = Object.keys(data).map(k => `${k} = ?`)
|
|
131
|
+
const { clause, vals } = buildWhere(where)
|
|
132
|
+
return this.query(`UPDATE ${table} SET ${setCols.join(', ')} WHERE ${clause}`, [...Object.values(data), ...vals])
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Delete records.
|
|
137
|
+
*
|
|
138
|
+
* @example await db.delete('users', { id: 1 })
|
|
139
|
+
* @example await db.delete('posts', { createdAt: ['<', '2024-01-01'] })
|
|
140
|
+
*/
|
|
141
|
+
async delete(table: string, where: Record<string, any>): Promise<QueryResult> {
|
|
142
|
+
const { clause, vals } = buildWhere(where)
|
|
143
|
+
return this.query(`DELETE FROM ${table} WHERE ${clause}`, vals)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Select records with ordering and limits.
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* const all = await db.get('users')
|
|
151
|
+
* const user = await db.get('users', { id: 1 })
|
|
152
|
+
* const recent = await db.get('posts', { status: 'published' }, { orderBy: 'created_at DESC', limit: 10 })
|
|
153
|
+
* const admins = await db.get('users', { role: 'admin', age: ['>=', 18] })
|
|
154
|
+
*/
|
|
155
|
+
async get<T = any>(table: string, where?: Record<string, any> | null, options?: {
|
|
156
|
+
select?: string
|
|
157
|
+
orderBy?: string
|
|
158
|
+
limit?: number
|
|
159
|
+
offset?: number
|
|
160
|
+
groupBy?: string
|
|
161
|
+
having?: Record<string, any>
|
|
162
|
+
}): Promise<T[]> {
|
|
163
|
+
let sql = `SELECT ${options?.select ?? '*'} FROM ${table}`
|
|
164
|
+
const params: unknown[] = []
|
|
165
|
+
|
|
166
|
+
if (where && Object.keys(where).length > 0) {
|
|
167
|
+
const r = buildWhere(where)
|
|
168
|
+
sql += ` WHERE ${r.clause}`
|
|
169
|
+
params.push(...r.vals)
|
|
170
|
+
}
|
|
171
|
+
if (options?.groupBy) sql += ` GROUP BY ${options.groupBy}`
|
|
172
|
+
if (options?.having && Object.keys(options.having).length > 0) {
|
|
173
|
+
const h = buildWhere(options.having)
|
|
174
|
+
sql += ` HAVING ${h.clause}`
|
|
175
|
+
params.push(...h.vals)
|
|
176
|
+
}
|
|
177
|
+
if (options?.orderBy) sql += ` ORDER BY ${options.orderBy}`
|
|
178
|
+
if (options?.limit) { sql += ' LIMIT ?'; params.push(options.limit) }
|
|
179
|
+
if (options?.offset) { sql += ' OFFSET ?'; params.push(options.offset) }
|
|
180
|
+
|
|
181
|
+
const result = await this.query<T>(sql, params)
|
|
182
|
+
return result.rows
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* SELECT with JOIN — CodeIgniter-style.
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* await db.getJoin('posts p', [['users u', 'u.id = p.user_id']], { where: { 'p.status': 'published' } })
|
|
190
|
+
* await db.getJoin('orders o', [
|
|
191
|
+
* ['users u', 'u.id = o.user_id'],
|
|
192
|
+
* ['order_items oi', 'oi.order_id = o.id', 'left'],
|
|
193
|
+
* ], { orderBy: 'o.created_at DESC', limit: 10 })
|
|
194
|
+
*/
|
|
195
|
+
async getJoin<T = any>(
|
|
196
|
+
from: string,
|
|
197
|
+
joins: Array<[string, string] | [string, string, string]>,
|
|
198
|
+
options?: {
|
|
199
|
+
where?: Record<string, any>
|
|
200
|
+
orderBy?: string
|
|
201
|
+
limit?: number
|
|
202
|
+
offset?: number
|
|
203
|
+
select?: string
|
|
204
|
+
groupBy?: string
|
|
205
|
+
having?: Record<string, any>
|
|
206
|
+
}
|
|
207
|
+
): Promise<T[]> {
|
|
208
|
+
let sql = `SELECT ${options?.select ?? '*'} FROM ${from}`
|
|
209
|
+
const params: unknown[] = []
|
|
210
|
+
|
|
211
|
+
for (const join of joins) {
|
|
212
|
+
const [table, on, type] = join
|
|
213
|
+
const joinType = (type?.toUpperCase() === 'LEFT' || type?.toUpperCase() === 'RIGHT' || type?.toUpperCase() === 'INNER')
|
|
214
|
+
? type.toUpperCase()
|
|
215
|
+
: type?.toUpperCase() === 'OUTER' ? 'LEFT' : ''
|
|
216
|
+
sql += joinType ? ` ${joinType} JOIN ${table} ON ${on}` : ` JOIN ${table} ON ${on}`
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (options?.where && Object.keys(options.where).length > 0) {
|
|
220
|
+
const r = buildWhere(options.where)
|
|
221
|
+
sql += ` WHERE ${r.clause}`
|
|
222
|
+
params.push(...r.vals)
|
|
223
|
+
}
|
|
224
|
+
if (options?.groupBy) sql += ` GROUP BY ${options.groupBy}`
|
|
225
|
+
if (options?.having && Object.keys(options.having).length > 0) {
|
|
226
|
+
const h = buildWhere(options.having)
|
|
227
|
+
sql += ` HAVING ${h.clause}`
|
|
228
|
+
params.push(...h.vals)
|
|
229
|
+
}
|
|
230
|
+
if (options?.orderBy) sql += ` ORDER BY ${options.orderBy}`
|
|
231
|
+
if (options?.limit) { sql += ' LIMIT ?'; params.push(options.limit) }
|
|
232
|
+
if (options?.offset) { sql += ' OFFSET ?'; params.push(options.offset) }
|
|
233
|
+
|
|
234
|
+
const result = await this.query<T>(sql, params)
|
|
235
|
+
return result.rows
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Count records in a table, optionally with WHERE.
|
|
240
|
+
* @example await db.count('users') // → 42
|
|
241
|
+
* @example await db.count('users', { role: 'admin' }) // → 5
|
|
242
|
+
*/
|
|
243
|
+
async count(table: string, where?: Record<string, any>): Promise<number> {
|
|
244
|
+
let sql = `SELECT count(*) as c FROM ${table}`
|
|
245
|
+
const params: unknown[] = []
|
|
246
|
+
if (where && Object.keys(where).length > 0) {
|
|
247
|
+
const r = buildWhere(where)
|
|
248
|
+
sql += ` WHERE ${r.clause}`
|
|
249
|
+
params.push(...r.vals)
|
|
250
|
+
}
|
|
251
|
+
const result = await this.query<{ c: number }>(sql, params)
|
|
252
|
+
return Number(result.rows[0]?.c ?? 0)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Batch insert multiple rows at once.
|
|
257
|
+
* @example await db.insertBatch('users', [{name:'A'},{name:'B'},{name:'C'}])
|
|
258
|
+
*/
|
|
259
|
+
async insertBatch(table: string, data: Record<string, any>[]): Promise<QueryResult> {
|
|
260
|
+
if (data.length === 0) return { rows: [], affectedRows: 0 }
|
|
261
|
+
const keys = Object.keys(data[0])
|
|
262
|
+
const placeholders = data.map(() => `(${keys.map(() => '?').join(',')})`).join(',')
|
|
263
|
+
const vals = data.flatMap(d => keys.map(k => d[k]))
|
|
264
|
+
return this.query(`INSERT INTO ${table} (${keys.join(',')}) VALUES ${placeholders}`, vals)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Delete all rows from a table (Drizzle truncate).
|
|
269
|
+
* @example await db.truncate('logs')
|
|
270
|
+
*/
|
|
271
|
+
async truncate(table: string): Promise<QueryResult> {
|
|
272
|
+
return this.query(`DELETE FROM ${table}`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async query<T = any>(sql: string, params: unknown[] = []): Promise<QueryResult<T>> {
|
|
276
|
+
this.assertOpen()
|
|
277
|
+
if (!this.rawExecutor) {
|
|
278
|
+
throw new Error(`[db] dialect "${this.dialect}" does not support raw queries`)
|
|
279
|
+
}
|
|
280
|
+
const start = performance.now()
|
|
281
|
+
const result = await this.rawExecutor.query(sql, params)
|
|
282
|
+
const duration = performance.now() - start
|
|
283
|
+
|
|
284
|
+
// Log to debug toolbar if active
|
|
285
|
+
try {
|
|
286
|
+
const ctx = getRequestContext()
|
|
287
|
+
if (ctx) {
|
|
288
|
+
const { getStore } = await import('../helpers/debug')
|
|
289
|
+
const data = getStore(ctx)
|
|
290
|
+
data.queries.push({
|
|
291
|
+
id: data.queries.length + 1,
|
|
292
|
+
sql,
|
|
293
|
+
duration: Math.round(duration * 100) / 100,
|
|
294
|
+
rows: result.rows.length,
|
|
295
|
+
params,
|
|
296
|
+
time: new Date().toLocaleTimeString(),
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
} catch {}
|
|
300
|
+
|
|
301
|
+
return result
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Execute a query and return the first row.
|
|
306
|
+
*/
|
|
307
|
+
async first<T = any>(sql: string, params: unknown[] = []): Promise<T | null> {
|
|
308
|
+
const result = await this.query<T>(sql, params)
|
|
309
|
+
return result.rows[0] ?? null
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Execute a query and return all rows.
|
|
314
|
+
*/
|
|
315
|
+
async all<T = any>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
316
|
+
const result = await this.query<T>(sql, params)
|
|
317
|
+
return result.rows
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ─── Pagination ─────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Paginated query with automatic count.
|
|
324
|
+
*
|
|
325
|
+
* @example
|
|
326
|
+
* const result = await db.paginate('SELECT * FROM users', { page: 2, perPage: 20 })
|
|
327
|
+
* const result = await db.paginate('SELECT * FROM posts WHERE status = ?', ['published'], { page: 1, perPage: 10 })
|
|
328
|
+
*/
|
|
329
|
+
async paginate<T = any>(
|
|
330
|
+
sql: string,
|
|
331
|
+
params: unknown[] = [],
|
|
332
|
+
options: { page?: number; perPage?: number } = {}
|
|
333
|
+
): Promise<{ data: T[]; total: number; page: number; perPage: number; pages: number }> {
|
|
334
|
+
const page = Math.max(1, options.page ?? 1)
|
|
335
|
+
const perPage = Math.max(1, options.perPage ?? 20)
|
|
336
|
+
const offset = (page - 1) * perPage
|
|
337
|
+
|
|
338
|
+
const countResult = await this.query<{ count: number }>(`SELECT count(*) as count FROM (${sql})`, params)
|
|
339
|
+
const total = Number(countResult.rows[0]?.count ?? 0)
|
|
340
|
+
|
|
341
|
+
const data = await this.all<T>(`${sql} LIMIT ? OFFSET ?`, [...params, perPage, offset])
|
|
342
|
+
|
|
343
|
+
return { data, total, page, perPage, pages: Math.ceil(total / perPage) }
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ─── Transactions ────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Execute a callback within an ACID transaction.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* const user = await db.transaction(async (tx) => {
|
|
354
|
+
* const [u] = await tx.query("INSERT INTO users (name) VALUES (?) RETURNING *", ['Bob'])
|
|
355
|
+
* await tx.query("INSERT INTO logs (action) VALUES (?)", ['created_user'])
|
|
356
|
+
* return u
|
|
357
|
+
* })
|
|
358
|
+
* ```
|
|
359
|
+
*/
|
|
360
|
+
async transaction<T>(fn: (tx: TxClient) => Promise<T>): Promise<T> {
|
|
361
|
+
this.assertOpen()
|
|
362
|
+
return this.client.transaction(async (tx: any) => {
|
|
363
|
+
const txClient = Object.create(this) as TxClient
|
|
364
|
+
Object.defineProperty(txClient, 'client', { value: tx, writable: false })
|
|
365
|
+
// Build a temporary raw executor from the tx client if needed
|
|
366
|
+
Object.defineProperty(txClient, 'rawExecutor', {
|
|
367
|
+
value: this.rawExecutor,
|
|
368
|
+
writable: false
|
|
369
|
+
})
|
|
370
|
+
return fn(txClient)
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ─── Lifecycle ───────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
async close(): Promise<void> {
|
|
377
|
+
if (!this.opened) return
|
|
378
|
+
await this.client?.close?.()
|
|
379
|
+
this.opened = false
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ─── Internal ────────────────────────────────────────────────
|
|
383
|
+
|
|
384
|
+
private assertOpen(): void {
|
|
385
|
+
if (!this.opened) {
|
|
386
|
+
throw new Error('[db] not opened. Call await db.open() first.')
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** Transaction client — same interface as DbClient. */
|
|
392
|
+
export type TxClient = DbClient
|
|
393
|
+
|
|
394
|
+
// ─── Driver resolution ──────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
interface RawExecutor {
|
|
397
|
+
query<T>(sql: string, params?: unknown[]): Promise<QueryResult<T>>
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
interface DriverResult {
|
|
401
|
+
db: any
|
|
402
|
+
rawExecutor?: RawExecutor
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Build WHERE clause with operator support. */
|
|
406
|
+
function buildWhere(where: Record<string, any>): { clause: string; vals: unknown[] } {
|
|
407
|
+
const parts: string[] = []
|
|
408
|
+
const vals: unknown[] = []
|
|
409
|
+
|
|
410
|
+
for (const [key, val] of Object.entries(where)) {
|
|
411
|
+
if (Array.isArray(val)) {
|
|
412
|
+
const op = val[0] ?? '='
|
|
413
|
+
const v = val[1] ?? val[0]
|
|
414
|
+
if (op.toUpperCase() === 'IN') {
|
|
415
|
+
const items = Array.isArray(v) ? v : [v]
|
|
416
|
+
parts.push(`${key} IN (${items.map(() => '?').join(', ')})`)
|
|
417
|
+
vals.push(...items)
|
|
418
|
+
} else {
|
|
419
|
+
parts.push(`${key} ${op} ?`)
|
|
420
|
+
vals.push(v)
|
|
421
|
+
}
|
|
422
|
+
} else {
|
|
423
|
+
parts.push(`${key} = ?`)
|
|
424
|
+
vals.push(val)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return { clause: parts.join(' AND '), vals }
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function resolveDriver(dialect: Dialect, config: DbConfig): Promise<DriverResult> {
|
|
432
|
+
const conn = config.connection
|
|
433
|
+
|
|
434
|
+
switch (dialect) {
|
|
435
|
+
case 'postgres': {
|
|
436
|
+
const drizzleMod = await import('drizzle-orm/postgres-js')
|
|
437
|
+
const postgres = await import('postgres')
|
|
438
|
+
const sql = postgres.default({
|
|
439
|
+
host: conn.host ?? 'localhost',
|
|
440
|
+
port: conn.port ?? 5432,
|
|
441
|
+
user: conn.user,
|
|
442
|
+
password: conn.password,
|
|
443
|
+
database: conn.database,
|
|
444
|
+
...(conn as any)
|
|
445
|
+
})
|
|
446
|
+
const db = drizzleMod.drizzle(sql, { logger: config.logging as any })
|
|
447
|
+
const rawExecutor: RawExecutor = {
|
|
448
|
+
async query<T>(querySql: string, params: unknown[] = []) {
|
|
449
|
+
const rows = await sql.unsafe(querySql, params as any[])
|
|
450
|
+
return {
|
|
451
|
+
rows: rows as T[],
|
|
452
|
+
affectedRows: rows.length
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
return { db, rawExecutor }
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
case 'bun-sqlite': {
|
|
460
|
+
const drizzleMod = await import('drizzle-orm/bun-sqlite')
|
|
461
|
+
const { Database } = await import('bun:sqlite')
|
|
462
|
+
const filename = (conn as any).filename ?? 'app.db'
|
|
463
|
+
const sqlite = new Database(filename)
|
|
464
|
+
const db = drizzleMod.drizzle(sqlite, { logger: config.logging as any })
|
|
465
|
+
const rawExecutor: RawExecutor = {
|
|
466
|
+
async query<T>(querySql: string, params: unknown[] = []) {
|
|
467
|
+
const stmt = sqlite.prepare(querySql)
|
|
468
|
+
const isSelect = /^\s*(select|pragma|with)\b/i.test(querySql)
|
|
469
|
+
if (isSelect) {
|
|
470
|
+
const rows = stmt.all(...params)
|
|
471
|
+
return { rows: rows as T[], affectedRows: 0 }
|
|
472
|
+
}
|
|
473
|
+
const r = stmt.run(...params)
|
|
474
|
+
return {
|
|
475
|
+
rows: [],
|
|
476
|
+
affectedRows: Number(r.changes ?? 0),
|
|
477
|
+
insertId: r.lastInsertRowid as number | string
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return { db, rawExecutor }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
case 'sqlite': {
|
|
485
|
+
const drizzleMod = await import('drizzle-orm/better-sqlite3')
|
|
486
|
+
const sqliteMod = await import('better-sqlite3')
|
|
487
|
+
const Database = (sqliteMod as any).default ?? sqliteMod
|
|
488
|
+
const filename = (conn as any).filename ?? 'app.db'
|
|
489
|
+
const sqlite = new Database(filename)
|
|
490
|
+
const db = drizzleMod.drizzle(sqlite, { logger: config.logging as any })
|
|
491
|
+
const rawExecutor: RawExecutor = {
|
|
492
|
+
async query<T>(querySql: string, params: unknown[] = []) {
|
|
493
|
+
const stmt = sqlite.prepare(querySql)
|
|
494
|
+
const isSelect = /^\s*(select|pragma|with)\b/i.test(querySql)
|
|
495
|
+
if (isSelect) {
|
|
496
|
+
const rows = stmt.all(...params)
|
|
497
|
+
return { rows: rows as T[], affectedRows: 0 }
|
|
498
|
+
}
|
|
499
|
+
const r = stmt.run(...params)
|
|
500
|
+
return {
|
|
501
|
+
rows: [],
|
|
502
|
+
affectedRows: Number(r.changes ?? 0),
|
|
503
|
+
insertId: r.lastInsertRowid as number | string
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return { db, rawExecutor }
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
case 'mysql': {
|
|
511
|
+
const drizzleMod = await import('drizzle-orm/mysql2')
|
|
512
|
+
const mysqlMod = await import('mysql2/promise')
|
|
513
|
+
const pool = (mysqlMod as any).createPool({
|
|
514
|
+
host: conn.host ?? 'localhost',
|
|
515
|
+
port: conn.port ?? 3306,
|
|
516
|
+
user: conn.user,
|
|
517
|
+
password: conn.password,
|
|
518
|
+
database: conn.database,
|
|
519
|
+
...(conn as any)
|
|
520
|
+
})
|
|
521
|
+
const db = drizzleMod.drizzle(pool, { logger: config.logging as any })
|
|
522
|
+
const rawExecutor: RawExecutor = {
|
|
523
|
+
async query<T>(querySql: string, params: unknown[] = []) {
|
|
524
|
+
const [rows] = await pool.query(querySql, params as any[])
|
|
525
|
+
return { rows: rows as T[], affectedRows: (rows as any[])?.length ?? 0 }
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return { db, rawExecutor }
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
case 'd1': {
|
|
532
|
+
const drizzleMod = await import('drizzle-orm/d1')
|
|
533
|
+
const binding = conn.binding as any
|
|
534
|
+
if (!binding) throw new Error('D1 driver requires connection.binding')
|
|
535
|
+
const db = drizzleMod.drizzle(binding, { logger: config.logging as any })
|
|
536
|
+
const rawExecutor: RawExecutor = {
|
|
537
|
+
async query<T>(querySql: string, params: unknown[] = []) {
|
|
538
|
+
const stmt = binding.prepare(querySql)
|
|
539
|
+
if (params.length > 0) stmt.bind(...params)
|
|
540
|
+
const result = await stmt.run()
|
|
541
|
+
return { rows: (result as any)?.results ?? [], affectedRows: result.meta?.changes ?? 0 }
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return { db, rawExecutor }
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
default:
|
|
548
|
+
throw new Error(`[db] unsupported dialect: ${dialect}`)
|
|
549
|
+
}
|
|
550
|
+
}
|