postgresai 0.16.0-rc.4 → 0.16.0
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 +154 -0
- package/dist/bin/postgres-ai.js +2911 -255
- package/package.json +12 -3
- package/schemas/A002.schema.json +63 -0
- package/schemas/A003.schema.json +73 -0
- package/schemas/A004.schema.json +81 -0
- package/schemas/A007.schema.json +71 -0
- package/schemas/A013.schema.json +61 -0
- package/schemas/D001.schema.json +71 -0
- package/schemas/D004.schema.json +136 -0
- package/schemas/F001.schema.json +73 -0
- package/schemas/F002.schema.json +108 -0
- package/schemas/F003.schema.json +138 -0
- package/schemas/F004.schema.json +125 -0
- package/schemas/F005.schema.json +131 -0
- package/schemas/F009.schema.json +155 -0
- package/schemas/G001.schema.json +135 -0
- package/schemas/G003.schema.json +90 -0
- package/schemas/H001.schema.json +141 -0
- package/schemas/H002.schema.json +129 -0
- package/schemas/H004.schema.json +128 -0
- package/schemas/I001.schema.json +149 -0
- package/schemas/K001.schema.json +161 -0
- package/schemas/K003.schema.json +163 -0
- package/schemas/K004.schema.json +110 -0
- package/schemas/K005.schema.json +110 -0
- package/schemas/K006.schema.json +110 -0
- package/schemas/K007.schema.json +110 -0
- package/schemas/K008.schema.json +110 -0
- package/schemas/M001.schema.json +119 -0
- package/schemas/M002.schema.json +110 -0
- package/schemas/M003.schema.json +128 -0
- package/schemas/N001.schema.json +161 -0
- package/schemas/query.schema.json +62 -0
- package/CHANGELOG.md +0 -11
- package/bin/postgres-ai.ts +0 -5578
- package/bun.lock +0 -258
- package/bunfig.toml +0 -20
- package/lib/aas-onboard.ts +0 -251
- package/lib/auth-server.ts +0 -285
- package/lib/checkup-api.ts +0 -526
- package/lib/checkup-dictionary.ts +0 -103
- package/lib/checkup-summary.ts +0 -338
- package/lib/checkup.ts +0 -2261
- package/lib/config.ts +0 -171
- package/lib/init.ts +0 -1152
- package/lib/instances.ts +0 -245
- package/lib/issues.ts +0 -1060
- package/lib/mcp-server.ts +0 -667
- package/lib/metrics-loader.ts +0 -134
- package/lib/pkce.ts +0 -79
- package/lib/reports.ts +0 -373
- package/lib/storage.ts +0 -367
- package/lib/supabase.ts +0 -826
- package/lib/util.ts +0 -134
- package/packages/postgres-ai/README.md +0 -26
- package/packages/postgres-ai/bin/postgres-ai.js +0 -27
- package/packages/postgres-ai/package.json +0 -27
- package/scripts/embed-checkup-dictionary.ts +0 -115
- package/scripts/embed-metrics.ts +0 -160
- package/scripts/generate-release-notes.ts +0 -668
- package/test/PERMISSION_CHECK_TEST_SUMMARY.md +0 -139
- package/test/aas-onboard.test.ts +0 -301
- package/test/auth.test.ts +0 -287
- package/test/checkup.integration.test.ts +0 -413
- package/test/checkup.test.ts +0 -3626
- package/test/compose-cmd.test.ts +0 -120
- package/test/config-consistency.test.ts +0 -352
- package/test/init.integration.test.ts +0 -438
- package/test/init.test.ts +0 -1816
- package/test/issues.cli.test.ts +0 -1162
- package/test/issues.test.ts +0 -456
- package/test/mcp-server.test.ts +0 -2530
- package/test/monitoring.test.ts +0 -746
- package/test/permission-check-sql.test.ts +0 -116
- package/test/reports.cli.test.ts +0 -793
- package/test/reports.test.ts +0 -977
- package/test/schema-validation.test.ts +0 -231
- package/test/storage.test.ts +0 -935
- package/test/supabase.test.ts +0 -709
- package/test/targets-add-config.test.ts +0 -28
- package/test/test-utils.ts +0 -190
- package/test/upgrade.test.ts +0 -1056
- package/test/util.test.ts +0 -44
- package/tsconfig.json +0 -20
package/lib/metrics-loader.ts
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Metrics loader for express checkup reports
|
|
3
|
-
*
|
|
4
|
-
* Loads SQL queries from embedded metrics data (generated from metrics.yml at build time).
|
|
5
|
-
* Provides version-aware query selection and row transformation utilities.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { METRICS, MetricDefinition } from "./metrics-embedded";
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Get SQL query for a specific metric, selecting the appropriate version.
|
|
12
|
-
*
|
|
13
|
-
* @param metricName - Name of the metric (e.g., "settings", "db_stats")
|
|
14
|
-
* @param pgMajorVersion - PostgreSQL major version (default: 16)
|
|
15
|
-
* @returns SQL query string
|
|
16
|
-
* @throws Error if metric not found or no compatible version available
|
|
17
|
-
*/
|
|
18
|
-
export function getMetricSql(metricName: string, pgMajorVersion: number = 16): string {
|
|
19
|
-
const metric = METRICS[metricName];
|
|
20
|
-
|
|
21
|
-
if (!metric) {
|
|
22
|
-
throw new Error(`Metric "${metricName}" not found. Available metrics: ${Object.keys(METRICS).join(", ")}`);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// Find the best matching version: highest version <= pgMajorVersion
|
|
26
|
-
const availableVersions = Object.keys(metric.sqls)
|
|
27
|
-
.map(v => parseInt(v, 10))
|
|
28
|
-
.sort((a, b) => b - a); // Sort descending
|
|
29
|
-
|
|
30
|
-
const matchingVersion = availableVersions.find(v => v <= pgMajorVersion);
|
|
31
|
-
|
|
32
|
-
if (matchingVersion === undefined) {
|
|
33
|
-
throw new Error(
|
|
34
|
-
`No compatible SQL version for metric "${metricName}" with PostgreSQL ${pgMajorVersion}. ` +
|
|
35
|
-
`Available versions: ${availableVersions.join(", ")}`
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
return metric.sqls[matchingVersion];
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get metric definition including all metadata.
|
|
44
|
-
*
|
|
45
|
-
* @param metricName - Name of the metric
|
|
46
|
-
* @returns MetricDefinition or undefined if not found
|
|
47
|
-
*/
|
|
48
|
-
export function getMetricDefinition(metricName: string): MetricDefinition | undefined {
|
|
49
|
-
return METRICS[metricName];
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* List all available metric names.
|
|
54
|
-
*/
|
|
55
|
-
export function listMetricNames(): string[] {
|
|
56
|
-
return Object.keys(METRICS);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Metric names that correspond to express report checks.
|
|
61
|
-
* Maps check IDs and logical names to metric names in the METRICS object.
|
|
62
|
-
*/
|
|
63
|
-
export const METRIC_NAMES = {
|
|
64
|
-
// Index health checks
|
|
65
|
-
H001: "pg_invalid_indexes",
|
|
66
|
-
H002: "unused_indexes",
|
|
67
|
-
H004: "redundant_indexes",
|
|
68
|
-
// Dead tuples and per-table autovacuum overrides
|
|
69
|
-
F003: "pg_dead_tuples",
|
|
70
|
-
// Bloat estimation
|
|
71
|
-
F004: "pg_table_bloat",
|
|
72
|
-
F005: "pg_btree_bloat",
|
|
73
|
-
// Settings and version info (A002, A003, A007, A013)
|
|
74
|
-
settings: "settings",
|
|
75
|
-
// Database statistics (A004)
|
|
76
|
-
dbStats: "db_stats",
|
|
77
|
-
dbSize: "db_size",
|
|
78
|
-
// Stats reset info (H002)
|
|
79
|
-
statsReset: "stats_reset",
|
|
80
|
-
// I/O statistics (I001) - PostgreSQL 16+
|
|
81
|
-
I001: "pg_stat_io",
|
|
82
|
-
} as const;
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Transform a row from metrics query output to JSON report format.
|
|
86
|
-
* Metrics use `tag_` prefix for dimensions; we strip it for JSON reports.
|
|
87
|
-
* Also removes Prometheus-specific fields like epoch_ns, num, tag_datname.
|
|
88
|
-
*/
|
|
89
|
-
export function transformMetricRow(row: Record<string, unknown>): Record<string, unknown> {
|
|
90
|
-
const result: Record<string, unknown> = {};
|
|
91
|
-
|
|
92
|
-
for (const [key, value] of Object.entries(row)) {
|
|
93
|
-
// Skip Prometheus-specific fields
|
|
94
|
-
if (key === "epoch_ns" || key === "num" || key === "tag_datname") {
|
|
95
|
-
continue;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Strip tag_ prefix
|
|
99
|
-
const newKey = key.startsWith("tag_") ? key.slice(4) : key;
|
|
100
|
-
result[newKey] = value;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return result;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Transform settings metric row to the format expected by express reports.
|
|
108
|
-
* The settings metric returns one row per setting with tag_setting_name as key.
|
|
109
|
-
*/
|
|
110
|
-
export function transformSettingsRow(row: Record<string, unknown>): {
|
|
111
|
-
name: string;
|
|
112
|
-
setting: string;
|
|
113
|
-
unit: string;
|
|
114
|
-
category: string;
|
|
115
|
-
vartype: string;
|
|
116
|
-
is_default: boolean;
|
|
117
|
-
} {
|
|
118
|
-
return {
|
|
119
|
-
name: String(row.tag_setting_name || ""),
|
|
120
|
-
setting: String(row.tag_setting_value || ""),
|
|
121
|
-
unit: String(row.tag_unit || ""),
|
|
122
|
-
category: String(row.tag_category || ""),
|
|
123
|
-
vartype: String(row.tag_vartype || ""),
|
|
124
|
-
is_default: row.is_default === 1 || row.is_default === true,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// Re-export types for convenience
|
|
129
|
-
export type { MetricDefinition } from "./metrics-embedded";
|
|
130
|
-
|
|
131
|
-
// Legacy export for backward compatibility
|
|
132
|
-
export function loadMetricsYml(): { metrics: Record<string, unknown> } {
|
|
133
|
-
return { metrics: METRICS };
|
|
134
|
-
}
|
package/lib/pkce.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import * as crypto from "crypto";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* PKCE parameters for OAuth 2.0 Authorization Code Flow with PKCE
|
|
5
|
-
*/
|
|
6
|
-
export interface PKCEParams {
|
|
7
|
-
codeVerifier: string;
|
|
8
|
-
codeChallenge: string;
|
|
9
|
-
codeChallengeMethod: "S256";
|
|
10
|
-
state: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Generate a cryptographically random string for PKCE
|
|
15
|
-
* @param length - Length of the string (43-128 characters per RFC 7636)
|
|
16
|
-
* @returns Base64URL-encoded random string
|
|
17
|
-
*/
|
|
18
|
-
function generateRandomString(length: number = 64): string {
|
|
19
|
-
const bytes = crypto.randomBytes(length);
|
|
20
|
-
return base64URLEncode(bytes);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Base64URL encode (without padding)
|
|
25
|
-
* @param buffer - Buffer to encode
|
|
26
|
-
* @returns Base64URL-encoded string
|
|
27
|
-
*/
|
|
28
|
-
function base64URLEncode(buffer: Buffer): string {
|
|
29
|
-
return buffer
|
|
30
|
-
.toString("base64")
|
|
31
|
-
.replace(/\+/g, "-")
|
|
32
|
-
.replace(/\//g, "_")
|
|
33
|
-
.replace(/=/g, "");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Generate PKCE code verifier
|
|
38
|
-
* @returns Random code verifier (43-128 characters)
|
|
39
|
-
*/
|
|
40
|
-
export function generateCodeVerifier(): string {
|
|
41
|
-
return generateRandomString(32); // 32 bytes = 43 chars after base64url encoding
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Generate PKCE code challenge from verifier
|
|
46
|
-
* Uses S256 method (SHA256)
|
|
47
|
-
* @param verifier - Code verifier string
|
|
48
|
-
* @returns Base64URL-encoded SHA256 hash of verifier
|
|
49
|
-
*/
|
|
50
|
-
export function generateCodeChallenge(verifier: string): string {
|
|
51
|
-
const hash = crypto.createHash("sha256").update(verifier).digest();
|
|
52
|
-
return base64URLEncode(hash);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Generate random state for CSRF protection
|
|
57
|
-
* @returns Random state string
|
|
58
|
-
*/
|
|
59
|
-
export function generateState(): string {
|
|
60
|
-
return generateRandomString(16); // 16 bytes = 22 chars
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Generate complete PKCE parameters
|
|
65
|
-
* @returns Object with verifier, challenge, challengeMethod, and state
|
|
66
|
-
*/
|
|
67
|
-
export function generatePKCEParams(): PKCEParams {
|
|
68
|
-
const verifier = generateCodeVerifier();
|
|
69
|
-
const challenge = generateCodeChallenge(verifier);
|
|
70
|
-
const state = generateState();
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
codeVerifier: verifier,
|
|
74
|
-
codeChallenge: challenge,
|
|
75
|
-
codeChallengeMethod: "S256",
|
|
76
|
-
state: state,
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
package/lib/reports.ts
DELETED
|
@@ -1,373 +0,0 @@
|
|
|
1
|
-
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
|
|
2
|
-
|
|
3
|
-
// ============================================================================
|
|
4
|
-
// Types
|
|
5
|
-
// ============================================================================
|
|
6
|
-
|
|
7
|
-
export interface CheckupReport {
|
|
8
|
-
id: number;
|
|
9
|
-
org_id: number;
|
|
10
|
-
org_name: string;
|
|
11
|
-
project_id: number;
|
|
12
|
-
project_name: string;
|
|
13
|
-
created_at: string;
|
|
14
|
-
created_formatted: string;
|
|
15
|
-
epoch: number;
|
|
16
|
-
status: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export interface CheckupReportFile {
|
|
20
|
-
id: number;
|
|
21
|
-
checkup_report_id: number;
|
|
22
|
-
filename: string;
|
|
23
|
-
check_id: string;
|
|
24
|
-
type: "json" | "md";
|
|
25
|
-
created_at: string;
|
|
26
|
-
created_formatted: string;
|
|
27
|
-
project_id: number;
|
|
28
|
-
project_name: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface CheckupReportFileData extends CheckupReportFile {
|
|
32
|
-
data: string;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ============================================================================
|
|
36
|
-
// Date parsing
|
|
37
|
-
// ============================================================================
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Parse a date string in various formats into an ISO 8601 string.
|
|
41
|
-
* Supported formats:
|
|
42
|
-
* YYYY-MM-DD 2025-01-15
|
|
43
|
-
* YYYY-MM-DDTHH:mm:ss 2025-01-15T10:30:00
|
|
44
|
-
* YYYY-MM-DD HH:mm:ss 2025-01-15 10:30:00
|
|
45
|
-
* YYYY-MM-DD HH:mm 2025-01-15 10:30
|
|
46
|
-
* DD.MM.YYYY 15.01.2025
|
|
47
|
-
* DD.MM.YYYY HH:mm 15.01.2025 10:30
|
|
48
|
-
* DD.MM.YYYY HH:mm:ss 15.01.2025 10:30:00
|
|
49
|
-
*/
|
|
50
|
-
export function parseFlexibleDate(input: string): string {
|
|
51
|
-
const s = input.trim();
|
|
52
|
-
|
|
53
|
-
// DD.MM.YYYY [HH:mm[:ss]]
|
|
54
|
-
const dotMatch = s.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})(?:\s+(\d{1,2}):(\d{2})(?::(\d{2}))?)?$/);
|
|
55
|
-
if (dotMatch) {
|
|
56
|
-
const [, dd, mm, yyyy, hh, min, ss] = dotMatch;
|
|
57
|
-
const iso = `${yyyy}-${mm.padStart(2, "0")}-${dd.padStart(2, "0")}T${(hh ?? "00").padStart(2, "0")}:${(min ?? "00").padStart(2, "0")}:${(ss ?? "00").padStart(2, "0")}Z`;
|
|
58
|
-
const d = new Date(iso);
|
|
59
|
-
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
|
|
60
|
-
return d.toISOString();
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// YYYY-MM-DD[T ]HH:mm[:ss] or YYYY-MM-DD
|
|
64
|
-
const isoMatch = s.match(/^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2}))?)?$/);
|
|
65
|
-
if (isoMatch) {
|
|
66
|
-
const [, yyyy, mm, dd, hh, min, ss] = isoMatch;
|
|
67
|
-
const iso = `${yyyy}-${mm}-${dd}T${hh ?? "00"}:${min ?? "00"}:${ss ?? "00"}Z`;
|
|
68
|
-
const d = new Date(iso);
|
|
69
|
-
if (isNaN(d.getTime())) throw new Error(`Invalid date: ${input}`);
|
|
70
|
-
return d.toISOString();
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
throw new Error(`Unrecognized date format: ${input}. Use YYYY-MM-DD or DD.MM.YYYY`);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// ============================================================================
|
|
77
|
-
// Params
|
|
78
|
-
// ============================================================================
|
|
79
|
-
|
|
80
|
-
export interface FetchReportsParams {
|
|
81
|
-
apiKey: string;
|
|
82
|
-
apiBaseUrl: string;
|
|
83
|
-
projectId?: number;
|
|
84
|
-
status?: string;
|
|
85
|
-
limit?: number;
|
|
86
|
-
beforeDate?: string;
|
|
87
|
-
/** @internal Used by fetchAllReports for keyset pagination */
|
|
88
|
-
beforeId?: number;
|
|
89
|
-
debug?: boolean;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export interface FetchReportFilesParams {
|
|
93
|
-
apiKey: string;
|
|
94
|
-
apiBaseUrl: string;
|
|
95
|
-
reportId?: number;
|
|
96
|
-
type?: "json" | "md";
|
|
97
|
-
checkId?: string;
|
|
98
|
-
debug?: boolean;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export interface FetchReportFileDataParams {
|
|
102
|
-
apiKey: string;
|
|
103
|
-
apiBaseUrl: string;
|
|
104
|
-
reportId?: number;
|
|
105
|
-
type?: "json" | "md";
|
|
106
|
-
checkId?: string;
|
|
107
|
-
debug?: boolean;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
// ============================================================================
|
|
111
|
-
// API functions
|
|
112
|
-
// ============================================================================
|
|
113
|
-
|
|
114
|
-
export async function fetchReports(params: FetchReportsParams): Promise<CheckupReport[]> {
|
|
115
|
-
const { apiKey, apiBaseUrl, projectId, status, limit = 20, beforeDate, beforeId, debug } = params;
|
|
116
|
-
if (!apiKey) {
|
|
117
|
-
throw new Error("API key is required");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const base = normalizeBaseUrl(apiBaseUrl);
|
|
121
|
-
const url = new URL(`${base}/checkup_reports`);
|
|
122
|
-
url.searchParams.set("order", "id.desc");
|
|
123
|
-
url.searchParams.set("limit", String(limit));
|
|
124
|
-
if (typeof projectId === "number") {
|
|
125
|
-
url.searchParams.set("project_id", `eq.${projectId}`);
|
|
126
|
-
}
|
|
127
|
-
if (status) {
|
|
128
|
-
url.searchParams.set("status", `eq.${status}`);
|
|
129
|
-
}
|
|
130
|
-
if (beforeDate) {
|
|
131
|
-
url.searchParams.set("created_at", `lt.${beforeDate}`);
|
|
132
|
-
}
|
|
133
|
-
if (typeof beforeId === "number") {
|
|
134
|
-
url.searchParams.set("id", `lt.${beforeId}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const headers: Record<string, string> = {
|
|
138
|
-
"access-token": apiKey,
|
|
139
|
-
"Prefer": "return=representation",
|
|
140
|
-
"Content-Type": "application/json",
|
|
141
|
-
"Connection": "close",
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
if (debug) {
|
|
145
|
-
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
146
|
-
console.error(`Debug: Resolved API base URL: ${base}`);
|
|
147
|
-
console.error(`Debug: GET URL: ${url.toString()}`);
|
|
148
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const response = await fetch(url.toString(), { method: "GET", headers });
|
|
152
|
-
|
|
153
|
-
if (debug) {
|
|
154
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const data = await response.text();
|
|
158
|
-
|
|
159
|
-
if (response.ok) {
|
|
160
|
-
try {
|
|
161
|
-
return JSON.parse(data) as CheckupReport[];
|
|
162
|
-
} catch {
|
|
163
|
-
throw new Error(`Failed to parse reports response: ${data}`);
|
|
164
|
-
}
|
|
165
|
-
} else {
|
|
166
|
-
throw new Error(formatHttpError("Failed to fetch reports", response.status, data));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const MAX_ALL_REPORTS = 10000;
|
|
171
|
-
|
|
172
|
-
export async function fetchAllReports(params: Omit<FetchReportsParams, "beforeId" | "beforeDate">): Promise<CheckupReport[]> {
|
|
173
|
-
const pageSize = params.limit ?? 100;
|
|
174
|
-
const all: CheckupReport[] = [];
|
|
175
|
-
let beforeId: number | undefined;
|
|
176
|
-
|
|
177
|
-
while (true) {
|
|
178
|
-
const page = await fetchReports({ ...params, limit: pageSize, beforeId });
|
|
179
|
-
if (page.length === 0) break;
|
|
180
|
-
all.push(...page);
|
|
181
|
-
if (all.length >= MAX_ALL_REPORTS) {
|
|
182
|
-
console.warn(`Warning: reached maximum of ${MAX_ALL_REPORTS} reports, stopping pagination`);
|
|
183
|
-
break;
|
|
184
|
-
}
|
|
185
|
-
beforeId = page[page.length - 1].id;
|
|
186
|
-
if (page.length < pageSize) break;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return all;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export async function fetchReportFiles(params: FetchReportFilesParams): Promise<CheckupReportFile[]> {
|
|
193
|
-
const { apiKey, apiBaseUrl, reportId, type, checkId, debug } = params;
|
|
194
|
-
if (!apiKey) {
|
|
195
|
-
throw new Error("API key is required");
|
|
196
|
-
}
|
|
197
|
-
if (reportId === undefined && !checkId) {
|
|
198
|
-
throw new Error("Either reportId or checkId is required");
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const base = normalizeBaseUrl(apiBaseUrl);
|
|
202
|
-
const url = new URL(`${base}/checkup_report_files`);
|
|
203
|
-
if (typeof reportId === "number") {
|
|
204
|
-
url.searchParams.set("checkup_report_id", `eq.${reportId}`);
|
|
205
|
-
}
|
|
206
|
-
url.searchParams.set("order", "id.asc");
|
|
207
|
-
if (type) {
|
|
208
|
-
url.searchParams.set("type", `eq.${type}`);
|
|
209
|
-
}
|
|
210
|
-
if (checkId) {
|
|
211
|
-
url.searchParams.set("check_id", `eq.${checkId}`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const headers: Record<string, string> = {
|
|
215
|
-
"access-token": apiKey,
|
|
216
|
-
"Prefer": "return=representation",
|
|
217
|
-
"Content-Type": "application/json",
|
|
218
|
-
"Connection": "close",
|
|
219
|
-
};
|
|
220
|
-
|
|
221
|
-
if (debug) {
|
|
222
|
-
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
223
|
-
console.error(`Debug: Resolved API base URL: ${base}`);
|
|
224
|
-
console.error(`Debug: GET URL: ${url.toString()}`);
|
|
225
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
const response = await fetch(url.toString(), { method: "GET", headers });
|
|
229
|
-
|
|
230
|
-
if (debug) {
|
|
231
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const data = await response.text();
|
|
235
|
-
|
|
236
|
-
if (response.ok) {
|
|
237
|
-
try {
|
|
238
|
-
return JSON.parse(data) as CheckupReportFile[];
|
|
239
|
-
} catch {
|
|
240
|
-
throw new Error(`Failed to parse report files response: ${data}`);
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
throw new Error(formatHttpError("Failed to fetch report files", response.status, data));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export async function fetchReportFileData(params: FetchReportFileDataParams): Promise<CheckupReportFileData[]> {
|
|
248
|
-
const { apiKey, apiBaseUrl, reportId, type, checkId, debug } = params;
|
|
249
|
-
if (!apiKey) {
|
|
250
|
-
throw new Error("API key is required");
|
|
251
|
-
}
|
|
252
|
-
if (reportId === undefined && !checkId) {
|
|
253
|
-
throw new Error("Either reportId or checkId is required");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const base = normalizeBaseUrl(apiBaseUrl);
|
|
257
|
-
const url = new URL(`${base}/checkup_report_file_data`);
|
|
258
|
-
if (typeof reportId === "number") {
|
|
259
|
-
url.searchParams.set("checkup_report_id", `eq.${reportId}`);
|
|
260
|
-
}
|
|
261
|
-
url.searchParams.set("order", "id.asc");
|
|
262
|
-
if (type) {
|
|
263
|
-
url.searchParams.set("type", `eq.${type}`);
|
|
264
|
-
}
|
|
265
|
-
if (checkId) {
|
|
266
|
-
url.searchParams.set("check_id", `eq.${checkId}`);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
const headers: Record<string, string> = {
|
|
270
|
-
"access-token": apiKey,
|
|
271
|
-
"Prefer": "return=representation",
|
|
272
|
-
"Content-Type": "application/json",
|
|
273
|
-
"Connection": "close",
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
if (debug) {
|
|
277
|
-
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
278
|
-
console.error(`Debug: Resolved API base URL: ${base}`);
|
|
279
|
-
console.error(`Debug: GET URL: ${url.toString()}`);
|
|
280
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
const response = await fetch(url.toString(), { method: "GET", headers });
|
|
284
|
-
|
|
285
|
-
if (debug) {
|
|
286
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
const data = await response.text();
|
|
290
|
-
|
|
291
|
-
if (response.ok) {
|
|
292
|
-
try {
|
|
293
|
-
return JSON.parse(data) as CheckupReportFileData[];
|
|
294
|
-
} catch {
|
|
295
|
-
throw new Error(`Failed to parse report file data response: ${data}`);
|
|
296
|
-
}
|
|
297
|
-
} else {
|
|
298
|
-
throw new Error(formatHttpError("Failed to fetch report file data", response.status, data));
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
// ============================================================================
|
|
303
|
-
// Lightweight markdown terminal renderer
|
|
304
|
-
// ============================================================================
|
|
305
|
-
|
|
306
|
-
export function renderMarkdownForTerminal(md: string): string {
|
|
307
|
-
if (!md) return "";
|
|
308
|
-
|
|
309
|
-
const RESET = "\x1b[0m";
|
|
310
|
-
const BOLD = "\x1b[1m";
|
|
311
|
-
const BOLD_UNDERLINE = "\x1b[1;4m";
|
|
312
|
-
const DIM = "\x1b[2m";
|
|
313
|
-
const ITALIC = "\x1b[3m";
|
|
314
|
-
const CYAN = "\x1b[36m";
|
|
315
|
-
|
|
316
|
-
const lines = md.split("\n");
|
|
317
|
-
const output: string[] = [];
|
|
318
|
-
let inCodeBlock = false;
|
|
319
|
-
|
|
320
|
-
for (const line of lines) {
|
|
321
|
-
// Code block toggle
|
|
322
|
-
if (line.trimStart().startsWith("```")) {
|
|
323
|
-
inCodeBlock = !inCodeBlock;
|
|
324
|
-
if (inCodeBlock) {
|
|
325
|
-
output.push(`${DIM}${"─".repeat(40)}${RESET}`);
|
|
326
|
-
} else {
|
|
327
|
-
output.push(`${DIM}${"─".repeat(40)}${RESET}`);
|
|
328
|
-
}
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Inside code block — dim output
|
|
333
|
-
if (inCodeBlock) {
|
|
334
|
-
output.push(`${DIM} ${line}${RESET}`);
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Horizontal rule
|
|
339
|
-
if (/^-{3,}$/.test(line.trim()) || /^\*{3,}$/.test(line.trim()) || /^_{3,}$/.test(line.trim())) {
|
|
340
|
-
output.push(`${DIM}${"─".repeat(60)}${RESET}`);
|
|
341
|
-
continue;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
// Headings
|
|
345
|
-
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
|
|
346
|
-
if (headingMatch) {
|
|
347
|
-
const level = headingMatch[1].length;
|
|
348
|
-
const text = headingMatch[2];
|
|
349
|
-
if (level === 1) {
|
|
350
|
-
output.push(`${BOLD_UNDERLINE}${text}${RESET}`);
|
|
351
|
-
} else {
|
|
352
|
-
output.push(`${BOLD}${text}${RESET}`);
|
|
353
|
-
}
|
|
354
|
-
continue;
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// Inline formatting
|
|
358
|
-
let formatted = line;
|
|
359
|
-
// Bold: **text** or __text__
|
|
360
|
-
formatted = formatted.replace(/\*\*(.+?)\*\*/g, `${BOLD}$1${RESET}`);
|
|
361
|
-
formatted = formatted.replace(/__(.+?)__/g, `${BOLD}$1${RESET}`);
|
|
362
|
-
// Italic: *text* (only single, not inside **)
|
|
363
|
-
formatted = formatted.replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, `${ITALIC}$1${RESET}`);
|
|
364
|
-
// Italic: _text_ — only at word boundaries (not inside identifiers like foo_bar_baz)
|
|
365
|
-
formatted = formatted.replace(/(?<=^|[\s(])_([^\s_](?:.*?[^\s_])?)_(?=$|[\s),.:;!?])/g, `${ITALIC}$1${RESET}`);
|
|
366
|
-
// Inline code: `text`
|
|
367
|
-
formatted = formatted.replace(/`([^`]+)`/g, `${CYAN}$1${RESET}`);
|
|
368
|
-
|
|
369
|
-
output.push(formatted);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
return output.join("\n");
|
|
373
|
-
}
|