appwrite-utils-cli 1.5.1 → 1.6.0

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 (233) hide show
  1. package/CHANGELOG.md +199 -0
  2. package/README.md +251 -29
  3. package/dist/adapters/AdapterFactory.d.ts +10 -3
  4. package/dist/adapters/AdapterFactory.js +213 -17
  5. package/dist/adapters/TablesDBAdapter.js +60 -17
  6. package/dist/backups/operations/bucketBackup.d.ts +19 -0
  7. package/dist/backups/operations/bucketBackup.js +197 -0
  8. package/dist/backups/operations/collectionBackup.d.ts +30 -0
  9. package/dist/backups/operations/collectionBackup.js +201 -0
  10. package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
  11. package/dist/backups/operations/comprehensiveBackup.js +238 -0
  12. package/dist/backups/schemas/bucketManifest.d.ts +93 -0
  13. package/dist/backups/schemas/bucketManifest.js +33 -0
  14. package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
  15. package/dist/backups/schemas/comprehensiveManifest.js +32 -0
  16. package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
  17. package/dist/backups/tracking/centralizedTracking.js +274 -0
  18. package/dist/cli/commands/configCommands.d.ts +8 -0
  19. package/dist/cli/commands/configCommands.js +160 -0
  20. package/dist/cli/commands/databaseCommands.d.ts +13 -0
  21. package/dist/cli/commands/databaseCommands.js +478 -0
  22. package/dist/cli/commands/functionCommands.d.ts +7 -0
  23. package/dist/cli/commands/functionCommands.js +289 -0
  24. package/dist/cli/commands/schemaCommands.d.ts +7 -0
  25. package/dist/cli/commands/schemaCommands.js +134 -0
  26. package/dist/cli/commands/transferCommands.d.ts +5 -0
  27. package/dist/cli/commands/transferCommands.js +384 -0
  28. package/dist/collections/attributes.d.ts +5 -4
  29. package/dist/collections/attributes.js +539 -246
  30. package/dist/collections/indexes.js +39 -37
  31. package/dist/collections/methods.d.ts +2 -16
  32. package/dist/collections/methods.js +90 -538
  33. package/dist/collections/transferOperations.d.ts +7 -0
  34. package/dist/collections/transferOperations.js +331 -0
  35. package/dist/collections/wipeOperations.d.ts +16 -0
  36. package/dist/collections/wipeOperations.js +328 -0
  37. package/dist/config/configMigration.d.ts +87 -0
  38. package/dist/config/configMigration.js +390 -0
  39. package/dist/config/configValidation.d.ts +66 -0
  40. package/dist/config/configValidation.js +358 -0
  41. package/dist/config/yamlConfig.d.ts +455 -1
  42. package/dist/config/yamlConfig.js +145 -52
  43. package/dist/databases/methods.js +3 -2
  44. package/dist/databases/setup.d.ts +1 -2
  45. package/dist/databases/setup.js +9 -87
  46. package/dist/examples/yamlTerminologyExample.d.ts +42 -0
  47. package/dist/examples/yamlTerminologyExample.js +269 -0
  48. package/dist/functions/deployments.js +11 -10
  49. package/dist/functions/methods.d.ts +1 -1
  50. package/dist/functions/methods.js +5 -4
  51. package/dist/init.js +9 -9
  52. package/dist/interactiveCLI.d.ts +8 -17
  53. package/dist/interactiveCLI.js +186 -1171
  54. package/dist/main.js +364 -21
  55. package/dist/migrations/afterImportActions.js +22 -30
  56. package/dist/migrations/appwriteToX.js +71 -25
  57. package/dist/migrations/dataLoader.js +35 -26
  58. package/dist/migrations/importController.js +29 -30
  59. package/dist/migrations/relationships.js +13 -12
  60. package/dist/migrations/services/ImportOrchestrator.js +16 -19
  61. package/dist/migrations/transfer.js +46 -46
  62. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +3 -1
  63. package/dist/migrations/yaml/YamlImportConfigLoader.js +6 -3
  64. package/dist/migrations/yaml/YamlImportIntegration.d.ts +9 -3
  65. package/dist/migrations/yaml/YamlImportIntegration.js +22 -11
  66. package/dist/migrations/yaml/generateImportSchemas.d.ts +14 -1
  67. package/dist/migrations/yaml/generateImportSchemas.js +736 -7
  68. package/dist/schemas/authUser.d.ts +1 -1
  69. package/dist/setupController.js +3 -2
  70. package/dist/shared/backupMetadataSchema.d.ts +94 -0
  71. package/dist/shared/backupMetadataSchema.js +38 -0
  72. package/dist/shared/backupTracking.d.ts +18 -0
  73. package/dist/shared/backupTracking.js +176 -0
  74. package/dist/shared/confirmationDialogs.js +15 -15
  75. package/dist/shared/errorUtils.d.ts +54 -0
  76. package/dist/shared/errorUtils.js +95 -0
  77. package/dist/shared/functionManager.js +20 -19
  78. package/dist/shared/indexManager.js +12 -11
  79. package/dist/shared/jsonSchemaGenerator.js +10 -26
  80. package/dist/shared/logging.d.ts +51 -0
  81. package/dist/shared/logging.js +70 -0
  82. package/dist/shared/messageFormatter.d.ts +2 -0
  83. package/dist/shared/messageFormatter.js +10 -0
  84. package/dist/shared/migrationHelpers.d.ts +6 -16
  85. package/dist/shared/migrationHelpers.js +24 -21
  86. package/dist/shared/operationLogger.d.ts +8 -1
  87. package/dist/shared/operationLogger.js +11 -24
  88. package/dist/shared/operationQueue.d.ts +28 -1
  89. package/dist/shared/operationQueue.js +268 -66
  90. package/dist/shared/operationsTable.d.ts +26 -0
  91. package/dist/shared/operationsTable.js +286 -0
  92. package/dist/shared/operationsTableSchema.d.ts +48 -0
  93. package/dist/shared/operationsTableSchema.js +35 -0
  94. package/dist/shared/relationshipExtractor.d.ts +56 -0
  95. package/dist/shared/relationshipExtractor.js +138 -0
  96. package/dist/shared/schemaGenerator.d.ts +19 -1
  97. package/dist/shared/schemaGenerator.js +56 -75
  98. package/dist/storage/backupCompression.d.ts +20 -0
  99. package/dist/storage/backupCompression.js +67 -0
  100. package/dist/storage/methods.d.ts +16 -2
  101. package/dist/storage/methods.js +98 -14
  102. package/dist/users/methods.js +9 -8
  103. package/dist/utils/configDiscovery.d.ts +78 -0
  104. package/dist/utils/configDiscovery.js +430 -0
  105. package/dist/utils/directoryUtils.d.ts +22 -0
  106. package/dist/utils/directoryUtils.js +59 -0
  107. package/dist/utils/getClientFromConfig.d.ts +17 -8
  108. package/dist/utils/getClientFromConfig.js +162 -17
  109. package/dist/utils/helperFunctions.d.ts +16 -2
  110. package/dist/utils/helperFunctions.js +19 -5
  111. package/dist/utils/loadConfigs.d.ts +34 -9
  112. package/dist/utils/loadConfigs.js +236 -316
  113. package/dist/utils/pathResolvers.d.ts +53 -0
  114. package/dist/utils/pathResolvers.js +72 -0
  115. package/dist/utils/projectConfig.d.ts +119 -0
  116. package/dist/utils/projectConfig.js +171 -0
  117. package/dist/utils/retryFailedPromises.js +4 -2
  118. package/dist/utils/sessionAuth.d.ts +48 -0
  119. package/dist/utils/sessionAuth.js +164 -0
  120. package/dist/utils/sessionPreservationExample.d.ts +1666 -0
  121. package/dist/utils/sessionPreservationExample.js +101 -0
  122. package/dist/utils/setupFiles.js +301 -41
  123. package/dist/utils/typeGuards.d.ts +35 -0
  124. package/dist/utils/typeGuards.js +57 -0
  125. package/dist/utils/versionDetection.js +145 -9
  126. package/dist/utils/yamlConverter.d.ts +53 -3
  127. package/dist/utils/yamlConverter.js +232 -13
  128. package/dist/utils/yamlLoader.d.ts +70 -0
  129. package/dist/utils/yamlLoader.js +263 -0
  130. package/dist/utilsController.d.ts +36 -3
  131. package/dist/utilsController.js +186 -56
  132. package/package.json +12 -2
  133. package/src/adapters/AdapterFactory.ts +263 -35
  134. package/src/adapters/TablesDBAdapter.ts +225 -36
  135. package/src/backups/operations/bucketBackup.ts +277 -0
  136. package/src/backups/operations/collectionBackup.ts +310 -0
  137. package/src/backups/operations/comprehensiveBackup.ts +342 -0
  138. package/src/backups/schemas/bucketManifest.ts +78 -0
  139. package/src/backups/schemas/comprehensiveManifest.ts +76 -0
  140. package/src/backups/tracking/centralizedTracking.ts +352 -0
  141. package/src/cli/commands/configCommands.ts +194 -0
  142. package/src/cli/commands/databaseCommands.ts +635 -0
  143. package/src/cli/commands/functionCommands.ts +379 -0
  144. package/src/cli/commands/schemaCommands.ts +163 -0
  145. package/src/cli/commands/transferCommands.ts +457 -0
  146. package/src/collections/attributes.ts +900 -621
  147. package/src/collections/attributes.ts.backup +1555 -0
  148. package/src/collections/indexes.ts +116 -114
  149. package/src/collections/methods.ts +295 -968
  150. package/src/collections/transferOperations.ts +516 -0
  151. package/src/collections/wipeOperations.ts +501 -0
  152. package/src/config/README.md +274 -0
  153. package/src/config/configMigration.ts +575 -0
  154. package/src/config/configValidation.ts +445 -0
  155. package/src/config/yamlConfig.ts +168 -55
  156. package/src/databases/methods.ts +3 -2
  157. package/src/databases/setup.ts +11 -138
  158. package/src/examples/yamlTerminologyExample.ts +341 -0
  159. package/src/functions/deployments.ts +14 -12
  160. package/src/functions/methods.ts +11 -11
  161. package/src/functions/templates/hono-typescript/README.md +286 -0
  162. package/src/functions/templates/hono-typescript/package.json +26 -0
  163. package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  164. package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  165. package/src/functions/templates/hono-typescript/src/app.ts +180 -0
  166. package/src/functions/templates/hono-typescript/src/context.ts +103 -0
  167. package/src/functions/templates/hono-typescript/src/index.ts +54 -0
  168. package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  169. package/src/functions/templates/hono-typescript/tsconfig.json +20 -0
  170. package/src/functions/templates/typescript-node/package.json +2 -1
  171. package/src/functions/templates/typescript-node/src/context.ts +103 -0
  172. package/src/functions/templates/typescript-node/src/index.ts +18 -12
  173. package/src/functions/templates/uv/pyproject.toml +1 -0
  174. package/src/functions/templates/uv/src/context.py +125 -0
  175. package/src/functions/templates/uv/src/index.py +35 -5
  176. package/src/init.ts +9 -11
  177. package/src/interactiveCLI.ts +276 -1591
  178. package/src/main.ts +418 -24
  179. package/src/migrations/afterImportActions.ts +71 -44
  180. package/src/migrations/appwriteToX.ts +100 -34
  181. package/src/migrations/dataLoader.ts +48 -34
  182. package/src/migrations/importController.ts +44 -39
  183. package/src/migrations/relationships.ts +28 -18
  184. package/src/migrations/services/ImportOrchestrator.ts +24 -27
  185. package/src/migrations/transfer.ts +159 -121
  186. package/src/migrations/yaml/YamlImportConfigLoader.ts +11 -4
  187. package/src/migrations/yaml/YamlImportIntegration.ts +47 -20
  188. package/src/migrations/yaml/generateImportSchemas.ts +751 -12
  189. package/src/setupController.ts +3 -2
  190. package/src/shared/backupMetadataSchema.ts +93 -0
  191. package/src/shared/backupTracking.ts +211 -0
  192. package/src/shared/confirmationDialogs.ts +19 -19
  193. package/src/shared/errorUtils.ts +110 -0
  194. package/src/shared/functionManager.ts +21 -20
  195. package/src/shared/indexManager.ts +12 -11
  196. package/src/shared/jsonSchemaGenerator.ts +38 -52
  197. package/src/shared/logging.ts +75 -0
  198. package/src/shared/messageFormatter.ts +14 -1
  199. package/src/shared/migrationHelpers.ts +45 -38
  200. package/src/shared/operationLogger.ts +11 -36
  201. package/src/shared/operationQueue.ts +322 -93
  202. package/src/shared/operationsTable.ts +338 -0
  203. package/src/shared/operationsTableSchema.ts +60 -0
  204. package/src/shared/relationshipExtractor.ts +214 -0
  205. package/src/shared/schemaGenerator.ts +179 -219
  206. package/src/storage/backupCompression.ts +88 -0
  207. package/src/storage/methods.ts +131 -34
  208. package/src/users/methods.ts +11 -9
  209. package/src/utils/configDiscovery.ts +502 -0
  210. package/src/utils/directoryUtils.ts +61 -0
  211. package/src/utils/getClientFromConfig.ts +205 -22
  212. package/src/utils/helperFunctions.ts +23 -5
  213. package/src/utils/loadConfigs.ts +313 -345
  214. package/src/utils/pathResolvers.ts +81 -0
  215. package/src/utils/projectConfig.ts +299 -0
  216. package/src/utils/retryFailedPromises.ts +4 -2
  217. package/src/utils/sessionAuth.ts +230 -0
  218. package/src/utils/setupFiles.ts +322 -54
  219. package/src/utils/typeGuards.ts +65 -0
  220. package/src/utils/versionDetection.ts +218 -64
  221. package/src/utils/yamlConverter.ts +296 -13
  222. package/src/utils/yamlLoader.ts +364 -0
  223. package/src/utilsController.ts +314 -110
  224. package/tests/README.md +497 -0
  225. package/tests/adapters/AdapterFactory.test.ts +277 -0
  226. package/tests/integration/syncOperations.test.ts +463 -0
  227. package/tests/jest.config.js +25 -0
  228. package/tests/migration/configMigration.test.ts +546 -0
  229. package/tests/setup.ts +62 -0
  230. package/tests/testUtils.ts +340 -0
  231. package/tests/utils/loadConfigs.test.ts +350 -0
  232. package/tests/validation/configValidation.test.ts +412 -0
  233. package/src/utils/schemaStrings.ts +0 -517
@@ -4,108 +4,47 @@ import {} from "appwrite-utils";
4
4
  import { register } from "tsx/esm/api"; // Import the register function
5
5
  import { pathToFileURL } from "node:url";
6
6
  import chalk from "chalk";
7
- import { findYamlConfig, loadYamlConfig } from "../config/yamlConfig.js";
7
+ import { findYamlConfig, loadYamlConfig, loadYamlConfigWithSession, extractSessionOptionsFromConfig } from "../config/yamlConfig.js";
8
8
  import { detectAppwriteVersionCached, fetchServerVersion, isVersionAtLeast } from "./versionDetection.js";
9
- import yaml from "js-yaml";
10
- import { z } from "zod";
11
9
  import { MessageFormatter } from "../shared/messageFormatter.js";
10
+ import { validateCollectionsTablesConfig, reportValidationResults } from "../config/configValidation.js";
11
+ import { resolveCollectionsDir, resolveTablesDir } from "./pathResolvers.js";
12
+ import { findAppwriteConfig, findAppwriteConfigTS, findFunctionsDir, discoverCollections, discoverTables, discoverLegacyDirectory } from "./configDiscovery.js";
12
13
  /**
13
- * Recursively searches for configuration files starting from the given directory.
14
- * Priority: 1) YAML configs in .appwrite directories, 2) appwriteConfig.ts files in subdirectories
15
- * @param dir The directory to start the search from.
16
- * @returns The directory path where the config was found, suitable for passing to loadConfig().
14
+ * Helper function to create session preservation options from session data
15
+ * @param sessionCookie The session cookie string
16
+ * @param email Optional email associated with the session
17
+ * @param expiresAt Optional expiration timestamp
18
+ * @returns SessionPreservationOptions object
17
19
  */
18
- export const findAppwriteConfig = (dir) => {
19
- // First try to find YAML config (already searches recursively for .appwrite dirs)
20
- const yamlConfig = findYamlConfig(dir);
21
- if (yamlConfig) {
22
- // Return the directory containing the config file
23
- return path.dirname(yamlConfig);
24
- }
25
- // Fall back to TypeScript config search
26
- const tsConfigPath = findAppwriteConfigTS(dir);
27
- if (tsConfigPath) {
28
- return path.dirname(tsConfigPath);
29
- }
30
- return null;
31
- };
32
- const shouldIgnoreDirectory = (dirName) => {
33
- const ignoredDirs = [
34
- 'node_modules',
35
- 'dist',
36
- 'build',
37
- 'coverage',
38
- '.next',
39
- '.nuxt',
40
- '.cache',
41
- '.git',
42
- '.svn',
43
- '.hg',
44
- '__pycache__',
45
- '.pytest_cache',
46
- '.mypy_cache',
47
- 'venv',
48
- '.venv',
49
- 'env',
50
- '.env',
51
- 'target',
52
- 'out',
53
- 'bin',
54
- 'obj',
55
- '.vs',
56
- '.vscode',
57
- '.idea',
58
- 'temp',
59
- 'tmp',
60
- '.tmp',
61
- 'logs',
62
- 'log',
63
- '.DS_Store',
64
- 'Thumbs.db'
65
- ];
66
- return ignoredDirs.includes(dirName) ||
67
- dirName.startsWith('.git') ||
68
- dirName.startsWith('node_modules') ||
69
- (dirName.startsWith('.') && dirName !== '.appwrite');
70
- };
71
- const findAppwriteConfigTS = (dir, depth = 0) => {
72
- // Limit search depth to prevent infinite recursion
73
- if (depth > 10) {
74
- return null;
75
- }
76
- if (shouldIgnoreDirectory(path.basename(dir))) {
77
- return null;
78
- }
79
- try {
80
- const entries = fs.readdirSync(dir, { withFileTypes: true });
81
- // First check current directory for appwriteConfig.ts
82
- for (const entry of entries) {
83
- if (entry.isFile() && entry.name === "appwriteConfig.ts") {
84
- return path.join(dir, entry.name);
85
- }
20
+ export function createSessionPreservation(sessionCookie, email, expiresAt) {
21
+ return {
22
+ sessionCookie,
23
+ authMethod: "session",
24
+ sessionMetadata: {
25
+ ...(email && { email }),
26
+ ...(expiresAt && { expiresAt })
86
27
  }
87
- // Then search subdirectories
88
- for (const entry of entries) {
89
- if (entry.isDirectory() && !shouldIgnoreDirectory(entry.name)) {
90
- const result = findAppwriteConfigTS(path.join(dir, entry.name), depth + 1);
91
- if (result)
92
- return result;
93
- }
94
- }
95
- }
96
- catch (error) {
97
- // Ignore directory access errors
98
- }
99
- return null;
100
- };
28
+ };
29
+ }
30
+ // Re-export config discovery functions for backward compatibility
31
+ export { findAppwriteConfig, findFunctionsDir } from "./configDiscovery.js";
101
32
  /**
102
33
  * Loads the Appwrite configuration and returns both config and the path where it was found.
103
34
  * @param configDir The directory to search for config files.
104
- * @returns Object containing the config and the actual path where it was found.
35
+ * @param options Loading options including validation settings and session preservation.
36
+ * @returns Object containing the config, path, and validation results.
105
37
  */
106
- export const loadConfigWithPath = async (configDir) => {
38
+ export const loadConfigWithPath = async (configDir, options = {}) => {
39
+ const { validate = true, strictMode = false, reportValidation = true } = options;
107
40
  let config = null;
108
41
  let actualConfigPath = null;
42
+ // Convert session preservation options to YAML format
43
+ const yamlSessionOptions = options.preserveAuth ? {
44
+ sessionCookie: options.preserveAuth.sessionCookie,
45
+ authMethod: options.preserveAuth.authMethod,
46
+ sessionMetadata: options.preserveAuth.sessionMetadata,
47
+ } : undefined;
109
48
  // Check if we're given the .appwrite directory directly
110
49
  if (configDir.endsWith('.appwrite')) {
111
50
  // Look for config files directly in this directory
@@ -113,7 +52,9 @@ export const loadConfigWithPath = async (configDir) => {
113
52
  for (const fileName of possibleYamlFiles) {
114
53
  const yamlPath = path.join(configDir, fileName);
115
54
  if (fs.existsSync(yamlPath)) {
116
- config = await loadYamlConfig(yamlPath);
55
+ config = yamlSessionOptions
56
+ ? await loadYamlConfigWithSession(yamlPath, yamlSessionOptions)
57
+ : await loadYamlConfig(yamlPath);
117
58
  actualConfigPath = yamlPath;
118
59
  break;
119
60
  }
@@ -123,7 +64,9 @@ export const loadConfigWithPath = async (configDir) => {
123
64
  // Original logic: search for .appwrite directories
124
65
  const yamlConfigPath = findYamlConfig(configDir);
125
66
  if (yamlConfigPath) {
126
- config = await loadYamlConfig(yamlConfigPath);
67
+ config = yamlSessionOptions
68
+ ? await loadYamlConfigWithSession(yamlConfigPath, yamlSessionOptions)
69
+ : await loadYamlConfig(yamlConfigPath);
127
70
  actualConfigPath = yamlConfigPath;
128
71
  }
129
72
  }
@@ -150,84 +93,136 @@ export const loadConfigWithPath = async (configDir) => {
150
93
  if (!config || !actualConfigPath) {
151
94
  throw new Error("No valid configuration found");
152
95
  }
153
- // Determine directory (collections or tables) based on server version / API mode
154
- let dirName = "collections";
155
- try {
156
- const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
157
- if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
158
- dirName = 'tables';
96
+ // Preserve session authentication if provided
97
+ // This allows maintaining session context when config is reloaded during CLI operations
98
+ if (options.preserveAuth) {
99
+ const { sessionCookie, authMethod, sessionMetadata } = options.preserveAuth;
100
+ // Inject session cookie into the loaded config
101
+ if (sessionCookie) {
102
+ config.sessionCookie = sessionCookie;
159
103
  }
160
- else {
161
- // Try health version if not provided
162
- const ver = await fetchServerVersion(config.appwriteEndpoint);
163
- if (isVersionAtLeast(ver || undefined, '1.8.0'))
164
- dirName = 'tables';
104
+ // Set or override authentication method preference
105
+ if (authMethod) {
106
+ config.authMethod = authMethod;
107
+ }
108
+ // Merge session metadata (email, expiration, etc.) with existing metadata
109
+ if (sessionMetadata) {
110
+ config.sessionMetadata = {
111
+ ...config.sessionMetadata,
112
+ ...sessionMetadata
113
+ };
114
+ }
115
+ // Auto-detect authentication method if not explicitly provided
116
+ // If we have a session cookie but no auth method specified, prefer session auth
117
+ if (!authMethod && sessionCookie) {
118
+ config.authMethod = "session";
165
119
  }
166
120
  }
167
- catch { }
168
- // Determine collections directory based on actual config file location and dirName
169
- let collectionsDir;
121
+ // Enhanced dual folder support: Load from BOTH collections/ AND tables/ directories
170
122
  const configFileDir = path.dirname(actualConfigPath);
171
- collectionsDir = path.join(configFileDir, dirName);
172
- // Fallback if not found
173
- if (!fs.existsSync(collectionsDir)) {
174
- const fallback = path.join(configFileDir, dirName === 'tables' ? 'collections' : 'tables');
175
- if (fs.existsSync(fallback))
176
- collectionsDir = fallback;
123
+ // Look for collections/tables directories in the same directory as the config file
124
+ const collectionsDir = resolveCollectionsDir(configFileDir);
125
+ const tablesDir = resolveTablesDir(configFileDir);
126
+ // Initialize collections array
127
+ config.collections = [];
128
+ // Load from collections/ directory first (higher priority)
129
+ const collectionsResult = await discoverCollections(collectionsDir);
130
+ config.collections.push(...collectionsResult.collections);
131
+ // Load from tables/ directory second (lower priority, check for conflicts)
132
+ const tablesResult = await discoverTables(tablesDir, collectionsResult.loadedNames);
133
+ config.collections.push(...tablesResult.tables);
134
+ // Combine conflicts from both discovery operations
135
+ const allConflicts = [...collectionsResult.conflicts, ...tablesResult.conflicts];
136
+ // Report conflicts if any
137
+ if (allConflicts.length > 0) {
138
+ MessageFormatter.warning(`Found ${allConflicts.length} naming conflicts between collections/ and tables/`, { prefix: "Config" });
139
+ allConflicts.forEach(conflict => {
140
+ MessageFormatter.info(` - '${conflict.name}': ${conflict.source1} (used) vs ${conflict.source2} (skipped)`, { prefix: "Config" });
141
+ });
177
142
  }
178
- // Load collections if they exist
179
- if (fs.existsSync(collectionsDir)) {
180
- const unregister = register(); // Register tsx for collections
143
+ // Fallback: If neither directory exists, try legacy single-directory detection
144
+ if (!fs.existsSync(collectionsDir) && !fs.existsSync(tablesDir)) {
145
+ // Determine directory (collections or tables) based on server version / API mode
146
+ let dirName = "collections";
181
147
  try {
182
- const collectionFiles = fs.readdirSync(collectionsDir);
183
- config.collections = [];
184
- for (const file of collectionFiles) {
185
- if (file === "index.ts") {
186
- continue;
187
- }
188
- const filePath = path.join(collectionsDir, file);
189
- // Handle YAML collections
190
- if (file.endsWith('.yaml') || file.endsWith('.yml')) {
191
- const collection = loadYamlCollection(filePath);
192
- if (collection) {
193
- config.collections.push(collection);
194
- }
195
- continue;
196
- }
197
- // Handle TypeScript collections
198
- if (file.endsWith('.ts')) {
199
- const fileUrl = pathToFileURL(filePath).href;
200
- const collectionModule = (await import(fileUrl));
201
- const collection = collectionModule.default?.default || collectionModule.default || collectionModule;
202
- if (collection) {
203
- // Ensure importDefs are properly loaded
204
- if (collectionModule.importDefs || collection.importDefs) {
205
- collection.importDefs = collectionModule.importDefs || collection.importDefs;
206
- }
207
- config.collections.push(collection);
208
- }
209
- }
148
+ const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
149
+ if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
150
+ dirName = 'tables';
151
+ }
152
+ else {
153
+ // Try health version if not provided
154
+ const ver = await fetchServerVersion(config.appwriteEndpoint);
155
+ if (isVersionAtLeast(ver || undefined, '1.8.0'))
156
+ dirName = 'tables';
210
157
  }
211
158
  }
212
- finally {
213
- unregister(); // Unregister tsx when done
159
+ catch { }
160
+ const legacyItems = await discoverLegacyDirectory(configFileDir, dirName);
161
+ config.collections.push(...legacyItems);
162
+ }
163
+ // Ensure array exists even if empty
164
+ config.collections = config.collections || [];
165
+ // Log the final result
166
+ const allCollections = config.collections || [];
167
+ const fromCollectionsDir = allCollections.filter((c) => !c._isFromTablesDir).length;
168
+ const fromTablesDir = allCollections.filter((c) => c._isFromTablesDir).length;
169
+ const totalLoaded = allCollections.length;
170
+ if (totalLoaded > 0) {
171
+ if (fromTablesDir > 0) {
172
+ MessageFormatter.success(`Successfully loaded ${totalLoaded} items total: ${fromCollectionsDir} from collections/ and ${fromTablesDir} from tables/`, { prefix: "Config" });
173
+ }
174
+ else {
175
+ MessageFormatter.success(`Successfully loaded ${totalLoaded} collections from collections/`, { prefix: "Config" });
214
176
  }
215
177
  }
216
- return { config, actualConfigPath };
178
+ // Validate configuration if requested
179
+ let validation;
180
+ if (validate) {
181
+ validation = validateCollectionsTablesConfig(config);
182
+ // In strict mode, treat warnings as errors
183
+ if (strictMode && validation.warnings.length > 0) {
184
+ const strictValidation = {
185
+ ...validation,
186
+ isValid: false,
187
+ errors: [...validation.errors, ...validation.warnings.map(w => ({ ...w, severity: "error" }))],
188
+ warnings: []
189
+ };
190
+ validation = strictValidation;
191
+ }
192
+ // Report validation results if requested
193
+ if (reportValidation) {
194
+ reportValidationResults(validation, { verbose: true });
195
+ }
196
+ // Throw error if validation fails in strict mode
197
+ if (strictMode && !validation.isValid) {
198
+ throw new Error(`Configuration validation failed in strict mode. Found ${validation.errors.length} validation errors.`);
199
+ }
200
+ }
201
+ return { config, actualConfigPath, validation };
217
202
  };
218
203
  /**
219
204
  * Loads the Appwrite configuration and all collection configurations from a specified directory.
220
205
  * Supports both YAML and TypeScript config formats with backward compatibility.
221
206
  * @param configDir The directory containing the config file and collections folder.
207
+ * @param options Loading options including validation settings and session preservation.
222
208
  * @returns The loaded Appwrite configuration including collections.
223
209
  */
224
- export const loadConfig = async (configDir) => {
210
+ export const loadConfig = async (configDir, options = {}) => {
211
+ const { validate = false, strictMode = false, reportValidation = false } = options;
225
212
  let config = null;
226
213
  let actualConfigPath = null;
214
+ // Convert session preservation options to YAML format
215
+ const yamlSessionOptions = options.preserveAuth ? {
216
+ sessionCookie: options.preserveAuth.sessionCookie,
217
+ authMethod: options.preserveAuth.authMethod,
218
+ sessionMetadata: options.preserveAuth.sessionMetadata,
219
+ } : undefined;
227
220
  // First try to find and load YAML config
228
221
  const yamlConfigPath = findYamlConfig(configDir);
229
222
  if (yamlConfigPath) {
230
- config = await loadYamlConfig(yamlConfigPath);
223
+ config = yamlSessionOptions
224
+ ? await loadYamlConfigWithSession(yamlConfigPath, yamlSessionOptions)
225
+ : await loadYamlConfig(yamlConfigPath);
231
226
  actualConfigPath = yamlConfigPath;
232
227
  }
233
228
  // Fall back to TypeScript config if YAML not found or failed to load
@@ -253,186 +248,111 @@ export const loadConfig = async (configDir) => {
253
248
  if (!config) {
254
249
  throw new Error("No valid configuration found");
255
250
  }
256
- // Determine directory (collections or tables) based on server version / API mode
257
- let dirName2 = "collections";
258
- try {
259
- const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
260
- if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
261
- dirName2 = 'tables';
251
+ // Preserve session authentication if provided
252
+ // This allows maintaining session context when config is reloaded during CLI operations
253
+ if (options.preserveAuth) {
254
+ const { sessionCookie, authMethod, sessionMetadata } = options.preserveAuth;
255
+ // Inject session cookie into the loaded config
256
+ if (sessionCookie) {
257
+ config.sessionCookie = sessionCookie;
262
258
  }
263
- else {
264
- const ver = await fetchServerVersion(config.appwriteEndpoint);
265
- if (isVersionAtLeast(ver || undefined, '1.8.0'))
266
- dirName2 = 'tables';
259
+ // Set or override authentication method preference
260
+ if (authMethod) {
261
+ config.authMethod = authMethod;
262
+ }
263
+ // Merge session metadata (email, expiration, etc.) with existing metadata
264
+ if (sessionMetadata) {
265
+ config.sessionMetadata = {
266
+ ...config.sessionMetadata,
267
+ ...sessionMetadata
268
+ };
269
+ }
270
+ // Auto-detect authentication method if not explicitly provided
271
+ // If we have a session cookie but no auth method specified, prefer session auth
272
+ if (!authMethod && sessionCookie) {
273
+ config.authMethod = "session";
267
274
  }
268
275
  }
269
- catch { }
270
- let collectionsDir;
271
- if (actualConfigPath) {
272
- const configFileDir = path.dirname(actualConfigPath);
273
- collectionsDir = path.join(configFileDir, dirName2);
274
- }
275
- else {
276
- collectionsDir = path.join(configDir, dirName2);
277
- }
278
- if (!fs.existsSync(collectionsDir)) {
279
- const fallback = path.join(path.dirname(actualConfigPath || configDir), dirName2 === 'tables' ? 'collections' : 'tables');
280
- if (fs.existsSync(fallback))
281
- collectionsDir = fallback;
276
+ // Enhanced dual folder support: Load from BOTH collections/ AND tables/ directories
277
+ const configFileDir = actualConfigPath ? path.dirname(actualConfigPath) : configDir;
278
+ // Look for collections/tables directories in the same directory as the config file
279
+ const collectionsDir = resolveCollectionsDir(configFileDir);
280
+ const tablesDir = resolveTablesDir(configFileDir);
281
+ // Initialize collections array
282
+ config.collections = [];
283
+ // Load from collections/ directory first (higher priority)
284
+ const collectionsResult = await discoverCollections(collectionsDir);
285
+ config.collections.push(...collectionsResult.collections);
286
+ // Load from tables/ directory second (lower priority, check for conflicts)
287
+ const tablesResult = await discoverTables(tablesDir, collectionsResult.loadedNames);
288
+ config.collections.push(...tablesResult.tables);
289
+ // Combine conflicts from both discovery operations
290
+ const allConflicts = [...collectionsResult.conflicts, ...tablesResult.conflicts];
291
+ // Report conflicts if any
292
+ if (allConflicts.length > 0) {
293
+ MessageFormatter.warning(`Found ${allConflicts.length} naming conflicts between collections/ and tables/`, { prefix: "Config" });
294
+ allConflicts.forEach(conflict => {
295
+ MessageFormatter.info(` - '${conflict.name}': ${conflict.source1} (used) vs ${conflict.source2} (skipped)`, { prefix: "Config" });
296
+ });
282
297
  }
283
- // Load collections if they exist
284
- if (fs.existsSync(collectionsDir)) {
285
- const unregister = register(); // Register tsx for collections
298
+ // Fallback: If neither directory exists, try legacy single-directory detection
299
+ if (!fs.existsSync(collectionsDir) && !fs.existsSync(tablesDir)) {
300
+ // Determine directory (collections or tables) based on server version / API mode
301
+ let dirName = "collections";
286
302
  try {
287
- const collectionFiles = fs.readdirSync(collectionsDir);
288
- config.collections = [];
289
- for (const file of collectionFiles) {
290
- if (file === "index.ts") {
291
- continue;
292
- }
293
- const filePath = path.join(collectionsDir, file);
294
- // Handle YAML collections
295
- if (file.endsWith('.yaml') || file.endsWith('.yml')) {
296
- const collection = loadYamlCollection(filePath);
297
- if (collection) {
298
- config.collections.push(collection);
299
- }
300
- continue;
301
- }
302
- // Handle TypeScript collections
303
- if (file.endsWith('.ts')) {
304
- const fileUrl = pathToFileURL(filePath).href;
305
- const collectionModule = (await import(fileUrl));
306
- const collection = collectionModule.default?.default || collectionModule.default || collectionModule;
307
- if (collection) {
308
- // Ensure importDefs are properly loaded
309
- if (collectionModule.importDefs || collection.importDefs) {
310
- collection.importDefs = collectionModule.importDefs || collection.importDefs;
311
- }
312
- config.collections.push(collection);
313
- }
314
- }
303
+ const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
304
+ if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
305
+ dirName = 'tables';
306
+ }
307
+ else {
308
+ const ver = await fetchServerVersion(config.appwriteEndpoint);
309
+ if (isVersionAtLeast(ver || undefined, '1.8.0'))
310
+ dirName = 'tables';
315
311
  }
316
312
  }
317
- finally {
318
- unregister(); // Unregister tsx when done
319
- }
313
+ catch { }
314
+ const legacyItems = await discoverLegacyDirectory(configFileDir, dirName);
315
+ config.collections.push(...legacyItems);
320
316
  }
321
- else {
322
- config.collections = config.collections || [];
317
+ // Ensure array exists even if empty
318
+ config.collections = config.collections || [];
319
+ // Log the final result
320
+ const allCollections = config.collections || [];
321
+ const fromCollectionsDir = allCollections.filter((c) => !c._isFromTablesDir).length;
322
+ const fromTablesDir = allCollections.filter((c) => c._isFromTablesDir).length;
323
+ const totalLoaded = allCollections.length;
324
+ if (totalLoaded > 0) {
325
+ if (fromTablesDir > 0) {
326
+ MessageFormatter.success(`Successfully loaded ${totalLoaded} items total: ${fromCollectionsDir} from collections/ and ${fromTablesDir} from tables/`, { prefix: "Config" });
327
+ }
328
+ else {
329
+ MessageFormatter.success(`Successfully loaded ${totalLoaded} collections from collections/`, { prefix: "Config" });
330
+ }
323
331
  }
324
332
  // Log successful config loading
325
333
  if (actualConfigPath) {
326
334
  MessageFormatter.success(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" });
327
335
  }
328
- return config;
329
- };
330
- export const findFunctionsDir = (dir, depth = 0) => {
331
- // Limit search depth to prevent infinite recursion
332
- if (depth > 5) {
333
- return null;
334
- }
335
- if (shouldIgnoreDirectory(path.basename(dir))) {
336
- return null;
337
- }
338
- try {
339
- const files = fs.readdirSync(dir, { withFileTypes: true });
340
- for (const entry of files) {
341
- if (!entry.isDirectory() || shouldIgnoreDirectory(entry.name)) {
342
- continue;
343
- }
344
- if (entry.name === "functions") {
345
- return path.join(dir, entry.name);
346
- }
347
- const result = findFunctionsDir(path.join(dir, entry.name), depth + 1);
348
- if (result)
349
- return result;
336
+ // Validate configuration if requested
337
+ if (validate) {
338
+ let validation = validateCollectionsTablesConfig(config);
339
+ // In strict mode, treat warnings as errors
340
+ if (strictMode && validation.warnings.length > 0) {
341
+ validation = {
342
+ ...validation,
343
+ isValid: false,
344
+ errors: [...validation.errors, ...validation.warnings.map(w => ({ ...w, severity: "error" }))],
345
+ warnings: []
346
+ };
347
+ }
348
+ // Report validation results if requested
349
+ if (reportValidation) {
350
+ reportValidationResults(validation, { verbose: true });
351
+ }
352
+ // Throw error if validation fails in strict mode
353
+ if (strictMode && !validation.isValid) {
354
+ throw new Error(`Configuration validation failed in strict mode. Found ${validation.errors.length} validation errors.`);
350
355
  }
351
356
  }
352
- catch (error) {
353
- // Ignore directory access errors
354
- }
355
- return null;
356
- };
357
- // YAML Collection Schema
358
- const YamlCollectionSchema = z.object({
359
- name: z.string(),
360
- id: z.string().optional(),
361
- documentSecurity: z.boolean().default(false),
362
- enabled: z.boolean().default(true),
363
- permissions: z.array(z.object({
364
- permission: z.string(),
365
- target: z.string()
366
- })).optional().default([]),
367
- attributes: z.array(z.object({
368
- key: z.string(),
369
- type: z.string(),
370
- size: z.number().optional(),
371
- required: z.boolean().default(false),
372
- array: z.boolean().optional(),
373
- default: z.any().optional(),
374
- min: z.number().optional(),
375
- max: z.number().optional(),
376
- elements: z.array(z.string()).optional(),
377
- relatedCollection: z.string().optional(),
378
- relationType: z.string().optional(),
379
- twoWay: z.boolean().optional(),
380
- twoWayKey: z.string().optional(),
381
- onDelete: z.string().optional(),
382
- side: z.string().optional()
383
- })).optional().default([]),
384
- indexes: z.array(z.object({
385
- key: z.string(),
386
- type: z.string(),
387
- attributes: z.array(z.string()),
388
- orders: z.array(z.string()).optional()
389
- })).optional().default([]),
390
- importDefs: z.array(z.any()).optional().default([])
391
- });
392
- const loadYamlCollection = (filePath) => {
393
- try {
394
- const fileContent = fs.readFileSync(filePath, "utf8");
395
- const yamlData = yaml.load(fileContent);
396
- const parsedCollection = YamlCollectionSchema.parse(yamlData);
397
- // Convert YAML collection to CollectionCreate format
398
- const collection = {
399
- name: parsedCollection.name,
400
- $id: parsedCollection.id || parsedCollection.name.toLowerCase().replace(/\s+/g, '_'),
401
- documentSecurity: parsedCollection.documentSecurity,
402
- enabled: parsedCollection.enabled,
403
- $permissions: parsedCollection.permissions.map(p => ({
404
- permission: p.permission,
405
- target: p.target
406
- })),
407
- attributes: parsedCollection.attributes.map(attr => ({
408
- key: attr.key,
409
- type: attr.type,
410
- size: attr.size,
411
- required: attr.required,
412
- array: attr.array,
413
- xdefault: attr.default,
414
- min: attr.min,
415
- max: attr.max,
416
- elements: attr.elements,
417
- relatedCollection: attr.relatedCollection,
418
- relationType: attr.relationType,
419
- twoWay: attr.twoWay,
420
- twoWayKey: attr.twoWayKey,
421
- onDelete: attr.onDelete,
422
- side: attr.side
423
- })),
424
- indexes: parsedCollection.indexes.map(idx => ({
425
- key: idx.key,
426
- type: idx.type,
427
- attributes: idx.attributes,
428
- orders: idx.orders
429
- })),
430
- importDefs: parsedCollection.importDefs
431
- };
432
- return collection;
433
- }
434
- catch (error) {
435
- console.error(`Error loading YAML collection from ${filePath}:`, error);
436
- return null;
437
- }
357
+ return config;
438
358
  };