postgresai 0.14.0-dev.8 → 0.14.0-dev.81
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 +161 -61
- package/bin/postgres-ai.ts +2596 -428
- package/bun.lock +258 -0
- package/bunfig.toml +20 -0
- package/dist/bin/postgres-ai.js +31277 -1575
- package/dist/sql/01.role.sql +16 -0
- package/dist/sql/02.extensions.sql +8 -0
- package/dist/sql/03.permissions.sql +38 -0
- package/dist/sql/04.optional_rds.sql +6 -0
- package/dist/sql/05.optional_self_managed.sql +8 -0
- package/dist/sql/06.helpers.sql +439 -0
- package/dist/sql/sql/01.role.sql +16 -0
- package/dist/sql/sql/02.extensions.sql +8 -0
- package/dist/sql/sql/03.permissions.sql +38 -0
- package/dist/sql/sql/04.optional_rds.sql +6 -0
- package/dist/sql/sql/05.optional_self_managed.sql +8 -0
- package/dist/sql/sql/06.helpers.sql +439 -0
- package/dist/sql/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/sql/uninit/03.role.sql +27 -0
- package/dist/sql/uninit/01.helpers.sql +5 -0
- package/dist/sql/uninit/02.permissions.sql +30 -0
- package/dist/sql/uninit/03.role.sql +27 -0
- package/lib/auth-server.ts +124 -106
- package/lib/checkup-api.ts +386 -0
- package/lib/checkup-dictionary.ts +113 -0
- package/lib/checkup.ts +1512 -0
- package/lib/config.ts +6 -3
- package/lib/init.ts +655 -189
- package/lib/issues.ts +848 -193
- package/lib/mcp-server.ts +391 -91
- package/lib/metrics-loader.ts +127 -0
- package/lib/supabase.ts +824 -0
- package/lib/util.ts +61 -0
- package/package.json +22 -10
- package/packages/postgres-ai/README.md +26 -0
- package/packages/postgres-ai/bin/postgres-ai.js +27 -0
- package/packages/postgres-ai/package.json +27 -0
- package/scripts/embed-checkup-dictionary.ts +106 -0
- package/scripts/embed-metrics.ts +154 -0
- package/sql/01.role.sql +16 -0
- package/sql/02.extensions.sql +8 -0
- package/sql/03.permissions.sql +38 -0
- package/sql/04.optional_rds.sql +6 -0
- package/sql/05.optional_self_managed.sql +8 -0
- package/sql/06.helpers.sql +439 -0
- package/sql/uninit/01.helpers.sql +5 -0
- package/sql/uninit/02.permissions.sql +30 -0
- package/sql/uninit/03.role.sql +27 -0
- package/test/auth.test.ts +258 -0
- package/test/checkup.integration.test.ts +321 -0
- package/test/checkup.test.ts +1116 -0
- package/test/config-consistency.test.ts +36 -0
- package/test/init.integration.test.ts +508 -0
- package/test/init.test.ts +916 -0
- package/test/issues.cli.test.ts +538 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +1527 -0
- package/test/schema-validation.test.ts +81 -0
- package/test/supabase.test.ts +568 -0
- package/test/test-utils.ts +128 -0
- package/tsconfig.json +12 -20
- package/dist/bin/postgres-ai.d.ts +0 -3
- package/dist/bin/postgres-ai.d.ts.map +0 -1
- package/dist/bin/postgres-ai.js.map +0 -1
- package/dist/lib/auth-server.d.ts +0 -31
- package/dist/lib/auth-server.d.ts.map +0 -1
- package/dist/lib/auth-server.js +0 -263
- package/dist/lib/auth-server.js.map +0 -1
- package/dist/lib/config.d.ts +0 -45
- package/dist/lib/config.d.ts.map +0 -1
- package/dist/lib/config.js +0 -181
- package/dist/lib/config.js.map +0 -1
- package/dist/lib/init.d.ts +0 -64
- package/dist/lib/init.d.ts.map +0 -1
- package/dist/lib/init.js +0 -399
- package/dist/lib/init.js.map +0 -1
- package/dist/lib/issues.d.ts +0 -75
- package/dist/lib/issues.d.ts.map +0 -1
- package/dist/lib/issues.js +0 -336
- package/dist/lib/issues.js.map +0 -1
- package/dist/lib/mcp-server.d.ts +0 -9
- package/dist/lib/mcp-server.d.ts.map +0 -1
- package/dist/lib/mcp-server.js +0 -168
- package/dist/lib/mcp-server.js.map +0 -1
- package/dist/lib/pkce.d.ts +0 -32
- package/dist/lib/pkce.d.ts.map +0 -1
- package/dist/lib/pkce.js +0 -101
- package/dist/lib/pkce.js.map +0 -1
- package/dist/lib/util.d.ts +0 -27
- package/dist/lib/util.d.ts.map +0 -1
- package/dist/lib/util.js +0 -46
- package/dist/lib/util.js.map +0 -1
- package/dist/package.json +0 -46
- package/test/init.integration.test.cjs +0 -269
- package/test/init.test.cjs +0 -76
package/lib/util.ts
CHANGED
|
@@ -1,3 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map of HTTP status codes to human-friendly messages.
|
|
3
|
+
*/
|
|
4
|
+
const HTTP_STATUS_MESSAGES: Record<number, string> = {
|
|
5
|
+
400: "Bad Request",
|
|
6
|
+
401: "Unauthorized - check your API key",
|
|
7
|
+
403: "Forbidden - access denied",
|
|
8
|
+
404: "Not Found",
|
|
9
|
+
408: "Request Timeout",
|
|
10
|
+
429: "Too Many Requests - rate limited",
|
|
11
|
+
500: "Internal Server Error",
|
|
12
|
+
502: "Bad Gateway - server temporarily unavailable",
|
|
13
|
+
503: "Service Unavailable - server temporarily unavailable",
|
|
14
|
+
504: "Gateway Timeout - server temporarily unavailable",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Check if a string looks like HTML content.
|
|
19
|
+
*/
|
|
20
|
+
function isHtmlContent(text: string): boolean {
|
|
21
|
+
const trimmed = text.trim();
|
|
22
|
+
return trimmed.startsWith("<!DOCTYPE") || trimmed.startsWith("<html") || trimmed.startsWith("<HTML");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Format an HTTP error response into a clean, developer-friendly message.
|
|
27
|
+
* Handles HTML error pages (e.g., from Cloudflare) by showing just the status code and message.
|
|
28
|
+
*/
|
|
29
|
+
export function formatHttpError(operation: string, status: number, responseBody?: string): string {
|
|
30
|
+
const statusMessage = HTTP_STATUS_MESSAGES[status] || "Request failed";
|
|
31
|
+
let errMsg = `${operation}: HTTP ${status} - ${statusMessage}`;
|
|
32
|
+
|
|
33
|
+
if (responseBody) {
|
|
34
|
+
// If it's HTML (like Cloudflare error pages), don't dump the raw HTML
|
|
35
|
+
if (isHtmlContent(responseBody)) {
|
|
36
|
+
// Just use the status message, don't append HTML
|
|
37
|
+
return errMsg;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Try to parse as JSON for structured error info
|
|
41
|
+
try {
|
|
42
|
+
const errObj = JSON.parse(responseBody);
|
|
43
|
+
// Extract common error message fields
|
|
44
|
+
const message = errObj.message || errObj.error || errObj.detail;
|
|
45
|
+
if (message && typeof message === "string") {
|
|
46
|
+
errMsg += `\n${message}`;
|
|
47
|
+
} else {
|
|
48
|
+
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
49
|
+
}
|
|
50
|
+
} catch {
|
|
51
|
+
// Plain text error - append it if it's short and useful
|
|
52
|
+
const trimmed = responseBody.trim();
|
|
53
|
+
if (trimmed.length > 0 && trimmed.length < 500) {
|
|
54
|
+
errMsg += `\n${trimmed}`;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return errMsg;
|
|
60
|
+
}
|
|
61
|
+
|
|
1
62
|
export function maskSecret(secret: string): string {
|
|
2
63
|
if (!secret) return "";
|
|
3
64
|
if (secret.length <= 8) return "****";
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresai",
|
|
3
|
-
"version": "0.14.0-dev.
|
|
4
|
-
"description": "postgres_ai CLI
|
|
3
|
+
"version": "0.14.0-dev.81",
|
|
4
|
+
"description": "postgres_ai CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"private": false,
|
|
7
7
|
"repository": {
|
|
@@ -13,20 +13,30 @@
|
|
|
13
13
|
"url": "https://gitlab.com/postgres-ai/postgres_ai/-/issues"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"postgres-ai": "./dist/bin/postgres-ai.js",
|
|
17
16
|
"postgresai": "./dist/bin/postgres-ai.js",
|
|
18
17
|
"pgai": "./dist/bin/postgres-ai.js"
|
|
19
18
|
},
|
|
20
|
-
"
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./dist/bin/postgres-ai.js",
|
|
21
|
+
"./cli": "./dist/bin/postgres-ai.js"
|
|
22
|
+
},
|
|
23
|
+
"type": "module",
|
|
21
24
|
"engines": {
|
|
22
25
|
"node": ">=18"
|
|
23
26
|
},
|
|
24
27
|
"scripts": {
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
28
|
+
"embed-metrics": "bun run scripts/embed-metrics.ts",
|
|
29
|
+
"embed-checkup-dictionary": "bun run scripts/embed-checkup-dictionary.ts",
|
|
30
|
+
"embed-all": "bun run embed-metrics && bun run embed-checkup-dictionary",
|
|
31
|
+
"build": "bun run embed-all && bun build ./bin/postgres-ai.ts --outdir ./dist/bin --target node && node -e \"const fs=require('fs');const f='./dist/bin/postgres-ai.js';fs.writeFileSync(f,fs.readFileSync(f,'utf8').replace('#!/usr/bin/env bun','#!/usr/bin/env node'))\" && cp -r ./sql ./dist/sql",
|
|
32
|
+
"prepublishOnly": "npm run build",
|
|
33
|
+
"start": "bun ./bin/postgres-ai.ts --help",
|
|
34
|
+
"start:node": "node ./dist/bin/postgres-ai.js --help",
|
|
35
|
+
"dev": "bun run embed-all && bun --watch ./bin/postgres-ai.ts",
|
|
36
|
+
"test": "bun run embed-all && bun test",
|
|
37
|
+
"test:fast": "bun run embed-all && bun test",
|
|
38
|
+
"test:coverage": "bun run embed-all && bun test --coverage && echo 'Coverage report: cli/coverage/lcov-report/index.html'",
|
|
39
|
+
"typecheck": "bun run embed-all && bunx tsc --noEmit"
|
|
30
40
|
},
|
|
31
41
|
"dependencies": {
|
|
32
42
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
@@ -35,9 +45,11 @@
|
|
|
35
45
|
"pg": "^8.16.3"
|
|
36
46
|
},
|
|
37
47
|
"devDependencies": {
|
|
48
|
+
"@types/bun": "^1.1.14",
|
|
38
49
|
"@types/js-yaml": "^4.0.9",
|
|
39
|
-
"@types/node": "^18.19.0",
|
|
40
50
|
"@types/pg": "^8.15.6",
|
|
51
|
+
"ajv": "^8.17.1",
|
|
52
|
+
"ajv-formats": "^3.0.1",
|
|
41
53
|
"typescript": "^5.3.3"
|
|
42
54
|
},
|
|
43
55
|
"publishConfig": {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# postgres-ai
|
|
2
|
+
|
|
3
|
+
This is a wrapper package for [postgresai](https://www.npmjs.com/package/postgresai).
|
|
4
|
+
|
|
5
|
+
## Prefer installing postgresai directly
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g postgresai
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This gives you two commands:
|
|
12
|
+
- `postgresai` — canonical, discoverable
|
|
13
|
+
- `pgai` — short and convenient
|
|
14
|
+
|
|
15
|
+
## Why this package exists
|
|
16
|
+
|
|
17
|
+
This package exists for discoverability on npm. If you search for "postgres-ai", you'll find this package which depends on and forwards to `postgresai`.
|
|
18
|
+
|
|
19
|
+
Installing this package (`npm install -g postgres-ai`) will install both packages, giving you all three command aliases:
|
|
20
|
+
- `postgres-ai` (from this package)
|
|
21
|
+
- `postgresai` (from the main package)
|
|
22
|
+
- `pgai` (from the main package)
|
|
23
|
+
|
|
24
|
+
## Documentation
|
|
25
|
+
|
|
26
|
+
See the main package for full documentation: https://www.npmjs.com/package/postgresai
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* postgres-ai wrapper - forwards all commands to postgresai CLI
|
|
4
|
+
*
|
|
5
|
+
* This package exists for discoverability. For direct installation,
|
|
6
|
+
* prefer: npm install -g postgresai
|
|
7
|
+
*/
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
|
|
10
|
+
// Find postgresai binary from the dependency
|
|
11
|
+
// Uses the "cli" export defined in postgresai's package.json
|
|
12
|
+
const postgresaiBin = require.resolve('postgresai/cli');
|
|
13
|
+
|
|
14
|
+
// Forward all arguments to postgresai
|
|
15
|
+
const child = spawn(process.execPath, [postgresaiBin, ...process.argv.slice(2)], {
|
|
16
|
+
stdio: 'inherit',
|
|
17
|
+
env: process.env,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
child.on('close', (code) => {
|
|
21
|
+
process.exit(code ?? 0);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
child.on('error', (err) => {
|
|
25
|
+
console.error(`Failed to start postgresai: ${err.message}`);
|
|
26
|
+
process.exit(1);
|
|
27
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "postgres-ai",
|
|
3
|
+
"version": "0.0.0-dev.0",
|
|
4
|
+
"description": "PostgresAI CLI (wrapper package - prefer installing postgresai directly)",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"private": false,
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://gitlab.com/postgres-ai/postgres_ai.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://gitlab.com/postgres-ai/postgres_ai",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://gitlab.com/postgres-ai/postgres_ai/-/issues"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"postgres-ai": "./bin/postgres-ai.js"
|
|
17
|
+
},
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"postgresai": ">=0.12.0"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Build script to fetch checkup dictionary from API and embed it.
|
|
4
|
+
*
|
|
5
|
+
* This script fetches from https://postgres.ai/api/general/checkup_dictionary
|
|
6
|
+
* and generates cli/lib/checkup-dictionary-embedded.ts with the data embedded.
|
|
7
|
+
*
|
|
8
|
+
* The generated file is NOT committed to git - it's regenerated at build time.
|
|
9
|
+
*
|
|
10
|
+
* Usage: bun run scripts/embed-checkup-dictionary.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
|
|
16
|
+
// API endpoint - always available without auth
|
|
17
|
+
const DICTIONARY_URL = "https://postgres.ai/api/general/checkup_dictionary";
|
|
18
|
+
|
|
19
|
+
// Output path relative to cli/ directory
|
|
20
|
+
const CLI_DIR = path.resolve(__dirname, "..");
|
|
21
|
+
const OUTPUT_PATH = path.resolve(CLI_DIR, "lib/checkup-dictionary-embedded.ts");
|
|
22
|
+
|
|
23
|
+
// Request timeout (10 seconds)
|
|
24
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
25
|
+
|
|
26
|
+
interface CheckupDictionaryEntry {
|
|
27
|
+
code: string;
|
|
28
|
+
title: string;
|
|
29
|
+
description: string;
|
|
30
|
+
category: string;
|
|
31
|
+
sort_order: number | null;
|
|
32
|
+
is_system_report: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function generateTypeScript(data: CheckupDictionaryEntry[], sourceUrl: string): string {
|
|
36
|
+
const lines: string[] = [
|
|
37
|
+
"// AUTO-GENERATED FILE - DO NOT EDIT",
|
|
38
|
+
`// Generated from: ${sourceUrl}`,
|
|
39
|
+
`// Generated at: ${new Date().toISOString()}`,
|
|
40
|
+
"// To regenerate: bun run embed-checkup-dictionary",
|
|
41
|
+
"",
|
|
42
|
+
'import { CheckupDictionaryEntry } from "./checkup-dictionary";',
|
|
43
|
+
"",
|
|
44
|
+
"/**",
|
|
45
|
+
" * Embedded checkup dictionary data fetched from API at build time.",
|
|
46
|
+
" * Contains all available checkup report codes, titles, and metadata.",
|
|
47
|
+
" */",
|
|
48
|
+
`export const CHECKUP_DICTIONARY_DATA: CheckupDictionaryEntry[] = ${JSON.stringify(data, null, 2)};`,
|
|
49
|
+
"",
|
|
50
|
+
];
|
|
51
|
+
return lines.join("\n");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function fetchWithTimeout(url: string, timeoutMs: number): Promise<Response> {
|
|
55
|
+
const controller = new AbortController();
|
|
56
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const response = await fetch(url, { signal: controller.signal });
|
|
60
|
+
return response;
|
|
61
|
+
} finally {
|
|
62
|
+
clearTimeout(timeoutId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function main() {
|
|
67
|
+
console.log(`Fetching checkup dictionary from: ${DICTIONARY_URL}`);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetchWithTimeout(DICTIONARY_URL, FETCH_TIMEOUT_MS);
|
|
71
|
+
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const data: CheckupDictionaryEntry[] = await response.json();
|
|
77
|
+
|
|
78
|
+
if (!Array.isArray(data)) {
|
|
79
|
+
throw new Error("Expected array response from API");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Validate entries have required fields
|
|
83
|
+
for (const entry of data) {
|
|
84
|
+
if (!entry.code || !entry.title) {
|
|
85
|
+
throw new Error(`Invalid entry missing code or title: ${JSON.stringify(entry)}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const tsCode = generateTypeScript(data, DICTIONARY_URL);
|
|
90
|
+
fs.writeFileSync(OUTPUT_PATH, tsCode, "utf8");
|
|
91
|
+
|
|
92
|
+
console.log(`Generated: ${OUTPUT_PATH}`);
|
|
93
|
+
console.log(`Dictionary contains ${data.length} entries`);
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
console.warn(`Warning: Failed to fetch checkup dictionary: ${errorMsg}`);
|
|
97
|
+
console.warn("Generating empty dictionary as fallback");
|
|
98
|
+
|
|
99
|
+
// Generate empty dictionary to allow build to proceed
|
|
100
|
+
const fallbackTs = generateTypeScript([], `N/A (fetch failed: ${errorMsg})`);
|
|
101
|
+
fs.writeFileSync(OUTPUT_PATH, fallbackTs, "utf8");
|
|
102
|
+
console.log(`Generated fallback dictionary at ${OUTPUT_PATH}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
main();
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Build script to embed metrics.yml into the CLI bundle.
|
|
4
|
+
*
|
|
5
|
+
* This script reads config/pgwatch-prometheus/metrics.yml and generates
|
|
6
|
+
* cli/lib/metrics-embedded.ts with the metrics data embedded as TypeScript.
|
|
7
|
+
*
|
|
8
|
+
* The generated file is NOT committed to git - it's regenerated at build time.
|
|
9
|
+
*
|
|
10
|
+
* Usage: bun run scripts/embed-metrics.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "fs";
|
|
14
|
+
import * as path from "path";
|
|
15
|
+
import * as yaml from "js-yaml";
|
|
16
|
+
|
|
17
|
+
// Resolve paths relative to cli/ directory
|
|
18
|
+
const CLI_DIR = path.resolve(__dirname, "..");
|
|
19
|
+
const METRICS_YML_PATH = path.resolve(CLI_DIR, "../config/pgwatch-prometheus/metrics.yml");
|
|
20
|
+
const OUTPUT_PATH = path.resolve(CLI_DIR, "lib/metrics-embedded.ts");
|
|
21
|
+
|
|
22
|
+
interface MetricDefinition {
|
|
23
|
+
description?: string;
|
|
24
|
+
// YAML parses numeric keys (e.g., 11:, 14:) as numbers, representing PG major versions
|
|
25
|
+
sqls: Record<number, string>;
|
|
26
|
+
gauges?: string[];
|
|
27
|
+
statement_timeout_seconds?: number;
|
|
28
|
+
is_instance_level?: boolean;
|
|
29
|
+
node_status?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MetricsYml {
|
|
33
|
+
metrics: Record<string, MetricDefinition>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Metrics needed for express mode reports
|
|
37
|
+
const REQUIRED_METRICS = [
|
|
38
|
+
// Settings and version (A002, A003, A007, A013)
|
|
39
|
+
"settings",
|
|
40
|
+
// Database stats (A004)
|
|
41
|
+
"db_stats",
|
|
42
|
+
"db_size",
|
|
43
|
+
// Index health (H001, H002, H004)
|
|
44
|
+
"pg_invalid_indexes",
|
|
45
|
+
"unused_indexes",
|
|
46
|
+
"redundant_indexes",
|
|
47
|
+
// Stats reset info (H002)
|
|
48
|
+
"stats_reset",
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
function main() {
|
|
52
|
+
console.log(`Reading metrics from: ${METRICS_YML_PATH}`);
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(METRICS_YML_PATH)) {
|
|
55
|
+
console.error(`ERROR: metrics.yml not found at ${METRICS_YML_PATH}`);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const yamlContent = fs.readFileSync(METRICS_YML_PATH, "utf8");
|
|
60
|
+
const parsed = yaml.load(yamlContent) as MetricsYml;
|
|
61
|
+
|
|
62
|
+
if (!parsed.metrics) {
|
|
63
|
+
console.error("ERROR: No 'metrics' section found in metrics.yml");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Extract only required metrics
|
|
68
|
+
const extractedMetrics: Record<string, MetricDefinition> = {};
|
|
69
|
+
const missingMetrics: string[] = [];
|
|
70
|
+
|
|
71
|
+
for (const metricName of REQUIRED_METRICS) {
|
|
72
|
+
if (parsed.metrics[metricName]) {
|
|
73
|
+
extractedMetrics[metricName] = parsed.metrics[metricName];
|
|
74
|
+
} else {
|
|
75
|
+
missingMetrics.push(metricName);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (missingMetrics.length > 0) {
|
|
80
|
+
console.error(`ERROR: Missing required metrics: ${missingMetrics.join(", ")}`);
|
|
81
|
+
process.exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Generate TypeScript code
|
|
85
|
+
const tsCode = generateTypeScript(extractedMetrics);
|
|
86
|
+
|
|
87
|
+
// Write output
|
|
88
|
+
fs.writeFileSync(OUTPUT_PATH, tsCode, "utf8");
|
|
89
|
+
console.log(`Generated: ${OUTPUT_PATH}`);
|
|
90
|
+
console.log(`Embedded ${Object.keys(extractedMetrics).length} metrics`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function generateTypeScript(metrics: Record<string, MetricDefinition>): string {
|
|
94
|
+
const lines: string[] = [
|
|
95
|
+
"// AUTO-GENERATED FILE - DO NOT EDIT",
|
|
96
|
+
"// Generated from config/pgwatch-prometheus/metrics.yml by scripts/embed-metrics.ts",
|
|
97
|
+
`// Generated at: ${new Date().toISOString()}`,
|
|
98
|
+
"",
|
|
99
|
+
"/**",
|
|
100
|
+
" * Metric definition from metrics.yml",
|
|
101
|
+
" */",
|
|
102
|
+
"export interface MetricDefinition {",
|
|
103
|
+
" description?: string;",
|
|
104
|
+
" sqls: Record<number, string>; // PG major version -> SQL query",
|
|
105
|
+
" gauges?: string[];",
|
|
106
|
+
" statement_timeout_seconds?: number;",
|
|
107
|
+
"}",
|
|
108
|
+
"",
|
|
109
|
+
"/**",
|
|
110
|
+
" * Embedded metrics for express mode reports.",
|
|
111
|
+
" * Only includes metrics required for CLI checkup reports.",
|
|
112
|
+
" */",
|
|
113
|
+
"export const METRICS: Record<string, MetricDefinition> = {",
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
for (const [name, metric] of Object.entries(metrics)) {
|
|
117
|
+
lines.push(` ${JSON.stringify(name)}: {`);
|
|
118
|
+
|
|
119
|
+
if (metric.description) {
|
|
120
|
+
// Escape description for TypeScript string
|
|
121
|
+
const desc = metric.description.trim().replace(/\n/g, " ").replace(/\s+/g, " ");
|
|
122
|
+
lines.push(` description: ${JSON.stringify(desc)},`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// sqls keys are PG major versions (numbers in YAML, but Object.entries returns strings)
|
|
126
|
+
lines.push(" sqls: {");
|
|
127
|
+
for (const [versionKey, sql] of Object.entries(metric.sqls)) {
|
|
128
|
+
// YAML numeric keys may be parsed as numbers or strings depending on context;
|
|
129
|
+
// explicitly convert to ensure consistent numeric keys in output
|
|
130
|
+
const versionNum = typeof versionKey === "number" ? versionKey : parseInt(versionKey, 10);
|
|
131
|
+
// Use JSON.stringify for robust escaping of all special characters
|
|
132
|
+
lines.push(` ${versionNum}: ${JSON.stringify(sql)},`);
|
|
133
|
+
}
|
|
134
|
+
lines.push(" },");
|
|
135
|
+
|
|
136
|
+
if (metric.gauges) {
|
|
137
|
+
lines.push(` gauges: ${JSON.stringify(metric.gauges)},`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (metric.statement_timeout_seconds !== undefined) {
|
|
141
|
+
lines.push(` statement_timeout_seconds: ${metric.statement_timeout_seconds},`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
lines.push(" },");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
lines.push("};");
|
|
148
|
+
lines.push("");
|
|
149
|
+
|
|
150
|
+
return lines.join("\n");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
main();
|
|
154
|
+
|
package/sql/01.role.sql
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Role creation / password update (template-filled by cli/lib/init.ts)
|
|
2
|
+
--
|
|
3
|
+
-- Always uses a race-safe pattern (create if missing, then always alter to set the password):
|
|
4
|
+
-- do $$ begin
|
|
5
|
+
-- if not exists (select 1 from pg_catalog.pg_roles where rolname = '...') then
|
|
6
|
+
-- begin
|
|
7
|
+
-- create user "..." with password '...';
|
|
8
|
+
-- exception when duplicate_object then
|
|
9
|
+
-- null;
|
|
10
|
+
-- end;
|
|
11
|
+
-- end if;
|
|
12
|
+
-- alter user "..." with password '...';
|
|
13
|
+
-- end $$;
|
|
14
|
+
{{ROLE_STMT}}
|
|
15
|
+
|
|
16
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Extensions required for postgres_ai monitoring
|
|
2
|
+
|
|
3
|
+
-- Enable pg_stat_statements for query performance monitoring
|
|
4
|
+
-- Note: Uses IF NOT EXISTS because extension may already be installed.
|
|
5
|
+
-- We do NOT drop this extension in unprepare-db since it may have been pre-existing.
|
|
6
|
+
create extension if not exists pg_stat_statements;
|
|
7
|
+
|
|
8
|
+
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
-- Required permissions for postgres_ai monitoring user (template-filled by cli/lib/init.ts)
|
|
2
|
+
|
|
3
|
+
-- Allow connect
|
|
4
|
+
grant connect on database {{DB_IDENT}} to {{ROLE_IDENT}};
|
|
5
|
+
|
|
6
|
+
-- Standard monitoring privileges
|
|
7
|
+
grant pg_monitor to {{ROLE_IDENT}};
|
|
8
|
+
grant select on pg_catalog.pg_index to {{ROLE_IDENT}};
|
|
9
|
+
|
|
10
|
+
-- Create postgres_ai schema for our objects
|
|
11
|
+
-- Using IF NOT EXISTS for idempotency - prepare-db can be run multiple times
|
|
12
|
+
create schema if not exists postgres_ai;
|
|
13
|
+
grant usage on schema postgres_ai to {{ROLE_IDENT}};
|
|
14
|
+
|
|
15
|
+
-- For bloat analysis: expose pg_statistic via a view
|
|
16
|
+
create or replace view postgres_ai.pg_statistic as
|
|
17
|
+
select
|
|
18
|
+
n.nspname as schemaname,
|
|
19
|
+
c.relname as tablename,
|
|
20
|
+
a.attname,
|
|
21
|
+
s.stanullfrac as null_frac,
|
|
22
|
+
s.stawidth as avg_width,
|
|
23
|
+
false as inherited
|
|
24
|
+
from pg_catalog.pg_statistic s
|
|
25
|
+
join pg_catalog.pg_class c on c.oid = s.starelid
|
|
26
|
+
join pg_catalog.pg_namespace n on n.oid = c.relnamespace
|
|
27
|
+
join pg_catalog.pg_attribute a on a.attrelid = s.starelid and a.attnum = s.staattnum
|
|
28
|
+
where a.attnum > 0 and not a.attisdropped;
|
|
29
|
+
|
|
30
|
+
grant select on postgres_ai.pg_statistic to {{ROLE_IDENT}};
|
|
31
|
+
|
|
32
|
+
-- Hardened clusters sometimes revoke PUBLIC on schema public
|
|
33
|
+
grant usage on schema public to {{ROLE_IDENT}};
|
|
34
|
+
|
|
35
|
+
-- Keep search_path predictable; postgres_ai first so our objects are found
|
|
36
|
+
alter user {{ROLE_IDENT}} set search_path = postgres_ai, "$user", public, pg_catalog;
|
|
37
|
+
|
|
38
|
+
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Optional permissions for self-managed Postgres (best effort)
|
|
2
|
+
|
|
3
|
+
grant execute on function pg_catalog.pg_stat_file(text) to {{ROLE_IDENT}};
|
|
4
|
+
grant execute on function pg_catalog.pg_stat_file(text, boolean) to {{ROLE_IDENT}};
|
|
5
|
+
grant execute on function pg_catalog.pg_ls_dir(text) to {{ROLE_IDENT}};
|
|
6
|
+
grant execute on function pg_catalog.pg_ls_dir(text, boolean, boolean) to {{ROLE_IDENT}};
|
|
7
|
+
|
|
8
|
+
|