postgresai 0.16.0-dev.3 → 0.16.0-dev.4
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/dist/bin/postgres-ai.js +700 -740
- package/lib/dblab.ts +30 -59
- package/lib/joe.ts +9 -0
- package/package.json +1 -1
- package/test/dblab.cli.test.ts +10 -10
- package/test/dblab.test.ts +38 -31
- package/test/joe.test.ts +4 -4
package/lib/dblab.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { formatHttpError, maskSecret, normalizeBaseUrl } from "./util";
|
|
27
|
+
import { listProjects, type ProjectListItem } from "./joe";
|
|
27
28
|
|
|
28
29
|
// ---------------------------------------------------------------------------
|
|
29
30
|
// Types
|
|
@@ -51,14 +52,12 @@ interface DblabCommon {
|
|
|
51
52
|
// ---------------------------------------------------------------------------
|
|
52
53
|
// Project → DBLab instance resolution
|
|
53
54
|
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// resolver + per-project instance surfacing land, collapse this into that shared
|
|
61
|
-
// path so `--project` id/alias resolution lives in exactly one place.
|
|
55
|
+
// Resolution rides the org-level projects listing rpc (`v1.projects_list`,
|
|
56
|
+
// S.1): its rows carry each project's `dblab_instance_id` (DB.4), and the rpc
|
|
57
|
+
// authenticates with the CLI's opaque org `access-token` header. The previous
|
|
58
|
+
// interim resolver read the `v1.dblab_instances` PostgREST view, which is
|
|
59
|
+
// JWT-scoped — permanently empty/403 for a token-only caller — so it never
|
|
60
|
+
// worked outside mocks and was dropped at wiring (dedupe note resolved).
|
|
62
61
|
// ---------------------------------------------------------------------------
|
|
63
62
|
|
|
64
63
|
/** A bare numeric `--project` value is a project id; anything else is an alias/name. */
|
|
@@ -71,23 +70,21 @@ export interface ResolveDblabInstanceParams {
|
|
|
71
70
|
apiBaseUrl: string;
|
|
72
71
|
/** `--project <id|alias>` — a numeric project id, or a project alias/name. */
|
|
73
72
|
project: string;
|
|
74
|
-
/** Optional org scope (narrows the
|
|
73
|
+
/** Optional org scope (narrows the projects listing). */
|
|
75
74
|
orgId?: number;
|
|
76
75
|
debug?: boolean;
|
|
77
76
|
}
|
|
78
77
|
|
|
79
78
|
/**
|
|
80
|
-
* Resolve `--project <id|alias>` to
|
|
79
|
+
* Resolve `--project <id|alias>` to the project's single DBLab `instance_id`.
|
|
81
80
|
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* `
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* number-precision loss.
|
|
81
|
+
* Lists the org's projects via `v1.projects_list` (the same call behind
|
|
82
|
+
* `pgai projects`): a numeric ref matches `project_id`; anything else matches
|
|
83
|
+
* `alias` / `name` / `label` (case-insensitive). Each project has at most one
|
|
84
|
+
* active DBLab instance (`dblab_instance_id`). The returned id is a string so
|
|
85
|
+
* a 64-bit id survives without JS number-precision loss.
|
|
88
86
|
*/
|
|
89
87
|
export async function resolveDblabInstanceId(params: ResolveDblabInstanceParams): Promise<string> {
|
|
90
|
-
// TODO(joe-v2): unify --project resolution once projects_list (S.1) returns the dblab instance id
|
|
91
88
|
const { apiKey, apiBaseUrl, orgId, debug } = params;
|
|
92
89
|
if (!apiKey) {
|
|
93
90
|
throw new Error("API key is required");
|
|
@@ -97,55 +94,24 @@ export async function resolveDblabInstanceId(params: ResolveDblabInstanceParams)
|
|
|
97
94
|
throw new Error("project is required (--project <id|alias>)");
|
|
98
95
|
}
|
|
99
96
|
|
|
100
|
-
|
|
101
|
-
const url = new URL(`${base}/dblab_instances`);
|
|
102
|
-
url.searchParams.set("select", "id,project_id,project_name,project_alias,project_label_or_name,org_id");
|
|
103
|
-
if (typeof orgId === "number") {
|
|
104
|
-
url.searchParams.set("org_id", `eq.${orgId}`);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const headers: Record<string, string> = {
|
|
108
|
-
"access-token": apiKey,
|
|
109
|
-
"Content-Type": "application/json",
|
|
110
|
-
"Connection": "close",
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
if (debug) {
|
|
114
|
-
const debugHeaders = { ...headers, "access-token": maskSecret(apiKey) };
|
|
115
|
-
console.error(`Debug: GET URL: ${url.toString()}`);
|
|
116
|
-
console.error(`Debug: Request headers: ${JSON.stringify(debugHeaders)}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const response = await fetch(url.toString(), { method: "GET", headers });
|
|
120
|
-
const text = await response.text();
|
|
121
|
-
|
|
122
|
-
if (debug) {
|
|
123
|
-
console.error(`Debug: Response status: ${response.status}`);
|
|
124
|
-
console.error(`Debug: Response body: ${text}`);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (!response.ok) {
|
|
128
|
-
throw new Error(formatHttpError("Failed to resolve project's DBLab instance", response.status, text));
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
let rows: DblabInstanceRow[];
|
|
97
|
+
let projects: ProjectListItem[];
|
|
132
98
|
try {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
throw new Error(`Failed to
|
|
99
|
+
projects = await listProjects({ apiKey, apiBaseUrl, orgId, debug });
|
|
100
|
+
} catch (err) {
|
|
101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
102
|
+
throw new Error(`Failed to resolve project's DBLab instance: ${message}`);
|
|
137
103
|
}
|
|
138
104
|
|
|
139
105
|
const numeric = isNumericProjectRef(ref);
|
|
140
106
|
const needle = ref.toLowerCase();
|
|
141
|
-
const match =
|
|
107
|
+
const match = projects.find((p) => {
|
|
142
108
|
if (numeric) {
|
|
143
|
-
return String(
|
|
109
|
+
return String(p.project_id) === ref;
|
|
144
110
|
}
|
|
145
111
|
return (
|
|
146
|
-
(
|
|
147
|
-
(
|
|
148
|
-
(
|
|
112
|
+
(p.alias !== null && p.alias.toLowerCase() === needle) ||
|
|
113
|
+
(p.name !== null && p.name.toLowerCase() === needle) ||
|
|
114
|
+
(p.label !== null && p.label.toLowerCase() === needle)
|
|
149
115
|
);
|
|
150
116
|
});
|
|
151
117
|
|
|
@@ -154,7 +120,12 @@ export async function resolveDblabInstanceId(params: ResolveDblabInstanceParams)
|
|
|
154
120
|
`No DBLab instance found for project '${ref}'. Run 'pgai projects' to see available projects.`
|
|
155
121
|
);
|
|
156
122
|
}
|
|
157
|
-
|
|
123
|
+
if (match.dblab_instance_id == null) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Project '${ref}' has no active DBLab instance. Register a DBLab instance for it in the Console first.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return String(match.dblab_instance_id);
|
|
158
129
|
}
|
|
159
130
|
|
|
160
131
|
// ---------------------------------------------------------------------------
|
package/lib/joe.ts
CHANGED
|
@@ -119,6 +119,11 @@ export interface ProjectListItem {
|
|
|
119
119
|
joe_ready: boolean;
|
|
120
120
|
/** Whether the project's DBLab tunnel is connected. */
|
|
121
121
|
tunnel: boolean;
|
|
122
|
+
/** The project's active JOE instance id (the M1a submit/status/result target). */
|
|
123
|
+
instance_id: number | null;
|
|
124
|
+
/** The project's active DBLAB instance id — the SPEC §8 DBLab-companion resolver
|
|
125
|
+
* passes this to `v1.dblab_api_call` (distinct from the Joe `instance_id`). */
|
|
126
|
+
dblab_instance_id: number | null;
|
|
122
127
|
}
|
|
123
128
|
|
|
124
129
|
// ---------------------------------------------------------------------------
|
|
@@ -285,6 +290,8 @@ interface RawProjectRow {
|
|
|
285
290
|
joe_api_v2_enabled?: boolean;
|
|
286
291
|
tunnel?: boolean;
|
|
287
292
|
tunnel_ready?: boolean;
|
|
293
|
+
instance_id?: number | null;
|
|
294
|
+
dblab_instance_id?: number | null;
|
|
288
295
|
}
|
|
289
296
|
|
|
290
297
|
function normalizeProjectRow(row: RawProjectRow): ProjectListItem {
|
|
@@ -296,6 +303,8 @@ function normalizeProjectRow(row: RawProjectRow): ProjectListItem {
|
|
|
296
303
|
label: row.label ?? null,
|
|
297
304
|
joe_ready: Boolean(row.joe_ready ?? row.joe_api_v2_enabled ?? false),
|
|
298
305
|
tunnel: Boolean(row.tunnel ?? row.tunnel_ready ?? false),
|
|
306
|
+
instance_id: row.instance_id == null ? null : Number(row.instance_id),
|
|
307
|
+
dblab_instance_id: row.dblab_instance_id == null ? null : Number(row.dblab_instance_id),
|
|
299
308
|
};
|
|
300
309
|
}
|
|
301
310
|
|
package/package.json
CHANGED
package/test/dblab.cli.test.ts
CHANGED
|
@@ -48,14 +48,14 @@ interface Recorded {
|
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
|
-
* Fake Platform API: serves the `/
|
|
52
|
-
*
|
|
53
|
-
* payload the CLI produced.
|
|
51
|
+
* Fake Platform API: serves the `/rpc/projects_list` resolver listing (rows
|
|
52
|
+
* carry `dblab_instance_id`, S.1/DB.4) and echoes every `/rpc/dblab_api_call`
|
|
53
|
+
* proxy call so tests can assert the action/method/payload the CLI produced.
|
|
54
54
|
*/
|
|
55
|
-
async function startFakeApi(
|
|
55
|
+
async function startFakeApi(projects?: unknown[]) {
|
|
56
56
|
const requests: Recorded[] = [];
|
|
57
|
-
const rows =
|
|
58
|
-
{
|
|
57
|
+
const rows = projects ?? [
|
|
58
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: false, instance_id: 1, dblab_instance_id: 7 },
|
|
59
59
|
];
|
|
60
60
|
|
|
61
61
|
const server = Bun.serve({
|
|
@@ -68,7 +68,7 @@ async function startFakeApi(instances?: unknown[]) {
|
|
|
68
68
|
try { bodyJson = bodyText ? JSON.parse(bodyText) : null; } catch { bodyJson = null; }
|
|
69
69
|
requests.push({ method: req.method, pathname: url.pathname, search: url.search, bodyJson });
|
|
70
70
|
|
|
71
|
-
if (req.method === "
|
|
71
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/projects_list")) {
|
|
72
72
|
return new Response(JSON.stringify(rows), { status: 200, headers: { "Content-Type": "application/json" } });
|
|
73
73
|
}
|
|
74
74
|
if (req.method === "POST" && url.pathname.endsWith("/rpc/dblab_api_call")) {
|
|
@@ -88,7 +88,7 @@ async function startFakeApi(instances?: unknown[]) {
|
|
|
88
88
|
baseUrl,
|
|
89
89
|
requests,
|
|
90
90
|
proxyCalls: () => requests.filter((r) => r.pathname.endsWith("/rpc/dblab_api_call")),
|
|
91
|
-
resolverCalls: () => requests.filter((r) => r.pathname.endsWith("/
|
|
91
|
+
resolverCalls: () => requests.filter((r) => r.pathname.endsWith("/rpc/projects_list")),
|
|
92
92
|
stop: () => server.stop(true),
|
|
93
93
|
};
|
|
94
94
|
}
|
|
@@ -318,8 +318,8 @@ describe("CLI DBLab companion command groups", () => {
|
|
|
318
318
|
port: 0,
|
|
319
319
|
async fetch(req) {
|
|
320
320
|
const url = new URL(req.url);
|
|
321
|
-
if (req.method === "
|
|
322
|
-
return new Response(JSON.stringify([{
|
|
321
|
+
if (req.method === "POST" && url.pathname.endsWith("/rpc/projects_list")) {
|
|
322
|
+
return new Response(JSON.stringify([{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }]), { status: 200 });
|
|
323
323
|
}
|
|
324
324
|
return new Response('{"message":"insufficient scope: joe:admin required"}', { status: 403 });
|
|
325
325
|
},
|
package/test/dblab.test.ts
CHANGED
|
@@ -40,13 +40,13 @@ function stubFetch(body: unknown, status = 200): { get: () => Captured | null }
|
|
|
40
40
|
return { get: () => captured };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
/** Route fetch by URL: `/
|
|
44
|
-
function routeFetch(
|
|
43
|
+
/** Route fetch by URL: `/rpc/projects_list` → projects, `/rpc/dblab_api_call` → reply. */
|
|
44
|
+
function routeFetch(projects: unknown[], reply: unknown): { calls: Captured[] } {
|
|
45
45
|
const calls: Captured[] = [];
|
|
46
46
|
globalThis.fetch = mock((url: string, options: RequestInit) => {
|
|
47
47
|
calls.push({ url, options });
|
|
48
|
-
if (String(url).includes("/
|
|
49
|
-
return Promise.resolve(new Response(JSON.stringify(
|
|
48
|
+
if (String(url).includes("/rpc/projects_list")) {
|
|
49
|
+
return Promise.resolve(new Response(JSON.stringify(projects), { status: 200 }));
|
|
50
50
|
}
|
|
51
51
|
return Promise.resolve(new Response(JSON.stringify(reply), { status: 200 }));
|
|
52
52
|
}) as unknown as typeof fetch;
|
|
@@ -66,9 +66,10 @@ afterEach(() => {
|
|
|
66
66
|
// ---------------------------------------------------------------------------
|
|
67
67
|
|
|
68
68
|
describe("resolveDblabInstanceId", () => {
|
|
69
|
-
const
|
|
70
|
-
{
|
|
71
|
-
{
|
|
69
|
+
const PROJECTS = [
|
|
70
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: false, instance_id: 1, dblab_instance_id: 7 },
|
|
71
|
+
{ project_id: 34, alias: "analytics", name: "Analytics", label: null, joe_ready: false, tunnel: false, instance_id: null, dblab_instance_id: 9 },
|
|
72
|
+
{ project_id: 56, alias: "no-dblab", name: "No DBLab", label: null, joe_ready: false, tunnel: false, instance_id: null, dblab_instance_id: null },
|
|
72
73
|
];
|
|
73
74
|
|
|
74
75
|
test("throws when apiKey is missing", async () => {
|
|
@@ -89,42 +90,48 @@ describe("resolveDblabInstanceId", () => {
|
|
|
89
90
|
expect(isNumericProjectRef("main-db")).toBe(false);
|
|
90
91
|
});
|
|
91
92
|
|
|
92
|
-
test("resolves a numeric project id to its instance id (string)", async () => {
|
|
93
|
-
const cap = stubFetch(
|
|
93
|
+
test("resolves a numeric project id to its dblab instance id (string) via projects_list", async () => {
|
|
94
|
+
const cap = stubFetch(PROJECTS);
|
|
94
95
|
const id = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "34" });
|
|
95
96
|
expect(id).toBe("9");
|
|
96
97
|
const c = cap.get()!;
|
|
97
|
-
expect(c.url).
|
|
98
|
-
expect(c.
|
|
99
|
-
expect(c.options.method).toBe("GET");
|
|
98
|
+
expect(c.url).toBe(`${API}/rpc/projects_list`);
|
|
99
|
+
expect(c.options.method).toBe("POST");
|
|
100
100
|
expect((c.options.headers as Record<string, string>)["access-token"]).toBe("k");
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test("resolves an alias (case-insensitive) to its instance id", async () => {
|
|
104
|
-
stubFetch(
|
|
103
|
+
test("resolves an alias (case-insensitive) to its dblab instance id", async () => {
|
|
104
|
+
stubFetch(PROJECTS);
|
|
105
105
|
const id = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "Main-DB" });
|
|
106
106
|
expect(id).toBe("7");
|
|
107
107
|
});
|
|
108
108
|
|
|
109
|
-
test("resolves a project name to its instance id", async () => {
|
|
110
|
-
stubFetch(
|
|
109
|
+
test("resolves a project name to its dblab instance id", async () => {
|
|
110
|
+
stubFetch(PROJECTS);
|
|
111
111
|
const id = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "analytics" });
|
|
112
112
|
expect(id).toBe("9");
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
test("
|
|
116
|
-
const cap = stubFetch(
|
|
115
|
+
test("passes org_id in the rpc body when orgId is provided", async () => {
|
|
116
|
+
const cap = stubFetch(PROJECTS);
|
|
117
117
|
await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "12", orgId: 5 });
|
|
118
|
-
expect(
|
|
118
|
+
expect(bodyOf(cap.get()).org_id).toBe(5);
|
|
119
119
|
});
|
|
120
120
|
|
|
121
|
-
test("throws a helpful error when no
|
|
122
|
-
stubFetch(
|
|
121
|
+
test("throws a helpful error when no project matches", async () => {
|
|
122
|
+
stubFetch(PROJECTS);
|
|
123
123
|
await expect(
|
|
124
124
|
resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "nope" })
|
|
125
125
|
).rejects.toThrow(/No DBLab instance found for project 'nope'/);
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test("throws when the project exists but has no active DBLab instance", async () => {
|
|
129
|
+
stubFetch(PROJECTS);
|
|
130
|
+
await expect(
|
|
131
|
+
resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "no-dblab" })
|
|
132
|
+
).rejects.toThrow(/has no active DBLab instance/);
|
|
133
|
+
});
|
|
134
|
+
|
|
128
135
|
test("surfaces an HTTP error from the listing", async () => {
|
|
129
136
|
stubFetch('{"message":"boom"}', 500);
|
|
130
137
|
await expect(
|
|
@@ -330,12 +337,12 @@ describe("proxy error paths", () => {
|
|
|
330
337
|
|
|
331
338
|
describe("project → instance → dblab_api_call", () => {
|
|
332
339
|
test("a verb driven off a resolved instance sends both requests", async () => {
|
|
333
|
-
const
|
|
334
|
-
const { calls } = routeFetch(
|
|
340
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
341
|
+
const { calls } = routeFetch(projects, { id: "c-1", status: "ready" });
|
|
335
342
|
const instanceId = await resolveDblabInstanceId({ apiKey: "k", apiBaseUrl: API, project: "main-db" });
|
|
336
343
|
await createClone({ apiKey: "k", apiBaseUrl: API, instanceId });
|
|
337
344
|
expect(calls.length).toBe(2);
|
|
338
|
-
expect(calls[0].url).toContain("/
|
|
345
|
+
expect(calls[0].url).toContain("/rpc/projects_list");
|
|
339
346
|
expect(calls[1].url).toBe(`${API}/rpc/dblab_api_call`);
|
|
340
347
|
expect(bodyOf(calls[1]).instance_id).toBe("7");
|
|
341
348
|
});
|
|
@@ -349,14 +356,14 @@ describe("MCP DBLab tools (handleToolCall)", () => {
|
|
|
349
356
|
const rootOpts = { apiKey: "k", apiBaseUrl: API };
|
|
350
357
|
|
|
351
358
|
test("clone_create resolves project then proxies /clone POST", async () => {
|
|
352
|
-
const
|
|
353
|
-
const { calls } = routeFetch(
|
|
359
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
360
|
+
const { calls } = routeFetch(projects, { ok: true });
|
|
354
361
|
const res = await handleToolCall(
|
|
355
362
|
{ params: { name: "clone_create", arguments: { project: "main-db" } } },
|
|
356
363
|
rootOpts
|
|
357
364
|
);
|
|
358
365
|
expect(res.isError).toBeFalsy();
|
|
359
|
-
expect(calls[0].url).toContain("/
|
|
366
|
+
expect(calls[0].url).toContain("/rpc/projects_list");
|
|
360
367
|
const b = bodyOf(calls[1]);
|
|
361
368
|
expect(b.instance_id).toBe("7");
|
|
362
369
|
expect(b.action).toBe("/clone");
|
|
@@ -364,8 +371,8 @@ describe("MCP DBLab tools (handleToolCall)", () => {
|
|
|
364
371
|
});
|
|
365
372
|
|
|
366
373
|
test("clone_destroy proxies /clone/<id> DELETE", async () => {
|
|
367
|
-
const
|
|
368
|
-
const { calls } = routeFetch(
|
|
374
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
375
|
+
const { calls } = routeFetch(projects, { ok: true });
|
|
369
376
|
const res = await handleToolCall(
|
|
370
377
|
{ params: { name: "clone_destroy", arguments: { project: "main-db", clone_id: "c-1" } } },
|
|
371
378
|
rootOpts
|
|
@@ -377,8 +384,8 @@ describe("MCP DBLab tools (handleToolCall)", () => {
|
|
|
377
384
|
});
|
|
378
385
|
|
|
379
386
|
test("snapshot_create proxies /branch/snapshot POST with cloneID", async () => {
|
|
380
|
-
const
|
|
381
|
-
const { calls } = routeFetch(
|
|
387
|
+
const projects = [{ project_id: 12, alias: "main-db", dblab_instance_id: 7 }];
|
|
388
|
+
const { calls } = routeFetch(projects, { ok: true });
|
|
382
389
|
await handleToolCall(
|
|
383
390
|
{ params: { name: "snapshot_create", arguments: { project: "main-db", clone_id: "c-1", message: "m" } } },
|
|
384
391
|
rootOpts
|
package/test/joe.test.ts
CHANGED
|
@@ -176,14 +176,14 @@ describe("listProjects", () => {
|
|
|
176
176
|
installFetch({
|
|
177
177
|
projects_list: () =>
|
|
178
178
|
json([
|
|
179
|
-
{ id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_api_v2_enabled: true, tunnel_ready: true },
|
|
179
|
+
{ id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_api_v2_enabled: true, tunnel_ready: true, instance_id: 3, dblab_instance_id: 7 },
|
|
180
180
|
{ project_id: 13, alias: "dw", name: "Warehouse", joe_ready: false, tunnel: false },
|
|
181
181
|
]),
|
|
182
182
|
});
|
|
183
183
|
const projects = await listProjects({ apiKey: "k", apiBaseUrl: BASE });
|
|
184
184
|
expect(projects).toEqual([
|
|
185
|
-
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: true },
|
|
186
|
-
{ project_id: 13, alias: "dw", name: "Warehouse", label: null, joe_ready: false, tunnel: false },
|
|
185
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: "Main DB", joe_ready: true, tunnel: true, instance_id: 3, dblab_instance_id: 7 },
|
|
186
|
+
{ project_id: 13, alias: "dw", name: "Warehouse", label: null, joe_ready: false, tunnel: false, instance_id: null, dblab_instance_id: null },
|
|
187
187
|
]);
|
|
188
188
|
});
|
|
189
189
|
|
|
@@ -498,7 +498,7 @@ describe("presentation helpers", () => {
|
|
|
498
498
|
|
|
499
499
|
test("formatProjectsTable renders the brief's column header", () => {
|
|
500
500
|
const table = formatProjectsTable([
|
|
501
|
-
{ project_id: 12, alias: "main-db", name: "Main DB", label: null, joe_ready: true, tunnel: true },
|
|
501
|
+
{ project_id: 12, alias: "main-db", name: "Main DB", label: null, joe_ready: true, tunnel: true, instance_id: null, dblab_instance_id: 7 },
|
|
502
502
|
]);
|
|
503
503
|
const [header, row] = table.split("\n");
|
|
504
504
|
expect(header).toContain("PROJECT_ID");
|