@batadata/cli 0.2.0 → 0.2.1
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/commands/import.d.ts +20 -1
- package/dist/commands/import.js +101 -17
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bata import — Neon (or any Postgres) → BataDB migration.
|
|
3
3
|
*
|
|
4
|
-
* bata import --source <postgres-uri> [--project <id> | --name <name>] [--yes] [--json]
|
|
4
|
+
* bata import --source <postgres-uri> [--project <id> | --name <name>] [--pg <major>] [--yes] [--json]
|
|
5
5
|
*
|
|
6
6
|
* Agent-native: shells out to the standard `pg_dump` / `psql` client tools
|
|
7
7
|
* (zero runtime deps) and streams a dump straight into a fresh BataDB project.
|
|
@@ -142,6 +142,21 @@ export declare const PLATFORM_SCAFFOLDING_TABLES: string[];
|
|
|
142
142
|
export declare const PLATFORM_SCAFFOLDING_SEQUENCES: string[];
|
|
143
143
|
/** Schemas whose ENTIRE contents are platform furniture (any `<schema>.*` object). */
|
|
144
144
|
export declare const PLATFORM_SCAFFOLDING_SCHEMAS: string[];
|
|
145
|
+
/**
|
|
146
|
+
* Merge the server-published scaffolding list (GET /v1/platform/import-scaffolding)
|
|
147
|
+
* into the baked-in defaults above. The server is the source of truth going
|
|
148
|
+
* forward — if provisioning grows new furniture, an already-installed CLI keeps
|
|
149
|
+
* verifying correctly instead of false-failing on it. Union (never replace): the
|
|
150
|
+
* baked defaults still apply against an older control plane, and a partial or
|
|
151
|
+
* malformed server response can only ever ADD exclusions it explicitly names.
|
|
152
|
+
* The exported const arrays are extended in place so every existing reference
|
|
153
|
+
* (the --yes guard, the created-target check, verify) sees the merged lists.
|
|
154
|
+
*/
|
|
155
|
+
export declare function applyServerScaffolding(server: {
|
|
156
|
+
tables?: unknown;
|
|
157
|
+
sequences?: unknown;
|
|
158
|
+
schemas?: unknown;
|
|
159
|
+
}): void;
|
|
145
160
|
/** Is this qualified name one of BataDB's seeded platform-scaffolding objects —
|
|
146
161
|
* either an exact named object, or anything inside a scaffolding schema? */
|
|
147
162
|
export declare function isPlatformScaffolding(qualifiedName: string): boolean;
|
|
@@ -171,8 +186,12 @@ export declare function parseImportArgs(args: string[]): {
|
|
|
171
186
|
source?: string;
|
|
172
187
|
projectId?: string;
|
|
173
188
|
name?: string;
|
|
189
|
+
pg?: string;
|
|
174
190
|
help: boolean;
|
|
175
191
|
} | {
|
|
176
192
|
error: string;
|
|
177
193
|
};
|
|
194
|
+
/** Default target major for a new project: match the source so features survive,
|
|
195
|
+
* capped at what BataDB ships (a PG18 source → 17, a PG16 or older source → 16). */
|
|
196
|
+
export declare function defaultTargetPg(sourceMajor: number): string;
|
|
178
197
|
export declare function importDb(args: string[]): Promise<void>;
|
package/dist/commands/import.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* bata import — Neon (or any Postgres) → BataDB migration.
|
|
3
3
|
*
|
|
4
|
-
* bata import --source <postgres-uri> [--project <id> | --name <name>] [--yes] [--json]
|
|
4
|
+
* bata import --source <postgres-uri> [--project <id> | --name <name>] [--pg <major>] [--yes] [--json]
|
|
5
5
|
*
|
|
6
6
|
* Agent-native: shells out to the standard `pg_dump` / `psql` client tools
|
|
7
7
|
* (zero runtime deps) and streams a dump straight into a fresh BataDB project.
|
|
@@ -279,11 +279,39 @@ export const PLATFORM_SCAFFOLDING_TABLES = ["public.health_check"];
|
|
|
279
279
|
export const PLATFORM_SCAFFOLDING_SEQUENCES = ["public.health_check_id_seq"];
|
|
280
280
|
/** Schemas whose ENTIRE contents are platform furniture (any `<schema>.*` object). */
|
|
281
281
|
export const PLATFORM_SCAFFOLDING_SCHEMAS = ["neon_migration"];
|
|
282
|
-
|
|
282
|
+
let PLATFORM_SCAFFOLDING_NAMES = new Set([
|
|
283
283
|
...PLATFORM_SCAFFOLDING_TABLES,
|
|
284
284
|
...PLATFORM_SCAFFOLDING_SEQUENCES,
|
|
285
285
|
]);
|
|
286
|
-
|
|
286
|
+
let PLATFORM_SCAFFOLDING_SCHEMA_PREFIXES = PLATFORM_SCAFFOLDING_SCHEMAS.map((s) => `${s}.`);
|
|
287
|
+
/**
|
|
288
|
+
* Merge the server-published scaffolding list (GET /v1/platform/import-scaffolding)
|
|
289
|
+
* into the baked-in defaults above. The server is the source of truth going
|
|
290
|
+
* forward — if provisioning grows new furniture, an already-installed CLI keeps
|
|
291
|
+
* verifying correctly instead of false-failing on it. Union (never replace): the
|
|
292
|
+
* baked defaults still apply against an older control plane, and a partial or
|
|
293
|
+
* malformed server response can only ever ADD exclusions it explicitly names.
|
|
294
|
+
* The exported const arrays are extended in place so every existing reference
|
|
295
|
+
* (the --yes guard, the created-target check, verify) sees the merged lists.
|
|
296
|
+
*/
|
|
297
|
+
export function applyServerScaffolding(server) {
|
|
298
|
+
const merge = (into, from) => {
|
|
299
|
+
if (!Array.isArray(from))
|
|
300
|
+
return;
|
|
301
|
+
for (const v of from) {
|
|
302
|
+
if (typeof v === "string" && v.length > 0 && !into.includes(v))
|
|
303
|
+
into.push(v);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
merge(PLATFORM_SCAFFOLDING_TABLES, server.tables);
|
|
307
|
+
merge(PLATFORM_SCAFFOLDING_SEQUENCES, server.sequences);
|
|
308
|
+
merge(PLATFORM_SCAFFOLDING_SCHEMAS, server.schemas);
|
|
309
|
+
PLATFORM_SCAFFOLDING_NAMES = new Set([
|
|
310
|
+
...PLATFORM_SCAFFOLDING_TABLES,
|
|
311
|
+
...PLATFORM_SCAFFOLDING_SEQUENCES,
|
|
312
|
+
]);
|
|
313
|
+
PLATFORM_SCAFFOLDING_SCHEMA_PREFIXES = PLATFORM_SCAFFOLDING_SCHEMAS.map((s) => `${s}.`);
|
|
314
|
+
}
|
|
287
315
|
/** Is this qualified name one of BataDB's seeded platform-scaffolding objects —
|
|
288
316
|
* either an exact named object, or anything inside a scaffolding schema? */
|
|
289
317
|
export function isPlatformScaffolding(qualifiedName) {
|
|
@@ -610,6 +638,7 @@ export function parseImportArgs(args) {
|
|
|
610
638
|
let source;
|
|
611
639
|
let projectId;
|
|
612
640
|
let name;
|
|
641
|
+
let pg;
|
|
613
642
|
let help = false;
|
|
614
643
|
for (let i = 0; i < args.length; i++) {
|
|
615
644
|
const a = args[i];
|
|
@@ -646,8 +675,23 @@ export function parseImportArgs(args) {
|
|
|
646
675
|
}
|
|
647
676
|
else if (a.startsWith("--name="))
|
|
648
677
|
name = a.slice("--name=".length);
|
|
678
|
+
else if (a === "--pg") {
|
|
679
|
+
const v = takeVal();
|
|
680
|
+
if (v === undefined)
|
|
681
|
+
return { error: "--pg requires a value (a PostgreSQL major, e.g. 17)." };
|
|
682
|
+
pg = v;
|
|
683
|
+
}
|
|
684
|
+
else if (a.startsWith("--pg="))
|
|
685
|
+
pg = a.slice("--pg=".length);
|
|
649
686
|
// Unknown tokens / positionals are ignored — --source is the only input.
|
|
650
687
|
}
|
|
688
|
+
if (pg !== undefined && !/^\d+$/.test(pg)) {
|
|
689
|
+
return { error: `--pg must be a PostgreSQL major version number (got "${pg}").` };
|
|
690
|
+
}
|
|
691
|
+
// --pg picks the version of a NEW project; an existing project's version is fixed.
|
|
692
|
+
if (pg !== undefined && projectId !== undefined && !help) {
|
|
693
|
+
return { error: "--pg only applies when creating a new project — it cannot change an existing --project's version." };
|
|
694
|
+
}
|
|
651
695
|
// --project (import into an existing project) and --name (create a new one) are
|
|
652
696
|
// opposite intents. Silently taking --project was the riskier read (mutating an
|
|
653
697
|
// existing project while ignoring the requested name) — make it an explicit error.
|
|
@@ -656,11 +700,21 @@ export function parseImportArgs(args) {
|
|
|
656
700
|
error: "--project and --name are mutually exclusive (import into an existing project OR create a new one).",
|
|
657
701
|
};
|
|
658
702
|
}
|
|
659
|
-
return { source, projectId, name, help };
|
|
703
|
+
return { source, projectId, name, pg, help };
|
|
704
|
+
}
|
|
705
|
+
/** Highest PostgreSQL major BataDB can create today — used only to pick the
|
|
706
|
+
* DEFAULT version for a new import target (min(source major, this), floored at
|
|
707
|
+
* 16). An explicit --pg is passed through as-is and validated by the API's
|
|
708
|
+
* allowlist, so a newer server accepts newer majors without a CLI release. */
|
|
709
|
+
const HIGHEST_SUPPORTED_PG = 17;
|
|
710
|
+
/** Default target major for a new project: match the source so features survive,
|
|
711
|
+
* capped at what BataDB ships (a PG18 source → 17, a PG16 or older source → 16). */
|
|
712
|
+
export function defaultTargetPg(sourceMajor) {
|
|
713
|
+
return String(Math.max(16, Math.min(sourceMajor, HIGHEST_SUPPORTED_PG)));
|
|
660
714
|
}
|
|
661
715
|
function printHelp() {
|
|
662
716
|
log();
|
|
663
|
-
log(` ${colors.bold("bata import")} ${colors.dim("--source <postgres-uri> [--project <id> | --name <name>]")}`);
|
|
717
|
+
log(` ${colors.bold("bata import")} ${colors.dim("--source <postgres-uri> [--project <id> | --name <name>] [--pg <major>]")}`);
|
|
664
718
|
log();
|
|
665
719
|
log(` Migrate a Neon (or any Postgres) database into BataDB via pg_dump | psql.`);
|
|
666
720
|
log();
|
|
@@ -668,6 +722,7 @@ function printHelp() {
|
|
|
668
722
|
log(` ${colors.dim("--source <uri>")} Source Postgres connection URI ${colors.dim("(required)")}`);
|
|
669
723
|
log(` ${colors.dim("--project <id>")} Import into an existing BataDB project`);
|
|
670
724
|
log(` ${colors.dim("--name <name>")} Create a new project with this name ${colors.dim("(default: source db name)")}`);
|
|
725
|
+
log(` ${colors.dim("--pg <major>")} PostgreSQL major for a NEW project ${colors.dim("(default: match the source, capped at 17)")}`);
|
|
671
726
|
log(` ${colors.dim("--yes, -y")} Proceed even if the target already has tables`);
|
|
672
727
|
log(` ${colors.dim("--json")} Machine-readable result`);
|
|
673
728
|
log();
|
|
@@ -679,8 +734,9 @@ function printHelp() {
|
|
|
679
734
|
log(` ${colors.cyan('bata import --source "postgresql://…@ep-x.neon.tech/neondb"')}`);
|
|
680
735
|
log(` ${colors.cyan('bata import --source "$NEON_URL" --name my-app --yes --json')}`);
|
|
681
736
|
log();
|
|
682
|
-
log(` ${colors.dim("BataDB runs PostgreSQL 16.
|
|
683
|
-
log(` ${colors.dim("
|
|
737
|
+
log(` ${colors.dim("BataDB runs PostgreSQL 16 and 17. A new target defaults to the source's")}`);
|
|
738
|
+
log(` ${colors.dim("major (capped at 17); features newer than the target fail loudly during")}`);
|
|
739
|
+
log(` ${colors.dim("restore (nothing is hidden).")}`);
|
|
684
740
|
log();
|
|
685
741
|
}
|
|
686
742
|
export async function importDb(args) {
|
|
@@ -707,6 +763,22 @@ export async function importDb(args) {
|
|
|
707
763
|
};
|
|
708
764
|
if (!jsonMode)
|
|
709
765
|
heading("Import into BataDB");
|
|
766
|
+
// Ask the control plane for the current platform-scaffolding list (tables/
|
|
767
|
+
// sequences/schemas provisioning seeds into every project) and merge it into
|
|
768
|
+
// the baked-in defaults, so every downstream consumer — the source-collision
|
|
769
|
+
// warning, the --yes non-empty guard, and verification — keeps excluding the
|
|
770
|
+
// platform's own furniture even when it grows after this CLI was installed.
|
|
771
|
+
// Best-effort: any failure — older server (404), network error, or a transport
|
|
772
|
+
// THROW (timeout/DNS) — silently keeps the defaults; it must never abort the run.
|
|
773
|
+
try {
|
|
774
|
+
const scaffRes = await api.get("/v1/platform/import-scaffolding", token);
|
|
775
|
+
if (scaffRes.ok && scaffRes.data && typeof scaffRes.data === "object") {
|
|
776
|
+
applyServerScaffolding(scaffRes.data);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
// baked-in defaults stay in effect
|
|
781
|
+
}
|
|
710
782
|
// The client tools are needed to even inspect the source, so verify they're on
|
|
711
783
|
// PATH up front (the pg_dump-major-vs-source-major gate stays in phase 2, once
|
|
712
784
|
// the source major is known).
|
|
@@ -793,6 +865,9 @@ export async function importDb(args) {
|
|
|
793
865
|
const teamId = await resolveTeamId(token);
|
|
794
866
|
let targetProjectId;
|
|
795
867
|
let created = false;
|
|
868
|
+
// The target's PostgreSQL major (for messaging + the JSON result). For a new
|
|
869
|
+
// project this is what we asked for; for --project it's read from the record.
|
|
870
|
+
let targetPg;
|
|
796
871
|
if (parsed.projectId) {
|
|
797
872
|
phase(`Resolving target project ${parsed.projectId}`);
|
|
798
873
|
const q = {};
|
|
@@ -807,27 +882,35 @@ export async function importDb(args) {
|
|
|
807
882
|
return;
|
|
808
883
|
}
|
|
809
884
|
targetProjectId = parsed.projectId;
|
|
885
|
+
// Older control planes may omit pgVersion; fall back to the platform's
|
|
886
|
+
// historical default rather than guessing anything newer.
|
|
887
|
+
targetPg = projRes.data.pgVersion ?? "16";
|
|
810
888
|
}
|
|
811
889
|
else {
|
|
812
890
|
const name = parsed.name ?? defaultProjectName(sourceSummary.database, source);
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
891
|
+
// An unparseable source version keeps the conservative historical default.
|
|
892
|
+
targetPg = parsed.pg ?? (sourceMajor === null ? "16" : defaultTargetPg(sourceMajor));
|
|
893
|
+
phase(`Creating project ${name} (serverless · PostgreSQL ${targetPg})`);
|
|
894
|
+
if (sourceMajor !== null && sourceMajor > Number(targetPg) && !jsonMode) {
|
|
895
|
+
warn(`source is PG${sourceMajor}, BataDB target is PG${targetPg} — standard schemas migrate cleanly; ` +
|
|
896
|
+
`PG${sourceMajor}-only features will fail loudly during restore`);
|
|
817
897
|
}
|
|
818
898
|
const body = {
|
|
819
899
|
name,
|
|
820
900
|
region: "us-east-1",
|
|
821
901
|
compute: { tier: "serverless", size_cu: 1 },
|
|
822
|
-
pg_version:
|
|
902
|
+
pg_version: targetPg,
|
|
823
903
|
};
|
|
824
904
|
if (teamId)
|
|
825
905
|
body.team_id = teamId;
|
|
826
906
|
const res = await api.post("/v1/projects", body, token);
|
|
827
907
|
if (!res.ok) {
|
|
908
|
+
const explicitPg = parsed.pg !== undefined;
|
|
828
909
|
emitError(res.status === 401 || res.status === 403 ? "INVALID_KEY"
|
|
829
910
|
: res.status >= 500 || res.status === 0 ? "API_UNAVAILABLE"
|
|
830
|
-
: "CLI_ERROR", apiError(res, "Failed to create target project"),
|
|
911
|
+
: "CLI_ERROR", apiError(res, "Failed to create target project"),
|
|
912
|
+
// A 400 on an explicit --pg is almost always an unsupported major.
|
|
913
|
+
explicitPg && res.status === 400 ? "Try --pg 16 (or omit --pg for the default)." : "");
|
|
831
914
|
return;
|
|
832
915
|
}
|
|
833
916
|
targetProjectId = res.data.id;
|
|
@@ -973,7 +1056,8 @@ export async function importDb(args) {
|
|
|
973
1056
|
json({
|
|
974
1057
|
error: "Restore failed — the target rejected part of the dump.",
|
|
975
1058
|
code: "RESTORE_FAILED",
|
|
976
|
-
hint:
|
|
1059
|
+
hint: `Schema features newer than the target's PostgreSQL ${targetPg} aren't supported there. ` +
|
|
1060
|
+
"Review the errors below. " +
|
|
977
1061
|
strandedHintJson,
|
|
978
1062
|
errors: migration.errors,
|
|
979
1063
|
target: strandedTargetJson(),
|
|
@@ -1169,7 +1253,7 @@ export async function importDb(args) {
|
|
|
1169
1253
|
target: {
|
|
1170
1254
|
project_id: targetProjectId,
|
|
1171
1255
|
created,
|
|
1172
|
-
pg_version:
|
|
1256
|
+
pg_version: targetPg,
|
|
1173
1257
|
connection: targetUri,
|
|
1174
1258
|
},
|
|
1175
1259
|
verify: {
|
|
@@ -1202,7 +1286,7 @@ export async function importDb(args) {
|
|
|
1202
1286
|
log();
|
|
1203
1287
|
kvList([
|
|
1204
1288
|
["Project ID", colors.dim(targetProjectId)],
|
|
1205
|
-
["PostgreSQL",
|
|
1289
|
+
["PostgreSQL", targetPg],
|
|
1206
1290
|
["Connection", colors.dim(targetUri)],
|
|
1207
1291
|
]);
|
|
1208
1292
|
log();
|
|
@@ -1218,7 +1302,7 @@ function emitErrorHuman(message, errors) {
|
|
|
1218
1302
|
log(` ${colors.dim(e)}`);
|
|
1219
1303
|
}
|
|
1220
1304
|
log();
|
|
1221
|
-
log(` ${colors.dim("
|
|
1305
|
+
log(` ${colors.dim("Schema features newer than the target's PostgreSQL major aren't supported there.")}`);
|
|
1222
1306
|
log();
|
|
1223
1307
|
}
|
|
1224
1308
|
function safeJsonArray(stdout) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@batadata/cli",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "CLI for BataDB — serverless Postgres platform",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bata": "./dist/index.js"
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"devDependencies": {
|
|
22
22
|
"@types/node": "^22.10.0",
|
|
23
23
|
"typescript": "^5.7.0",
|
|
24
|
-
"vitest": "^3.2.
|
|
24
|
+
"vitest": "^3.2.7"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"dist"
|