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/issues.ts
CHANGED
|
@@ -1,6 +1,26 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
|
|
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
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Represents a PostgreSQL configuration parameter change recommendation.
|
|
16
|
+
* Used in action items to suggest config tuning.
|
|
17
|
+
*/
|
|
18
|
+
export interface ConfigChange {
|
|
19
|
+
/** PostgreSQL configuration parameter name (e.g., 'work_mem', 'shared_buffers') */
|
|
20
|
+
parameter: string;
|
|
21
|
+
/** Recommended value for the parameter (e.g., '256MB', '4GB') */
|
|
22
|
+
value: string;
|
|
23
|
+
}
|
|
4
24
|
|
|
5
25
|
export interface IssueActionItem {
|
|
6
26
|
id: string;
|
|
@@ -11,10 +31,27 @@ export interface IssueActionItem {
|
|
|
11
31
|
is_done: boolean;
|
|
12
32
|
done_by: number | null;
|
|
13
33
|
done_at: string | null;
|
|
34
|
+
status: string;
|
|
35
|
+
status_reason: string | null;
|
|
36
|
+
status_changed_by: number | null;
|
|
37
|
+
status_changed_at: string | null;
|
|
38
|
+
sql_action: string | null;
|
|
39
|
+
configs: ConfigChange[];
|
|
14
40
|
created_at: string;
|
|
15
41
|
updated_at: string;
|
|
16
42
|
}
|
|
17
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Summary of an action item (minimal fields for list views).
|
|
46
|
+
* Used in issue detail responses to provide quick overview of action items.
|
|
47
|
+
*/
|
|
48
|
+
export interface IssueActionItemSummary {
|
|
49
|
+
/** Action item ID (UUID) */
|
|
50
|
+
id: string;
|
|
51
|
+
/** Action item title */
|
|
52
|
+
title: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
18
55
|
export interface Issue {
|
|
19
56
|
id: string;
|
|
20
57
|
title: string;
|
|
@@ -50,15 +87,21 @@ export interface IssueComment {
|
|
|
50
87
|
|
|
51
88
|
export type IssueListItem = Pick<Issue, "id" | "title" | "status" | "created_at">;
|
|
52
89
|
|
|
53
|
-
export type IssueDetail = Pick<Issue, "id" | "title" | "description" | "status" | "created_at" | "author_display_name"
|
|
90
|
+
export type IssueDetail = Pick<Issue, "id" | "title" | "description" | "status" | "created_at" | "author_display_name"> & {
|
|
91
|
+
action_items: IssueActionItemSummary[];
|
|
92
|
+
};
|
|
54
93
|
export interface FetchIssuesParams {
|
|
55
94
|
apiKey: string;
|
|
56
95
|
apiBaseUrl: string;
|
|
96
|
+
orgId?: number;
|
|
97
|
+
status?: "open" | "closed";
|
|
98
|
+
limit?: number;
|
|
99
|
+
offset?: number;
|
|
57
100
|
debug?: boolean;
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
export async function fetchIssues(params: FetchIssuesParams): Promise<IssueListItem[]> {
|
|
61
|
-
const { apiKey, apiBaseUrl, debug } = params;
|
|
104
|
+
const { apiKey, apiBaseUrl, orgId, status, limit = 20, offset = 0, debug } = params;
|
|
62
105
|
if (!apiKey) {
|
|
63
106
|
throw new Error("API key is required");
|
|
64
107
|
}
|
|
@@ -66,68 +109,54 @@ export async function fetchIssues(params: FetchIssuesParams): Promise<IssueListI
|
|
|
66
109
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
67
110
|
const url = new URL(`${base}/issues`);
|
|
68
111
|
url.searchParams.set("select", "id,title,status,created_at");
|
|
112
|
+
url.searchParams.set("order", "id.desc");
|
|
113
|
+
url.searchParams.set("limit", String(limit));
|
|
114
|
+
url.searchParams.set("offset", String(offset));
|
|
115
|
+
if (typeof orgId === "number") {
|
|
116
|
+
url.searchParams.set("org_id", `eq.${orgId}`);
|
|
117
|
+
}
|
|
118
|
+
if (status === "open") {
|
|
119
|
+
url.searchParams.set("status", "eq.0");
|
|
120
|
+
} else if (status === "closed") {
|
|
121
|
+
url.searchParams.set("status", "eq.1");
|
|
122
|
+
}
|
|
69
123
|
|
|
70
124
|
const headers: Record<string, string> = {
|
|
71
125
|
"access-token": apiKey,
|
|
72
126
|
"Prefer": "return=representation",
|
|
73
127
|
"Content-Type": "application/json",
|
|
128
|
+
"Connection": "close",
|
|
74
129
|
};
|
|
75
130
|
|
|
76
131
|
if (debug) {
|
|
77
132
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
78
|
-
// eslint-disable-next-line no-console
|
|
79
133
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
80
|
-
// eslint-disable-next-line no-console
|
|
81
134
|
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
82
|
-
// eslint-disable-next-line no-console
|
|
83
135
|
console.log(`Debug: Auth scheme: access-token`);
|
|
84
|
-
// eslint-disable-next-line no-console
|
|
85
136
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
86
137
|
}
|
|
87
138
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
method: "GET",
|
|
93
|
-
headers,
|
|
94
|
-
},
|
|
95
|
-
(res) => {
|
|
96
|
-
let data = "";
|
|
97
|
-
res.on("data", (chunk) => (data += chunk));
|
|
98
|
-
res.on("end", () => {
|
|
99
|
-
if (debug) {
|
|
100
|
-
// eslint-disable-next-line no-console
|
|
101
|
-
console.log(`Debug: Response status: ${res.statusCode}`);
|
|
102
|
-
// eslint-disable-next-line no-console
|
|
103
|
-
console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
|
|
104
|
-
}
|
|
105
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
106
|
-
try {
|
|
107
|
-
const parsed = JSON.parse(data) as IssueListItem[];
|
|
108
|
-
resolve(parsed);
|
|
109
|
-
} catch {
|
|
110
|
-
reject(new Error(`Failed to parse issues response: ${data}`));
|
|
111
|
-
}
|
|
112
|
-
} else {
|
|
113
|
-
let errMsg = `Failed to fetch issues: HTTP ${res.statusCode}`;
|
|
114
|
-
if (data) {
|
|
115
|
-
try {
|
|
116
|
-
const errObj = JSON.parse(data);
|
|
117
|
-
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
118
|
-
} catch {
|
|
119
|
-
errMsg += `\n${data}`;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
reject(new Error(errMsg));
|
|
123
|
-
}
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
req.on("error", (err: Error) => reject(err));
|
|
129
|
-
req.end();
|
|
139
|
+
const response = await fetch(url.toString(), {
|
|
140
|
+
method: "GET",
|
|
141
|
+
headers,
|
|
130
142
|
});
|
|
143
|
+
|
|
144
|
+
if (debug) {
|
|
145
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
146
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const data = await response.text();
|
|
150
|
+
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
try {
|
|
153
|
+
return JSON.parse(data) as IssueListItem[];
|
|
154
|
+
} catch {
|
|
155
|
+
throw new Error(`Failed to parse issues response: ${data}`);
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
throw new Error(formatHttpError("Failed to fetch issues", response.status, data));
|
|
159
|
+
}
|
|
131
160
|
}
|
|
132
161
|
|
|
133
162
|
|
|
@@ -154,63 +183,38 @@ export async function fetchIssueComments(params: FetchIssueCommentsParams): Prom
|
|
|
154
183
|
"access-token": apiKey,
|
|
155
184
|
"Prefer": "return=representation",
|
|
156
185
|
"Content-Type": "application/json",
|
|
186
|
+
"Connection": "close",
|
|
157
187
|
};
|
|
158
188
|
|
|
159
189
|
if (debug) {
|
|
160
190
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
161
|
-
// eslint-disable-next-line no-console
|
|
162
191
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
163
|
-
// eslint-disable-next-line no-console
|
|
164
192
|
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
165
|
-
// eslint-disable-next-line no-console
|
|
166
193
|
console.log(`Debug: Auth scheme: access-token`);
|
|
167
|
-
// eslint-disable-next-line no-console
|
|
168
194
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
169
195
|
}
|
|
170
196
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
{
|
|
175
|
-
method: "GET",
|
|
176
|
-
headers,
|
|
177
|
-
},
|
|
178
|
-
(res) => {
|
|
179
|
-
let data = "";
|
|
180
|
-
res.on("data", (chunk) => (data += chunk));
|
|
181
|
-
res.on("end", () => {
|
|
182
|
-
if (debug) {
|
|
183
|
-
// eslint-disable-next-line no-console
|
|
184
|
-
console.log(`Debug: Response status: ${res.statusCode}`);
|
|
185
|
-
// eslint-disable-next-line no-console
|
|
186
|
-
console.log(`Debug: Response headers: ${JSON.stringify(res.headers)}`);
|
|
187
|
-
}
|
|
188
|
-
if (res.statusCode && res.statusCode >= 200 && res.statusCode < 300) {
|
|
189
|
-
try {
|
|
190
|
-
const parsed = JSON.parse(data) as IssueComment[];
|
|
191
|
-
resolve(parsed);
|
|
192
|
-
} catch {
|
|
193
|
-
reject(new Error(`Failed to parse issue comments response: ${data}`));
|
|
194
|
-
}
|
|
195
|
-
} else {
|
|
196
|
-
let errMsg = `Failed to fetch issue comments: HTTP ${res.statusCode}`;
|
|
197
|
-
if (data) {
|
|
198
|
-
try {
|
|
199
|
-
const errObj = JSON.parse(data);
|
|
200
|
-
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
201
|
-
} catch {
|
|
202
|
-
errMsg += `\n${data}`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
reject(new Error(errMsg));
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
req.on("error", (err: Error) => reject(err));
|
|
212
|
-
req.end();
|
|
197
|
+
const response = await fetch(url.toString(), {
|
|
198
|
+
method: "GET",
|
|
199
|
+
headers,
|
|
213
200
|
});
|
|
201
|
+
|
|
202
|
+
if (debug) {
|
|
203
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
204
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const data = await response.text();
|
|
208
|
+
|
|
209
|
+
if (response.ok) {
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(data) as IssueComment[];
|
|
212
|
+
} catch {
|
|
213
|
+
throw new Error(`Failed to parse issue comments response: ${data}`);
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
throw new Error(formatHttpError("Failed to fetch issue comments", response.status, data));
|
|
217
|
+
}
|
|
214
218
|
}
|
|
215
219
|
|
|
216
220
|
export interface FetchIssueParams {
|
|
@@ -231,7 +235,7 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
|
|
|
231
235
|
|
|
232
236
|
const base = normalizeBaseUrl(apiBaseUrl);
|
|
233
237
|
const url = new URL(`${base}/issues`);
|
|
234
|
-
url.searchParams.set("select", "id,title,description,status,created_at,author_display_name");
|
|
238
|
+
url.searchParams.set("select", "id,title,description,status,created_at,author_display_name,action_items");
|
|
235
239
|
url.searchParams.set("id", `eq.${issueId}`);
|
|
236
240
|
url.searchParams.set("limit", "1");
|
|
237
241
|
|
|
@@ -239,67 +243,161 @@ export async function fetchIssue(params: FetchIssueParams): Promise<IssueDetail
|
|
|
239
243
|
"access-token": apiKey,
|
|
240
244
|
"Prefer": "return=representation",
|
|
241
245
|
"Content-Type": "application/json",
|
|
246
|
+
"Connection": "close",
|
|
242
247
|
};
|
|
243
248
|
|
|
244
249
|
if (debug) {
|
|
245
250
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
246
|
-
// eslint-disable-next-line no-console
|
|
247
251
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
248
|
-
// eslint-disable-next-line no-console
|
|
249
252
|
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
250
|
-
// eslint-disable-next-line no-console
|
|
251
253
|
console.log(`Debug: Auth scheme: access-token`);
|
|
252
|
-
// eslint-disable-next-line no-console
|
|
253
254
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
254
255
|
}
|
|
255
256
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
try {
|
|
275
|
-
const parsed = JSON.parse(data);
|
|
276
|
-
if (Array.isArray(parsed)) {
|
|
277
|
-
resolve((parsed[0] as IssueDetail) ?? null);
|
|
278
|
-
} else {
|
|
279
|
-
resolve(parsed as IssueDetail);
|
|
280
|
-
}
|
|
281
|
-
} catch {
|
|
282
|
-
reject(new Error(`Failed to parse issue response: ${data}`));
|
|
283
|
-
}
|
|
284
|
-
} else {
|
|
285
|
-
let errMsg = `Failed to fetch issue: HTTP ${res.statusCode}`;
|
|
286
|
-
if (data) {
|
|
287
|
-
try {
|
|
288
|
-
const errObj = JSON.parse(data);
|
|
289
|
-
errMsg += `\n${JSON.stringify(errObj, null, 2)}`;
|
|
290
|
-
} catch {
|
|
291
|
-
errMsg += `\n${data}`;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
reject(new Error(errMsg));
|
|
295
|
-
}
|
|
296
|
-
});
|
|
257
|
+
const response = await fetch(url.toString(), {
|
|
258
|
+
method: "GET",
|
|
259
|
+
headers,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
if (debug) {
|
|
263
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
264
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const data = await response.text();
|
|
268
|
+
|
|
269
|
+
if (response.ok) {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(data);
|
|
272
|
+
const rawIssue = Array.isArray(parsed) ? parsed[0] : parsed;
|
|
273
|
+
if (!rawIssue) {
|
|
274
|
+
return null;
|
|
297
275
|
}
|
|
298
|
-
|
|
276
|
+
// Map action_items to summary (id, title only)
|
|
277
|
+
const actionItemsSummary: IssueActionItemSummary[] = Array.isArray(rawIssue.action_items)
|
|
278
|
+
? rawIssue.action_items.map((item: IssueActionItem) => ({ id: item.id, title: item.title }))
|
|
279
|
+
: [];
|
|
280
|
+
return {
|
|
281
|
+
id: rawIssue.id,
|
|
282
|
+
title: rawIssue.title,
|
|
283
|
+
description: rawIssue.description,
|
|
284
|
+
status: rawIssue.status,
|
|
285
|
+
created_at: rawIssue.created_at,
|
|
286
|
+
author_display_name: rawIssue.author_display_name,
|
|
287
|
+
action_items: actionItemsSummary,
|
|
288
|
+
} as IssueDetail;
|
|
289
|
+
} catch {
|
|
290
|
+
throw new Error(`Failed to parse issue response: ${data}`);
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
throw new Error(formatHttpError("Failed to fetch issue", response.status, data));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
299
296
|
|
|
300
|
-
|
|
301
|
-
|
|
297
|
+
export interface CreateIssueParams {
|
|
298
|
+
apiKey: string;
|
|
299
|
+
apiBaseUrl: string;
|
|
300
|
+
title: string;
|
|
301
|
+
orgId: number;
|
|
302
|
+
description?: string;
|
|
303
|
+
projectId?: number;
|
|
304
|
+
labels?: string[];
|
|
305
|
+
debug?: boolean;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export interface CreatedIssue {
|
|
309
|
+
id: string;
|
|
310
|
+
title: string;
|
|
311
|
+
description: string | null;
|
|
312
|
+
created_at: string;
|
|
313
|
+
status: number;
|
|
314
|
+
project_id: number | null;
|
|
315
|
+
labels: string[] | null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Create a new issue in the PostgresAI platform.
|
|
320
|
+
*
|
|
321
|
+
* @param params - The parameters for creating an issue
|
|
322
|
+
* @param params.apiKey - API key for authentication
|
|
323
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
324
|
+
* @param params.title - Issue title (required)
|
|
325
|
+
* @param params.orgId - Organization ID (required)
|
|
326
|
+
* @param params.description - Optional issue description
|
|
327
|
+
* @param params.projectId - Optional project ID to associate with
|
|
328
|
+
* @param params.labels - Optional array of label strings
|
|
329
|
+
* @param params.debug - Enable debug logging
|
|
330
|
+
* @returns The created issue object
|
|
331
|
+
* @throws Error if API key, title, or orgId is missing, or if the API call fails
|
|
332
|
+
*/
|
|
333
|
+
export async function createIssue(params: CreateIssueParams): Promise<CreatedIssue> {
|
|
334
|
+
const { apiKey, apiBaseUrl, title, orgId, description, projectId, labels, debug } = params;
|
|
335
|
+
if (!apiKey) {
|
|
336
|
+
throw new Error("API key is required");
|
|
337
|
+
}
|
|
338
|
+
if (!title) {
|
|
339
|
+
throw new Error("title is required");
|
|
340
|
+
}
|
|
341
|
+
if (typeof orgId !== "number") {
|
|
342
|
+
throw new Error("orgId is required");
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
346
|
+
const url = new URL(`${base}/rpc/issue_create`);
|
|
347
|
+
|
|
348
|
+
const bodyObj: Record<string, unknown> = {
|
|
349
|
+
title: title,
|
|
350
|
+
org_id: orgId,
|
|
351
|
+
};
|
|
352
|
+
if (description !== undefined) {
|
|
353
|
+
bodyObj.description = description;
|
|
354
|
+
}
|
|
355
|
+
if (projectId !== undefined) {
|
|
356
|
+
bodyObj.project_id = projectId;
|
|
357
|
+
}
|
|
358
|
+
if (labels && labels.length > 0) {
|
|
359
|
+
bodyObj.labels = labels;
|
|
360
|
+
}
|
|
361
|
+
const body = JSON.stringify(bodyObj);
|
|
362
|
+
|
|
363
|
+
const headers: Record<string, string> = {
|
|
364
|
+
"access-token": apiKey,
|
|
365
|
+
"Prefer": "return=representation",
|
|
366
|
+
"Content-Type": "application/json",
|
|
367
|
+
"Connection": "close",
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
if (debug) {
|
|
371
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
372
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
373
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
374
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
375
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
376
|
+
console.log(`Debug: Request body: ${body}`);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const response = await fetch(url.toString(), {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers,
|
|
382
|
+
body,
|
|
302
383
|
});
|
|
384
|
+
|
|
385
|
+
if (debug) {
|
|
386
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
387
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const data = await response.text();
|
|
391
|
+
|
|
392
|
+
if (response.ok) {
|
|
393
|
+
try {
|
|
394
|
+
return JSON.parse(data) as CreatedIssue;
|
|
395
|
+
} catch {
|
|
396
|
+
throw new Error(`Failed to parse create issue response: ${data}`);
|
|
397
|
+
}
|
|
398
|
+
} else {
|
|
399
|
+
throw new Error(formatHttpError("Failed to create issue", response.status, data));
|
|
400
|
+
}
|
|
303
401
|
}
|
|
304
402
|
|
|
305
403
|
export interface CreateIssueCommentParams {
|
|
@@ -339,67 +437,624 @@ export async function createIssueComment(params: CreateIssueCommentParams): Prom
|
|
|
339
437
|
"access-token": apiKey,
|
|
340
438
|
"Prefer": "return=representation",
|
|
341
439
|
"Content-Type": "application/json",
|
|
342
|
-
"
|
|
440
|
+
"Connection": "close",
|
|
343
441
|
};
|
|
344
442
|
|
|
345
443
|
if (debug) {
|
|
346
444
|
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
347
|
-
// eslint-disable-next-line no-console
|
|
348
445
|
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
349
|
-
// eslint-disable-next-line no-console
|
|
350
446
|
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
351
|
-
// eslint-disable-next-line no-console
|
|
352
447
|
console.log(`Debug: Auth scheme: access-token`);
|
|
353
|
-
// eslint-disable-next-line no-console
|
|
354
448
|
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
355
|
-
// eslint-disable-next-line no-console
|
|
356
449
|
console.log(`Debug: Request body: ${body}`);
|
|
357
450
|
}
|
|
358
451
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
452
|
+
const response = await fetch(url.toString(), {
|
|
453
|
+
method: "POST",
|
|
454
|
+
headers,
|
|
455
|
+
body,
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
if (debug) {
|
|
459
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
460
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const data = await response.text();
|
|
464
|
+
|
|
465
|
+
if (response.ok) {
|
|
466
|
+
try {
|
|
467
|
+
return JSON.parse(data) as IssueComment;
|
|
468
|
+
} catch {
|
|
469
|
+
throw new Error(`Failed to parse create comment response: ${data}`);
|
|
470
|
+
}
|
|
471
|
+
} else {
|
|
472
|
+
throw new Error(formatHttpError("Failed to create issue comment", response.status, data));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
export interface UpdateIssueParams {
|
|
477
|
+
apiKey: string;
|
|
478
|
+
apiBaseUrl: string;
|
|
479
|
+
issueId: string;
|
|
480
|
+
title?: string;
|
|
481
|
+
description?: string;
|
|
482
|
+
status?: number;
|
|
483
|
+
labels?: string[];
|
|
484
|
+
debug?: boolean;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export interface UpdatedIssue {
|
|
488
|
+
id: string;
|
|
489
|
+
title: string;
|
|
490
|
+
description: string | null;
|
|
491
|
+
status: number;
|
|
492
|
+
updated_at: string;
|
|
493
|
+
labels: string[] | null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Update an existing issue in the PostgresAI platform.
|
|
498
|
+
*
|
|
499
|
+
* @param params - The parameters for updating an issue
|
|
500
|
+
* @param params.apiKey - API key for authentication
|
|
501
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
502
|
+
* @param params.issueId - ID of the issue to update (required)
|
|
503
|
+
* @param params.title - New title (optional)
|
|
504
|
+
* @param params.description - New description (optional)
|
|
505
|
+
* @param params.status - New status: 0 = open, 1 = closed (optional)
|
|
506
|
+
* @param params.labels - New labels array (optional, replaces existing)
|
|
507
|
+
* @param params.debug - Enable debug logging
|
|
508
|
+
* @returns The updated issue object
|
|
509
|
+
* @throws Error if API key or issueId is missing, if no fields to update are provided, or if the API call fails
|
|
510
|
+
*/
|
|
511
|
+
export async function updateIssue(params: UpdateIssueParams): Promise<UpdatedIssue> {
|
|
512
|
+
const { apiKey, apiBaseUrl, issueId, title, description, status, labels, debug } = params;
|
|
513
|
+
if (!apiKey) {
|
|
514
|
+
throw new Error("API key is required");
|
|
515
|
+
}
|
|
516
|
+
if (!issueId) {
|
|
517
|
+
throw new Error("issueId is required");
|
|
518
|
+
}
|
|
519
|
+
if (title === undefined && description === undefined && status === undefined && labels === undefined) {
|
|
520
|
+
throw new Error("At least one field to update is required (title, description, status, or labels)");
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
524
|
+
const url = new URL(`${base}/rpc/issue_update`);
|
|
525
|
+
|
|
526
|
+
// Prod RPC expects p_* argument names (see OpenAPI at /api/general/).
|
|
527
|
+
const bodyObj: Record<string, unknown> = {
|
|
528
|
+
p_id: issueId,
|
|
529
|
+
};
|
|
530
|
+
if (title !== undefined) {
|
|
531
|
+
bodyObj.p_title = title;
|
|
532
|
+
}
|
|
533
|
+
if (description !== undefined) {
|
|
534
|
+
bodyObj.p_description = description;
|
|
535
|
+
}
|
|
536
|
+
if (status !== undefined) {
|
|
537
|
+
bodyObj.p_status = status;
|
|
538
|
+
}
|
|
539
|
+
if (labels !== undefined) {
|
|
540
|
+
bodyObj.p_labels = labels;
|
|
541
|
+
}
|
|
542
|
+
const body = JSON.stringify(bodyObj);
|
|
543
|
+
|
|
544
|
+
const headers: Record<string, string> = {
|
|
545
|
+
"access-token": apiKey,
|
|
546
|
+
"Prefer": "return=representation",
|
|
547
|
+
"Content-Type": "application/json",
|
|
548
|
+
"Connection": "close",
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
if (debug) {
|
|
552
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
553
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
554
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
555
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
556
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
557
|
+
console.log(`Debug: Request body: ${body}`);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const response = await fetch(url.toString(), {
|
|
561
|
+
method: "POST",
|
|
562
|
+
headers,
|
|
563
|
+
body,
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
if (debug) {
|
|
567
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
568
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const data = await response.text();
|
|
572
|
+
|
|
573
|
+
if (response.ok) {
|
|
574
|
+
try {
|
|
575
|
+
return JSON.parse(data) as UpdatedIssue;
|
|
576
|
+
} catch {
|
|
577
|
+
throw new Error(`Failed to parse update issue response: ${data}`);
|
|
578
|
+
}
|
|
579
|
+
} else {
|
|
580
|
+
throw new Error(formatHttpError("Failed to update issue", response.status, data));
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
export interface UpdateIssueCommentParams {
|
|
585
|
+
apiKey: string;
|
|
586
|
+
apiBaseUrl: string;
|
|
587
|
+
commentId: string;
|
|
588
|
+
content: string;
|
|
589
|
+
debug?: boolean;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
export interface UpdatedIssueComment {
|
|
593
|
+
id: string;
|
|
594
|
+
issue_id: string;
|
|
595
|
+
content: string;
|
|
596
|
+
updated_at: string;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Update an existing issue comment in the PostgresAI platform.
|
|
601
|
+
*
|
|
602
|
+
* @param params - The parameters for updating a comment
|
|
603
|
+
* @param params.apiKey - API key for authentication
|
|
604
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
605
|
+
* @param params.commentId - ID of the comment to update (required)
|
|
606
|
+
* @param params.content - New comment content (required)
|
|
607
|
+
* @param params.debug - Enable debug logging
|
|
608
|
+
* @returns The updated comment object
|
|
609
|
+
* @throws Error if API key, commentId, or content is missing, or if the API call fails
|
|
610
|
+
*/
|
|
611
|
+
export async function updateIssueComment(params: UpdateIssueCommentParams): Promise<UpdatedIssueComment> {
|
|
612
|
+
const { apiKey, apiBaseUrl, commentId, content, debug } = params;
|
|
613
|
+
if (!apiKey) {
|
|
614
|
+
throw new Error("API key is required");
|
|
615
|
+
}
|
|
616
|
+
if (!commentId) {
|
|
617
|
+
throw new Error("commentId is required");
|
|
618
|
+
}
|
|
619
|
+
if (!content) {
|
|
620
|
+
throw new Error("content is required");
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
624
|
+
const url = new URL(`${base}/rpc/issue_comment_update`);
|
|
625
|
+
|
|
626
|
+
const bodyObj: Record<string, unknown> = {
|
|
627
|
+
// Prod RPC expects p_* argument names (see OpenAPI at /api/general/).
|
|
628
|
+
p_id: commentId,
|
|
629
|
+
p_content: content,
|
|
630
|
+
};
|
|
631
|
+
const body = JSON.stringify(bodyObj);
|
|
632
|
+
|
|
633
|
+
const headers: Record<string, string> = {
|
|
634
|
+
"access-token": apiKey,
|
|
635
|
+
"Prefer": "return=representation",
|
|
636
|
+
"Content-Type": "application/json",
|
|
637
|
+
"Connection": "close",
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
if (debug) {
|
|
641
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
642
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
643
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
644
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
645
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
646
|
+
console.log(`Debug: Request body: ${body}`);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const response = await fetch(url.toString(), {
|
|
650
|
+
method: "POST",
|
|
651
|
+
headers,
|
|
652
|
+
body,
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
if (debug) {
|
|
656
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
657
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const data = await response.text();
|
|
661
|
+
|
|
662
|
+
if (response.ok) {
|
|
663
|
+
try {
|
|
664
|
+
return JSON.parse(data) as UpdatedIssueComment;
|
|
665
|
+
} catch {
|
|
666
|
+
throw new Error(`Failed to parse update comment response: ${data}`);
|
|
667
|
+
}
|
|
668
|
+
} else {
|
|
669
|
+
throw new Error(formatHttpError("Failed to update issue comment", response.status, data));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// ============================================================================
|
|
674
|
+
// Action Items API Functions
|
|
675
|
+
// ============================================================================
|
|
676
|
+
|
|
677
|
+
export interface FetchActionItemParams {
|
|
678
|
+
apiKey: string;
|
|
679
|
+
apiBaseUrl: string;
|
|
680
|
+
actionItemIds: string | string[];
|
|
681
|
+
debug?: boolean;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Fetch action item(s) by ID(s).
|
|
686
|
+
* Supports single ID or array of IDs.
|
|
687
|
+
*
|
|
688
|
+
* @param params - Fetch parameters
|
|
689
|
+
* @param params.apiKey - API authentication key
|
|
690
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
691
|
+
* @param params.actionItemIds - Single action item ID or array of IDs (UUIDs)
|
|
692
|
+
* @param params.debug - Enable debug logging
|
|
693
|
+
* @returns Array of action items matching the provided IDs
|
|
694
|
+
* @throws Error if API key is missing or no valid IDs provided
|
|
695
|
+
*
|
|
696
|
+
* @example
|
|
697
|
+
* // Fetch single action item
|
|
698
|
+
* const items = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds: "uuid-123" });
|
|
699
|
+
*
|
|
700
|
+
* @example
|
|
701
|
+
* // Fetch multiple action items
|
|
702
|
+
* const items = await fetchActionItem({ apiKey, apiBaseUrl, actionItemIds: ["uuid-1", "uuid-2"] });
|
|
703
|
+
*/
|
|
704
|
+
export async function fetchActionItem(params: FetchActionItemParams): Promise<IssueActionItem[]> {
|
|
705
|
+
const { apiKey, apiBaseUrl, actionItemIds, debug } = params;
|
|
706
|
+
if (!apiKey) {
|
|
707
|
+
throw new Error("API key is required");
|
|
708
|
+
}
|
|
709
|
+
// UUID format pattern for validation
|
|
710
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
711
|
+
// Normalize to array, filter out null/undefined, trim, and validate UUID format
|
|
712
|
+
const rawIds = Array.isArray(actionItemIds) ? actionItemIds : [actionItemIds];
|
|
713
|
+
const validIds = rawIds
|
|
714
|
+
.filter((id): id is string => id != null && typeof id === "string")
|
|
715
|
+
.map(id => id.trim())
|
|
716
|
+
.filter(id => id.length > 0 && uuidPattern.test(id));
|
|
717
|
+
if (validIds.length === 0) {
|
|
718
|
+
throw new Error("actionItemId is required and must be a valid UUID");
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
722
|
+
const url = new URL(`${base}/issue_action_items`);
|
|
723
|
+
if (validIds.length === 1) {
|
|
724
|
+
url.searchParams.set("id", `eq.${validIds[0]}`);
|
|
725
|
+
} else {
|
|
726
|
+
// PostgREST IN syntax: id=in.(val1,val2,val3)
|
|
727
|
+
url.searchParams.set("id", `in.(${validIds.join(",")})`)
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const headers: Record<string, string> = {
|
|
731
|
+
"access-token": apiKey,
|
|
732
|
+
"Prefer": "return=representation",
|
|
733
|
+
"Content-Type": "application/json",
|
|
734
|
+
"Connection": "close",
|
|
735
|
+
};
|
|
736
|
+
|
|
737
|
+
if (debug) {
|
|
738
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
739
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
740
|
+
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
741
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
742
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const response = await fetch(url.toString(), {
|
|
746
|
+
method: "GET",
|
|
747
|
+
headers,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
if (debug) {
|
|
751
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
752
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
const data = await response.text();
|
|
756
|
+
|
|
757
|
+
if (response.ok) {
|
|
758
|
+
try {
|
|
759
|
+
const parsed = JSON.parse(data);
|
|
760
|
+
if (Array.isArray(parsed)) {
|
|
761
|
+
return parsed as IssueActionItem[];
|
|
396
762
|
}
|
|
397
|
-
|
|
763
|
+
return parsed ? [parsed as IssueActionItem] : [];
|
|
764
|
+
} catch {
|
|
765
|
+
throw new Error(`Failed to parse action item response: ${data}`);
|
|
766
|
+
}
|
|
767
|
+
} else {
|
|
768
|
+
throw new Error(formatHttpError("Failed to fetch action item", response.status, data));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
export interface FetchActionItemsParams {
|
|
773
|
+
apiKey: string;
|
|
774
|
+
apiBaseUrl: string;
|
|
775
|
+
issueId: string;
|
|
776
|
+
debug?: boolean;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/**
|
|
780
|
+
* Fetch all action items for an issue.
|
|
781
|
+
*
|
|
782
|
+
* @param params - Fetch parameters
|
|
783
|
+
* @param params.apiKey - API authentication key
|
|
784
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
785
|
+
* @param params.issueId - Issue ID (UUID) to fetch action items for
|
|
786
|
+
* @param params.debug - Enable debug logging
|
|
787
|
+
* @returns Array of action items for the specified issue
|
|
788
|
+
* @throws Error if API key or issue ID is missing
|
|
789
|
+
*/
|
|
790
|
+
export async function fetchActionItems(params: FetchActionItemsParams): Promise<IssueActionItem[]> {
|
|
791
|
+
const { apiKey, apiBaseUrl, issueId, debug } = params;
|
|
792
|
+
if (!apiKey) {
|
|
793
|
+
throw new Error("API key is required");
|
|
794
|
+
}
|
|
795
|
+
if (!issueId) {
|
|
796
|
+
throw new Error("issueId is required");
|
|
797
|
+
}
|
|
798
|
+
// Validate UUID format to prevent PostgREST injection
|
|
799
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
800
|
+
if (!uuidPattern.test(issueId.trim())) {
|
|
801
|
+
throw new Error("issueId must be a valid UUID");
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
805
|
+
const url = new URL(`${base}/issue_action_items`);
|
|
806
|
+
url.searchParams.set("issue_id", `eq.${issueId.trim()}`);
|
|
807
|
+
|
|
808
|
+
const headers: Record<string, string> = {
|
|
809
|
+
"access-token": apiKey,
|
|
810
|
+
"Prefer": "return=representation",
|
|
811
|
+
"Content-Type": "application/json",
|
|
812
|
+
"Connection": "close",
|
|
813
|
+
};
|
|
814
|
+
|
|
815
|
+
if (debug) {
|
|
816
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
817
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
818
|
+
console.log(`Debug: GET URL: ${url.toString()}`);
|
|
819
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
820
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const response = await fetch(url.toString(), {
|
|
824
|
+
method: "GET",
|
|
825
|
+
headers,
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
if (debug) {
|
|
829
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
830
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const data = await response.text();
|
|
834
|
+
|
|
835
|
+
if (response.ok) {
|
|
836
|
+
try {
|
|
837
|
+
return JSON.parse(data) as IssueActionItem[];
|
|
838
|
+
} catch {
|
|
839
|
+
throw new Error(`Failed to parse action items response: ${data}`);
|
|
840
|
+
}
|
|
841
|
+
} else {
|
|
842
|
+
throw new Error(formatHttpError("Failed to fetch action items", response.status, data));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export interface CreateActionItemParams {
|
|
847
|
+
apiKey: string;
|
|
848
|
+
apiBaseUrl: string;
|
|
849
|
+
issueId: string;
|
|
850
|
+
title: string;
|
|
851
|
+
description?: string;
|
|
852
|
+
sqlAction?: string;
|
|
853
|
+
configs?: ConfigChange[];
|
|
854
|
+
debug?: boolean;
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Create a new action item for an issue.
|
|
859
|
+
*
|
|
860
|
+
* @param params - Creation parameters
|
|
861
|
+
* @param params.apiKey - API authentication key
|
|
862
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
863
|
+
* @param params.issueId - Issue ID (UUID) to create action item for
|
|
864
|
+
* @param params.title - Action item title
|
|
865
|
+
* @param params.description - Optional detailed description
|
|
866
|
+
* @param params.sqlAction - Optional SQL command to execute
|
|
867
|
+
* @param params.configs - Optional configuration parameter changes
|
|
868
|
+
* @param params.debug - Enable debug logging
|
|
869
|
+
* @returns Created action item ID
|
|
870
|
+
* @throws Error if required fields are missing or API call fails
|
|
871
|
+
*/
|
|
872
|
+
export async function createActionItem(params: CreateActionItemParams): Promise<string> {
|
|
873
|
+
const { apiKey, apiBaseUrl, issueId, title, description, sqlAction, configs, debug } = params;
|
|
874
|
+
if (!apiKey) {
|
|
875
|
+
throw new Error("API key is required");
|
|
876
|
+
}
|
|
877
|
+
if (!issueId) {
|
|
878
|
+
throw new Error("issueId is required");
|
|
879
|
+
}
|
|
880
|
+
// Validate UUID format
|
|
881
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
882
|
+
if (!uuidPattern.test(issueId.trim())) {
|
|
883
|
+
throw new Error("issueId must be a valid UUID");
|
|
884
|
+
}
|
|
885
|
+
if (!title) {
|
|
886
|
+
throw new Error("title is required");
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
890
|
+
const url = new URL(`${base}/rpc/issue_action_item_create`);
|
|
891
|
+
|
|
892
|
+
const bodyObj: Record<string, unknown> = {
|
|
893
|
+
issue_id: issueId,
|
|
894
|
+
title: title,
|
|
895
|
+
};
|
|
896
|
+
if (description !== undefined) {
|
|
897
|
+
bodyObj.description = description;
|
|
898
|
+
}
|
|
899
|
+
if (sqlAction !== undefined) {
|
|
900
|
+
bodyObj.sql_action = sqlAction;
|
|
901
|
+
}
|
|
902
|
+
if (configs !== undefined) {
|
|
903
|
+
bodyObj.configs = configs;
|
|
904
|
+
}
|
|
905
|
+
const body = JSON.stringify(bodyObj);
|
|
906
|
+
|
|
907
|
+
const headers: Record<string, string> = {
|
|
908
|
+
"access-token": apiKey,
|
|
909
|
+
"Prefer": "return=representation",
|
|
910
|
+
"Content-Type": "application/json",
|
|
911
|
+
"Connection": "close",
|
|
912
|
+
};
|
|
398
913
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
914
|
+
if (debug) {
|
|
915
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
916
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
917
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
918
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
919
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
920
|
+
console.log(`Debug: Request body: ${body}`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
const response = await fetch(url.toString(), {
|
|
924
|
+
method: "POST",
|
|
925
|
+
headers,
|
|
926
|
+
body,
|
|
402
927
|
});
|
|
928
|
+
|
|
929
|
+
if (debug) {
|
|
930
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
931
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
const data = await response.text();
|
|
935
|
+
|
|
936
|
+
if (response.ok) {
|
|
937
|
+
try {
|
|
938
|
+
return JSON.parse(data) as string;
|
|
939
|
+
} catch {
|
|
940
|
+
throw new Error(`Failed to parse create action item response: ${data}`);
|
|
941
|
+
}
|
|
942
|
+
} else {
|
|
943
|
+
throw new Error(formatHttpError("Failed to create action item", response.status, data));
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
export interface UpdateActionItemParams {
|
|
948
|
+
apiKey: string;
|
|
949
|
+
apiBaseUrl: string;
|
|
950
|
+
actionItemId: string;
|
|
951
|
+
title?: string;
|
|
952
|
+
description?: string;
|
|
953
|
+
isDone?: boolean;
|
|
954
|
+
status?: string;
|
|
955
|
+
statusReason?: string;
|
|
956
|
+
sqlAction?: string;
|
|
957
|
+
configs?: ConfigChange[];
|
|
958
|
+
debug?: boolean;
|
|
403
959
|
}
|
|
404
960
|
|
|
961
|
+
/**
|
|
962
|
+
* Update an existing action item.
|
|
963
|
+
*
|
|
964
|
+
* @param params - Update parameters
|
|
965
|
+
* @param params.apiKey - API authentication key
|
|
966
|
+
* @param params.apiBaseUrl - Base URL for the API
|
|
967
|
+
* @param params.actionItemId - Action item ID (UUID) to update
|
|
968
|
+
* @param params.title - New title
|
|
969
|
+
* @param params.description - New description
|
|
970
|
+
* @param params.isDone - Mark as done/not done
|
|
971
|
+
* @param params.status - Approval status: 'waiting_for_approval', 'approved', 'rejected'
|
|
972
|
+
* @param params.statusReason - Reason for status change
|
|
973
|
+
* @param params.sqlAction - SQL command to execute
|
|
974
|
+
* @param params.configs - Configuration parameter changes
|
|
975
|
+
* @param params.debug - Enable debug logging
|
|
976
|
+
* @throws Error if required fields missing or no update fields provided
|
|
977
|
+
*/
|
|
978
|
+
export async function updateActionItem(params: UpdateActionItemParams): Promise<void> {
|
|
979
|
+
const { apiKey, apiBaseUrl, actionItemId, title, description, isDone, status, statusReason, sqlAction, configs, debug } = params;
|
|
980
|
+
if (!apiKey) {
|
|
981
|
+
throw new Error("API key is required");
|
|
982
|
+
}
|
|
983
|
+
if (!actionItemId) {
|
|
984
|
+
throw new Error("actionItemId is required");
|
|
985
|
+
}
|
|
986
|
+
// Validate UUID format
|
|
987
|
+
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
988
|
+
if (!uuidPattern.test(actionItemId.trim())) {
|
|
989
|
+
throw new Error("actionItemId must be a valid UUID");
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// Check that at least one update field is provided
|
|
993
|
+
const hasUpdateField = title !== undefined || description !== undefined ||
|
|
994
|
+
isDone !== undefined || status !== undefined ||
|
|
995
|
+
statusReason !== undefined || sqlAction !== undefined || configs !== undefined;
|
|
996
|
+
if (!hasUpdateField) {
|
|
997
|
+
throw new Error("At least one field to update is required");
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const base = normalizeBaseUrl(apiBaseUrl);
|
|
1001
|
+
const url = new URL(`${base}/rpc/issue_action_item_update`);
|
|
1002
|
+
|
|
1003
|
+
const bodyObj: Record<string, unknown> = {
|
|
1004
|
+
action_item_id: actionItemId,
|
|
1005
|
+
};
|
|
1006
|
+
if (title !== undefined) {
|
|
1007
|
+
bodyObj.title = title;
|
|
1008
|
+
}
|
|
1009
|
+
if (description !== undefined) {
|
|
1010
|
+
bodyObj.description = description;
|
|
1011
|
+
}
|
|
1012
|
+
if (isDone !== undefined) {
|
|
1013
|
+
bodyObj.is_done = isDone;
|
|
1014
|
+
}
|
|
1015
|
+
if (status !== undefined) {
|
|
1016
|
+
bodyObj.status = status;
|
|
1017
|
+
}
|
|
1018
|
+
if (statusReason !== undefined) {
|
|
1019
|
+
bodyObj.status_reason = statusReason;
|
|
1020
|
+
}
|
|
1021
|
+
if (sqlAction !== undefined) {
|
|
1022
|
+
bodyObj.sql_action = sqlAction;
|
|
1023
|
+
}
|
|
1024
|
+
if (configs !== undefined) {
|
|
1025
|
+
bodyObj.configs = configs;
|
|
1026
|
+
}
|
|
1027
|
+
const body = JSON.stringify(bodyObj);
|
|
1028
|
+
|
|
1029
|
+
const headers: Record<string, string> = {
|
|
1030
|
+
"access-token": apiKey,
|
|
1031
|
+
"Prefer": "return=representation",
|
|
1032
|
+
"Content-Type": "application/json",
|
|
1033
|
+
"Connection": "close",
|
|
1034
|
+
};
|
|
405
1035
|
|
|
1036
|
+
if (debug) {
|
|
1037
|
+
const debugHeaders: Record<string, string> = { ...headers, "access-token": maskSecret(apiKey) };
|
|
1038
|
+
console.log(`Debug: Resolved API base URL: ${base}`);
|
|
1039
|
+
console.log(`Debug: POST URL: ${url.toString()}`);
|
|
1040
|
+
console.log(`Debug: Auth scheme: access-token`);
|
|
1041
|
+
console.log(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
1042
|
+
console.log(`Debug: Request body: ${body}`);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
const response = await fetch(url.toString(), {
|
|
1046
|
+
method: "POST",
|
|
1047
|
+
headers,
|
|
1048
|
+
body,
|
|
1049
|
+
});
|
|
1050
|
+
|
|
1051
|
+
if (debug) {
|
|
1052
|
+
console.log(`Debug: Response status: ${response.status}`);
|
|
1053
|
+
console.log(`Debug: Response headers: ${JSON.stringify(Object.fromEntries(response.headers.entries()))}`);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
if (!response.ok) {
|
|
1057
|
+
const data = await response.text();
|
|
1058
|
+
throw new Error(formatHttpError("Failed to update action item", response.status, data));
|
|
1059
|
+
}
|
|
1060
|
+
}
|