driftsql 1.0.16 → 1.0.17
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/.prettierrc +5 -0
- package/package.json +17 -29
- package/src/drivers/libsql.ts +70 -0
- package/src/drivers/mysql.ts +70 -0
- package/src/drivers/postgres.ts +112 -0
- package/src/drivers/sqlite.ts +122 -0
- package/src/index.ts +225 -0
- package/src/pull.ts +318 -0
- package/src/types.ts +78 -0
- package/tsconfig.json +20 -0
- package/dist/index.d.mts +0 -171
- package/dist/index.d.ts +0 -171
- package/dist/index.mjs +0 -731
package/.prettierrc
ADDED
package/package.json
CHANGED
|
@@ -1,21 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "driftsql",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"author": "lasse vestergaard",
|
|
3
|
+
"version": "1.0.17",
|
|
5
4
|
"description": "A lightweight SQL client for TypeScript",
|
|
6
|
-
"repository": "lassejlv/driftsql",
|
|
7
|
-
"license": "MIT",
|
|
8
|
-
"sideEffects": false,
|
|
9
|
-
"type": "module",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": {
|
|
12
|
-
"types": "./dist/index.d.mts",
|
|
13
|
-
"default": "./dist/index.mjs"
|
|
14
|
-
}
|
|
15
|
-
},
|
|
16
|
-
"files": [
|
|
17
|
-
"dist"
|
|
18
|
-
],
|
|
19
5
|
"scripts": {
|
|
20
6
|
"build": "unbuild",
|
|
21
7
|
"dev": "vitest dev",
|
|
@@ -26,20 +12,8 @@
|
|
|
26
12
|
"test": "pnpm lint && pnpm test:types && vitest run --coverage",
|
|
27
13
|
"test:types": "tsc --noEmit --skipLibCheck"
|
|
28
14
|
},
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
"@types/node": "^22.15.34",
|
|
32
|
-
"@vitest/coverage-v8": "^3.2.4",
|
|
33
|
-
"automd": "^0.4.0",
|
|
34
|
-
"changelogen": "^0.6.1",
|
|
35
|
-
"eslint": "^9.30.0",
|
|
36
|
-
"eslint-config-unjs": "^0.4.2",
|
|
37
|
-
"prettier": "^3.6.2",
|
|
38
|
-
"typescript": "^5.8.3",
|
|
39
|
-
"unbuild": "^3.5.0",
|
|
40
|
-
"vitest": "^3.2.4"
|
|
41
|
-
},
|
|
42
|
-
"packageManager": "pnpm@10.12.1",
|
|
15
|
+
"author": "lasse vestergaard",
|
|
16
|
+
"license": "MIT",
|
|
43
17
|
"dependencies": {
|
|
44
18
|
"@libsql/client": "^0.15.9",
|
|
45
19
|
"@neondatabase/serverless": "^1.0.1",
|
|
@@ -54,5 +28,19 @@
|
|
|
54
28
|
"mysql2": "^3.14.1",
|
|
55
29
|
"pg": "^8.16.3",
|
|
56
30
|
"postgres": "^3.4.7"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
34
|
+
"@types/bun": "^1.2.20",
|
|
35
|
+
"@types/node": "^22.15.34",
|
|
36
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
37
|
+
"automd": "^0.4.0",
|
|
38
|
+
"changelogen": "^0.6.1",
|
|
39
|
+
"eslint": "^9.30.0",
|
|
40
|
+
"eslint-config-unjs": "^0.4.2",
|
|
41
|
+
"prettier": "^3.6.2",
|
|
42
|
+
"typescript": "^5.8.3",
|
|
43
|
+
"unbuild": "^3.5.0",
|
|
44
|
+
"vitest": "^3.2.4"
|
|
57
45
|
}
|
|
58
46
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createClient, type ResultSet } from '@libsql/client'
|
|
2
|
+
import { createClient as tursoServerLessClient, type ResultSet as tursoServerLessResultSet } from '@tursodatabase/serverless/compat'
|
|
3
|
+
import type { DatabaseDriver, QueryResult, TransactionCapable } from '../types'
|
|
4
|
+
import { QueryError, ConnectionError } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface LibSQLConfig {
|
|
7
|
+
url: string
|
|
8
|
+
authToken?: string
|
|
9
|
+
useTursoServerlessDriver?: boolean
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class LibSQLDriver implements DatabaseDriver, TransactionCapable {
|
|
13
|
+
private client: ReturnType<typeof createClient> | ReturnType<typeof tursoServerLessClient>
|
|
14
|
+
|
|
15
|
+
constructor(config: LibSQLConfig) {
|
|
16
|
+
try {
|
|
17
|
+
this.client = config.useTursoServerlessDriver
|
|
18
|
+
? tursoServerLessClient({
|
|
19
|
+
url: config.url,
|
|
20
|
+
...(config.authToken ? { authToken: config.authToken } : {}),
|
|
21
|
+
})
|
|
22
|
+
: createClient({
|
|
23
|
+
url: config.url,
|
|
24
|
+
...(config.authToken ? { authToken: config.authToken } : {}),
|
|
25
|
+
})
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new ConnectionError('libsql', error as Error)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
32
|
+
try {
|
|
33
|
+
const result = await this.client.execute(sql, params)
|
|
34
|
+
return this.convertLibsqlResult<T>(result)
|
|
35
|
+
} catch (error) {
|
|
36
|
+
throw new QueryError('libsql', sql, error as Error)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async transaction<T>(callback: (driver: DatabaseDriver) => Promise<T>): Promise<T> {
|
|
41
|
+
const transactionDriver = new LibSQLDriver({ url: '', authToken: '' })
|
|
42
|
+
;(transactionDriver as any).client = this.client
|
|
43
|
+
return await callback(transactionDriver)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async close(): Promise<void> {
|
|
47
|
+
try {
|
|
48
|
+
this.client.close()
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error closing LibSQL client:', error)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private convertLibsqlResult<T = any>(result: ResultSet | tursoServerLessResultSet): QueryResult<T> {
|
|
55
|
+
const rows = result.rows.map((row) => {
|
|
56
|
+
const obj: Record<string, any> = {}
|
|
57
|
+
result.columns.forEach((col, index) => {
|
|
58
|
+
obj[col] = row[index]
|
|
59
|
+
})
|
|
60
|
+
return obj as T
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
rows,
|
|
65
|
+
rowCount: result.rowsAffected || rows.length,
|
|
66
|
+
command: undefined,
|
|
67
|
+
fields: result.columns.map((col) => ({ name: col, dataTypeID: 0 })),
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import consola from 'consola'
|
|
2
|
+
import * as mysql from 'mysql2/promise'
|
|
3
|
+
import type { DatabaseDriver, QueryResult, TransactionCapable } from '../types'
|
|
4
|
+
import { QueryError, ConnectionError } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface MySQLConfig {
|
|
7
|
+
connectionString: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class MySQLDriver implements DatabaseDriver, TransactionCapable {
|
|
11
|
+
private client: ReturnType<typeof mysql.createConnection>
|
|
12
|
+
|
|
13
|
+
constructor(config: MySQLConfig) {
|
|
14
|
+
consola.warn('MySQL client is experimental and may not be compatible with the helper functions, since they originally designed for PostgreSQL and libsql. But .query() method should work.')
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
this.client = mysql.createConnection(config.connectionString)
|
|
18
|
+
} catch (error) {
|
|
19
|
+
throw new ConnectionError('mysql', error as Error)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
24
|
+
try {
|
|
25
|
+
const [rows, fields] = await (await this.client).execute(sql, params || [])
|
|
26
|
+
const rowCount = Array.isArray(rows) ? rows.length : 0
|
|
27
|
+
|
|
28
|
+
const normalizedFields = fields.map((field: any) => ({
|
|
29
|
+
name: field.name,
|
|
30
|
+
dataTypeID: field.columnType,
|
|
31
|
+
}))
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
rows: rows as T[],
|
|
35
|
+
rowCount,
|
|
36
|
+
command: undefined,
|
|
37
|
+
fields: normalizedFields,
|
|
38
|
+
}
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw new QueryError('mysql', sql, error as Error)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async transaction<T>(callback: (driver: DatabaseDriver) => Promise<T>): Promise<T> {
|
|
45
|
+
const connection = await this.client
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await connection.beginTransaction()
|
|
49
|
+
|
|
50
|
+
const transactionDriver = new MySQLDriver({ connectionString: '' })
|
|
51
|
+
transactionDriver.client = Promise.resolve(connection)
|
|
52
|
+
|
|
53
|
+
const result = await callback(transactionDriver)
|
|
54
|
+
|
|
55
|
+
await connection.commit()
|
|
56
|
+
return result
|
|
57
|
+
} catch (error) {
|
|
58
|
+
await connection.rollback()
|
|
59
|
+
throw error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async close(): Promise<void> {
|
|
64
|
+
try {
|
|
65
|
+
await (await this.client).end()
|
|
66
|
+
} catch (error) {
|
|
67
|
+
consola.error('Error closing MySQL client:', error)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import postgres from 'postgres'
|
|
2
|
+
import ky from 'ky'
|
|
3
|
+
import type { DatabaseDriver, QueryResult, TransactionCapable } from '../types'
|
|
4
|
+
import { QueryError, ConnectionError } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface PostgresConfig {
|
|
7
|
+
connectionString?: string
|
|
8
|
+
experimental?: {
|
|
9
|
+
http?: {
|
|
10
|
+
url: string
|
|
11
|
+
apiKey?: string
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class PostgresDriver implements DatabaseDriver, TransactionCapable {
|
|
17
|
+
private client: postgres.Sql | PostgresHTTPDriver
|
|
18
|
+
|
|
19
|
+
constructor(config: PostgresConfig) {
|
|
20
|
+
try {
|
|
21
|
+
if (config.experimental?.http) {
|
|
22
|
+
this.client = new PostgresHTTPDriver(config.experimental.http)
|
|
23
|
+
} else {
|
|
24
|
+
this.client = postgres(config.connectionString || '')
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new ConnectionError('postgres', error as Error)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
32
|
+
try {
|
|
33
|
+
if (this.client instanceof PostgresHTTPDriver) {
|
|
34
|
+
return await this.client.query<T>(sql, params)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const result = await this.client.unsafe(sql, params || [])
|
|
38
|
+
return {
|
|
39
|
+
rows: result as unknown as T[],
|
|
40
|
+
rowCount: Array.isArray(result) ? result.length : 0,
|
|
41
|
+
command: undefined,
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new QueryError('postgres', sql, error as Error)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async transaction<T>(callback: (driver: DatabaseDriver) => Promise<T>): Promise<T> {
|
|
49
|
+
if (this.client instanceof PostgresHTTPDriver) {
|
|
50
|
+
throw new Error('Transactions not supported with HTTP driver')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const result = await this.client.begin(async (sql) => {
|
|
54
|
+
const transactionDriver = new PostgresDriver({ connectionString: '' })
|
|
55
|
+
transactionDriver.client = sql
|
|
56
|
+
return await callback(transactionDriver)
|
|
57
|
+
})
|
|
58
|
+
return result as T
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Helper methods for findMany, findFirst, insert, update, delete
|
|
62
|
+
async findMany<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
63
|
+
return this.query<T>(sql, params)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async close(): Promise<void> {
|
|
67
|
+
try {
|
|
68
|
+
if (this.client instanceof PostgresHTTPDriver) {
|
|
69
|
+
return await this.client.close()
|
|
70
|
+
}
|
|
71
|
+
await this.client.end()
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error('Error closing Postgres client:', error)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
class PostgresHTTPDriver {
|
|
79
|
+
private httpClient: typeof ky
|
|
80
|
+
|
|
81
|
+
constructor(config: { url: string; apiKey?: string }) {
|
|
82
|
+
this.httpClient = ky.create({
|
|
83
|
+
prefixUrl: config.url,
|
|
84
|
+
headers: {
|
|
85
|
+
Authorization: `Bearer ${config.apiKey || ''}`,
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
91
|
+
try {
|
|
92
|
+
const response = await this.httpClient
|
|
93
|
+
.post('query', {
|
|
94
|
+
json: { query: sql, args: params },
|
|
95
|
+
})
|
|
96
|
+
.json<{ rows: T[]; rowCount: number }>()
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
rows: response.rows,
|
|
100
|
+
rowCount: response.rowCount,
|
|
101
|
+
command: undefined,
|
|
102
|
+
}
|
|
103
|
+
} catch (error) {
|
|
104
|
+
throw new QueryError('postgres-http', sql, error as Error)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async close(): Promise<void> {
|
|
109
|
+
// HTTP connections don't need explicit closing
|
|
110
|
+
return Promise.resolve()
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import Database from 'better-sqlite3'
|
|
2
|
+
import type { DatabaseDriver, QueryResult, TransactionCapable, PreparedStatementCapable, PreparedStatement } from '../types'
|
|
3
|
+
import { QueryError, ConnectionError } from '../types'
|
|
4
|
+
|
|
5
|
+
export interface SqliteConfig {
|
|
6
|
+
filename: string
|
|
7
|
+
readonly?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class SqliteDriver implements DatabaseDriver, TransactionCapable, PreparedStatementCapable {
|
|
11
|
+
private client: Database.Database
|
|
12
|
+
|
|
13
|
+
constructor(config: SqliteConfig) {
|
|
14
|
+
try {
|
|
15
|
+
this.client = new Database(config.filename, {
|
|
16
|
+
readonly: config.readonly || false,
|
|
17
|
+
fileMustExist: config.readonly || false,
|
|
18
|
+
})
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw new ConnectionError('sqlite', error as Error)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
25
|
+
try {
|
|
26
|
+
const stmt = this.client.prepare(sql)
|
|
27
|
+
const rows = stmt.all(params || []) as T[]
|
|
28
|
+
|
|
29
|
+
// Get column names from the first row if available
|
|
30
|
+
const fields = rows.length > 0 && typeof rows[0] === 'object' && rows[0] !== null ? Object.keys(rows[0] as object).map((name) => ({ name, dataTypeID: 0 })) : []
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
rows,
|
|
34
|
+
rowCount: rows.length,
|
|
35
|
+
command: undefined,
|
|
36
|
+
fields,
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new QueryError('sqlite', sql, error as Error)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async transaction<T>(callback: (driver: DatabaseDriver) => Promise<T>): Promise<T> {
|
|
44
|
+
const transaction = this.client.transaction(() => {
|
|
45
|
+
const transactionDriver = new SqliteDriver({ filename: '' })
|
|
46
|
+
;(transactionDriver as any).client = this.client
|
|
47
|
+
return callback(transactionDriver)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return await transaction()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async prepare(sql: string): Promise<PreparedStatement> {
|
|
54
|
+
return new SqlitePreparedStatement(this.client.prepare(sql))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async close(): Promise<void> {
|
|
58
|
+
try {
|
|
59
|
+
this.client.close()
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.error('Error closing SQLite client:', error)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// SQLite-specific methods
|
|
66
|
+
exec(sql: string): void {
|
|
67
|
+
this.client.exec(sql)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
backup(filename: string): Promise<void> {
|
|
71
|
+
return new Promise((resolve, reject) => {
|
|
72
|
+
try {
|
|
73
|
+
this.client.backup(filename)
|
|
74
|
+
resolve()
|
|
75
|
+
} catch (error) {
|
|
76
|
+
reject(error)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
pragma(pragma: string): any {
|
|
82
|
+
return this.client.pragma(pragma)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class SqlitePreparedStatement implements PreparedStatement {
|
|
87
|
+
constructor(private stmt: Database.Statement) {}
|
|
88
|
+
|
|
89
|
+
async execute<T = any>(params?: any[]): Promise<QueryResult<T>> {
|
|
90
|
+
try {
|
|
91
|
+
const rows = this.stmt.all(params || []) as T[]
|
|
92
|
+
const fields = rows.length > 0 && typeof rows[0] === 'object' && rows[0] !== null ? Object.keys(rows[0] as object).map((name) => ({ name, dataTypeID: 0 })) : []
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
rows,
|
|
96
|
+
rowCount: rows.length,
|
|
97
|
+
command: undefined,
|
|
98
|
+
fields,
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new QueryError('sqlite', 'prepared statement', error as Error)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async finalize(): Promise<void> {
|
|
106
|
+
// better-sqlite3 doesn't have finalize method, we can just ignore this
|
|
107
|
+
return Promise.resolve()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// SQLite-specific methods for prepared statements
|
|
111
|
+
run(params?: any[]): Database.RunResult {
|
|
112
|
+
return this.stmt.run(params || [])
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
get(params?: any[]): any {
|
|
116
|
+
return this.stmt.get(params || [])
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
all(params?: any[]): any[] {
|
|
120
|
+
return this.stmt.all(params || [])
|
|
121
|
+
}
|
|
122
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import consola from 'consola'
|
|
2
|
+
import type { DatabaseDriver, QueryResult } from './types'
|
|
3
|
+
import { hasTransactionSupport, hasPreparedStatementSupport, DatabaseError } from './types'
|
|
4
|
+
import { PostgresDriver } from './drivers/postgres'
|
|
5
|
+
import { LibSQLDriver } from './drivers/libsql'
|
|
6
|
+
import { MySQLDriver } from './drivers/mysql'
|
|
7
|
+
import { SqliteDriver } from './drivers/sqlite'
|
|
8
|
+
|
|
9
|
+
// Re-export types and drivers for convenience
|
|
10
|
+
export type { DatabaseDriver, QueryResult, QueryError, QueryField, ConnectionError } from './types'
|
|
11
|
+
export { PostgresDriver } from './drivers/postgres'
|
|
12
|
+
export { LibSQLDriver } from './drivers/libsql'
|
|
13
|
+
export { MySQLDriver } from './drivers/mysql'
|
|
14
|
+
export { SqliteDriver } from './drivers/sqlite'
|
|
15
|
+
|
|
16
|
+
// Re-export inspection utilities
|
|
17
|
+
export { inspectDB, inspectPostgres, inspectLibSQL, inspectMySQL, inspectSQLite } from './pull'
|
|
18
|
+
|
|
19
|
+
export interface ClientOptions<T extends DatabaseDriver = DatabaseDriver> {
|
|
20
|
+
driver: T
|
|
21
|
+
fallbackDrivers?: DatabaseDriver[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SQLClient<DT = any> {
|
|
25
|
+
private primaryDriver: DatabaseDriver
|
|
26
|
+
private fallbackDrivers: DatabaseDriver[]
|
|
27
|
+
|
|
28
|
+
constructor(options: ClientOptions) {
|
|
29
|
+
this.primaryDriver = options.driver
|
|
30
|
+
this.fallbackDrivers = options.fallbackDrivers || []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async query<T = any>(sql: string, params?: any[]): Promise<QueryResult<T>> {
|
|
34
|
+
const drivers = [this.primaryDriver, ...this.fallbackDrivers]
|
|
35
|
+
let lastError: Error | undefined
|
|
36
|
+
|
|
37
|
+
for (const driver of drivers) {
|
|
38
|
+
try {
|
|
39
|
+
return await driver.query<T>(sql, params)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
lastError = error as Error
|
|
42
|
+
consola.warn(`Query failed with ${driver.constructor.name}:`, error)
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw lastError || new DatabaseError('All drivers failed to execute query', 'unknown')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async transaction<T>(callback: (client: SQLClient<DT>) => Promise<T>): Promise<T> {
|
|
51
|
+
if (!hasTransactionSupport(this.primaryDriver)) {
|
|
52
|
+
throw new DatabaseError('Primary driver does not support transactions', this.primaryDriver.constructor.name)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return await this.primaryDriver.transaction(async (transactionDriver) => {
|
|
56
|
+
const transactionClient = new SQLClient<DT>({
|
|
57
|
+
driver: transactionDriver,
|
|
58
|
+
fallbackDrivers: [],
|
|
59
|
+
})
|
|
60
|
+
return await callback(transactionClient)
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async prepare(sql: string) {
|
|
65
|
+
if (!hasPreparedStatementSupport(this.primaryDriver)) {
|
|
66
|
+
throw new DatabaseError('Primary driver does not support prepared statements', this.primaryDriver.constructor.name)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return await this.primaryDriver.prepare(sql)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Helper methods for common database operations
|
|
73
|
+
async findFirst<K extends keyof DT>(table: K, where?: Partial<DT[K]>): Promise<DT[K] | null> {
|
|
74
|
+
const tableName = String(table)
|
|
75
|
+
const whereEntries = Object.entries(where || {})
|
|
76
|
+
|
|
77
|
+
let sql = `SELECT * FROM ${tableName}`
|
|
78
|
+
let params: any[] = []
|
|
79
|
+
|
|
80
|
+
if (whereEntries.length > 0) {
|
|
81
|
+
const whereClause = whereEntries.map((_, index) => `${whereEntries[index]?.[0]} = $${index + 1}`).join(' AND ')
|
|
82
|
+
sql += ` WHERE ${whereClause}`
|
|
83
|
+
params = whereEntries.map(([, value]) => value)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
sql += ' LIMIT 1'
|
|
87
|
+
|
|
88
|
+
const result = await this.query<DT[K]>(sql, params)
|
|
89
|
+
return result.rows[0] || null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async findMany<K extends keyof DT>(
|
|
93
|
+
table: K,
|
|
94
|
+
options?: {
|
|
95
|
+
where?: Partial<DT[K]>
|
|
96
|
+
limit?: number
|
|
97
|
+
offset?: number
|
|
98
|
+
},
|
|
99
|
+
): Promise<DT[K][]> {
|
|
100
|
+
const tableName = String(table)
|
|
101
|
+
const { where, limit, offset } = options || {}
|
|
102
|
+
const whereEntries = Object.entries(where || {})
|
|
103
|
+
|
|
104
|
+
let sql = `SELECT * FROM ${tableName}`
|
|
105
|
+
let params: any[] = []
|
|
106
|
+
|
|
107
|
+
if (whereEntries.length > 0) {
|
|
108
|
+
const whereClause = whereEntries.map((_, index) => `${whereEntries[index]?.[0]} = $${index + 1}`).join(' AND ')
|
|
109
|
+
sql += ` WHERE ${whereClause}`
|
|
110
|
+
params = whereEntries.map(([, value]) => value)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof limit === 'number' && limit > 0) {
|
|
114
|
+
sql += ` LIMIT $${params.length + 1}`
|
|
115
|
+
params.push(limit)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof offset === 'number' && offset > 0) {
|
|
119
|
+
sql += ` OFFSET $${params.length + 1}`
|
|
120
|
+
params.push(offset)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = await this.query<DT[K]>(sql, params)
|
|
124
|
+
return result.rows
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async insert<K extends keyof DT>(table: K, data: Partial<DT[K]>): Promise<DT[K]> {
|
|
128
|
+
const tableName = String(table)
|
|
129
|
+
const keys = Object.keys(data)
|
|
130
|
+
const values = Object.values(data)
|
|
131
|
+
|
|
132
|
+
if (keys.length === 0) {
|
|
133
|
+
throw new Error('No data provided for insert')
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const placeholders = keys.map((_, index) => `$${index + 1}`).join(', ')
|
|
137
|
+
const sql = `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES (${placeholders}) RETURNING *`
|
|
138
|
+
|
|
139
|
+
const result = await this.query<DT[K]>(sql, values)
|
|
140
|
+
|
|
141
|
+
if (!result.rows[0]) {
|
|
142
|
+
throw new Error('Insert failed: No data returned')
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return result.rows[0]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async update<K extends keyof DT>(table: K, data: Partial<DT[K]>, where: Partial<DT[K]>): Promise<DT[K] | null> {
|
|
149
|
+
const tableName = String(table)
|
|
150
|
+
const setEntries = Object.entries(data)
|
|
151
|
+
const whereEntries = Object.entries(where)
|
|
152
|
+
|
|
153
|
+
if (setEntries.length === 0) {
|
|
154
|
+
throw new Error('No data provided for update')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (whereEntries.length === 0) {
|
|
158
|
+
throw new Error('No conditions provided for update')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const setClause = setEntries.map((_, index) => `${setEntries[index]?.[0]} = $${index + 1}`).join(', ')
|
|
162
|
+
const whereClause = whereEntries.map((_, index) => `${whereEntries[index]?.[0]} = $${setEntries.length + index + 1}`).join(' AND ')
|
|
163
|
+
|
|
164
|
+
const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause} RETURNING *`
|
|
165
|
+
const params = [...setEntries.map(([, value]) => value), ...whereEntries.map(([, value]) => value)]
|
|
166
|
+
|
|
167
|
+
const result = await this.query<DT[K]>(sql, params)
|
|
168
|
+
return result.rows[0] || null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async delete<K extends keyof DT>(table: K, where: Partial<DT[K]>): Promise<number> {
|
|
172
|
+
const tableName = String(table)
|
|
173
|
+
const whereEntries = Object.entries(where)
|
|
174
|
+
|
|
175
|
+
if (whereEntries.length === 0) {
|
|
176
|
+
throw new Error('No conditions provided for delete')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const whereClause = whereEntries.map((_, index) => `${whereEntries[index]?.[0]} = $${index + 1}`).join(' AND ')
|
|
180
|
+
const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`
|
|
181
|
+
const params = whereEntries.map(([, value]) => value)
|
|
182
|
+
|
|
183
|
+
const result = await this.query<DT[K]>(sql, params)
|
|
184
|
+
return result.rowCount || 0
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Get the primary driver (useful for driver-specific operations)
|
|
188
|
+
getDriver(): DatabaseDriver {
|
|
189
|
+
return this.primaryDriver
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Check driver capabilities
|
|
193
|
+
supportsTransactions(): boolean {
|
|
194
|
+
return hasTransactionSupport(this.primaryDriver)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
supportsPreparedStatements(): boolean {
|
|
198
|
+
return hasPreparedStatementSupport(this.primaryDriver)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async close(): Promise<void> {
|
|
202
|
+
const drivers = [this.primaryDriver, ...this.fallbackDrivers]
|
|
203
|
+
await Promise.all(drivers.map((driver) => driver.close().catch((err) => consola.warn(`Error closing ${driver.constructor.name}:`, err))))
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Factory functions for common use cases
|
|
208
|
+
export function createPostgresClient<DT = any>(config: { connectionString?: string; experimental?: { http?: { url: string; apiKey?: string } } }) {
|
|
209
|
+
return new SQLClient<DT>({ driver: new PostgresDriver(config) })
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export function createLibSQLClient<DT = any>(config: { url: string; authToken?: string; useTursoServerlessDriver?: boolean }) {
|
|
213
|
+
return new SQLClient<DT>({ driver: new LibSQLDriver(config) })
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function createMySQLClient<DT = any>(config: { connectionString: string }) {
|
|
217
|
+
return new SQLClient<DT>({ driver: new MySQLDriver(config) })
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function createSqliteClient<DT = any>(config: { filename: string; readonly?: boolean }) {
|
|
221
|
+
return new SQLClient<DT>({ driver: new SqliteDriver(config) })
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Legacy export for backward compatibility
|
|
225
|
+
export const DriftSQLClient = SQLClient
|