@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.
@@ -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>;
@@ -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
- const PLATFORM_SCAFFOLDING_NAMES = new Set([
282
+ let PLATFORM_SCAFFOLDING_NAMES = new Set([
283
283
  ...PLATFORM_SCAFFOLDING_TABLES,
284
284
  ...PLATFORM_SCAFFOLDING_SEQUENCES,
285
285
  ]);
286
- const PLATFORM_SCAFFOLDING_SCHEMA_PREFIXES = PLATFORM_SCAFFOLDING_SCHEMAS.map((s) => `${s}.`);
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. Standard PG17 schemas migrate cleanly;")}`);
683
- log(` ${colors.dim("PG17-only features fail loudly during restore (nothing is hidden).")}`);
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
- phase(`Creating project ${name} (serverless · PostgreSQL 16)`);
814
- if (sourceMajor === 17 && !jsonMode) {
815
- warn("source is PG17, BataDB target is PG16 — standard schemas migrate cleanly; " +
816
- "PG17-only features will fail loudly during restore");
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: "16",
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: "PG17-only features aren't supported on the PG16 target. Review the errors below. " +
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: "16",
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", "16"],
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("PG17-only features aren't supported on the PG16 target.")}`);
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.0",
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.6"
24
+ "vitest": "^3.2.7"
25
25
  },
26
26
  "files": [
27
27
  "dist"