@njdamstra/appwrite-utils-cli 1.11.1 → 1.11.3

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,5 +1,6 @@
1
1
  import inquirer from "inquirer";
2
2
  import path from "node:path";
3
+ import fs from "node:fs";
3
4
  import { MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
4
5
  import { analyzeStringAttributes, executeMigrationPlan, } from "../../migrations/migrateStrings.js";
5
6
  export const migrateCommands = {
@@ -37,6 +38,30 @@ export const migrateCommands = {
37
38
  MessageFormatter.error("No database adapter available. Ensure a server connection is established.", undefined, { prefix: "Analyze" });
38
39
  return;
39
40
  }
41
+ // Prompt for database selection
42
+ const allDatabases = controller.config.databases || [];
43
+ let databaseIds;
44
+ if (allDatabases.length > 1) {
45
+ const { selectedDbs } = await inquirer.prompt([
46
+ {
47
+ type: "checkbox",
48
+ name: "selectedDbs",
49
+ message: "Select databases to include in the analysis:",
50
+ choices: allDatabases.map((db) => ({
51
+ name: `${db.name} (${db.$id})`,
52
+ value: db.$id,
53
+ checked: true,
54
+ })),
55
+ },
56
+ ]);
57
+ if (selectedDbs.length === 0) {
58
+ MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
59
+ return;
60
+ }
61
+ if (selectedDbs.length < allDatabases.length) {
62
+ databaseIds = selectedDbs;
63
+ }
64
+ }
40
65
  // Prompt for output path
41
66
  const { outputPath } = await inquirer.prompt([
42
67
  {
@@ -46,7 +71,7 @@ export const migrateCommands = {
46
71
  default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
47
72
  },
48
73
  ]);
49
- const options = { outputPath };
74
+ const options = { outputPath, databaseIds };
50
75
  try {
51
76
  await analyzeStringAttributes(controller.adapter, controller.config, options);
52
77
  MessageFormatter.success("Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.", { prefix: "Analyze" });
@@ -70,6 +95,27 @@ export const migrateCommands = {
70
95
  default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
71
96
  },
72
97
  ]);
98
+ // Check for existing checkpoint
99
+ let freshRun = false;
100
+ const checkpointPath = planPath.replace(/\.ya?ml$/, ".checkpoint.json");
101
+ if (fs.existsSync(checkpointPath)) {
102
+ const { cpAction } = await inquirer.prompt([
103
+ {
104
+ type: "list",
105
+ name: "cpAction",
106
+ message: "Found an existing checkpoint from a previous run. What would you like to do?",
107
+ choices: [
108
+ { name: "Resume — continue from where it left off", value: "resume" },
109
+ { name: "Start fresh — delete checkpoint and start over", value: "fresh" },
110
+ { name: "Cancel", value: "cancel" },
111
+ ],
112
+ },
113
+ ]);
114
+ if (cpAction === "cancel")
115
+ return;
116
+ if (cpAction === "fresh")
117
+ freshRun = true;
118
+ }
73
119
  const { keepBackups } = await inquirer.prompt([
74
120
  {
75
121
  type: "confirm",
@@ -90,6 +136,7 @@ export const migrateCommands = {
90
136
  planPath,
91
137
  keepBackups,
92
138
  dryRun,
139
+ freshRun,
93
140
  };
94
141
  try {
95
142
  const results = await executeMigrationPlan(controller.adapter, options);
package/dist/main.js CHANGED
@@ -469,6 +469,11 @@ const argv = yargs(hideBin(process.argv))
469
469
  alias: ["migrate-strings-output"],
470
470
  type: "string",
471
471
  description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
472
+ })
473
+ .option("migrateStringsDbIds", {
474
+ alias: ["migrate-strings-db-ids"],
475
+ type: "string",
476
+ description: "Comma-separated database IDs to include in analysis (default: all)",
472
477
  })
473
478
  .option("migrateStringsKeepBackups", {
474
479
  alias: ["migrate-strings-keep-backups"],
@@ -480,6 +485,11 @@ const argv = yargs(hideBin(process.argv))
480
485
  alias: ["migrate-strings-dry-run"],
481
486
  type: "boolean",
482
487
  description: "Dry run — show what would happen without making changes",
488
+ })
489
+ .option("migrateStringsFresh", {
490
+ alias: ["migrate-strings-fresh"],
491
+ type: "boolean",
492
+ description: "Ignore existing checkpoint and start migration fresh",
483
493
  })
484
494
  .parse();
485
495
  async function main() {
@@ -652,8 +662,12 @@ async function main() {
652
662
  MessageFormatter.error("No database adapter available. Ensure config has valid credentials.", undefined, { prefix: "Migration" });
653
663
  return;
654
664
  }
665
+ const databaseIds = argv.migrateStringsDbIds
666
+ ? argv.migrateStringsDbIds.split(",").map((s) => s.trim()).filter(Boolean)
667
+ : undefined;
655
668
  await analyzeStringAttributes(controller.adapter, controller.config, {
656
669
  outputPath: argv.migrateStringsOutput,
670
+ databaseIds,
657
671
  });
658
672
  }
659
673
  else if (argv.migrateStringsExecute) {
@@ -665,6 +679,7 @@ async function main() {
665
679
  planPath: argv.migrateStringsExecute,
666
680
  keepBackups: argv.migrateStringsKeepBackups ?? true,
667
681
  dryRun: argv.migrateStringsDryRun ?? false,
682
+ freshRun: argv.migrateStringsFresh ?? false,
668
683
  });
669
684
  if (results.failed > 0) {
670
685
  process.exit(1);
@@ -11,7 +11,11 @@ import { MigrationPlanSchema, MigrationCheckpointSchema, suggestTargetType, gene
11
11
  // Phase 1: Analyze — queries Appwrite server for real state
12
12
  // ────────────────────────────────────────────────────────
13
13
  export async function analyzeStringAttributes(adapter, config, options = {}) {
14
- const databases = config.databases || [];
14
+ let databasesToScan = config.databases || [];
15
+ if (options.databaseIds?.length) {
16
+ databasesToScan = databasesToScan.filter(db => options.databaseIds.includes(db.$id));
17
+ }
18
+ const databases = databasesToScan;
15
19
  if (databases.length === 0) {
16
20
  MessageFormatter.warning("No databases configured. Nothing to analyze.", {
17
21
  prefix: "Analyze",
@@ -189,6 +193,12 @@ export async function executeMigrationPlan(adapter, options) {
189
193
  // Load or create checkpoint
190
194
  const checkpointPath = options.checkpointPath ||
191
195
  options.planPath.replace(/\.ya?ml$/, ".checkpoint.json");
196
+ if (options.freshRun && fs.existsSync(checkpointPath)) {
197
+ fs.unlinkSync(checkpointPath);
198
+ MessageFormatter.info("Deleted old checkpoint — starting fresh.", {
199
+ prefix: "Checkpoint",
200
+ });
201
+ }
192
202
  const checkpoint = loadOrCreateCheckpoint(checkpointPath, options.planPath);
193
203
  const batchSize = options.batchSize || 100;
194
204
  const batchDelayMs = options.batchDelayMs || 50;
@@ -182,6 +182,7 @@ export type MigrationCheckpoint = z.infer<typeof MigrationCheckpointSchema>;
182
182
  export interface AnalyzeOptions {
183
183
  outputPath?: string;
184
184
  verbose?: boolean;
185
+ databaseIds?: string[];
185
186
  }
186
187
  export interface ExecuteOptions {
187
188
  planPath: string;
@@ -190,6 +191,7 @@ export interface ExecuteOptions {
190
191
  batchSize?: number;
191
192
  batchDelayMs?: number;
192
193
  checkpointPath?: string;
194
+ freshRun?: boolean;
193
195
  }
194
196
  export declare function suggestTargetType(size: number, hasIndex: boolean): MigrationTargetType;
195
197
  export declare function generateBackupKey(originalKey: string): string;
@@ -97,16 +97,17 @@ export function suggestTargetType(size, hasIndex) {
97
97
  }
98
98
  // ── Helper: generate backup key with length limits ──
99
99
  const MAX_KEY_LENGTH = 36;
100
- const BACKUP_PREFIX = "_mig_";
100
+ const BACKUP_PREFIX = "mig_";
101
101
  export function generateBackupKey(originalKey) {
102
102
  const candidate = `${BACKUP_PREFIX}${originalKey}`;
103
103
  if (candidate.length <= MAX_KEY_LENGTH) {
104
104
  return candidate;
105
105
  }
106
- // Truncate + 4-char hash for uniqueness
106
+ // Truncate + 4-char hash for uniqueness: m_ + orig + _ + hash(4)
107
107
  const hash = simpleHash(originalKey);
108
- const maxOrigLen = MAX_KEY_LENGTH - BACKUP_PREFIX.length - 1 - 4; // _mig_ + _ + hash4
109
- return `_m_${originalKey.slice(0, maxOrigLen)}_${hash}`;
108
+ const TRUNC_PREFIX = "m_";
109
+ const maxOrigLen = MAX_KEY_LENGTH - TRUNC_PREFIX.length - 1 - 4;
110
+ return `${TRUNC_PREFIX}${originalKey.slice(0, maxOrigLen)}_${hash}`;
110
111
  }
111
112
  function simpleHash(str) {
112
113
  let h = 0;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@njdamstra/appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.11.1",
4
+ "version": "1.11.3",
5
5
  "main": "dist/main.js",
6
6
  "type": "module",
7
7
  "repository": {
@@ -1,5 +1,6 @@
1
1
  import inquirer from "inquirer";
2
2
  import path from "node:path";
3
+ import fs from "node:fs";
3
4
  import { MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
4
5
  import type { InteractiveCLI } from "../../interactiveCLI.js";
5
6
  import {
@@ -48,6 +49,31 @@ export const migrateCommands = {
48
49
  return;
49
50
  }
50
51
 
52
+ // Prompt for database selection
53
+ const allDatabases = controller.config.databases || [];
54
+ let databaseIds: string[] | undefined;
55
+ if (allDatabases.length > 1) {
56
+ const { selectedDbs } = await inquirer.prompt([
57
+ {
58
+ type: "checkbox",
59
+ name: "selectedDbs",
60
+ message: "Select databases to include in the analysis:",
61
+ choices: allDatabases.map((db: any) => ({
62
+ name: `${db.name} (${db.$id})`,
63
+ value: db.$id,
64
+ checked: true,
65
+ })),
66
+ },
67
+ ]);
68
+ if (selectedDbs.length === 0) {
69
+ MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
70
+ return;
71
+ }
72
+ if (selectedDbs.length < allDatabases.length) {
73
+ databaseIds = selectedDbs;
74
+ }
75
+ }
76
+
51
77
  // Prompt for output path
52
78
  const { outputPath } = await inquirer.prompt([
53
79
  {
@@ -58,7 +84,7 @@ export const migrateCommands = {
58
84
  },
59
85
  ]);
60
86
 
61
- const options: AnalyzeOptions = { outputPath };
87
+ const options: AnalyzeOptions = { outputPath, databaseIds };
62
88
 
63
89
  try {
64
90
  await analyzeStringAttributes(controller.adapter, controller.config, options);
@@ -96,6 +122,26 @@ export const migrateCommands = {
96
122
  },
97
123
  ]);
98
124
 
125
+ // Check for existing checkpoint
126
+ let freshRun = false;
127
+ const checkpointPath = planPath.replace(/\.ya?ml$/, ".checkpoint.json");
128
+ if (fs.existsSync(checkpointPath)) {
129
+ const { cpAction } = await inquirer.prompt([
130
+ {
131
+ type: "list",
132
+ name: "cpAction",
133
+ message: "Found an existing checkpoint from a previous run. What would you like to do?",
134
+ choices: [
135
+ { name: "Resume — continue from where it left off", value: "resume" },
136
+ { name: "Start fresh — delete checkpoint and start over", value: "fresh" },
137
+ { name: "Cancel", value: "cancel" },
138
+ ],
139
+ },
140
+ ]);
141
+ if (cpAction === "cancel") return;
142
+ if (cpAction === "fresh") freshRun = true;
143
+ }
144
+
99
145
  const { keepBackups } = await inquirer.prompt([
100
146
  {
101
147
  type: "confirm",
@@ -118,6 +164,7 @@ export const migrateCommands = {
118
164
  planPath,
119
165
  keepBackups,
120
166
  dryRun,
167
+ freshRun,
121
168
  };
122
169
 
123
170
  try {
package/src/main.ts CHANGED
@@ -86,8 +86,10 @@ interface CliOptions {
86
86
  migrateStringsAnalyze?: boolean;
87
87
  migrateStringsExecute?: string;
88
88
  migrateStringsOutput?: string;
89
+ migrateStringsDbIds?: string;
89
90
  migrateStringsKeepBackups?: boolean;
90
91
  migrateStringsDryRun?: boolean;
92
+ migrateStringsFresh?: boolean;
91
93
  }
92
94
 
93
95
  type ParsedArgv = ArgumentsCamelCase<CliOptions>;
@@ -642,6 +644,11 @@ const argv = yargs(hideBin(process.argv))
642
644
  type: "string",
643
645
  description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
644
646
  })
647
+ .option("migrateStringsDbIds", {
648
+ alias: ["migrate-strings-db-ids"],
649
+ type: "string",
650
+ description: "Comma-separated database IDs to include in analysis (default: all)",
651
+ })
645
652
  .option("migrateStringsKeepBackups", {
646
653
  alias: ["migrate-strings-keep-backups"],
647
654
  type: "boolean",
@@ -653,6 +660,11 @@ const argv = yargs(hideBin(process.argv))
653
660
  type: "boolean",
654
661
  description: "Dry run — show what would happen without making changes",
655
662
  })
663
+ .option("migrateStringsFresh", {
664
+ alias: ["migrate-strings-fresh"],
665
+ type: "boolean",
666
+ description: "Ignore existing checkpoint and start migration fresh",
667
+ })
656
668
  .parse() as ParsedArgv;
657
669
 
658
670
  async function main() {
@@ -889,8 +901,12 @@ async function main() {
889
901
  );
890
902
  return;
891
903
  }
904
+ const databaseIds = argv.migrateStringsDbIds
905
+ ? argv.migrateStringsDbIds.split(",").map((s: string) => s.trim()).filter(Boolean)
906
+ : undefined;
892
907
  await analyzeStringAttributes(controller.adapter, controller.config, {
893
908
  outputPath: argv.migrateStringsOutput,
909
+ databaseIds,
894
910
  });
895
911
  } else if (argv.migrateStringsExecute) {
896
912
  if (!controller.adapter) {
@@ -905,6 +921,7 @@ async function main() {
905
921
  planPath: argv.migrateStringsExecute,
906
922
  keepBackups: argv.migrateStringsKeepBackups ?? true,
907
923
  dryRun: argv.migrateStringsDryRun ?? false,
924
+ freshRun: argv.migrateStringsFresh ?? false,
908
925
  });
909
926
  if (results.failed > 0) {
910
927
  process.exit(1);
@@ -34,7 +34,11 @@ export async function analyzeStringAttributes(
34
34
  config: AppwriteConfig,
35
35
  options: AnalyzeOptions = {}
36
36
  ): Promise<MigrationPlan> {
37
- const databases = config.databases || [];
37
+ let databasesToScan = config.databases || [];
38
+ if (options.databaseIds?.length) {
39
+ databasesToScan = databasesToScan.filter(db => options.databaseIds!.includes(db.$id));
40
+ }
41
+ const databases = databasesToScan;
38
42
  if (databases.length === 0) {
39
43
  MessageFormatter.warning("No databases configured. Nothing to analyze.", {
40
44
  prefix: "Analyze",
@@ -260,6 +264,12 @@ export async function executeMigrationPlan(
260
264
  const checkpointPath =
261
265
  options.checkpointPath ||
262
266
  options.planPath.replace(/\.ya?ml$/, ".checkpoint.json");
267
+ if (options.freshRun && fs.existsSync(checkpointPath)) {
268
+ fs.unlinkSync(checkpointPath);
269
+ MessageFormatter.info("Deleted old checkpoint — starting fresh.", {
270
+ prefix: "Checkpoint",
271
+ });
272
+ }
263
273
  const checkpoint = loadOrCreateCheckpoint(checkpointPath, options.planPath);
264
274
 
265
275
  const batchSize = options.batchSize || 100;
@@ -108,6 +108,7 @@ export type MigrationCheckpoint = z.infer<typeof MigrationCheckpointSchema>;
108
108
  export interface AnalyzeOptions {
109
109
  outputPath?: string;
110
110
  verbose?: boolean;
111
+ databaseIds?: string[];
111
112
  }
112
113
 
113
114
  export interface ExecuteOptions {
@@ -117,6 +118,7 @@ export interface ExecuteOptions {
117
118
  batchSize?: number;
118
119
  batchDelayMs?: number;
119
120
  checkpointPath?: string;
121
+ freshRun?: boolean;
120
122
  }
121
123
 
122
124
  // ── Helper: suggest target type from size + index presence ──
@@ -136,17 +138,18 @@ export function suggestTargetType(
136
138
  // ── Helper: generate backup key with length limits ──
137
139
 
138
140
  const MAX_KEY_LENGTH = 36;
139
- const BACKUP_PREFIX = "_mig_";
141
+ const BACKUP_PREFIX = "mig_";
140
142
 
141
143
  export function generateBackupKey(originalKey: string): string {
142
144
  const candidate = `${BACKUP_PREFIX}${originalKey}`;
143
145
  if (candidate.length <= MAX_KEY_LENGTH) {
144
146
  return candidate;
145
147
  }
146
- // Truncate + 4-char hash for uniqueness
148
+ // Truncate + 4-char hash for uniqueness: m_ + orig + _ + hash(4)
147
149
  const hash = simpleHash(originalKey);
148
- const maxOrigLen = MAX_KEY_LENGTH - BACKUP_PREFIX.length - 1 - 4; // _mig_ + _ + hash4
149
- return `_m_${originalKey.slice(0, maxOrigLen)}_${hash}`;
150
+ const TRUNC_PREFIX = "m_";
151
+ const maxOrigLen = MAX_KEY_LENGTH - TRUNC_PREFIX.length - 1 - 4;
152
+ return `${TRUNC_PREFIX}${originalKey.slice(0, maxOrigLen)}_${hash}`;
150
153
  }
151
154
 
152
155
  function simpleHash(str: string): string {