appwrite-utils-cli 1.7.7 → 1.7.9

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 (65) hide show
  1. package/SELECTION_DIALOGS.md +146 -0
  2. package/dist/cli/commands/databaseCommands.js +89 -23
  3. package/dist/config/services/ConfigLoaderService.d.ts +7 -0
  4. package/dist/config/services/ConfigLoaderService.js +47 -1
  5. package/dist/functions/deployments.js +5 -23
  6. package/dist/functions/methods.js +4 -2
  7. package/dist/functions/pathResolution.d.ts +37 -0
  8. package/dist/functions/pathResolution.js +185 -0
  9. package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
  10. package/dist/functions/templates/count-docs-in-collection/package.json +25 -0
  11. package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
  12. package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
  13. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +28 -0
  14. package/dist/functions/templates/hono-typescript/README.md +286 -0
  15. package/dist/functions/templates/hono-typescript/package.json +26 -0
  16. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  17. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  18. package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
  19. package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
  20. package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
  21. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  22. package/dist/functions/templates/hono-typescript/tsconfig.json +20 -0
  23. package/dist/functions/templates/typescript-node/README.md +32 -0
  24. package/dist/functions/templates/typescript-node/package.json +25 -0
  25. package/dist/functions/templates/typescript-node/src/context.ts +103 -0
  26. package/dist/functions/templates/typescript-node/src/index.ts +29 -0
  27. package/dist/functions/templates/typescript-node/tsconfig.json +28 -0
  28. package/dist/functions/templates/uv/README.md +31 -0
  29. package/dist/functions/templates/uv/pyproject.toml +30 -0
  30. package/dist/functions/templates/uv/src/__init__.py +0 -0
  31. package/dist/functions/templates/uv/src/context.py +125 -0
  32. package/dist/functions/templates/uv/src/index.py +46 -0
  33. package/dist/main.js +175 -4
  34. package/dist/migrations/appwriteToX.d.ts +27 -2
  35. package/dist/migrations/appwriteToX.js +293 -69
  36. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
  37. package/dist/migrations/yaml/generateImportSchemas.js +23 -8
  38. package/dist/shared/schemaGenerator.js +25 -12
  39. package/dist/shared/selectionDialogs.d.ts +214 -0
  40. package/dist/shared/selectionDialogs.js +540 -0
  41. package/dist/utils/configDiscovery.d.ts +4 -4
  42. package/dist/utils/configDiscovery.js +66 -30
  43. package/dist/utils/yamlConverter.d.ts +1 -0
  44. package/dist/utils/yamlConverter.js +26 -3
  45. package/dist/utilsController.d.ts +7 -1
  46. package/dist/utilsController.js +198 -17
  47. package/package.json +4 -2
  48. package/scripts/copy-templates.ts +23 -0
  49. package/src/cli/commands/databaseCommands.ts +133 -34
  50. package/src/config/services/ConfigLoaderService.ts +62 -1
  51. package/src/functions/deployments.ts +10 -35
  52. package/src/functions/methods.ts +4 -2
  53. package/src/functions/pathResolution.ts +227 -0
  54. package/src/main.ts +276 -34
  55. package/src/migrations/appwriteToX.ts +385 -90
  56. package/src/migrations/yaml/generateImportSchemas.ts +26 -8
  57. package/src/shared/schemaGenerator.ts +29 -12
  58. package/src/shared/selectionDialogs.ts +745 -0
  59. package/src/utils/configDiscovery.ts +83 -39
  60. package/src/utils/yamlConverter.ts +29 -3
  61. package/src/utilsController.ts +250 -22
  62. package/dist/utils/schemaStrings.d.ts +0 -14
  63. package/dist/utils/schemaStrings.js +0 -428
  64. package/dist/utils/sessionPreservationExample.d.ts +0 -1666
  65. package/dist/utils/sessionPreservationExample.js +0 -101
@@ -157,6 +157,53 @@ const YamlCollectionSchema = z.object({
157
157
 
158
158
  type YamlCollection = z.infer<typeof YamlCollectionSchema>;
159
159
 
160
+ // YAML Table Schema - Supports table-specific terminology
161
+ const YamlTableSchema = z.object({
162
+ name: z.string(),
163
+ id: z.string().optional(),
164
+ rowSecurity: z.boolean().default(false), // Tables use rowSecurity
165
+ enabled: z.boolean().default(true),
166
+ permissions: z.array(
167
+ z.object({
168
+ permission: z.string(),
169
+ target: z.string()
170
+ })
171
+ ).optional().default([]),
172
+ columns: z.array( // Tables use columns terminology
173
+ z.object({
174
+ key: z.string(),
175
+ type: z.string(),
176
+ size: z.number().optional(),
177
+ required: z.boolean().default(false),
178
+ array: z.boolean().optional(),
179
+ encrypted: z.boolean().optional(), // Tables support encrypted property
180
+ default: z.any().optional(),
181
+ min: z.number().optional(),
182
+ max: z.number().optional(),
183
+ elements: z.array(z.string()).optional(),
184
+ relatedTable: z.string().optional(), // Tables use relatedTable
185
+ relationType: z.string().optional(),
186
+ twoWay: z.boolean().optional(),
187
+ twoWayKey: z.string().optional(),
188
+ onDelete: z.string().optional(),
189
+ side: z.string().optional(),
190
+ encrypt: z.boolean().optional(),
191
+ format: z.string().optional()
192
+ })
193
+ ).optional().default([]),
194
+ indexes: z.array(
195
+ z.object({
196
+ key: z.string(),
197
+ type: z.string(),
198
+ columns: z.array(z.string()), // Tables use columns in indexes
199
+ orders: z.array(z.string()).optional()
200
+ })
201
+ ).optional().default([]),
202
+ importDefs: z.array(z.any()).optional().default([])
203
+ });
204
+
205
+ type YamlTable = z.infer<typeof YamlTableSchema>;
206
+
160
207
  /**
161
208
  * Loads a YAML collection file and converts it to CollectionCreate format
162
209
  * @param filePath Path to the YAML collection file
@@ -214,57 +261,54 @@ export const loadYamlCollection = (filePath: string): CollectionCreate | null =>
214
261
  };
215
262
 
216
263
  /**
217
- * Loads a YAML table file and converts it to table format
264
+ * Loads a YAML table file and converts it to CollectionCreate format
218
265
  * @param filePath Path to the YAML table file
219
- * @returns Table object or null if loading fails
266
+ * @returns CollectionCreate object or null if loading fails
220
267
  */
221
- export const loadYamlTable = (filePath: string): any | null => {
268
+ export const loadYamlTable = (filePath: string): CollectionCreate | null => {
222
269
  try {
223
270
  const fileContent = fs.readFileSync(filePath, "utf8");
224
271
  const yamlData = yaml.load(fileContent) as unknown;
225
272
 
226
- // For now, use the collection schema as base and adapt for tables
227
- const parsedTable = YamlCollectionSchema.parse(yamlData);
273
+ // Use the new table-specific schema
274
+ const parsedTable = YamlTableSchema.parse(yamlData);
228
275
 
229
- // Convert YAML table to TableCreate format
230
- const table: any = {
276
+ // Convert YAML table to CollectionCreate format (internal representation)
277
+ const table: CollectionCreate = {
231
278
  name: parsedTable.name,
232
- tableId: (yamlData as any).tableId || parsedTable.id || parsedTable.name.toLowerCase().replace(/\s+/g, '_'),
233
- documentSecurity: parsedTable.documentSecurity,
279
+ $id: (yamlData as any).tableId || parsedTable.id || parsedTable.name.toLowerCase().replace(/\s+/g, '_'),
280
+ documentSecurity: parsedTable.rowSecurity, // Convert rowSecurity to documentSecurity
234
281
  enabled: parsedTable.enabled,
235
282
  $permissions: parsedTable.permissions.map(p => ({
236
283
  permission: p.permission as any,
237
284
  target: p.target
238
285
  })),
239
- attributes: parsedTable.attributes.map(attr => ({
240
- key: attr.key,
241
- type: attr.type as any,
242
- size: attr.size,
243
- required: attr.required,
244
- array: attr.array,
245
- xdefault: attr.default,
246
- min: attr.min,
247
- max: attr.max,
248
- elements: attr.elements,
249
- relatedCollection: attr.relatedCollection,
250
- relationType: attr.relationType as any,
251
- twoWay: attr.twoWay,
252
- twoWayKey: attr.twoWayKey,
253
- onDelete: attr.onDelete as any,
254
- side: attr.side as any,
255
- encrypted: (attr as any).encrypt,
256
- format: (attr as any).format
286
+ attributes: parsedTable.columns.map(col => ({ // Convert columns to attributes
287
+ key: col.key,
288
+ type: col.type as any,
289
+ size: col.size,
290
+ required: col.required,
291
+ array: col.array,
292
+ xdefault: col.default,
293
+ min: col.min,
294
+ max: col.max,
295
+ elements: col.elements,
296
+ relatedCollection: col.relatedTable, // Convert relatedTable to relatedCollection
297
+ relationType: col.relationType as any,
298
+ twoWay: col.twoWay,
299
+ twoWayKey: col.twoWayKey,
300
+ onDelete: col.onDelete as any,
301
+ side: col.side as any,
302
+ encrypted: col.encrypted || col.encrypt, // Support both encrypted and encrypt
303
+ format: col.format
257
304
  })),
258
305
  indexes: parsedTable.indexes.map(idx => ({
259
306
  key: idx.key,
260
307
  type: idx.type as any,
261
- attributes: idx.attributes,
308
+ attributes: idx.columns, // Convert columns to attributes
262
309
  orders: idx.orders as any
263
310
  })),
264
- importDefs: parsedTable.importDefs,
265
- databaseId: (yamlData as any).databaseId,
266
- // Add backward compatibility field
267
- $id: (yamlData as any).$id || parsedTable.id
311
+ importDefs: parsedTable.importDefs || []
268
312
  };
269
313
 
270
314
  return table;
@@ -346,7 +390,7 @@ export const discoverCollections = async (collectionsDir: string): Promise<Colle
346
390
  * Result of discovering tables from a directory
347
391
  */
348
392
  export interface TableDiscoveryResult {
349
- tables: any[];
393
+ tables: CollectionCreate[];
350
394
  loadedNames: Set<string>;
351
395
  conflicts: Array<{ name: string; source1: string; source2: string }>;
352
396
  }
@@ -361,7 +405,7 @@ export const discoverTables = async (
361
405
  tablesDir: string,
362
406
  existingNames: Set<string> = new Set()
363
407
  ): Promise<TableDiscoveryResult> => {
364
- const tables: any[] = [];
408
+ const tables: CollectionCreate[] = [];
365
409
  const loadedNames = new Set<string>();
366
410
  const conflicts: Array<{ name: string; source1: string; source2: string }> = [];
367
411
 
@@ -380,7 +424,7 @@ export const discoverTables = async (
380
424
  continue;
381
425
  }
382
426
  const filePath = path.join(tablesDir, file);
383
- let table: any | null = null;
427
+ let table: CollectionCreate | null = null;
384
428
 
385
429
  // Handle YAML tables
386
430
  if (file.endsWith('.yaml') || file.endsWith('.yml')) {
@@ -391,7 +435,7 @@ export const discoverTables = async (
391
435
  else if (file.endsWith('.ts')) {
392
436
  const fileUrl = pathToFileURL(filePath).href;
393
437
  const tableModule = (await import(fileUrl));
394
- const importedTable: any = tableModule.default?.default || tableModule.default || tableModule;
438
+ const importedTable: CollectionCreate = tableModule.default?.default || tableModule.default || tableModule;
395
439
  if (importedTable) {
396
440
  table = importedTable;
397
441
  // Ensure importDefs are properly loaded
@@ -402,7 +446,7 @@ export const discoverTables = async (
402
446
  }
403
447
 
404
448
  if (table) {
405
- const tableName = table.name || table.tableId || table.$id || file;
449
+ const tableName = table.name || (table as any).tableId || table.$id || file;
406
450
 
407
451
  // Check for naming conflicts with existing collections
408
452
  if (existingNames.has(tableName)) {
@@ -415,7 +459,7 @@ export const discoverTables = async (
415
459
  } else {
416
460
  loadedNames.add(tableName);
417
461
  // Mark as coming from tables directory
418
- table._isFromTablesDir = true;
462
+ (table as any)._isFromTablesDir = true;
419
463
  tables.push(table);
420
464
  }
421
465
  }
@@ -467,7 +511,7 @@ export const discoverLegacyDirectory = async (
467
511
  ...collection,
468
512
  _isFromTablesDir: true,
469
513
  tableId: collection.$id || collection.name.toLowerCase().replace(/\s+/g, '_')
470
- };
514
+ } as CollectionCreate;
471
515
  items.push(table);
472
516
  } else {
473
517
  items.push(collection);
@@ -8,6 +8,7 @@ export interface YamlCollectionData {
8
8
  name: string;
9
9
  id?: string;
10
10
  documentSecurity?: boolean;
11
+ rowSecurity?: boolean;
11
12
  enabled?: boolean;
12
13
  permissions?: Array<{
13
14
  permission: string;
@@ -86,10 +87,16 @@ export function collectionToYaml(
86
87
  const yamlData: YamlCollectionData = {
87
88
  name: collection.name,
88
89
  id: collection.$id,
89
- documentSecurity: collection.documentSecurity,
90
90
  enabled: collection.enabled,
91
91
  };
92
92
 
93
+ // Use appropriate security field based on terminology
94
+ if (config.useTableTerminology) {
95
+ yamlData.rowSecurity = collection.documentSecurity;
96
+ } else {
97
+ yamlData.documentSecurity = collection.documentSecurity;
98
+ }
99
+
93
100
  // Convert permissions
94
101
  if (collection.$permissions && collection.$permissions.length > 0) {
95
102
  yamlData.permissions = collection.$permissions.map(p => ({
@@ -240,6 +247,12 @@ export function normalizeYamlData(yamlData: YamlCollectionData): YamlCollectionD
240
247
  }));
241
248
  }
242
249
 
250
+ // Normalize security fields - prefer documentSecurity for consistency
251
+ if (yamlData.rowSecurity !== undefined && yamlData.documentSecurity === undefined) {
252
+ normalized.documentSecurity = yamlData.rowSecurity;
253
+ delete normalized.rowSecurity;
254
+ }
255
+
243
256
  return normalized;
244
257
  }
245
258
 
@@ -248,7 +261,8 @@ export function normalizeYamlData(yamlData: YamlCollectionData): YamlCollectionD
248
261
  */
249
262
  export function usesTableTerminology(yamlData: YamlCollectionData): boolean {
250
263
  return !!(yamlData.columns && yamlData.columns.length > 0) ||
251
- !!(yamlData.indexes?.some(idx => !!(idx as any).columns));
264
+ !!(yamlData.indexes?.some(idx => !!(idx as any).columns)) ||
265
+ yamlData.rowSecurity !== undefined;
252
266
  }
253
267
 
254
268
  /**
@@ -279,6 +293,12 @@ export function convertTerminology(
279
293
  }));
280
294
  }
281
295
 
296
+ // Convert security field
297
+ if (yamlData.documentSecurity !== undefined && yamlData.rowSecurity === undefined) {
298
+ converted.rowSecurity = yamlData.documentSecurity;
299
+ delete converted.documentSecurity;
300
+ }
301
+
282
302
  return converted;
283
303
  } else {
284
304
  // Convert columns to attributes (normalize)
@@ -367,7 +387,6 @@ export function generateYamlTemplate(
367
387
  const template: YamlCollectionData = {
368
388
  name: entityName,
369
389
  id: entityName.toLowerCase().replace(/\s+/g, '_'),
370
- documentSecurity: false,
371
390
  enabled: true,
372
391
  permissions: [
373
392
  {
@@ -390,6 +409,13 @@ export function generateYamlTemplate(
390
409
  importDefs: []
391
410
  };
392
411
 
412
+ // Use appropriate security field based on terminology
413
+ if (config.useTableTerminology) {
414
+ template.rowSecurity = false;
415
+ } else {
416
+ template.documentSecurity = false;
417
+ }
418
+
393
419
  // Assign fields with correct property name
394
420
  (template as any)[fieldsKey] = fieldsArray;
395
421
  template.indexes = indexesArray as any;
@@ -12,11 +12,10 @@ import {
12
12
  type Specification,
13
13
  } from "appwrite-utils";
14
14
  import {
15
- loadConfig,
16
- loadConfigWithPath,
17
15
  findAppwriteConfig,
18
16
  findFunctionsDir,
19
17
  } from "./utils/loadConfigs.js";
18
+ import { normalizeFunctionName, validateFunctionDirectory } from './functions/pathResolution.js';
20
19
  import { UsersController } from "./users/methods.js";
21
20
  import { AppwriteToX } from "./migrations/appwriteToX.js";
22
21
  import { ImportController } from "./migrations/importController.js";
@@ -72,6 +71,7 @@ import { configureLogging, updateLogger, logger } from "./shared/logging.js";
72
71
  import { MessageFormatter, Messages } from "./shared/messageFormatter.js";
73
72
  import { SchemaGenerator } from "./shared/schemaGenerator.js";
74
73
  import { findYamlConfig } from "./config/yamlConfig.js";
74
+ import { createImportSchemas } from "./migrations/yaml/generateImportSchemas.js";
75
75
  import {
76
76
  validateCollectionsTablesConfig,
77
77
  reportValidationResults,
@@ -80,6 +80,7 @@ import {
80
80
  } from "./config/configValidation.js";
81
81
  import { ConfigManager } from "./config/ConfigManager.js";
82
82
  import { ClientFactory } from "./utils/ClientFactory.js";
83
+ import type { DatabaseSelection, BucketSelection } from "./shared/selectionDialogs.js";
83
84
 
84
85
  export interface SetupOptions {
85
86
  databases?: Models.Database[];
@@ -114,9 +115,33 @@ export class UtilsController {
114
115
  appwriteKey?: string;
115
116
  }
116
117
  ): UtilsController {
118
+ // Clear instance if currentUserDir has changed
119
+ if (UtilsController.instance &&
120
+ UtilsController.instance.currentUserDir !== currentUserDir) {
121
+ logger.debug(`Clearing singleton: currentUserDir changed from ${UtilsController.instance.currentUserDir} to ${currentUserDir}`, { prefix: "UtilsController" });
122
+ UtilsController.clearInstance();
123
+ }
124
+
125
+ // Clear instance if directConfig endpoint or project has changed
126
+ if (UtilsController.instance && directConfig) {
127
+ const existingConfig = UtilsController.instance.config;
128
+ if (existingConfig) {
129
+ const endpointChanged = directConfig.appwriteEndpoint &&
130
+ existingConfig.appwriteEndpoint !== directConfig.appwriteEndpoint;
131
+ const projectChanged = directConfig.appwriteProject &&
132
+ existingConfig.appwriteProject !== directConfig.appwriteProject;
133
+
134
+ if (endpointChanged || projectChanged) {
135
+ logger.debug("Clearing singleton: endpoint or project changed", { prefix: "UtilsController" });
136
+ UtilsController.clearInstance();
137
+ }
138
+ }
139
+ }
140
+
117
141
  if (!UtilsController.instance) {
118
142
  UtilsController.instance = new UtilsController(currentUserDir, directConfig);
119
143
  }
144
+
120
145
  return UtilsController.instance;
121
146
  }
122
147
 
@@ -341,6 +366,26 @@ export class UtilsController {
341
366
  return dbs.databases;
342
367
  }
343
368
 
369
+ async fetchAllBuckets(): Promise<{ buckets: Models.Bucket[] }> {
370
+ await this.init();
371
+ if (!this.storage) {
372
+ MessageFormatter.warning("Storage not initialized - buckets will be empty", { prefix: "Controller" });
373
+ return { buckets: [] };
374
+ }
375
+
376
+ try {
377
+ const result = await this.storage.listBuckets([
378
+ Query.limit(1000) // Increase limit to get all buckets
379
+ ]);
380
+
381
+ MessageFormatter.success(`Found ${result.buckets.length} buckets`, { prefix: "Controller" });
382
+ return result;
383
+ } catch (error: any) {
384
+ MessageFormatter.error(`Failed to fetch buckets: ${error.message || error}`, error instanceof Error ? error : undefined, { prefix: "Controller" });
385
+ return { buckets: [] };
386
+ }
387
+ }
388
+
344
389
  async wipeOtherDatabases(databasesToKeep: Models.Database[]) {
345
390
  await this.init();
346
391
  if (!this.database) {
@@ -404,10 +449,17 @@ export class UtilsController {
404
449
  for (const entry of entries) {
405
450
  if (entry.isDirectory()) {
406
451
  const functionPath = path.join(functionsDir, entry.name);
407
- // Match with config functions by name
452
+
453
+ // Validate it's a function directory
454
+ if (!validateFunctionDirectory(functionPath)) {
455
+ continue; // Skip invalid directories
456
+ }
457
+
458
+ // Match with config functions using normalized names
408
459
  if (this.config?.functions) {
460
+ const normalizedEntryName = normalizeFunctionName(entry.name);
409
461
  const matchingFunc = this.config.functions.find(
410
- (f) => f.name.toLowerCase() === entry.name.toLowerCase()
462
+ (f) => normalizeFunctionName(f.name) === normalizedEntryName
411
463
  );
412
464
  if (matchingFunc) {
413
465
  functionDirMap.set(matchingFunc.name, functionPath);
@@ -569,28 +621,32 @@ export class UtilsController {
569
621
  async generateSchemas() {
570
622
  // Schema generation doesn't need Appwrite connection, just config
571
623
  if (!this.config) {
572
- if (this.appwriteFolderPath && this.appwriteConfigPath) {
573
- MessageFormatter.progress("Loading config from file...", { prefix: "Config" });
574
- try {
575
- const { config, actualConfigPath } = await loadConfigWithPath(
576
- this.appwriteFolderPath,
577
- { validate: false, strictMode: false, reportValidation: false }
578
- );
579
- this.config = config;
580
- MessageFormatter.info(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" });
581
- } catch (error) {
582
- MessageFormatter.error("Failed to load config from file", error instanceof Error ? error : undefined, { prefix: "Config" });
583
- return;
624
+ MessageFormatter.progress("Loading config from ConfigManager...", { prefix: "Config" });
625
+ try {
626
+ const configManager = ConfigManager.getInstance();
627
+
628
+ // Load config if not already loaded
629
+ if (!configManager.hasConfig()) {
630
+ await configManager.loadConfig({
631
+ configDir: this.currentUserDir,
632
+ validate: false,
633
+ strictMode: false,
634
+ });
584
635
  }
585
- } else {
586
- MessageFormatter.error("No configuration available", undefined, { prefix: "Controller" });
636
+
637
+ this.config = configManager.getConfig();
638
+ MessageFormatter.info("Config loaded successfully from ConfigManager", { prefix: "Config" });
639
+ } catch (error) {
640
+ MessageFormatter.error("Failed to load config", error instanceof Error ? error : undefined, { prefix: "Config" });
587
641
  return;
588
642
  }
589
643
  }
644
+
590
645
  if (!this.appwriteFolderPath) {
591
646
  MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" });
592
647
  return;
593
648
  }
649
+
594
650
  await generateSchemas(this.config, this.appwriteFolderPath);
595
651
  }
596
652
 
@@ -636,7 +692,9 @@ export class UtilsController {
636
692
 
637
693
  async synchronizeConfigurations(
638
694
  databases?: Models.Database[],
639
- config?: AppwriteConfig
695
+ config?: AppwriteConfig,
696
+ databaseSelections?: DatabaseSelection[],
697
+ bucketSelections?: BucketSelection[]
640
698
  ) {
641
699
  await this.init();
642
700
  if (!this.storage) {
@@ -652,21 +710,191 @@ export class UtilsController {
652
710
  MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" });
653
711
  return;
654
712
  }
713
+
714
+ // If selections are provided, filter the databases accordingly
715
+ let filteredDatabases = databases;
716
+ if (databaseSelections && databaseSelections.length > 0) {
717
+ // Convert selections to Models.Database format
718
+ filteredDatabases = [];
719
+ const allDatabases = databases ? databases : await fetchAllDatabases(this.database!);
720
+
721
+ for (const selection of databaseSelections) {
722
+ const database = allDatabases.find(db => db.$id === selection.databaseId);
723
+ if (database) {
724
+ filteredDatabases.push(database);
725
+ } else {
726
+ MessageFormatter.warning(`Database with ID ${selection.databaseId} not found`, { prefix: "Controller" });
727
+ }
728
+ }
729
+
730
+ MessageFormatter.info(`Syncing ${filteredDatabases.length} selected databases out of ${allDatabases.length} available`, { prefix: "Controller" });
731
+ }
732
+
655
733
  const appwriteToX = new AppwriteToX(
656
734
  configToUse,
657
735
  this.appwriteFolderPath,
658
736
  this.storage
659
737
  );
660
- await appwriteToX.toSchemas(databases);
661
-
738
+ await appwriteToX.toSchemas(filteredDatabases);
739
+
662
740
  // Update the controller's config with the synchronized collections
663
741
  this.config = appwriteToX.updatedConfig;
664
-
742
+
665
743
  // Write the updated config back to disk
666
744
  const generator = new SchemaGenerator(this.config, this.appwriteFolderPath);
667
745
  const yamlConfigPath = findYamlConfig(this.appwriteFolderPath);
668
746
  const isYamlProject = !!yamlConfigPath;
669
747
  await generator.updateConfig(this.config, isYamlProject);
748
+
749
+ // Regenerate JSON schemas to reflect any table terminology fixes
750
+ try {
751
+ MessageFormatter.progress("Regenerating JSON schemas...", { prefix: "Sync" });
752
+ await createImportSchemas(this.appwriteFolderPath);
753
+ MessageFormatter.success("JSON schemas regenerated successfully", { prefix: "Sync" });
754
+ } catch (error) {
755
+ // Log error but don't fail the sync process
756
+ const errorMessage = error instanceof Error ? error.message : String(error);
757
+ MessageFormatter.warning(
758
+ `Failed to regenerate JSON schemas, but sync completed: ${errorMessage}`,
759
+ { prefix: "Sync" }
760
+ );
761
+ logger.warn("Schema regeneration failed during sync:", error);
762
+ }
763
+ }
764
+
765
+ async selectivePull(
766
+ databaseSelections: DatabaseSelection[],
767
+ bucketSelections: BucketSelection[]
768
+ ): Promise<void> {
769
+ await this.init();
770
+ if (!this.database) {
771
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
772
+ return;
773
+ }
774
+
775
+ MessageFormatter.progress("Starting selective pull (Appwrite → local config)...", { prefix: "Controller" });
776
+
777
+ // Convert database selections to Models.Database format
778
+ const selectedDatabases: Models.Database[] = [];
779
+
780
+ for (const dbSelection of databaseSelections) {
781
+ // Get the full database object from the controller
782
+ const databases = await fetchAllDatabases(this.database);
783
+ const database = databases.find(db => db.$id === dbSelection.databaseId);
784
+
785
+ if (database) {
786
+ selectedDatabases.push(database);
787
+ MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
788
+
789
+ // Log selected tables for this database
790
+ if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
791
+ MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
792
+ }
793
+ } else {
794
+ MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
795
+ }
796
+ }
797
+
798
+ if (selectedDatabases.length === 0) {
799
+ MessageFormatter.warning("No valid databases selected for pull", { prefix: "Controller" });
800
+ return;
801
+ }
802
+
803
+ // Log bucket selections if provided
804
+ if (bucketSelections && bucketSelections.length > 0) {
805
+ MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
806
+ for (const bucketSelection of bucketSelections) {
807
+ const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
808
+ MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
809
+ }
810
+ }
811
+
812
+ // Perform selective sync using the enhanced synchronizeConfigurations method
813
+ await this.synchronizeConfigurations(selectedDatabases, this.config, databaseSelections, bucketSelections);
814
+
815
+ MessageFormatter.success("Selective pull completed successfully! Remote config pulled to local.", { prefix: "Controller" });
816
+ }
817
+
818
+ async selectivePush(
819
+ databaseSelections: DatabaseSelection[],
820
+ bucketSelections: BucketSelection[]
821
+ ): Promise<void> {
822
+ await this.init();
823
+ if (!this.database) {
824
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
825
+ return;
826
+ }
827
+
828
+ MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
829
+
830
+ // Convert database selections to Models.Database format
831
+ const selectedDatabases: Models.Database[] = [];
832
+
833
+ for (const dbSelection of databaseSelections) {
834
+ // Get the full database object from the controller
835
+ const databases = await fetchAllDatabases(this.database);
836
+ const database = databases.find(db => db.$id === dbSelection.databaseId);
837
+
838
+ if (database) {
839
+ selectedDatabases.push(database);
840
+ MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
841
+
842
+ // Log selected tables for this database
843
+ if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
844
+ MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
845
+ }
846
+ } else {
847
+ MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
848
+ }
849
+ }
850
+
851
+ if (selectedDatabases.length === 0) {
852
+ MessageFormatter.warning("No valid databases selected for push", { prefix: "Controller" });
853
+ return;
854
+ }
855
+
856
+ // Log bucket selections if provided
857
+ if (bucketSelections && bucketSelections.length > 0) {
858
+ MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
859
+ for (const bucketSelection of bucketSelections) {
860
+ const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
861
+ MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
862
+ }
863
+ }
864
+
865
+ // PUSH OPERATION: Push local configuration to Appwrite
866
+ // Build selected collections/tables from databaseSelections
867
+ const selectedCollections: any[] = [];
868
+
869
+ // Get all collections/tables from config (they're at the root level, not nested in databases)
870
+ const allCollections = this.config?.collections || this.config?.tables || [];
871
+
872
+ // Collect all selected table IDs from all database selections
873
+ const selectedTableIds = new Set<string>();
874
+ for (const dbSelection of databaseSelections) {
875
+ for (const tableId of dbSelection.tableIds) {
876
+ selectedTableIds.add(tableId);
877
+ }
878
+ }
879
+
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);
885
+ }
886
+ }
887
+
888
+ MessageFormatter.info(`Pushing ${selectedCollections.length} selected tables/collections to Appwrite`, { prefix: "Controller" });
889
+
890
+ // Ensure databases exist
891
+ await this.ensureDatabasesExist(selectedDatabases);
892
+ await this.ensureDatabaseConfigBucketsExist(selectedDatabases);
893
+
894
+ // Create/update ONLY the selected collections/tables
895
+ await this.createOrUpdateCollectionsForDatabases(selectedDatabases, selectedCollections);
896
+
897
+ MessageFormatter.success("Selective push completed successfully! Local config pushed to Appwrite.", { prefix: "Controller" });
670
898
  }
671
899
 
672
900
  async syncDb(
@@ -1,14 +0,0 @@
1
- import type { AppwriteConfig, Attribute } from "appwrite-utils";
2
- export declare class SchemaGenerator {
3
- private relationshipMap;
4
- private config;
5
- private appwriteFolderPath;
6
- constructor(config: AppwriteConfig, appwriteFolderPath: string);
7
- private resolveCollectionName;
8
- updateTsSchemas(): void;
9
- private extractRelationships;
10
- private addRelationship;
11
- generateSchemas(): void;
12
- createSchemaStringV4: (name: string, attributes: Attribute[]) => string;
13
- typeToZod: (attribute: Attribute) => string;
14
- }