appwrite-utils-cli 1.7.9 → 1.8.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.
Files changed (70) hide show
  1. package/CHANGELOG.md +14 -199
  2. package/README.md +87 -30
  3. package/dist/adapters/AdapterFactory.js +5 -25
  4. package/dist/adapters/DatabaseAdapter.d.ts +17 -2
  5. package/dist/adapters/LegacyAdapter.d.ts +2 -1
  6. package/dist/adapters/LegacyAdapter.js +212 -16
  7. package/dist/adapters/TablesDBAdapter.d.ts +2 -12
  8. package/dist/adapters/TablesDBAdapter.js +261 -57
  9. package/dist/cli/commands/databaseCommands.js +4 -3
  10. package/dist/cli/commands/functionCommands.js +17 -8
  11. package/dist/collections/attributes.js +447 -125
  12. package/dist/collections/methods.js +197 -186
  13. package/dist/collections/tableOperations.d.ts +86 -0
  14. package/dist/collections/tableOperations.js +434 -0
  15. package/dist/collections/transferOperations.d.ts +3 -2
  16. package/dist/collections/transferOperations.js +93 -12
  17. package/dist/config/yamlConfig.d.ts +221 -88
  18. package/dist/examples/yamlTerminologyExample.d.ts +1 -1
  19. package/dist/examples/yamlTerminologyExample.js +6 -3
  20. package/dist/functions/fnConfigDiscovery.d.ts +3 -0
  21. package/dist/functions/fnConfigDiscovery.js +108 -0
  22. package/dist/interactiveCLI.js +18 -15
  23. package/dist/main.js +211 -73
  24. package/dist/migrations/appwriteToX.d.ts +88 -23
  25. package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
  26. package/dist/migrations/comprehensiveTransfer.js +83 -6
  27. package/dist/migrations/dataLoader.d.ts +227 -69
  28. package/dist/migrations/dataLoader.js +3 -3
  29. package/dist/migrations/importController.js +3 -3
  30. package/dist/migrations/relationships.d.ts +8 -2
  31. package/dist/migrations/services/ImportOrchestrator.js +3 -3
  32. package/dist/migrations/transfer.js +159 -37
  33. package/dist/shared/attributeMapper.d.ts +20 -0
  34. package/dist/shared/attributeMapper.js +203 -0
  35. package/dist/shared/selectionDialogs.js +8 -4
  36. package/dist/storage/schemas.d.ts +354 -92
  37. package/dist/utils/configDiscovery.js +4 -3
  38. package/dist/utils/versionDetection.d.ts +0 -4
  39. package/dist/utils/versionDetection.js +41 -173
  40. package/dist/utils/yamlConverter.js +89 -16
  41. package/dist/utils/yamlLoader.d.ts +1 -1
  42. package/dist/utils/yamlLoader.js +6 -2
  43. package/dist/utilsController.js +56 -19
  44. package/package.json +4 -4
  45. package/src/adapters/AdapterFactory.ts +119 -143
  46. package/src/adapters/DatabaseAdapter.ts +18 -3
  47. package/src/adapters/LegacyAdapter.ts +236 -105
  48. package/src/adapters/TablesDBAdapter.ts +773 -643
  49. package/src/cli/commands/databaseCommands.ts +13 -12
  50. package/src/cli/commands/functionCommands.ts +23 -14
  51. package/src/collections/attributes.ts +2054 -1611
  52. package/src/collections/methods.ts +208 -293
  53. package/src/collections/tableOperations.ts +506 -0
  54. package/src/collections/transferOperations.ts +218 -144
  55. package/src/examples/yamlTerminologyExample.ts +10 -5
  56. package/src/functions/fnConfigDiscovery.ts +103 -0
  57. package/src/interactiveCLI.ts +25 -20
  58. package/src/main.ts +549 -194
  59. package/src/migrations/comprehensiveTransfer.ts +126 -50
  60. package/src/migrations/dataLoader.ts +3 -3
  61. package/src/migrations/importController.ts +3 -3
  62. package/src/migrations/services/ImportOrchestrator.ts +3 -3
  63. package/src/migrations/transfer.ts +148 -131
  64. package/src/shared/attributeMapper.ts +229 -0
  65. package/src/shared/selectionDialogs.ts +29 -25
  66. package/src/utils/configDiscovery.ts +9 -3
  67. package/src/utils/versionDetection.ts +74 -228
  68. package/src/utils/yamlConverter.ts +94 -17
  69. package/src/utils/yamlLoader.ts +11 -4
  70. package/src/utilsController.ts +80 -30
@@ -1,8 +1,95 @@
1
1
  import yaml from "js-yaml";
2
2
  import type { Collection, CollectionCreate } from "appwrite-utils";
3
+ import { Decimal } from "decimal.js";
3
4
 
4
- // Threshold for treating min/max values as undefined (1 trillion)
5
- const MIN_MAX_THRESHOLD = 1_000_000_000_000;
5
+ // Extreme values that Appwrite may return, which should be treated as undefined
6
+ const EXTREME_MIN_INTEGER = -9223372036854776000;
7
+ const EXTREME_MAX_INTEGER = 9223372036854776000;
8
+ const EXTREME_MIN_FLOAT = -1.7976931348623157e+308;
9
+ const EXTREME_MAX_FLOAT = 1.7976931348623157e+308;
10
+
11
+ /**
12
+ * Type guard to check if an attribute has min/max properties
13
+ */
14
+ const hasMinMaxProperties = (yamlAttr: any): boolean => {
15
+ return yamlAttr.type === 'integer' || yamlAttr.type === 'double' || yamlAttr.type === 'float';
16
+ };
17
+
18
+ /**
19
+ * Normalizes min/max values for integer and float attributes using Decimal.js for precision
20
+ * Validates that min < max and handles extreme database values
21
+ */
22
+ const normalizeMinMaxValues = (yamlAttr: any): { min?: number; max?: number } => {
23
+ if (!hasMinMaxProperties(yamlAttr)) {
24
+ return {};
25
+ }
26
+
27
+ const { type, min, max, key } = yamlAttr;
28
+ let normalizedMin = min;
29
+ let normalizedMax = max;
30
+
31
+ // Handle min value - only filter out extreme database values
32
+ if (normalizedMin !== undefined && normalizedMin !== null) {
33
+ const minValue = Number(normalizedMin);
34
+ const originalMin = normalizedMin;
35
+
36
+ // Check if it's an extreme database value (but don't filter out large numbers)
37
+ if (type === 'integer') {
38
+ if (minValue === EXTREME_MIN_INTEGER) {
39
+ console.debug(`Min value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`);
40
+ normalizedMin = undefined;
41
+ }
42
+ } else { // float/double
43
+ if (minValue === EXTREME_MIN_FLOAT) {
44
+ console.debug(`Min value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`);
45
+ normalizedMin = undefined;
46
+ }
47
+ }
48
+ }
49
+
50
+ // Handle max value - only filter out extreme database values
51
+ if (normalizedMax !== undefined && normalizedMax !== null) {
52
+ const maxValue = Number(normalizedMax);
53
+ const originalMax = normalizedMax;
54
+
55
+ // Check if it's an extreme database value (but don't filter out large numbers)
56
+ if (type === 'integer') {
57
+ if (maxValue === EXTREME_MAX_INTEGER) {
58
+ console.debug(`Max value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`);
59
+ normalizedMax = undefined;
60
+ }
61
+ } else { // float/double
62
+ if (maxValue === EXTREME_MAX_FLOAT) {
63
+ console.debug(`Max value normalized to undefined for attribute '${yamlAttr.key}': extreme database value`);
64
+ normalizedMax = undefined;
65
+ }
66
+ }
67
+ }
68
+
69
+ // Validate that min < max using Decimal.js for safe comparison
70
+ if (normalizedMin !== undefined && normalizedMax !== undefined &&
71
+ normalizedMin !== null && normalizedMax !== null) {
72
+ try {
73
+ const minDecimal = new Decimal(normalizedMin.toString());
74
+ const maxDecimal = new Decimal(normalizedMax.toString());
75
+
76
+ if (minDecimal.greaterThanOrEqualTo(maxDecimal)) {
77
+ // Swap values to ensure min < max (graceful handling)
78
+ console.warn(`Swapping min/max values for attribute '${yamlAttr.key}' to fix validation: min (${normalizedMin}) must be less than max (${normalizedMax})`);
79
+ const temp = normalizedMin;
80
+ normalizedMin = normalizedMax;
81
+ normalizedMax = temp;
82
+ }
83
+ } catch (error) {
84
+ console.error(`Error comparing min/max values for attribute '${yamlAttr.key}':`, error);
85
+ // If Decimal comparison fails, set both to undefined to avoid API errors
86
+ normalizedMin = undefined;
87
+ normalizedMax = undefined;
88
+ }
89
+ }
90
+
91
+ return { min: normalizedMin, max: normalizedMax };
92
+ };
6
93
 
7
94
  export interface YamlCollectionData {
8
95
  name: string;
@@ -125,21 +212,11 @@ export function collectionToYaml(
125
212
 
126
213
  if ('xdefault' in attr && attr.xdefault !== undefined) yamlAttr.default = attr.xdefault;
127
214
 
128
- // Normalize min/max values - filter out extreme database values
129
- if ('min' in attr && attr.min !== undefined) {
130
- const minValue = Number(attr.min);
131
- // Only include min if it's within reasonable range (< 1 trillion)
132
- if (Math.abs(minValue) < MIN_MAX_THRESHOLD) {
133
- yamlAttr.min = attr.min;
134
- }
135
- }
136
-
137
- if ('max' in attr && attr.max !== undefined) {
138
- const maxValue = Number(attr.max);
139
- // Only include max if it's within reasonable range (< 1 trillion)
140
- if (Math.abs(maxValue) < MIN_MAX_THRESHOLD) {
141
- yamlAttr.max = attr.max;
142
- }
215
+ // Normalize min/max values using Decimal.js precision
216
+ if ('min' in attr || 'max' in attr) {
217
+ const { min, max } = normalizeMinMaxValues(attr);
218
+ if (min !== undefined) yamlAttr.min = min;
219
+ if (max !== undefined) yamlAttr.max = max;
143
220
  }
144
221
  if ('elements' in attr && attr.elements !== undefined) yamlAttr.elements = attr.elements;
145
222
  if ('relatedCollection' in attr && attr.relatedCollection !== undefined) yamlAttr.relatedCollection = attr.relatedCollection;
@@ -9,7 +9,10 @@ import {
9
9
  type YamlCollectionData,
10
10
  type YamlTerminologyConfig
11
11
  } from "./yamlConverter.js";
12
- import type { CollectionCreate } from "appwrite-utils";
12
+ import {
13
+ CollectionCreateSchema,
14
+ type CollectionCreate,
15
+ } from "appwrite-utils";
13
16
 
14
17
  /**
15
18
  * Enhanced YAML loader with dual terminology support
@@ -149,7 +152,7 @@ export class YamlLoader {
149
152
  // Always normalize to ensure consistent attribute terminology
150
153
  const normalized = normalizeYamlData(yamlData);
151
154
 
152
- return {
155
+ const collectionInput: CollectionCreate = {
153
156
  name: normalized.name,
154
157
  $id: normalized.id || normalized.name.toLowerCase().replace(/\s+/g, '_'),
155
158
  enabled: normalized.enabled !== false,
@@ -173,7 +176,9 @@ export class YamlLoader {
173
176
  twoWay: attr.twoWay,
174
177
  twoWayKey: attr.twoWayKey,
175
178
  onDelete: attr.onDelete as any,
176
- side: attr.side as any
179
+ side: attr.side as any,
180
+ encrypt: (attr as any).encrypt,
181
+ format: (attr as any).format
177
182
  })) || [],
178
183
  indexes: normalized.indexes?.map(idx => ({
179
184
  key: idx.key,
@@ -183,6 +188,8 @@ export class YamlLoader {
183
188
  })) || [],
184
189
  importDefs: normalized.importDefs || []
185
190
  };
191
+
192
+ return CollectionCreateSchema.parse(collectionInput);
186
193
  }
187
194
 
188
195
  /**
@@ -361,4 +368,4 @@ export class YamlLoader {
361
368
  */
362
369
  export function createYamlLoader(baseDirectory: string): YamlLoader {
363
370
  return new YamlLoader(baseDirectory);
364
- }
371
+ }
@@ -27,6 +27,7 @@ import {
27
27
  } from "./databases/setup.js";
28
28
  import {
29
29
  createOrUpdateCollections,
30
+ createOrUpdateCollectionsViaAdapter,
30
31
  wipeDatabase,
31
32
  generateSchemas,
32
33
  fetchAllCollections,
@@ -273,6 +274,13 @@ export class UtilsController {
273
274
  this.appwriteServer = client;
274
275
  this.adapter = adapter;
275
276
  this.config = config;
277
+
278
+ // Update config.apiMode from adapter if it's auto or not set
279
+ if (adapter && (!config.apiMode || config.apiMode === 'auto')) {
280
+ this.config.apiMode = adapter.getApiMode();
281
+ logger.debug(`Updated config.apiMode from adapter during init: ${this.config.apiMode}`, { prefix: "UtilsController" });
282
+ }
283
+
276
284
  this.database = new Databases(this.appwriteServer);
277
285
  this.storage = new Storage(this.appwriteServer);
278
286
  this.config.appwriteClient = this.appwriteServer;
@@ -280,7 +288,8 @@ export class UtilsController {
280
288
  // Log only on FIRST initialization to avoid spam
281
289
  if (!this.isInitialized) {
282
290
  const apiMode = adapter.getApiMode();
283
- MessageFormatter.info(`Database adapter initialized (apiMode: ${apiMode})`, { prefix: "Adapter" });
291
+ const configApiMode = this.config.apiMode;
292
+ MessageFormatter.info(`Database adapter initialized (apiMode: ${apiMode}, config.apiMode: ${configApiMode})`, { prefix: "Adapter" });
284
293
  this.isInitialized = true;
285
294
  } else {
286
295
  logger.debug("Adapter reused from cache", { prefix: "UtilsController" });
@@ -601,22 +610,46 @@ export class UtilsController {
601
610
  }
602
611
  }
603
612
 
604
- async createOrUpdateCollections(
605
- database: Models.Database,
606
- deletedCollections?: { collectionId: string; collectionName: string }[],
607
- collections: Models.Collection[] = []
608
- ) {
613
+ async createOrUpdateCollections(
614
+ database: Models.Database,
615
+ deletedCollections?: { collectionId: string; collectionName: string }[],
616
+ collections: Models.Collection[] = []
617
+ ) {
609
618
  await this.init();
610
619
  if (!this.database || !this.config)
611
620
  throw new Error("Database or config not initialized");
612
- await createOrUpdateCollections(
613
- this.database,
614
- database.$id,
615
- this.config,
616
- deletedCollections,
617
- collections
618
- );
619
- }
621
+
622
+ // Ensure apiMode is properly set from adapter
623
+ if (this.adapter && (!this.config.apiMode || this.config.apiMode === 'auto')) {
624
+ this.config.apiMode = this.adapter.getApiMode();
625
+ logger.debug(`Updated config.apiMode from adapter: ${this.config.apiMode}`, { prefix: "UtilsController" });
626
+ }
627
+
628
+ // Always prefer adapter path for unified behavior. LegacyAdapter internally translates when needed.
629
+ if (this.adapter) {
630
+ logger.debug("Using adapter for createOrUpdateCollections (unified path)", {
631
+ prefix: "UtilsController",
632
+ apiMode: this.adapter.getApiMode()
633
+ });
634
+ await createOrUpdateCollectionsViaAdapter(
635
+ this.adapter,
636
+ database.$id,
637
+ this.config,
638
+ deletedCollections,
639
+ collections
640
+ );
641
+ } else {
642
+ // Fallback if adapter is unavailable for some reason
643
+ logger.debug("Adapter unavailable, falling back to legacy Databases path", { prefix: "UtilsController" });
644
+ await createOrUpdateCollections(
645
+ this.database,
646
+ database.$id,
647
+ this.config,
648
+ deletedCollections,
649
+ collections
650
+ );
651
+ }
652
+ }
620
653
 
621
654
  async generateSchemas() {
622
655
  // Schema generation doesn't need Appwrite connection, just config
@@ -863,36 +896,53 @@ export class UtilsController {
863
896
  }
864
897
 
865
898
  // PUSH OPERATION: Push local configuration to Appwrite
866
- // Build selected collections/tables from databaseSelections
867
- const selectedCollections: any[] = [];
899
+ // Build database-specific collection mappings from databaseSelections
900
+ const databaseCollectionsMap = new Map<string, any[]>();
868
901
 
869
902
  // Get all collections/tables from config (they're at the root level, not nested in databases)
870
903
  const allCollections = this.config?.collections || this.config?.tables || [];
871
904
 
872
- // Collect all selected table IDs from all database selections
873
- const selectedTableIds = new Set<string>();
905
+ // Create database-specific collection mapping to preserve relationships
874
906
  for (const dbSelection of databaseSelections) {
875
- for (const tableId of dbSelection.tableIds) {
876
- selectedTableIds.add(tableId);
877
- }
878
- }
907
+ const collectionsForDatabase: any[] = [];
879
908
 
880
- // Filter to only the selected table IDs
881
- for (const collection of allCollections) {
882
- const collectionId = collection.$id || (collection as any).id;
883
- if (selectedTableIds.has(collectionId)) {
884
- selectedCollections.push(collection);
909
+ MessageFormatter.info(`Processing collections for database: ${dbSelection.databaseId}`, { prefix: "Controller" });
910
+
911
+ // Filter collections that were selected for THIS specific database
912
+ for (const collection of allCollections) {
913
+ const collectionId = collection.$id || (collection as any).id;
914
+
915
+ // Check if this collection was selected for THIS database
916
+ if (dbSelection.tableIds.includes(collectionId)) {
917
+ collectionsForDatabase.push(collection);
918
+ MessageFormatter.info(` - Selected collection: ${collection.name || collectionId} for database ${dbSelection.databaseId}`, { prefix: "Controller" });
919
+ }
885
920
  }
921
+
922
+ databaseCollectionsMap.set(dbSelection.databaseId, collectionsForDatabase);
923
+ MessageFormatter.info(`Database ${dbSelection.databaseId}: ${collectionsForDatabase.length} collections selected`, { prefix: "Controller" });
886
924
  }
887
925
 
888
- MessageFormatter.info(`Pushing ${selectedCollections.length} selected tables/collections to Appwrite`, { prefix: "Controller" });
926
+ // Calculate total collections for logging
927
+ const totalSelectedCollections = Array.from(databaseCollectionsMap.values())
928
+ .reduce((total, collections) => total + collections.length, 0);
929
+
930
+ MessageFormatter.info(`Pushing ${totalSelectedCollections} selected tables/collections to ${databaseCollectionsMap.size} databases`, { prefix: "Controller" });
889
931
 
890
932
  // Ensure databases exist
891
933
  await this.ensureDatabasesExist(selectedDatabases);
892
934
  await this.ensureDatabaseConfigBucketsExist(selectedDatabases);
893
935
 
894
- // Create/update ONLY the selected collections/tables
895
- await this.createOrUpdateCollectionsForDatabases(selectedDatabases, selectedCollections);
936
+ // Create/update collections with database-specific context
937
+ for (const database of selectedDatabases) {
938
+ const collectionsForThisDatabase = databaseCollectionsMap.get(database.$id) || [];
939
+ if (collectionsForThisDatabase.length > 0) {
940
+ MessageFormatter.info(`Pushing ${collectionsForThisDatabase.length} collections to database ${database.$id} (${database.name})`, { prefix: "Controller" });
941
+ await this.createOrUpdateCollections(database, undefined, collectionsForThisDatabase);
942
+ } else {
943
+ MessageFormatter.info(`No collections selected for database ${database.$id} (${database.name})`, { prefix: "Controller" });
944
+ }
945
+ }
896
946
 
897
947
  MessageFormatter.success("Selective push completed successfully! Local config pushed to Appwrite.", { prefix: "Controller" });
898
948
  }