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
@@ -135,6 +135,45 @@ const YamlCollectionSchema = z.object({
135
135
  })).optional().default([]),
136
136
  importDefs: z.array(z.any()).optional().default([])
137
137
  });
138
+ // YAML Table Schema - Supports table-specific terminology
139
+ const YamlTableSchema = z.object({
140
+ name: z.string(),
141
+ id: z.string().optional(),
142
+ rowSecurity: z.boolean().default(false), // Tables use rowSecurity
143
+ enabled: z.boolean().default(true),
144
+ permissions: z.array(z.object({
145
+ permission: z.string(),
146
+ target: z.string()
147
+ })).optional().default([]),
148
+ columns: z.array(// Tables use columns terminology
149
+ z.object({
150
+ key: z.string(),
151
+ type: z.string(),
152
+ size: z.number().optional(),
153
+ required: z.boolean().default(false),
154
+ array: z.boolean().optional(),
155
+ encrypted: z.boolean().optional(), // Tables support encrypted property
156
+ default: z.any().optional(),
157
+ min: z.number().optional(),
158
+ max: z.number().optional(),
159
+ elements: z.array(z.string()).optional(),
160
+ relatedTable: z.string().optional(), // Tables use relatedTable
161
+ relationType: z.string().optional(),
162
+ twoWay: z.boolean().optional(),
163
+ twoWayKey: z.string().optional(),
164
+ onDelete: z.string().optional(),
165
+ side: z.string().optional(),
166
+ encrypt: z.boolean().optional(),
167
+ format: z.string().optional()
168
+ })).optional().default([]),
169
+ indexes: z.array(z.object({
170
+ key: z.string(),
171
+ type: z.string(),
172
+ columns: z.array(z.string()), // Tables use columns in indexes
173
+ orders: z.array(z.string()).optional()
174
+ })).optional().default([]),
175
+ importDefs: z.array(z.any()).optional().default([])
176
+ });
138
177
  /**
139
178
  * Loads a YAML collection file and converts it to CollectionCreate format
140
179
  * @param filePath Path to the YAML collection file
@@ -190,55 +229,52 @@ export const loadYamlCollection = (filePath) => {
190
229
  }
191
230
  };
192
231
  /**
193
- * Loads a YAML table file and converts it to table format
232
+ * Loads a YAML table file and converts it to CollectionCreate format
194
233
  * @param filePath Path to the YAML table file
195
- * @returns Table object or null if loading fails
234
+ * @returns CollectionCreate object or null if loading fails
196
235
  */
197
236
  export const loadYamlTable = (filePath) => {
198
237
  try {
199
238
  const fileContent = fs.readFileSync(filePath, "utf8");
200
239
  const yamlData = yaml.load(fileContent);
201
- // For now, use the collection schema as base and adapt for tables
202
- const parsedTable = YamlCollectionSchema.parse(yamlData);
203
- // Convert YAML table to TableCreate format
240
+ // Use the new table-specific schema
241
+ const parsedTable = YamlTableSchema.parse(yamlData);
242
+ // Convert YAML table to CollectionCreate format (internal representation)
204
243
  const table = {
205
244
  name: parsedTable.name,
206
- tableId: yamlData.tableId || parsedTable.id || parsedTable.name.toLowerCase().replace(/\s+/g, '_'),
207
- documentSecurity: parsedTable.documentSecurity,
245
+ $id: yamlData.tableId || parsedTable.id || parsedTable.name.toLowerCase().replace(/\s+/g, '_'),
246
+ documentSecurity: parsedTable.rowSecurity, // Convert rowSecurity to documentSecurity
208
247
  enabled: parsedTable.enabled,
209
248
  $permissions: parsedTable.permissions.map(p => ({
210
249
  permission: p.permission,
211
250
  target: p.target
212
251
  })),
213
- attributes: parsedTable.attributes.map(attr => ({
214
- key: attr.key,
215
- type: attr.type,
216
- size: attr.size,
217
- required: attr.required,
218
- array: attr.array,
219
- xdefault: attr.default,
220
- min: attr.min,
221
- max: attr.max,
222
- elements: attr.elements,
223
- relatedCollection: attr.relatedCollection,
224
- relationType: attr.relationType,
225
- twoWay: attr.twoWay,
226
- twoWayKey: attr.twoWayKey,
227
- onDelete: attr.onDelete,
228
- side: attr.side,
229
- encrypted: attr.encrypt,
230
- format: attr.format
252
+ attributes: parsedTable.columns.map(col => ({
253
+ key: col.key,
254
+ type: col.type,
255
+ size: col.size,
256
+ required: col.required,
257
+ array: col.array,
258
+ xdefault: col.default,
259
+ min: col.min,
260
+ max: col.max,
261
+ elements: col.elements,
262
+ relatedCollection: col.relatedTable, // Convert relatedTable to relatedCollection
263
+ relationType: col.relationType,
264
+ twoWay: col.twoWay,
265
+ twoWayKey: col.twoWayKey,
266
+ onDelete: col.onDelete,
267
+ side: col.side,
268
+ encrypted: col.encrypted || col.encrypt, // Support both encrypted and encrypt
269
+ format: col.format
231
270
  })),
232
271
  indexes: parsedTable.indexes.map(idx => ({
233
272
  key: idx.key,
234
273
  type: idx.type,
235
- attributes: idx.attributes,
274
+ attributes: idx.columns, // Convert columns to attributes
236
275
  orders: idx.orders
237
276
  })),
238
- importDefs: parsedTable.importDefs,
239
- databaseId: yamlData.databaseId,
240
- // Add backward compatibility field
241
- $id: yamlData.$id || parsedTable.id
277
+ importDefs: parsedTable.importDefs || []
242
278
  };
243
279
  return table;
244
280
  }
@@ -3,6 +3,7 @@ export interface YamlCollectionData {
3
3
  name: string;
4
4
  id?: string;
5
5
  documentSecurity?: boolean;
6
+ rowSecurity?: boolean;
6
7
  enabled?: boolean;
7
8
  permissions?: Array<{
8
9
  permission: string;
@@ -15,9 +15,15 @@ export function collectionToYaml(collection, config = {
15
15
  const yamlData = {
16
16
  name: collection.name,
17
17
  id: collection.$id,
18
- documentSecurity: collection.documentSecurity,
19
18
  enabled: collection.enabled,
20
19
  };
20
+ // Use appropriate security field based on terminology
21
+ if (config.useTableTerminology) {
22
+ yamlData.rowSecurity = collection.documentSecurity;
23
+ }
24
+ else {
25
+ yamlData.documentSecurity = collection.documentSecurity;
26
+ }
21
27
  // Convert permissions
22
28
  if (collection.$permissions && collection.$permissions.length > 0) {
23
29
  yamlData.permissions = collection.$permissions.map(p => ({
@@ -158,6 +164,11 @@ export function normalizeYamlData(yamlData) {
158
164
  columns: undefined
159
165
  }));
160
166
  }
167
+ // Normalize security fields - prefer documentSecurity for consistency
168
+ if (yamlData.rowSecurity !== undefined && yamlData.documentSecurity === undefined) {
169
+ normalized.documentSecurity = yamlData.rowSecurity;
170
+ delete normalized.rowSecurity;
171
+ }
161
172
  return normalized;
162
173
  }
163
174
  /**
@@ -165,7 +176,8 @@ export function normalizeYamlData(yamlData) {
165
176
  */
166
177
  export function usesTableTerminology(yamlData) {
167
178
  return !!(yamlData.columns && yamlData.columns.length > 0) ||
168
- !!(yamlData.indexes?.some(idx => !!idx.columns));
179
+ !!(yamlData.indexes?.some(idx => !!idx.columns)) ||
180
+ yamlData.rowSecurity !== undefined;
169
181
  }
170
182
  /**
171
183
  * Converts between attribute and column terminology
@@ -190,6 +202,11 @@ export function convertTerminology(yamlData, toTableTerminology) {
190
202
  attributes: idx.attributes // Keep both for compatibility
191
203
  }));
192
204
  }
205
+ // Convert security field
206
+ if (yamlData.documentSecurity !== undefined && yamlData.rowSecurity === undefined) {
207
+ converted.rowSecurity = yamlData.documentSecurity;
208
+ delete converted.documentSecurity;
209
+ }
193
210
  return converted;
194
211
  }
195
212
  else {
@@ -272,7 +289,6 @@ export function generateYamlTemplate(entityName, config) {
272
289
  const template = {
273
290
  name: entityName,
274
291
  id: entityName.toLowerCase().replace(/\s+/g, '_'),
275
- documentSecurity: false,
276
292
  enabled: true,
277
293
  permissions: [
278
294
  {
@@ -294,6 +310,13 @@ export function generateYamlTemplate(entityName, config) {
294
310
  ],
295
311
  importDefs: []
296
312
  };
313
+ // Use appropriate security field based on terminology
314
+ if (config.useTableTerminology) {
315
+ template.rowSecurity = false;
316
+ }
317
+ else {
318
+ template.documentSecurity = false;
319
+ }
297
320
  // Assign fields with correct property name
298
321
  template[fieldsKey] = fieldsArray;
299
322
  template.indexes = indexesArray;
@@ -4,6 +4,7 @@ import { type AfterImportActions, type ConverterFunctions, type ValidationRules
4
4
  import { type TransferOptions } from "./migrations/transfer.js";
5
5
  import type { DatabaseAdapter } from './adapters/DatabaseAdapter.js';
6
6
  import { type ValidationResult } from "./config/configValidation.js";
7
+ import type { DatabaseSelection, BucketSelection } from "./shared/selectionDialogs.js";
7
8
  export interface SetupOptions {
8
9
  databases?: Models.Database[];
9
10
  collections?: string[];
@@ -60,6 +61,9 @@ export declare class UtilsController {
60
61
  ensureDatabasesExist(databases?: Models.Database[]): Promise<void>;
61
62
  ensureCollectionsExist(database: Models.Database, collections?: Models.Collection[]): Promise<void>;
62
63
  getDatabasesByIds(ids: string[]): Promise<Models.Database[] | undefined>;
64
+ fetchAllBuckets(): Promise<{
65
+ buckets: Models.Bucket[];
66
+ }>;
63
67
  wipeOtherDatabases(databasesToKeep: Models.Database[]): Promise<void>;
64
68
  wipeUsers(): Promise<void>;
65
69
  backupDatabase(database: Models.Database, format?: 'json' | 'zip'): Promise<void>;
@@ -78,7 +82,9 @@ export declare class UtilsController {
78
82
  }[], collections?: Models.Collection[]): Promise<void>;
79
83
  generateSchemas(): Promise<void>;
80
84
  importData(options?: SetupOptions): Promise<void>;
81
- synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig): Promise<void>;
85
+ synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig, databaseSelections?: DatabaseSelection[], bucketSelections?: BucketSelection[]): Promise<void>;
86
+ selectivePull(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
87
+ selectivePush(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
82
88
  syncDb(databases?: Models.Database[], collections?: Models.Collection[]): Promise<void>;
83
89
  getAppwriteFolderPath(): string | undefined;
84
90
  transferData(options: TransferOptions): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { Client, Databases, Query, Storage, Users, } from "node-appwrite";
2
2
  import {} from "appwrite-utils";
3
- import { loadConfig, loadConfigWithPath, findAppwriteConfig, findFunctionsDir, } from "./utils/loadConfigs.js";
3
+ import { findAppwriteConfig, findFunctionsDir, } from "./utils/loadConfigs.js";
4
+ import { normalizeFunctionName, validateFunctionDirectory } from './functions/pathResolution.js';
4
5
  import { UsersController } from "./users/methods.js";
5
6
  import { AppwriteToX } from "./migrations/appwriteToX.js";
6
7
  import { ImportController } from "./migrations/importController.js";
@@ -25,6 +26,7 @@ import { configureLogging, updateLogger, logger } from "./shared/logging.js";
25
26
  import { MessageFormatter, Messages } from "./shared/messageFormatter.js";
26
27
  import { SchemaGenerator } from "./shared/schemaGenerator.js";
27
28
  import { findYamlConfig } from "./config/yamlConfig.js";
29
+ import { createImportSchemas } from "./migrations/yaml/generateImportSchemas.js";
28
30
  import { validateCollectionsTablesConfig, reportValidationResults, validateWithStrictMode } from "./config/configValidation.js";
29
31
  import { ConfigManager } from "./config/ConfigManager.js";
30
32
  import { ClientFactory } from "./utils/ClientFactory.js";
@@ -38,6 +40,26 @@ export class UtilsController {
38
40
  * Get the UtilsController singleton instance
39
41
  */
40
42
  static getInstance(currentUserDir, directConfig) {
43
+ // Clear instance if currentUserDir has changed
44
+ if (UtilsController.instance &&
45
+ UtilsController.instance.currentUserDir !== currentUserDir) {
46
+ logger.debug(`Clearing singleton: currentUserDir changed from ${UtilsController.instance.currentUserDir} to ${currentUserDir}`, { prefix: "UtilsController" });
47
+ UtilsController.clearInstance();
48
+ }
49
+ // Clear instance if directConfig endpoint or project has changed
50
+ if (UtilsController.instance && directConfig) {
51
+ const existingConfig = UtilsController.instance.config;
52
+ if (existingConfig) {
53
+ const endpointChanged = directConfig.appwriteEndpoint &&
54
+ existingConfig.appwriteEndpoint !== directConfig.appwriteEndpoint;
55
+ const projectChanged = directConfig.appwriteProject &&
56
+ existingConfig.appwriteProject !== directConfig.appwriteProject;
57
+ if (endpointChanged || projectChanged) {
58
+ logger.debug("Clearing singleton: endpoint or project changed", { prefix: "UtilsController" });
59
+ UtilsController.clearInstance();
60
+ }
61
+ }
62
+ }
41
63
  if (!UtilsController.instance) {
42
64
  UtilsController.instance = new UtilsController(currentUserDir, directConfig);
43
65
  }
@@ -225,6 +247,24 @@ export class UtilsController {
225
247
  ]);
226
248
  return dbs.databases;
227
249
  }
250
+ async fetchAllBuckets() {
251
+ await this.init();
252
+ if (!this.storage) {
253
+ MessageFormatter.warning("Storage not initialized - buckets will be empty", { prefix: "Controller" });
254
+ return { buckets: [] };
255
+ }
256
+ try {
257
+ const result = await this.storage.listBuckets([
258
+ Query.limit(1000) // Increase limit to get all buckets
259
+ ]);
260
+ MessageFormatter.success(`Found ${result.buckets.length} buckets`, { prefix: "Controller" });
261
+ return result;
262
+ }
263
+ catch (error) {
264
+ MessageFormatter.error(`Failed to fetch buckets: ${error.message || error}`, error instanceof Error ? error : undefined, { prefix: "Controller" });
265
+ return { buckets: [] };
266
+ }
267
+ }
228
268
  async wipeOtherDatabases(databasesToKeep) {
229
269
  await this.init();
230
270
  if (!this.database) {
@@ -276,9 +316,14 @@ export class UtilsController {
276
316
  for (const entry of entries) {
277
317
  if (entry.isDirectory()) {
278
318
  const functionPath = path.join(functionsDir, entry.name);
279
- // Match with config functions by name
319
+ // Validate it's a function directory
320
+ if (!validateFunctionDirectory(functionPath)) {
321
+ continue; // Skip invalid directories
322
+ }
323
+ // Match with config functions using normalized names
280
324
  if (this.config?.functions) {
281
- const matchingFunc = this.config.functions.find((f) => f.name.toLowerCase() === entry.name.toLowerCase());
325
+ const normalizedEntryName = normalizeFunctionName(entry.name);
326
+ const matchingFunc = this.config.functions.find((f) => normalizeFunctionName(f.name) === normalizedEntryName);
282
327
  if (matchingFunc) {
283
328
  functionDirMap.set(matchingFunc.name, functionPath);
284
329
  }
@@ -403,20 +448,22 @@ export class UtilsController {
403
448
  async generateSchemas() {
404
449
  // Schema generation doesn't need Appwrite connection, just config
405
450
  if (!this.config) {
406
- if (this.appwriteFolderPath && this.appwriteConfigPath) {
407
- MessageFormatter.progress("Loading config from file...", { prefix: "Config" });
408
- try {
409
- const { config, actualConfigPath } = await loadConfigWithPath(this.appwriteFolderPath, { validate: false, strictMode: false, reportValidation: false });
410
- this.config = config;
411
- MessageFormatter.info(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" });
412
- }
413
- catch (error) {
414
- MessageFormatter.error("Failed to load config from file", error instanceof Error ? error : undefined, { prefix: "Config" });
415
- return;
451
+ MessageFormatter.progress("Loading config from ConfigManager...", { prefix: "Config" });
452
+ try {
453
+ const configManager = ConfigManager.getInstance();
454
+ // Load config if not already loaded
455
+ if (!configManager.hasConfig()) {
456
+ await configManager.loadConfig({
457
+ configDir: this.currentUserDir,
458
+ validate: false,
459
+ strictMode: false,
460
+ });
416
461
  }
462
+ this.config = configManager.getConfig();
463
+ MessageFormatter.info("Config loaded successfully from ConfigManager", { prefix: "Config" });
417
464
  }
418
- else {
419
- MessageFormatter.error("No configuration available", undefined, { prefix: "Controller" });
465
+ catch (error) {
466
+ MessageFormatter.error("Failed to load config", error instanceof Error ? error : undefined, { prefix: "Config" });
420
467
  return;
421
468
  }
422
469
  }
@@ -448,7 +495,7 @@ export class UtilsController {
448
495
  const importController = new ImportController(this.config, this.database, this.storage, this.appwriteFolderPath, importDataActions, options, options.databases);
449
496
  await importController.run(options.collections);
450
497
  }
451
- async synchronizeConfigurations(databases, config) {
498
+ async synchronizeConfigurations(databases, config, databaseSelections, bucketSelections) {
452
499
  await this.init();
453
500
  if (!this.storage) {
454
501
  MessageFormatter.error("Storage not initialized", undefined, { prefix: "Controller" });
@@ -463,8 +510,25 @@ export class UtilsController {
463
510
  MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" });
464
511
  return;
465
512
  }
513
+ // If selections are provided, filter the databases accordingly
514
+ let filteredDatabases = databases;
515
+ if (databaseSelections && databaseSelections.length > 0) {
516
+ // Convert selections to Models.Database format
517
+ filteredDatabases = [];
518
+ const allDatabases = databases ? databases : await fetchAllDatabases(this.database);
519
+ for (const selection of databaseSelections) {
520
+ const database = allDatabases.find(db => db.$id === selection.databaseId);
521
+ if (database) {
522
+ filteredDatabases.push(database);
523
+ }
524
+ else {
525
+ MessageFormatter.warning(`Database with ID ${selection.databaseId} not found`, { prefix: "Controller" });
526
+ }
527
+ }
528
+ MessageFormatter.info(`Syncing ${filteredDatabases.length} selected databases out of ${allDatabases.length} available`, { prefix: "Controller" });
529
+ }
466
530
  const appwriteToX = new AppwriteToX(configToUse, this.appwriteFolderPath, this.storage);
467
- await appwriteToX.toSchemas(databases);
531
+ await appwriteToX.toSchemas(filteredDatabases);
468
532
  // Update the controller's config with the synchronized collections
469
533
  this.config = appwriteToX.updatedConfig;
470
534
  // Write the updated config back to disk
@@ -472,6 +536,123 @@ export class UtilsController {
472
536
  const yamlConfigPath = findYamlConfig(this.appwriteFolderPath);
473
537
  const isYamlProject = !!yamlConfigPath;
474
538
  await generator.updateConfig(this.config, isYamlProject);
539
+ // Regenerate JSON schemas to reflect any table terminology fixes
540
+ try {
541
+ MessageFormatter.progress("Regenerating JSON schemas...", { prefix: "Sync" });
542
+ await createImportSchemas(this.appwriteFolderPath);
543
+ MessageFormatter.success("JSON schemas regenerated successfully", { prefix: "Sync" });
544
+ }
545
+ catch (error) {
546
+ // Log error but don't fail the sync process
547
+ const errorMessage = error instanceof Error ? error.message : String(error);
548
+ MessageFormatter.warning(`Failed to regenerate JSON schemas, but sync completed: ${errorMessage}`, { prefix: "Sync" });
549
+ logger.warn("Schema regeneration failed during sync:", error);
550
+ }
551
+ }
552
+ async selectivePull(databaseSelections, bucketSelections) {
553
+ await this.init();
554
+ if (!this.database) {
555
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
556
+ return;
557
+ }
558
+ MessageFormatter.progress("Starting selective pull (Appwrite → local config)...", { prefix: "Controller" });
559
+ // Convert database selections to Models.Database format
560
+ const selectedDatabases = [];
561
+ for (const dbSelection of databaseSelections) {
562
+ // Get the full database object from the controller
563
+ const databases = await fetchAllDatabases(this.database);
564
+ const database = databases.find(db => db.$id === dbSelection.databaseId);
565
+ if (database) {
566
+ selectedDatabases.push(database);
567
+ MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
568
+ // Log selected tables for this database
569
+ if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
570
+ MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
571
+ }
572
+ }
573
+ else {
574
+ MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
575
+ }
576
+ }
577
+ if (selectedDatabases.length === 0) {
578
+ MessageFormatter.warning("No valid databases selected for pull", { prefix: "Controller" });
579
+ return;
580
+ }
581
+ // Log bucket selections if provided
582
+ if (bucketSelections && bucketSelections.length > 0) {
583
+ MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
584
+ for (const bucketSelection of bucketSelections) {
585
+ const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
586
+ MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
587
+ }
588
+ }
589
+ // Perform selective sync using the enhanced synchronizeConfigurations method
590
+ await this.synchronizeConfigurations(selectedDatabases, this.config, databaseSelections, bucketSelections);
591
+ MessageFormatter.success("Selective pull completed successfully! Remote config pulled to local.", { prefix: "Controller" });
592
+ }
593
+ async selectivePush(databaseSelections, bucketSelections) {
594
+ await this.init();
595
+ if (!this.database) {
596
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
597
+ return;
598
+ }
599
+ MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
600
+ // Convert database selections to Models.Database format
601
+ const selectedDatabases = [];
602
+ for (const dbSelection of databaseSelections) {
603
+ // Get the full database object from the controller
604
+ const databases = await fetchAllDatabases(this.database);
605
+ const database = databases.find(db => db.$id === dbSelection.databaseId);
606
+ if (database) {
607
+ selectedDatabases.push(database);
608
+ MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
609
+ // Log selected tables for this database
610
+ if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
611
+ MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
612
+ }
613
+ }
614
+ else {
615
+ MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
616
+ }
617
+ }
618
+ if (selectedDatabases.length === 0) {
619
+ MessageFormatter.warning("No valid databases selected for push", { prefix: "Controller" });
620
+ return;
621
+ }
622
+ // Log bucket selections if provided
623
+ if (bucketSelections && bucketSelections.length > 0) {
624
+ MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
625
+ for (const bucketSelection of bucketSelections) {
626
+ const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
627
+ MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
628
+ }
629
+ }
630
+ // PUSH OPERATION: Push local configuration to Appwrite
631
+ // Build selected collections/tables from databaseSelections
632
+ const selectedCollections = [];
633
+ // Get all collections/tables from config (they're at the root level, not nested in databases)
634
+ const allCollections = this.config?.collections || this.config?.tables || [];
635
+ // Collect all selected table IDs from all database selections
636
+ const selectedTableIds = new Set();
637
+ for (const dbSelection of databaseSelections) {
638
+ for (const tableId of dbSelection.tableIds) {
639
+ selectedTableIds.add(tableId);
640
+ }
641
+ }
642
+ // Filter to only the selected table IDs
643
+ for (const collection of allCollections) {
644
+ const collectionId = collection.$id || collection.id;
645
+ if (selectedTableIds.has(collectionId)) {
646
+ selectedCollections.push(collection);
647
+ }
648
+ }
649
+ MessageFormatter.info(`Pushing ${selectedCollections.length} selected tables/collections to Appwrite`, { prefix: "Controller" });
650
+ // Ensure databases exist
651
+ await this.ensureDatabasesExist(selectedDatabases);
652
+ await this.ensureDatabaseConfigBucketsExist(selectedDatabases);
653
+ // Create/update ONLY the selected collections/tables
654
+ await this.createOrUpdateCollectionsForDatabases(selectedDatabases, selectedCollections);
655
+ MessageFormatter.success("Selective push completed successfully! Local config pushed to Appwrite.", { prefix: "Controller" });
475
656
  }
476
657
  async syncDb(databases = [], collections = []) {
477
658
  await this.init();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "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.7.7",
4
+ "version": "1.7.9",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -24,7 +24,9 @@
24
24
  "appwrite-migrate": "./dist/main.js"
25
25
  },
26
26
  "scripts": {
27
- "build": "bun run tsc",
27
+ "build": "bun run tsc && bun run copy-templates",
28
+ "prebuild": "rm -rf dist",
29
+ "copy-templates": "tsx scripts/copy-templates.ts",
28
30
  "start": "tsx --no-cache src/main.ts",
29
31
  "deploy": "bun run build && npm publish --access public",
30
32
  "test": "jest",
@@ -0,0 +1,23 @@
1
+ import { cpSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const src = join(process.cwd(), 'src', 'functions', 'templates');
5
+ const dest = join(process.cwd(), 'dist', 'functions', 'templates');
6
+
7
+ // Verify source exists
8
+ if (!existsSync(src)) {
9
+ console.error('❌ Error: Template source directory not found:', src);
10
+ process.exit(1);
11
+ }
12
+
13
+ // Create destination directory
14
+ mkdirSync(dest, { recursive: true });
15
+
16
+ // Copy templates recursively
17
+ try {
18
+ cpSync(src, dest, { recursive: true });
19
+ console.log('✓ Templates copied to dist/functions/templates/');
20
+ } catch (error) {
21
+ console.error('❌ Failed to copy templates:', error);
22
+ process.exit(1);
23
+ }