appwrite-utils-cli 1.6.3 → 1.6.4

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 (55) hide show
  1. package/CONFIG_TODO.md +1189 -0
  2. package/SERVICE_IMPLEMENTATION_REPORT.md +462 -0
  3. package/dist/cli/commands/configCommands.js +7 -1
  4. package/dist/collections/attributes.js +102 -30
  5. package/dist/config/ConfigManager.d.ts +445 -0
  6. package/dist/config/ConfigManager.js +625 -0
  7. package/dist/config/index.d.ts +8 -0
  8. package/dist/config/index.js +7 -0
  9. package/dist/config/services/ConfigDiscoveryService.d.ts +126 -0
  10. package/dist/config/services/ConfigDiscoveryService.js +374 -0
  11. package/dist/config/services/ConfigLoaderService.d.ts +105 -0
  12. package/dist/config/services/ConfigLoaderService.js +410 -0
  13. package/dist/config/services/ConfigMergeService.d.ts +208 -0
  14. package/dist/config/services/ConfigMergeService.js +307 -0
  15. package/dist/config/services/ConfigValidationService.d.ts +214 -0
  16. package/dist/config/services/ConfigValidationService.js +310 -0
  17. package/dist/config/services/SessionAuthService.d.ts +225 -0
  18. package/dist/config/services/SessionAuthService.js +456 -0
  19. package/dist/config/services/__tests__/ConfigMergeService.test.d.ts +1 -0
  20. package/dist/config/services/__tests__/ConfigMergeService.test.js +271 -0
  21. package/dist/config/services/index.d.ts +13 -0
  22. package/dist/config/services/index.js +10 -0
  23. package/dist/interactiveCLI.js +8 -6
  24. package/dist/main.js +2 -2
  25. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
  26. package/dist/shared/operationQueue.js +1 -1
  27. package/dist/utils/ClientFactory.d.ts +87 -0
  28. package/dist/utils/ClientFactory.js +164 -0
  29. package/dist/utils/getClientFromConfig.js +4 -3
  30. package/dist/utils/helperFunctions.d.ts +1 -0
  31. package/dist/utils/helperFunctions.js +21 -5
  32. package/dist/utils/yamlConverter.d.ts +2 -2
  33. package/dist/utils/yamlConverter.js +2 -2
  34. package/dist/utilsController.d.ts +18 -15
  35. package/dist/utilsController.js +83 -131
  36. package/package.json +1 -1
  37. package/src/cli/commands/configCommands.ts +8 -1
  38. package/src/collections/attributes.ts +118 -31
  39. package/src/config/ConfigManager.ts +808 -0
  40. package/src/config/index.ts +10 -0
  41. package/src/config/services/ConfigDiscoveryService.ts +463 -0
  42. package/src/config/services/ConfigLoaderService.ts +560 -0
  43. package/src/config/services/ConfigMergeService.ts +386 -0
  44. package/src/config/services/ConfigValidationService.ts +394 -0
  45. package/src/config/services/SessionAuthService.ts +565 -0
  46. package/src/config/services/__tests__/ConfigMergeService.test.ts +351 -0
  47. package/src/config/services/index.ts +29 -0
  48. package/src/interactiveCLI.ts +9 -7
  49. package/src/main.ts +2 -2
  50. package/src/shared/operationQueue.ts +1 -1
  51. package/src/utils/ClientFactory.ts +186 -0
  52. package/src/utils/getClientFromConfig.ts +4 -3
  53. package/src/utils/helperFunctions.ts +27 -7
  54. package/src/utils/yamlConverter.ts +4 -4
  55. package/src/utilsController.ts +99 -187
@@ -0,0 +1,808 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import type { AppwriteConfig, Collection, CollectionCreate, AppwriteFunction } from "appwrite-utils";
4
+ import {
5
+ ConfigDiscoveryService,
6
+ ConfigLoaderService,
7
+ ConfigMergeService,
8
+ ConfigValidationService,
9
+ SessionAuthService,
10
+ type ConfigOverrides,
11
+ type SessionAuthInfo,
12
+ type AuthenticationStatus,
13
+ type ValidationResult,
14
+ } from "./services/index.js";
15
+ import { MessageFormatter } from "../shared/messageFormatter.js";
16
+ import { logger } from "../shared/logging.js";
17
+
18
+ /**
19
+ * Database type from AppwriteConfig
20
+ */
21
+ type Database = {
22
+ $id: string;
23
+ name: string;
24
+ bucket?: any;
25
+ };
26
+
27
+ /**
28
+ * Bucket type from AppwriteConfig
29
+ */
30
+ type Bucket = {
31
+ $id: string;
32
+ name: string;
33
+ $permissions?: any[];
34
+ [key: string]: any;
35
+ };
36
+
37
+ /**
38
+ * Options for loading configuration
39
+ */
40
+ export interface ConfigLoadOptions {
41
+ /**
42
+ * Directory to start searching for config files
43
+ * @default process.cwd()
44
+ */
45
+ configDir?: string;
46
+
47
+ /**
48
+ * Force reload configuration even if cached
49
+ * @default false
50
+ */
51
+ forceReload?: boolean;
52
+
53
+ /**
54
+ * Validate configuration after loading
55
+ * @default false
56
+ */
57
+ validate?: boolean;
58
+
59
+ /**
60
+ * Use strict validation mode (warnings as errors)
61
+ * @default false
62
+ */
63
+ strictMode?: boolean;
64
+
65
+ /**
66
+ * Report validation results to console
67
+ * @default false
68
+ */
69
+ reportValidation?: boolean;
70
+
71
+ /**
72
+ * Override configuration values
73
+ */
74
+ overrides?: ConfigOverrides;
75
+
76
+ /**
77
+ * Override session authentication (used for preserving session on reload)
78
+ */
79
+ sessionOverride?: SessionAuthInfo;
80
+ }
81
+
82
+ /**
83
+ * Filter function for collections (accepts both Collection and CollectionCreate)
84
+ */
85
+ export type CollectionFilter = (collection: Collection | CollectionCreate) => boolean;
86
+
87
+ /**
88
+ * Centralized configuration manager with intelligent caching and session management.
89
+ *
90
+ * This singleton provides a unified interface for:
91
+ * - Configuration discovery and loading (YAML, TypeScript, JSON)
92
+ * - Session authentication with automatic caching
93
+ * - Configuration validation and merging
94
+ * - Session preservation across reloads
95
+ * - File watching for configuration changes
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * const configManager = ConfigManager.getInstance();
100
+ *
101
+ * // Load configuration (cached after first call)
102
+ * const config = await configManager.loadConfig({
103
+ * validate: true,
104
+ * reportValidation: true,
105
+ * });
106
+ *
107
+ * // Get cached config synchronously
108
+ * const cachedConfig = configManager.getConfig();
109
+ *
110
+ * // Reload with session preservation
111
+ * const reloadedConfig = await configManager.reloadConfig();
112
+ *
113
+ * // Watch for config changes
114
+ * const unwatch = configManager.watchConfig(async (config) => {
115
+ * console.log("Config changed:", config);
116
+ * });
117
+ * ```
118
+ */
119
+ export class ConfigManager {
120
+ // ──────────────────────────────────────────────────
121
+ // SINGLETON PATTERN
122
+ // ──────────────────────────────────────────────────
123
+
124
+ private static instance: ConfigManager | null = null;
125
+
126
+ /**
127
+ * Get the ConfigManager singleton instance
128
+ *
129
+ * @returns The singleton ConfigManager instance
130
+ *
131
+ * @example
132
+ * ```typescript
133
+ * const configManager = ConfigManager.getInstance();
134
+ * ```
135
+ */
136
+ public static getInstance(): ConfigManager {
137
+ if (!ConfigManager.instance) {
138
+ ConfigManager.instance = new ConfigManager();
139
+ }
140
+ return ConfigManager.instance;
141
+ }
142
+
143
+ /**
144
+ * Reset the singleton instance (useful for testing)
145
+ *
146
+ * @example
147
+ * ```typescript
148
+ * ConfigManager.resetInstance();
149
+ * const newInstance = ConfigManager.getInstance();
150
+ * ```
151
+ */
152
+ public static resetInstance(): void {
153
+ ConfigManager.instance = null;
154
+ }
155
+
156
+ // ──────────────────────────────────────────────────
157
+ // STATE (Private)
158
+ // ──────────────────────────────────────────────────
159
+
160
+ private cachedConfig: AppwriteConfig | null = null;
161
+ private cachedConfigPath: string | null = null;
162
+ private cachedSession: SessionAuthInfo | null = null;
163
+ private lastLoadTimestamp: number = 0;
164
+ private isInitialized: boolean = false;
165
+
166
+ // Service dependencies
167
+ private discoveryService: ConfigDiscoveryService;
168
+ private loaderService: ConfigLoaderService;
169
+ private validationService: ConfigValidationService;
170
+ private sessionService: SessionAuthService;
171
+ private mergeService: ConfigMergeService;
172
+
173
+ // ──────────────────────────────────────────────────
174
+ // CONSTRUCTOR
175
+ // ──────────────────────────────────────────────────
176
+
177
+ /**
178
+ * Private constructor to enforce singleton pattern.
179
+ * Use ConfigManager.getInstance() instead.
180
+ */
181
+ private constructor() {
182
+ // Initialize all services
183
+ this.discoveryService = new ConfigDiscoveryService();
184
+ this.loaderService = new ConfigLoaderService();
185
+ this.validationService = new ConfigValidationService();
186
+ this.sessionService = new SessionAuthService();
187
+ this.mergeService = new ConfigMergeService();
188
+
189
+ logger.debug("ConfigManager instance created", { prefix: "ConfigManager" });
190
+ }
191
+
192
+ // ──────────────────────────────────────────────────
193
+ // CORE CONFIGURATION METHODS
194
+ // ──────────────────────────────────────────────────
195
+
196
+ /**
197
+ * Load configuration with intelligent caching and session management.
198
+ *
199
+ * This method orchestrates the entire configuration loading flow:
200
+ * 1. Returns cached config if available (unless forceReload is true)
201
+ * 2. Discovers config file using ConfigDiscoveryService
202
+ * 3. Loads config from file using ConfigLoaderService
203
+ * 4. Loads and merges session authentication using SessionAuthService
204
+ * 5. Applies CLI/environment overrides using ConfigMergeService
205
+ * 6. Validates configuration if requested
206
+ * 7. Caches the result for future calls
207
+ *
208
+ * @param options - Configuration loading options
209
+ * @returns The loaded and processed configuration
210
+ * @throws Error if no configuration file is found
211
+ * @throws Error if validation fails in strict mode
212
+ *
213
+ * @example
214
+ * ```typescript
215
+ * const config = await configManager.loadConfig({
216
+ * configDir: process.cwd(),
217
+ * validate: true,
218
+ * strictMode: true,
219
+ * reportValidation: true,
220
+ * overrides: {
221
+ * appwriteEndpoint: "https://custom.endpoint.com/v1"
222
+ * }
223
+ * });
224
+ * ```
225
+ */
226
+ public async loadConfig(options: ConfigLoadOptions = {}): Promise<AppwriteConfig> {
227
+ // 1. Return cache if available and not forcing reload
228
+ if (this.cachedConfig && !options.forceReload) {
229
+ logger.debug("Returning cached config", { prefix: "ConfigManager" });
230
+ return this.getCachedConfigWithOverrides(options.overrides);
231
+ }
232
+
233
+ logger.debug("Loading config from file", { prefix: "ConfigManager", options });
234
+
235
+ // 2. Discover config file
236
+ const configPath = this.discoveryService.findConfig(options.configDir || process.cwd());
237
+
238
+ if (!configPath) {
239
+ const searchDir = options.configDir || process.cwd();
240
+ throw new Error(
241
+ `No Appwrite configuration found in "${searchDir}".\n` +
242
+ `Searched for: YAML (.appwrite/config.yaml), TypeScript (appwriteConfig.ts), or JSON (appwrite.json).\n` +
243
+ `Suggestion: Create a configuration file using "npx appwrite-migrate --init" or refer to the documentation.`
244
+ );
245
+ }
246
+
247
+ logger.debug(`Config discovered at: ${configPath}`, { prefix: "ConfigManager" });
248
+
249
+ // 3. Load config from file
250
+ let config = await this.loaderService.loadFromPath(configPath);
251
+
252
+ // 4. Load session authentication
253
+ const session =
254
+ options.sessionOverride ||
255
+ (await this.sessionService.findSession(config.appwriteEndpoint, config.appwriteProject));
256
+
257
+ // 5. Merge session into config
258
+ if (session) {
259
+ logger.debug("Merging session authentication into config", { prefix: "ConfigManager" });
260
+ config = this.mergeService.mergeSession(config, session);
261
+ this.cachedSession = session;
262
+ }
263
+
264
+ // 6. Apply CLI/env overrides
265
+ if (options.overrides) {
266
+ logger.debug("Applying configuration overrides", { prefix: "ConfigManager" });
267
+ config = this.mergeService.applyOverrides(config, options.overrides);
268
+ }
269
+
270
+ // 7. Validate if requested
271
+ if (options.validate) {
272
+ logger.debug("Validating configuration", { prefix: "ConfigManager", strictMode: options.strictMode });
273
+
274
+ const validation = options.strictMode
275
+ ? this.validationService.validateStrict(config)
276
+ : this.validationService.validate(config);
277
+
278
+ if (options.reportValidation) {
279
+ this.validationService.reportResults(validation, { verbose: false });
280
+ }
281
+
282
+ if (options.strictMode && !validation.isValid) {
283
+ throw new Error(
284
+ `Configuration validation failed in strict mode.\n` +
285
+ `Errors: ${validation.errors.length}, Warnings: ${validation.warnings.length}\n` +
286
+ `Run with reportValidation: true to see details.`
287
+ );
288
+ }
289
+ }
290
+
291
+ // 8. Cache the config
292
+ this.cachedConfig = config;
293
+ this.cachedConfigPath = configPath;
294
+ this.lastLoadTimestamp = Date.now();
295
+ this.isInitialized = true;
296
+
297
+ logger.debug("Config loaded and cached successfully", {
298
+ prefix: "ConfigManager",
299
+ path: configPath,
300
+ hasSession: !!session,
301
+ });
302
+
303
+ return config;
304
+ }
305
+
306
+ /**
307
+ * Get the cached configuration synchronously.
308
+ *
309
+ * @returns The cached configuration
310
+ * @throws Error if configuration has not been loaded yet
311
+ *
312
+ * @example
313
+ * ```typescript
314
+ * const config = configManager.getConfig();
315
+ * ```
316
+ */
317
+ public getConfig(): AppwriteConfig {
318
+ if (!this.cachedConfig) {
319
+ throw new Error(
320
+ "Configuration not loaded. Call loadConfig() first.\n" +
321
+ "Suggestion: await configManager.loadConfig();"
322
+ );
323
+ }
324
+ return this.cachedConfig;
325
+ }
326
+
327
+ /**
328
+ * Get the path to the loaded configuration file.
329
+ *
330
+ * @returns The configuration file path, or null if not loaded
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * const configPath = configManager.getConfigPath();
335
+ * console.log(`Config loaded from: ${configPath}`);
336
+ * ```
337
+ */
338
+ public getConfigPath(): string | null {
339
+ return this.cachedConfigPath;
340
+ }
341
+
342
+ /**
343
+ * Check if configuration has been loaded.
344
+ *
345
+ * @returns True if configuration is loaded and cached
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * if (!configManager.hasConfig()) {
350
+ * await configManager.loadConfig();
351
+ * }
352
+ * ```
353
+ */
354
+ public hasConfig(): boolean {
355
+ return this.cachedConfig !== null;
356
+ }
357
+
358
+ /**
359
+ * Check if the ConfigManager has been initialized.
360
+ *
361
+ * @returns True if initialized (config loaded at least once)
362
+ *
363
+ * @example
364
+ * ```typescript
365
+ * if (configManager.isConfigInitialized()) {
366
+ * console.log("Config ready");
367
+ * }
368
+ * ```
369
+ */
370
+ public isConfigInitialized(): boolean {
371
+ return this.isInitialized;
372
+ }
373
+
374
+ /**
375
+ * Invalidate the configuration cache.
376
+ * Next call to loadConfig() will reload from disk.
377
+ *
378
+ * @example
379
+ * ```typescript
380
+ * configManager.invalidateCache();
381
+ * const freshConfig = await configManager.loadConfig();
382
+ * ```
383
+ */
384
+ public invalidateCache(): void {
385
+ logger.debug("Invalidating config cache", { prefix: "ConfigManager" });
386
+ this.cachedConfig = null;
387
+ this.cachedConfigPath = null;
388
+ this.lastLoadTimestamp = 0;
389
+ // Note: session cache is preserved by SessionAuthService
390
+ }
391
+
392
+ /**
393
+ * Reload configuration from disk, preserving current session.
394
+ *
395
+ * This is a convenience method that combines invalidation and reloading
396
+ * while automatically preserving the current session authentication.
397
+ *
398
+ * @param options - Configuration loading options (forceReload is automatically set)
399
+ * @returns The reloaded configuration
400
+ *
401
+ * @example
402
+ * ```typescript
403
+ * // Reload config after manual file changes
404
+ * const config = await configManager.reloadConfig({
405
+ * validate: true,
406
+ * reportValidation: true
407
+ * });
408
+ * ```
409
+ */
410
+ public async reloadConfig(
411
+ options: Omit<ConfigLoadOptions, "forceReload"> = {}
412
+ ): Promise<AppwriteConfig> {
413
+ logger.debug("Reloading config with session preservation", { prefix: "ConfigManager" });
414
+
415
+ // Preserve current session during reload
416
+ const currentSession = this.cachedSession;
417
+
418
+ return this.loadConfig({
419
+ ...options,
420
+ forceReload: true,
421
+ sessionOverride: currentSession || undefined,
422
+ });
423
+ }
424
+
425
+ // ──────────────────────────────────────────────────
426
+ // SESSION MANAGEMENT
427
+ // ──────────────────────────────────────────────────
428
+
429
+ /**
430
+ * Get the current session information.
431
+ *
432
+ * @returns The cached session info, or null if no session
433
+ *
434
+ * @example
435
+ * ```typescript
436
+ * const session = configManager.getSession();
437
+ * if (session) {
438
+ * console.log(`Authenticated as: ${session.email}`);
439
+ * }
440
+ * ```
441
+ */
442
+ public getSession(): SessionAuthInfo | null {
443
+ return this.cachedSession;
444
+ }
445
+
446
+ /**
447
+ * Check if a session is available.
448
+ *
449
+ * @returns True if a session is cached
450
+ *
451
+ * @example
452
+ * ```typescript
453
+ * if (configManager.hasSession()) {
454
+ * console.log("Using session authentication");
455
+ * } else {
456
+ * console.log("Using API key authentication");
457
+ * }
458
+ * ```
459
+ */
460
+ public hasSession(): boolean {
461
+ return this.cachedSession !== null;
462
+ }
463
+
464
+ /**
465
+ * Refresh session from disk (bypasses session cache).
466
+ *
467
+ * This forces SessionAuthService to reload from ~/.appwrite/prefs.json
468
+ * and update the cached session.
469
+ *
470
+ * @returns The refreshed session, or null if no session found
471
+ *
472
+ * @example
473
+ * ```typescript
474
+ * // After logging in via appwrite CLI
475
+ * const session = await configManager.refreshSession();
476
+ * if (session) {
477
+ * console.log("Session refreshed successfully");
478
+ * }
479
+ * ```
480
+ */
481
+ public async refreshSession(): Promise<SessionAuthInfo | null> {
482
+ if (!this.cachedConfig) {
483
+ logger.debug("Cannot refresh session - no config loaded", { prefix: "ConfigManager" });
484
+ return null;
485
+ }
486
+
487
+ logger.debug("Refreshing session from disk", { prefix: "ConfigManager" });
488
+
489
+ // Invalidate session cache
490
+ this.sessionService.invalidateCache();
491
+
492
+ // Reload session
493
+ const session = await this.sessionService.findSession(
494
+ this.cachedConfig.appwriteEndpoint,
495
+ this.cachedConfig.appwriteProject
496
+ );
497
+
498
+ if (session) {
499
+ this.cachedSession = session;
500
+ logger.debug("Session refreshed successfully", { prefix: "ConfigManager" });
501
+ } else {
502
+ this.cachedSession = null;
503
+ logger.debug("No session found after refresh", { prefix: "ConfigManager" });
504
+ }
505
+
506
+ return session;
507
+ }
508
+
509
+ /**
510
+ * Get authentication status for the current configuration.
511
+ *
512
+ * @returns Authentication status object with details about current auth method
513
+ * @throws Error if configuration has not been loaded
514
+ *
515
+ * @example
516
+ * ```typescript
517
+ * const authStatus = await configManager.getAuthStatus();
518
+ * console.log(`Auth method: ${authStatus.authMethod}`);
519
+ * console.log(`Has valid session: ${authStatus.hasValidSession}`);
520
+ * ```
521
+ */
522
+ public async getAuthStatus(): Promise<AuthenticationStatus> {
523
+ if (!this.cachedConfig) {
524
+ throw new Error(
525
+ "Configuration not loaded. Call loadConfig() first.\n" +
526
+ "Suggestion: await configManager.loadConfig();"
527
+ );
528
+ }
529
+
530
+ return await this.sessionService.getAuthenticationStatus(
531
+ this.cachedConfig.appwriteEndpoint,
532
+ this.cachedConfig.appwriteProject,
533
+ this.cachedConfig.appwriteKey,
534
+ this.cachedSession
535
+ );
536
+ }
537
+
538
+ /**
539
+ * Clear the cached session.
540
+ * Next load will attempt to find a new session.
541
+ *
542
+ * @example
543
+ * ```typescript
544
+ * configManager.clearSession();
545
+ * const config = await configManager.reloadConfig();
546
+ * ```
547
+ */
548
+ public clearSession(): void {
549
+ logger.debug("Clearing cached session", { prefix: "ConfigManager" });
550
+ this.cachedSession = null;
551
+ }
552
+
553
+ // ──────────────────────────────────────────────────
554
+ // VALIDATION
555
+ // ──────────────────────────────────────────────────
556
+
557
+ /**
558
+ * Validate the current configuration (warnings allowed).
559
+ *
560
+ * @param reportResults - Whether to report validation results to console
561
+ * @returns Validation result with errors and warnings
562
+ * @throws Error if configuration has not been loaded
563
+ *
564
+ * @example
565
+ * ```typescript
566
+ * const validation = configManager.validateConfig(true);
567
+ * if (!validation.isValid) {
568
+ * console.log(`Found ${validation.errors.length} errors`);
569
+ * }
570
+ * ```
571
+ */
572
+ public validateConfig(reportResults: boolean = false): ValidationResult {
573
+ if (!this.cachedConfig) {
574
+ throw new Error(
575
+ "Configuration not loaded. Call loadConfig() first.\n" +
576
+ "Suggestion: await configManager.loadConfig();"
577
+ );
578
+ }
579
+
580
+ const validation = this.validationService.validate(this.cachedConfig);
581
+
582
+ if (reportResults) {
583
+ this.validationService.reportResults(validation);
584
+ }
585
+
586
+ return validation;
587
+ }
588
+
589
+ /**
590
+ * Validate the current configuration in strict mode (warnings as errors).
591
+ *
592
+ * @param reportResults - Whether to report validation results to console
593
+ * @returns Validation result with errors and warnings (warnings treated as errors)
594
+ * @throws Error if configuration has not been loaded
595
+ *
596
+ * @example
597
+ * ```typescript
598
+ * const validation = configManager.validateConfigStrict(true);
599
+ * if (!validation.isValid) {
600
+ * console.log("Config validation failed (strict mode)");
601
+ * }
602
+ * ```
603
+ */
604
+ public validateConfigStrict(reportResults: boolean = false): ValidationResult {
605
+ if (!this.cachedConfig) {
606
+ throw new Error(
607
+ "Configuration not loaded. Call loadConfig() first.\n" +
608
+ "Suggestion: await configManager.loadConfig();"
609
+ );
610
+ }
611
+
612
+ const validation = this.validationService.validateStrict(this.cachedConfig);
613
+
614
+ if (reportResults) {
615
+ this.validationService.reportResults(validation);
616
+ }
617
+
618
+ return validation;
619
+ }
620
+
621
+ // ──────────────────────────────────────────────────
622
+ // ADVANCED / CONVENIENCE
623
+ // ──────────────────────────────────────────────────
624
+
625
+ /**
626
+ * Watch configuration file for changes and reload automatically.
627
+ *
628
+ * Returns a function to stop watching.
629
+ *
630
+ * @param callback - Function to call when config changes
631
+ * @returns Function to stop watching
632
+ *
633
+ * @example
634
+ * ```typescript
635
+ * const unwatch = configManager.watchConfig(async (config) => {
636
+ * console.log("Config changed, reloading...");
637
+ * // Handle config change
638
+ * });
639
+ *
640
+ * // Later, stop watching
641
+ * unwatch();
642
+ * ```
643
+ */
644
+ public watchConfig(callback: (config: AppwriteConfig) => void | Promise<void>): () => void {
645
+ if (!this.cachedConfigPath) {
646
+ throw new Error(
647
+ "Cannot watch config - no config file loaded.\n" +
648
+ "Suggestion: Call loadConfig() before watchConfig()."
649
+ );
650
+ }
651
+
652
+ const configPath = this.cachedConfigPath;
653
+ logger.debug(`Setting up config file watcher: ${configPath}`, { prefix: "ConfigManager" });
654
+
655
+ let isProcessing = false;
656
+
657
+ const watcher = fs.watch(configPath, async (eventType) => {
658
+ if (eventType === "change" && !isProcessing) {
659
+ isProcessing = true;
660
+ logger.debug("Config file changed, reloading...", { prefix: "ConfigManager" });
661
+
662
+ try {
663
+ const newConfig = await this.reloadConfig();
664
+ await callback(newConfig);
665
+ } catch (error) {
666
+ MessageFormatter.error(
667
+ "Failed to reload config after file change",
668
+ error instanceof Error ? error : String(error),
669
+ { prefix: "ConfigManager" }
670
+ );
671
+ } finally {
672
+ isProcessing = false;
673
+ }
674
+ }
675
+ });
676
+
677
+ // Return cleanup function
678
+ return () => {
679
+ logger.debug("Stopping config file watcher", { prefix: "ConfigManager" });
680
+ watcher.close();
681
+ };
682
+ }
683
+
684
+ /**
685
+ * Merge overrides into the current configuration without persisting.
686
+ *
687
+ * This creates a new config object with overrides applied, without
688
+ * affecting the cached configuration.
689
+ *
690
+ * @param overrides - Configuration overrides to apply
691
+ * @returns New configuration with overrides applied
692
+ * @throws Error if configuration has not been loaded
693
+ *
694
+ * @example
695
+ * ```typescript
696
+ * const customConfig = configManager.mergeOverrides({
697
+ * appwriteEndpoint: "https://test.endpoint.com/v1"
698
+ * });
699
+ * ```
700
+ */
701
+ public mergeOverrides(overrides: ConfigOverrides): AppwriteConfig {
702
+ if (!this.cachedConfig) {
703
+ throw new Error(
704
+ "Configuration not loaded. Call loadConfig() first.\n" +
705
+ "Suggestion: await configManager.loadConfig();"
706
+ );
707
+ }
708
+
709
+ return this.mergeService.applyOverrides(this.cachedConfig, overrides);
710
+ }
711
+
712
+ /**
713
+ * Get collections from the configuration, optionally filtered.
714
+ *
715
+ * @param filter - Optional filter function for collections
716
+ * @returns Array of collections (or tables, depending on API mode)
717
+ *
718
+ * @example
719
+ * ```typescript
720
+ * // Get all collections
721
+ * const allCollections = configManager.getCollections();
722
+ *
723
+ * // Get enabled collections only
724
+ * const enabledCollections = configManager.getCollections(
725
+ * (c) => c.enabled !== false
726
+ * );
727
+ * ```
728
+ */
729
+ public getCollections(filter?: CollectionFilter): (Collection | CollectionCreate)[] {
730
+ const config = this.getConfig();
731
+ const collections = config.collections || config.tables || [];
732
+
733
+ if (filter) {
734
+ return collections.filter(filter);
735
+ }
736
+
737
+ return collections;
738
+ }
739
+
740
+ /**
741
+ * Get databases from the configuration.
742
+ *
743
+ * @returns Array of databases
744
+ *
745
+ * @example
746
+ * ```typescript
747
+ * const databases = configManager.getDatabases();
748
+ * console.log(`Found ${databases.length} databases`);
749
+ * ```
750
+ */
751
+ public getDatabases(): Database[] {
752
+ const config = this.getConfig();
753
+ return config.databases || [];
754
+ }
755
+
756
+ /**
757
+ * Get buckets from the configuration.
758
+ *
759
+ * @returns Array of buckets
760
+ *
761
+ * @example
762
+ * ```typescript
763
+ * const buckets = configManager.getBuckets();
764
+ * console.log(`Found ${buckets.length} buckets`);
765
+ * ```
766
+ */
767
+ public getBuckets(): Bucket[] {
768
+ const config = this.getConfig();
769
+ return config.buckets || [];
770
+ }
771
+
772
+ /**
773
+ * Get functions from the configuration.
774
+ *
775
+ * @returns Array of functions
776
+ *
777
+ * @example
778
+ * ```typescript
779
+ * const functions = configManager.getFunctions();
780
+ * console.log(`Found ${functions.length} functions`);
781
+ * ```
782
+ */
783
+ public getFunctions(): AppwriteFunction[] {
784
+ const config = this.getConfig();
785
+ return config.functions || [];
786
+ }
787
+
788
+ // ──────────────────────────────────────────────────
789
+ // PRIVATE HELPERS
790
+ // ──────────────────────────────────────────────────
791
+
792
+ /**
793
+ * Get cached config with optional overrides applied.
794
+ * Used internally to avoid reloading when only overrides change.
795
+ */
796
+ private getCachedConfigWithOverrides(overrides?: ConfigOverrides): AppwriteConfig {
797
+ if (!this.cachedConfig) {
798
+ throw new Error("No cached config available");
799
+ }
800
+
801
+ if (!overrides) {
802
+ return this.cachedConfig;
803
+ }
804
+
805
+ logger.debug("Applying overrides to cached config", { prefix: "ConfigManager" });
806
+ return this.mergeService.applyOverrides(this.cachedConfig, overrides);
807
+ }
808
+ }