postgresai 0.14.0-dev.56 → 0.14.0-dev.58
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/bin/postgres-ai.ts +201 -8
- package/dist/bin/postgres-ai.js +693 -83
- package/dist/sql/05.helpers.sql +31 -7
- package/dist/sql/sql/05.helpers.sql +31 -7
- package/lib/config.ts +4 -4
- package/lib/issues.ts +318 -0
- package/lib/mcp-server.ts +207 -73
- package/lib/metrics-embedded.ts +1 -1
- package/package.json +1 -1
- package/sql/05.helpers.sql +31 -7
- package/test/init.integration.test.ts +98 -0
- package/test/init.test.ts +72 -0
- package/test/issues.cli.test.ts +314 -0
- package/test/issues.test.ts +456 -0
- package/test/mcp-server.test.ts +988 -0
package/dist/sql/05.helpers.sql
CHANGED
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
-- operations they don't have direct permissions for.
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
*
|
|
6
|
+
* explain_generic
|
|
7
7
|
*
|
|
8
8
|
* Function to get generic explain plans with optional HypoPG index testing.
|
|
9
9
|
* Requires: PostgreSQL 16+ (for generic_plan option), HypoPG extension (optional).
|
|
10
10
|
*
|
|
11
|
+
* Security notes:
|
|
12
|
+
* - EXPLAIN without ANALYZE is read-only (plans but doesn't execute the query)
|
|
13
|
+
* - PostgreSQL's EXPLAIN only accepts a single statement (primary protection)
|
|
14
|
+
* - Input validation uses a simple heuristic to detect multiple statements
|
|
15
|
+
* (Note: may reject valid queries containing semicolons in string literals)
|
|
16
|
+
*
|
|
11
17
|
* Usage examples:
|
|
12
18
|
* -- Basic generic plan
|
|
13
19
|
* select postgres_ai.explain_generic('select * from users where id = $1');
|
|
@@ -39,6 +45,7 @@ declare
|
|
|
39
45
|
v_hypo_result record;
|
|
40
46
|
v_version int;
|
|
41
47
|
v_hypopg_available boolean;
|
|
48
|
+
v_clean_query text;
|
|
42
49
|
begin
|
|
43
50
|
-- Check PostgreSQL version (generic_plan requires 16+)
|
|
44
51
|
select current_setting('server_version_num')::int into v_version;
|
|
@@ -48,6 +55,24 @@ begin
|
|
|
48
55
|
current_setting('server_version');
|
|
49
56
|
end if;
|
|
50
57
|
|
|
58
|
+
-- Input validation: reject empty queries
|
|
59
|
+
if query is null or trim(query) = '' then
|
|
60
|
+
raise exception 'query cannot be empty';
|
|
61
|
+
end if;
|
|
62
|
+
|
|
63
|
+
-- Input validation: detect multiple statements (defense-in-depth)
|
|
64
|
+
-- Note: This is a simple heuristic - EXPLAIN itself only accepts single statements
|
|
65
|
+
-- Limitation: Queries with semicolons inside string literals will be rejected
|
|
66
|
+
v_clean_query := trim(query);
|
|
67
|
+
if v_clean_query like '%;%' then
|
|
68
|
+
-- Strip trailing semicolon if present (common user convenience)
|
|
69
|
+
v_clean_query := regexp_replace(v_clean_query, ';\s*$', '');
|
|
70
|
+
-- If there's still a semicolon, reject (likely multiple statements or semicolon in string)
|
|
71
|
+
if v_clean_query like '%;%' then
|
|
72
|
+
raise exception 'query contains semicolon (multiple statements not allowed; note: semicolons in string literals are also not supported)';
|
|
73
|
+
end if;
|
|
74
|
+
end if;
|
|
75
|
+
|
|
51
76
|
-- Check if HypoPG extension is available
|
|
52
77
|
if hypopg_index is not null then
|
|
53
78
|
select exists(
|
|
@@ -64,15 +89,14 @@ begin
|
|
|
64
89
|
v_hypo_result.indexname, v_hypo_result.indexrelid;
|
|
65
90
|
end if;
|
|
66
91
|
|
|
67
|
-
-- Build and execute
|
|
68
|
-
--
|
|
92
|
+
-- Build and execute EXPLAIN query
|
|
93
|
+
-- Note: EXPLAIN is read-only (plans but doesn't execute), making this safe
|
|
69
94
|
begin
|
|
70
95
|
if lower(format) = 'json' then
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
execute 'explain (verbose, settings, generic_plan, format json) ' || v_clean_query
|
|
97
|
+
into result;
|
|
73
98
|
else
|
|
74
|
-
|
|
75
|
-
for v_line in execute v_explain_query loop
|
|
99
|
+
for v_line in execute 'explain (verbose, settings, generic_plan) ' || v_clean_query loop
|
|
76
100
|
v_lines := array_append(v_lines, v_line."QUERY PLAN");
|
|
77
101
|
end loop;
|
|
78
102
|
result := array_to_string(v_lines, e'\n');
|
|
@@ -3,11 +3,17 @@
|
|
|
3
3
|
-- operations they don't have direct permissions for.
|
|
4
4
|
|
|
5
5
|
/*
|
|
6
|
-
*
|
|
6
|
+
* explain_generic
|
|
7
7
|
*
|
|
8
8
|
* Function to get generic explain plans with optional HypoPG index testing.
|
|
9
9
|
* Requires: PostgreSQL 16+ (for generic_plan option), HypoPG extension (optional).
|
|
10
10
|
*
|
|
11
|
+
* Security notes:
|
|
12
|
+
* - EXPLAIN without ANALYZE is read-only (plans but doesn't execute the query)
|
|
13
|
+
* - PostgreSQL's EXPLAIN only accepts a single statement (primary protection)
|
|
14
|
+
* - Input validation uses a simple heuristic to detect multiple statements
|
|
15
|
+
* (Note: may reject valid queries containing semicolons in string literals)
|
|
16
|
+
*
|
|
11
17
|
* Usage examples:
|
|
12
18
|
* -- Basic generic plan
|
|
13
19
|
* select postgres_ai.explain_generic('select * from users where id = $1');
|
|
@@ -39,6 +45,7 @@ declare
|
|
|
39
45
|
v_hypo_result record;
|
|
40
46
|
v_version int;
|
|
41
47
|
v_hypopg_available boolean;
|
|
48
|
+
v_clean_query text;
|
|
42
49
|
begin
|
|
43
50
|
-- Check PostgreSQL version (generic_plan requires 16+)
|
|
44
51
|
select current_setting('server_version_num')::int into v_version;
|
|
@@ -48,6 +55,24 @@ begin
|
|
|
48
55
|
current_setting('server_version');
|
|
49
56
|
end if;
|
|
50
57
|
|
|
58
|
+
-- Input validation: reject empty queries
|
|
59
|
+
if query is null or trim(query) = '' then
|
|
60
|
+
raise exception 'query cannot be empty';
|
|
61
|
+
end if;
|
|
62
|
+
|
|
63
|
+
-- Input validation: detect multiple statements (defense-in-depth)
|
|
64
|
+
-- Note: This is a simple heuristic - EXPLAIN itself only accepts single statements
|
|
65
|
+
-- Limitation: Queries with semicolons inside string literals will be rejected
|
|
66
|
+
v_clean_query := trim(query);
|
|
67
|
+
if v_clean_query like '%;%' then
|
|
68
|
+
-- Strip trailing semicolon if present (common user convenience)
|
|
69
|
+
v_clean_query := regexp_replace(v_clean_query, ';\s*$', '');
|
|
70
|
+
-- If there's still a semicolon, reject (likely multiple statements or semicolon in string)
|
|
71
|
+
if v_clean_query like '%;%' then
|
|
72
|
+
raise exception 'query contains semicolon (multiple statements not allowed; note: semicolons in string literals are also not supported)';
|
|
73
|
+
end if;
|
|
74
|
+
end if;
|
|
75
|
+
|
|
51
76
|
-- Check if HypoPG extension is available
|
|
52
77
|
if hypopg_index is not null then
|
|
53
78
|
select exists(
|
|
@@ -64,15 +89,14 @@ begin
|
|
|
64
89
|
v_hypo_result.indexname, v_hypo_result.indexrelid;
|
|
65
90
|
end if;
|
|
66
91
|
|
|
67
|
-
-- Build and execute
|
|
68
|
-
--
|
|
92
|
+
-- Build and execute EXPLAIN query
|
|
93
|
+
-- Note: EXPLAIN is read-only (plans but doesn't execute), making this safe
|
|
69
94
|
begin
|
|
70
95
|
if lower(format) = 'json' then
|
|
71
|
-
|
|
72
|
-
|
|
96
|
+
execute 'explain (verbose, settings, generic_plan, format json) ' || v_clean_query
|
|
97
|
+
into result;
|
|
73
98
|
else
|
|
74
|
-
|
|
75
|
-
for v_line in execute v_explain_query loop
|
|
99
|
+
for v_line in execute 'explain (verbose, settings, generic_plan) ' || v_clean_query loop
|
|
76
100
|
v_lines := array_append(v_lines, v_line."QUERY PLAN");
|
|
77
101
|
end loop;
|
|
78
102
|
result := array_to_string(v_lines, e'\n');
|
package/lib/config.ts
CHANGED
|
@@ -56,10 +56,10 @@ export function readConfig(): Config {
|
|
|
56
56
|
try {
|
|
57
57
|
const content = fs.readFileSync(userConfigPath, "utf8");
|
|
58
58
|
const parsed = JSON.parse(content);
|
|
59
|
-
config.apiKey = parsed.apiKey
|
|
60
|
-
config.baseUrl = parsed.baseUrl
|
|
61
|
-
config.orgId = parsed.orgId
|
|
62
|
-
config.defaultProject = parsed.defaultProject
|
|
59
|
+
config.apiKey = parsed.apiKey ?? null;
|
|
60
|
+
config.baseUrl = parsed.baseUrl ?? null;
|
|
61
|
+
config.orgId = parsed.orgId ?? null;
|
|
62
|
+
config.defaultProject = parsed.defaultProject ?? null;
|
|
63
63
|
return config;
|
|
64
64
|
} catch (err) {
|
|
65
65
|
const message = err instanceof Error ? err.message : String(err);
|
package/lib/issues.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Issue status constants.
|
|
5
|
+
* Used in updateIssue to change issue state.
|
|
6
|
+
*/
|
|
7
|
+
export const IssueStatus = {
|
|
8
|
+
/** Issue is open and active */
|
|
9
|
+
OPEN: 0,
|
|
10
|
+
/** Issue is closed/resolved */
|
|
11
|
+
CLOSED: 1,
|
|
12
|
+
} as const;
|
|
13
|
+
|
|
3
14
|
export interface IssueActionItem {
|
|
4
15
|
id: string;
|
|
5
16
|
issue_id: string;
|
|
@@ -69,6 +80,7 @@ export async function fetchIssues(params: FetchIssuesParams): Promise<IssueListI
|
|
|
69
80
|
"access-token": apiKey,
|
|
70
81
|
"Prefer": "return=representation",
|
|
71
82
|
"Content-Type": "application/json",
|
|
83
|
+
"Connection": "close",
|
|
72
84
|
};
|
|
73
85
|
|
|
74
86
|
if (debug) {
|
|
@@ -126,6 +138,7 @@ export async function fetchIssueComments(params: FetchIssueCommentsParams): Prom
|
|
|
126
138
|
"access-token": apiKey,
|
|
127
139
|
"Prefer": "return=representation",
|
|
128
140
|
"Content-Type": "application/json",
|
|
141
|
+
"Connection": "close",
|
|
129
142
|
};
|
|
130
143
|
|
|
131
144
|
if (debug) {
|
|
@@ -185,6 +198,7 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
|
|
|
185
198
|
"access-token": apiKey,
|
|
186
199
|
"Prefer": "return=representation",
|
|
187
200
|
"Content-Type": "application/json",
|
|
201
|
+
"Connection": "close",
|
|
188
202
|
};
|
|
189
203
|
|
|
190
204
|
if (debug) {
|
|
@@ -223,6 +237,112 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
|
|
|
223
237
|
}
|
|
224
238
|
}
|
|
225
239
|
|
|
240
|
+
export interface CreateIssueParams {
|
|
241
|
+
apiKey: string;
|
|
242
|
+
apiBaseUrl: string;
|
|
243
|
+
title: string;
|
|
244
|
+
orgId: number;
|
|
245
|
+
description?: string;
|
|
246
|
+
projectId?: number;
|
|
247
|
+
labels?: string[];
|
|
248
|
+
debug?: boolean;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface CreatedIssue {
|
|
252
|
+
id: string;
|
|
253
|
+
title: string;
|
|
254
|
+
description: string | null;
|
|
255
|
+
created_at: string;
|
|
256
|
+
status: number;
|
|
257
|
+
project_id: number | null;
|
|
258
|
+
labels: string[] | null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Create a new issue in the PostgresAI platform.
|
|
263
|
+
*
|
|
264
|
+
* @param params - The parameters for creating an issue
|
|
265
|
+
* @param params.apiKey - API key for authentication
|
|
266
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
267
|
+
* @param params.title - Issue title (required)
|
|
268
|
+
* @param params.orgId - Organization ID (required)
|
|
269
|
+
* @param params.description - Optional issue description
|
|
270
|
+
* @param params.projectId - Optional project ID to associate with
|
|
271
|
+
* @param params.labels - Optional array of label strings
|
|
272
|
+
* @param params.debug - Enable debug logging
|
|
273
|
+
* @returns The created issue object
|
|
274
|
+
* @throws Error if API key, title, or orgId is missing, or if the API call fails
|
|
275
|
+
*/
|
|
276
|
+
export async function createIssue(params: CreateIssueParams): Promise<CreatedIssue> {
|
|
277
|
+
const { apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug } = params;
|
|
278
|
+
if (!apiKey) {
|
|
279
|
+
throw new Error("API key is required");
|
|
280
|
+
}
|
|
281
|
+
if (!title) {
|
|
282
|
+
throw new Error("title is required");
|
|
283
|
+
}
|
|
284
|
+
if (typeof orgId !== "number") {
|
|
285
|
+
throw new Error("orgId is required");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
289
|
+
const url = new URL(`${base}/rpc/issue_create`);
|
|
290
|
+
|
|
291
|
+
const bodyObj: Record<string, unknown> = {
|
|
292
|
+
title: title,
|
|
293
|
+
org_id: orgId,
|
|
294
|
+
};
|
|
295
|
+
if (description !== undefined) {
|
|
296
|
+
bodyObj.description = description;
|
|
297
|
+
}
|
|
298
|
+
if (projectId !== undefined) {
|
|
299
|
+
bodyObj.project_id = projectId;
|
|
300
|
+
}
|
|
301
|
+
if (labels && labels.length > 0) {
|
|
302
|
+
bodyObj.labels = labels;
|
|
303
|
+
}
|
|
304
|
+
const body = JSON.stringify(bodyObj);
|
|
305
|
+
|
|
306
|
+
const headers: Record<string, string> = {
|
|
307
|
+
"access-token": apiKey,
|
|
308
|
+
"Prefer": "return=representation",
|
|
309
|
+
"Content-Type": "application/json",
|
|
310
|
+
"Connection": "close",
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (debug) {
|
|
314
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
315
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
316
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
317
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
318
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
319
|
+
console.log(`Debug: Request body: ${body}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const response = await fetch(url.toString(), {
|
|
323
|
+
method: "POST",
|
|
324
|
+
headers,
|
|
325
|
+
body,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (debug) {
|
|
329
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
330
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const data = await response.text();
|
|
334
|
+
|
|
335
|
+
if (response.ok) {
|
|
336
|
+
try {
|
|
337
|
+
return JSON.parse(data) as CreatedIssue;
|
|
338
|
+
} catch {
|
|
339
|
+
throw new Error(`Failed to parse create issue response: ${data}`);
|
|
340
|
+
}
|
|
341
|
+
} else {
|
|
342
|
+
throw new Error(formatHttpError("Failed to create issue", response.status, data));
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
226
346
|
export interface CreateIssueCommentParams {
|
|
227
347
|
apiKey: string;
|
|
228
348
|
apiBaseUrl: string;
|
|
@@ -260,6 +380,7 @@ export async function createIssueComment(params: CreateIssueCommentParams): Prom
|
|
|
260
380
|
"access-token": apiKey,
|
|
261
381
|
"Prefer": "return=representation",
|
|
262
382
|
"Content-Type": "application/json",
|
|
383
|
+
"Connection": "close",
|
|
263
384
|
};
|
|
264
385
|
|
|
265
386
|
if (debug) {
|
|
@@ -294,3 +415,200 @@ export async function createIssueComment(params: CreateIssueCommentParams): Prom
|
|
|
294
415
|
throw new Error(formatHttpError("Failed to create issue comment", response.status, data));
|
|
295
416
|
}
|
|
296
417
|
}
|
|
418
|
+
|
|
419
|
+
export interface UpdateIssueParams {
|
|
420
|
+
apiKey: string;
|
|
421
|
+
apiBaseUrl: string;
|
|
422
|
+
issueId: string;
|
|
423
|
+
title?: string;
|
|
424
|
+
description?: string;
|
|
425
|
+
status?: number;
|
|
426
|
+
labels?: string[];
|
|
427
|
+
debug?: boolean;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export interface UpdatedIssue {
|
|
431
|
+
id: string;
|
|
432
|
+
title: string;
|
|
433
|
+
description: string | null;
|
|
434
|
+
status: number;
|
|
435
|
+
updated_at: string;
|
|
436
|
+
labels: string[] | null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Update an existing issue in the PostgresAI platform.
|
|
441
|
+
*
|
|
442
|
+
* @param params - The parameters for updating an issue
|
|
443
|
+
* @param params.apiKey - API key for authentication
|
|
444
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
445
|
+
* @param params.issueId - ID of the issue to update (required)
|
|
446
|
+
* @param params.title - New title (optional)
|
|
447
|
+
* @param params.description - New description (optional)
|
|
448
|
+
* @param params.status - New status: 0 = open, 1 = closed (optional)
|
|
449
|
+
* @param params.labels - New labels array (optional, replaces existing)
|
|
450
|
+
* @param params.debug - Enable debug logging
|
|
451
|
+
* @returns The updated issue object
|
|
452
|
+
* @throws Error if API key or issueId is missing, if no fields to update are provided, or if the API call fails
|
|
453
|
+
*/
|
|
454
|
+
export async function updateIssue(params: UpdateIssueParams): Promise<UpdatedIssue> {
|
|
455
|
+
const { apiKey, apiBaseUrl, issueId, title, description, status, labels, debug } = params;
|
|
456
|
+
if (!apiKey) {
|
|
457
|
+
throw new Error("API key is required");
|
|
458
|
+
}
|
|
459
|
+
if (!issueId) {
|
|
460
|
+
throw new Error("issueId is required");
|
|
461
|
+
}
|
|
462
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
463
|
+
throw new Error("At least one field to update is required (title, description, status, or labels)");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
467
|
+
const url = new URL(`${base}/rpc/issue_update`);
|
|
468
|
+
|
|
469
|
+
// Prod RPC expects p_* argument names (see OpenAPI at /api/general/).
|
|
470
|
+
const bodyObj: Record<string, unknown> = {
|
|
471
|
+
p_id: issueId,
|
|
472
|
+
};
|
|
473
|
+
if (title !== undefined) {
|
|
474
|
+
bodyObj.p_title = title;
|
|
475
|
+
}
|
|
476
|
+
if (description !== undefined) {
|
|
477
|
+
bodyObj.p_description = description;
|
|
478
|
+
}
|
|
479
|
+
if (status !== undefined) {
|
|
480
|
+
bodyObj.p_status = status;
|
|
481
|
+
}
|
|
482
|
+
if (labels !== undefined) {
|
|
483
|
+
bodyObj.p_labels = labels;
|
|
484
|
+
}
|
|
485
|
+
const body = JSON.stringify(bodyObj);
|
|
486
|
+
|
|
487
|
+
const headers: Record<string, string> = {
|
|
488
|
+
"access-token": apiKey,
|
|
489
|
+
"Prefer": "return=representation",
|
|
490
|
+
"Content-Type": "application/json",
|
|
491
|
+
"Connection": "close",
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
if (debug) {
|
|
495
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
496
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
497
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
498
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
499
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
500
|
+
console.log(`Debug: Request body: ${body}`);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const response = await fetch(url.toString(), {
|
|
504
|
+
method: "POST",
|
|
505
|
+
headers,
|
|
506
|
+
body,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (debug) {
|
|
510
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
511
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const data = await response.text();
|
|
515
|
+
|
|
516
|
+
if (response.ok) {
|
|
517
|
+
try {
|
|
518
|
+
return JSON.parse(data) as UpdatedIssue;
|
|
519
|
+
} catch {
|
|
520
|
+
throw new Error(`Failed to parse update issue response: ${data}`);
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
throw new Error(formatHttpError("Failed to update issue", response.status, data));
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export interface UpdateIssueCommentParams {
|
|
528
|
+
apiKey: string;
|
|
529
|
+
apiBaseUrl: string;
|
|
530
|
+
commentId: string;
|
|
531
|
+
content: string;
|
|
532
|
+
debug?: boolean;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
export interface UpdatedIssueComment {
|
|
536
|
+
id: string;
|
|
537
|
+
issue_id: string;
|
|
538
|
+
content: string;
|
|
539
|
+
updated_at: string;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Update an existing issue comment in the PostgresAI platform.
|
|
544
|
+
*
|
|
545
|
+
* @param params - The parameters for updating a comment
|
|
546
|
+
* @param params.apiKey - API key for authentication
|
|
547
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
548
|
+
* @param params.commentId - ID of the comment to update (required)
|
|
549
|
+
* @param params.content - New comment content (required)
|
|
550
|
+
* @param params.debug - Enable debug logging
|
|
551
|
+
* @returns The updated comment object
|
|
552
|
+
* @throws Error if API key, commentId, or content is missing, or if the API call fails
|
|
553
|
+
*/
|
|
554
|
+
export async function updateIssueComment(params: UpdateIssueCommentParams): Promise<UpdatedIssueComment> {
|
|
555
|
+
const { apiKey, apiBaseUrl, commentId, content, debug } = params;
|
|
556
|
+
if (!apiKey) {
|
|
557
|
+
throw new Error("API key is required");
|
|
558
|
+
}
|
|
559
|
+
if (!commentId) {
|
|
560
|
+
throw new Error("commentId is required");
|
|
561
|
+
}
|
|
562
|
+
if (!content) {
|
|
563
|
+
throw new Error("content is required");
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
567
|
+
const url = new URL(`${base}/rpc/issue_comment_update`);
|
|
568
|
+
|
|
569
|
+
const bodyObj: Record<string, unknown> = {
|
|
570
|
+
// Prod RPC expects p_* argument names (see OpenAPI at /api/general/).
|
|
571
|
+
p_id: commentId,
|
|
572
|
+
p_content: content,
|
|
573
|
+
};
|
|
574
|
+
const body = JSON.stringify(bodyObj);
|
|
575
|
+
|
|
576
|
+
const headers: Record<string, string> = {
|
|
577
|
+
"access-token": apiKey,
|
|
578
|
+
"Prefer": "return=representation",
|
|
579
|
+
"Content-Type": "application/json",
|
|
580
|
+
"Connection": "close",
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
if (debug) {
|
|
584
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
585
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
586
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
587
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
588
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
589
|
+
console.log(`Debug: Request body: ${body}`);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const response = await fetch(url.toString(), {
|
|
593
|
+
method: "POST",
|
|
594
|
+
headers,
|
|
595
|
+
body,
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
if (debug) {
|
|
599
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
600
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const data = await response.text();
|
|
604
|
+
|
|
605
|
+
if (response.ok) {
|
|
606
|
+
try {
|
|
607
|
+
return JSON.parse(data) as UpdatedIssueComment;
|
|
608
|
+
} catch {
|
|
609
|
+
throw new Error(`Failed to parse update comment response: ${data}`);
|
|
610
|
+
}
|
|
611
|
+
} else {
|
|
612
|
+
throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
|
|
613
|
+
}
|
|
614
|
+
}
|