@batadata/cli 0.1.3 → 0.1.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/api.js +1 -1
- package/dist/commands/db.js +64 -39
- package/dist/commands/db.test.js +36 -16
- package/dist/index.js +1 -1
- package/dist/utils/logger.js +1 -1
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -14,7 +14,7 @@ export async function request(method, path, options = {}) {
|
|
|
14
14
|
}
|
|
15
15
|
const headers = {
|
|
16
16
|
"Content-Type": "application/json",
|
|
17
|
-
"User-Agent": "@batadata/cli 0.1.
|
|
17
|
+
"User-Agent": "@batadata/cli 0.1.4",
|
|
18
18
|
};
|
|
19
19
|
if (options.token) {
|
|
20
20
|
headers["Authorization"] = `Bearer ${options.token}`;
|
package/dist/commands/db.js
CHANGED
|
@@ -36,26 +36,46 @@ function formatDate(iso) {
|
|
|
36
36
|
* when the compute is ready to serve a query without a cold start.
|
|
37
37
|
*/
|
|
38
38
|
function branchStatusLabel(b) {
|
|
39
|
-
const base = b.computeStatus ||
|
|
39
|
+
const base = b.computeStatus || "-";
|
|
40
40
|
if (b.ready === true)
|
|
41
41
|
return `${base} ${colors.green("✓")}`;
|
|
42
42
|
return base;
|
|
43
43
|
}
|
|
44
44
|
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
* the primary, falling back to the first branch. Returns null if the project
|
|
48
|
-
* has no branches or the lookup failed.
|
|
45
|
+
* List a project's branches (GET /v1/projects/:id), each enriched by the server
|
|
46
|
+
* with computeStatus + ready. Returns null if the lookup failed.
|
|
49
47
|
*/
|
|
50
|
-
async function
|
|
48
|
+
async function listBranches(projectId, token, teamId) {
|
|
51
49
|
const query = {};
|
|
52
50
|
if (teamId)
|
|
53
51
|
query.team_id = teamId;
|
|
54
52
|
const res = await api.get(`/v1/projects/${projectId}`, token, query);
|
|
55
53
|
if (!res.ok)
|
|
56
54
|
return null;
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
return { branches: res.data.branches ?? [], projectName: res.data.name };
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve a project's primary branch (the one to run a query against), falling
|
|
59
|
+
* back to the first branch. Returns null if the project has no branches or the
|
|
60
|
+
* lookup failed.
|
|
61
|
+
*/
|
|
62
|
+
async function getPrimaryBranch(projectId, token, teamId) {
|
|
63
|
+
const result = await listBranches(projectId, token, teamId);
|
|
64
|
+
if (!result)
|
|
65
|
+
return null;
|
|
66
|
+
return result.branches.find((b) => b.isPrimary) ?? result.branches[0] ?? null;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a `--branch` reference — which may be a branch ID **or** a branch
|
|
70
|
+
* name — to a concrete branch. Agents naturally pass the name (the same string
|
|
71
|
+
* they used with `db branch create`), so accepting only an id was a footgun
|
|
72
|
+
* ("Branch not found"). Returns null if no branch matches.
|
|
73
|
+
*/
|
|
74
|
+
async function resolveBranchRef(projectId, token, teamId, ref) {
|
|
75
|
+
const result = await listBranches(projectId, token, teamId);
|
|
76
|
+
if (!result)
|
|
77
|
+
return null;
|
|
78
|
+
return result.branches.find((b) => b.id === ref || b.name === ref) ?? null;
|
|
59
79
|
}
|
|
60
80
|
export async function connect() {
|
|
61
81
|
// psql is an interactive session — there's no headless equivalent. Don't spawn
|
|
@@ -123,38 +143,31 @@ export async function branches() {
|
|
|
123
143
|
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
124
144
|
}
|
|
125
145
|
const s = jsonMode ? null : spinner("Fetching branches");
|
|
126
|
-
const
|
|
127
|
-
if (config.defaultTeam)
|
|
128
|
-
query.team_id = config.defaultTeam;
|
|
129
|
-
const res = await api.get(`/v1/projects/${projectId}`, token, query);
|
|
146
|
+
const result = await listBranches(projectId, token, config.defaultTeam);
|
|
130
147
|
s?.stop();
|
|
131
|
-
if (!
|
|
132
|
-
emitError(
|
|
133
|
-
: res.status === 404 ? "NO_PROJECT"
|
|
134
|
-
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
135
|
-
: "CLI_ERROR", apiError(res, "Failed to fetch branches"), "");
|
|
148
|
+
if (!result) {
|
|
149
|
+
emitError("API_UNAVAILABLE", "Failed to fetch branches.", "");
|
|
136
150
|
}
|
|
137
|
-
const branchList =
|
|
151
|
+
const branchList = result.branches;
|
|
138
152
|
if (jsonMode) {
|
|
139
153
|
json({
|
|
140
154
|
project_id: projectId,
|
|
141
|
-
project_name:
|
|
155
|
+
project_name: result.projectName,
|
|
142
156
|
branches: branchList.map((b) => ({
|
|
143
157
|
id: b.id,
|
|
144
158
|
name: b.name,
|
|
145
|
-
is_primary: b.
|
|
146
|
-
status: b.status,
|
|
159
|
+
is_primary: b.isPrimary ?? false,
|
|
147
160
|
// Compute readiness — what agents poll on between a create/cold start
|
|
148
161
|
// and a successful query.
|
|
149
162
|
computeStatus: b.computeStatus ?? null,
|
|
150
163
|
ready: b.ready ?? null,
|
|
151
|
-
created_at: b.
|
|
164
|
+
created_at: b.createdAt ?? null,
|
|
152
165
|
})),
|
|
153
166
|
count: branchList.length,
|
|
154
167
|
});
|
|
155
168
|
return;
|
|
156
169
|
}
|
|
157
|
-
heading(`Branches — ${
|
|
170
|
+
heading(`Branches — ${result.projectName}`);
|
|
158
171
|
if (branchList.length === 0) {
|
|
159
172
|
log(` ${colors.dim("No branches found.")}`);
|
|
160
173
|
log();
|
|
@@ -162,11 +175,11 @@ export async function branches() {
|
|
|
162
175
|
}
|
|
163
176
|
table(["NAME", "PRIMARY", "STATUS", "CREATED"], branchList.map((b) => [
|
|
164
177
|
b.name,
|
|
165
|
-
b.
|
|
166
|
-
//
|
|
167
|
-
//
|
|
178
|
+
b.isPrimary ? colors.green("yes") : "-",
|
|
179
|
+
// Live compute lifecycle (computeStatus), with a check once ready so the
|
|
180
|
+
// column is scannable.
|
|
168
181
|
branchStatusLabel(b),
|
|
169
|
-
formatDate(b.
|
|
182
|
+
formatDate(b.createdAt ?? ""),
|
|
170
183
|
]));
|
|
171
184
|
log();
|
|
172
185
|
}
|
|
@@ -275,10 +288,11 @@ export async function studio() {
|
|
|
275
288
|
openBrowser(studioUrl);
|
|
276
289
|
}
|
|
277
290
|
/**
|
|
278
|
-
* Pull a `--branch <
|
|
279
|
-
* the explicit branch
|
|
280
|
-
* SQL string. Keeps `db query`
|
|
281
|
-
*
|
|
291
|
+
* Pull a `--branch <ref>` / `--branch=<ref>` flag out of the query args and
|
|
292
|
+
* return the explicit branch ref — an id OR a name, resolved later — plus the
|
|
293
|
+
* remaining args, which join into the SQL string. Keeps `db query`
|
|
294
|
+
* order-independent (the flag can sit before or after the SQL) and consistent
|
|
295
|
+
* with how global flags are parsed.
|
|
282
296
|
*/
|
|
283
297
|
function parseBranchFlag(args) {
|
|
284
298
|
let branchId;
|
|
@@ -311,11 +325,22 @@ export async function query(args = []) {
|
|
|
311
325
|
emitError("NO_PROJECT", "No default project set.", "Set one with: bata projects info <id>");
|
|
312
326
|
}
|
|
313
327
|
const s = jsonMode ? null : spinner("Running query");
|
|
314
|
-
// Target branch: an explicit --branch <id> wins; otherwise resolve
|
|
315
|
-
// project's primary branch. /v1/sql/execute is keyed on branch_id, not
|
|
316
|
-
// project. (The old code POSTed to /v1/query/:projectId, which 404s.)
|
|
317
|
-
|
|
318
|
-
|
|
328
|
+
// Target branch: an explicit --branch <id-or-name> wins; otherwise resolve
|
|
329
|
+
// the project's primary branch. /v1/sql/execute is keyed on branch_id, not
|
|
330
|
+
// project. (The old code POSTed to /v1/query/:projectId, which 404s.) We
|
|
331
|
+
// resolve the --branch value against the project's branch list so a NAME
|
|
332
|
+
// works too — sending a name straight through as branch_id 404'd ("Branch
|
|
333
|
+
// not found").
|
|
334
|
+
let branchId_;
|
|
335
|
+
if (branchId) {
|
|
336
|
+
const branch = await resolveBranchRef(projectId, token, config.defaultTeam, branchId);
|
|
337
|
+
if (!branch) {
|
|
338
|
+
s?.stop();
|
|
339
|
+
emitError("BRANCH_NOT_FOUND", `Branch "${branchId}" not found in this project.`, "List branches with: bata db branches --json");
|
|
340
|
+
}
|
|
341
|
+
branchId_ = branch.id;
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
319
344
|
const branch = await getPrimaryBranch(projectId, token, config.defaultTeam);
|
|
320
345
|
if (!branch) {
|
|
321
346
|
s?.stop();
|
|
@@ -401,9 +426,9 @@ function dbHelp(sub) {
|
|
|
401
426
|
log(` ${colors.bold("bata db query")} — run a SQL query against a branch`);
|
|
402
427
|
log();
|
|
403
428
|
usage('bata db query "SELECT 1"');
|
|
404
|
-
usage("bata db query <sql> [--branch <
|
|
429
|
+
usage("bata db query <sql> [--branch <id-or-name>] [--json]");
|
|
405
430
|
log();
|
|
406
|
-
note("--branch <
|
|
431
|
+
note("--branch <id-or-name> Target a specific branch by id OR name (default: the project's primary)");
|
|
407
432
|
note("--json Emit rows as JSON objects + row_count");
|
|
408
433
|
note("Cold-start note: a just-created/idle branch may answer with exit 6");
|
|
409
434
|
note("(retryable) while its compute wakes — retry in a few seconds.");
|
|
@@ -447,7 +472,7 @@ function dbHelp(sub) {
|
|
|
447
472
|
usage("bata db branch create Create a new branch");
|
|
448
473
|
usage("bata db branch delete Delete a branch");
|
|
449
474
|
usage("bata db studio Open the table browser");
|
|
450
|
-
usage("bata db query <sql> Run a SQL query (--branch <id> to target a branch)");
|
|
475
|
+
usage("bata db query <sql> Run a SQL query (--branch <id-or-name> to target a branch)");
|
|
451
476
|
log();
|
|
452
477
|
note("Add --help to any subcommand for details (e.g. bata db query --help).");
|
|
453
478
|
}
|
package/dist/commands/db.test.js
CHANGED
|
@@ -6,7 +6,17 @@ const { get, post } = vi.hoisted(() => ({
|
|
|
6
6
|
get: vi.fn(async () => ({
|
|
7
7
|
ok: true,
|
|
8
8
|
status: 200,
|
|
9
|
-
|
|
9
|
+
// Shape matches GET /v1/projects/:id: camelCase branch fields enriched
|
|
10
|
+
// with computeStatus + ready. `feature` is a second, non-primary branch
|
|
11
|
+
// so name/id resolution has something to match.
|
|
12
|
+
data: {
|
|
13
|
+
id: "proj_1",
|
|
14
|
+
name: "demo",
|
|
15
|
+
branches: [
|
|
16
|
+
{ id: "br_primary", name: "main", isPrimary: true, computeStatus: "active", ready: true },
|
|
17
|
+
{ id: "br_feature", name: "feature", isPrimary: false, computeStatus: "idle", ready: false },
|
|
18
|
+
],
|
|
19
|
+
},
|
|
10
20
|
})),
|
|
11
21
|
post: vi.fn(async () => ({ ok: true, status: 200, data: { columns: ["n"], rows: [{ n: "1" }], rowCount: 1 } })),
|
|
12
22
|
}));
|
|
@@ -94,16 +104,26 @@ describe("db query — --help short-circuits before any network call", () => {
|
|
|
94
104
|
});
|
|
95
105
|
});
|
|
96
106
|
describe("db query — branch targeting", () => {
|
|
97
|
-
it("--branch <id>
|
|
98
|
-
await exitCodeOf(() => query(["SELECT 1", "--branch", "
|
|
107
|
+
it("--branch <id> resolves to that branch and sets branch_id", async () => {
|
|
108
|
+
await exitCodeOf(() => query(["SELECT 1", "--branch", "br_feature"]));
|
|
99
109
|
expect(post).toHaveBeenCalledTimes(1);
|
|
100
|
-
expect(post).toHaveBeenCalledWith("/v1/sql/execute", expect.objectContaining({ branch_id: "
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
expect(post).toHaveBeenCalledWith("/v1/sql/execute", expect.objectContaining({ branch_id: "br_feature", query: "SELECT 1" }), "test-token");
|
|
111
|
+
});
|
|
112
|
+
it("--branch <name> resolves the NAME to the branch id (was 'Branch not found')", async () => {
|
|
113
|
+
await exitCodeOf(() => query(["SELECT 1", "--branch", "feature"]));
|
|
114
|
+
expect(post).toHaveBeenCalledTimes(1);
|
|
115
|
+
expect(post).toHaveBeenCalledWith("/v1/sql/execute",
|
|
116
|
+
// Resolved the NAME "feature" → its id "br_feature".
|
|
117
|
+
expect.objectContaining({ branch_id: "br_feature", query: "SELECT 1" }), "test-token");
|
|
103
118
|
});
|
|
104
|
-
it("--branch=<
|
|
105
|
-
await exitCodeOf(() => query(["--branch=
|
|
106
|
-
expect(post).toHaveBeenCalledWith("/v1/sql/execute", expect.objectContaining({ branch_id: "
|
|
119
|
+
it("--branch=<name> (equals form) is also resolved", async () => {
|
|
120
|
+
await exitCodeOf(() => query(["--branch=feature", "SELECT 2"]));
|
|
121
|
+
expect(post).toHaveBeenCalledWith("/v1/sql/execute", expect.objectContaining({ branch_id: "br_feature", query: "SELECT 2" }), "test-token");
|
|
122
|
+
});
|
|
123
|
+
it("--branch <unknown> exits 5 (not-found) and never runs a query", async () => {
|
|
124
|
+
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "does-not-exist"]));
|
|
125
|
+
expect(code).toBe(5);
|
|
126
|
+
expect(post).not.toHaveBeenCalled();
|
|
107
127
|
});
|
|
108
128
|
it("without --branch, resolves the project's primary branch (default behavior)", async () => {
|
|
109
129
|
await exitCodeOf(() => query(["SELECT 1"]));
|
|
@@ -118,7 +138,7 @@ describe("db query — retryable cold-start mapping (exit 6)", () => {
|
|
|
118
138
|
status: 503,
|
|
119
139
|
data: { error: "compute is starting", code: "COMPUTE_STARTING" },
|
|
120
140
|
});
|
|
121
|
-
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "
|
|
141
|
+
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "feature"]));
|
|
122
142
|
expect(code).toBe(6);
|
|
123
143
|
});
|
|
124
144
|
it("REGRESSION GUARD: a non-5xx status carrying { code: COMPUTE_STARTING } still maps to exit 6", async () => {
|
|
@@ -130,7 +150,7 @@ describe("db query — retryable cold-start mapping (exit 6)", () => {
|
|
|
130
150
|
status: 425,
|
|
131
151
|
data: { error: "compute is starting", code: "COMPUTE_STARTING" },
|
|
132
152
|
});
|
|
133
|
-
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "
|
|
153
|
+
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "feature"]));
|
|
134
154
|
expect(code).toBe(6);
|
|
135
155
|
});
|
|
136
156
|
it("a connection-refused error body maps to exit 6", async () => {
|
|
@@ -139,12 +159,12 @@ describe("db query — retryable cold-start mapping (exit 6)", () => {
|
|
|
139
159
|
status: 0,
|
|
140
160
|
data: { error: "connect ECONNREFUSED 127.0.0.1:5432" },
|
|
141
161
|
});
|
|
142
|
-
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "
|
|
162
|
+
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "feature"]));
|
|
143
163
|
expect(code).toBe(6);
|
|
144
164
|
});
|
|
145
165
|
it("a generic 5xx maps to exit 6", async () => {
|
|
146
166
|
post.mockResolvedValueOnce({ ok: false, status: 502, data: { error: "bad gateway" } });
|
|
147
|
-
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "
|
|
167
|
+
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "feature"]));
|
|
148
168
|
expect(code).toBe(6);
|
|
149
169
|
});
|
|
150
170
|
it("a genuine SQL syntax error (200 with error field) stays exit 1, NOT retryable", async () => {
|
|
@@ -153,7 +173,7 @@ describe("db query — retryable cold-start mapping (exit 6)", () => {
|
|
|
153
173
|
status: 200,
|
|
154
174
|
data: { error: 'syntax error at or near "SELCT"' },
|
|
155
175
|
});
|
|
156
|
-
const code = await exitCodeOf(() => query(["SELCT 1", "--branch", "
|
|
176
|
+
const code = await exitCodeOf(() => query(["SELCT 1", "--branch", "feature"]));
|
|
157
177
|
expect(code).toBe(1);
|
|
158
178
|
});
|
|
159
179
|
it("a 400 bad-input SQL error stays exit 1, NOT retryable", async () => {
|
|
@@ -162,12 +182,12 @@ describe("db query — retryable cold-start mapping (exit 6)", () => {
|
|
|
162
182
|
status: 400,
|
|
163
183
|
data: { error: "relation does not exist" },
|
|
164
184
|
});
|
|
165
|
-
const code = await exitCodeOf(() => query(["SELECT * FROM nope", "--branch", "
|
|
185
|
+
const code = await exitCodeOf(() => query(["SELECT * FROM nope", "--branch", "feature"]));
|
|
166
186
|
expect(code).toBe(1);
|
|
167
187
|
});
|
|
168
188
|
it("a 401 stays auth (exit 4), not retryable", async () => {
|
|
169
189
|
post.mockResolvedValueOnce({ ok: false, status: 401, data: { error: "invalid key" } });
|
|
170
|
-
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "
|
|
190
|
+
const code = await exitCodeOf(() => query(["SELECT 1", "--branch", "feature"]));
|
|
171
191
|
expect(code).toBe(4);
|
|
172
192
|
});
|
|
173
193
|
});
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { parseGlobalFlags } from "./args.js";
|
|
|
15
15
|
import { isJsonMode } from "./config.js";
|
|
16
16
|
import { colors, log, banner } from "./utils/logger.js";
|
|
17
17
|
import { exitCodeFor, isRetryable } from "./utils/errors.js";
|
|
18
|
-
const VERSION = "0.1.
|
|
18
|
+
const VERSION = "0.1.4";
|
|
19
19
|
function help() {
|
|
20
20
|
banner();
|
|
21
21
|
log(` ${colors.bold("Usage")}`);
|
package/dist/utils/logger.js
CHANGED
|
@@ -124,6 +124,6 @@ export function kvList(items) {
|
|
|
124
124
|
// Banner
|
|
125
125
|
export function banner() {
|
|
126
126
|
log();
|
|
127
|
-
log(` ${colors.cyan(colors.bold("BataDB"))} ${colors.dim("v0.1.
|
|
127
|
+
log(` ${colors.cyan(colors.bold("BataDB"))} ${colors.dim("v0.1.4")} ${colors.dim("— serverless Postgres platform")}`);
|
|
128
128
|
log();
|
|
129
129
|
}
|