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,271 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from "@jest/globals";
2
+ import { ConfigMergeService } from "../ConfigMergeService.js";
3
+ describe("ConfigMergeService", () => {
4
+ let service;
5
+ let baseConfig;
6
+ let sessionInfo;
7
+ // Store original env vars
8
+ const originalEnv = { ...process.env };
9
+ beforeEach(() => {
10
+ service = new ConfigMergeService();
11
+ baseConfig = {
12
+ appwriteEndpoint: "https://cloud.appwrite.io/v1",
13
+ appwriteProject: "test-project",
14
+ appwriteKey: "test-api-key",
15
+ databases: [],
16
+ buckets: [],
17
+ functions: [],
18
+ };
19
+ sessionInfo = {
20
+ projectId: "test-project",
21
+ endpoint: "https://cloud.appwrite.io/v1",
22
+ cookie: "eyJhbGc.test.cookie",
23
+ email: "test@example.com",
24
+ };
25
+ });
26
+ afterEach(() => {
27
+ // Restore original env vars
28
+ process.env = { ...originalEnv };
29
+ });
30
+ describe("mergeSession", () => {
31
+ it("should merge session cookie into config", () => {
32
+ const result = service.mergeSession(baseConfig, sessionInfo);
33
+ expect(result.sessionCookie).toBe("eyJhbGc.test.cookie");
34
+ expect(result.authMethod).toBe("session");
35
+ });
36
+ it("should not mutate original config", () => {
37
+ const original = { ...baseConfig };
38
+ service.mergeSession(baseConfig, sessionInfo);
39
+ expect(baseConfig).toEqual(original);
40
+ });
41
+ it("should preserve all existing config properties", () => {
42
+ const result = service.mergeSession(baseConfig, sessionInfo);
43
+ expect(result.appwriteEndpoint).toBe(baseConfig.appwriteEndpoint);
44
+ expect(result.appwriteProject).toBe(baseConfig.appwriteProject);
45
+ expect(result.appwriteKey).toBe(baseConfig.appwriteKey);
46
+ });
47
+ });
48
+ describe("applyOverrides", () => {
49
+ it("should apply all provided overrides", () => {
50
+ const overrides = {
51
+ appwriteEndpoint: "https://custom.appwrite.io/v1",
52
+ appwriteProject: "override-project",
53
+ appwriteKey: "override-key",
54
+ sessionCookie: "override-cookie",
55
+ authMethod: "apikey",
56
+ };
57
+ const result = service.applyOverrides(baseConfig, overrides);
58
+ expect(result.appwriteEndpoint).toBe("https://custom.appwrite.io/v1");
59
+ expect(result.appwriteProject).toBe("override-project");
60
+ expect(result.appwriteKey).toBe("override-key");
61
+ expect(result.sessionCookie).toBe("override-cookie");
62
+ expect(result.authMethod).toBe("apikey");
63
+ });
64
+ it("should skip undefined and null values", () => {
65
+ const overrides = {
66
+ appwriteEndpoint: undefined,
67
+ appwriteProject: null,
68
+ appwriteKey: "new-key",
69
+ };
70
+ const result = service.applyOverrides(baseConfig, overrides);
71
+ expect(result.appwriteEndpoint).toBe(baseConfig.appwriteEndpoint);
72
+ expect(result.appwriteProject).toBe(baseConfig.appwriteProject);
73
+ expect(result.appwriteKey).toBe("new-key");
74
+ });
75
+ it("should not mutate original config", () => {
76
+ const original = { ...baseConfig };
77
+ const overrides = {
78
+ appwriteEndpoint: "https://custom.appwrite.io/v1",
79
+ };
80
+ service.applyOverrides(baseConfig, overrides);
81
+ expect(baseConfig).toEqual(original);
82
+ });
83
+ });
84
+ describe("mergeEnvironmentVariables", () => {
85
+ it("should merge environment variables when config values are not set", () => {
86
+ process.env.APPWRITE_ENDPOINT = "https://env.appwrite.io/v1";
87
+ process.env.APPWRITE_PROJECT = "env-project";
88
+ process.env.APPWRITE_API_KEY = "env-api-key";
89
+ process.env.APPWRITE_SESSION_COOKIE = "env-cookie";
90
+ const emptyConfig = {
91
+ databases: [],
92
+ buckets: [],
93
+ functions: [],
94
+ };
95
+ const result = service.mergeEnvironmentVariables(emptyConfig);
96
+ expect(result.appwriteEndpoint).toBe("https://env.appwrite.io/v1");
97
+ expect(result.appwriteProject).toBe("env-project");
98
+ expect(result.appwriteKey).toBe("env-api-key");
99
+ expect(result.sessionCookie).toBe("env-cookie");
100
+ });
101
+ it("should not override existing config values", () => {
102
+ process.env.APPWRITE_ENDPOINT = "https://env.appwrite.io/v1";
103
+ process.env.APPWRITE_PROJECT = "env-project";
104
+ const result = service.mergeEnvironmentVariables(baseConfig);
105
+ expect(result.appwriteEndpoint).toBe(baseConfig.appwriteEndpoint);
106
+ expect(result.appwriteProject).toBe(baseConfig.appwriteProject);
107
+ });
108
+ it("should ignore empty environment variables", () => {
109
+ process.env.APPWRITE_ENDPOINT = "";
110
+ process.env.APPWRITE_PROJECT = "";
111
+ const emptyConfig = {
112
+ databases: [],
113
+ buckets: [],
114
+ functions: [],
115
+ };
116
+ const result = service.mergeEnvironmentVariables(emptyConfig);
117
+ expect(result.appwriteEndpoint).toBeUndefined();
118
+ expect(result.appwriteProject).toBeUndefined();
119
+ });
120
+ });
121
+ describe("mergeSources", () => {
122
+ it("should merge multiple sources in priority order", () => {
123
+ const source1 = {
124
+ appwriteEndpoint: "https://source1.appwrite.io/v1",
125
+ appwriteProject: "source1-project",
126
+ };
127
+ const source2 = {
128
+ appwriteProject: "source2-project",
129
+ appwriteKey: "source2-key",
130
+ };
131
+ const source3 = {
132
+ appwriteKey: "source3-key",
133
+ sessionCookie: "source3-cookie",
134
+ };
135
+ const result = service.mergeSources([source1, source2, source3]);
136
+ expect(result.appwriteEndpoint).toBe("https://source1.appwrite.io/v1");
137
+ expect(result.appwriteProject).toBe("source2-project");
138
+ expect(result.appwriteKey).toBe("source3-key");
139
+ expect(result.sessionCookie).toBe("source3-cookie");
140
+ });
141
+ it("should handle arrays by replacing, not merging", () => {
142
+ const source1 = {
143
+ appwriteEndpoint: "https://cloud.appwrite.io/v1",
144
+ appwriteProject: "test",
145
+ databases: [{ $id: "db1", name: "db1" }],
146
+ };
147
+ const source2 = {
148
+ databases: [{ $id: "db2", name: "db2" }, { $id: "db3", name: "db3" }],
149
+ };
150
+ const result = service.mergeSources([source1, source2]);
151
+ expect(result.databases).toHaveLength(2);
152
+ expect(result.databases).toEqual([{ $id: "db2", name: "db2" }, { $id: "db3", name: "db3" }]);
153
+ });
154
+ it("should skip undefined and null sources", () => {
155
+ const source1 = {
156
+ appwriteEndpoint: "https://cloud.appwrite.io/v1",
157
+ appwriteProject: "test",
158
+ };
159
+ const result = service.mergeSources([
160
+ source1,
161
+ undefined,
162
+ null,
163
+ { appwriteKey: "key" },
164
+ ]);
165
+ expect(result.appwriteEndpoint).toBe("https://cloud.appwrite.io/v1");
166
+ expect(result.appwriteProject).toBe("test");
167
+ expect(result.appwriteKey).toBe("key");
168
+ });
169
+ it("should throw error when no valid sources provided", () => {
170
+ expect(() => service.mergeSources([])).toThrow("No valid configuration sources provided for merging");
171
+ expect(() => service.mergeSources([undefined, null])).toThrow("No valid configuration sources provided for merging");
172
+ });
173
+ it("should throw error when merged config lacks required fields", () => {
174
+ const invalidSource = {
175
+ appwriteKey: "key",
176
+ };
177
+ expect(() => service.mergeSources([invalidSource])).toThrow("Merged configuration is missing required fields");
178
+ });
179
+ });
180
+ describe("mergeAllSources", () => {
181
+ it("should merge all sources in correct priority order", () => {
182
+ process.env.APPWRITE_ENDPOINT = "https://env.appwrite.io/v1";
183
+ process.env.APPWRITE_API_KEY = "env-key";
184
+ const overrides = {
185
+ appwriteKey: "override-key",
186
+ };
187
+ const result = service.mergeAllSources(baseConfig, {
188
+ session: sessionInfo,
189
+ overrides,
190
+ includeEnv: true,
191
+ });
192
+ // Base config endpoint
193
+ expect(result.appwriteEndpoint).toBe(baseConfig.appwriteEndpoint);
194
+ // Session should set session cookie
195
+ expect(result.sessionCookie).toBe(sessionInfo.cookie);
196
+ // Override should win for API key
197
+ expect(result.appwriteKey).toBe("override-key");
198
+ });
199
+ it("should exclude environment variables when includeEnv is false", () => {
200
+ process.env.APPWRITE_SESSION_COOKIE = "env-cookie";
201
+ const configWithoutKey = {
202
+ ...baseConfig,
203
+ appwriteKey: undefined,
204
+ };
205
+ const result = service.mergeAllSources(configWithoutKey, {
206
+ includeEnv: false,
207
+ });
208
+ expect(result.sessionCookie).toBeUndefined();
209
+ });
210
+ it("should work with only base config", () => {
211
+ const result = service.mergeAllSources(baseConfig, {});
212
+ expect(result.appwriteEndpoint).toBe(baseConfig.appwriteEndpoint);
213
+ expect(result.appwriteProject).toBe(baseConfig.appwriteProject);
214
+ });
215
+ it("should apply priority: overrides > session > env > base", () => {
216
+ process.env.APPWRITE_KEY = "env-key";
217
+ const configWithoutKey = {
218
+ ...baseConfig,
219
+ appwriteKey: undefined,
220
+ sessionCookie: undefined,
221
+ };
222
+ // Test with session (should override env)
223
+ const withSession = service.mergeAllSources(configWithoutKey, {
224
+ session: sessionInfo,
225
+ includeEnv: true,
226
+ });
227
+ expect(withSession.sessionCookie).toBe(sessionInfo.cookie);
228
+ expect(withSession.authMethod).toBe("session");
229
+ // Test with override (should override session)
230
+ const withOverride = service.mergeAllSources(configWithoutKey, {
231
+ session: sessionInfo,
232
+ overrides: { authMethod: "apikey" },
233
+ includeEnv: true,
234
+ });
235
+ expect(withOverride.authMethod).toBe("apikey");
236
+ });
237
+ });
238
+ describe("deep merge behavior", () => {
239
+ it("should deeply merge nested objects", () => {
240
+ const source1 = {
241
+ appwriteEndpoint: "https://cloud.appwrite.io/v1",
242
+ appwriteProject: "test",
243
+ databases: [
244
+ {
245
+ $id: "db1",
246
+ name: "Database 1",
247
+ },
248
+ ],
249
+ };
250
+ const source2 = {
251
+ databases: [
252
+ {
253
+ $id: "db1",
254
+ name: "Updated Database",
255
+ },
256
+ ],
257
+ };
258
+ const result = service.mergeSources([source1, source2]);
259
+ // Arrays are replaced, not merged
260
+ expect(result.databases).toHaveLength(1);
261
+ expect(result.databases[0].name).toBe("Updated Database");
262
+ });
263
+ it("should preserve immutability throughout merge chain", () => {
264
+ const original1 = { ...baseConfig };
265
+ const original2 = { appwriteKey: "new-key" };
266
+ service.mergeSources([baseConfig, original2]);
267
+ expect(baseConfig).toEqual(original1);
268
+ expect(original2).toEqual({ appwriteKey: "new-key" });
269
+ });
270
+ });
271
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Configuration Services
3
+ *
4
+ * Modular services for configuration management, discovery, loading, validation, and merging.
5
+ */
6
+ export { ConfigDiscoveryService } from "./ConfigDiscoveryService.js";
7
+ export type { DiscoveryResult } from "./ConfigDiscoveryService.js";
8
+ export { ConfigLoaderService } from "./ConfigLoaderService.js";
9
+ export type { CollectionLoadOptions } from "./ConfigLoaderService.js";
10
+ export { ConfigMergeService } from "./ConfigMergeService.js";
11
+ export type { ConfigOverrides } from "./ConfigMergeService.js";
12
+ export { SessionAuthService, type SessionAuthInfo, type AppwriteSessionPrefs, type AuthenticationStatus } from "./SessionAuthService.js";
13
+ export { ConfigValidationService, type ValidationResult, type ValidationError, type ValidationWarning, type ValidationReportOptions } from "./ConfigValidationService.js";
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Configuration Services
3
+ *
4
+ * Modular services for configuration management, discovery, loading, validation, and merging.
5
+ */
6
+ export { ConfigDiscoveryService } from "./ConfigDiscoveryService.js";
7
+ export { ConfigLoaderService } from "./ConfigLoaderService.js";
8
+ export { ConfigMergeService } from "./ConfigMergeService.js";
9
+ export { SessionAuthService } from "./SessionAuthService.js";
10
+ export { ConfigValidationService } from "./ConfigValidationService.js";
@@ -160,12 +160,12 @@ export class InteractiveCLI {
160
160
  }
161
161
  async initControllerIfNeeded(directConfig) {
162
162
  if (!this.controller) {
163
- this.controller = new UtilsController(this.currentDir, directConfig);
163
+ this.controller = UtilsController.getInstance(this.currentDir, directConfig);
164
164
  await this.controller.init();
165
165
  }
166
166
  else {
167
167
  // Extract session info from existing controller before reinitializing
168
- const sessionInfo = this.controller.getSessionInfo();
168
+ const sessionInfo = await this.controller.getSessionInfo();
169
169
  if (sessionInfo.hasSession && directConfig) {
170
170
  // Create enhanced directConfig with session preservation
171
171
  const enhancedDirectConfig = {
@@ -174,12 +174,14 @@ export class InteractiveCLI {
174
174
  sessionMetadata: this.controller.sessionMetadata
175
175
  };
176
176
  // Reinitialize with session preservation
177
- this.controller = new UtilsController(this.currentDir, enhancedDirectConfig);
177
+ UtilsController.clearInstance();
178
+ this.controller = UtilsController.getInstance(this.currentDir, enhancedDirectConfig);
178
179
  await this.controller.init();
179
180
  }
180
181
  else if (directConfig) {
181
182
  // Standard reinitialize without session
182
- this.controller = new UtilsController(this.currentDir, directConfig);
183
+ UtilsController.clearInstance();
184
+ this.controller = UtilsController.getInstance(this.currentDir, directConfig);
183
185
  await this.controller.init();
184
186
  }
185
187
  // If no directConfig provided, keep existing controller
@@ -776,11 +778,11 @@ export class InteractiveCLI {
776
778
  /**
777
779
  * Extract session information from current controller for preservation
778
780
  */
779
- extractSessionFromController() {
781
+ async extractSessionFromController() {
780
782
  if (!this.controller?.config) {
781
783
  return undefined;
782
784
  }
783
- const sessionInfo = this.controller.getSessionInfo();
785
+ const sessionInfo = await this.controller.getSessionInfo();
784
786
  const config = this.controller.config;
785
787
  if (!config.appwriteEndpoint || !config.appwriteProject) {
786
788
  return undefined;
package/dist/main.js CHANGED
@@ -353,8 +353,8 @@ async function main() {
353
353
  argv.useSession = true;
354
354
  }
355
355
  }
356
- // Create controller with session authentication support
357
- const controller = new UtilsController(process.cwd(), finalDirectConfig);
356
+ // Create controller with session authentication support using singleton
357
+ const controller = UtilsController.getInstance(process.cwd(), finalDirectConfig);
358
358
  // Pass session authentication options to the controller
359
359
  const initOptions = {};
360
360
  if (argv.useSession || argv.sessionCookie) {
@@ -6,8 +6,8 @@ export declare const YamlImportConfigSchema: z.ZodObject<{
6
6
  basePath: z.ZodOptional<z.ZodString>;
7
7
  type: z.ZodDefault<z.ZodEnum<{
8
8
  json: "json";
9
- csv: "csv";
10
9
  yaml: "yaml";
10
+ csv: "csv";
11
11
  }>>;
12
12
  }, z.core.$strip>;
13
13
  target: z.ZodObject<{
@@ -55,7 +55,7 @@ export const clearProcessingState = () => {
55
55
  processedCollections.clear();
56
56
  processedAttributes.clear();
57
57
  nameToIdMapping.clear();
58
- MessageFormatter.success("Cleared processing state caches");
58
+ logger.debug("Cleared processing state caches", { operation: "clearProcessingState", sizeBefore });
59
59
  logger.info('Processing state cleared', {
60
60
  sizeBefore,
61
61
  operation: 'clearProcessingState'
@@ -0,0 +1,87 @@
1
+ import { Client } from "node-appwrite";
2
+ import type { AppwriteConfig } from "appwrite-utils";
3
+ import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
4
+ /**
5
+ * Factory for creating authenticated Appwrite clients and database adapters.
6
+ *
7
+ * This factory provides a clean separation of concerns by taking pre-configured
8
+ * AppwriteConfig objects (with authentication already resolved by ConfigManager)
9
+ * and creating the necessary client and adapter instances.
10
+ *
11
+ * Key features:
12
+ * - Takes pre-authenticated config from ConfigManager
13
+ * - Creates client with appropriate authentication method
14
+ * - Creates adapter with automatic version detection
15
+ * - Leverages AdapterFactory's internal caching for performance
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const configManager = ConfigManager.getInstance();
20
+ * const config = await configManager.loadConfig();
21
+ *
22
+ * // Config already has session or API key resolved
23
+ * const { client, adapter } = await ClientFactory.createFromConfig(config);
24
+ * ```
25
+ */
26
+ export declare class ClientFactory {
27
+ /**
28
+ * Create authenticated client and database adapter from configuration.
29
+ *
30
+ * This method expects the config to have authentication already resolved:
31
+ * - Either `sessionCookie` is set (from ConfigManager's session loading)
32
+ * - Or `appwriteKey` is set (from config file or CLI overrides)
33
+ *
34
+ * The ConfigManager handles all session discovery and authentication priority,
35
+ * so this factory simply applies the resolved authentication to the client.
36
+ *
37
+ * @param config - AppwriteConfig with resolved authentication
38
+ * @returns Object containing authenticated client and database adapter
39
+ * @throws Error if no authentication method is available in config
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const config = await ConfigManager.getInstance().loadConfig();
44
+ * const { client, adapter } = await ClientFactory.createFromConfig(config);
45
+ *
46
+ * // Client is now authenticated and ready to use
47
+ * const databases = new Databases(client);
48
+ * const collections = await adapter.listCollections(databaseId);
49
+ * ```
50
+ */
51
+ static createFromConfig(config: AppwriteConfig): Promise<{
52
+ client: Client;
53
+ adapter: DatabaseAdapter;
54
+ }>;
55
+ /**
56
+ * Create client and adapter from individual parameters.
57
+ *
58
+ * This is a lower-level method for cases where you don't have a full
59
+ * AppwriteConfig object. For most use cases, prefer createFromConfig().
60
+ *
61
+ * @param endpoint - Appwrite endpoint URL
62
+ * @param project - Appwrite project ID
63
+ * @param options - Authentication and API mode options
64
+ * @returns Object containing authenticated client and database adapter
65
+ * @throws Error if no authentication method is provided
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * const { client, adapter } = await ClientFactory.create(
70
+ * "https://cloud.appwrite.io/v1",
71
+ * "my-project-id",
72
+ * {
73
+ * apiKey: "my-api-key",
74
+ * apiMode: "auto"
75
+ * }
76
+ * );
77
+ * ```
78
+ */
79
+ static create(endpoint: string, project: string, options?: {
80
+ apiKey?: string;
81
+ sessionCookie?: string;
82
+ apiMode?: "auto" | "legacy" | "tablesdb";
83
+ }): Promise<{
84
+ client: Client;
85
+ adapter: DatabaseAdapter;
86
+ }>;
87
+ }
@@ -0,0 +1,164 @@
1
+ import { Client } from "node-appwrite";
2
+ import { AdapterFactory } from "../adapters/AdapterFactory.js";
3
+ import { logger } from "../shared/logging.js";
4
+ /**
5
+ * Factory for creating authenticated Appwrite clients and database adapters.
6
+ *
7
+ * This factory provides a clean separation of concerns by taking pre-configured
8
+ * AppwriteConfig objects (with authentication already resolved by ConfigManager)
9
+ * and creating the necessary client and adapter instances.
10
+ *
11
+ * Key features:
12
+ * - Takes pre-authenticated config from ConfigManager
13
+ * - Creates client with appropriate authentication method
14
+ * - Creates adapter with automatic version detection
15
+ * - Leverages AdapterFactory's internal caching for performance
16
+ *
17
+ * @example
18
+ * ```typescript
19
+ * const configManager = ConfigManager.getInstance();
20
+ * const config = await configManager.loadConfig();
21
+ *
22
+ * // Config already has session or API key resolved
23
+ * const { client, adapter } = await ClientFactory.createFromConfig(config);
24
+ * ```
25
+ */
26
+ export class ClientFactory {
27
+ /**
28
+ * Create authenticated client and database adapter from configuration.
29
+ *
30
+ * This method expects the config to have authentication already resolved:
31
+ * - Either `sessionCookie` is set (from ConfigManager's session loading)
32
+ * - Or `appwriteKey` is set (from config file or CLI overrides)
33
+ *
34
+ * The ConfigManager handles all session discovery and authentication priority,
35
+ * so this factory simply applies the resolved authentication to the client.
36
+ *
37
+ * @param config - AppwriteConfig with resolved authentication
38
+ * @returns Object containing authenticated client and database adapter
39
+ * @throws Error if no authentication method is available in config
40
+ *
41
+ * @example
42
+ * ```typescript
43
+ * const config = await ConfigManager.getInstance().loadConfig();
44
+ * const { client, adapter } = await ClientFactory.createFromConfig(config);
45
+ *
46
+ * // Client is now authenticated and ready to use
47
+ * const databases = new Databases(client);
48
+ * const collections = await adapter.listCollections(databaseId);
49
+ * ```
50
+ */
51
+ static async createFromConfig(config) {
52
+ logger.debug("Creating client from config", {
53
+ prefix: "ClientFactory",
54
+ hasSession: !!config.sessionCookie,
55
+ hasApiKey: !!config.appwriteKey,
56
+ endpoint: config.appwriteEndpoint,
57
+ project: config.appwriteProject,
58
+ });
59
+ // Create base client with endpoint and project
60
+ const client = new Client()
61
+ .setEndpoint(config.appwriteEndpoint)
62
+ .setProject(config.appwriteProject);
63
+ // Apply authentication (priority already resolved by ConfigManager)
64
+ if (config.sessionCookie) {
65
+ // Session authentication (from ConfigManager's session loading)
66
+ client.setSession(config.sessionCookie);
67
+ logger.debug("Applied session authentication to client", {
68
+ prefix: "ClientFactory",
69
+ email: config.sessionMetadata?.email,
70
+ });
71
+ }
72
+ else if (config.appwriteKey) {
73
+ // API key authentication (from config file or overrides)
74
+ client.setKey(config.appwriteKey);
75
+ logger.debug("Applied API key authentication to client", {
76
+ prefix: "ClientFactory",
77
+ });
78
+ }
79
+ else {
80
+ // No authentication available - this should have been caught by ConfigManager
81
+ const error = new Error("No authentication method available in configuration.\n\n" +
82
+ "This should have been resolved by ConfigManager during config loading.\n" +
83
+ "Expected either:\n" +
84
+ " - config.sessionCookie (from session authentication)\n" +
85
+ " - config.appwriteKey (from config file or CLI overrides)\n\n" +
86
+ "Suggestion: Ensure ConfigManager.loadConfig() was called before ClientFactory.createFromConfig().");
87
+ logger.error("Failed to create client - no authentication", { prefix: "ClientFactory" });
88
+ throw error;
89
+ }
90
+ // Create adapter with version detection
91
+ // AdapterFactory uses internal caching, so repeated calls are fast
92
+ logger.debug("Creating database adapter", {
93
+ prefix: "ClientFactory",
94
+ apiMode: config.apiMode || "auto",
95
+ });
96
+ const { adapter } = await AdapterFactory.createFromConfig(config);
97
+ logger.debug("Client and adapter created successfully", {
98
+ prefix: "ClientFactory",
99
+ adapterType: adapter.getApiMode(),
100
+ });
101
+ return { client, adapter };
102
+ }
103
+ /**
104
+ * Create client and adapter from individual parameters.
105
+ *
106
+ * This is a lower-level method for cases where you don't have a full
107
+ * AppwriteConfig object. For most use cases, prefer createFromConfig().
108
+ *
109
+ * @param endpoint - Appwrite endpoint URL
110
+ * @param project - Appwrite project ID
111
+ * @param options - Authentication and API mode options
112
+ * @returns Object containing authenticated client and database adapter
113
+ * @throws Error if no authentication method is provided
114
+ *
115
+ * @example
116
+ * ```typescript
117
+ * const { client, adapter } = await ClientFactory.create(
118
+ * "https://cloud.appwrite.io/v1",
119
+ * "my-project-id",
120
+ * {
121
+ * apiKey: "my-api-key",
122
+ * apiMode: "auto"
123
+ * }
124
+ * );
125
+ * ```
126
+ */
127
+ static async create(endpoint, project, options = {}) {
128
+ logger.debug("Creating client from parameters", {
129
+ prefix: "ClientFactory",
130
+ hasSession: !!options.sessionCookie,
131
+ hasApiKey: !!options.apiKey,
132
+ endpoint,
133
+ project,
134
+ });
135
+ // Create minimal config object
136
+ const config = {
137
+ appwriteEndpoint: endpoint,
138
+ appwriteProject: project,
139
+ appwriteKey: options.apiKey || "",
140
+ sessionCookie: options.sessionCookie,
141
+ apiMode: options.apiMode || "auto",
142
+ // Minimal required fields
143
+ appwriteClient: null,
144
+ authMethod: options.sessionCookie ? "session" : "apikey",
145
+ enableBackups: false,
146
+ backupInterval: 0,
147
+ backupRetention: 0,
148
+ enableBackupCleanup: false,
149
+ enableMockData: false,
150
+ documentBucketId: "",
151
+ usersCollectionName: "",
152
+ databases: [],
153
+ buckets: [],
154
+ functions: [],
155
+ logging: {
156
+ enabled: false,
157
+ level: "info",
158
+ console: false,
159
+ },
160
+ };
161
+ // Use main createFromConfig method
162
+ return this.createFromConfig(config);
163
+ }
164
+ }
@@ -3,6 +3,7 @@ import { Client } from "node-appwrite";
3
3
  import { AdapterFactory } from "../adapters/AdapterFactory.js";
4
4
  import { findSessionByEndpointAndProject, hasSessionAuth, isValidSessionCookie } from "./sessionAuth.js";
5
5
  import { MessageFormatter } from "../shared/messageFormatter.js";
6
+ import { logger } from "../shared/logging.js";
6
7
  /**
7
8
  * Enhanced client creation from config with session authentication support
8
9
  * @deprecated Use getAdapterFromConfig for dual API support with session auth
@@ -28,7 +29,7 @@ export const getClientWithAuth = (endpoint, project, key, sessionCookie) => {
28
29
  if (sessionCookie) {
29
30
  if (isValidSessionCookie(sessionCookie)) {
30
31
  client.setSession(sessionCookie);
31
- MessageFormatter.info(`Using explicit session authentication for project ${project}`, { prefix: "Auth" });
32
+ logger.debug("Using explicit session authentication", { prefix: "Auth", project });
32
33
  return client;
33
34
  }
34
35
  else {
@@ -41,7 +42,7 @@ export const getClientWithAuth = (endpoint, project, key, sessionCookie) => {
41
42
  if (sessionAuth) {
42
43
  if (isValidSessionCookie(sessionAuth.sessionCookie)) {
43
44
  client.setSession(sessionAuth.sessionCookie);
44
- MessageFormatter.info(`Using session authentication for project ${project} (${sessionAuth.email || 'unknown user'})`, { prefix: "Auth" });
45
+ logger.debug("Using session authentication", { prefix: "Auth", project, email: sessionAuth.email || 'unknown user' });
45
46
  return client;
46
47
  }
47
48
  else {
@@ -56,7 +57,7 @@ export const getClientWithAuth = (endpoint, project, key, sessionCookie) => {
56
57
  }
57
58
  else {
58
59
  client.setKey(key);
59
- MessageFormatter.info(`Using API key authentication for project ${project}`, { prefix: "Auth" });
60
+ logger.debug("Using API key authentication", { prefix: "Auth", project });
60
61
  return client;
61
62
  }
62
63
  }
@@ -38,6 +38,7 @@ export declare const finalizeByAttributeMap: (appwriteFolderPath: string, collec
38
38
  export declare let numTimesFailedTotal: number;
39
39
  /**
40
40
  * Tries to execute the given createFunction and retries up to 5 times if it fails.
41
+ * Only retries on transient errors (network failures, 5xx errors). Does NOT retry validation errors (4xx).
41
42
  *
42
43
  * @param {() => Promise<any>} createFunction - The function to be executed.
43
44
  * @param {number} [attemptNum=0] - The number of attempts made so far (default: 0).