@njdamstra/appwrite-utils-cli 1.11.0 → 1.11.2

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.
@@ -11,7 +11,7 @@ export const migrateCommands = {
11
11
  message: "String attribute migration:",
12
12
  choices: [
13
13
  {
14
- name: "Analyze — scan local configs, generate migration plan (YAML)",
14
+ name: "Analyze — scan Appwrite server, generate migration plan (YAML)",
15
15
  value: "analyze",
16
16
  },
17
17
  {
@@ -33,18 +33,33 @@ export const migrateCommands = {
33
33
  },
34
34
  async analyzePhase(cli) {
35
35
  const controller = cli.controller;
36
- if (!controller?.config) {
37
- MessageFormatter.error("No configuration loaded. Make sure you have a valid .appwrite config.", undefined, { prefix: "Analyze" });
36
+ if (!controller?.adapter) {
37
+ MessageFormatter.error("No database adapter available. Ensure a server connection is established.", undefined, { prefix: "Analyze" });
38
38
  return;
39
39
  }
40
- const config = controller.config;
41
- const collections = [
42
- ...(config.collections || []),
43
- ...(config.tables || []),
44
- ];
45
- if (collections.length === 0) {
46
- MessageFormatter.warning("No collections/tables found in local config. Nothing to analyze.", { prefix: "Analyze" });
47
- return;
40
+ // Prompt for database selection
41
+ const allDatabases = controller.config.databases || [];
42
+ let databaseIds;
43
+ if (allDatabases.length > 1) {
44
+ const { selectedDbs } = await inquirer.prompt([
45
+ {
46
+ type: "checkbox",
47
+ name: "selectedDbs",
48
+ message: "Select databases to include in the analysis:",
49
+ choices: allDatabases.map((db) => ({
50
+ name: `${db.name} (${db.$id})`,
51
+ value: db.$id,
52
+ checked: true,
53
+ })),
54
+ },
55
+ ]);
56
+ if (selectedDbs.length === 0) {
57
+ MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
58
+ return;
59
+ }
60
+ if (selectedDbs.length < allDatabases.length) {
61
+ databaseIds = selectedDbs;
62
+ }
48
63
  }
49
64
  // Prompt for output path
50
65
  const { outputPath } = await inquirer.prompt([
@@ -55,9 +70,9 @@ export const migrateCommands = {
55
70
  default: path.join(process.cwd(), "migrate-strings-plan.yaml"),
56
71
  },
57
72
  ]);
58
- const options = { outputPath };
73
+ const options = { outputPath, databaseIds };
59
74
  try {
60
- analyzeStringAttributes(config, options);
75
+ await analyzeStringAttributes(controller.adapter, controller.config, options);
61
76
  MessageFormatter.success("Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.", { prefix: "Analyze" });
62
77
  }
63
78
  catch (err) {
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"],
@@ -648,14 +653,16 @@ async function main() {
648
653
  if (argv.migrateStringsAnalyze || argv.migrateStringsExecute) {
649
654
  const { analyzeStringAttributes, executeMigrationPlan } = await import("./migrations/migrateStrings.js");
650
655
  if (argv.migrateStringsAnalyze) {
651
- if (!controller.config) {
652
- MessageFormatter.error("No Appwrite configuration found", undefined, {
653
- prefix: "Migration",
654
- });
656
+ if (!controller.adapter) {
657
+ MessageFormatter.error("No database adapter available. Ensure config has valid credentials.", undefined, { prefix: "Migration" });
655
658
  return;
656
659
  }
657
- analyzeStringAttributes(controller.config, {
660
+ const databaseIds = argv.migrateStringsDbIds
661
+ ? argv.migrateStringsDbIds.split(",").map((s) => s.trim()).filter(Boolean)
662
+ : undefined;
663
+ await analyzeStringAttributes(controller.adapter, controller.config, {
658
664
  outputPath: argv.migrateStringsOutput,
665
+ databaseIds,
659
666
  });
660
667
  }
661
668
  else if (argv.migrateStringsExecute) {
@@ -1,7 +1,7 @@
1
1
  import { type DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
2
2
  import type { AppwriteConfig } from "@njdamstra/appwrite-utils";
3
3
  import { type MigrationPlan, type AnalyzeOptions, type ExecuteOptions } from "./migrateStringsTypes.js";
4
- export declare function analyzeStringAttributes(config: AppwriteConfig, options?: AnalyzeOptions): MigrationPlan;
4
+ export declare function analyzeStringAttributes(adapter: DatabaseAdapter, config: AppwriteConfig, options?: AnalyzeOptions): Promise<MigrationPlan>;
5
5
  export declare function executeMigrationPlan(adapter: DatabaseAdapter, options: ExecuteOptions): Promise<{
6
6
  succeeded: number;
7
7
  failed: number;
@@ -8,55 +8,56 @@ import { MessageFormatter, tryAwaitWithRetry, } from "@njdamstra/appwrite-utils-
8
8
  import { ProgressManager } from "../shared/progressManager.js";
9
9
  import { MigrationPlanSchema, MigrationCheckpointSchema, suggestTargetType, generateBackupKey, } from "./migrateStringsTypes.js";
10
10
  // ────────────────────────────────────────────────────────
11
- // Phase 1: Analyze — offline, reads local YAML configs
11
+ // Phase 1: Analyze — queries Appwrite server for real state
12
12
  // ────────────────────────────────────────────────────────
13
- export function analyzeStringAttributes(config, options = {}) {
14
- const collections = [
15
- ...(config.collections || []),
16
- ...(config.tables || []),
17
- ];
18
- const databases = config.databases || [];
13
+ export async function analyzeStringAttributes(adapter, config, options = {}) {
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;
19
+ if (databases.length === 0) {
20
+ MessageFormatter.warning("No databases configured. Nothing to analyze.", {
21
+ prefix: "Analyze",
22
+ });
23
+ return emptyPlan(config);
24
+ }
19
25
  const entries = [];
20
- const collectionsSeen = new Set();
21
26
  for (const db of databases) {
22
- // Find collections assigned to this database (or unassigned = applied to all)
23
- const dbCollections = collections.filter((c) => {
24
- const coll = c;
25
- if (coll.databaseId)
26
- return coll.databaseId === db.$id;
27
- if (coll.databaseIds?.length)
28
- return coll.databaseIds.includes(db.$id);
29
- return true; // unassigned → applied to all databases
27
+ MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
28
+ prefix: "Analyze",
30
29
  });
31
- for (const coll of dbCollections) {
32
- const collId = coll.$id || coll.name;
33
- const dedupKey = `${db.$id}:${collId}`;
34
- if (collectionsSeen.has(dedupKey))
35
- continue;
36
- collectionsSeen.add(dedupKey);
37
- const attributes = coll.attributes || [];
38
- const indexes = coll.indexes || [];
30
+ // Fetch all collections/tables from server
31
+ const tablesRes = await tryAwaitWithRetry(() => adapter.listTables({ databaseId: db.$id }));
32
+ const tables = tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
33
+ for (const table of tables) {
34
+ const tableId = table.$id || table.key || table.name;
35
+ const tableName = table.name || tableId;
36
+ // Fetch full schema from server
37
+ const schemaRes = await tryAwaitWithRetry(() => adapter.getTable({ databaseId: db.$id, tableId }));
38
+ const attributes = schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
39
+ // Fetch indexes from server
40
+ const indexRes = await tryAwaitWithRetry(() => adapter.listIndexes({ databaseId: db.$id, tableId }));
41
+ const indexes = indexRes?.data || [];
39
42
  for (const attr of attributes) {
40
43
  if (attr.type !== "string")
41
44
  continue;
42
- const stringAttr = attr;
43
- const size = stringAttr.size || 50;
44
- const isEncrypted = !!stringAttr.encrypt;
45
+ const size = attr.size || 50;
46
+ const isEncrypted = !!attr.encrypt;
45
47
  const isRequired = !!attr.required;
46
48
  const isArray = !!attr.array;
47
- const hasDefault = stringAttr.xdefault !== undefined && stringAttr.xdefault !== null;
49
+ const hasDefault = attr.xdefault !== undefined && attr.xdefault !== null;
48
50
  // Find indexes that reference this attribute
49
51
  const affectedIndexes = indexes
50
- .filter((idx) => idx.attributes.includes(attr.key))
52
+ .filter((idx) => idx.attributes?.includes(attr.key))
51
53
  .map((idx) => idx.key);
52
54
  const hasIndex = affectedIndexes.length > 0;
53
55
  const suggested = suggestTargetType(size, hasIndex);
54
- // Build entry
55
56
  const entry = {
56
57
  databaseId: db.$id,
57
58
  databaseName: db.name,
58
- collectionId: collId,
59
- collectionName: coll.name,
59
+ collectionId: tableId,
60
+ collectionName: tableName,
60
61
  attributeKey: attr.key,
61
62
  currentType: "string",
62
63
  currentSize: size,
@@ -64,7 +65,7 @@ export function analyzeStringAttributes(config, options = {}) {
64
65
  isArray,
65
66
  isEncrypted,
66
67
  hasDefault,
67
- defaultValue: hasDefault ? stringAttr.xdefault : undefined,
68
+ defaultValue: hasDefault ? attr.xdefault : undefined,
68
69
  suggestedType: suggested,
69
70
  targetType: suggested,
70
71
  targetSize: suggested === "varchar" ? size : undefined,
@@ -72,10 +73,8 @@ export function analyzeStringAttributes(config, options = {}) {
72
73
  skipReason: isEncrypted ? "encrypted" : undefined,
73
74
  indexesAffected: affectedIndexes,
74
75
  };
75
- // Warn if indexed attr gets non-varchar suggestion
76
- if (hasIndex &&
77
- suggested !== "varchar" &&
78
- !isEncrypted) {
76
+ // Indexed attrs must stay varchar
77
+ if (hasIndex && suggested !== "varchar" && !isEncrypted) {
79
78
  entry.targetType = "varchar";
80
79
  entry.targetSize = size;
81
80
  }
@@ -116,6 +115,22 @@ export function analyzeStringAttributes(config, options = {}) {
116
115
  printPlanSummary(plan);
117
116
  return plan;
118
117
  }
118
+ function emptyPlan(config) {
119
+ return {
120
+ version: 1,
121
+ generatedAt: new Date().toISOString(),
122
+ appwriteEndpoint: config.appwriteEndpoint,
123
+ appwriteProject: config.appwriteProject,
124
+ summary: {
125
+ totalStringAttributes: 0,
126
+ toMigrate: 0,
127
+ toSkip: 0,
128
+ databaseCount: 0,
129
+ collectionCount: 0,
130
+ },
131
+ entries: [],
132
+ };
133
+ }
119
134
  function printPlanSummary(plan) {
120
135
  const { summary } = plan;
121
136
  console.log("");
@@ -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;
@@ -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.0",
4
+ "version": "1.11.2",
5
5
  "main": "dist/main.js",
6
6
  "type": "module",
7
7
  "repository": {
@@ -17,7 +17,7 @@ export const migrateCommands = {
17
17
  message: "String attribute migration:",
18
18
  choices: [
19
19
  {
20
- name: "Analyze — scan local configs, generate migration plan (YAML)",
20
+ name: "Analyze — scan Appwrite server, generate migration plan (YAML)",
21
21
  value: "analyze",
22
22
  },
23
23
  {
@@ -39,27 +39,38 @@ export const migrateCommands = {
39
39
 
40
40
  async analyzePhase(cli: InteractiveCLI): Promise<void> {
41
41
  const controller = (cli as any).controller;
42
- if (!controller?.config) {
42
+ if (!controller?.adapter) {
43
43
  MessageFormatter.error(
44
- "No configuration loaded. Make sure you have a valid .appwrite config.",
44
+ "No database adapter available. Ensure a server connection is established.",
45
45
  undefined,
46
46
  { prefix: "Analyze" }
47
47
  );
48
48
  return;
49
49
  }
50
50
 
51
- const config = controller.config;
52
- const collections = [
53
- ...(config.collections || []),
54
- ...(config.tables || []),
55
- ];
56
-
57
- if (collections.length === 0) {
58
- MessageFormatter.warning(
59
- "No collections/tables found in local config. Nothing to analyze.",
60
- { prefix: "Analyze" }
61
- );
62
- return;
51
+ // Prompt for database selection
52
+ const allDatabases = controller.config.databases || [];
53
+ let databaseIds: string[] | undefined;
54
+ if (allDatabases.length > 1) {
55
+ const { selectedDbs } = await inquirer.prompt([
56
+ {
57
+ type: "checkbox",
58
+ name: "selectedDbs",
59
+ message: "Select databases to include in the analysis:",
60
+ choices: allDatabases.map((db: any) => ({
61
+ name: `${db.name} (${db.$id})`,
62
+ value: db.$id,
63
+ checked: true,
64
+ })),
65
+ },
66
+ ]);
67
+ if (selectedDbs.length === 0) {
68
+ MessageFormatter.warning("No databases selected. Aborting.", { prefix: "Analyze" });
69
+ return;
70
+ }
71
+ if (selectedDbs.length < allDatabases.length) {
72
+ databaseIds = selectedDbs;
73
+ }
63
74
  }
64
75
 
65
76
  // Prompt for output path
@@ -72,10 +83,10 @@ export const migrateCommands = {
72
83
  },
73
84
  ]);
74
85
 
75
- const options: AnalyzeOptions = { outputPath };
86
+ const options: AnalyzeOptions = { outputPath, databaseIds };
76
87
 
77
88
  try {
78
- analyzeStringAttributes(config, options);
89
+ await analyzeStringAttributes(controller.adapter, controller.config, options);
79
90
  MessageFormatter.success(
80
91
  "Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.",
81
92
  { prefix: "Analyze" }
package/src/main.ts CHANGED
@@ -86,6 +86,7 @@ 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;
91
92
  }
@@ -642,6 +643,11 @@ const argv = yargs(hideBin(process.argv))
642
643
  type: "string",
643
644
  description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
644
645
  })
646
+ .option("migrateStringsDbIds", {
647
+ alias: ["migrate-strings-db-ids"],
648
+ type: "string",
649
+ description: "Comma-separated database IDs to include in analysis (default: all)",
650
+ })
645
651
  .option("migrateStringsKeepBackups", {
646
652
  alias: ["migrate-strings-keep-backups"],
647
653
  type: "boolean",
@@ -881,14 +887,20 @@ async function main() {
881
887
  );
882
888
 
883
889
  if (argv.migrateStringsAnalyze) {
884
- if (!controller.config) {
885
- MessageFormatter.error("No Appwrite configuration found", undefined, {
886
- prefix: "Migration",
887
- });
890
+ if (!controller.adapter) {
891
+ MessageFormatter.error(
892
+ "No database adapter available. Ensure config has valid credentials.",
893
+ undefined,
894
+ { prefix: "Migration" }
895
+ );
888
896
  return;
889
897
  }
890
- analyzeStringAttributes(controller.config, {
898
+ const databaseIds = argv.migrateStringsDbIds
899
+ ? argv.migrateStringsDbIds.split(",").map((s: string) => s.trim()).filter(Boolean)
900
+ : undefined;
901
+ await analyzeStringAttributes(controller.adapter, controller.config, {
891
902
  outputPath: argv.migrateStringsOutput,
903
+ databaseIds,
892
904
  });
893
905
  } else if (argv.migrateStringsExecute) {
894
906
  if (!controller.adapter) {
@@ -9,11 +9,7 @@ import {
9
9
  MessageFormatter,
10
10
  tryAwaitWithRetry,
11
11
  } from "@njdamstra/appwrite-utils-helpers";
12
- import type {
13
- AppwriteConfig,
14
- Attribute,
15
- Index,
16
- } from "@njdamstra/appwrite-utils";
12
+ import type { AppwriteConfig } from "@njdamstra/appwrite-utils";
17
13
  import { ProgressManager } from "../shared/progressManager.js";
18
14
  import {
19
15
  type MigrationPlan,
@@ -30,65 +26,80 @@ import {
30
26
  } from "./migrateStringsTypes.js";
31
27
 
32
28
  // ────────────────────────────────────────────────────────
33
- // Phase 1: Analyze — offline, reads local YAML configs
29
+ // Phase 1: Analyze — queries Appwrite server for real state
34
30
  // ────────────────────────────────────────────────────────
35
31
 
36
- export function analyzeStringAttributes(
32
+ export async function analyzeStringAttributes(
33
+ adapter: DatabaseAdapter,
37
34
  config: AppwriteConfig,
38
35
  options: AnalyzeOptions = {}
39
- ): MigrationPlan {
40
- const collections = [
41
- ...(config.collections || []),
42
- ...(config.tables || []),
43
- ];
44
- const databases = config.databases || [];
36
+ ): Promise<MigrationPlan> {
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;
42
+ if (databases.length === 0) {
43
+ MessageFormatter.warning("No databases configured. Nothing to analyze.", {
44
+ prefix: "Analyze",
45
+ });
46
+ return emptyPlan(config);
47
+ }
45
48
 
46
49
  const entries: MigrationPlanEntry[] = [];
47
- const collectionsSeen = new Set<string>();
48
50
 
49
51
  for (const db of databases) {
50
- // Find collections assigned to this database (or unassigned = applied to all)
51
- const dbCollections = collections.filter((c) => {
52
- const coll = c as any;
53
- if (coll.databaseId) return coll.databaseId === db.$id;
54
- if (coll.databaseIds?.length) return coll.databaseIds.includes(db.$id);
55
- return true; // unassigned → applied to all databases
52
+ MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
53
+ prefix: "Analyze",
56
54
  });
57
55
 
58
- for (const coll of dbCollections) {
59
- const collId = (coll as any).$id || coll.name;
60
- const dedupKey = `${db.$id}:${collId}`;
61
- if (collectionsSeen.has(dedupKey)) continue;
62
- collectionsSeen.add(dedupKey);
56
+ // Fetch all collections/tables from server
57
+ const tablesRes = await tryAwaitWithRetry(() =>
58
+ adapter.listTables({ databaseId: db.$id })
59
+ );
60
+ const tables: any[] =
61
+ tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
62
+
63
+ for (const table of tables) {
64
+ const tableId: string = table.$id || table.key || table.name;
65
+ const tableName: string = table.name || tableId;
63
66
 
64
- const attributes: Attribute[] = (coll as any).attributes || [];
65
- const indexes: Index[] = (coll as any).indexes || [];
67
+ // Fetch full schema from server
68
+ const schemaRes = await tryAwaitWithRetry(() =>
69
+ adapter.getTable({ databaseId: db.$id, tableId })
70
+ );
71
+ const attributes: any[] =
72
+ schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
73
+
74
+ // Fetch indexes from server
75
+ const indexRes = await tryAwaitWithRetry(() =>
76
+ adapter.listIndexes({ databaseId: db.$id, tableId })
77
+ );
78
+ const indexes: any[] = indexRes?.data || [];
66
79
 
67
80
  for (const attr of attributes) {
68
81
  if (attr.type !== "string") continue;
69
82
 
70
- const stringAttr = attr as any;
71
- const size: number = stringAttr.size || 50;
72
- const isEncrypted = !!stringAttr.encrypt;
83
+ const size: number = attr.size || 50;
84
+ const isEncrypted = !!(attr as any).encrypt;
73
85
  const isRequired = !!attr.required;
74
86
  const isArray = !!attr.array;
75
87
  const hasDefault =
76
- stringAttr.xdefault !== undefined && stringAttr.xdefault !== null;
88
+ attr.xdefault !== undefined && attr.xdefault !== null;
77
89
 
78
90
  // Find indexes that reference this attribute
79
91
  const affectedIndexes = indexes
80
- .filter((idx) => idx.attributes.includes(attr.key))
81
- .map((idx) => idx.key);
92
+ .filter((idx: any) => idx.attributes?.includes(attr.key))
93
+ .map((idx: any) => idx.key);
82
94
  const hasIndex = affectedIndexes.length > 0;
83
95
 
84
96
  const suggested = suggestTargetType(size, hasIndex);
85
97
 
86
- // Build entry
87
98
  const entry: MigrationPlanEntry = {
88
99
  databaseId: db.$id,
89
100
  databaseName: db.name,
90
- collectionId: collId,
91
- collectionName: coll.name,
101
+ collectionId: tableId,
102
+ collectionName: tableName,
92
103
  attributeKey: attr.key,
93
104
  currentType: "string",
94
105
  currentSize: size,
@@ -96,7 +107,7 @@ export function analyzeStringAttributes(
96
107
  isArray,
97
108
  isEncrypted,
98
109
  hasDefault,
99
- defaultValue: hasDefault ? stringAttr.xdefault : undefined,
110
+ defaultValue: hasDefault ? attr.xdefault : undefined,
100
111
  suggestedType: suggested,
101
112
  targetType: suggested,
102
113
  targetSize: suggested === "varchar" ? size : undefined,
@@ -105,12 +116,8 @@ export function analyzeStringAttributes(
105
116
  indexesAffected: affectedIndexes,
106
117
  };
107
118
 
108
- // Warn if indexed attr gets non-varchar suggestion
109
- if (
110
- hasIndex &&
111
- suggested !== "varchar" &&
112
- !isEncrypted
113
- ) {
119
+ // Indexed attrs must stay varchar
120
+ if (hasIndex && suggested !== "varchar" && !isEncrypted) {
114
121
  entry.targetType = "varchar";
115
122
  entry.targetSize = size;
116
123
  }
@@ -161,6 +168,23 @@ export function analyzeStringAttributes(
161
168
  return plan;
162
169
  }
163
170
 
171
+ function emptyPlan(config: AppwriteConfig): MigrationPlan {
172
+ return {
173
+ version: 1,
174
+ generatedAt: new Date().toISOString(),
175
+ appwriteEndpoint: config.appwriteEndpoint,
176
+ appwriteProject: config.appwriteProject,
177
+ summary: {
178
+ totalStringAttributes: 0,
179
+ toMigrate: 0,
180
+ toSkip: 0,
181
+ databaseCount: 0,
182
+ collectionCount: 0,
183
+ },
184
+ entries: [],
185
+ };
186
+ }
187
+
164
188
  function printPlanSummary(plan: MigrationPlan): void {
165
189
  const { summary } = plan;
166
190
  console.log("");
@@ -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 {
@@ -136,17 +137,18 @@ export function suggestTargetType(
136
137
  // ── Helper: generate backup key with length limits ──
137
138
 
138
139
  const MAX_KEY_LENGTH = 36;
139
- const BACKUP_PREFIX = "_mig_";
140
+ const BACKUP_PREFIX = "mig_";
140
141
 
141
142
  export function generateBackupKey(originalKey: string): string {
142
143
  const candidate = `${BACKUP_PREFIX}${originalKey}`;
143
144
  if (candidate.length <= MAX_KEY_LENGTH) {
144
145
  return candidate;
145
146
  }
146
- // Truncate + 4-char hash for uniqueness
147
+ // Truncate + 4-char hash for uniqueness: m_ + orig + _ + hash(4)
147
148
  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}`;
149
+ const TRUNC_PREFIX = "m_";
150
+ const maxOrigLen = MAX_KEY_LENGTH - TRUNC_PREFIX.length - 1 - 4;
151
+ return `${TRUNC_PREFIX}${originalKey.slice(0, maxOrigLen)}_${hash}`;
150
152
  }
151
153
 
152
154
  function simpleHash(str: string): string {