@njdamstra/appwrite-utils-cli 1.11.0 → 1.11.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.
@@ -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,17 +33,8 @@ 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" });
38
- return;
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" });
36
+ if (!controller?.adapter) {
37
+ MessageFormatter.error("No database adapter available. Ensure a server connection is established.", undefined, { prefix: "Analyze" });
47
38
  return;
48
39
  }
49
40
  // Prompt for output path
@@ -57,7 +48,7 @@ export const migrateCommands = {
57
48
  ]);
58
49
  const options = { outputPath };
59
50
  try {
60
- analyzeStringAttributes(config, options);
51
+ await analyzeStringAttributes(controller.adapter, controller.config, options);
61
52
  MessageFormatter.success("Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.", { prefix: "Analyze" });
62
53
  }
63
54
  catch (err) {
package/dist/main.js CHANGED
@@ -648,13 +648,11 @@ async function main() {
648
648
  if (argv.migrateStringsAnalyze || argv.migrateStringsExecute) {
649
649
  const { analyzeStringAttributes, executeMigrationPlan } = await import("./migrations/migrateStrings.js");
650
650
  if (argv.migrateStringsAnalyze) {
651
- if (!controller.config) {
652
- MessageFormatter.error("No Appwrite configuration found", undefined, {
653
- prefix: "Migration",
654
- });
651
+ if (!controller.adapter) {
652
+ MessageFormatter.error("No database adapter available. Ensure config has valid credentials.", undefined, { prefix: "Migration" });
655
653
  return;
656
654
  }
657
- analyzeStringAttributes(controller.config, {
655
+ await analyzeStringAttributes(controller.adapter, controller.config, {
658
656
  outputPath: argv.migrateStringsOutput,
659
657
  });
660
658
  }
@@ -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,52 @@ 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
- ];
13
+ export async function analyzeStringAttributes(adapter, config, options = {}) {
18
14
  const databases = config.databases || [];
15
+ if (databases.length === 0) {
16
+ MessageFormatter.warning("No databases configured. Nothing to analyze.", {
17
+ prefix: "Analyze",
18
+ });
19
+ return emptyPlan(config);
20
+ }
19
21
  const entries = [];
20
- const collectionsSeen = new Set();
21
22
  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
23
+ MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
24
+ prefix: "Analyze",
30
25
  });
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 || [];
26
+ // Fetch all collections/tables from server
27
+ const tablesRes = await tryAwaitWithRetry(() => adapter.listTables({ databaseId: db.$id }));
28
+ const tables = tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
29
+ for (const table of tables) {
30
+ const tableId = table.$id || table.key || table.name;
31
+ const tableName = table.name || tableId;
32
+ // Fetch full schema from server
33
+ const schemaRes = await tryAwaitWithRetry(() => adapter.getTable({ databaseId: db.$id, tableId }));
34
+ const attributes = schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
35
+ // Fetch indexes from server
36
+ const indexRes = await tryAwaitWithRetry(() => adapter.listIndexes({ databaseId: db.$id, tableId }));
37
+ const indexes = indexRes?.data || [];
39
38
  for (const attr of attributes) {
40
39
  if (attr.type !== "string")
41
40
  continue;
42
- const stringAttr = attr;
43
- const size = stringAttr.size || 50;
44
- const isEncrypted = !!stringAttr.encrypt;
41
+ const size = attr.size || 50;
42
+ const isEncrypted = !!attr.encrypt;
45
43
  const isRequired = !!attr.required;
46
44
  const isArray = !!attr.array;
47
- const hasDefault = stringAttr.xdefault !== undefined && stringAttr.xdefault !== null;
45
+ const hasDefault = attr.xdefault !== undefined && attr.xdefault !== null;
48
46
  // Find indexes that reference this attribute
49
47
  const affectedIndexes = indexes
50
- .filter((idx) => idx.attributes.includes(attr.key))
48
+ .filter((idx) => idx.attributes?.includes(attr.key))
51
49
  .map((idx) => idx.key);
52
50
  const hasIndex = affectedIndexes.length > 0;
53
51
  const suggested = suggestTargetType(size, hasIndex);
54
- // Build entry
55
52
  const entry = {
56
53
  databaseId: db.$id,
57
54
  databaseName: db.name,
58
- collectionId: collId,
59
- collectionName: coll.name,
55
+ collectionId: tableId,
56
+ collectionName: tableName,
60
57
  attributeKey: attr.key,
61
58
  currentType: "string",
62
59
  currentSize: size,
@@ -64,7 +61,7 @@ export function analyzeStringAttributes(config, options = {}) {
64
61
  isArray,
65
62
  isEncrypted,
66
63
  hasDefault,
67
- defaultValue: hasDefault ? stringAttr.xdefault : undefined,
64
+ defaultValue: hasDefault ? attr.xdefault : undefined,
68
65
  suggestedType: suggested,
69
66
  targetType: suggested,
70
67
  targetSize: suggested === "varchar" ? size : undefined,
@@ -72,10 +69,8 @@ export function analyzeStringAttributes(config, options = {}) {
72
69
  skipReason: isEncrypted ? "encrypted" : undefined,
73
70
  indexesAffected: affectedIndexes,
74
71
  };
75
- // Warn if indexed attr gets non-varchar suggestion
76
- if (hasIndex &&
77
- suggested !== "varchar" &&
78
- !isEncrypted) {
72
+ // Indexed attrs must stay varchar
73
+ if (hasIndex && suggested !== "varchar" && !isEncrypted) {
79
74
  entry.targetType = "varchar";
80
75
  entry.targetSize = size;
81
76
  }
@@ -116,6 +111,22 @@ export function analyzeStringAttributes(config, options = {}) {
116
111
  printPlanSummary(plan);
117
112
  return plan;
118
113
  }
114
+ function emptyPlan(config) {
115
+ return {
116
+ version: 1,
117
+ generatedAt: new Date().toISOString(),
118
+ appwriteEndpoint: config.appwriteEndpoint,
119
+ appwriteProject: config.appwriteProject,
120
+ summary: {
121
+ totalStringAttributes: 0,
122
+ toMigrate: 0,
123
+ toSkip: 0,
124
+ databaseCount: 0,
125
+ collectionCount: 0,
126
+ },
127
+ entries: [],
128
+ };
129
+ }
119
130
  function printPlanSummary(plan) {
120
131
  const { summary } = plan;
121
132
  console.log("");
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.1",
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,29 +39,15 @@ 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;
63
- }
64
-
65
51
  // Prompt for output path
66
52
  const { outputPath } = await inquirer.prompt([
67
53
  {
@@ -75,7 +61,7 @@ export const migrateCommands = {
75
61
  const options: AnalyzeOptions = { outputPath };
76
62
 
77
63
  try {
78
- analyzeStringAttributes(config, options);
64
+ await analyzeStringAttributes(controller.adapter, controller.config, options);
79
65
  MessageFormatter.success(
80
66
  "Analysis complete. Review the YAML plan, edit targetType/action as needed, then run Execute.",
81
67
  { prefix: "Analyze" }
package/src/main.ts CHANGED
@@ -881,13 +881,15 @@ async function main() {
881
881
  );
882
882
 
883
883
  if (argv.migrateStringsAnalyze) {
884
- if (!controller.config) {
885
- MessageFormatter.error("No Appwrite configuration found", undefined, {
886
- prefix: "Migration",
887
- });
884
+ if (!controller.adapter) {
885
+ MessageFormatter.error(
886
+ "No database adapter available. Ensure config has valid credentials.",
887
+ undefined,
888
+ { prefix: "Migration" }
889
+ );
888
890
  return;
889
891
  }
890
- analyzeStringAttributes(controller.config, {
892
+ await analyzeStringAttributes(controller.adapter, controller.config, {
891
893
  outputPath: argv.migrateStringsOutput,
892
894
  });
893
895
  } else if (argv.migrateStringsExecute) {
@@ -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,76 @@ 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
- ];
36
+ ): Promise<MigrationPlan> {
44
37
  const databases = config.databases || [];
38
+ if (databases.length === 0) {
39
+ MessageFormatter.warning("No databases configured. Nothing to analyze.", {
40
+ prefix: "Analyze",
41
+ });
42
+ return emptyPlan(config);
43
+ }
45
44
 
46
45
  const entries: MigrationPlanEntry[] = [];
47
- const collectionsSeen = new Set<string>();
48
46
 
49
47
  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
48
+ MessageFormatter.info(`Scanning database: ${db.name} (${db.$id})`, {
49
+ prefix: "Analyze",
56
50
  });
57
51
 
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);
52
+ // Fetch all collections/tables from server
53
+ const tablesRes = await tryAwaitWithRetry(() =>
54
+ adapter.listTables({ databaseId: db.$id })
55
+ );
56
+ const tables: any[] =
57
+ tablesRes?.tables || tablesRes?.collections || tablesRes?.data || [];
58
+
59
+ for (const table of tables) {
60
+ const tableId: string = table.$id || table.key || table.name;
61
+ const tableName: string = table.name || tableId;
63
62
 
64
- const attributes: Attribute[] = (coll as any).attributes || [];
65
- const indexes: Index[] = (coll as any).indexes || [];
63
+ // Fetch full schema from server
64
+ const schemaRes = await tryAwaitWithRetry(() =>
65
+ adapter.getTable({ databaseId: db.$id, tableId })
66
+ );
67
+ const attributes: any[] =
68
+ schemaRes?.data?.columns || schemaRes?.data?.attributes || [];
69
+
70
+ // Fetch indexes from server
71
+ const indexRes = await tryAwaitWithRetry(() =>
72
+ adapter.listIndexes({ databaseId: db.$id, tableId })
73
+ );
74
+ const indexes: any[] = indexRes?.data || [];
66
75
 
67
76
  for (const attr of attributes) {
68
77
  if (attr.type !== "string") continue;
69
78
 
70
- const stringAttr = attr as any;
71
- const size: number = stringAttr.size || 50;
72
- const isEncrypted = !!stringAttr.encrypt;
79
+ const size: number = attr.size || 50;
80
+ const isEncrypted = !!(attr as any).encrypt;
73
81
  const isRequired = !!attr.required;
74
82
  const isArray = !!attr.array;
75
83
  const hasDefault =
76
- stringAttr.xdefault !== undefined && stringAttr.xdefault !== null;
84
+ attr.xdefault !== undefined && attr.xdefault !== null;
77
85
 
78
86
  // Find indexes that reference this attribute
79
87
  const affectedIndexes = indexes
80
- .filter((idx) => idx.attributes.includes(attr.key))
81
- .map((idx) => idx.key);
88
+ .filter((idx: any) => idx.attributes?.includes(attr.key))
89
+ .map((idx: any) => idx.key);
82
90
  const hasIndex = affectedIndexes.length > 0;
83
91
 
84
92
  const suggested = suggestTargetType(size, hasIndex);
85
93
 
86
- // Build entry
87
94
  const entry: MigrationPlanEntry = {
88
95
  databaseId: db.$id,
89
96
  databaseName: db.name,
90
- collectionId: collId,
91
- collectionName: coll.name,
97
+ collectionId: tableId,
98
+ collectionName: tableName,
92
99
  attributeKey: attr.key,
93
100
  currentType: "string",
94
101
  currentSize: size,
@@ -96,7 +103,7 @@ export function analyzeStringAttributes(
96
103
  isArray,
97
104
  isEncrypted,
98
105
  hasDefault,
99
- defaultValue: hasDefault ? stringAttr.xdefault : undefined,
106
+ defaultValue: hasDefault ? attr.xdefault : undefined,
100
107
  suggestedType: suggested,
101
108
  targetType: suggested,
102
109
  targetSize: suggested === "varchar" ? size : undefined,
@@ -105,12 +112,8 @@ export function analyzeStringAttributes(
105
112
  indexesAffected: affectedIndexes,
106
113
  };
107
114
 
108
- // Warn if indexed attr gets non-varchar suggestion
109
- if (
110
- hasIndex &&
111
- suggested !== "varchar" &&
112
- !isEncrypted
113
- ) {
115
+ // Indexed attrs must stay varchar
116
+ if (hasIndex && suggested !== "varchar" && !isEncrypted) {
114
117
  entry.targetType = "varchar";
115
118
  entry.targetSize = size;
116
119
  }
@@ -161,6 +164,23 @@ export function analyzeStringAttributes(
161
164
  return plan;
162
165
  }
163
166
 
167
+ function emptyPlan(config: AppwriteConfig): MigrationPlan {
168
+ return {
169
+ version: 1,
170
+ generatedAt: new Date().toISOString(),
171
+ appwriteEndpoint: config.appwriteEndpoint,
172
+ appwriteProject: config.appwriteProject,
173
+ summary: {
174
+ totalStringAttributes: 0,
175
+ toMigrate: 0,
176
+ toSkip: 0,
177
+ databaseCount: 0,
178
+ collectionCount: 0,
179
+ },
180
+ entries: [],
181
+ };
182
+ }
183
+
164
184
  function printPlanSummary(plan: MigrationPlan): void {
165
185
  const { summary } = plan;
166
186
  console.log("");