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