@dotdo/postgres 0.1.1 → 0.1.3
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 +73 -1
- package/dist/client/index.d.ts +47 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +47 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/postgres-client.d.ts +273 -0
- package/dist/client/postgres-client.d.ts.map +1 -0
- package/dist/client/postgres-client.js +389 -0
- package/dist/client/postgres-client.js.map +1 -0
- package/dist/client/types.d.ts +167 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +7 -0
- package/dist/client/types.js.map +1 -0
- package/dist/do/index.d.ts +18 -0
- package/dist/do/index.d.ts.map +1 -0
- package/dist/do/index.js +18 -0
- package/dist/do/index.js.map +1 -0
- package/dist/do/postgres.d.ts +110 -0
- package/dist/do/postgres.d.ts.map +1 -0
- package/dist/do/postgres.js +266 -0
- package/dist/do/postgres.js.map +1 -0
- package/dist/do/sql.d.ts +92 -0
- package/dist/do/sql.d.ts.map +1 -0
- package/dist/do/sql.js +204 -0
- package/dist/do/sql.js.map +1 -0
- package/dist/index.d.ts +25 -30
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -30
- package/dist/index.js.map +1 -1
- package/dist/mcp/binding.d.ts +47 -0
- package/dist/mcp/binding.d.ts.map +1 -0
- package/dist/mcp/binding.js +183 -0
- package/dist/mcp/binding.js.map +1 -0
- package/dist/mcp/index.d.ts +92 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +91 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.d.ts +62 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +278 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/tools.d.ts +58 -0
- package/dist/mcp/tools.d.ts.map +1 -0
- package/dist/mcp/tools.js +356 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/mcp/types.d.ts +139 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/pglite/workers-pglite.d.ts +13 -4
- package/dist/pglite/workers-pglite.d.ts.map +1 -1
- package/dist/pglite/workers-pglite.js +110 -5
- package/dist/pglite/workers-pglite.js.map +1 -1
- package/dist/pglite-assets/pglite.data +0 -0
- package/dist/pglite-assets/pglite.wasm +0 -0
- package/dist/worker/auth.d.ts.map +1 -1
- package/dist/worker/auth.js +16 -6
- package/dist/worker/auth.js.map +1 -1
- package/dist/worker/background-pglite-manager.d.ts +243 -0
- package/dist/worker/background-pglite-manager.d.ts.map +1 -0
- package/dist/worker/background-pglite-manager.js +528 -0
- package/dist/worker/background-pglite-manager.js.map +1 -0
- package/dist/worker/do-pglite-manager.d.ts +77 -0
- package/dist/worker/do-pglite-manager.d.ts.map +1 -1
- package/dist/worker/do-pglite-manager.js +189 -12
- package/dist/worker/do-pglite-manager.js.map +1 -1
- package/dist/worker/entry.d.ts.map +1 -1
- package/dist/worker/entry.js +108 -26
- package/dist/worker/entry.js.map +1 -1
- package/dist/worker/index.d.ts +7 -1
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +19 -1
- package/dist/worker/index.js.map +1 -1
- package/dist/worker/lazy-pglite-manager.d.ts +242 -0
- package/dist/worker/lazy-pglite-manager.d.ts.map +1 -0
- package/dist/worker/lazy-pglite-manager.js +463 -0
- package/dist/worker/lazy-pglite-manager.js.map +1 -0
- package/package.json +20 -6
- package/src/client/index.ts +61 -0
- package/src/client/postgres-client.ts +442 -0
- package/src/client/types.ts +211 -0
- package/src/do/index.ts +18 -0
- package/src/do/postgres.ts +367 -0
- package/src/do/sql.ts +280 -0
- package/src/index.ts +50 -30
- package/src/mcp/binding.ts +236 -0
- package/src/mcp/index.ts +122 -0
- package/src/mcp/server.ts +361 -0
- package/src/mcp/tools.ts +464 -0
- package/src/mcp/types.ts +148 -0
- package/src/pglite/workers-pglite.ts +141 -12
- package/src/pglite-assets/pglite.data +0 -0
- package/src/pglite-assets/pglite.wasm +0 -0
- package/src/worker/auth.ts +17 -6
- package/src/worker/background-pglite-manager.ts +680 -0
- package/src/worker/do-pglite-manager.ts +235 -19
- package/src/worker/entry.ts +112 -30
- package/src/worker/index.ts +71 -1
- package/src/worker/lazy-pglite-manager.ts +595 -0
- package/dist/iceberg/duckdb-wasm.d.ts +0 -447
- package/dist/iceberg/duckdb-wasm.d.ts.map +0 -1
- package/dist/iceberg/duckdb-wasm.js +0 -600
- package/dist/iceberg/duckdb-wasm.js.map +0 -1
- package/dist/iceberg/test-fixtures.d.ts +0 -151
- package/dist/iceberg/test-fixtures.d.ts.map +0 -1
- package/dist/iceberg/test-fixtures.js +0 -446
- package/dist/iceberg/test-fixtures.js.map +0 -1
- package/dist/worker/__mocks__/cloudflare-workers.d.ts +0 -31
- package/dist/worker/__mocks__/cloudflare-workers.d.ts.map +0 -1
- package/dist/worker/__mocks__/cloudflare-workers.js +0 -33
- package/dist/worker/__mocks__/cloudflare-workers.js.map +0 -1
package/src/do/sql.ts
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL Tagged Template & Database Client
|
|
3
|
+
*
|
|
4
|
+
* Execute PostgreSQL queries with a simple API.
|
|
5
|
+
* Automatically routes to the Postgres Durable Object using cloudflare:workers env.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { sql, db } from '@dotdo/postgres'
|
|
10
|
+
*
|
|
11
|
+
* // Tagged template (safe from SQL injection)
|
|
12
|
+
* const posts = await sql`SELECT * FROM posts WHERE id = ${id}`
|
|
13
|
+
*
|
|
14
|
+
* // db.query for dynamic queries
|
|
15
|
+
* const result = await db.query('SELECT * FROM posts')
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface SqlConfig {
|
|
20
|
+
/** DO binding name (default: 'POSTGRES') */
|
|
21
|
+
binding?: string
|
|
22
|
+
/** DO instance name (default: 'default') */
|
|
23
|
+
instance?: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SqlResult<T = Record<string, unknown>> {
|
|
27
|
+
rows: T[]
|
|
28
|
+
rowCount: number
|
|
29
|
+
fields?: { name: string; dataTypeID: number }[]
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type PostgresNamespace = {
|
|
33
|
+
idFromName(name: string): { toString(): string }
|
|
34
|
+
get(id: { toString(): string }): {
|
|
35
|
+
fetch(request: Request): Promise<Response>
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type DOStub = {
|
|
40
|
+
fetch(request: Request): Promise<Response>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a parameterized SQL query from template literals
|
|
45
|
+
*/
|
|
46
|
+
function buildQuery(strings: TemplateStringsArray, values: unknown[]): { sql: string; params: unknown[] } {
|
|
47
|
+
let sql = ''
|
|
48
|
+
const params: unknown[] = []
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < strings.length; i++) {
|
|
51
|
+
sql += strings[i]
|
|
52
|
+
if (i < values.length) {
|
|
53
|
+
params.push(values[i])
|
|
54
|
+
sql += `$${params.length}`
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { sql, params }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Cached env and stub
|
|
62
|
+
let cachedEnv: Record<string, unknown> | null = null
|
|
63
|
+
let cachedStub: DOStub | null = null
|
|
64
|
+
|
|
65
|
+
async function getStub(config: SqlConfig = {}): Promise<DOStub> {
|
|
66
|
+
if (cachedStub) return cachedStub
|
|
67
|
+
|
|
68
|
+
if (!cachedEnv) {
|
|
69
|
+
try {
|
|
70
|
+
const mod = await import('cloudflare:workers')
|
|
71
|
+
cachedEnv = mod.env as Record<string, unknown>
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error(
|
|
74
|
+
'Could not import cloudflare:workers. ' +
|
|
75
|
+
'Ensure you are running in Cloudflare Workers with nodejs_compat.'
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const binding = config.binding || 'POSTGRES'
|
|
81
|
+
const instance = config.instance || 'default'
|
|
82
|
+
|
|
83
|
+
const namespace = cachedEnv[binding] as PostgresNamespace | undefined
|
|
84
|
+
if (!namespace) {
|
|
85
|
+
throw new Error(`Postgres binding "${binding}" not found. Add it to wrangler.toml.`)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
cachedStub = namespace.get(namespace.idFromName(instance))
|
|
89
|
+
return cachedStub
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function executeQuery<T>(sql: string, params?: unknown[]): Promise<SqlResult<T>> {
|
|
93
|
+
const stub = await getStub()
|
|
94
|
+
|
|
95
|
+
const response = await stub.fetch(new Request('http://do/query', {
|
|
96
|
+
method: 'POST',
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
body: JSON.stringify({ sql, params }),
|
|
99
|
+
}))
|
|
100
|
+
|
|
101
|
+
if (!response.ok) {
|
|
102
|
+
const error = await response.json() as { error: string }
|
|
103
|
+
throw new Error(error.error || 'Query failed')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return response.json() as Promise<SqlResult<T>>
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// =============================================================================
|
|
110
|
+
// sql - Tagged Template API
|
|
111
|
+
// =============================================================================
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* SQL tagged template - safe parameterized queries
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```typescript
|
|
118
|
+
* import { sql } from '@dotdo/postgres'
|
|
119
|
+
*
|
|
120
|
+
* const posts = await sql`SELECT * FROM posts`
|
|
121
|
+
* const post = await sql`SELECT * FROM posts WHERE id = ${id}`
|
|
122
|
+
* const newPost = await sql`INSERT INTO posts (title) VALUES (${title}) RETURNING *`
|
|
123
|
+
* ```
|
|
124
|
+
*/
|
|
125
|
+
export async function sql<T = Record<string, unknown>>(
|
|
126
|
+
strings: TemplateStringsArray,
|
|
127
|
+
...values: unknown[]
|
|
128
|
+
): Promise<SqlResult<T>> {
|
|
129
|
+
const { sql: query, params } = buildQuery(strings, values)
|
|
130
|
+
return executeQuery<T>(query, params)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Execute raw SQL (use with caution - ensure input is sanitized)
|
|
135
|
+
*/
|
|
136
|
+
sql.unsafe = async function<T = Record<string, unknown>>(
|
|
137
|
+
query: string,
|
|
138
|
+
params?: unknown[]
|
|
139
|
+
): Promise<SqlResult<T>> {
|
|
140
|
+
return executeQuery<T>(query, params)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// =============================================================================
|
|
144
|
+
// db - Database Client API
|
|
145
|
+
// =============================================================================
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Database client for programmatic access
|
|
149
|
+
*
|
|
150
|
+
* @example
|
|
151
|
+
* ```typescript
|
|
152
|
+
* import { db } from '@dotdo/postgres'
|
|
153
|
+
*
|
|
154
|
+
* const result = await db.query('SELECT * FROM posts')
|
|
155
|
+
* await db.exec('CREATE TABLE posts (id SERIAL PRIMARY KEY, title TEXT)')
|
|
156
|
+
* ```
|
|
157
|
+
*/
|
|
158
|
+
export const db = {
|
|
159
|
+
/**
|
|
160
|
+
* Execute a query and return results
|
|
161
|
+
*/
|
|
162
|
+
async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<SqlResult<T>> {
|
|
163
|
+
return executeQuery<T>(sql, params)
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Execute SQL without returning results (DDL, etc.)
|
|
168
|
+
*/
|
|
169
|
+
async exec(sql: string): Promise<void> {
|
|
170
|
+
await executeQuery(sql)
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Get raw access to the DO stub for advanced use cases
|
|
175
|
+
*/
|
|
176
|
+
async getStub(): Promise<DOStub> {
|
|
177
|
+
return getStub()
|
|
178
|
+
},
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// =============================================================================
|
|
182
|
+
// Factory Functions (for custom binding/instance)
|
|
183
|
+
// =============================================================================
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Create a sql function bound to a specific binding/instance
|
|
187
|
+
*/
|
|
188
|
+
export function createSql(env: Record<string, unknown>, config: SqlConfig = {}) {
|
|
189
|
+
const binding = config.binding || 'POSTGRES'
|
|
190
|
+
const instance = config.instance || 'default'
|
|
191
|
+
|
|
192
|
+
const namespace = env[binding] as PostgresNamespace | undefined
|
|
193
|
+
if (!namespace) {
|
|
194
|
+
throw new Error(`Postgres binding "${binding}" not found in env.`)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const stub = namespace.get(namespace.idFromName(instance))
|
|
198
|
+
|
|
199
|
+
async function boundSql<T = Record<string, unknown>>(
|
|
200
|
+
strings: TemplateStringsArray,
|
|
201
|
+
...values: unknown[]
|
|
202
|
+
): Promise<SqlResult<T>> {
|
|
203
|
+
const { sql: query, params } = buildQuery(strings, values)
|
|
204
|
+
|
|
205
|
+
const response = await stub.fetch(new Request('http://do/query', {
|
|
206
|
+
method: 'POST',
|
|
207
|
+
headers: { 'Content-Type': 'application/json' },
|
|
208
|
+
body: JSON.stringify({ sql: query, params }),
|
|
209
|
+
}))
|
|
210
|
+
|
|
211
|
+
if (!response.ok) {
|
|
212
|
+
const error = await response.json() as { error: string }
|
|
213
|
+
throw new Error(error.error || 'Query failed')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return response.json() as Promise<SqlResult<T>>
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
boundSql.unsafe = async function<T = Record<string, unknown>>(
|
|
220
|
+
query: string,
|
|
221
|
+
params?: unknown[]
|
|
222
|
+
): Promise<SqlResult<T>> {
|
|
223
|
+
const response = await stub.fetch(new Request('http://do/query', {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ sql: query, params }),
|
|
227
|
+
}))
|
|
228
|
+
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
const error = await response.json() as { error: string }
|
|
231
|
+
throw new Error(error.error || 'Query failed')
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return response.json() as Promise<SqlResult<T>>
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return boundSql
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Create a db client bound to a specific binding/instance
|
|
242
|
+
*/
|
|
243
|
+
export function createDb(env: Record<string, unknown>, config: SqlConfig = {}) {
|
|
244
|
+
const binding = config.binding || 'POSTGRES'
|
|
245
|
+
const instance = config.instance || 'default'
|
|
246
|
+
|
|
247
|
+
const namespace = env[binding] as PostgresNamespace | undefined
|
|
248
|
+
if (!namespace) {
|
|
249
|
+
throw new Error(`Postgres binding "${binding}" not found in env.`)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const stub = namespace.get(namespace.idFromName(instance))
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
async query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<SqlResult<T>> {
|
|
256
|
+
const response = await stub.fetch(new Request('http://do/query', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'Content-Type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ sql, params }),
|
|
260
|
+
}))
|
|
261
|
+
|
|
262
|
+
if (!response.ok) {
|
|
263
|
+
const error = await response.json() as { error: string }
|
|
264
|
+
throw new Error(error.error || 'Query failed')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return response.json() as Promise<SqlResult<T>>
|
|
268
|
+
},
|
|
269
|
+
|
|
270
|
+
async exec(sql: string): Promise<void> {
|
|
271
|
+
await this.query(sql)
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
getStub(): DOStub {
|
|
275
|
+
return stub
|
|
276
|
+
},
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export default sql
|
package/src/index.ts
CHANGED
|
@@ -1,48 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module @dotdo/postgres
|
|
3
3
|
*
|
|
4
|
-
* PostgreSQL
|
|
4
|
+
* PostgreSQL for Cloudflare Workers - Simple, Persistent, Powerful.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
* optimized for Cloudflare's edge infrastructure:
|
|
8
|
-
*
|
|
9
|
-
* - **Worker Module**: PostgresDO Durable Object with HTTP/WebSocket APIs
|
|
10
|
-
* - **PGLite Module**: Tiered storage system (Cache/DO/R2) for PGLite
|
|
11
|
-
* - **Extensions Module**: PGLite extension management and pgvector support
|
|
12
|
-
* - **Routing Module**: Intelligent query routing based on CPU cost estimation
|
|
13
|
-
* - **Read-Only Module**: Security-focused read-only deployment mode
|
|
14
|
-
* - **Middleware Module**: Rate limiting with sliding window and token bucket algorithms
|
|
15
|
-
*
|
|
16
|
-
* @example
|
|
6
|
+
* @example Minimal API
|
|
17
7
|
* ```typescript
|
|
18
|
-
* import {
|
|
19
|
-
*
|
|
20
|
-
* createRoutes,
|
|
21
|
-
* WebSocketHandler,
|
|
22
|
-
* } from '@dotdo/postgres'
|
|
23
|
-
*
|
|
24
|
-
* // Export the Durable Object
|
|
25
|
-
* export { PostgresDO }
|
|
26
|
-
*
|
|
27
|
-
* // Create Hono routes
|
|
28
|
-
* const app = new Hono()
|
|
29
|
-
* app.route('/api/sql', createRoutes(env))
|
|
8
|
+
* import { sql, Postgres } from '@dotdo/postgres'
|
|
9
|
+
* export { Postgres }
|
|
30
10
|
*
|
|
31
11
|
* export default {
|
|
32
|
-
* fetch:
|
|
12
|
+
* fetch: () => Response.json(await sql`SELECT * FROM posts`)
|
|
33
13
|
* }
|
|
34
14
|
* ```
|
|
35
15
|
*
|
|
36
|
-
* @example
|
|
16
|
+
* @example With Hono
|
|
37
17
|
* ```typescript
|
|
38
|
-
*
|
|
39
|
-
* import {
|
|
18
|
+
* import { Hono } from 'hono'
|
|
19
|
+
* import { sql, Postgres } from '@dotdo/postgres'
|
|
20
|
+
* export { Postgres }
|
|
40
21
|
*
|
|
41
|
-
* const
|
|
42
|
-
*
|
|
22
|
+
* const app = new Hono()
|
|
23
|
+
* app.get('/posts', c => sql`SELECT * FROM posts`.then(r => c.json(r)))
|
|
24
|
+
* app.get('/posts/:id', c => sql`SELECT * FROM posts WHERE id = ${c.req.param('id')}`.then(r => c.json(r)))
|
|
25
|
+
*
|
|
26
|
+
* export default app
|
|
43
27
|
* ```
|
|
28
|
+
*
|
|
29
|
+
* The Postgres Durable Object provides:
|
|
30
|
+
* - Full PostgreSQL via PGLite WASM
|
|
31
|
+
* - Automatic persistence via DO SQLite
|
|
32
|
+
* - Safe parameterized queries via tagged templates
|
|
33
|
+
*
|
|
34
|
+
* For advanced features (tiered storage, migrations, observability), see submodules.
|
|
44
35
|
*/
|
|
45
36
|
|
|
37
|
+
// ============================================================================
|
|
38
|
+
// Core API - The Minimal Interface
|
|
39
|
+
// ============================================================================
|
|
40
|
+
|
|
41
|
+
export { sql, db, createSql, createDb, Postgres } from './do'
|
|
42
|
+
export type { SqlConfig, SqlResult, PostgresEnv, QueryResult } from './do'
|
|
43
|
+
|
|
46
44
|
// ============================================================================
|
|
47
45
|
// Configuration Module - Centralized Constants
|
|
48
46
|
// ============================================================================
|
|
@@ -210,3 +208,25 @@ export type {
|
|
|
210
208
|
// Discriminated union
|
|
211
209
|
DiscriminatedMessage,
|
|
212
210
|
} from './types/utilities'
|
|
211
|
+
|
|
212
|
+
// ============================================================================
|
|
213
|
+
// Client Module - WebSocket client with rpc.do integration
|
|
214
|
+
// ============================================================================
|
|
215
|
+
|
|
216
|
+
export {
|
|
217
|
+
PostgresClient,
|
|
218
|
+
createPostgresClient,
|
|
219
|
+
} from './client'
|
|
220
|
+
|
|
221
|
+
export type {
|
|
222
|
+
PostgresClientConfig,
|
|
223
|
+
PostgresDORpcApi,
|
|
224
|
+
RpcQueryResult,
|
|
225
|
+
RpcBatchQuery,
|
|
226
|
+
RpcBatchResult,
|
|
227
|
+
RpcTransactionOptions,
|
|
228
|
+
TransactionApi,
|
|
229
|
+
ColumnInfo,
|
|
230
|
+
DatabaseStats,
|
|
231
|
+
ConnectionState,
|
|
232
|
+
} from './client'
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL Binding for MCP Do Tool
|
|
3
|
+
*
|
|
4
|
+
* Creates the `pg` binding that is available in the sandboxed code execution
|
|
5
|
+
* environment. Wraps PostgresDO RPC calls in a clean API.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
PGBinding,
|
|
10
|
+
PGTransaction,
|
|
11
|
+
QueryResult,
|
|
12
|
+
TableInfo,
|
|
13
|
+
ColumnInfo,
|
|
14
|
+
} from './types.js'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Interface for the query executor (PostgresDO stub)
|
|
18
|
+
*/
|
|
19
|
+
export interface QueryExecutor {
|
|
20
|
+
rpcQuery(sql: string, params?: unknown[]): Promise<{
|
|
21
|
+
rows: Record<string, unknown>[]
|
|
22
|
+
fields: Array<{ name: string; dataTypeID: number }>
|
|
23
|
+
rowCount: number
|
|
24
|
+
}>
|
|
25
|
+
rpcBatchTransaction(
|
|
26
|
+
queries: Array<{ sql: string; params?: unknown[] }>
|
|
27
|
+
): Promise<{
|
|
28
|
+
results: Array<{
|
|
29
|
+
rows: Record<string, unknown>[]
|
|
30
|
+
fields: Array<{ name: string; dataTypeID: number }>
|
|
31
|
+
rowCount: number
|
|
32
|
+
}>
|
|
33
|
+
success: boolean
|
|
34
|
+
}>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create the pg binding for the do tool
|
|
39
|
+
*
|
|
40
|
+
* @param executor - Query executor (PostgresDO stub)
|
|
41
|
+
* @returns The pg binding object
|
|
42
|
+
*/
|
|
43
|
+
export function createPGBinding(executor: QueryExecutor): PGBinding {
|
|
44
|
+
return {
|
|
45
|
+
async query<T = Record<string, unknown>>(
|
|
46
|
+
sql: string,
|
|
47
|
+
params?: unknown[]
|
|
48
|
+
): Promise<QueryResult<T>> {
|
|
49
|
+
const result = await executor.rpcQuery(sql, params)
|
|
50
|
+
return {
|
|
51
|
+
rows: result.rows as T[],
|
|
52
|
+
rowCount: result.rowCount,
|
|
53
|
+
fields: result.fields,
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
async execute(
|
|
58
|
+
sql: string,
|
|
59
|
+
params?: unknown[]
|
|
60
|
+
): Promise<{ rowCount: number }> {
|
|
61
|
+
const result = await executor.rpcQuery(sql, params)
|
|
62
|
+
return { rowCount: result.rowCount }
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
async transaction<T>(fn: (tx: PGTransaction) => Promise<T>): Promise<T> {
|
|
66
|
+
// Execute BEGIN, run function with transaction context, execute COMMIT/ROLLBACK
|
|
67
|
+
await executor.rpcQuery('BEGIN')
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const result = await fn({
|
|
71
|
+
async query<R = Record<string, unknown>>(
|
|
72
|
+
sql: string,
|
|
73
|
+
params?: unknown[]
|
|
74
|
+
): Promise<QueryResult<R>> {
|
|
75
|
+
const res = await executor.rpcQuery(sql, params)
|
|
76
|
+
return {
|
|
77
|
+
rows: res.rows as R[],
|
|
78
|
+
rowCount: res.rowCount,
|
|
79
|
+
fields: res.fields,
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
async execute(
|
|
83
|
+
sql: string,
|
|
84
|
+
params?: unknown[]
|
|
85
|
+
): Promise<{ rowCount: number }> {
|
|
86
|
+
const res = await executor.rpcQuery(sql, params)
|
|
87
|
+
return { rowCount: res.rowCount }
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
await executor.rpcQuery('COMMIT')
|
|
92
|
+
return result
|
|
93
|
+
} catch (error) {
|
|
94
|
+
await executor.rpcQuery('ROLLBACK')
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async tables(): Promise<TableInfo[]> {
|
|
100
|
+
const result = await executor.rpcQuery(`
|
|
101
|
+
SELECT
|
|
102
|
+
table_name as name,
|
|
103
|
+
table_schema as schema,
|
|
104
|
+
CASE table_type
|
|
105
|
+
WHEN 'BASE TABLE' THEN 'table'
|
|
106
|
+
WHEN 'VIEW' THEN 'view'
|
|
107
|
+
ELSE 'table'
|
|
108
|
+
END as type
|
|
109
|
+
FROM information_schema.tables
|
|
110
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
111
|
+
ORDER BY table_schema, table_name
|
|
112
|
+
`)
|
|
113
|
+
|
|
114
|
+
return result.rows.map((row) => ({
|
|
115
|
+
name: row.name as string,
|
|
116
|
+
schema: row.schema as string,
|
|
117
|
+
type: row.type as 'table' | 'view' | 'materialized_view',
|
|
118
|
+
}))
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
async schema(tableName: string): Promise<ColumnInfo[]> {
|
|
122
|
+
// Validate table name to prevent SQL injection
|
|
123
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(tableName)) {
|
|
124
|
+
throw new Error('Invalid table name')
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const result = await executor.rpcQuery(
|
|
128
|
+
`
|
|
129
|
+
SELECT
|
|
130
|
+
c.column_name as name,
|
|
131
|
+
c.data_type as type,
|
|
132
|
+
c.is_nullable = 'YES' as nullable,
|
|
133
|
+
c.column_default as "default",
|
|
134
|
+
COALESCE(
|
|
135
|
+
(SELECT true FROM information_schema.key_column_usage kcu
|
|
136
|
+
JOIN information_schema.table_constraints tc
|
|
137
|
+
ON kcu.constraint_name = tc.constraint_name
|
|
138
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
139
|
+
AND kcu.table_name = c.table_name
|
|
140
|
+
AND kcu.column_name = c.column_name
|
|
141
|
+
LIMIT 1),
|
|
142
|
+
false
|
|
143
|
+
) as "primaryKey"
|
|
144
|
+
FROM information_schema.columns c
|
|
145
|
+
WHERE c.table_name = $1
|
|
146
|
+
AND c.table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
147
|
+
ORDER BY c.ordinal_position
|
|
148
|
+
`,
|
|
149
|
+
[tableName]
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
return result.rows.map((row) => ({
|
|
153
|
+
name: row.name as string,
|
|
154
|
+
type: row.type as string,
|
|
155
|
+
nullable: Boolean(row.nullable),
|
|
156
|
+
...(row.default !== null && { default: row.default as string }),
|
|
157
|
+
primaryKey: Boolean(row.primaryKey),
|
|
158
|
+
}))
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* TypeScript type definitions for the pg binding
|
|
165
|
+
* Used by the do tool to provide type information to LLMs
|
|
166
|
+
*/
|
|
167
|
+
export const PG_BINDING_TYPES = `
|
|
168
|
+
interface QueryResult<T = Record<string, unknown>> {
|
|
169
|
+
rows: T[]
|
|
170
|
+
rowCount: number
|
|
171
|
+
fields: Array<{ name: string; dataTypeID: number }>
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
interface TableInfo {
|
|
175
|
+
name: string
|
|
176
|
+
schema: string
|
|
177
|
+
type: 'table' | 'view' | 'materialized_view'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
interface ColumnInfo {
|
|
181
|
+
name: string
|
|
182
|
+
type: string
|
|
183
|
+
nullable: boolean
|
|
184
|
+
default?: string
|
|
185
|
+
primaryKey: boolean
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
interface PGTransaction {
|
|
189
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>
|
|
190
|
+
execute(sql: string, params?: unknown[]): Promise<{ rowCount: number }>
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
declare const pg: {
|
|
194
|
+
/**
|
|
195
|
+
* Execute a SQL query with optional parameters
|
|
196
|
+
* @example
|
|
197
|
+
* const users = await pg.query('SELECT * FROM users WHERE age > $1', [18])
|
|
198
|
+
* console.log(users.rows)
|
|
199
|
+
*/
|
|
200
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<QueryResult<T>>
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Execute a SQL statement (INSERT, UPDATE, DELETE)
|
|
204
|
+
* @example
|
|
205
|
+
* const result = await pg.execute('UPDATE users SET active = true WHERE id = $1', [1])
|
|
206
|
+
* console.log(result.rowCount)
|
|
207
|
+
*/
|
|
208
|
+
execute(sql: string, params?: unknown[]): Promise<{ rowCount: number }>
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Execute multiple statements in a transaction
|
|
212
|
+
* @example
|
|
213
|
+
* await pg.transaction(async (tx) => {
|
|
214
|
+
* await tx.execute('INSERT INTO orders (user_id) VALUES ($1)', [1])
|
|
215
|
+
* await tx.execute('UPDATE users SET order_count = order_count + 1 WHERE id = $1', [1])
|
|
216
|
+
* })
|
|
217
|
+
*/
|
|
218
|
+
transaction<T>(fn: (tx: PGTransaction) => Promise<T>): Promise<T>
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* List all tables in the database
|
|
222
|
+
* @example
|
|
223
|
+
* const tables = await pg.tables()
|
|
224
|
+
* tables.forEach(t => console.log(t.name))
|
|
225
|
+
*/
|
|
226
|
+
tables(): Promise<TableInfo[]>
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Get schema information for a table
|
|
230
|
+
* @example
|
|
231
|
+
* const columns = await pg.schema('users')
|
|
232
|
+
* columns.forEach(c => console.log(\`\${c.name}: \${c.type}\`))
|
|
233
|
+
*/
|
|
234
|
+
schema(tableName: string): Promise<ColumnInfo[]>
|
|
235
|
+
}
|
|
236
|
+
`
|