@matimo/postgres 0.1.0-alpha.10

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 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,449 @@
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 { MatimoInstance, getGlobalApprovalHandler } from '@matimo/core';
148
+ import * as readline from 'readline';
149
+
150
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
151
+ const handler = getGlobalApprovalHandler();
152
+
153
+ // Set interactive callback
154
+ handler.setApprovalCallback(async (request) => {
155
+ const rl = readline.createInterface({
156
+ input: process.stdin,
157
+ output: process.stdout
158
+ });
159
+
160
+ return new Promise(resolve => {
161
+ rl.question(
162
+ `\nApprove ${request.toolName}?\nSQL: ${request.params.sql}\n(yes/no): `,
163
+ answer => {
164
+ rl.close();
165
+ resolve(answer.toLowerCase() === 'yes');
166
+ }
167
+ );
168
+ });
169
+ });
170
+
171
+ // Approve write operations
172
+ await matimo.execute('postgres-execute-sql', {
173
+ sql: 'UPDATE users SET active = true'
174
+ });
175
+ ```
176
+
177
+ #### 2. **Automatic Approval (CI/CD)**
178
+ For automated environments:
179
+
180
+ ```bash
181
+ # Enable auto-approval for all operations requiring approval
182
+ export MATIMO_AUTO_APPROVE=true
183
+ ```
184
+
185
+ #### 3. **Pattern-Based Approval**
186
+ Pre-approve specific patterns:
187
+
188
+ ```bash
189
+ # Approve only postgres-execute-sql tool
190
+ export MATIMO_APPROVED_PATTERNS="postgres-*"
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Integration Patterns
196
+
197
+ ### Factory Pattern
198
+
199
+ ```typescript
200
+ import { MatimoInstance } from '@matimo/core';
201
+
202
+ async function main() {
203
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
204
+
205
+ // Step 1: Discover tables
206
+ const tables = await matimo.execute('postgres-execute-sql', {
207
+ sql: `
208
+ SELECT table_name
209
+ FROM information_schema.tables
210
+ WHERE table_schema = 'public'
211
+ ORDER BY table_name;
212
+ `
213
+ });
214
+
215
+ console.log('Tables:', tables.rows?.map(r => r.table_name));
216
+ }
217
+
218
+ main();
219
+ ```
220
+
221
+ ### Decorator Pattern
222
+
223
+ ```typescript
224
+ import { MatimoInstance, setGlobalMatimoInstance, tool } from '@matimo/core';
225
+
226
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
227
+ setGlobalMatimoInstance(matimo);
228
+
229
+ class DatabaseClient {
230
+ @tool('postgres-execute-sql')
231
+ async queryUsers(minAge: number) {
232
+ // Auto-executes via Matimo
233
+ }
234
+
235
+ @tool('postgres-execute-sql')
236
+ async updateUserStatus(userId: number, status: string) {
237
+ // Requires approval (UPDATE operation)
238
+ }
239
+ }
240
+
241
+ const db = new DatabaseClient();
242
+ ```
243
+
244
+ ### LangChain Integration
245
+
246
+ ```typescript
247
+ import { MatimoInstance, convertToolsToLangChain } from '@matimo/core';
248
+ import { ChatOpenAI } from '@langchain/openai';
249
+
250
+ const matimo = await MatimoInstance.init({ autoDiscover: true });
251
+
252
+ // Convert Postgres tool to LangChain format
253
+ const tools = await convertToolsToLangChain(
254
+ [matimo.getTool('postgres-execute-sql')!],
255
+ matimo
256
+ );
257
+
258
+ // LLM can now use SQL execution tool
259
+ const agent = await createAgent({
260
+ model: new ChatOpenAI({ modelName: 'gpt-4o-mini' }),
261
+ tools: tools
262
+ });
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Sequential Discovery Pattern
268
+
269
+ The recommended workflow for safe database exploration:
270
+
271
+ ```
272
+ Step 1: Discover Tables (SELECT - no approval)
273
+
274
+ Step 2: Get Table Counts/Structure (SELECT - no approval)
275
+
276
+ Step 3: Execute Destructive Operations (requires approval)
277
+
278
+ Step 4: Use Discovered Data
279
+ ```
280
+
281
+ Example implementation:
282
+
283
+ ```typescript
284
+ // 1. What tables exist?
285
+ const tables = await matimo.execute('postgres-execute-sql', {
286
+ sql: `SELECT table_name FROM information_schema.tables
287
+ WHERE table_schema = 'public' ORDER BY table_name;`
288
+ });
289
+
290
+ // 2. How much data in each table?
291
+ const counts = await matimo.execute('postgres-execute-sql', {
292
+ sql: `SELECT table_name, (SELECT count(*) FROM X)
293
+ FROM information_schema.tables WHERE table_schema = 'public';`
294
+ });
295
+
296
+ // 3. Now safely operate on discovered tables
297
+ if (tables.rows?.length > 0) {
298
+ const tableName = tables.rows[0].table_name;
299
+
300
+ // This will require approval
301
+ const result = await matimo.execute('postgres-execute-sql', {
302
+ sql: `DELETE FROM ${tableName} WHERE archived = true;`
303
+ });
304
+ }
305
+ ```
306
+
307
+ ---
308
+
309
+ ## Error Handling
310
+
311
+ ```typescript
312
+ import { MatimoError, ErrorCode } from '@matimo/core';
313
+
314
+ try {
315
+ const result = await matimo.execute('postgres-execute-sql', {
316
+ sql: 'SELECT * FROM nonexistent_table;'
317
+ });
318
+ } catch (err) {
319
+ if (err instanceof MatimoError) {
320
+ console.error(`Code: ${err.code}`);
321
+ console.error(`Message: ${err.message}`);
322
+ console.error(`Details:`, err.details);
323
+ }
324
+ }
325
+ ```
326
+
327
+ Common error codes:
328
+ - `INVALID_SCHEMA` — Missing required SQL parameter
329
+ - `EXECUTION_FAILED` — Query execution error (connection, syntax, etc.)
330
+ - `AUTH_FAILED` — Destructive operation not approved
331
+
332
+ ---
333
+
334
+ ## Connection String Format
335
+
336
+ Standard PostgreSQL connection string format:
337
+
338
+ ```
339
+ postgresql://[user[:password]@][host][:port][/database]
340
+ ```
341
+
342
+ **Examples:**
343
+ ```
344
+ postgresql://user:password@localhost:5432/mydb
345
+ postgresql://localhost/mydb # Local with defaults
346
+ postgresql://user@localhost # Without port
347
+ ```
348
+
349
+ ---
350
+
351
+ ## Environment Variables
352
+
353
+ ### Connection
354
+
355
+ | Variable | Required | Example | Description |
356
+ |----------|----------|---------|-------------|
357
+ | `MATIMO_POSTGRES_URL` | One of these | `postgresql://...` | Full connection string |
358
+ | `MATIMO_POSTGRES_HOST` | ✅ (if not URL) | `localhost` | Database host |
359
+ | `MATIMO_POSTGRES_PORT` | ❌ | `5432` | Database port (default: 5432) |
360
+ | `MATIMO_POSTGRES_USER` | ✅ (if not URL) | `postgres` | Database user |
361
+ | `MATIMO_POSTGRES_PASSWORD` | ✅ (if not URL) | `secret` | Database password |
362
+ | `MATIMO_POSTGRES_DB` | ✅ (if not URL) | `mydb` | Database name |
363
+
364
+ ### Approval
365
+
366
+ | Variable | Values | Description |
367
+ |----------|--------|-------------|
368
+ | `MATIMO_SQL_AUTO_APPROVE` | `true` / `false` | Auto-approve all destructive operations (for CI/CD) |
369
+ | `MATIMO_SQL_APPROVED_PATTERNS` | Regex patterns | Comma-separated patterns for pre-approved queries |
370
+
371
+ ---
372
+
373
+ ## Examples
374
+
375
+ See `examples/tools/postgres/` in the Matimo repository:
376
+
377
+ - **`postgres-factory.ts`** — Factory pattern with sequential discovery
378
+ - **`postgres-decorator.ts`** — Class-based decorator pattern
379
+ - **`postgres-langchain.ts`** — AI agent using LangChain (GPT-4o-mini)
380
+ - **`postgres-with-approval.ts`** — Interactive approval workflow
381
+
382
+ Run examples:
383
+ ```bash
384
+ cd examples/tools
385
+
386
+ # Factory pattern
387
+ pnpm postgres:factory
388
+
389
+ # Decorator pattern
390
+ pnpm postgres:decorator
391
+
392
+ # LangChain AI agent
393
+ pnpm postgres:langchain
394
+
395
+ # Interactive approval flow (requires terminal)
396
+ pnpm postgres:approval
397
+ ```
398
+
399
+ ---
400
+
401
+ ## Troubleshooting
402
+
403
+ ### Connection Refused
404
+ **Error:** `connect ECONNREFUSED`
405
+
406
+ **Solution:**
407
+ - Check Postgres is running: `pg_isready -h localhost -p 5432`
408
+ - Verify credentials and host
409
+ - Ensure database exists: `createdb mydb`
410
+
411
+ ### Authentication Failed
412
+ **Error:** `password authentication failed`
413
+
414
+ **Solution:**
415
+ - Check `MATIMO_POSTGRES_USER` and `MATIMO_POSTGRES_PASSWORD`
416
+ - Reset password: `ALTER USER postgres WITH PASSWORD 'newpassword';`
417
+
418
+ ### Approval Required Error
419
+ **Error:** `Destructive SQL requires approval`
420
+
421
+ **Solution:**
422
+ - Set `MATIMO_SQL_AUTO_APPROVE=true` in CI/CD
423
+ - Or use interactive approval: `pnpm postgres:approval`
424
+ - Or pre-approve patterns: `export MATIMO_SQL_APPROVED_PATTERNS="DELETE.*,UPDATE.*"`
425
+
426
+ ### Parameter Binding Error
427
+ **Error:** `bind message supplies X parameters, but prepared statement requires Y`
428
+
429
+ **Solution:**
430
+ - Use placeholders for all parameters: `$1`, `$2`, etc.
431
+ - Match number of `params` to number of placeholders
432
+ - Example: `'WHERE id = $1'` with `params: [42]`
433
+
434
+ ---
435
+
436
+ ## Contributing
437
+
438
+ Found a bug or want to request a feature?
439
+ - [Open an issue](https://github.com/tallclub/matimo/issues)
440
+ - [Start a discussion](https://github.com/tallclub/matimo/discussions)
441
+
442
+ ---
443
+
444
+ ## Part of the Matimo Ecosystem
445
+
446
+ Learn more about Matimo:
447
+ - 📖 [Documentation](https://matimo.dev/docs)
448
+ - 🔗 [GitHub Repository](https://github.com/tallclub/matimo)
449
+ - ⭐ [Star the project](https://github.com/tallclub/matimo)
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@matimo/postgres",
3
+ "version": "0.1.0-alpha.10",
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
+ },
14
+ "peerDependencies": {
15
+ "matimo": "0.1.0-alpha.10"
16
+ },
17
+ "devDependencies": {
18
+ "@types/pg": "^8.6.6"
19
+ }
20
+ }
@@ -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,70 @@
1
+ import { Client } from 'pg';
2
+ import { MatimoError, ErrorCode } 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
+ if (!connectionString) {
19
+ const host = process.env.MATIMO_POSTGRES_HOST;
20
+ const port = process.env.MATIMO_POSTGRES_PORT || '5432';
21
+ const user = process.env.MATIMO_POSTGRES_USER;
22
+ const password = process.env.MATIMO_POSTGRES_PASSWORD;
23
+ const database = process.env.MATIMO_POSTGRES_DB;
24
+
25
+ if (host && user && password && database) {
26
+ // Build a simple connection string. Do not log secrets.
27
+ connectionString = `postgresql://${encodeURIComponent(user)}:${encodeURIComponent(
28
+ password
29
+ )}@${host}:${port}/${database}`;
30
+ }
31
+ }
32
+
33
+ if (!connectionString) {
34
+ throw new MatimoError(
35
+ 'Postgres connection information not provided. Set MATIMO_POSTGRES_URL or MATIMO_POSTGRES_HOST/PORT/USER/PASSWORD/DB',
36
+ ErrorCode.EXECUTION_FAILED,
37
+ { toolName: 'postgres-execute-sql' }
38
+ );
39
+ }
40
+
41
+ const client = new Client({ connectionString });
42
+ try {
43
+ await client.connect();
44
+ const result = await client.query(sql, (params ?? []) as unknown[]);
45
+ return { rows: result.rows, rowCount: result.rowCount };
46
+ } catch (err) {
47
+ // Extract meaningful error message
48
+ const originalError = String((err as Record<string, unknown>).message || err);
49
+ const details: Record<string, unknown> = {
50
+ originalMessage: originalError,
51
+ };
52
+ // Check if it's a connection error vs query error
53
+ if (originalError.includes('ECONNREFUSED')) {
54
+ details.hint = 'Connection refused - is Postgres running at the configured host/port?';
55
+ } else if (originalError.includes('role') && originalError.includes('does not exist')) {
56
+ details.hint = 'Database user does not exist - check MATIMO_POSTGRES_USER env var';
57
+ } else if (originalError.includes('database') && originalError.includes('does not exist')) {
58
+ details.hint = 'Database does not exist - check MATIMO_POSTGRES_DB env var';
59
+ }
60
+ // Wrap errors to avoid leaking secrets
61
+ throw new MatimoError(`Postgres query failed: ${originalError}`, ErrorCode.EXECUTION_FAILED, {
62
+ toolName: 'postgres-execute-sql',
63
+ details,
64
+ });
65
+ } finally {
66
+ await client.end().catch(() => {
67
+ // Ignore connection close errors
68
+ });
69
+ }
70
+ }