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
@@ -0,0 +1,340 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { tmpdir } from 'os';
4
+ import { randomBytes } from 'crypto';
5
+ import { type AppwriteConfig, type CollectionCreate } from 'appwrite-utils';
6
+
7
+ /**
8
+ * Test utilities for creating temporary directories and test fixtures
9
+ */
10
+ export class TestUtils {
11
+ private static tempDirs: string[] = [];
12
+
13
+ /**
14
+ * Creates a temporary directory for testing
15
+ */
16
+ static createTempDir(prefix = 'appwrite-test'): string {
17
+ const tempDir = path.join(tmpdir(), `${prefix}-${randomBytes(8).toString('hex')}`);
18
+ fs.mkdirSync(tempDir, { recursive: true });
19
+ this.tempDirs.push(tempDir);
20
+ return tempDir;
21
+ }
22
+
23
+ /**
24
+ * Cleans up all created temporary directories
25
+ */
26
+ static cleanup(): void {
27
+ this.tempDirs.forEach(dir => {
28
+ try {
29
+ fs.rmSync(dir, { recursive: true, force: true });
30
+ } catch (error) {
31
+ // Ignore cleanup errors
32
+ }
33
+ });
34
+ this.tempDirs = [];
35
+ }
36
+
37
+ /**
38
+ * Creates a test appwrite config
39
+ */
40
+ static createTestAppwriteConfig(overrides: Partial<AppwriteConfig> = {}): AppwriteConfig {
41
+ return {
42
+ appwriteEndpoint: 'http://localhost:8080/v1',
43
+ appwriteProject: 'test-project',
44
+ appwriteKey: 'test-key',
45
+ databases: [
46
+ {
47
+ name: 'test-database',
48
+ $id: 'test-db-id',
49
+ enabled: true,
50
+ }
51
+ ],
52
+ buckets: [],
53
+ teams: [],
54
+ functions: [],
55
+ messaging: [],
56
+ collections: [],
57
+ ...overrides,
58
+ };
59
+ }
60
+
61
+ /**
62
+ * Creates a test collection configuration
63
+ */
64
+ static createTestCollection(overrides: Partial<CollectionCreate> = {}): CollectionCreate {
65
+ return {
66
+ name: 'TestCollection',
67
+ $id: 'test-collection',
68
+ documentSecurity: false,
69
+ enabled: true,
70
+ $permissions: [],
71
+ attributes: [
72
+ {
73
+ key: 'name',
74
+ type: 'string',
75
+ size: 255,
76
+ required: true,
77
+ array: false,
78
+ },
79
+ {
80
+ key: 'email',
81
+ type: 'email',
82
+ required: true,
83
+ array: false,
84
+ }
85
+ ],
86
+ indexes: [
87
+ {
88
+ key: 'email_index',
89
+ type: 'unique',
90
+ attributes: ['email'],
91
+ }
92
+ ],
93
+ ...overrides,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Creates a test table configuration (tables API)
99
+ */
100
+ static createTestTable(overrides: any = {}): any {
101
+ return {
102
+ name: 'TestTable',
103
+ tableId: 'test-table',
104
+ databaseId: 'test-db-id',
105
+ documentSecurity: false,
106
+ enabled: true,
107
+ $permissions: [],
108
+ attributes: [
109
+ {
110
+ key: 'title',
111
+ type: 'string',
112
+ size: 255,
113
+ required: true,
114
+ array: false,
115
+ },
116
+ {
117
+ key: 'count',
118
+ type: 'integer',
119
+ required: false,
120
+ min: 0,
121
+ max: 1000,
122
+ }
123
+ ],
124
+ indexes: [
125
+ {
126
+ key: 'title_index',
127
+ type: 'key',
128
+ attributes: ['title'],
129
+ }
130
+ ],
131
+ _isFromTablesDir: true,
132
+ ...overrides,
133
+ };
134
+ }
135
+
136
+ /**
137
+ * Creates a test project structure with collections and/or tables
138
+ */
139
+ static createTestProject(options: {
140
+ hasCollections?: boolean;
141
+ hasTables?: boolean;
142
+ hasConflicts?: boolean;
143
+ useYaml?: boolean;
144
+ configDir?: string;
145
+ } = {}): string {
146
+ const {
147
+ hasCollections = true,
148
+ hasTables = false,
149
+ hasConflicts = false,
150
+ useYaml = false,
151
+ configDir,
152
+ } = options;
153
+
154
+ const testDir = configDir || this.createTempDir('test-project');
155
+
156
+ // Create .appwrite directory
157
+ const appwriteDir = path.join(testDir, '.appwrite');
158
+ fs.mkdirSync(appwriteDir, { recursive: true });
159
+
160
+ // Create config file
161
+ const config = this.createTestAppwriteConfig();
162
+ if (useYaml) {
163
+ const yamlContent = `
164
+ appwriteEndpoint: ${config.appwriteEndpoint}
165
+ appwriteProject: ${config.appwriteProject}
166
+ appwriteKey: ${config.appwriteKey}
167
+ databases:
168
+ - name: ${config.databases[0].name}
169
+ $id: ${config.databases[0].$id}
170
+ enabled: ${config.databases[0].enabled}
171
+ buckets: []
172
+ teams: []
173
+ functions: []
174
+ messaging: []
175
+ `;
176
+ fs.writeFileSync(path.join(appwriteDir, 'config.yaml'), yamlContent);
177
+ } else {
178
+ const tsContent = `
179
+ import { type AppwriteConfig } from 'appwrite-utils';
180
+
181
+ const appwriteConfig: AppwriteConfig = ${JSON.stringify(config, null, 2)};
182
+
183
+ export default appwriteConfig;
184
+ `;
185
+ fs.writeFileSync(path.join(testDir, 'appwriteConfig.ts'), tsContent);
186
+ }
187
+
188
+ // Create collections directory if requested
189
+ if (hasCollections) {
190
+ const collectionsDir = path.join(testDir, 'collections');
191
+ fs.mkdirSync(collectionsDir);
192
+
193
+ const collection = this.createTestCollection();
194
+ if (useYaml) {
195
+ const yamlContent = `
196
+ name: ${collection.name}
197
+ id: ${collection.$id}
198
+ documentSecurity: ${collection.documentSecurity}
199
+ enabled: ${collection.enabled}
200
+ permissions: []
201
+ attributes:
202
+ - key: name
203
+ type: string
204
+ size: 255
205
+ required: true
206
+ - key: email
207
+ type: email
208
+ required: true
209
+ indexes:
210
+ - key: email_index
211
+ type: unique
212
+ attributes: [email]
213
+ `;
214
+ fs.writeFileSync(path.join(collectionsDir, 'TestCollection.yaml'), yamlContent);
215
+ } else {
216
+ const tsContent = `
217
+ import { type CollectionCreate } from 'appwrite-utils';
218
+
219
+ const TestCollection: CollectionCreate = ${JSON.stringify(collection, null, 2)};
220
+
221
+ export default TestCollection;
222
+ `;
223
+ fs.writeFileSync(path.join(collectionsDir, 'TestCollection.ts'), tsContent);
224
+ }
225
+ }
226
+
227
+ // Create tables directory if requested
228
+ if (hasTables) {
229
+ const tablesDir = path.join(testDir, 'tables');
230
+ fs.mkdirSync(tablesDir);
231
+
232
+ const table = this.createTestTable();
233
+ if (useYaml) {
234
+ const yamlContent = `
235
+ name: ${table.name}
236
+ tableId: ${table.tableId}
237
+ databaseId: ${table.databaseId}
238
+ documentSecurity: ${table.documentSecurity}
239
+ enabled: ${table.enabled}
240
+ permissions: []
241
+ attributes:
242
+ - key: title
243
+ type: string
244
+ size: 255
245
+ required: true
246
+ - key: count
247
+ type: integer
248
+ required: false
249
+ min: 0
250
+ max: 1000
251
+ indexes:
252
+ - key: title_index
253
+ type: key
254
+ attributes: [title]
255
+ `;
256
+ fs.writeFileSync(path.join(tablesDir, 'TestTable.yaml'), yamlContent);
257
+ } else {
258
+ const tsContent = `
259
+ import { type TableCreate } from 'appwrite-utils';
260
+
261
+ const TestTable: any = ${JSON.stringify(table, null, 2)};
262
+
263
+ export default TestTable;
264
+ `;
265
+ fs.writeFileSync(path.join(tablesDir, 'TestTable.ts'), tsContent);
266
+ }
267
+
268
+ // Create conflicting collection if requested
269
+ if (hasConflicts && hasCollections) {
270
+ const conflictTable = this.createTestTable({
271
+ name: 'TestCollection', // Same name as collection
272
+ tableId: 'test-collection-conflict'
273
+ });
274
+ const tsContent = `
275
+ const ConflictTable: any = ${JSON.stringify(conflictTable, null, 2)};
276
+ export default ConflictTable;
277
+ `;
278
+ fs.writeFileSync(path.join(tablesDir, 'ConflictTable.ts'), tsContent);
279
+ }
280
+ }
281
+
282
+ return testDir;
283
+ }
284
+
285
+ /**
286
+ * Creates mock Appwrite client responses
287
+ */
288
+ static createMockAppwriteResponses() {
289
+ return {
290
+ databases: {
291
+ list: jest.fn().mockResolvedValue({
292
+ databases: [
293
+ {
294
+ $id: 'test-db-id',
295
+ name: 'test-database',
296
+ enabled: true,
297
+ $createdAt: new Date().toISOString(),
298
+ $updatedAt: new Date().toISOString(),
299
+ }
300
+ ],
301
+ total: 1,
302
+ }),
303
+ get: jest.fn().mockResolvedValue({
304
+ $id: 'test-db-id',
305
+ name: 'test-database',
306
+ enabled: true,
307
+ $createdAt: new Date().toISOString(),
308
+ $updatedAt: new Date().toISOString(),
309
+ }),
310
+ listCollections: jest.fn().mockResolvedValue({
311
+ collections: [
312
+ {
313
+ $id: 'test-collection',
314
+ name: 'TestCollection',
315
+ enabled: true,
316
+ documentSecurity: false,
317
+ $permissions: [],
318
+ attributes: [],
319
+ indexes: [],
320
+ $createdAt: new Date().toISOString(),
321
+ $updatedAt: new Date().toISOString(),
322
+ }
323
+ ],
324
+ total: 1,
325
+ }),
326
+ },
327
+ health: {
328
+ get: jest.fn().mockResolvedValue({
329
+ status: 'OK',
330
+ version: '1.6.0',
331
+ }),
332
+ },
333
+ };
334
+ }
335
+ }
336
+
337
+ // Clean up after all tests
338
+ afterAll(() => {
339
+ TestUtils.cleanup();
340
+ });
@@ -0,0 +1,350 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { jest } from '@jest/globals';
4
+ import { TestUtils } from '../testUtils';
5
+ import { loadConfig, loadConfigWithPath, findAppwriteConfig } from '../../src/utils/loadConfigs';
6
+ import { MessageFormatter } from '../../src/shared/messageFormatter';
7
+
8
+ // Mock MessageFormatter to capture log messages
9
+ jest.mock('../../src/shared/messageFormatter', () => ({
10
+ MessageFormatter: {
11
+ success: jest.fn(),
12
+ warning: jest.fn(),
13
+ info: jest.fn(),
14
+ error: jest.fn(),
15
+ },
16
+ }));
17
+
18
+ // Mock version detection
19
+ jest.mock('../../src/utils/versionDetection', () => ({
20
+ detectAppwriteVersionCached: jest.fn().mockResolvedValue({
21
+ serverVersion: '1.6.0',
22
+ apiMode: 'database',
23
+ }),
24
+ fetchServerVersion: jest.fn().mockResolvedValue('1.6.0'),
25
+ isVersionAtLeast: jest.fn((version, target) => {
26
+ if (!version) return false;
27
+ return version >= target;
28
+ }),
29
+ }));
30
+
31
+ describe('loadConfigs - Dual Schema Support', () => {
32
+ let testDir: string;
33
+
34
+ beforeEach(() => {
35
+ jest.clearAllMocks();
36
+ });
37
+
38
+ afterEach(() => {
39
+ TestUtils.cleanup();
40
+ });
41
+
42
+ describe('findAppwriteConfig', () => {
43
+ it('should find YAML config in .appwrite directory', () => {
44
+ testDir = TestUtils.createTestProject({ useYaml: true });
45
+ const configPath = findAppwriteConfig(testDir);
46
+ expect(configPath).toBe(testDir);
47
+ });
48
+
49
+ it('should find TypeScript config as fallback', () => {
50
+ testDir = TestUtils.createTestProject({ useYaml: false });
51
+ const configPath = findAppwriteConfig(testDir);
52
+ expect(configPath).toBe(testDir);
53
+ });
54
+
55
+ it('should return null when no config found', () => {
56
+ testDir = TestUtils.createTempDir();
57
+ const configPath = findAppwriteConfig(testDir);
58
+ expect(configPath).toBeNull();
59
+ });
60
+ });
61
+
62
+ describe('Dual Folder Loading', () => {
63
+ it('should load collections from collections/ directory only', async () => {
64
+ testDir = TestUtils.createTestProject({
65
+ hasCollections: true,
66
+ hasTables: false,
67
+ });
68
+
69
+ const config = await loadConfig(testDir);
70
+
71
+ expect(config.collections).toHaveLength(1);
72
+ expect(config.collections[0].name).toBe('TestCollection');
73
+ expect(config.collections[0]._isFromTablesDir).toBeUndefined();
74
+ expect(MessageFormatter.success).toHaveBeenCalledWith(
75
+ 'Loading from collections/ directory: 1 files found',
76
+ { prefix: 'Config' }
77
+ );
78
+ });
79
+
80
+ it('should load tables from tables/ directory only', async () => {
81
+ testDir = TestUtils.createTestProject({
82
+ hasCollections: false,
83
+ hasTables: true,
84
+ });
85
+
86
+ const config = await loadConfig(testDir);
87
+
88
+ expect(config.collections).toHaveLength(1);
89
+ expect(config.collections[0].name).toBe('TestTable');
90
+ expect(config.collections[0]._isFromTablesDir).toBe(true);
91
+ expect(MessageFormatter.success).toHaveBeenCalledWith(
92
+ 'Loading from tables/ directory: 1 files found',
93
+ { prefix: 'Config' }
94
+ );
95
+ });
96
+
97
+ it('should load from both collections/ and tables/ directories', async () => {
98
+ testDir = TestUtils.createTestProject({
99
+ hasCollections: true,
100
+ hasTables: true,
101
+ });
102
+
103
+ const config = await loadConfig(testDir);
104
+
105
+ expect(config.collections).toHaveLength(2);
106
+
107
+ const collection = config.collections.find((c: any) => !c._isFromTablesDir);
108
+ const table = config.collections.find((c: any) => c._isFromTablesDir);
109
+
110
+ expect(collection).toBeDefined();
111
+ expect(collection.name).toBe('TestCollection');
112
+
113
+ expect(table).toBeDefined();
114
+ expect(table.name).toBe('TestTable');
115
+ expect(table._isFromTablesDir).toBe(true);
116
+
117
+ expect(MessageFormatter.success).toHaveBeenCalledWith(
118
+ 'Loading from collections/ directory: 1 files found',
119
+ { prefix: 'Config' }
120
+ );
121
+ expect(MessageFormatter.success).toHaveBeenCalledWith(
122
+ 'Loading from tables/ directory: 1 files found',
123
+ { prefix: 'Config' }
124
+ );
125
+ });
126
+
127
+ it('should handle naming conflicts with collections/ taking priority', async () => {
128
+ testDir = TestUtils.createTestProject({
129
+ hasCollections: true,
130
+ hasTables: true,
131
+ hasConflicts: true,
132
+ });
133
+
134
+ const config = await loadConfig(testDir);
135
+
136
+ // Should have 2 items: original collection + one table (conflict skipped)
137
+ expect(config.collections).toHaveLength(2);
138
+
139
+ // Check that the collection takes priority
140
+ const collectionItems = config.collections.filter((c: any) => !c._isFromTablesDir);
141
+ expect(collectionItems).toHaveLength(1);
142
+ expect(collectionItems[0].name).toBe('TestCollection');
143
+
144
+ // Check warning was logged
145
+ expect(MessageFormatter.warning).toHaveBeenCalledWith(
146
+ expect.stringContaining('Found 1 naming conflicts'),
147
+ { prefix: 'Config' }
148
+ );
149
+ });
150
+ });
151
+
152
+ describe('YAML Support', () => {
153
+ it('should load YAML collections', async () => {
154
+ testDir = TestUtils.createTestProject({
155
+ hasCollections: true,
156
+ useYaml: true,
157
+ });
158
+
159
+ const config = await loadConfig(testDir);
160
+
161
+ expect(config.collections).toHaveLength(1);
162
+ expect(config.collections[0].name).toBe('TestCollection');
163
+ expect(config.collections[0].$id).toBe('test-collection');
164
+ expect(config.collections[0].attributes).toHaveLength(2);
165
+ });
166
+
167
+ it('should load YAML tables', async () => {
168
+ testDir = TestUtils.createTestProject({
169
+ hasTables: true,
170
+ useYaml: true,
171
+ });
172
+
173
+ const config = await loadConfig(testDir);
174
+
175
+ expect(config.collections).toHaveLength(1);
176
+ expect(config.collections[0].name).toBe('TestTable');
177
+ expect(config.collections[0]._isFromTablesDir).toBe(true);
178
+ });
179
+
180
+ it('should load mixed YAML and TypeScript files', async () => {
181
+ testDir = TestUtils.createTestProject({
182
+ hasCollections: true,
183
+ useYaml: false,
184
+ });
185
+
186
+ // Add a YAML table alongside TypeScript collection
187
+ const tablesDir = path.join(testDir, 'tables');
188
+ fs.mkdirSync(tablesDir);
189
+
190
+ const yamlContent = `
191
+ name: YamlTable
192
+ tableId: yaml-table
193
+ databaseId: test-db-id
194
+ documentSecurity: false
195
+ enabled: true
196
+ permissions: []
197
+ attributes:
198
+ - key: content
199
+ type: string
200
+ size: 1000
201
+ required: true
202
+ indexes: []
203
+ `;
204
+ fs.writeFileSync(path.join(tablesDir, 'YamlTable.yaml'), yamlContent);
205
+
206
+ const config = await loadConfig(testDir);
207
+
208
+ expect(config.collections).toHaveLength(2);
209
+ expect(config.collections.some((c: any) => c.name === 'TestCollection')).toBe(true);
210
+ expect(config.collections.some((c: any) => c.name === 'YamlTable')).toBe(true);
211
+ });
212
+ });
213
+
214
+ describe('Legacy Single Directory Support', () => {
215
+ it('should fall back to legacy collections/ directory when no dual directories', async () => {
216
+ testDir = TestUtils.createTempDir();
217
+
218
+ // Create config without .appwrite structure
219
+ const config = TestUtils.createTestAppwriteConfig();
220
+ const tsContent = `
221
+ import { type AppwriteConfig } from 'appwrite-utils';
222
+ const appwriteConfig: AppwriteConfig = ${JSON.stringify(config, null, 2)};
223
+ export default appwriteConfig;
224
+ `;
225
+ fs.writeFileSync(path.join(testDir, 'appwriteConfig.ts'), tsContent);
226
+
227
+ // Create only collections directory (legacy)
228
+ const collectionsDir = path.join(testDir, 'collections');
229
+ fs.mkdirSync(collectionsDir);
230
+
231
+ const collection = TestUtils.createTestCollection();
232
+ const collectionContent = `
233
+ const TestCollection = ${JSON.stringify(collection, null, 2)};
234
+ export default TestCollection;
235
+ `;
236
+ fs.writeFileSync(path.join(collectionsDir, 'TestCollection.ts'), collectionContent);
237
+
238
+ const loadedConfig = await loadConfig(testDir);
239
+
240
+ expect(loadedConfig.collections).toHaveLength(1);
241
+ expect(loadedConfig.collections[0].name).toBe('TestCollection');
242
+ expect(MessageFormatter.info).toHaveBeenCalledWith(
243
+ 'Using legacy single directory: collections/',
244
+ { prefix: 'Config' }
245
+ );
246
+ });
247
+ });
248
+
249
+ describe('loadConfigWithPath', () => {
250
+ it('should return both config and actual config path', async () => {
251
+ testDir = TestUtils.createTestProject({ useYaml: true });
252
+
253
+ const result = await loadConfigWithPath(testDir);
254
+
255
+ expect(result.config).toBeDefined();
256
+ expect(result.actualConfigPath).toContain('config.yaml');
257
+ expect(result.config.appwriteProject).toBe('test-project');
258
+ });
259
+
260
+ it('should handle .appwrite directory path directly', async () => {
261
+ testDir = TestUtils.createTestProject({ useYaml: true });
262
+ const appwriteDir = path.join(testDir, '.appwrite');
263
+
264
+ const result = await loadConfigWithPath(appwriteDir);
265
+
266
+ expect(result.config).toBeDefined();
267
+ expect(result.actualConfigPath).toContain('config.yaml');
268
+ });
269
+ });
270
+
271
+ describe('Error Handling', () => {
272
+ it('should throw error when no config found', async () => {
273
+ testDir = TestUtils.createTempDir();
274
+
275
+ await expect(loadConfig(testDir)).rejects.toThrow('No valid configuration found');
276
+ });
277
+
278
+ it('should handle malformed YAML gracefully', async () => {
279
+ testDir = TestUtils.createTestProject({ useYaml: true });
280
+
281
+ // Create malformed YAML collection
282
+ const collectionsDir = path.join(testDir, 'collections');
283
+ fs.writeFileSync(path.join(collectionsDir, 'BadCollection.yaml'), 'invalid: yaml: content:');
284
+
285
+ const config = await loadConfig(testDir);
286
+
287
+ // Should still load other valid collections
288
+ expect(config).toBeDefined();
289
+ // The malformed collection should be skipped
290
+ });
291
+
292
+ it('should handle missing TypeScript exports gracefully', async () => {
293
+ testDir = TestUtils.createTestProject({ useYaml: false });
294
+
295
+ // Create TypeScript file with no export
296
+ const collectionsDir = path.join(testDir, 'collections');
297
+ fs.writeFileSync(path.join(collectionsDir, 'NoExport.ts'), 'const collection = {};');
298
+
299
+ const config = await loadConfig(testDir);
300
+
301
+ expect(config).toBeDefined();
302
+ // Should still have the original test collection
303
+ expect(config.collections.length).toBeGreaterThanOrEqual(1);
304
+ });
305
+ });
306
+
307
+ describe('Performance and Scalability', () => {
308
+ it('should handle large numbers of collections and tables', async () => {
309
+ testDir = TestUtils.createTempDir();
310
+
311
+ // Create config
312
+ const config = TestUtils.createTestAppwriteConfig();
313
+ const tsContent = `export default ${JSON.stringify(config, null, 2)};`;
314
+ fs.writeFileSync(path.join(testDir, 'appwriteConfig.ts'), tsContent);
315
+
316
+ // Create many collections
317
+ const collectionsDir = path.join(testDir, 'collections');
318
+ fs.mkdirSync(collectionsDir);
319
+
320
+ for (let i = 0; i < 50; i++) {
321
+ const collection = TestUtils.createTestCollection({
322
+ name: `Collection${i}`,
323
+ $id: `collection-${i}`,
324
+ });
325
+ const content = `export default ${JSON.stringify(collection, null, 2)};`;
326
+ fs.writeFileSync(path.join(collectionsDir, `Collection${i}.ts`), content);
327
+ }
328
+
329
+ // Create many tables
330
+ const tablesDir = path.join(testDir, 'tables');
331
+ fs.mkdirSync(tablesDir);
332
+
333
+ for (let i = 0; i < 30; i++) {
334
+ const table = TestUtils.createTestTable({
335
+ name: `Table${i}`,
336
+ tableId: `table-${i}`,
337
+ });
338
+ const content = `export default ${JSON.stringify(table, null, 2)};`;
339
+ fs.writeFileSync(path.join(tablesDir, `Table${i}.ts`), content);
340
+ }
341
+
342
+ const startTime = Date.now();
343
+ const loadedConfig = await loadConfig(testDir);
344
+ const loadTime = Date.now() - startTime;
345
+
346
+ expect(loadedConfig.collections).toHaveLength(80); // 50 collections + 30 tables
347
+ expect(loadTime).toBeLessThan(5000); // Should load within 5 seconds
348
+ });
349
+ });
350
+ });