postgresai 0.14.0-dev.52 → 0.14.0-dev.54
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 +34 -35
- package/bin/postgres-ai.ts +436 -4
- package/bun.lock +3 -1
- package/bunfig.toml +11 -0
- package/dist/bin/postgres-ai.js +2184 -218
- package/lib/auth-server.ts +52 -5
- package/lib/checkup-api.ts +386 -0
- package/lib/checkup.ts +1327 -0
- package/lib/config.ts +3 -0
- package/lib/issues.ts +5 -41
- package/lib/metrics-embedded.ts +79 -0
- package/lib/metrics-loader.ts +127 -0
- package/lib/util.ts +61 -0
- package/package.json +12 -6
- 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-metrics.ts +154 -0
- package/test/checkup.integration.test.ts +273 -0
- package/test/checkup.test.ts +890 -0
- package/test/init.integration.test.ts +36 -33
- package/test/schema-validation.test.ts +81 -0
- package/test/test-utils.ts +122 -0
- package/dist/sql/01.role.sql +0 -16
- package/dist/sql/02.permissions.sql +0 -37
- package/dist/sql/03.optional_rds.sql +0 -6
- package/dist/sql/04.optional_self_managed.sql +0 -8
- package/dist/sql/05.helpers.sql +0 -415
package/lib/auth-server.ts
CHANGED
|
@@ -14,6 +14,7 @@ export interface CallbackResult {
|
|
|
14
14
|
export interface CallbackServer {
|
|
15
15
|
server: { stop: () => void };
|
|
16
16
|
promise: Promise<CallbackResult>;
|
|
17
|
+
ready: Promise<number>; // Resolves with actual port when server is listening
|
|
17
18
|
getPort: () => number;
|
|
18
19
|
}
|
|
19
20
|
|
|
@@ -38,9 +39,12 @@ function escapeHtml(str: string | null): string {
|
|
|
38
39
|
* @param port - Port to listen on (0 for random available port)
|
|
39
40
|
* @param expectedState - Expected state parameter for CSRF protection
|
|
40
41
|
* @param timeoutMs - Timeout in milliseconds
|
|
41
|
-
* @returns Server object with promise and getPort function
|
|
42
|
+
* @returns Server object with promise, ready promise, and getPort function
|
|
42
43
|
*
|
|
43
44
|
* @remarks
|
|
45
|
+
* The `ready` promise resolves with the actual port once the server is listening.
|
|
46
|
+
* Callers should await `ready` before using `getPort()` when using port 0.
|
|
47
|
+
*
|
|
44
48
|
* The server stops asynchronously ~100ms after the callback resolves/rejects.
|
|
45
49
|
* This delay ensures the HTTP response is fully sent before closing the connection.
|
|
46
50
|
* Callers should not attempt to reuse the same port immediately after the promise
|
|
@@ -55,6 +59,8 @@ export function createCallbackServer(
|
|
|
55
59
|
let actualPort = port;
|
|
56
60
|
let resolveCallback: (value: CallbackResult) => void;
|
|
57
61
|
let rejectCallback: (reason: Error) => void;
|
|
62
|
+
let resolveReady: (port: number) => void;
|
|
63
|
+
let rejectReady: (reason: Error) => void;
|
|
58
64
|
let serverInstance: http.Server | null = null;
|
|
59
65
|
|
|
60
66
|
const promise = new Promise<CallbackResult>((resolve, reject) => {
|
|
@@ -62,7 +68,19 @@ export function createCallbackServer(
|
|
|
62
68
|
rejectCallback = reject;
|
|
63
69
|
});
|
|
64
70
|
|
|
71
|
+
const ready = new Promise<number>((resolve, reject) => {
|
|
72
|
+
resolveReady = resolve;
|
|
73
|
+
rejectReady = reject;
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
77
|
+
|
|
65
78
|
const stopServer = () => {
|
|
79
|
+
// Clear timeout to prevent it firing after manual stop
|
|
80
|
+
if (timeoutId) {
|
|
81
|
+
clearTimeout(timeoutId);
|
|
82
|
+
timeoutId = null;
|
|
83
|
+
}
|
|
66
84
|
if (serverInstance) {
|
|
67
85
|
serverInstance.close();
|
|
68
86
|
serverInstance = null;
|
|
@@ -70,9 +88,10 @@ export function createCallbackServer(
|
|
|
70
88
|
};
|
|
71
89
|
|
|
72
90
|
// Timeout handler
|
|
73
|
-
|
|
91
|
+
timeoutId = setTimeout(() => {
|
|
74
92
|
if (!resolved) {
|
|
75
93
|
resolved = true;
|
|
94
|
+
timeoutId = null; // Already fired, clear reference
|
|
76
95
|
stopServer();
|
|
77
96
|
rejectCallback(new Error("Authentication timeout. Please try again."));
|
|
78
97
|
}
|
|
@@ -102,7 +121,10 @@ export function createCallbackServer(
|
|
|
102
121
|
// Handle OAuth error
|
|
103
122
|
if (error) {
|
|
104
123
|
resolved = true;
|
|
105
|
-
|
|
124
|
+
if (timeoutId) {
|
|
125
|
+
clearTimeout(timeoutId);
|
|
126
|
+
timeoutId = null;
|
|
127
|
+
}
|
|
106
128
|
|
|
107
129
|
setTimeout(() => stopServer(), 100);
|
|
108
130
|
rejectCallback(new Error(`OAuth error: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`));
|
|
@@ -162,7 +184,10 @@ export function createCallbackServer(
|
|
|
162
184
|
// Validate state (CSRF protection)
|
|
163
185
|
if (expectedState && state !== expectedState) {
|
|
164
186
|
resolved = true;
|
|
165
|
-
|
|
187
|
+
if (timeoutId) {
|
|
188
|
+
clearTimeout(timeoutId);
|
|
189
|
+
timeoutId = null;
|
|
190
|
+
}
|
|
166
191
|
|
|
167
192
|
setTimeout(() => stopServer(), 100);
|
|
168
193
|
rejectCallback(new Error("State mismatch (possible CSRF attack)"));
|
|
@@ -193,7 +218,10 @@ export function createCallbackServer(
|
|
|
193
218
|
|
|
194
219
|
// Success!
|
|
195
220
|
resolved = true;
|
|
196
|
-
|
|
221
|
+
if (timeoutId) {
|
|
222
|
+
clearTimeout(timeoutId);
|
|
223
|
+
timeoutId = null;
|
|
224
|
+
}
|
|
197
225
|
|
|
198
226
|
// Resolve first, then stop server asynchronously after response is sent.
|
|
199
227
|
// The 100ms delay ensures the HTTP response is fully written before closing.
|
|
@@ -223,16 +251,35 @@ export function createCallbackServer(
|
|
|
223
251
|
`);
|
|
224
252
|
});
|
|
225
253
|
|
|
254
|
+
// Handle server errors (e.g., EADDRINUSE)
|
|
255
|
+
serverInstance.on("error", (err: NodeJS.ErrnoException) => {
|
|
256
|
+
if (timeoutId) {
|
|
257
|
+
clearTimeout(timeoutId);
|
|
258
|
+
timeoutId = null;
|
|
259
|
+
}
|
|
260
|
+
if (err.code === "EADDRINUSE") {
|
|
261
|
+
rejectReady(new Error(`Port ${port} is already in use`));
|
|
262
|
+
} else {
|
|
263
|
+
rejectReady(new Error(`Server error: ${err.message}`));
|
|
264
|
+
}
|
|
265
|
+
if (!resolved) {
|
|
266
|
+
resolved = true;
|
|
267
|
+
rejectCallback(err);
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
|
|
226
271
|
serverInstance.listen(port, "127.0.0.1", () => {
|
|
227
272
|
const address = serverInstance?.address();
|
|
228
273
|
if (address && typeof address === "object") {
|
|
229
274
|
actualPort = address.port;
|
|
230
275
|
}
|
|
276
|
+
resolveReady(actualPort);
|
|
231
277
|
});
|
|
232
278
|
|
|
233
279
|
return {
|
|
234
280
|
server: { stop: stopServer },
|
|
235
281
|
promise,
|
|
282
|
+
ready,
|
|
236
283
|
getPort: () => actualPort,
|
|
237
284
|
};
|
|
238
285
|
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import * as https from "https";
|
|
2
|
+
import { URL } from "url";
|
|
3
|
+
import { normalizeBaseUrl } from "./util";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Retry configuration for network operations
|
|
7
|
+
*/
|
|
8
|
+
export interface RetryConfig {
|
|
9
|
+
maxAttempts: number;
|
|
10
|
+
initialDelayMs: number;
|
|
11
|
+
maxDelayMs: number;
|
|
12
|
+
backoffMultiplier: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const DEFAULT_RETRY_CONFIG: RetryConfig = {
|
|
16
|
+
maxAttempts: 3,
|
|
17
|
+
initialDelayMs: 1000,
|
|
18
|
+
maxDelayMs: 10000,
|
|
19
|
+
backoffMultiplier: 2,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if an error is retryable (network errors, timeouts, 5xx errors)
|
|
24
|
+
*/
|
|
25
|
+
function isRetryableError(err: unknown): boolean {
|
|
26
|
+
if (err instanceof RpcError) {
|
|
27
|
+
// Retry on server errors (5xx), not on client errors (4xx)
|
|
28
|
+
return err.statusCode >= 500 && err.statusCode < 600;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check for Node.js error codes (works on Error and Error-like objects)
|
|
32
|
+
if (typeof err === "object" && err !== null && "code" in err) {
|
|
33
|
+
const code = String((err as { code: unknown }).code);
|
|
34
|
+
if (["ECONNRESET", "ECONNREFUSED", "ENOTFOUND", "ETIMEDOUT"].includes(code)) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (err instanceof Error) {
|
|
40
|
+
const msg = err.message.toLowerCase();
|
|
41
|
+
// Retry on network-related errors based on message content
|
|
42
|
+
return (
|
|
43
|
+
msg.includes("timeout") ||
|
|
44
|
+
msg.includes("timed out") ||
|
|
45
|
+
msg.includes("econnreset") ||
|
|
46
|
+
msg.includes("econnrefused") ||
|
|
47
|
+
msg.includes("enotfound") ||
|
|
48
|
+
msg.includes("socket hang up") ||
|
|
49
|
+
msg.includes("network")
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Execute an async function with exponential backoff retry.
|
|
58
|
+
* Retries on network errors, timeouts, and 5xx server errors.
|
|
59
|
+
* Does not retry on 4xx client errors.
|
|
60
|
+
*
|
|
61
|
+
* @param fn - Async function to execute
|
|
62
|
+
* @param config - Optional retry configuration (uses defaults if not provided)
|
|
63
|
+
* @param onRetry - Optional callback invoked before each retry attempt
|
|
64
|
+
* @returns Promise resolving to the function result
|
|
65
|
+
* @throws The last error if all retry attempts fail or error is non-retryable
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* const result = await withRetry(
|
|
70
|
+
* () => fetchData(),
|
|
71
|
+
* { maxAttempts: 3 },
|
|
72
|
+
* (attempt, err, delay) => console.log(`Retry ${attempt}, waiting ${delay}ms`)
|
|
73
|
+
* );
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
export async function withRetry<T>(
|
|
77
|
+
fn: () => Promise<T>,
|
|
78
|
+
config: Partial<RetryConfig> = {},
|
|
79
|
+
onRetry?: (attempt: number, error: unknown, delayMs: number) => void
|
|
80
|
+
): Promise<T> {
|
|
81
|
+
const { maxAttempts, initialDelayMs, maxDelayMs, backoffMultiplier } = {
|
|
82
|
+
...DEFAULT_RETRY_CONFIG,
|
|
83
|
+
...config,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
let lastError: unknown;
|
|
87
|
+
let delayMs = initialDelayMs;
|
|
88
|
+
|
|
89
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
90
|
+
try {
|
|
91
|
+
return await fn();
|
|
92
|
+
} catch (err) {
|
|
93
|
+
lastError = err;
|
|
94
|
+
|
|
95
|
+
if (attempt === maxAttempts || !isRetryableError(err)) {
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (onRetry) {
|
|
100
|
+
onRetry(attempt, err, delayMs);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
104
|
+
delayMs = Math.min(delayMs * backoffMultiplier, maxDelayMs);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
throw lastError;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Error thrown when an RPC call to the PostgresAI API fails.
|
|
113
|
+
* Contains detailed information about the failure for debugging and display.
|
|
114
|
+
*/
|
|
115
|
+
export class RpcError extends Error {
|
|
116
|
+
/** Name of the RPC endpoint that failed */
|
|
117
|
+
rpcName: string;
|
|
118
|
+
/** HTTP status code returned by the server */
|
|
119
|
+
statusCode: number;
|
|
120
|
+
/** Raw response body text */
|
|
121
|
+
payloadText: string;
|
|
122
|
+
/** Parsed JSON response body, or null if parsing failed */
|
|
123
|
+
payloadJson: any | null;
|
|
124
|
+
|
|
125
|
+
constructor(params: { rpcName: string; statusCode: number; payloadText: string; payloadJson: any | null }) {
|
|
126
|
+
const { rpcName, statusCode, payloadText, payloadJson } = params;
|
|
127
|
+
super(`RPC ${rpcName} failed: HTTP ${statusCode}`);
|
|
128
|
+
this.name = "RpcError";
|
|
129
|
+
this.rpcName = rpcName;
|
|
130
|
+
this.statusCode = statusCode;
|
|
131
|
+
this.payloadText = payloadText;
|
|
132
|
+
this.payloadJson = payloadJson;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Format an RpcError for human-readable console display.
|
|
138
|
+
* Extracts message, details, and hint from the error payload if available.
|
|
139
|
+
*
|
|
140
|
+
* @param err - The RpcError to format
|
|
141
|
+
* @returns Array of lines suitable for console output
|
|
142
|
+
*/
|
|
143
|
+
export function formatRpcErrorForDisplay(err: RpcError): string[] {
|
|
144
|
+
const lines: string[] = [];
|
|
145
|
+
lines.push(`Error: RPC ${err.rpcName} failed: HTTP ${err.statusCode}`);
|
|
146
|
+
|
|
147
|
+
const obj = err.payloadJson && typeof err.payloadJson === "object" ? err.payloadJson : null;
|
|
148
|
+
const details = obj && typeof (obj as any).details === "string" ? (obj as any).details : "";
|
|
149
|
+
const hint = obj && typeof (obj as any).hint === "string" ? (obj as any).hint : "";
|
|
150
|
+
const message = obj && typeof (obj as any).message === "string" ? (obj as any).message : "";
|
|
151
|
+
|
|
152
|
+
if (message) lines.push(`Message: ${message}`);
|
|
153
|
+
if (details) lines.push(`Details: ${details}`);
|
|
154
|
+
if (hint) lines.push(`Hint: ${hint}`);
|
|
155
|
+
|
|
156
|
+
// Fallback to raw payload if we couldn't extract anything useful.
|
|
157
|
+
if (!message && !details && !hint) {
|
|
158
|
+
const t = (err.payloadText || "").trim();
|
|
159
|
+
if (t) lines.push(t);
|
|
160
|
+
}
|
|
161
|
+
return lines;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function unwrapRpcResponse(parsed: unknown): any {
|
|
165
|
+
// Some deployments return a plain object, others return an array of rows,
|
|
166
|
+
// and some wrap OUT params under a "result" key.
|
|
167
|
+
if (Array.isArray(parsed)) {
|
|
168
|
+
if (parsed.length === 1) return unwrapRpcResponse(parsed[0]);
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
if (parsed && typeof parsed === "object") {
|
|
172
|
+
const obj = parsed as any;
|
|
173
|
+
if (obj.result !== undefined) return obj.result;
|
|
174
|
+
}
|
|
175
|
+
return parsed as any;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Default timeout for HTTP requests (30 seconds)
|
|
179
|
+
const HTTP_TIMEOUT_MS = 30_000;
|
|
180
|
+
|
|
181
|
+
async function postRpc<T>(params: {
|
|
182
|
+
apiKey: string;
|
|
183
|
+
apiBaseUrl: string;
|
|
184
|
+
rpcName: string;
|
|
185
|
+
bodyObj: Record<string, unknown>;
|
|
186
|
+
timeoutMs?: number;
|
|
187
|
+
}): Promise<T> {
|
|
188
|
+
const { apiKey, apiBaseUrl, rpcName, bodyObj, timeoutMs = HTTP_TIMEOUT_MS } = params;
|
|
189
|
+
if (!apiKey) throw new Error("API key is required");
|
|
190
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
191
|
+
const url = new URL(`${base}/rpc/${rpcName}`);
|
|
192
|
+
const body = JSON.stringify(bodyObj);
|
|
193
|
+
|
|
194
|
+
const headers: Record<string, string> = {
|
|
195
|
+
// API key is sent in BOTH header and body (see bodyObj.access_token):
|
|
196
|
+
// - Header: Used by the API gateway/proxy for HTTP authentication
|
|
197
|
+
// - Body: Passed to PostgreSQL RPC function for in-database authorization
|
|
198
|
+
// This is intentional for defense-in-depth; backend validates both.
|
|
199
|
+
"access-token": apiKey,
|
|
200
|
+
"Prefer": "return=representation",
|
|
201
|
+
"Content-Type": "application/json",
|
|
202
|
+
"Content-Length": Buffer.byteLength(body).toString(),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Use AbortController for clean timeout handling
|
|
206
|
+
const controller = new AbortController();
|
|
207
|
+
let timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
208
|
+
let settled = false;
|
|
209
|
+
|
|
210
|
+
return new Promise((resolve, reject) => {
|
|
211
|
+
const settledReject = (err: Error) => {
|
|
212
|
+
if (settled) return;
|
|
213
|
+
settled = true;
|
|
214
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
215
|
+
reject(err);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const settledResolve = (value: T) => {
|
|
219
|
+
if (settled) return;
|
|
220
|
+
settled = true;
|
|
221
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
222
|
+
resolve(value);
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const req = https.request(
|
|
226
|
+
url,
|
|
227
|
+
{
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers,
|
|
230
|
+
signal: controller.signal,
|
|
231
|
+
},
|
|
232
|
+
(res) => {
|
|
233
|
+
// Response started (headers received) - clear the connection timeout.
|
|
234
|
+
// Once the server starts responding, we let it complete rather than
|
|
235
|
+
// timing out mid-response which would cause confusing errors.
|
|
236
|
+
if (timeoutId) {
|
|
237
|
+
clearTimeout(timeoutId);
|
|
238
|
+
timeoutId = null;
|
|
239
|
+
}
|
|
240
|
+
let data = "";
|
|
241
|
+
res.on("data", (chunk) => (data += chunk));
|
|
242
|
+
res.on("end", () => {
|
|
243
|
+
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
244
|
+
try {
|
|
245
|
+
const parsed = JSON.parse(data);
|
|
246
|
+
settledResolve(unwrapRpcResponse(parsed) as T);
|
|
247
|
+
} catch {
|
|
248
|
+
settledReject(new Error(`Failed to parse RPC response: ${data}`));
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
const statusCode = res.statusCode || 0;
|
|
252
|
+
let payloadJson: any | null = null;
|
|
253
|
+
if (data) {
|
|
254
|
+
try {
|
|
255
|
+
payloadJson = JSON.parse(data);
|
|
256
|
+
} catch {
|
|
257
|
+
payloadJson = null;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
settledReject(new RpcError({ rpcName, statusCode, payloadText: data, payloadJson }));
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
res.on("error", (err) => {
|
|
264
|
+
settledReject(err);
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
// Set up connection timeout - applies until response headers are received.
|
|
270
|
+
// Once response starts, timeout is cleared (see response callback above).
|
|
271
|
+
timeoutId = setTimeout(() => {
|
|
272
|
+
controller.abort();
|
|
273
|
+
req.destroy(); // Backup: ensure request is terminated
|
|
274
|
+
settledReject(new Error(`RPC ${rpcName} timed out after ${timeoutMs}ms (no response)`));
|
|
275
|
+
}, timeoutMs);
|
|
276
|
+
|
|
277
|
+
req.on("error", (err: Error) => {
|
|
278
|
+
// Handle abort as timeout (may already be rejected by timeout handler)
|
|
279
|
+
if (err.name === "AbortError" || (err as any).code === "ABORT_ERR") {
|
|
280
|
+
settledReject(new Error(`RPC ${rpcName} timed out after ${timeoutMs}ms`));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// Provide clearer error for common network issues
|
|
284
|
+
if ((err as any).code === "ECONNREFUSED") {
|
|
285
|
+
settledReject(new Error(`RPC ${rpcName} failed: connection refused to ${url.host}`));
|
|
286
|
+
} else if ((err as any).code === "ENOTFOUND") {
|
|
287
|
+
settledReject(new Error(`RPC ${rpcName} failed: DNS lookup failed for ${url.host}`));
|
|
288
|
+
} else if ((err as any).code === "ECONNRESET") {
|
|
289
|
+
settledReject(new Error(`RPC ${rpcName} failed: connection reset by server`));
|
|
290
|
+
} else {
|
|
291
|
+
settledReject(err);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
req.write(body);
|
|
296
|
+
req.end();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Create a new checkup report in the PostgresAI backend.
|
|
302
|
+
* This creates the parent report container; individual check results
|
|
303
|
+
* are uploaded separately via uploadCheckupReportJson().
|
|
304
|
+
*
|
|
305
|
+
* @param params - Configuration for report creation
|
|
306
|
+
* @param params.apiKey - PostgresAI API access token
|
|
307
|
+
* @param params.apiBaseUrl - Base URL of the PostgresAI API
|
|
308
|
+
* @param params.project - Project name or ID to associate the report with
|
|
309
|
+
* @param params.status - Optional initial status for the report
|
|
310
|
+
* @returns Promise resolving to the created report ID
|
|
311
|
+
* @throws {RpcError} On API failures (4xx/5xx responses)
|
|
312
|
+
* @throws {Error} On network errors or unexpected response format
|
|
313
|
+
*/
|
|
314
|
+
export async function createCheckupReport(params: {
|
|
315
|
+
apiKey: string;
|
|
316
|
+
apiBaseUrl: string;
|
|
317
|
+
project: string;
|
|
318
|
+
status?: string;
|
|
319
|
+
}): Promise<{ reportId: number }> {
|
|
320
|
+
const { apiKey, apiBaseUrl, project, status } = params;
|
|
321
|
+
const bodyObj: Record<string, unknown> = {
|
|
322
|
+
access_token: apiKey,
|
|
323
|
+
project,
|
|
324
|
+
};
|
|
325
|
+
if (status) bodyObj.status = status;
|
|
326
|
+
|
|
327
|
+
const resp = await postRpc<any>({
|
|
328
|
+
apiKey,
|
|
329
|
+
apiBaseUrl,
|
|
330
|
+
rpcName: "checkup_report_create",
|
|
331
|
+
bodyObj,
|
|
332
|
+
});
|
|
333
|
+
const reportId = Number(resp?.report_id);
|
|
334
|
+
if (!Number.isFinite(reportId) || reportId <= 0) {
|
|
335
|
+
throw new Error(`Unexpected checkup_report_create response: ${JSON.stringify(resp)}`);
|
|
336
|
+
}
|
|
337
|
+
return { reportId };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Upload a JSON check result to an existing checkup report.
|
|
342
|
+
* Each check (e.g., H001, A003) is uploaded as a separate JSON file.
|
|
343
|
+
*
|
|
344
|
+
* @param params - Configuration for the upload
|
|
345
|
+
* @param params.apiKey - PostgresAI API access token
|
|
346
|
+
* @param params.apiBaseUrl - Base URL of the PostgresAI API
|
|
347
|
+
* @param params.reportId - ID of the parent report (from createCheckupReport)
|
|
348
|
+
* @param params.filename - Filename for the uploaded JSON (e.g., "H001.json")
|
|
349
|
+
* @param params.checkId - Check identifier (e.g., "H001", "A003")
|
|
350
|
+
* @param params.jsonText - JSON content as a string
|
|
351
|
+
* @returns Promise resolving to the created report chunk ID
|
|
352
|
+
* @throws {RpcError} On API failures (4xx/5xx responses)
|
|
353
|
+
* @throws {Error} On network errors or unexpected response format
|
|
354
|
+
*/
|
|
355
|
+
export async function uploadCheckupReportJson(params: {
|
|
356
|
+
apiKey: string;
|
|
357
|
+
apiBaseUrl: string;
|
|
358
|
+
reportId: number;
|
|
359
|
+
filename: string;
|
|
360
|
+
checkId: string;
|
|
361
|
+
jsonText: string;
|
|
362
|
+
}): Promise<{ reportChunkId: number }> {
|
|
363
|
+
const { apiKey, apiBaseUrl, reportId, filename, checkId, jsonText } = params;
|
|
364
|
+
const bodyObj: Record<string, unknown> = {
|
|
365
|
+
access_token: apiKey,
|
|
366
|
+
checkup_report_id: reportId,
|
|
367
|
+
filename,
|
|
368
|
+
check_id: checkId,
|
|
369
|
+
data: jsonText,
|
|
370
|
+
type: "json",
|
|
371
|
+
generate_issue: true,
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const resp = await postRpc<any>({
|
|
375
|
+
apiKey,
|
|
376
|
+
apiBaseUrl,
|
|
377
|
+
rpcName: "checkup_report_file_post",
|
|
378
|
+
bodyObj,
|
|
379
|
+
});
|
|
380
|
+
// Backend has a typo: "report_chunck_id" (with 'ck') - handle both spellings for compatibility
|
|
381
|
+
const chunkId = Number(resp?.report_chunck_id ?? resp?.report_chunk_id);
|
|
382
|
+
if (!Number.isFinite(chunkId) || chunkId <= 0) {
|
|
383
|
+
throw new Error(`Unexpected checkup_report_file_post response: ${JSON.stringify(resp)}`);
|
|
384
|
+
}
|
|
385
|
+
return { reportChunkId: chunkId };
|
|
386
|
+
}
|