@matimo/postgres 0.1.0-alpha.7
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 +440 -0
- package/package.json +18 -0
- package/tools/execute-sql/definition.yaml +59 -0
- package/tools/execute-sql/execute-sql.ts +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tallclub
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# @matimo/postgres — Postgres Tools for Matimo
|
|
2
|
+
|
|
3
|
+
Secure, approval-aware SQL execution for Postgres databases with built-in protection for destructive operations.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
✨ **Core Capabilities**
|
|
8
|
+
- Execute any SQL query (SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, etc.)
|
|
9
|
+
- Automatic destructive operation detection
|
|
10
|
+
- Approval workflow for dangerous operations
|
|
11
|
+
- Flexible connection options (URL or individual parameters)
|
|
12
|
+
- Full parameter binding support
|
|
13
|
+
- Sequential discovery pattern for safe database exploration
|
|
14
|
+
|
|
15
|
+
🔒 **Safety & Security**
|
|
16
|
+
- Detects destructive SQL: `CREATE`, `DROP`, `ALTER`, `TRUNCATE`, `DELETE`, `UPDATE`, `INSERT`
|
|
17
|
+
- Requires explicit approval before executing destructive operations
|
|
18
|
+
- Support for both interactive approval (CLI) and automated approval (CI/CD)
|
|
19
|
+
- Never exposes database credentials to external systems
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add @matimo/postgres
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Quick Start
|
|
32
|
+
|
|
33
|
+
### 1. Set Up Database Connection
|
|
34
|
+
|
|
35
|
+
Choose one approach:
|
|
36
|
+
|
|
37
|
+
**Option A: Connection String**
|
|
38
|
+
```bash
|
|
39
|
+
export MATIMO_POSTGRES_URL="postgresql://user:password@localhost:5432/dbname"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Option B: Individual Parameters**
|
|
43
|
+
```bash
|
|
44
|
+
export MATIMO_POSTGRES_HOST="localhost"
|
|
45
|
+
export MATIMO_POSTGRES_PORT="5432"
|
|
46
|
+
export MATIMO_POSTGRES_USER="user"
|
|
47
|
+
export MATIMO_POSTGRES_PASSWORD="password"
|
|
48
|
+
export MATIMO_POSTGRES_DB="dbname"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### 2. Use in Code
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { MatimoInstance } from '@matimo/core';
|
|
55
|
+
|
|
56
|
+
// Initialize with auto-discovery
|
|
57
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
58
|
+
|
|
59
|
+
// Execute a safe SELECT query (no approval needed)
|
|
60
|
+
const result = await matimo.execute('postgres-execute-sql', {
|
|
61
|
+
sql: 'SELECT * FROM users WHERE id = $1;',
|
|
62
|
+
params: [42]
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
console.log(result.rows);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Available Tools
|
|
71
|
+
|
|
72
|
+
### `postgres-execute-sql`
|
|
73
|
+
|
|
74
|
+
Execute SQL queries against a Postgres database with automatic approval for destructive operations.
|
|
75
|
+
|
|
76
|
+
#### Parameters
|
|
77
|
+
|
|
78
|
+
| Parameter | Type | Required | Description |
|
|
79
|
+
|-----------|------|----------|-------------|
|
|
80
|
+
| `sql` | `string` | ✅ | SQL query to execute. Use `$1`, `$2`, etc. for parameterized queries. |
|
|
81
|
+
| `params` | `unknown[]` \| `unknown[][]` | ❌ | Query parameters for parameterized SQL. |
|
|
82
|
+
|
|
83
|
+
#### Returns
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
{
|
|
87
|
+
success: boolean;
|
|
88
|
+
rows?: any[]; // Returned rows (for SELECT queries)
|
|
89
|
+
rowCount?: number; // Affected rows (for INSERT/UPDATE/DELETE)
|
|
90
|
+
command?: string; // SQL command executed
|
|
91
|
+
error?: string; // Error message (if failed)
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Examples
|
|
96
|
+
|
|
97
|
+
**Safe SELECT (no approval needed)**
|
|
98
|
+
```typescript
|
|
99
|
+
const result = await matimo.execute('postgres-execute-sql', {
|
|
100
|
+
sql: 'SELECT id, name FROM users LIMIT 10;'
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Parameterized Query (prevents SQL injection)**
|
|
105
|
+
```typescript
|
|
106
|
+
const result = await matimo.execute('postgres-execute-sql', {
|
|
107
|
+
sql: 'SELECT * FROM users WHERE name = $1 AND age > $2;',
|
|
108
|
+
params: ['Alice', 25]
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
**Destructive Operation (requires approval)**
|
|
113
|
+
```typescript
|
|
114
|
+
// This will trigger approval workflow
|
|
115
|
+
const result = await matimo.execute('postgres-execute-sql', {
|
|
116
|
+
sql: 'UPDATE users SET last_login = NOW() WHERE id = $1;',
|
|
117
|
+
params: [42]
|
|
118
|
+
});
|
|
119
|
+
// ⚠️ User will be prompted for approval (unless auto-approved)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Approval Flow
|
|
125
|
+
|
|
126
|
+
### Automatic Detection
|
|
127
|
+
|
|
128
|
+
The tool automatically detects destructive operations:
|
|
129
|
+
|
|
130
|
+
| Operation | Detected | Auto-Approved | Requires Approval |
|
|
131
|
+
|-----------|----------|---------------|-------------------|
|
|
132
|
+
| SELECT | ✅ | ✅ Yes | ❌ No |
|
|
133
|
+
| INSERT | ✅ | ✅ Yes| ❌ No|
|
|
134
|
+
| UPDATE | ✅ | ❌ No | ✅ Yes |
|
|
135
|
+
| DELETE | ✅ | ❌ No | ✅ Yes |
|
|
136
|
+
| CREATE | ✅ | ❌ No | ✅ Yes |
|
|
137
|
+
| DROP | ✅ | ❌ No | ✅ Yes |
|
|
138
|
+
| ALTER | ✅ | ❌ No | ✅ Yes |
|
|
139
|
+
| TRUNCATE | ✅ | ❌ No | ✅ Yes |
|
|
140
|
+
|
|
141
|
+
### Approval Methods
|
|
142
|
+
|
|
143
|
+
#### 1. **Interactive Approval (CLI)**
|
|
144
|
+
For interactive terminal environments:
|
|
145
|
+
|
|
146
|
+
```typescript
|
|
147
|
+
import { getSQLApprovalManager } from '@matimo/core';
|
|
148
|
+
import * as readline from 'readline';
|
|
149
|
+
|
|
150
|
+
const manager = getSQLApprovalManager();
|
|
151
|
+
|
|
152
|
+
// Set interactive callback
|
|
153
|
+
manager.setApprovalCallback(async (sql: string, mode: string) => {
|
|
154
|
+
const rl = readline.createInterface({
|
|
155
|
+
input: process.stdin,
|
|
156
|
+
output: process.stdout
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return new Promise(resolve => {
|
|
160
|
+
rl.question(`Approve ${mode} operation: ${sql}? (yes/no): `, answer => {
|
|
161
|
+
rl.close();
|
|
162
|
+
resolve(answer.toLowerCase() === 'yes');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### 2. **Automatic Approval (CI/CD)**
|
|
169
|
+
For automated environments:
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
# Enable auto-approval for all destructive operations
|
|
173
|
+
export MATIMO_SQL_AUTO_APPROVE=true
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
#### 3. **Pattern-Based Approval**
|
|
177
|
+
Pre-approve specific patterns:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# Approve all DELETE queries and UPDATE queries on users table
|
|
181
|
+
export MATIMO_SQL_APPROVED_PATTERNS="DELETE.*,UPDATE users.*"
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Integration Patterns
|
|
187
|
+
|
|
188
|
+
### Factory Pattern
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { MatimoInstance } from '@matimo/core';
|
|
192
|
+
|
|
193
|
+
async function main() {
|
|
194
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
195
|
+
|
|
196
|
+
// Step 1: Discover tables
|
|
197
|
+
const tables = await matimo.execute('postgres-execute-sql', {
|
|
198
|
+
sql: `
|
|
199
|
+
SELECT table_name
|
|
200
|
+
FROM information_schema.tables
|
|
201
|
+
WHERE table_schema = 'public'
|
|
202
|
+
ORDER BY table_name;
|
|
203
|
+
`
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
console.log('Tables:', tables.rows?.map(r => r.table_name));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
main();
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Decorator Pattern
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { MatimoInstance, setGlobalMatimoInstance, tool } from '@matimo/core';
|
|
216
|
+
|
|
217
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
218
|
+
setGlobalMatimoInstance(matimo);
|
|
219
|
+
|
|
220
|
+
class DatabaseClient {
|
|
221
|
+
@tool('postgres-execute-sql')
|
|
222
|
+
async queryUsers(minAge: number) {
|
|
223
|
+
// Auto-executes via Matimo
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
@tool('postgres-execute-sql')
|
|
227
|
+
async updateUserStatus(userId: number, status: string) {
|
|
228
|
+
// Requires approval (UPDATE operation)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const db = new DatabaseClient();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### LangChain Integration
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
import { MatimoInstance, convertToolsToLangChain } from '@matimo/core';
|
|
239
|
+
import { ChatOpenAI } from '@langchain/openai';
|
|
240
|
+
|
|
241
|
+
const matimo = await MatimoInstance.init({ autoDiscover: true });
|
|
242
|
+
|
|
243
|
+
// Convert Postgres tool to LangChain format
|
|
244
|
+
const tools = await convertToolsToLangChain(
|
|
245
|
+
[matimo.getTool('postgres-execute-sql')!],
|
|
246
|
+
matimo
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// LLM can now use SQL execution tool
|
|
250
|
+
const agent = await createAgent({
|
|
251
|
+
model: new ChatOpenAI({ modelName: 'gpt-4o-mini' }),
|
|
252
|
+
tools: tools
|
|
253
|
+
});
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Sequential Discovery Pattern
|
|
259
|
+
|
|
260
|
+
The recommended workflow for safe database exploration:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
Step 1: Discover Tables (SELECT - no approval)
|
|
264
|
+
↓
|
|
265
|
+
Step 2: Get Table Counts/Structure (SELECT - no approval)
|
|
266
|
+
↓
|
|
267
|
+
Step 3: Execute Destructive Operations (requires approval)
|
|
268
|
+
↓
|
|
269
|
+
Step 4: Use Discovered Data
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
Example implementation:
|
|
273
|
+
|
|
274
|
+
```typescript
|
|
275
|
+
// 1. What tables exist?
|
|
276
|
+
const tables = await matimo.execute('postgres-execute-sql', {
|
|
277
|
+
sql: `SELECT table_name FROM information_schema.tables
|
|
278
|
+
WHERE table_schema = 'public' ORDER BY table_name;`
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// 2. How much data in each table?
|
|
282
|
+
const counts = await matimo.execute('postgres-execute-sql', {
|
|
283
|
+
sql: `SELECT table_name, (SELECT count(*) FROM X)
|
|
284
|
+
FROM information_schema.tables WHERE table_schema = 'public';`
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
// 3. Now safely operate on discovered tables
|
|
288
|
+
if (tables.rows?.length > 0) {
|
|
289
|
+
const tableName = tables.rows[0].table_name;
|
|
290
|
+
|
|
291
|
+
// This will require approval
|
|
292
|
+
const result = await matimo.execute('postgres-execute-sql', {
|
|
293
|
+
sql: `DELETE FROM ${tableName} WHERE archived = true;`
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
## Error Handling
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
import { MatimoError, ErrorCode } from '@matimo/core';
|
|
304
|
+
|
|
305
|
+
try {
|
|
306
|
+
const result = await matimo.execute('postgres-execute-sql', {
|
|
307
|
+
sql: 'SELECT * FROM nonexistent_table;'
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err instanceof MatimoError) {
|
|
311
|
+
console.error(`Code: ${err.code}`);
|
|
312
|
+
console.error(`Message: ${err.message}`);
|
|
313
|
+
console.error(`Details:`, err.details);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
Common error codes:
|
|
319
|
+
- `INVALID_SCHEMA` — Missing required SQL parameter
|
|
320
|
+
- `EXECUTION_FAILED` — Query execution error (connection, syntax, etc.)
|
|
321
|
+
- `AUTH_FAILED` — Destructive operation not approved
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## Connection String Format
|
|
326
|
+
|
|
327
|
+
Standard PostgreSQL connection string format:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
postgresql://[user[:password]@][host][:port][/database]
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
**Examples:**
|
|
334
|
+
```
|
|
335
|
+
postgresql://user:password@localhost:5432/mydb
|
|
336
|
+
postgresql://localhost/mydb # Local with defaults
|
|
337
|
+
postgresql://user@localhost # Without port
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Environment Variables
|
|
343
|
+
|
|
344
|
+
### Connection
|
|
345
|
+
|
|
346
|
+
| Variable | Required | Example | Description |
|
|
347
|
+
|----------|----------|---------|-------------|
|
|
348
|
+
| `MATIMO_POSTGRES_URL` | One of these | `postgresql://...` | Full connection string |
|
|
349
|
+
| `MATIMO_POSTGRES_HOST` | ✅ (if not URL) | `localhost` | Database host |
|
|
350
|
+
| `MATIMO_POSTGRES_PORT` | ❌ | `5432` | Database port (default: 5432) |
|
|
351
|
+
| `MATIMO_POSTGRES_USER` | ✅ (if not URL) | `postgres` | Database user |
|
|
352
|
+
| `MATIMO_POSTGRES_PASSWORD` | ✅ (if not URL) | `secret` | Database password |
|
|
353
|
+
| `MATIMO_POSTGRES_DB` | ✅ (if not URL) | `mydb` | Database name |
|
|
354
|
+
|
|
355
|
+
### Approval
|
|
356
|
+
|
|
357
|
+
| Variable | Values | Description |
|
|
358
|
+
|----------|--------|-------------|
|
|
359
|
+
| `MATIMO_SQL_AUTO_APPROVE` | `true` / `false` | Auto-approve all destructive operations (for CI/CD) |
|
|
360
|
+
| `MATIMO_SQL_APPROVED_PATTERNS` | Regex patterns | Comma-separated patterns for pre-approved queries |
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
## Examples
|
|
365
|
+
|
|
366
|
+
See `examples/tools/postgres/` in the Matimo repository:
|
|
367
|
+
|
|
368
|
+
- **`postgres-factory.ts`** — Factory pattern with sequential discovery
|
|
369
|
+
- **`postgres-decorator.ts`** — Class-based decorator pattern
|
|
370
|
+
- **`postgres-langchain.ts`** — AI agent using LangChain (GPT-4o-mini)
|
|
371
|
+
- **`postgres-with-approval.ts`** — Interactive approval workflow
|
|
372
|
+
|
|
373
|
+
Run examples:
|
|
374
|
+
```bash
|
|
375
|
+
cd examples/tools
|
|
376
|
+
|
|
377
|
+
# Factory pattern
|
|
378
|
+
pnpm postgres:factory
|
|
379
|
+
|
|
380
|
+
# Decorator pattern
|
|
381
|
+
pnpm postgres:decorator
|
|
382
|
+
|
|
383
|
+
# LangChain AI agent
|
|
384
|
+
pnpm postgres:langchain
|
|
385
|
+
|
|
386
|
+
# Interactive approval flow (requires terminal)
|
|
387
|
+
pnpm postgres:approval
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
---
|
|
391
|
+
|
|
392
|
+
## Troubleshooting
|
|
393
|
+
|
|
394
|
+
### Connection Refused
|
|
395
|
+
**Error:** `connect ECONNREFUSED`
|
|
396
|
+
|
|
397
|
+
**Solution:**
|
|
398
|
+
- Check Postgres is running: `pg_isready -h localhost -p 5432`
|
|
399
|
+
- Verify credentials and host
|
|
400
|
+
- Ensure database exists: `createdb mydb`
|
|
401
|
+
|
|
402
|
+
### Authentication Failed
|
|
403
|
+
**Error:** `password authentication failed`
|
|
404
|
+
|
|
405
|
+
**Solution:**
|
|
406
|
+
- Check `MATIMO_POSTGRES_USER` and `MATIMO_POSTGRES_PASSWORD`
|
|
407
|
+
- Reset password: `ALTER USER postgres WITH PASSWORD 'newpassword';`
|
|
408
|
+
|
|
409
|
+
### Approval Required Error
|
|
410
|
+
**Error:** `Destructive SQL requires approval`
|
|
411
|
+
|
|
412
|
+
**Solution:**
|
|
413
|
+
- Set `MATIMO_SQL_AUTO_APPROVE=true` in CI/CD
|
|
414
|
+
- Or use interactive approval: `pnpm postgres:approval`
|
|
415
|
+
- Or pre-approve patterns: `export MATIMO_SQL_APPROVED_PATTERNS="DELETE.*,UPDATE.*"`
|
|
416
|
+
|
|
417
|
+
### Parameter Binding Error
|
|
418
|
+
**Error:** `bind message supplies X parameters, but prepared statement requires Y`
|
|
419
|
+
|
|
420
|
+
**Solution:**
|
|
421
|
+
- Use placeholders for all parameters: `$1`, `$2`, etc.
|
|
422
|
+
- Match number of `params` to number of placeholders
|
|
423
|
+
- Example: `'WHERE id = $1'` with `params: [42]`
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## Contributing
|
|
428
|
+
|
|
429
|
+
Found a bug or want to request a feature?
|
|
430
|
+
- [Open an issue](https://github.com/tallclub/matimo/issues)
|
|
431
|
+
- [Start a discussion](https://github.com/tallclub/matimo/discussions)
|
|
432
|
+
|
|
433
|
+
---
|
|
434
|
+
|
|
435
|
+
## Part of the Matimo Ecosystem
|
|
436
|
+
|
|
437
|
+
Learn more about Matimo:
|
|
438
|
+
- 📖 [Documentation](https://matimo.dev/docs)
|
|
439
|
+
- 🔗 [GitHub Repository](https://github.com/tallclub/matimo)
|
|
440
|
+
- ⭐ [Star the project](https://github.com/tallclub/matimo)
|
package/package.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@matimo/postgres",
|
|
3
|
+
"version": "0.1.0-alpha.7",
|
|
4
|
+
"description": "Postgres tools for Matimo",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"tools",
|
|
8
|
+
"README.md",
|
|
9
|
+
"definition.yaml"
|
|
10
|
+
],
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"pg": "^8.18.0",
|
|
13
|
+
"@matimo/core": "0.1.0-alpha.7"
|
|
14
|
+
},
|
|
15
|
+
"devDependencies": {
|
|
16
|
+
"@types/pg": "^8.6.6"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: postgres-execute-sql
|
|
2
|
+
version: '1.0.0'
|
|
3
|
+
description: Execute arbitrary SQL against a Postgres database. Supports both connection string and explicit env var configuration.
|
|
4
|
+
|
|
5
|
+
parameters:
|
|
6
|
+
sql:
|
|
7
|
+
type: string
|
|
8
|
+
description: "SQL statement to execute. Use parameterized queries for safety (e.g. $1, $2)."
|
|
9
|
+
required: true
|
|
10
|
+
params:
|
|
11
|
+
type: array
|
|
12
|
+
description: "Optional array of parameters to pass to the query."
|
|
13
|
+
required: false
|
|
14
|
+
schema:
|
|
15
|
+
type: string
|
|
16
|
+
description: "Optional schema name to search for tables or qualify queries."
|
|
17
|
+
required: false
|
|
18
|
+
|
|
19
|
+
execution:
|
|
20
|
+
type: function
|
|
21
|
+
# The function file path is resolved relative to this definition.yaml
|
|
22
|
+
code: ./execute-sql.ts
|
|
23
|
+
timeout: 30000
|
|
24
|
+
|
|
25
|
+
authentication:
|
|
26
|
+
type: custom
|
|
27
|
+
notes: |
|
|
28
|
+
The tool supports two authentication modes (choose either):
|
|
29
|
+
1. Connection string - set `MATIMO_POSTGRES_URL` to a full Postgres connection string (recommended).
|
|
30
|
+
2. Separate env vars - set `MATIMO_POSTGRES_HOST`, `MATIMO_POSTGRES_PORT`, `MATIMO_POSTGRES_USER`, `MATIMO_POSTGRES_PASSWORD`, `MATIMO_POSTGRES_DB`.
|
|
31
|
+
# Consumers should treat secrets as env variables. This custom auth type
|
|
32
|
+
# documents expected environment variables but does not enforce a single
|
|
33
|
+
# auth scheme - the executor reads either `MATIMO_POSTGRES_URL` or the
|
|
34
|
+
# individual env vars at runtime.
|
|
35
|
+
|
|
36
|
+
error_handling:
|
|
37
|
+
retry: 1
|
|
38
|
+
backoff_type: exponential
|
|
39
|
+
initial_delay_ms: 500
|
|
40
|
+
|
|
41
|
+
output_schema:
|
|
42
|
+
type: object
|
|
43
|
+
properties:
|
|
44
|
+
rows:
|
|
45
|
+
type: array
|
|
46
|
+
items:
|
|
47
|
+
type: object
|
|
48
|
+
rowCount:
|
|
49
|
+
type: number
|
|
50
|
+
|
|
51
|
+
examples:
|
|
52
|
+
- name: Simple select
|
|
53
|
+
params:
|
|
54
|
+
sql: "SELECT id, name FROM users WHERE id = $1"
|
|
55
|
+
params: [1]
|
|
56
|
+
|
|
57
|
+
- name: Parameterless query
|
|
58
|
+
params:
|
|
59
|
+
sql: "SELECT count(*) as cnt FROM users"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Client } from 'pg';
|
|
2
|
+
import { MatimoError, ErrorCode, getSQLApprovalManager } from '@matimo/core';
|
|
3
|
+
|
|
4
|
+
export default async function (input: Record<string, unknown>) {
|
|
5
|
+
const sql = (input.sql as string) || '';
|
|
6
|
+
const params = (input.params as unknown) as unknown[] | undefined;
|
|
7
|
+
|
|
8
|
+
if (!sql || sql.trim().length === 0) {
|
|
9
|
+
throw new MatimoError('Missing SQL statement', ErrorCode.EXECUTION_FAILED, {
|
|
10
|
+
toolName: 'postgres-execute-sql',
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Build connection string from either MATIMO_POSTGRES_URL or separate env vars
|
|
15
|
+
const envUrl = process.env.MATIMO_POSTGRES_URL;
|
|
16
|
+
let connectionString: string | undefined = envUrl;
|
|
17
|
+
|
|
18
|
+
// Detect destructive SQL and require approval
|
|
19
|
+
const destructiveRegex = /^\s*(CREATE|DROP|ALTER|TRUNCATE|DELETE|UPDATE)\b/i;
|
|
20
|
+
const isDestructive = destructiveRegex.test(sql);
|
|
21
|
+
if (isDestructive) {
|
|
22
|
+
const manager = getSQLApprovalManager();
|
|
23
|
+
try {
|
|
24
|
+
const ok = await manager.isApproved(sql, 'write');
|
|
25
|
+
if (!ok) {
|
|
26
|
+
throw new MatimoError('Destructive SQL not approved', ErrorCode.EXECUTION_FAILED, {
|
|
27
|
+
toolName: 'postgres-execute-sql',
|
|
28
|
+
hint:
|
|
29
|
+
'Destructive SQL requires approval. Use getSQLApprovalManager().setApprovalCallback() or set MATIMO_SQL_APPROVED_PATTERNS / MATIMO_SQL_AUTO_APPROVE=true',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
33
|
+
} catch (e: any) {
|
|
34
|
+
// Re-throw MatimoError or wrap
|
|
35
|
+
if (e instanceof MatimoError) throw e;
|
|
36
|
+
throw new MatimoError('SQL approval check failed', ErrorCode.EXECUTION_FAILED, {
|
|
37
|
+
toolName: 'postgres-execute-sql',
|
|
38
|
+
details: { message: e?.message || String(e) },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!connectionString) {
|
|
44
|
+
const host = process.env.MATIMO_POSTGRES_HOST;
|
|
45
|
+
const port = process.env.MATIMO_POSTGRES_PORT || '5432';
|
|
46
|
+
const user = process.env.MATIMO_POSTGRES_USER;
|
|
47
|
+
const password = process.env.MATIMO_POSTGRES_PASSWORD;
|
|
48
|
+
const database = process.env.MATIMO_POSTGRES_DB;
|
|
49
|
+
|
|
50
|
+
if (host && user && password && database) {
|
|
51
|
+
// Build a simple connection string. Do not log secrets.
|
|
52
|
+
connectionString = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(
|
|
53
|
+
password
|
|
54
|
+
)}@${host}:${port}/${database}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!connectionString) {
|
|
59
|
+
throw new MatimoError(
|
|
60
|
+
'Postgres connection information not provided. Set MATIMO_POSTGRES_URL or MATIMO_POSTGRES_HOST/PORT/USER/PASSWORD/DB',
|
|
61
|
+
ErrorCode.EXECUTION_FAILED,
|
|
62
|
+
{ toolName: 'postgres-execute-sql' }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const client = new Client({ connectionString });
|
|
67
|
+
try {
|
|
68
|
+
await client.connect();
|
|
69
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
+
const result = await client.query(sql, (params ?? []) as any);
|
|
71
|
+
return { rows: result.rows, rowCount: result.rowCount };
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Extract meaningful error message
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
const originalError = (err as any)?.message || String(err);
|
|
76
|
+
const details: Record<string, unknown> = {
|
|
77
|
+
originalMessage: originalError,
|
|
78
|
+
};
|
|
79
|
+
// Check if it's a connection error vs query error
|
|
80
|
+
if (originalError.includes('ECONNREFUSED')) {
|
|
81
|
+
details.hint = 'Connection refused - is Postgres running at the configured host/port?';
|
|
82
|
+
} else if (originalError.includes('role') && originalError.includes('does not exist')) {
|
|
83
|
+
details.hint = 'Database user does not exist - check MATIMO_POSTGRES_USER env var';
|
|
84
|
+
} else if (originalError.includes('database') && originalError.includes('does not exist')) {
|
|
85
|
+
details.hint = 'Database does not exist - check MATIMO_POSTGRES_DB env var';
|
|
86
|
+
}
|
|
87
|
+
// Wrap errors to avoid leaking secrets
|
|
88
|
+
throw new MatimoError(`Postgres query failed: ${originalError}`, ErrorCode.EXECUTION_FAILED, {
|
|
89
|
+
toolName: 'postgres-execute-sql',
|
|
90
|
+
details,
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
try {
|
|
94
|
+
await client.end();
|
|
95
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
96
|
+
} catch (_e) {
|
|
97
|
+
// ignore
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|