@flightdev/db 0.0.2
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/.turbo/turbo-build.log +26 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +226 -0
- package/dist/d1.d.ts +60 -0
- package/dist/d1.js +97 -0
- package/dist/index.d.ts +87 -0
- package/dist/index.js +19 -0
- package/dist/neon.d.ts +32 -0
- package/dist/neon.js +117 -0
- package/dist/postgres.d.ts +38 -0
- package/dist/postgres.js +99 -0
- package/dist/supabase.d.ts +42 -0
- package/dist/supabase.js +76 -0
- package/dist/turso.d.ts +39 -0
- package/dist/turso.js +102 -0
- package/package.json +76 -0
- package/src/ambient.d.ts +154 -0
- package/src/d1.ts +177 -0
- package/src/index.ts +140 -0
- package/src/neon.ts +164 -0
- package/src/postgres.ts +145 -0
- package/src/supabase.ts +136 -0
- package/src/turso.ts +150 -0
- package/tsconfig.json +17 -0
- package/tsup.config.ts +26 -0
package/src/d1.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flightdev/db/d1 - Cloudflare D1 Driver
|
|
3
|
+
*
|
|
4
|
+
* Native Cloudflare D1 support for edge SQLite.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* // In your Cloudflare Worker
|
|
9
|
+
* import { d1 } from '@flightdev/db/d1';
|
|
10
|
+
* import { createDb } from '@flightdev/db';
|
|
11
|
+
*
|
|
12
|
+
* export default {
|
|
13
|
+
* async fetch(request, env) {
|
|
14
|
+
* const db = createDb(d1({ binding: env.DB }));
|
|
15
|
+
* const users = await db.query('SELECT * FROM users');
|
|
16
|
+
* return Response.json(users.rows);
|
|
17
|
+
* }
|
|
18
|
+
* };
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { DatabaseDriver, QueryResult, ExecuteResult, Transaction } from './index.js';
|
|
23
|
+
|
|
24
|
+
/** D1 Database binding from Cloudflare Workers */
|
|
25
|
+
export interface D1Database {
|
|
26
|
+
prepare(query: string): D1PreparedStatement;
|
|
27
|
+
batch<T = unknown>(statements: D1PreparedStatement[]): Promise<D1Result<T>[]>;
|
|
28
|
+
exec(query: string): Promise<D1ExecResult>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface D1PreparedStatement {
|
|
32
|
+
bind(...values: unknown[]): D1PreparedStatement;
|
|
33
|
+
first<T = unknown>(colName?: string): Promise<T>;
|
|
34
|
+
run(): Promise<D1Result>;
|
|
35
|
+
all<T = unknown>(): Promise<D1Result<T>>;
|
|
36
|
+
raw<T = unknown>(): Promise<T[]>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface D1Result<T = unknown> {
|
|
40
|
+
results?: T[];
|
|
41
|
+
success: boolean;
|
|
42
|
+
error?: string;
|
|
43
|
+
meta: {
|
|
44
|
+
changes: number;
|
|
45
|
+
last_row_id: number;
|
|
46
|
+
duration: number;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface D1ExecResult {
|
|
51
|
+
count: number;
|
|
52
|
+
duration: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface D1Config {
|
|
56
|
+
/** D1 database binding from env */
|
|
57
|
+
binding: D1Database;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Create a Cloudflare D1 driver
|
|
62
|
+
*/
|
|
63
|
+
export function d1(config: D1Config): DatabaseDriver {
|
|
64
|
+
const db = config.binding;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
name: 'd1',
|
|
68
|
+
|
|
69
|
+
async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>> {
|
|
70
|
+
let stmt = db.prepare(sql);
|
|
71
|
+
if (params?.length) {
|
|
72
|
+
stmt = stmt.bind(...params);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const result = await stmt.all<T>();
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
rows: result.results ?? [],
|
|
79
|
+
rowCount: result.results?.length ?? 0,
|
|
80
|
+
raw: result,
|
|
81
|
+
};
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async execute(sql: string, params?: unknown[]): Promise<ExecuteResult> {
|
|
85
|
+
let stmt = db.prepare(sql);
|
|
86
|
+
if (params?.length) {
|
|
87
|
+
stmt = stmt.bind(...params);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const result = await stmt.run();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
rowsAffected: result.meta.changes,
|
|
94
|
+
insertId: result.meta.last_row_id,
|
|
95
|
+
raw: result,
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T> {
|
|
100
|
+
// D1 supports batch operations which are atomic
|
|
101
|
+
const statements: D1PreparedStatement[] = [];
|
|
102
|
+
const results: Array<QueryResult | ExecuteResult> = [];
|
|
103
|
+
|
|
104
|
+
const tx: Transaction = {
|
|
105
|
+
async query<R = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<R>> {
|
|
106
|
+
let stmt = db.prepare(sql);
|
|
107
|
+
if (params?.length) {
|
|
108
|
+
stmt = stmt.bind(...params);
|
|
109
|
+
}
|
|
110
|
+
statements.push(stmt);
|
|
111
|
+
// Return placeholder - actual results come from batch
|
|
112
|
+
const placeholder: QueryResult<R> = { rows: [], rowCount: 0 };
|
|
113
|
+
results.push(placeholder as QueryResult);
|
|
114
|
+
return placeholder;
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async execute(sql: string, params?: unknown[]): Promise<ExecuteResult> {
|
|
118
|
+
let stmt = db.prepare(sql);
|
|
119
|
+
if (params?.length) {
|
|
120
|
+
stmt = stmt.bind(...params);
|
|
121
|
+
}
|
|
122
|
+
statements.push(stmt);
|
|
123
|
+
const placeholder: ExecuteResult = { rowsAffected: 0 };
|
|
124
|
+
results.push(placeholder);
|
|
125
|
+
return placeholder;
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
async commit() {
|
|
129
|
+
// D1 batch is atomic
|
|
130
|
+
const batchResults = await db.batch(statements);
|
|
131
|
+
|
|
132
|
+
// Update placeholders with real results
|
|
133
|
+
batchResults.forEach((result, i) => {
|
|
134
|
+
const placeholder = results[i];
|
|
135
|
+
if (placeholder && 'rows' in placeholder) {
|
|
136
|
+
(placeholder as QueryResult).rows = (result.results ?? []) as Record<string, unknown>[];
|
|
137
|
+
(placeholder as QueryResult).rowCount = result.results?.length ?? 0;
|
|
138
|
+
} else if (placeholder) {
|
|
139
|
+
(placeholder as ExecuteResult).rowsAffected = result.meta.changes;
|
|
140
|
+
(placeholder as ExecuteResult).insertId = result.meta.last_row_id;
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
async rollback() {
|
|
146
|
+
// D1 batch is all-or-nothing, so just clear statements
|
|
147
|
+
statements.length = 0;
|
|
148
|
+
results.length = 0;
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const result = await fn(tx);
|
|
154
|
+
await tx.commit();
|
|
155
|
+
return result;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
await tx.rollback();
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
async close(): Promise<void> {
|
|
163
|
+
// D1 bindings don't need closing
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async ping(): Promise<boolean> {
|
|
167
|
+
try {
|
|
168
|
+
await db.prepare('SELECT 1').first();
|
|
169
|
+
return true;
|
|
170
|
+
} catch {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default d1;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flightdev/db - Agnostic Database Layer
|
|
3
|
+
*
|
|
4
|
+
* Universal database abstraction with pluggable drivers.
|
|
5
|
+
* Inspired by Drizzle ORM's driver pattern but framework-agnostic.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { createDb } from '@flightdev/db';
|
|
10
|
+
* import { postgres } from '@flightdev/db/postgres';
|
|
11
|
+
*
|
|
12
|
+
* const db = createDb(postgres({ connectionString: process.env.DATABASE_URL }));
|
|
13
|
+
*
|
|
14
|
+
* const users = await db.query('SELECT * FROM users');
|
|
15
|
+
* await db.execute('INSERT INTO users (name) VALUES (?)', ['John']);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Core Types
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
/** Result of a database query */
|
|
24
|
+
export interface QueryResult<T = Record<string, unknown>> {
|
|
25
|
+
rows: T[];
|
|
26
|
+
rowCount: number;
|
|
27
|
+
/** Raw result from underlying driver (for advanced use) */
|
|
28
|
+
raw?: unknown;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Result of a database execute (INSERT, UPDATE, DELETE) */
|
|
32
|
+
export interface ExecuteResult {
|
|
33
|
+
rowsAffected: number;
|
|
34
|
+
insertId?: string | number;
|
|
35
|
+
raw?: unknown;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Transaction interface */
|
|
39
|
+
export interface Transaction {
|
|
40
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
|
|
41
|
+
execute(sql: string, params?: unknown[]): Promise<ExecuteResult>;
|
|
42
|
+
commit(): Promise<void>;
|
|
43
|
+
rollback(): Promise<void>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Database connection interface - what drivers must implement */
|
|
47
|
+
export interface DatabaseDriver {
|
|
48
|
+
/** Driver name for identification */
|
|
49
|
+
readonly name: string;
|
|
50
|
+
|
|
51
|
+
/** Query that returns rows */
|
|
52
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>;
|
|
53
|
+
|
|
54
|
+
/** Execute that doesn't return rows (INSERT, UPDATE, DELETE) */
|
|
55
|
+
execute(sql: string, params?: unknown[]): Promise<ExecuteResult>;
|
|
56
|
+
|
|
57
|
+
/** Begin a transaction */
|
|
58
|
+
transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T>;
|
|
59
|
+
|
|
60
|
+
/** Close the connection */
|
|
61
|
+
close(): Promise<void>;
|
|
62
|
+
|
|
63
|
+
/** Check if connection is alive */
|
|
64
|
+
ping(): Promise<boolean>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Database instance with full API */
|
|
68
|
+
export interface Database extends DatabaseDriver {
|
|
69
|
+
/** The underlying driver */
|
|
70
|
+
readonly driver: DatabaseDriver;
|
|
71
|
+
|
|
72
|
+
/** Infer types from database (Better Auth style) */
|
|
73
|
+
readonly $Infer: {
|
|
74
|
+
Query: QueryResult;
|
|
75
|
+
Execute: ExecuteResult;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ============================================================================
|
|
80
|
+
// Database Factory
|
|
81
|
+
// ============================================================================
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Create a database instance from a driver
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```typescript
|
|
88
|
+
* import { createDb } from '@flightdev/db';
|
|
89
|
+
* import { postgres } from '@flightdev/db/postgres';
|
|
90
|
+
*
|
|
91
|
+
* const db = createDb(postgres({ connectionString: process.env.DATABASE_URL }));
|
|
92
|
+
* ```
|
|
93
|
+
*/
|
|
94
|
+
export function createDb(driver: DatabaseDriver): Database {
|
|
95
|
+
return {
|
|
96
|
+
name: driver.name,
|
|
97
|
+
driver,
|
|
98
|
+
|
|
99
|
+
query: <T = Record<string, unknown>>(sql: string, params?: unknown[]) =>
|
|
100
|
+
driver.query<T>(sql, params),
|
|
101
|
+
|
|
102
|
+
execute: (sql: string, params?: unknown[]) =>
|
|
103
|
+
driver.execute(sql, params),
|
|
104
|
+
|
|
105
|
+
transaction: <T>(fn: (tx: Transaction) => Promise<T>) =>
|
|
106
|
+
driver.transaction(fn),
|
|
107
|
+
|
|
108
|
+
close: () => driver.close(),
|
|
109
|
+
|
|
110
|
+
ping: () => driver.ping(),
|
|
111
|
+
|
|
112
|
+
$Infer: {
|
|
113
|
+
Query: {} as QueryResult,
|
|
114
|
+
Execute: {} as ExecuteResult,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================================================
|
|
120
|
+
// Helper Types (for type inference)
|
|
121
|
+
// ============================================================================
|
|
122
|
+
|
|
123
|
+
/** Infer row type from a query */
|
|
124
|
+
export type InferRow<T extends QueryResult> = T['rows'][number];
|
|
125
|
+
|
|
126
|
+
/** Database configuration base */
|
|
127
|
+
export interface DatabaseConfig {
|
|
128
|
+
/** Connection string */
|
|
129
|
+
connectionString?: string;
|
|
130
|
+
/** Pool size */
|
|
131
|
+
poolSize?: number;
|
|
132
|
+
/** Connection timeout in ms */
|
|
133
|
+
timeout?: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ============================================================================
|
|
137
|
+
// Re-exports for convenience
|
|
138
|
+
// ============================================================================
|
|
139
|
+
|
|
140
|
+
export type { DatabaseDriver as Driver };
|
package/src/neon.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flightdev/db/neon - Neon Serverless Driver
|
|
3
|
+
*
|
|
4
|
+
* Uses @neondatabase/serverless for edge-compatible PostgreSQL.
|
|
5
|
+
* Supports both HTTP (single queries) and WebSocket (transactions) modes.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { neon } from '@flightdev/db/neon';
|
|
10
|
+
* import { createDb } from '@flightdev/db';
|
|
11
|
+
*
|
|
12
|
+
* const db = createDb(neon({
|
|
13
|
+
* connectionString: process.env.NEON_DATABASE_URL,
|
|
14
|
+
* mode: 'http', // 'http' for serverless, 'websocket' for transactions
|
|
15
|
+
* }));
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { DatabaseDriver, QueryResult, ExecuteResult, Transaction } from './index.js';
|
|
20
|
+
|
|
21
|
+
export interface NeonConfig {
|
|
22
|
+
/** Neon connection string */
|
|
23
|
+
connectionString: string;
|
|
24
|
+
/** Connection mode */
|
|
25
|
+
mode?: 'http' | 'websocket';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a Neon Serverless driver
|
|
30
|
+
*/
|
|
31
|
+
export function neon(config: NeonConfig): DatabaseDriver {
|
|
32
|
+
const mode = config.mode ?? 'http';
|
|
33
|
+
let sql: any = null;
|
|
34
|
+
let pool: any = null;
|
|
35
|
+
|
|
36
|
+
async function getSql() {
|
|
37
|
+
if (!sql) {
|
|
38
|
+
const neonModule = await import('@neondatabase/serverless');
|
|
39
|
+
|
|
40
|
+
if (mode === 'http') {
|
|
41
|
+
// HTTP mode - stateless, perfect for edge
|
|
42
|
+
sql = neonModule.neon(config.connectionString);
|
|
43
|
+
} else {
|
|
44
|
+
// WebSocket mode - for transactions
|
|
45
|
+
const { Pool } = neonModule;
|
|
46
|
+
pool = new Pool({ connectionString: config.connectionString });
|
|
47
|
+
sql = pool;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { sql, pool };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
name: 'neon',
|
|
55
|
+
|
|
56
|
+
async query<T = Record<string, unknown>>(sqlQuery: string, params?: unknown[]): Promise<QueryResult<T>> {
|
|
57
|
+
const { sql: client } = await getSql();
|
|
58
|
+
|
|
59
|
+
if (mode === 'http') {
|
|
60
|
+
// HTTP mode returns array directly
|
|
61
|
+
const rows = await client(sqlQuery, params ?? []);
|
|
62
|
+
return {
|
|
63
|
+
rows: rows as T[],
|
|
64
|
+
rowCount: rows.length,
|
|
65
|
+
raw: rows,
|
|
66
|
+
};
|
|
67
|
+
} else {
|
|
68
|
+
// WebSocket mode uses pg-like API
|
|
69
|
+
const result = await client.query(sqlQuery, params);
|
|
70
|
+
return {
|
|
71
|
+
rows: result.rows as T[],
|
|
72
|
+
rowCount: result.rowCount ?? 0,
|
|
73
|
+
raw: result,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async execute(sqlQuery: string, params?: unknown[]): Promise<ExecuteResult> {
|
|
79
|
+
const { sql: client } = await getSql();
|
|
80
|
+
|
|
81
|
+
if (mode === 'http') {
|
|
82
|
+
const result = await client(sqlQuery, params ?? []);
|
|
83
|
+
return {
|
|
84
|
+
rowsAffected: result.length ?? 0,
|
|
85
|
+
raw: result,
|
|
86
|
+
};
|
|
87
|
+
} else {
|
|
88
|
+
const result = await client.query(sqlQuery, params);
|
|
89
|
+
return {
|
|
90
|
+
rowsAffected: result.rowCount ?? 0,
|
|
91
|
+
raw: result,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
async transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T> {
|
|
97
|
+
if (mode === 'http') {
|
|
98
|
+
throw new Error('Transactions require websocket mode. Use neon({ mode: "websocket" })');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const { pool: p } = await getSql();
|
|
102
|
+
const client = await p.connect();
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
await client.query('BEGIN');
|
|
106
|
+
|
|
107
|
+
const tx: Transaction = {
|
|
108
|
+
async query<R = Record<string, unknown>>(sqlQuery: string, params?: unknown[]): Promise<QueryResult<R>> {
|
|
109
|
+
const result = await client.query(sqlQuery, params);
|
|
110
|
+
return {
|
|
111
|
+
rows: result.rows as R[],
|
|
112
|
+
rowCount: result.rowCount ?? 0,
|
|
113
|
+
raw: result,
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
async execute(sqlQuery: string, params?: unknown[]): Promise<ExecuteResult> {
|
|
118
|
+
const result = await client.query(sqlQuery, params);
|
|
119
|
+
return {
|
|
120
|
+
rowsAffected: result.rowCount ?? 0,
|
|
121
|
+
raw: result,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async commit() {
|
|
126
|
+
await client.query('COMMIT');
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
async rollback() {
|
|
130
|
+
await client.query('ROLLBACK');
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const result = await fn(tx);
|
|
135
|
+
await client.query('COMMIT');
|
|
136
|
+
return result;
|
|
137
|
+
} catch (error) {
|
|
138
|
+
await client.query('ROLLBACK');
|
|
139
|
+
throw error;
|
|
140
|
+
} finally {
|
|
141
|
+
client.release();
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
async close(): Promise<void> {
|
|
146
|
+
if (pool) {
|
|
147
|
+
await pool.end();
|
|
148
|
+
pool = null;
|
|
149
|
+
}
|
|
150
|
+
sql = null;
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
async ping(): Promise<boolean> {
|
|
154
|
+
try {
|
|
155
|
+
await this.query('SELECT 1');
|
|
156
|
+
return true;
|
|
157
|
+
} catch {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export default neon;
|
package/src/postgres.ts
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flightdev/db/postgres - PostgreSQL Driver
|
|
3
|
+
*
|
|
4
|
+
* Uses 'pg' (node-postgres) for maximum compatibility.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```typescript
|
|
8
|
+
* import { postgres } from '@flightdev/db/postgres';
|
|
9
|
+
* import { createDb } from '@flightdev/db';
|
|
10
|
+
*
|
|
11
|
+
* const db = createDb(postgres({
|
|
12
|
+
* connectionString: process.env.DATABASE_URL,
|
|
13
|
+
* // OR individual options:
|
|
14
|
+
* host: 'localhost',
|
|
15
|
+
* port: 5432,
|
|
16
|
+
* database: 'mydb',
|
|
17
|
+
* user: 'user',
|
|
18
|
+
* password: 'pass',
|
|
19
|
+
* }));
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { DatabaseDriver, QueryResult, ExecuteResult, Transaction, DatabaseConfig } from './index.js';
|
|
24
|
+
|
|
25
|
+
export interface PostgresConfig extends DatabaseConfig {
|
|
26
|
+
host?: string;
|
|
27
|
+
port?: number;
|
|
28
|
+
database?: string;
|
|
29
|
+
user?: string;
|
|
30
|
+
password?: string;
|
|
31
|
+
ssl?: boolean | object;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a PostgreSQL driver
|
|
36
|
+
*/
|
|
37
|
+
export function postgres(config: PostgresConfig): DatabaseDriver {
|
|
38
|
+
// Import Pool type from pg module (declared in ambient.d.ts)
|
|
39
|
+
let pool: import('pg').Pool | null = null;
|
|
40
|
+
|
|
41
|
+
async function getPool(): Promise<import('pg').Pool> {
|
|
42
|
+
if (!pool) {
|
|
43
|
+
const { Pool } = await import('pg');
|
|
44
|
+
pool = new Pool({
|
|
45
|
+
connectionString: config.connectionString,
|
|
46
|
+
host: config.host,
|
|
47
|
+
port: config.port,
|
|
48
|
+
database: config.database,
|
|
49
|
+
user: config.user,
|
|
50
|
+
password: config.password,
|
|
51
|
+
ssl: config.ssl,
|
|
52
|
+
max: config.poolSize ?? 10,
|
|
53
|
+
connectionTimeoutMillis: config.timeout ?? 30000,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
return pool;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
name: 'postgres',
|
|
61
|
+
|
|
62
|
+
async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>> {
|
|
63
|
+
const p = await getPool();
|
|
64
|
+
const result = await p.query(sql, params);
|
|
65
|
+
return {
|
|
66
|
+
rows: result.rows as T[],
|
|
67
|
+
rowCount: result.rowCount ?? 0,
|
|
68
|
+
raw: result,
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
async execute(sql: string, params?: unknown[]): Promise<ExecuteResult> {
|
|
73
|
+
const p = await getPool();
|
|
74
|
+
const result = await p.query(sql, params);
|
|
75
|
+
return {
|
|
76
|
+
rowsAffected: result.rowCount ?? 0,
|
|
77
|
+
raw: result,
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async transaction<T>(fn: (tx: Transaction) => Promise<T>): Promise<T> {
|
|
82
|
+
const p = await getPool();
|
|
83
|
+
const client = await p.connect();
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
await client.query('BEGIN');
|
|
87
|
+
|
|
88
|
+
const tx: Transaction = {
|
|
89
|
+
async query<R = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<R>> {
|
|
90
|
+
const result = await client.query(sql, params);
|
|
91
|
+
return {
|
|
92
|
+
rows: result.rows as R[],
|
|
93
|
+
rowCount: result.rowCount ?? 0,
|
|
94
|
+
raw: result,
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
async execute(sql: string, params?: unknown[]): Promise<ExecuteResult> {
|
|
99
|
+
const result = await client.query(sql, params);
|
|
100
|
+
return {
|
|
101
|
+
rowsAffected: result.rowCount ?? 0,
|
|
102
|
+
raw: result,
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
async commit() {
|
|
107
|
+
await client.query('COMMIT');
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async rollback() {
|
|
111
|
+
await client.query('ROLLBACK');
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const result = await fn(tx);
|
|
116
|
+
await client.query('COMMIT');
|
|
117
|
+
return result;
|
|
118
|
+
} catch (error) {
|
|
119
|
+
await client.query('ROLLBACK');
|
|
120
|
+
throw error;
|
|
121
|
+
} finally {
|
|
122
|
+
client.release();
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
async close(): Promise<void> {
|
|
127
|
+
if (pool) {
|
|
128
|
+
await pool.end();
|
|
129
|
+
pool = null;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
async ping(): Promise<boolean> {
|
|
134
|
+
try {
|
|
135
|
+
const p = await getPool();
|
|
136
|
+
await p.query('SELECT 1');
|
|
137
|
+
return true;
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export default postgres;
|