@njdamstra/appwrite-utils-cli 1.10.0 → 1.11.0

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.
@@ -593,6 +593,74 @@ const createLegacyAttribute = async (
593
593
  attribute.array || false
594
594
  );
595
595
  break;
596
+ case "varchar":
597
+ await db.createVarcharAttribute(
598
+ dbId,
599
+ collectionId,
600
+ attribute.key,
601
+ (attribute as any).size || 255,
602
+ attribute.required || false,
603
+ (attribute as any).xdefault !== undefined && !attribute.required
604
+ ? (attribute as any).xdefault
605
+ : undefined,
606
+ attribute.array || false,
607
+ (attribute as any).encrypt
608
+ );
609
+ break;
610
+ case "text":
611
+ case "mediumtext":
612
+ case "longtext": {
613
+ const createFn = attribute.type === "text"
614
+ ? db.createTextAttribute.bind(db)
615
+ : attribute.type === "mediumtext"
616
+ ? db.createMediumtextAttribute.bind(db)
617
+ : db.createLongtextAttribute.bind(db);
618
+ await createFn(
619
+ dbId,
620
+ collectionId,
621
+ attribute.key,
622
+ attribute.required || false,
623
+ (attribute as any).xdefault !== undefined && !attribute.required
624
+ ? (attribute as any).xdefault
625
+ : undefined,
626
+ attribute.array || false,
627
+ (attribute as any).encrypt
628
+ );
629
+ break;
630
+ }
631
+ case "point":
632
+ await db.createPointAttribute(
633
+ dbId,
634
+ collectionId,
635
+ attribute.key,
636
+ attribute.required || false,
637
+ (attribute as any).xdefault !== undefined && !attribute.required
638
+ ? (attribute as any).xdefault
639
+ : undefined
640
+ );
641
+ break;
642
+ case "line":
643
+ await db.createLineAttribute(
644
+ dbId,
645
+ collectionId,
646
+ attribute.key,
647
+ attribute.required || false,
648
+ (attribute as any).xdefault !== undefined && !attribute.required
649
+ ? (attribute as any).xdefault
650
+ : undefined
651
+ );
652
+ break;
653
+ case "polygon":
654
+ await db.createPolygonAttribute(
655
+ dbId,
656
+ collectionId,
657
+ attribute.key,
658
+ attribute.required || false,
659
+ (attribute as any).xdefault !== undefined && !attribute.required
660
+ ? (attribute as any).xdefault
661
+ : undefined
662
+ );
663
+ break;
596
664
  case "relationship":
597
665
  await db.createRelationshipAttribute(
598
666
  dbId,
@@ -615,6 +683,10 @@ const createLegacyAttribute = async (
615
683
  type: (attribute as any).type,
616
684
  supportedTypes: [
617
685
  "string",
686
+ "varchar",
687
+ "text",
688
+ "mediumtext",
689
+ "longtext",
618
690
  "integer",
619
691
  "double",
620
692
  "float",
@@ -624,6 +696,9 @@ const createLegacyAttribute = async (
624
696
  "ip",
625
697
  "url",
626
698
  "enum",
699
+ "point",
700
+ "line",
701
+ "polygon",
627
702
  "relationship",
628
703
  ],
629
704
  operation: "createLegacyAttribute",
@@ -770,6 +845,70 @@ const updateLegacyAttribute = async (
770
845
  : null
771
846
  );
772
847
  break;
848
+ case "varchar":
849
+ await db.updateVarcharAttribute(
850
+ dbId,
851
+ collectionId,
852
+ attribute.key,
853
+ attribute.required || false,
854
+ !attribute.required && (attribute as any).xdefault !== undefined
855
+ ? (attribute as any).xdefault
856
+ : null,
857
+ (attribute as any).size
858
+ );
859
+ break;
860
+ case "text":
861
+ case "mediumtext":
862
+ case "longtext": {
863
+ const updateFn = attribute.type === "text"
864
+ ? db.updateTextAttribute.bind(db)
865
+ : attribute.type === "mediumtext"
866
+ ? db.updateMediumtextAttribute.bind(db)
867
+ : db.updateLongtextAttribute.bind(db);
868
+ await updateFn(
869
+ dbId,
870
+ collectionId,
871
+ attribute.key,
872
+ attribute.required || false,
873
+ !attribute.required && (attribute as any).xdefault !== undefined
874
+ ? (attribute as any).xdefault
875
+ : null
876
+ );
877
+ break;
878
+ }
879
+ case "point":
880
+ await db.updatePointAttribute(
881
+ dbId,
882
+ collectionId,
883
+ attribute.key,
884
+ attribute.required || false,
885
+ !attribute.required && (attribute as any).xdefault !== undefined
886
+ ? (attribute as any).xdefault
887
+ : null
888
+ );
889
+ break;
890
+ case "line":
891
+ await db.updateLineAttribute(
892
+ dbId,
893
+ collectionId,
894
+ attribute.key,
895
+ attribute.required || false,
896
+ !attribute.required && (attribute as any).xdefault !== undefined
897
+ ? (attribute as any).xdefault
898
+ : null
899
+ );
900
+ break;
901
+ case "polygon":
902
+ await db.updatePolygonAttribute(
903
+ dbId,
904
+ collectionId,
905
+ attribute.key,
906
+ attribute.required || false,
907
+ !attribute.required && (attribute as any).xdefault !== undefined
908
+ ? (attribute as any).xdefault
909
+ : null
910
+ );
911
+ break;
773
912
  case "relationship":
774
913
  await db.updateRelationshipAttribute(
775
914
  dbId,
@@ -1048,6 +1187,14 @@ const getComparableFields = (type: string): string[] => {
1048
1187
  case "string":
1049
1188
  return [...baseFields, "size", "encrypt"];
1050
1189
 
1190
+ case "varchar":
1191
+ return [...baseFields, "size", "encrypt"];
1192
+
1193
+ case "text":
1194
+ case "mediumtext":
1195
+ case "longtext":
1196
+ return [...baseFields, "encrypt"];
1197
+
1051
1198
  case "integer":
1052
1199
  case "double":
1053
1200
  case "float":
@@ -1056,6 +1203,11 @@ const getComparableFields = (type: string): string[] => {
1056
1203
  case "enum":
1057
1204
  return [...baseFields, "elements"];
1058
1205
 
1206
+ case "point":
1207
+ case "line":
1208
+ case "polygon":
1209
+ return baseFields;
1210
+
1059
1211
  case "relationship":
1060
1212
  return [
1061
1213
  ...baseFields,
@@ -1,5 +1,5 @@
1
1
  import { indexSchema, type Index } from "@njdamstra/appwrite-utils";
2
- import { Databases, IndexType, Query, type Models } from "node-appwrite";
2
+ import { Databases, IndexType, OrderBy, Query, type Models } from "node-appwrite";
3
3
  import type { DatabaseAdapter } from "@njdamstra/appwrite-utils-helpers";
4
4
  import { delay, tryAwaitWithRetry, calculateExponentialBackoff, isLegacyDatabases, MessageFormatter } from "@njdamstra/appwrite-utils-helpers";
5
5
 
@@ -327,10 +327,10 @@ export const createOrUpdateIndex = async (
327
327
  index.key,
328
328
  index.type as IndexType,
329
329
  index.attributes,
330
- orders
330
+ orders as OrderBy[]
331
331
  );
332
332
  }
333
-
333
+
334
334
  return newIndex;
335
335
  };
336
336
 
@@ -32,6 +32,10 @@ interface ColumnOperationPlan {
32
32
  // Property configuration for different column types
33
33
  const MUTABLE_PROPERTIES = {
34
34
  string: ["required", "default", "size", "array"],
35
+ varchar: ["required", "default", "size", "array"],
36
+ text: ["required", "default", "array"],
37
+ mediumtext: ["required", "default", "array"],
38
+ longtext: ["required", "default", "array"],
35
39
  integer: ["required", "default", "min", "max", "array"],
36
40
  float: ["required", "default", "min", "max", "array"],
37
41
  double: ["required", "default", "min", "max", "array"],
@@ -41,11 +45,18 @@ const MUTABLE_PROPERTIES = {
41
45
  ip: ["required", "default", "array"],
42
46
  url: ["required", "default", "array"],
43
47
  enum: ["required", "default", "elements", "array"],
48
+ point: ["required", "default"],
49
+ line: ["required", "default"],
50
+ polygon: ["required", "default"],
44
51
  relationship: ["required", "default"],
45
52
  } as const;
46
53
 
47
54
  const IMMUTABLE_PROPERTIES = {
48
55
  string: ["encrypt", "key"],
56
+ varchar: ["encrypt", "key"],
57
+ text: ["encrypt", "key"],
58
+ mediumtext: ["encrypt", "key"],
59
+ longtext: ["encrypt", "key"],
49
60
  integer: ["encrypt", "key"],
50
61
  float: ["encrypt", "key"],
51
62
  double: ["encrypt", "key"],
@@ -55,11 +66,18 @@ const IMMUTABLE_PROPERTIES = {
55
66
  ip: ["key"],
56
67
  url: ["key"],
57
68
  enum: ["key"],
69
+ point: ["key"],
70
+ line: ["key"],
71
+ polygon: ["key"],
58
72
  relationship: ["key", "relatedCollection", "relationType", "twoWay", "twoWayKey", "onDelete"],
59
73
  } as const;
60
74
 
61
75
  const TYPE_CHANGE_REQUIRES_RECREATE = [
62
76
  "string",
77
+ "varchar",
78
+ "text",
79
+ "mediumtext",
80
+ "longtext",
63
81
  "integer",
64
82
  "float",
65
83
  "double",
@@ -69,6 +87,9 @@ const TYPE_CHANGE_REQUIRES_RECREATE = [
69
87
  "ip",
70
88
  "url",
71
89
  "enum",
90
+ "point",
91
+ "line",
92
+ "polygon",
72
93
  "relationship",
73
94
  ];
74
95
 
@@ -117,6 +138,13 @@ export function normalizeAttributeToComparable(attr: Attribute): ComparableColum
117
138
  base.size = (attr as any).size ?? 255;
118
139
  base.encrypt = !!((attr as any).encrypt);
119
140
  }
141
+ if (t === 'varchar') {
142
+ base.size = (attr as any).size ?? 255;
143
+ base.encrypt = !!((attr as any).encrypt);
144
+ }
145
+ if (t === 'text' || t === 'mediumtext' || t === 'longtext') {
146
+ base.encrypt = !!((attr as any).encrypt);
147
+ }
120
148
  if (t === 'integer' || t === 'float' || t === 'double') {
121
149
  const min = toNumber((attr as any).min);
122
150
  const max = toNumber((attr as any).max);
@@ -162,6 +190,13 @@ export function normalizeColumnToComparable(col: any): ComparableColumn {
162
190
  base.size = typeof col?.size === 'number' ? col.size : undefined;
163
191
  base.encrypt = !!col?.encrypt;
164
192
  }
193
+ if (t === 'varchar') {
194
+ base.size = typeof col?.size === 'number' ? col.size : undefined;
195
+ base.encrypt = !!col?.encrypt;
196
+ }
197
+ if (t === 'text' || t === 'mediumtext' || t === 'longtext') {
198
+ base.encrypt = !!col?.encrypt;
199
+ }
165
200
  if (t === 'integer' || t === 'float' || t === 'double') {
166
201
  // Preserve raw min/max without forcing extremes; compare with Decimal in shallowEqual
167
202
  const rawMin = (col as any)?.min;
@@ -137,7 +137,7 @@ export const createFunction = async (
137
137
  functionConfig.logging,
138
138
  functionConfig.entrypoint,
139
139
  functionConfig.commands,
140
- functionConfig.scopes,
140
+ functionConfig.scopes as any[],
141
141
  functionConfig.installationId,
142
142
  functionConfig.providerRepositoryId,
143
143
  functionConfig.providerBranch,
@@ -217,7 +217,7 @@ export const updateFunction = async (
217
217
  functionConfig.logging,
218
218
  functionConfig.entrypoint,
219
219
  functionConfig.commands,
220
- functionConfig.scopes,
220
+ functionConfig.scopes as any[],
221
221
  functionConfig.installationId,
222
222
  functionConfig.providerRepositoryId,
223
223
  functionConfig.providerBranch,
@@ -46,6 +46,7 @@ import { storageCommands } from "./cli/commands/storageCommands.js";
46
46
  import { transferCommands } from "./cli/commands/transferCommands.js";
47
47
  import { schemaCommands } from "./cli/commands/schemaCommands.js";
48
48
  import { importFileCommands } from "./cli/commands/importFileCommands.js";
49
+ import { migrateCommands } from "./cli/commands/migrateCommands.js";
49
50
 
50
51
  enum CHOICES {
51
52
  MIGRATE_CONFIG = "🔄 Migrate TypeScript config to YAML (.appwrite structure)",
@@ -71,6 +72,7 @@ enum CHOICES {
71
72
  RELOAD_CONFIG = "🔄 Reload configuration files",
72
73
  UPDATE_FUNCTION_SPEC = "⚙️ Update function specifications",
73
74
  MANAGE_BUCKETS = "🪣 Manage storage buckets",
75
+ MIGRATE_STRINGS = "🔄 Migrate string attributes to varchar/text types",
74
76
  EXIT = "👋 Exit",
75
77
  }
76
78
 
@@ -92,7 +94,7 @@ export class InteractiveCLI {
92
94
  async run(): Promise<void> {
93
95
  MessageFormatter.banner(
94
96
  "Appwrite Utils CLI",
95
- "Welcome to Appwrite Utils CLI Tool by Zach Handley"
97
+ "Welcome to Appwrite Utils CLI Tool"
96
98
  );
97
99
  MessageFormatter.info(
98
100
  "For more information, visit https://github.com/njdamstra/AppwriteUtils"
@@ -203,6 +205,10 @@ export class InteractiveCLI {
203
205
  case CHOICES.MANAGE_BUCKETS:
204
206
  await this.manageBuckets();
205
207
  break;
208
+ case CHOICES.MIGRATE_STRINGS:
209
+ await this.initControllerIfNeeded();
210
+ await migrateCommands.migrateStrings(this);
211
+ break;
206
212
  case CHOICES.EXIT:
207
213
  MessageFormatter.success("Goodbye!");
208
214
  process.exit(0);
@@ -1069,7 +1075,7 @@ export class InteractiveCLI {
1069
1075
  compression: bucketCompressionType as Compression,
1070
1076
  encryption: bucketEncryption,
1071
1077
  antivirus: bucketAntivirus,
1072
- },
1078
+ } as any,
1073
1079
  bucketId.length > 0 ? bucketId : ulid()
1074
1080
  );
1075
1081
  }
@@ -1111,7 +1117,7 @@ export class InteractiveCLI {
1111
1117
  name: db.name,
1112
1118
  enabled: true,
1113
1119
  type: "tablesdb" as DatabaseType,
1114
- }));
1120
+ } as Models.Database));
1115
1121
  }
1116
1122
 
1117
1123
 
package/src/main.ts CHANGED
@@ -82,6 +82,12 @@ interface CliOptions {
82
82
  importFile?: string;
83
83
  targetDb?: string;
84
84
  targetTable?: string;
85
+ // String attribute migration
86
+ migrateStringsAnalyze?: boolean;
87
+ migrateStringsExecute?: string;
88
+ migrateStringsOutput?: string;
89
+ migrateStringsKeepBackups?: boolean;
90
+ migrateStringsDryRun?: boolean;
85
91
  }
86
92
 
87
93
  type ParsedArgv = ArgumentsCamelCase<CliOptions>;
@@ -621,6 +627,32 @@ const argv = yargs(hideBin(process.argv))
621
627
  type: "string",
622
628
  description: "Target table ID for --importFile (prompted if omitted)",
623
629
  })
630
+ .option("migrateStringsAnalyze", {
631
+ alias: ["migrate-strings-analyze"],
632
+ type: "boolean",
633
+ description: "Analyze local configs and generate a string-to-varchar/text migration plan (YAML)",
634
+ })
635
+ .option("migrateStringsExecute", {
636
+ alias: ["migrate-strings-execute"],
637
+ type: "string",
638
+ description: "Execute a string migration plan from the given YAML path",
639
+ })
640
+ .option("migrateStringsOutput", {
641
+ alias: ["migrate-strings-output"],
642
+ type: "string",
643
+ description: "Output path for the migration plan (default: ./migrate-strings-plan.yaml)",
644
+ })
645
+ .option("migrateStringsKeepBackups", {
646
+ alias: ["migrate-strings-keep-backups"],
647
+ type: "boolean",
648
+ default: true,
649
+ description: "Keep backup attributes after migration (default: true)",
650
+ })
651
+ .option("migrateStringsDryRun", {
652
+ alias: ["migrate-strings-dry-run"],
653
+ type: "boolean",
654
+ description: "Dry run — show what would happen without making changes",
655
+ })
624
656
  .parse() as ParsedArgv;
625
657
 
626
658
  async function main() {
@@ -842,6 +874,43 @@ async function main() {
842
874
  return;
843
875
  }
844
876
 
877
+ // String attribute migration (analyze or execute)
878
+ if (argv.migrateStringsAnalyze || argv.migrateStringsExecute) {
879
+ const { analyzeStringAttributes, executeMigrationPlan } = await import(
880
+ "./migrations/migrateStrings.js"
881
+ );
882
+
883
+ if (argv.migrateStringsAnalyze) {
884
+ if (!controller.config) {
885
+ MessageFormatter.error("No Appwrite configuration found", undefined, {
886
+ prefix: "Migration",
887
+ });
888
+ return;
889
+ }
890
+ analyzeStringAttributes(controller.config, {
891
+ outputPath: argv.migrateStringsOutput,
892
+ });
893
+ } else if (argv.migrateStringsExecute) {
894
+ if (!controller.adapter) {
895
+ MessageFormatter.error(
896
+ "No database adapter available. Ensure config has valid credentials.",
897
+ undefined,
898
+ { prefix: "Migration" }
899
+ );
900
+ return;
901
+ }
902
+ const results = await executeMigrationPlan(controller.adapter, {
903
+ planPath: argv.migrateStringsExecute,
904
+ keepBackups: argv.migrateStringsKeepBackups ?? true,
905
+ dryRun: argv.migrateStringsDryRun ?? false,
906
+ });
907
+ if (results.failed > 0) {
908
+ process.exit(1);
909
+ }
910
+ }
911
+ return;
912
+ }
913
+
845
914
  // List backups if requested
846
915
  if (parsedArgv.listBackups) {
847
916
  const { AdapterFactory } = await import("@njdamstra/appwrite-utils-helpers");