postgresai 0.14.0-dev.72 → 0.14.0-dev.74

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.
@@ -760,7 +760,7 @@ program
760
760
  // Fetch database URL for JSON output (non-blocking, best-effort)
761
761
  let databaseUrl: string | null = null;
762
762
  if (jsonOutput) {
763
- databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig);
763
+ databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
764
764
  }
765
765
 
766
766
  try {
@@ -13064,7 +13064,7 @@ var {
13064
13064
  // package.json
13065
13065
  var package_default = {
13066
13066
  name: "postgresai",
13067
- version: "0.14.0-dev.72",
13067
+ version: "0.14.0-dev.74",
13068
13068
  description: "postgres_ai CLI",
13069
13069
  license: "Apache-2.0",
13070
13070
  private: false,
@@ -15887,7 +15887,7 @@ var Result = import_lib.default.Result;
15887
15887
  var TypeOverrides = import_lib.default.TypeOverrides;
15888
15888
  var defaults = import_lib.default.defaults;
15889
15889
  // package.json
15890
- var version = "0.14.0-dev.72";
15890
+ var version = "0.14.0-dev.74";
15891
15891
  var package_default2 = {
15892
15892
  name: "postgresai",
15893
15893
  version,
@@ -25312,8 +25312,12 @@ class SupabaseClient {
25312
25312
  return err;
25313
25313
  }
25314
25314
  }
25315
- async function fetchPoolerDatabaseUrl(config2) {
25315
+ async function fetchPoolerDatabaseUrl(config2, username) {
25316
25316
  const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(config2.projectRef)}/config/database/pooler`;
25317
+ const effectiveUsername = (() => {
25318
+ const suffix = `.${config2.projectRef}`;
25319
+ return username.endsWith(suffix) ? username : `${username}${suffix}`;
25320
+ })();
25317
25321
  try {
25318
25322
  const response = await fetch(url, {
25319
25323
  method: "GET",
@@ -25327,13 +25331,14 @@ async function fetchPoolerDatabaseUrl(config2) {
25327
25331
  const data = await response.json();
25328
25332
  if (Array.isArray(data) && data.length > 0) {
25329
25333
  const pooler = data[0];
25330
- if (pooler.db_host && pooler.db_port && pooler.db_name && pooler.db_user) {
25331
- return `postgresql://${pooler.db_user}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
25334
+ if (pooler.db_host && pooler.db_port && pooler.db_name) {
25335
+ return `postgresql://${effectiveUsername}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
25332
25336
  }
25333
25337
  if (typeof pooler.connection_string === "string") {
25334
25338
  try {
25335
25339
  const connUrl = new URL(pooler.connection_string);
25336
- return `postgresql://${connUrl.username}@${connUrl.hostname}:${connUrl.port}${connUrl.pathname}`;
25340
+ const portPart = connUrl.port ? `:${connUrl.port}` : "";
25341
+ return `postgresql://${effectiveUsername}@${connUrl.hostname}${portPart}${connUrl.pathname}`;
25337
25342
  } catch {
25338
25343
  return null;
25339
25344
  }
@@ -28004,7 +28009,7 @@ program2.command("prepare-db [conn]").description("prepare database for monitori
28004
28009
  const supabaseClient = new SupabaseClient(supabaseConfig);
28005
28010
  let databaseUrl = null;
28006
28011
  if (jsonOutput) {
28007
- databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig);
28012
+ databaseUrl = await fetchPoolerDatabaseUrl(supabaseConfig, opts.monitoringUser);
28008
28013
  }
28009
28014
  try {
28010
28015
  const database = await supabaseClient.getCurrentDatabase();
@@ -1,6 +1,6 @@
1
1
  // AUTO-GENERATED FILE - DO NOT EDIT
2
2
  // Generated from config/pgwatch-prometheus/metrics.yml by scripts/embed-metrics.ts
3
- // Generated at: 2026-01-09T15:26:19.729Z
3
+ // Generated at: 2026-01-09T18:10:39.847Z
4
4
 
5
5
  /**
6
6
  * Metric definition from metrics.yml
package/lib/supabase.ts CHANGED
@@ -335,16 +335,26 @@ export class SupabaseClient {
335
335
 
336
336
  /**
337
337
  * Fetch the database pooler connection string from Supabase Management API.
338
- * Returns a postgresql:// URL with username but no password.
338
+ * Returns a postgresql:// URL with the specified username but no password.
339
339
  *
340
340
  * @param config Supabase configuration with projectRef and accessToken
341
- * @returns Database URL without password (e.g., "postgresql://postgres.ref@host:port/postgres")
341
+ * @param username Username to include in the URL (e.g., monitoring user)
342
+ * @returns Database URL without password (e.g., "postgresql://user@host:port/postgres")
342
343
  */
343
344
  export async function fetchPoolerDatabaseUrl(
344
- config: SupabaseConfig
345
+ config: SupabaseConfig,
346
+ username: string
345
347
  ): Promise<string | null> {
346
348
  const url = `${SUPABASE_API_BASE}/v1/projects/${encodeURIComponent(config.projectRef)}/config/database/pooler`;
347
349
 
350
+ // For Supabase pooler connections, the username must include the project ref:
351
+ // <user>.<project_ref>
352
+ // Example:
353
+ // postgresql://postgres_ai_mon.xhaqmsvczjkkvkgdyast@aws-1-eu-west-1.pooler.supabase.com:6543/postgres
354
+ const effectiveUsername = (() => {
355
+ const suffix = `.${config.projectRef}`;
356
+ return username.endsWith(suffix) ? username : `${username}${suffix}`;
357
+ })();
348
358
  try {
349
359
  const response = await fetch(url, {
350
360
  method: "GET",
@@ -364,15 +374,16 @@ export async function fetchPoolerDatabaseUrl(
364
374
  if (Array.isArray(data) && data.length > 0) {
365
375
  const pooler = data[0];
366
376
  // Build URL from components if available
367
- if (pooler.db_host && pooler.db_port && pooler.db_name && pooler.db_user) {
368
- return `postgresql://${pooler.db_user}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
377
+ if (pooler.db_host && pooler.db_port && pooler.db_name) {
378
+ return `postgresql://${effectiveUsername}@${pooler.db_host}:${pooler.db_port}/${pooler.db_name}`;
369
379
  }
370
380
  // Fallback: try to extract from connection_string if present
371
381
  if (typeof pooler.connection_string === "string") {
372
382
  try {
373
383
  const connUrl = new URL(pooler.connection_string);
374
- // Remove password from URL
375
- return `postgresql://${connUrl.username}@${connUrl.hostname}:${connUrl.port}${connUrl.pathname}`;
384
+ // Use provided username; handle empty port for default ports (e.g., 5432)
385
+ const portPart = connUrl.port ? `:${connUrl.port}` : "";
386
+ return `postgresql://${effectiveUsername}@${connUrl.hostname}${portPart}${connUrl.pathname}`;
376
387
  } catch {
377
388
  return null;
378
389
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "postgresai",
3
- "version": "0.14.0-dev.72",
3
+ "version": "0.14.0-dev.74",
4
4
  "description": "postgres_ai CLI",
5
5
  "license": "Apache-2.0",
6
6
  "private": false,
@@ -2,6 +2,7 @@ import { describe, expect, test, beforeEach, afterEach, mock } from "bun:test";
2
2
  import {
3
3
  resolveSupabaseConfig,
4
4
  extractProjectRefFromUrl,
5
+ fetchPoolerDatabaseUrl,
5
6
  SupabaseClient,
6
7
  applyInitPlanViaSupabase,
7
8
  verifyInitSetupViaSupabase,
@@ -137,6 +138,64 @@ describe("Supabase module", () => {
137
138
  });
138
139
  });
139
140
 
141
+ describe("fetchPoolerDatabaseUrl", () => {
142
+ const originalFetch = globalThis.fetch;
143
+
144
+ afterEach(() => {
145
+ globalThis.fetch = originalFetch;
146
+ });
147
+
148
+ test("returns pooler db url with username including project ref (db_host/db_port/db_name response)", async () => {
149
+ globalThis.fetch = mock(() =>
150
+ Promise.resolve(
151
+ new Response(
152
+ JSON.stringify([
153
+ {
154
+ db_host: "aws-1-eu-west-1.pooler.supabase.com",
155
+ db_port: 6543,
156
+ db_name: "postgres",
157
+ },
158
+ ]),
159
+ { status: 200 }
160
+ )
161
+ )
162
+ ) as unknown as typeof fetch;
163
+
164
+ const url = await fetchPoolerDatabaseUrl(
165
+ { projectRef: "xhaqmsvczjkkvkgdyast", accessToken: "token" },
166
+ "postgres_ai_mon"
167
+ );
168
+ expect(url).toBe(
169
+ "postgresql://postgres_ai_mon.xhaqmsvczjkkvkgdyast@aws-1-eu-west-1.pooler.supabase.com:6543/postgres"
170
+ );
171
+ });
172
+
173
+ test("does not double-append project ref if username already has it", async () => {
174
+ globalThis.fetch = mock(() =>
175
+ Promise.resolve(
176
+ new Response(
177
+ JSON.stringify([
178
+ {
179
+ db_host: "aws-1-eu-west-1.pooler.supabase.com",
180
+ db_port: 6543,
181
+ db_name: "postgres",
182
+ },
183
+ ]),
184
+ { status: 200 }
185
+ )
186
+ )
187
+ ) as unknown as typeof fetch;
188
+
189
+ const url = await fetchPoolerDatabaseUrl(
190
+ { projectRef: "xhaqmsvczjkkvkgdyast", accessToken: "token" },
191
+ "postgres_ai_mon.xhaqmsvczjkkvkgdyast"
192
+ );
193
+ expect(url).toBe(
194
+ "postgresql://postgres_ai_mon.xhaqmsvczjkkvkgdyast@aws-1-eu-west-1.pooler.supabase.com:6543/postgres"
195
+ );
196
+ });
197
+ });
198
+
140
199
  describe("SupabaseClient", () => {
141
200
  test("throws error when project ref is empty", () => {
142
201
  expect(() => new SupabaseClient({ projectRef: "", accessToken: "token" })).toThrow(