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.
- package/CHANGELOG.md +199 -0
- package/README.md +251 -29
- package/dist/adapters/AdapterFactory.d.ts +10 -3
- package/dist/adapters/AdapterFactory.js +213 -17
- package/dist/adapters/TablesDBAdapter.js +60 -17
- package/dist/backups/operations/bucketBackup.d.ts +19 -0
- package/dist/backups/operations/bucketBackup.js +197 -0
- package/dist/backups/operations/collectionBackup.d.ts +30 -0
- package/dist/backups/operations/collectionBackup.js +201 -0
- package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
- package/dist/backups/operations/comprehensiveBackup.js +238 -0
- package/dist/backups/schemas/bucketManifest.d.ts +93 -0
- package/dist/backups/schemas/bucketManifest.js +33 -0
- package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
- package/dist/backups/schemas/comprehensiveManifest.js +32 -0
- package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
- package/dist/backups/tracking/centralizedTracking.js +274 -0
- package/dist/cli/commands/configCommands.d.ts +8 -0
- package/dist/cli/commands/configCommands.js +160 -0
- package/dist/cli/commands/databaseCommands.d.ts +13 -0
- package/dist/cli/commands/databaseCommands.js +478 -0
- package/dist/cli/commands/functionCommands.d.ts +7 -0
- package/dist/cli/commands/functionCommands.js +289 -0
- package/dist/cli/commands/schemaCommands.d.ts +7 -0
- package/dist/cli/commands/schemaCommands.js +134 -0
- package/dist/cli/commands/transferCommands.d.ts +5 -0
- package/dist/cli/commands/transferCommands.js +384 -0
- package/dist/collections/attributes.d.ts +5 -4
- package/dist/collections/attributes.js +539 -246
- package/dist/collections/indexes.js +39 -37
- package/dist/collections/methods.d.ts +2 -16
- package/dist/collections/methods.js +90 -538
- package/dist/collections/transferOperations.d.ts +7 -0
- package/dist/collections/transferOperations.js +331 -0
- package/dist/collections/wipeOperations.d.ts +16 -0
- package/dist/collections/wipeOperations.js +328 -0
- package/dist/config/configMigration.d.ts +87 -0
- package/dist/config/configMigration.js +390 -0
- package/dist/config/configValidation.d.ts +66 -0
- package/dist/config/configValidation.js +358 -0
- package/dist/config/yamlConfig.d.ts +455 -1
- package/dist/config/yamlConfig.js +145 -52
- package/dist/databases/methods.js +3 -2
- package/dist/databases/setup.d.ts +1 -2
- package/dist/databases/setup.js +9 -87
- package/dist/examples/yamlTerminologyExample.d.ts +42 -0
- package/dist/examples/yamlTerminologyExample.js +269 -0
- package/dist/functions/deployments.js +11 -10
- package/dist/functions/methods.d.ts +1 -1
- package/dist/functions/methods.js +5 -4
- package/dist/init.js +9 -9
- package/dist/interactiveCLI.d.ts +8 -17
- package/dist/interactiveCLI.js +181 -1172
- package/dist/main.js +364 -21
- package/dist/migrations/afterImportActions.js +22 -30
- package/dist/migrations/appwriteToX.js +71 -25
- package/dist/migrations/dataLoader.js +35 -26
- package/dist/migrations/importController.js +29 -30
- package/dist/migrations/relationships.js +13 -12
- package/dist/migrations/services/ImportOrchestrator.js +16 -19
- package/dist/migrations/transfer.js +46 -46
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +3 -1
- package/dist/migrations/yaml/YamlImportConfigLoader.js +6 -3
- package/dist/migrations/yaml/YamlImportIntegration.d.ts +9 -3
- package/dist/migrations/yaml/YamlImportIntegration.js +22 -11
- package/dist/migrations/yaml/generateImportSchemas.d.ts +14 -1
- package/dist/migrations/yaml/generateImportSchemas.js +736 -7
- package/dist/schemas/authUser.d.ts +1 -1
- package/dist/setupController.js +3 -2
- package/dist/shared/backupMetadataSchema.d.ts +94 -0
- package/dist/shared/backupMetadataSchema.js +38 -0
- package/dist/shared/backupTracking.d.ts +18 -0
- package/dist/shared/backupTracking.js +176 -0
- package/dist/shared/confirmationDialogs.js +15 -15
- package/dist/shared/errorUtils.d.ts +54 -0
- package/dist/shared/errorUtils.js +95 -0
- package/dist/shared/functionManager.js +20 -19
- package/dist/shared/indexManager.js +12 -11
- package/dist/shared/jsonSchemaGenerator.js +10 -26
- package/dist/shared/logging.d.ts +51 -0
- package/dist/shared/logging.js +70 -0
- package/dist/shared/messageFormatter.d.ts +2 -0
- package/dist/shared/messageFormatter.js +10 -0
- package/dist/shared/migrationHelpers.d.ts +6 -16
- package/dist/shared/migrationHelpers.js +24 -21
- package/dist/shared/operationLogger.d.ts +8 -1
- package/dist/shared/operationLogger.js +11 -24
- package/dist/shared/operationQueue.d.ts +28 -1
- package/dist/shared/operationQueue.js +268 -66
- package/dist/shared/operationsTable.d.ts +26 -0
- package/dist/shared/operationsTable.js +286 -0
- package/dist/shared/operationsTableSchema.d.ts +48 -0
- package/dist/shared/operationsTableSchema.js +35 -0
- package/dist/shared/relationshipExtractor.d.ts +56 -0
- package/dist/shared/relationshipExtractor.js +138 -0
- package/dist/shared/schemaGenerator.d.ts +19 -1
- package/dist/shared/schemaGenerator.js +56 -75
- package/dist/storage/backupCompression.d.ts +20 -0
- package/dist/storage/backupCompression.js +67 -0
- package/dist/storage/methods.d.ts +16 -2
- package/dist/storage/methods.js +98 -14
- package/dist/users/methods.js +9 -8
- package/dist/utils/configDiscovery.d.ts +78 -0
- package/dist/utils/configDiscovery.js +430 -0
- package/dist/utils/directoryUtils.d.ts +22 -0
- package/dist/utils/directoryUtils.js +59 -0
- package/dist/utils/getClientFromConfig.d.ts +17 -8
- package/dist/utils/getClientFromConfig.js +162 -17
- package/dist/utils/helperFunctions.d.ts +16 -2
- package/dist/utils/helperFunctions.js +19 -5
- package/dist/utils/loadConfigs.d.ts +34 -9
- package/dist/utils/loadConfigs.js +236 -316
- package/dist/utils/pathResolvers.d.ts +53 -0
- package/dist/utils/pathResolvers.js +72 -0
- package/dist/utils/projectConfig.d.ts +119 -0
- package/dist/utils/projectConfig.js +171 -0
- package/dist/utils/retryFailedPromises.js +4 -2
- package/dist/utils/sessionAuth.d.ts +48 -0
- package/dist/utils/sessionAuth.js +164 -0
- package/dist/utils/sessionPreservationExample.d.ts +1666 -0
- package/dist/utils/sessionPreservationExample.js +101 -0
- package/dist/utils/setupFiles.js +301 -41
- package/dist/utils/typeGuards.d.ts +35 -0
- package/dist/utils/typeGuards.js +57 -0
- package/dist/utils/versionDetection.js +145 -9
- package/dist/utils/yamlConverter.d.ts +53 -3
- package/dist/utils/yamlConverter.js +232 -13
- package/dist/utils/yamlLoader.d.ts +70 -0
- package/dist/utils/yamlLoader.js +263 -0
- package/dist/utilsController.d.ts +36 -3
- package/dist/utilsController.js +186 -56
- package/package.json +12 -2
- package/src/adapters/AdapterFactory.ts +263 -35
- package/src/adapters/TablesDBAdapter.ts +225 -36
- package/src/backups/operations/bucketBackup.ts +277 -0
- package/src/backups/operations/collectionBackup.ts +310 -0
- package/src/backups/operations/comprehensiveBackup.ts +342 -0
- package/src/backups/schemas/bucketManifest.ts +78 -0
- package/src/backups/schemas/comprehensiveManifest.ts +76 -0
- package/src/backups/tracking/centralizedTracking.ts +352 -0
- package/src/cli/commands/configCommands.ts +194 -0
- package/src/cli/commands/databaseCommands.ts +635 -0
- package/src/cli/commands/functionCommands.ts +379 -0
- package/src/cli/commands/schemaCommands.ts +163 -0
- package/src/cli/commands/transferCommands.ts +457 -0
- package/src/collections/attributes.ts +900 -621
- package/src/collections/attributes.ts.backup +1555 -0
- package/src/collections/indexes.ts +116 -114
- package/src/collections/methods.ts +295 -968
- package/src/collections/transferOperations.ts +516 -0
- package/src/collections/wipeOperations.ts +501 -0
- package/src/config/README.md +274 -0
- package/src/config/configMigration.ts +575 -0
- package/src/config/configValidation.ts +445 -0
- package/src/config/yamlConfig.ts +168 -55
- package/src/databases/methods.ts +3 -2
- package/src/databases/setup.ts +11 -138
- package/src/examples/yamlTerminologyExample.ts +341 -0
- package/src/functions/deployments.ts +14 -12
- package/src/functions/methods.ts +11 -11
- package/src/functions/templates/hono-typescript/README.md +286 -0
- package/src/functions/templates/hono-typescript/package.json +26 -0
- package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/src/functions/templates/hono-typescript/src/app.ts +180 -0
- package/src/functions/templates/hono-typescript/src/context.ts +103 -0
- package/src/functions/templates/hono-typescript/src/index.ts +54 -0
- package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/src/functions/templates/hono-typescript/tsconfig.json +20 -0
- package/src/functions/templates/typescript-node/package.json +2 -1
- package/src/functions/templates/typescript-node/src/context.ts +103 -0
- package/src/functions/templates/typescript-node/src/index.ts +18 -12
- package/src/functions/templates/uv/pyproject.toml +1 -0
- package/src/functions/templates/uv/src/context.py +125 -0
- package/src/functions/templates/uv/src/index.py +35 -5
- package/src/init.ts +9 -11
- package/src/interactiveCLI.ts +278 -1596
- package/src/main.ts +418 -24
- package/src/migrations/afterImportActions.ts +71 -44
- package/src/migrations/appwriteToX.ts +100 -34
- package/src/migrations/dataLoader.ts +48 -34
- package/src/migrations/importController.ts +44 -39
- package/src/migrations/relationships.ts +28 -18
- package/src/migrations/services/ImportOrchestrator.ts +24 -27
- package/src/migrations/transfer.ts +159 -121
- package/src/migrations/yaml/YamlImportConfigLoader.ts +11 -4
- package/src/migrations/yaml/YamlImportIntegration.ts +47 -20
- package/src/migrations/yaml/generateImportSchemas.ts +751 -12
- package/src/setupController.ts +3 -2
- package/src/shared/backupMetadataSchema.ts +93 -0
- package/src/shared/backupTracking.ts +211 -0
- package/src/shared/confirmationDialogs.ts +19 -19
- package/src/shared/errorUtils.ts +110 -0
- package/src/shared/functionManager.ts +21 -20
- package/src/shared/indexManager.ts +12 -11
- package/src/shared/jsonSchemaGenerator.ts +38 -52
- package/src/shared/logging.ts +75 -0
- package/src/shared/messageFormatter.ts +14 -1
- package/src/shared/migrationHelpers.ts +45 -38
- package/src/shared/operationLogger.ts +11 -36
- package/src/shared/operationQueue.ts +322 -93
- package/src/shared/operationsTable.ts +338 -0
- package/src/shared/operationsTableSchema.ts +60 -0
- package/src/shared/relationshipExtractor.ts +214 -0
- package/src/shared/schemaGenerator.ts +179 -219
- package/src/storage/backupCompression.ts +88 -0
- package/src/storage/methods.ts +131 -34
- package/src/users/methods.ts +11 -9
- package/src/utils/configDiscovery.ts +502 -0
- package/src/utils/directoryUtils.ts +61 -0
- package/src/utils/getClientFromConfig.ts +205 -22
- package/src/utils/helperFunctions.ts +23 -5
- package/src/utils/loadConfigs.ts +313 -345
- package/src/utils/pathResolvers.ts +81 -0
- package/src/utils/projectConfig.ts +299 -0
- package/src/utils/retryFailedPromises.ts +4 -2
- package/src/utils/sessionAuth.ts +230 -0
- package/src/utils/setupFiles.ts +322 -54
- package/src/utils/typeGuards.ts +65 -0
- package/src/utils/versionDetection.ts +218 -64
- package/src/utils/yamlConverter.ts +296 -13
- package/src/utils/yamlLoader.ts +364 -0
- package/src/utilsController.ts +314 -110
- package/tests/README.md +497 -0
- package/tests/adapters/AdapterFactory.test.ts +277 -0
- package/tests/integration/syncOperations.test.ts +463 -0
- package/tests/jest.config.js +25 -0
- package/tests/migration/configMigration.test.ts +546 -0
- package/tests/setup.ts +62 -0
- package/tests/testUtils.ts +340 -0
- package/tests/utils/loadConfigs.test.ts +350 -0
- package/tests/validation/configValidation.test.ts +412 -0
- package/src/utils/schemaStrings.ts +0 -517
package/src/interactiveCLI.ts
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
import inquirer from "inquirer";
|
2
2
|
import { UtilsController } from "./utilsController.js";
|
3
|
-
import { createEmptyCollection, setupDirsFiles } from "./utils/setupFiles.js";
|
4
|
-
import { fetchAllDatabases } from "./databases/methods.js";
|
5
3
|
import { fetchAllCollections } from "./collections/methods.js";
|
6
4
|
import { listBuckets, createBucket } from "./storage/methods.js";
|
7
5
|
import {
|
@@ -13,12 +11,7 @@ import {
|
|
13
11
|
Query,
|
14
12
|
Functions,
|
15
13
|
} from "node-appwrite";
|
16
|
-
import { getClient } from "./utils/getClientFromConfig.js";
|
17
|
-
import type { TransferOptions } from "./migrations/transfer.js";
|
18
|
-
import { ComprehensiveTransfer, type ComprehensiveTransferOptions } from "./migrations/comprehensiveTransfer.js";
|
19
14
|
import {
|
20
|
-
AppwriteFunctionSchema,
|
21
|
-
parseAttribute,
|
22
15
|
PermissionToAppwritePermission,
|
23
16
|
RuntimeSchema,
|
24
17
|
permissionSchema,
|
@@ -33,27 +26,29 @@ import { ulid } from "ulidx";
|
|
33
26
|
import chalk from "chalk";
|
34
27
|
import { DateTime } from "luxon";
|
35
28
|
import {
|
36
|
-
createFunctionTemplate,
|
37
|
-
deleteFunction,
|
38
|
-
downloadLatestFunctionDeployment,
|
39
29
|
getFunction,
|
30
|
+
downloadLatestFunctionDeployment,
|
40
31
|
listFunctions,
|
41
|
-
listSpecifications,
|
42
32
|
} from "./functions/methods.js";
|
43
|
-
import { deployLocalFunction } from "./functions/deployments.js";
|
44
33
|
import { join } from "node:path";
|
45
34
|
import path from "path";
|
46
35
|
import fs from "node:fs";
|
47
36
|
import os from "node:os";
|
48
|
-
import { SchemaGenerator } from "./shared/schemaGenerator.js";
|
49
|
-
import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
|
50
37
|
import { MessageFormatter } from "./shared/messageFormatter.js";
|
51
|
-
import { migrateConfig } from "./utils/configMigration.js";
|
52
38
|
import { findAppwriteConfig } from "./utils/loadConfigs.js";
|
53
|
-
import { findYamlConfig
|
39
|
+
import { findYamlConfig } from "./config/yamlConfig.js";
|
40
|
+
|
41
|
+
// Import command modules
|
42
|
+
import { configCommands } from "./cli/commands/configCommands.js";
|
43
|
+
import { databaseCommands } from "./cli/commands/databaseCommands.js";
|
44
|
+
import { functionCommands } from "./cli/commands/functionCommands.js";
|
45
|
+
import { transferCommands } from "./cli/commands/transferCommands.js";
|
46
|
+
import { schemaCommands } from "./cli/commands/schemaCommands.js";
|
54
47
|
|
55
48
|
enum CHOICES {
|
56
49
|
MIGRATE_CONFIG = "🔄 Migrate TypeScript config to YAML (.appwrite structure)",
|
50
|
+
VALIDATE_CONFIG = "✅ Validate configuration (collections/tables conflicts)",
|
51
|
+
MIGRATE_COLLECTIONS_TO_TABLES = "🔀 Migrate collections to tables format",
|
57
52
|
CREATE_COLLECTION_CONFIG = "📄 Create collection config file",
|
58
53
|
CREATE_FUNCTION = "⚡ Create a new function, from scratch or using a template",
|
59
54
|
DEPLOY_FUNCTION = "🚀 Deploy function(s)",
|
@@ -113,75 +108,80 @@ export class InteractiveCLI {
|
|
113
108
|
|
114
109
|
switch (action) {
|
115
110
|
case CHOICES.MIGRATE_CONFIG:
|
116
|
-
await
|
111
|
+
await configCommands.migrateTypeScriptConfig(this);
|
112
|
+
break;
|
113
|
+
case CHOICES.VALIDATE_CONFIG:
|
114
|
+
await configCommands.validateConfiguration(this);
|
115
|
+
break;
|
116
|
+
case CHOICES.MIGRATE_COLLECTIONS_TO_TABLES:
|
117
|
+
await configCommands.migrateCollectionsToTables(this);
|
117
118
|
break;
|
118
119
|
case CHOICES.CREATE_COLLECTION_CONFIG:
|
119
|
-
await
|
120
|
+
await configCommands.createCollectionConfig(this);
|
120
121
|
break;
|
121
122
|
case CHOICES.CREATE_FUNCTION:
|
122
123
|
await this.initControllerIfNeeded();
|
123
|
-
await
|
124
|
+
await functionCommands.createFunction(this);
|
124
125
|
break;
|
125
126
|
case CHOICES.DEPLOY_FUNCTION:
|
126
127
|
await this.initControllerIfNeeded();
|
127
|
-
await
|
128
|
+
await functionCommands.deployFunction(this);
|
128
129
|
break;
|
129
130
|
case CHOICES.DELETE_FUNCTION:
|
130
131
|
await this.initControllerIfNeeded();
|
131
|
-
await
|
132
|
+
await functionCommands.deleteFunction(this);
|
132
133
|
break;
|
133
134
|
case CHOICES.SETUP_DIRS_FILES:
|
134
|
-
await setupDirsFiles(
|
135
|
+
await schemaCommands.setupDirsFiles(this, false);
|
135
136
|
break;
|
136
137
|
case CHOICES.SETUP_DIRS_FILES_WITH_EXAMPLE_DATA:
|
137
|
-
await setupDirsFiles(
|
138
|
+
await schemaCommands.setupDirsFiles(this, true);
|
138
139
|
break;
|
139
140
|
case CHOICES.SYNCHRONIZE_CONFIGURATIONS:
|
140
141
|
await this.initControllerIfNeeded();
|
141
|
-
await
|
142
|
+
await databaseCommands.synchronizeConfigurations(this);
|
142
143
|
break;
|
143
144
|
case CHOICES.SYNC_DB:
|
144
145
|
await this.initControllerIfNeeded();
|
145
|
-
await
|
146
|
+
await databaseCommands.syncDb(this);
|
146
147
|
break;
|
147
148
|
case CHOICES.TRANSFER_DATA:
|
148
149
|
await this.initControllerIfNeeded();
|
149
|
-
await
|
150
|
+
await transferCommands.transferData(this);
|
150
151
|
break;
|
151
152
|
case CHOICES.COMPREHENSIVE_TRANSFER:
|
152
|
-
await
|
153
|
+
await transferCommands.comprehensiveTransfer(this);
|
153
154
|
break;
|
154
155
|
case CHOICES.BACKUP_DATABASE:
|
155
156
|
await this.initControllerIfNeeded();
|
156
|
-
await
|
157
|
+
await databaseCommands.backupDatabase(this);
|
157
158
|
break;
|
158
159
|
case CHOICES.WIPE_DATABASE:
|
159
160
|
await this.initControllerIfNeeded();
|
160
|
-
await
|
161
|
+
await databaseCommands.wipeDatabase(this);
|
161
162
|
break;
|
162
163
|
case CHOICES.WIPE_COLLECTIONS:
|
163
164
|
await this.initControllerIfNeeded();
|
164
|
-
await
|
165
|
+
await databaseCommands.wipeCollections(this);
|
165
166
|
break;
|
166
167
|
case CHOICES.GENERATE_SCHEMAS:
|
167
168
|
await this.initControllerIfNeeded();
|
168
|
-
await
|
169
|
+
await schemaCommands.generateSchemas(this);
|
169
170
|
break;
|
170
171
|
case CHOICES.GENERATE_CONSTANTS:
|
171
172
|
await this.initControllerIfNeeded();
|
172
|
-
await
|
173
|
+
await schemaCommands.generateConstants(this);
|
173
174
|
break;
|
174
175
|
case CHOICES.IMPORT_DATA:
|
175
176
|
await this.initControllerIfNeeded();
|
176
|
-
await
|
177
|
+
await schemaCommands.importData(this);
|
177
178
|
break;
|
178
179
|
case CHOICES.RELOAD_CONFIG:
|
179
|
-
await
|
180
|
-
await this.reloadConfig();
|
180
|
+
await configCommands.reloadConfigWithSessionPreservation(this);
|
181
181
|
break;
|
182
182
|
case CHOICES.UPDATE_FUNCTION_SPEC:
|
183
183
|
await this.initControllerIfNeeded();
|
184
|
-
await
|
184
|
+
await functionCommands.updateFunctionSpec(this);
|
185
185
|
break;
|
186
186
|
case CHOICES.EXIT:
|
187
187
|
MessageFormatter.success("Goodbye!");
|
@@ -198,6 +198,26 @@ export class InteractiveCLI {
|
|
198
198
|
if (!this.controller) {
|
199
199
|
this.controller = new UtilsController(this.currentDir, directConfig);
|
200
200
|
await this.controller.init();
|
201
|
+
} else {
|
202
|
+
// Extract session info from existing controller before reinitializing
|
203
|
+
const sessionInfo = this.controller.getSessionInfo();
|
204
|
+
if (sessionInfo.hasSession && directConfig) {
|
205
|
+
// Create enhanced directConfig with session preservation
|
206
|
+
const enhancedDirectConfig = {
|
207
|
+
...directConfig,
|
208
|
+
sessionCookie: (this.controller as any).sessionCookie,
|
209
|
+
sessionMetadata: (this.controller as any).sessionMetadata
|
210
|
+
};
|
211
|
+
|
212
|
+
// Reinitialize with session preservation
|
213
|
+
this.controller = new UtilsController(this.currentDir, enhancedDirectConfig);
|
214
|
+
await this.controller.init();
|
215
|
+
} else if (directConfig) {
|
216
|
+
// Standard reinitialize without session
|
217
|
+
this.controller = new UtilsController(this.currentDir, directConfig);
|
218
|
+
await this.controller.init();
|
219
|
+
}
|
220
|
+
// If no directConfig provided, keep existing controller
|
201
221
|
}
|
202
222
|
}
|
203
223
|
|
@@ -220,11 +240,7 @@ export class InteractiveCLI {
|
|
220
240
|
acc.push(db);
|
221
241
|
}
|
222
242
|
return acc;
|
223
|
-
}, [] as Models.Database[])
|
224
|
-
.filter((db) => {
|
225
|
-
const useMigrations = this.controller?.config?.useMigrations ?? true;
|
226
|
-
return useMigrations || db.name.toLowerCase() !== "migrations";
|
227
|
-
});
|
243
|
+
}, [] as Models.Database[]);
|
228
244
|
|
229
245
|
const hasLocalAndRemote =
|
230
246
|
allDatabases.some((db) =>
|
@@ -245,11 +261,7 @@ export class InteractiveCLI {
|
|
245
261
|
: " (Remote)"
|
246
262
|
: ""),
|
247
263
|
value: db,
|
248
|
-
}))
|
249
|
-
.filter((db) => {
|
250
|
-
const useMigrations = this.controller?.config?.useMigrations ?? true;
|
251
|
-
return useMigrations || db.name.toLowerCase() !== "migrations";
|
252
|
-
});
|
264
|
+
}));
|
253
265
|
|
254
266
|
const { selectedDatabases } = await inquirer.prompt([
|
255
267
|
{
|
@@ -282,10 +294,9 @@ export class InteractiveCLI {
|
|
282
294
|
Query.equal("name", database.name),
|
283
295
|
]);
|
284
296
|
if (dbExists.total === 0) {
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
)
|
297
|
+
MessageFormatter.warning(
|
298
|
+
`Database "${database.name}" does not exist, using only local collection/table options`,
|
299
|
+
{ prefix: "Database" }
|
289
300
|
);
|
290
301
|
shouldFilterByDatabase = false;
|
291
302
|
} else {
|
@@ -312,14 +323,27 @@ export class InteractiveCLI {
|
|
312
323
|
),
|
313
324
|
];
|
314
325
|
|
315
|
-
if (shouldFilterByDatabase) {
|
316
|
-
//
|
317
|
-
|
318
|
-
|
319
|
-
if (
|
320
|
-
|
321
|
-
|
322
|
-
|
326
|
+
if (shouldFilterByDatabase) {
|
327
|
+
// Enhanced filtering for tables with optional databaseId
|
328
|
+
allCollections = allCollections.filter((c: any) => {
|
329
|
+
// For remote collections, they should match the selected database
|
330
|
+
if (remoteCollections.some((rc) => rc.name === c.name)) {
|
331
|
+
return c.databaseId === database.$id;
|
332
|
+
}
|
333
|
+
|
334
|
+
// For local collections/tables:
|
335
|
+
// - Collections without databaseId are kept (backward compatibility)
|
336
|
+
// - Tables with databaseId must match the selected database
|
337
|
+
// - Tables without databaseId are kept (fallback for misconfigured tables)
|
338
|
+
if (!c.databaseId) return true;
|
339
|
+
return c.databaseId === database.$id;
|
340
|
+
});
|
341
|
+
}
|
342
|
+
|
343
|
+
// Filter out system tables (those starting with underscore)
|
344
|
+
allCollections = allCollections.filter(
|
345
|
+
(collection) => !collection.$id.startsWith('_')
|
346
|
+
);
|
323
347
|
|
324
348
|
const hasLocalAndRemote =
|
325
349
|
allCollections.some((coll) =>
|
@@ -329,18 +353,60 @@ export class InteractiveCLI {
|
|
329
353
|
(coll) => !configCollections.some((c) => c.name === coll.name)
|
330
354
|
);
|
331
355
|
|
356
|
+
// Enhanced choice display with type indicators
|
332
357
|
const choices = allCollections
|
333
|
-
.sort((a, b) =>
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
})
|
358
|
+
.sort((a, b) => {
|
359
|
+
// Sort by type first (collections before tables), then by name
|
360
|
+
const aIsTable = (a as any)._isFromTablesDir || false;
|
361
|
+
const bIsTable = (b as any)._isFromTablesDir || false;
|
362
|
+
|
363
|
+
if (aIsTable !== bIsTable) {
|
364
|
+
return aIsTable ? 1 : -1; // Collections first, then tables
|
365
|
+
}
|
366
|
+
|
367
|
+
return a.name.localeCompare(b.name);
|
368
|
+
})
|
369
|
+
.map((collection) => {
|
370
|
+
const localCollection = configCollections.find((c) => c.name === collection.name);
|
371
|
+
const isLocal = !!localCollection;
|
372
|
+
const isTable = localCollection?._isFromTablesDir || (collection as any)._isFromTablesDir || false;
|
373
|
+
const sourceFolder = localCollection?._sourceFolder || (collection as any)._sourceFolder || 'collections';
|
374
|
+
|
375
|
+
let typeIndicator = '';
|
376
|
+
let locationIndicator = '';
|
377
|
+
|
378
|
+
// Type indicator
|
379
|
+
if (isTable) {
|
380
|
+
typeIndicator = chalk.cyan('[Table]');
|
381
|
+
} else {
|
382
|
+
typeIndicator = chalk.green('[Collection]');
|
383
|
+
}
|
384
|
+
|
385
|
+
// Location indicator
|
386
|
+
if (hasLocalAndRemote) {
|
387
|
+
if (isLocal) {
|
388
|
+
locationIndicator = chalk.gray(`(Local/${sourceFolder})`);
|
389
|
+
} else {
|
390
|
+
locationIndicator = chalk.gray('(Remote)');
|
391
|
+
}
|
392
|
+
} else if (isLocal) {
|
393
|
+
locationIndicator = chalk.gray(`(${sourceFolder}/)`);
|
394
|
+
}
|
395
|
+
|
396
|
+
// Database indicator for tables with explicit databaseId
|
397
|
+
let dbIndicator = '';
|
398
|
+
if (isTable && collection.databaseId && shouldFilterByDatabase) {
|
399
|
+
const matchesCurrentDb = collection.databaseId === database.$id;
|
400
|
+
if (!matchesCurrentDb) {
|
401
|
+
dbIndicator = chalk.yellow(` [DB: ${collection.databaseId}]`);
|
402
|
+
}
|
403
|
+
}
|
404
|
+
|
405
|
+
return {
|
406
|
+
name: `${typeIndicator} ${collection.name} ${locationIndicator}${dbIndicator}`,
|
407
|
+
value: collection,
|
408
|
+
};
|
409
|
+
});
|
344
410
|
|
345
411
|
const { selectedCollections } = await inquirer.prompt([
|
346
412
|
{
|
@@ -349,13 +415,52 @@ export class InteractiveCLI {
|
|
349
415
|
message: chalk.blue(message),
|
350
416
|
choices,
|
351
417
|
loop: true,
|
352
|
-
pageSize:
|
418
|
+
pageSize: 15, // Increased page size to accommodate additional info
|
353
419
|
},
|
354
420
|
]);
|
355
421
|
|
356
422
|
return selectedCollections;
|
357
423
|
}
|
358
424
|
|
425
|
+
/**
|
426
|
+
* Enhanced collection/table selection with better guidance for mixed scenarios
|
427
|
+
*/
|
428
|
+
private async selectCollectionsAndTables(
|
429
|
+
database: Models.Database,
|
430
|
+
databasesClient: Databases,
|
431
|
+
message: string,
|
432
|
+
multiSelect = true,
|
433
|
+
preferLocal = false,
|
434
|
+
shouldFilterByDatabase = false
|
435
|
+
): Promise<Models.Collection[]> {
|
436
|
+
const configCollections = this.getLocalCollections();
|
437
|
+
const collectionsCount = configCollections.filter(c => !c._isFromTablesDir).length;
|
438
|
+
const tablesCount = configCollections.filter(c => c._isFromTablesDir).length;
|
439
|
+
|
440
|
+
// Provide context about what's available
|
441
|
+
if (collectionsCount > 0 && tablesCount > 0) {
|
442
|
+
MessageFormatter.info(`\n📋 Available items for database "${database.name}":`, { prefix: "Collections" });
|
443
|
+
MessageFormatter.info(` Collections: ${collectionsCount} (from collections/ folder)`, { prefix: "Collections" });
|
444
|
+
MessageFormatter.info(` Tables: ${tablesCount} (from tables/ folder)`, { prefix: "Collections" });
|
445
|
+
|
446
|
+
if (shouldFilterByDatabase) {
|
447
|
+
const filteredTables = configCollections.filter(c =>
|
448
|
+
c._isFromTablesDir && (!c.databaseId || c.databaseId === database.$id)
|
449
|
+
).length;
|
450
|
+
if (filteredTables !== tablesCount) {
|
451
|
+
MessageFormatter.warning(` Note: ${filteredTables}/${tablesCount} tables match this database`, { prefix: "Collections" });
|
452
|
+
}
|
453
|
+
}
|
454
|
+
MessageFormatter.info('', { prefix: "Collections" });
|
455
|
+
} else if (collectionsCount > 0) {
|
456
|
+
MessageFormatter.info(`📁 ${collectionsCount} collections available from collections/ folder\n`, { prefix: "Collections" });
|
457
|
+
} else if (tablesCount > 0) {
|
458
|
+
MessageFormatter.info(`📊 ${tablesCount} tables available from tables/ folder\n`, { prefix: "Collections" });
|
459
|
+
}
|
460
|
+
|
461
|
+
return this.selectCollections(database, databasesClient, message, multiSelect, preferLocal, shouldFilterByDatabase);
|
462
|
+
}
|
463
|
+
|
359
464
|
private getTemplateDefaults(template: string) {
|
360
465
|
const defaults = {
|
361
466
|
"typescript-node": {
|
@@ -364,6 +469,12 @@ export class InteractiveCLI {
|
|
364
469
|
commands: "npm install && npm run build",
|
365
470
|
specification: "s-0.5vcpu-512mb" as Specification,
|
366
471
|
},
|
472
|
+
"hono-typescript": {
|
473
|
+
runtime: "node-21.0" as Runtime,
|
474
|
+
entrypoint: "src/index.ts",
|
475
|
+
commands: "npm install && npm run build",
|
476
|
+
specification: "s-0.5vcpu-512mb" as Specification,
|
477
|
+
},
|
367
478
|
"uv": {
|
368
479
|
runtime: "python-3.12" as Runtime,
|
369
480
|
entrypoint: "src/index.py",
|
@@ -386,115 +497,6 @@ export class InteractiveCLI {
|
|
386
497
|
};
|
387
498
|
}
|
388
499
|
|
389
|
-
private async createFunction(): Promise<void> {
|
390
|
-
const { name } = await inquirer.prompt([
|
391
|
-
{
|
392
|
-
type: "input",
|
393
|
-
name: "name",
|
394
|
-
message: "Function name:",
|
395
|
-
validate: (input) => input.length > 0,
|
396
|
-
},
|
397
|
-
]);
|
398
|
-
|
399
|
-
const { template } = await inquirer.prompt([
|
400
|
-
{
|
401
|
-
type: "list",
|
402
|
-
name: "template",
|
403
|
-
message: "Select a template:",
|
404
|
-
choices: [
|
405
|
-
{ name: "TypeScript Node.js", value: "typescript-node" },
|
406
|
-
{ name: "Python with UV", value: "uv" },
|
407
|
-
{ name: "Count Documents in Collection", value: "count-docs-in-collection" },
|
408
|
-
{ name: "None (Empty Function)", value: "none" },
|
409
|
-
],
|
410
|
-
},
|
411
|
-
]);
|
412
|
-
|
413
|
-
// Get template defaults
|
414
|
-
const templateDefaults = this.getTemplateDefaults(template);
|
415
|
-
|
416
|
-
const { runtime } = await inquirer.prompt([
|
417
|
-
{
|
418
|
-
type: "list",
|
419
|
-
name: "runtime",
|
420
|
-
message: "Select runtime:",
|
421
|
-
choices: Object.values(RuntimeSchema.Values),
|
422
|
-
default: templateDefaults.runtime,
|
423
|
-
},
|
424
|
-
]);
|
425
|
-
|
426
|
-
const specifications = await listSpecifications(
|
427
|
-
this.controller!.appwriteServer!
|
428
|
-
);
|
429
|
-
const { specification } = await inquirer.prompt([
|
430
|
-
{
|
431
|
-
type: "list",
|
432
|
-
name: "specification",
|
433
|
-
message: "Select specification:",
|
434
|
-
choices: [
|
435
|
-
{ name: "None", value: undefined },
|
436
|
-
...specifications.specifications.map((s) => ({
|
437
|
-
name: s.slug,
|
438
|
-
value: s.slug,
|
439
|
-
})),
|
440
|
-
],
|
441
|
-
default: templateDefaults.specification,
|
442
|
-
},
|
443
|
-
]);
|
444
|
-
|
445
|
-
const functionConfig: AppwriteFunction = {
|
446
|
-
$id: ulid(),
|
447
|
-
name,
|
448
|
-
runtime,
|
449
|
-
events: [],
|
450
|
-
execute: ["any"],
|
451
|
-
enabled: true,
|
452
|
-
logging: true,
|
453
|
-
entrypoint: templateDefaults.entrypoint,
|
454
|
-
commands: templateDefaults.commands,
|
455
|
-
specification: specification || templateDefaults.specification,
|
456
|
-
scopes: [],
|
457
|
-
timeout: 15,
|
458
|
-
schedule: "",
|
459
|
-
installationId: "",
|
460
|
-
providerRepositoryId: "",
|
461
|
-
providerBranch: "",
|
462
|
-
providerSilentMode: false,
|
463
|
-
providerRootDirectory: "",
|
464
|
-
templateRepository: "",
|
465
|
-
templateOwner: "",
|
466
|
-
templateRootDirectory: "",
|
467
|
-
};
|
468
|
-
|
469
|
-
if (template !== "none") {
|
470
|
-
await createFunctionTemplate(
|
471
|
-
template as "typescript-node" | "uv" | "count-docs-in-collection",
|
472
|
-
name,
|
473
|
-
"./functions"
|
474
|
-
);
|
475
|
-
}
|
476
|
-
|
477
|
-
// Add to in-memory config
|
478
|
-
if (!this.controller!.config!.functions) {
|
479
|
-
this.controller!.config!.functions = [];
|
480
|
-
}
|
481
|
-
this.controller!.config!.functions.push(functionConfig);
|
482
|
-
|
483
|
-
// If using YAML config, also add to YAML file
|
484
|
-
const yamlConfigPath = findYamlConfig(this.currentDir);
|
485
|
-
if (yamlConfigPath) {
|
486
|
-
try {
|
487
|
-
await addFunctionToYamlConfig(yamlConfigPath, functionConfig);
|
488
|
-
} catch (error) {
|
489
|
-
MessageFormatter.warning(
|
490
|
-
`Function created but failed to update YAML config: ${error instanceof Error ? error.message : error}`,
|
491
|
-
{ prefix: "Functions" }
|
492
|
-
);
|
493
|
-
}
|
494
|
-
}
|
495
|
-
|
496
|
-
MessageFormatter.success("Function created successfully!", { prefix: "Functions" });
|
497
|
-
}
|
498
500
|
|
499
501
|
private async findFunctionInSubdirectories(
|
500
502
|
basePaths: string[],
|
@@ -521,9 +523,7 @@ export class InteractiveCLI {
|
|
521
523
|
try {
|
522
524
|
const stats = await fs.promises.stat(path);
|
523
525
|
if (stats.isDirectory()) {
|
524
|
-
|
525
|
-
chalk.green(`Found function at common location: ${path}`)
|
526
|
-
);
|
526
|
+
MessageFormatter.success(`Found function at common location: ${path}`, { prefix: "Functions" });
|
527
527
|
return path;
|
528
528
|
}
|
529
529
|
} catch (error) {
|
@@ -532,11 +532,7 @@ export class InteractiveCLI {
|
|
532
532
|
}
|
533
533
|
|
534
534
|
// If not found in common locations, do recursive search
|
535
|
-
|
536
|
-
chalk.yellow(
|
537
|
-
"Function not found in common locations, searching subdirectories..."
|
538
|
-
)
|
539
|
-
);
|
535
|
+
MessageFormatter.info("Function not found in common locations, searching subdirectories...", { prefix: "Functions" });
|
540
536
|
|
541
537
|
const queue = [...basePaths];
|
542
538
|
const searched = new Set<string>();
|
@@ -574,7 +570,7 @@ export class InteractiveCLI {
|
|
574
570
|
);
|
575
571
|
|
576
572
|
if (hasMatch) {
|
577
|
-
|
573
|
+
MessageFormatter.success(`Found function at: ${fullPath}`, { prefix: "Functions" });
|
578
574
|
return fullPath;
|
579
575
|
}
|
580
576
|
|
@@ -582,220 +578,13 @@ export class InteractiveCLI {
|
|
582
578
|
}
|
583
579
|
}
|
584
580
|
} catch (error) {
|
585
|
-
|
586
|
-
chalk.yellow(`Error reading directory ${currentPath}:`, error)
|
587
|
-
);
|
581
|
+
MessageFormatter.warning(`Error reading directory ${currentPath}: ${error}`, { prefix: "Functions" });
|
588
582
|
}
|
589
583
|
}
|
590
584
|
|
591
585
|
return null;
|
592
586
|
}
|
593
587
|
|
594
|
-
private async deployFunction(): Promise<void> {
|
595
|
-
await this.initControllerIfNeeded();
|
596
|
-
if (!this.controller?.config) {
|
597
|
-
console.log(chalk.red("Failed to initialize controller or load config"));
|
598
|
-
return;
|
599
|
-
}
|
600
|
-
|
601
|
-
const functions = await this.selectFunctions(
|
602
|
-
"Select function(s) to deploy:",
|
603
|
-
true,
|
604
|
-
true
|
605
|
-
);
|
606
|
-
|
607
|
-
if (!functions?.length) {
|
608
|
-
console.log(chalk.red("No function selected"));
|
609
|
-
return;
|
610
|
-
}
|
611
|
-
|
612
|
-
for (const functionConfig of functions) {
|
613
|
-
if (!functionConfig) {
|
614
|
-
console.log(chalk.red("Invalid function configuration"));
|
615
|
-
return;
|
616
|
-
}
|
617
|
-
|
618
|
-
// Ensure functions array exists
|
619
|
-
if (!this.controller.config.functions) {
|
620
|
-
this.controller.config.functions = [];
|
621
|
-
}
|
622
|
-
|
623
|
-
const functionNameLower = functionConfig.name
|
624
|
-
.toLowerCase()
|
625
|
-
.replace(/\s+/g, "-");
|
626
|
-
|
627
|
-
// Debug logging
|
628
|
-
console.log(chalk.blue(`🔍 Function deployment debug:`));
|
629
|
-
console.log(chalk.gray(` Function name: ${functionConfig.name}`));
|
630
|
-
console.log(chalk.gray(` Function ID: ${functionConfig.$id}`));
|
631
|
-
console.log(chalk.gray(` Config dirPath: ${functionConfig.dirPath || 'undefined'}`));
|
632
|
-
if (functionConfig.dirPath) {
|
633
|
-
const expandedPath = functionConfig.dirPath.startsWith('~/')
|
634
|
-
? functionConfig.dirPath.replace('~', os.homedir())
|
635
|
-
: functionConfig.dirPath;
|
636
|
-
console.log(chalk.gray(` Expanded dirPath: ${expandedPath}`));
|
637
|
-
}
|
638
|
-
console.log(chalk.gray(` Appwrite folder: ${this.controller.getAppwriteFolderPath()}`));
|
639
|
-
console.log(chalk.gray(` Current working dir: ${process.cwd()}`));
|
640
|
-
|
641
|
-
// Helper function to expand tilde in paths
|
642
|
-
const expandTildePath = (path: string): string => {
|
643
|
-
if (path.startsWith('~/')) {
|
644
|
-
return path.replace('~', os.homedir());
|
645
|
-
}
|
646
|
-
return path;
|
647
|
-
};
|
648
|
-
|
649
|
-
// Check locations in priority order:
|
650
|
-
const priorityLocations = [
|
651
|
-
// 1. Config dirPath if specified (with tilde expansion)
|
652
|
-
functionConfig.dirPath ? expandTildePath(functionConfig.dirPath) : undefined,
|
653
|
-
// 2. Appwrite config folder/functions/name
|
654
|
-
join(
|
655
|
-
this.controller.getAppwriteFolderPath()!,
|
656
|
-
"functions",
|
657
|
-
functionNameLower
|
658
|
-
),
|
659
|
-
// 3. Current working directory/functions/name
|
660
|
-
join(process.cwd(), "functions", functionNameLower),
|
661
|
-
// 4. Current working directory/name
|
662
|
-
join(process.cwd(), functionNameLower),
|
663
|
-
].filter((val): val is string => val !== undefined); // Remove undefined entries (in case dirPath is undefined)
|
664
|
-
|
665
|
-
console.log(chalk.blue(`🔍 Priority locations to check:`));
|
666
|
-
priorityLocations.forEach((loc, i) => {
|
667
|
-
console.log(chalk.gray(` ${i + 1}. ${loc}`));
|
668
|
-
});
|
669
|
-
|
670
|
-
let functionPath: string | null = null;
|
671
|
-
|
672
|
-
// Check each priority location
|
673
|
-
for (const location of priorityLocations) {
|
674
|
-
console.log(chalk.gray(` Checking: ${location} - ${fs.existsSync(location) ? 'EXISTS' : 'NOT FOUND'}`));
|
675
|
-
if (fs.existsSync(location)) {
|
676
|
-
console.log(chalk.green(`✅ Found function at: ${location}`));
|
677
|
-
functionPath = location;
|
678
|
-
break;
|
679
|
-
}
|
680
|
-
}
|
681
|
-
|
682
|
-
// If not found in priority locations, do a broader search
|
683
|
-
if (!functionPath) {
|
684
|
-
console.log(
|
685
|
-
chalk.yellow(
|
686
|
-
`Function not found in primary locations, searching subdirectories...`
|
687
|
-
)
|
688
|
-
);
|
689
|
-
|
690
|
-
// Search in both appwrite config directory and current working directory
|
691
|
-
functionPath = await this.findFunctionInSubdirectories(
|
692
|
-
[this.controller.getAppwriteFolderPath()!, process.cwd()],
|
693
|
-
functionNameLower
|
694
|
-
);
|
695
|
-
}
|
696
|
-
|
697
|
-
if (!functionPath) {
|
698
|
-
const { shouldDownload } = await inquirer.prompt([
|
699
|
-
{
|
700
|
-
type: "confirm",
|
701
|
-
name: "shouldDownload",
|
702
|
-
message:
|
703
|
-
"Function not found locally. Would you like to download the latest deployment?",
|
704
|
-
default: false,
|
705
|
-
},
|
706
|
-
]);
|
707
|
-
|
708
|
-
if (shouldDownload) {
|
709
|
-
try {
|
710
|
-
console.log(chalk.blue("Downloading latest deployment..."));
|
711
|
-
const { path: downloadedPath, function: remoteFunction } =
|
712
|
-
await downloadLatestFunctionDeployment(
|
713
|
-
this.controller.appwriteServer!,
|
714
|
-
functionConfig.$id,
|
715
|
-
join(this.controller.getAppwriteFolderPath()!, "functions")
|
716
|
-
);
|
717
|
-
console.log(
|
718
|
-
chalk.green(`✨ Function downloaded to ${downloadedPath}`)
|
719
|
-
);
|
720
|
-
|
721
|
-
functionPath = downloadedPath;
|
722
|
-
functionConfig.dirPath = downloadedPath;
|
723
|
-
|
724
|
-
const existingIndex = this.controller.config.functions.findIndex(
|
725
|
-
(f) => f?.$id === remoteFunction.$id
|
726
|
-
);
|
727
|
-
|
728
|
-
if (existingIndex >= 0) {
|
729
|
-
this.controller.config.functions[existingIndex].dirPath =
|
730
|
-
downloadedPath;
|
731
|
-
}
|
732
|
-
|
733
|
-
await this.controller.reloadConfig();
|
734
|
-
} catch (error) {
|
735
|
-
console.error(
|
736
|
-
chalk.red("Failed to download function deployment:"),
|
737
|
-
error
|
738
|
-
);
|
739
|
-
return;
|
740
|
-
}
|
741
|
-
} else {
|
742
|
-
console.log(
|
743
|
-
chalk.red(
|
744
|
-
`Function ${functionConfig.name} not found locally. Cannot deploy.`
|
745
|
-
)
|
746
|
-
);
|
747
|
-
return;
|
748
|
-
}
|
749
|
-
}
|
750
|
-
|
751
|
-
if (!this.controller.appwriteServer) {
|
752
|
-
console.log(chalk.red("Appwrite server not initialized"));
|
753
|
-
return;
|
754
|
-
}
|
755
|
-
|
756
|
-
try {
|
757
|
-
await deployLocalFunction(
|
758
|
-
this.controller.appwriteServer,
|
759
|
-
functionConfig.name,
|
760
|
-
{
|
761
|
-
...functionConfig,
|
762
|
-
dirPath: functionPath,
|
763
|
-
},
|
764
|
-
functionPath
|
765
|
-
);
|
766
|
-
MessageFormatter.success("Function deployed successfully!", { prefix: "Functions" });
|
767
|
-
} catch (error) {
|
768
|
-
console.error(chalk.red("Failed to deploy function:"), error);
|
769
|
-
}
|
770
|
-
}
|
771
|
-
}
|
772
|
-
|
773
|
-
private async deleteFunction(): Promise<void> {
|
774
|
-
const functions = await this.selectFunctions(
|
775
|
-
"Select functions to delete:",
|
776
|
-
true,
|
777
|
-
false
|
778
|
-
);
|
779
|
-
|
780
|
-
if (!functions.length) {
|
781
|
-
console.log(chalk.red("No functions selected"));
|
782
|
-
return;
|
783
|
-
}
|
784
|
-
|
785
|
-
for (const func of functions) {
|
786
|
-
try {
|
787
|
-
await deleteFunction(this.controller!.appwriteServer!, func.$id);
|
788
|
-
console.log(
|
789
|
-
chalk.green(`✨ Function ${func.name} deleted successfully!`)
|
790
|
-
);
|
791
|
-
} catch (error) {
|
792
|
-
console.error(
|
793
|
-
chalk.red(`Failed to delete function ${func.name}:`),
|
794
|
-
error
|
795
|
-
);
|
796
|
-
}
|
797
|
-
}
|
798
|
-
}
|
799
588
|
|
800
589
|
private async selectFunctions(
|
801
590
|
message: string,
|
@@ -813,7 +602,7 @@ export class InteractiveCLI {
|
|
813
602
|
const allFunctions = [
|
814
603
|
...localFunctions,
|
815
604
|
...remoteFunctions.functions.filter(
|
816
|
-
(rf) => !localFunctions.some((lf) => lf.name === rf.name)
|
605
|
+
(rf: any) => !localFunctions.some((lf) => lf.name === rf.name)
|
817
606
|
),
|
818
607
|
];
|
819
608
|
|
@@ -893,21 +682,6 @@ export class InteractiveCLI {
|
|
893
682
|
return selectedBuckets;
|
894
683
|
}
|
895
684
|
|
896
|
-
private async createCollectionConfig(): Promise<void> {
|
897
|
-
const { collectionName } = await inquirer.prompt([
|
898
|
-
{
|
899
|
-
type: "input",
|
900
|
-
name: "collectionName",
|
901
|
-
message: chalk.blue("Enter the name of the collection:"),
|
902
|
-
validate: (input) =>
|
903
|
-
input.trim() !== "" || "Collection name cannot be empty.",
|
904
|
-
},
|
905
|
-
]);
|
906
|
-
console.log(
|
907
|
-
chalk.green(`Creating collection config file for '${collectionName}'...`)
|
908
|
-
);
|
909
|
-
createEmptyCollection(collectionName);
|
910
|
-
}
|
911
685
|
|
912
686
|
private async configureBuckets(
|
913
687
|
config: AppwriteConfig,
|
@@ -1146,907 +920,113 @@ export class InteractiveCLI {
|
|
1146
920
|
);
|
1147
921
|
}
|
1148
922
|
|
1149
|
-
private async syncDb(): Promise<void> {
|
1150
|
-
console.log(chalk.blue("Pushing local configuration to Appwrite..."));
|
1151
923
|
|
1152
|
-
const databases = await this.selectDatabases(
|
1153
|
-
this.getLocalDatabases(),
|
1154
|
-
chalk.blue("Select local databases to push:"),
|
1155
|
-
true
|
1156
|
-
);
|
1157
924
|
|
1158
|
-
|
1159
|
-
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
925
|
+
private getLocalCollections(): (Models.Collection & {
|
926
|
+
_isFromTablesDir?: boolean;
|
927
|
+
_sourceFolder?: string;
|
928
|
+
databaseId?: string;
|
929
|
+
})[] {
|
930
|
+
const configCollections = this.controller!.config?.collections || [];
|
931
|
+
// @ts-expect-error - appwrite invalid types
|
932
|
+
return configCollections.map((c) => ({
|
933
|
+
$id: c.$id || ulid(),
|
934
|
+
$createdAt: DateTime.now().toISO(),
|
935
|
+
$updatedAt: DateTime.now().toISO(),
|
936
|
+
name: c.name,
|
937
|
+
enabled: c.enabled || true,
|
938
|
+
documentSecurity: c.documentSecurity || false,
|
939
|
+
attributes: c.attributes || [],
|
940
|
+
indexes: c.indexes || [],
|
941
|
+
$permissions: PermissionToAppwritePermission(c.$permissions) || [],
|
942
|
+
databaseId: c.databaseId,
|
943
|
+
_isFromTablesDir: (c as any)._isFromTablesDir || false,
|
944
|
+
_sourceFolder: (c as any)._isFromTablesDir ? 'tables' : 'collections',
|
945
|
+
}));
|
946
|
+
}
|
947
|
+
|
948
|
+
private getLocalDatabases(): Models.Database[] {
|
949
|
+
const configDatabases = this.controller!.config?.databases || [];
|
950
|
+
return configDatabases.map((db) => ({
|
951
|
+
$id: db.$id || ulid(),
|
952
|
+
$createdAt: DateTime.now().toISO(),
|
953
|
+
$updatedAt: DateTime.now().toISO(),
|
954
|
+
name: db.name,
|
955
|
+
enabled: true,
|
956
|
+
}));
|
957
|
+
}
|
958
|
+
|
959
|
+
|
960
|
+
/**
|
961
|
+
* Extract session information from current controller for preservation
|
962
|
+
*/
|
963
|
+
private extractSessionFromController(): {
|
964
|
+
appwriteEndpoint: string;
|
965
|
+
appwriteProject: string;
|
966
|
+
appwriteKey?: string;
|
967
|
+
sessionCookie?: string;
|
968
|
+
sessionMetadata?: any;
|
969
|
+
} | undefined {
|
970
|
+
if (!this.controller?.config) {
|
971
|
+
return undefined;
|
1163
972
|
}
|
1164
973
|
|
1165
|
-
const
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
974
|
+
const sessionInfo = this.controller.getSessionInfo();
|
975
|
+
const config = this.controller.config;
|
976
|
+
|
977
|
+
if (!config.appwriteEndpoint || !config.appwriteProject) {
|
978
|
+
return undefined;
|
979
|
+
}
|
980
|
+
|
981
|
+
const result: any = {
|
982
|
+
appwriteEndpoint: config.appwriteEndpoint,
|
983
|
+
appwriteProject: config.appwriteProject,
|
984
|
+
appwriteKey: config.appwriteKey
|
985
|
+
};
|
986
|
+
|
987
|
+
// Add session data if available
|
988
|
+
if (sessionInfo.hasSession) {
|
989
|
+
result.sessionCookie = (this.controller as any).sessionCookie;
|
990
|
+
result.sessionMetadata = (this.controller as any).sessionMetadata;
|
991
|
+
}
|
992
|
+
|
993
|
+
return result;
|
994
|
+
}
|
1173
995
|
|
1174
|
-
const { syncFunctions } = await inquirer.prompt([
|
1175
|
-
{
|
1176
|
-
type: "confirm",
|
1177
|
-
name: "syncFunctions",
|
1178
|
-
message: "Do you want to push local functions to remote?",
|
1179
|
-
default: false,
|
1180
|
-
},
|
1181
|
-
]);
|
1182
996
|
|
997
|
+
private async detectConfigurationType(): Promise<void> {
|
1183
998
|
try {
|
1184
|
-
//
|
1185
|
-
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
chalk.blue("Select local functions to push:"),
|
1192
|
-
true,
|
1193
|
-
true // prefer local
|
1194
|
-
);
|
999
|
+
// Check for YAML config first
|
1000
|
+
const yamlConfigPath = findYamlConfig(this.currentDir);
|
1001
|
+
if (yamlConfigPath) {
|
1002
|
+
this.isUsingTypeScriptConfig = false;
|
1003
|
+
MessageFormatter.info("Using YAML configuration", { prefix: "Config" });
|
1004
|
+
return;
|
1005
|
+
}
|
1195
1006
|
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
error
|
1206
|
-
);
|
1207
|
-
}
|
1007
|
+
// Then check for TypeScript config
|
1008
|
+
const configPath = findAppwriteConfig(this.currentDir);
|
1009
|
+
if (configPath) {
|
1010
|
+
const tsConfigPath = join(configPath, 'appwriteConfig.ts');
|
1011
|
+
if (fs.existsSync(tsConfigPath)) {
|
1012
|
+
this.isUsingTypeScriptConfig = true;
|
1013
|
+
MessageFormatter.info("TypeScript configuration detected", { prefix: "Config" });
|
1014
|
+
MessageFormatter.info("Consider migrating to YAML for better organization", { prefix: "Config" });
|
1015
|
+
return;
|
1208
1016
|
}
|
1209
1017
|
}
|
1210
1018
|
|
1211
|
-
|
1212
|
-
|
1213
|
-
);
|
1019
|
+
// No config found
|
1020
|
+
this.isUsingTypeScriptConfig = false;
|
1021
|
+
MessageFormatter.info("No configuration file found", { prefix: "Config" });
|
1214
1022
|
} catch (error) {
|
1215
|
-
|
1216
|
-
|
1023
|
+
// Silently handle detection errors and continue
|
1024
|
+
this.isUsingTypeScriptConfig = false;
|
1217
1025
|
}
|
1218
1026
|
}
|
1219
1027
|
|
1220
|
-
private
|
1221
|
-
|
1222
|
-
await this.controller!.init();
|
1223
|
-
|
1224
|
-
// Sync databases, collections, and buckets
|
1225
|
-
const { syncDatabases } = await inquirer.prompt([
|
1226
|
-
{
|
1227
|
-
type: "confirm",
|
1228
|
-
name: "syncDatabases",
|
1229
|
-
message: "Do you want to synchronize databases, collections, and their buckets?",
|
1230
|
-
default: true,
|
1231
|
-
},
|
1232
|
-
]);
|
1233
|
-
|
1234
|
-
if (syncDatabases) {
|
1235
|
-
const remoteDatabases = await fetchAllDatabases(
|
1236
|
-
this.controller!.database!
|
1237
|
-
);
|
1238
|
-
|
1239
|
-
// Use the controller's synchronizeConfigurations method which handles collections properly
|
1240
|
-
console.log(chalk.blue("Pulling collections and generating collection files..."));
|
1241
|
-
await this.controller!.synchronizeConfigurations(remoteDatabases);
|
1242
|
-
|
1243
|
-
// Also configure buckets for any new databases
|
1244
|
-
const localDatabases = this.controller!.config?.databases || [];
|
1245
|
-
const updatedConfig = await this.configureBuckets({
|
1246
|
-
...this.controller!.config!,
|
1247
|
-
databases: [
|
1248
|
-
...localDatabases,
|
1249
|
-
...remoteDatabases.filter(
|
1250
|
-
(rd) => !localDatabases.some((ld) => ld.name === rd.name)
|
1251
|
-
),
|
1252
|
-
],
|
1253
|
-
});
|
1254
|
-
|
1255
|
-
this.controller!.config = updatedConfig;
|
1256
|
-
}
|
1257
|
-
|
1258
|
-
// Then sync functions
|
1259
|
-
const { syncFunctions } = await inquirer.prompt([
|
1260
|
-
{
|
1261
|
-
type: "confirm",
|
1262
|
-
name: "syncFunctions",
|
1263
|
-
message: "Do you want to synchronize functions?",
|
1264
|
-
default: true,
|
1265
|
-
},
|
1266
|
-
]);
|
1267
|
-
|
1268
|
-
if (syncFunctions) {
|
1269
|
-
const remoteFunctions = await this.controller!.listAllFunctions();
|
1270
|
-
const localFunctions = this.controller!.config?.functions || [];
|
1271
|
-
|
1272
|
-
const allFunctions = [
|
1273
|
-
...remoteFunctions,
|
1274
|
-
...localFunctions.filter(
|
1275
|
-
(f) => !remoteFunctions.some((rf) => rf.$id === f.$id)
|
1276
|
-
),
|
1277
|
-
];
|
1278
|
-
|
1279
|
-
for (const func of allFunctions) {
|
1280
|
-
const hasLocal = localFunctions.some((lf) => lf.$id === func.$id);
|
1281
|
-
const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
|
1282
|
-
|
1283
|
-
if (hasLocal && hasRemote) {
|
1284
|
-
// First try to find the function locally
|
1285
|
-
let functionPath = join(
|
1286
|
-
this.controller!.getAppwriteFolderPath()!,
|
1287
|
-
"functions",
|
1288
|
-
func.name
|
1289
|
-
);
|
1290
|
-
|
1291
|
-
if (!fs.existsSync(functionPath)) {
|
1292
|
-
console.log(
|
1293
|
-
chalk.yellow(
|
1294
|
-
`Function not found in primary location, searching subdirectories...`
|
1295
|
-
)
|
1296
|
-
);
|
1297
|
-
const foundPath = await this.findFunctionInSubdirectories(
|
1298
|
-
[this.controller!.getAppwriteFolderPath()!, process.cwd()],
|
1299
|
-
func.name
|
1300
|
-
);
|
1301
|
-
|
1302
|
-
if (foundPath) {
|
1303
|
-
console.log(chalk.green(`Found function at: ${foundPath}`));
|
1304
|
-
functionPath = foundPath;
|
1305
|
-
}
|
1306
|
-
}
|
1307
|
-
|
1308
|
-
const { preference } = await inquirer.prompt([
|
1309
|
-
{
|
1310
|
-
type: "list",
|
1311
|
-
name: "preference",
|
1312
|
-
message: `Function "${func.name}" ${
|
1313
|
-
functionPath ? "found at " + functionPath : "not found locally"
|
1314
|
-
}. What would you like to do?`,
|
1315
|
-
choices: [
|
1316
|
-
...(functionPath
|
1317
|
-
? [
|
1318
|
-
{
|
1319
|
-
name: "Keep local version (deploy to remote)",
|
1320
|
-
value: "local",
|
1321
|
-
},
|
1322
|
-
]
|
1323
|
-
: []),
|
1324
|
-
{ name: "Use remote version (download)", value: "remote" },
|
1325
|
-
{ name: "Update config only", value: "config" },
|
1326
|
-
{ name: "Skip this function", value: "skip" },
|
1327
|
-
],
|
1328
|
-
},
|
1329
|
-
]);
|
1330
|
-
|
1331
|
-
if (preference === "local" && functionPath) {
|
1332
|
-
await this.controller!.deployFunction(func.name);
|
1333
|
-
} else if (preference === "remote") {
|
1334
|
-
await downloadLatestFunctionDeployment(
|
1335
|
-
this.controller!.appwriteServer!,
|
1336
|
-
func.$id,
|
1337
|
-
join(this.controller!.getAppwriteFolderPath()!, "functions")
|
1338
|
-
);
|
1339
|
-
} else if (preference === "config") {
|
1340
|
-
const remoteFunction = await getFunction(
|
1341
|
-
this.controller!.appwriteServer!,
|
1342
|
-
func.$id
|
1343
|
-
);
|
1344
|
-
|
1345
|
-
const newFunction = {
|
1346
|
-
$id: remoteFunction.$id,
|
1347
|
-
name: remoteFunction.name,
|
1348
|
-
runtime: remoteFunction.runtime as Runtime,
|
1349
|
-
execute: remoteFunction.execute || [],
|
1350
|
-
events: remoteFunction.events || [],
|
1351
|
-
schedule: remoteFunction.schedule || "",
|
1352
|
-
timeout: remoteFunction.timeout || 15,
|
1353
|
-
enabled: remoteFunction.enabled !== false,
|
1354
|
-
logging: remoteFunction.logging !== false,
|
1355
|
-
entrypoint: remoteFunction.entrypoint || "src/index.ts",
|
1356
|
-
commands: remoteFunction.commands || "npm install",
|
1357
|
-
scopes: (remoteFunction.scopes || []) as FunctionScope[],
|
1358
|
-
installationId: remoteFunction.installationId,
|
1359
|
-
providerRepositoryId: remoteFunction.providerRepositoryId,
|
1360
|
-
providerBranch: remoteFunction.providerBranch,
|
1361
|
-
providerSilentMode: remoteFunction.providerSilentMode,
|
1362
|
-
providerRootDirectory: remoteFunction.providerRootDirectory,
|
1363
|
-
specification: remoteFunction.specification as Specification,
|
1364
|
-
};
|
1365
|
-
|
1366
|
-
const existingIndex = this.controller!.config!.functions!.findIndex(
|
1367
|
-
(f) => f.$id === remoteFunction.$id
|
1368
|
-
);
|
1369
|
-
|
1370
|
-
if (existingIndex >= 0) {
|
1371
|
-
this.controller!.config!.functions![existingIndex] = newFunction;
|
1372
|
-
} else {
|
1373
|
-
this.controller!.config!.functions!.push(newFunction);
|
1374
|
-
}
|
1375
|
-
console.log(
|
1376
|
-
chalk.green(`Updated config for function: ${func.name}`)
|
1377
|
-
);
|
1378
|
-
}
|
1379
|
-
} else if (hasLocal) {
|
1380
|
-
// Similar check for local-only functions
|
1381
|
-
let functionPath = join(
|
1382
|
-
this.controller!.getAppwriteFolderPath()!,
|
1383
|
-
"functions",
|
1384
|
-
func.name
|
1385
|
-
);
|
1386
|
-
|
1387
|
-
if (!fs.existsSync(functionPath)) {
|
1388
|
-
const foundPath = await this.findFunctionInSubdirectories(
|
1389
|
-
[this.controller!.getAppwriteFolderPath()!, process.cwd()],
|
1390
|
-
func.name
|
1391
|
-
);
|
1392
|
-
|
1393
|
-
if (foundPath) {
|
1394
|
-
functionPath = foundPath;
|
1395
|
-
}
|
1396
|
-
}
|
1397
|
-
|
1398
|
-
const { action } = await inquirer.prompt([
|
1399
|
-
{
|
1400
|
-
type: "list",
|
1401
|
-
name: "action",
|
1402
|
-
message: `Function "${func.name}" ${
|
1403
|
-
functionPath ? "found at " + functionPath : "not found locally"
|
1404
|
-
}. What would you like to do?`,
|
1405
|
-
choices: [
|
1406
|
-
...(functionPath
|
1407
|
-
? [
|
1408
|
-
{
|
1409
|
-
name: "Deploy to remote",
|
1410
|
-
value: "deploy",
|
1411
|
-
},
|
1412
|
-
]
|
1413
|
-
: []),
|
1414
|
-
{ name: "Skip this function", value: "skip" },
|
1415
|
-
],
|
1416
|
-
},
|
1417
|
-
]);
|
1418
|
-
|
1419
|
-
if (action === "deploy" && functionPath) {
|
1420
|
-
await this.controller!.deployFunction(func.name);
|
1421
|
-
}
|
1422
|
-
} else if (hasRemote) {
|
1423
|
-
const { action } = await inquirer.prompt([
|
1424
|
-
{
|
1425
|
-
type: "list",
|
1426
|
-
name: "action",
|
1427
|
-
message: `Function "${func.name}" exists only remotely. What would you like to do?`,
|
1428
|
-
choices: [
|
1429
|
-
{ name: "Update config only", value: "config" },
|
1430
|
-
{ name: "Download locally", value: "download" },
|
1431
|
-
{ name: "Skip this function", value: "skip" },
|
1432
|
-
],
|
1433
|
-
},
|
1434
|
-
]);
|
1435
|
-
|
1436
|
-
if (action === "download") {
|
1437
|
-
await downloadLatestFunctionDeployment(
|
1438
|
-
this.controller!.appwriteServer!,
|
1439
|
-
func.$id,
|
1440
|
-
join(this.controller!.getAppwriteFolderPath()!, "functions")
|
1441
|
-
);
|
1442
|
-
} else if (action === "config") {
|
1443
|
-
const remoteFunction = await getFunction(
|
1444
|
-
this.controller!.appwriteServer!,
|
1445
|
-
func.$id
|
1446
|
-
);
|
1447
|
-
|
1448
|
-
const newFunction = {
|
1449
|
-
$id: remoteFunction.$id,
|
1450
|
-
name: remoteFunction.name,
|
1451
|
-
runtime: remoteFunction.runtime as Runtime,
|
1452
|
-
execute: remoteFunction.execute || [],
|
1453
|
-
events: remoteFunction.events || [],
|
1454
|
-
schedule: remoteFunction.schedule || "",
|
1455
|
-
timeout: remoteFunction.timeout || 15,
|
1456
|
-
enabled: remoteFunction.enabled !== false,
|
1457
|
-
logging: remoteFunction.logging !== false,
|
1458
|
-
entrypoint: remoteFunction.entrypoint || "src/index.ts",
|
1459
|
-
commands: remoteFunction.commands || "npm install",
|
1460
|
-
scopes: (remoteFunction.scopes || []) as FunctionScope[],
|
1461
|
-
installationId: remoteFunction.installationId,
|
1462
|
-
providerRepositoryId: remoteFunction.providerRepositoryId,
|
1463
|
-
providerBranch: remoteFunction.providerBranch,
|
1464
|
-
providerSilentMode: remoteFunction.providerSilentMode,
|
1465
|
-
providerRootDirectory: remoteFunction.providerRootDirectory,
|
1466
|
-
specification: remoteFunction.specification as Specification,
|
1467
|
-
};
|
1468
|
-
|
1469
|
-
this.controller!.config!.functions =
|
1470
|
-
this.controller!.config!.functions || [];
|
1471
|
-
this.controller!.config!.functions.push(newFunction);
|
1472
|
-
console.log(
|
1473
|
-
chalk.green(`Added config for remote function: ${func.name}`)
|
1474
|
-
);
|
1475
|
-
}
|
1476
|
-
}
|
1477
|
-
}
|
1478
|
-
|
1479
|
-
// Schema generation and collection file writing is handled by controller.synchronizeConfigurations()
|
1480
|
-
}
|
1481
|
-
|
1482
|
-
console.log(chalk.green("✨ Configurations synchronized successfully!"));
|
1483
|
-
}
|
1484
|
-
|
1485
|
-
private async backupDatabase(): Promise<void> {
|
1486
|
-
if (!this.controller!.database) {
|
1487
|
-
throw new Error(
|
1488
|
-
"Database is not initialized, is the config file correct & created?"
|
1489
|
-
);
|
1490
|
-
}
|
1491
|
-
const databases = await fetchAllDatabases(this.controller!.database);
|
1492
|
-
|
1493
|
-
const selectedDatabases = await this.selectDatabases(
|
1494
|
-
databases,
|
1495
|
-
"Select databases to backup:"
|
1496
|
-
);
|
1497
|
-
|
1498
|
-
for (const db of selectedDatabases) {
|
1499
|
-
console.log(chalk.yellow(`Backing up database: ${db.name}`));
|
1500
|
-
await this.controller!.backupDatabase(db);
|
1501
|
-
}
|
1502
|
-
MessageFormatter.success("Database backup completed", { prefix: "Backup" });
|
1503
|
-
}
|
1504
|
-
|
1505
|
-
private async wipeDatabase(): Promise<void> {
|
1506
|
-
if (!this.controller!.database || !this.controller!.storage) {
|
1507
|
-
throw new Error(
|
1508
|
-
"Database or Storage is not initialized, is the config file correct & created?"
|
1509
|
-
);
|
1510
|
-
}
|
1511
|
-
const databases = await fetchAllDatabases(this.controller!.database);
|
1512
|
-
const storage = await listBuckets(this.controller!.storage);
|
1513
|
-
|
1514
|
-
const selectedDatabases = await this.selectDatabases(
|
1515
|
-
databases,
|
1516
|
-
"Select databases to wipe:"
|
1517
|
-
);
|
1518
|
-
|
1519
|
-
const { selectedStorage } = await inquirer.prompt([
|
1520
|
-
{
|
1521
|
-
type: "checkbox",
|
1522
|
-
name: "selectedStorage",
|
1523
|
-
message: "Select storage buckets to wipe:",
|
1524
|
-
choices: storage.buckets.map((s) => ({ name: s.name, value: s.$id })),
|
1525
|
-
},
|
1526
|
-
]);
|
1527
|
-
|
1528
|
-
const { wipeUsers } = await inquirer.prompt([
|
1529
|
-
{
|
1530
|
-
type: "confirm",
|
1531
|
-
name: "wipeUsers",
|
1532
|
-
message: "Do you want to wipe users as well?",
|
1533
|
-
default: false,
|
1534
|
-
},
|
1535
|
-
]);
|
1536
|
-
|
1537
|
-
const databaseNames = selectedDatabases.map(db => db.name);
|
1538
|
-
const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, {
|
1539
|
-
includeStorage: selectedStorage.length > 0,
|
1540
|
-
includeUsers: wipeUsers
|
1541
|
-
});
|
1542
|
-
|
1543
|
-
if (confirmed) {
|
1544
|
-
MessageFormatter.info("Starting wipe operation...", { prefix: "Wipe" });
|
1545
|
-
for (const db of selectedDatabases) {
|
1546
|
-
await this.controller!.wipeDatabase(db);
|
1547
|
-
}
|
1548
|
-
for (const bucketId of selectedStorage) {
|
1549
|
-
await this.controller!.wipeDocumentStorage(bucketId);
|
1550
|
-
}
|
1551
|
-
if (wipeUsers) {
|
1552
|
-
await this.controller!.wipeUsers();
|
1553
|
-
}
|
1554
|
-
MessageFormatter.success("Wipe operation completed", { prefix: "Wipe" });
|
1555
|
-
} else {
|
1556
|
-
MessageFormatter.info("Wipe operation cancelled", { prefix: "Wipe" });
|
1557
|
-
}
|
1558
|
-
}
|
1559
|
-
|
1560
|
-
private async wipeCollections(): Promise<void> {
|
1561
|
-
if (!this.controller!.database) {
|
1562
|
-
throw new Error(
|
1563
|
-
"Database is not initialized, is the config file correct & created?"
|
1564
|
-
);
|
1565
|
-
}
|
1566
|
-
const databases = await fetchAllDatabases(this.controller!.database);
|
1567
|
-
const selectedDatabases = await this.selectDatabases(
|
1568
|
-
databases,
|
1569
|
-
"Select the database(s) containing the collections to wipe:",
|
1570
|
-
true
|
1571
|
-
);
|
1572
|
-
|
1573
|
-
for (const database of selectedDatabases) {
|
1574
|
-
const collections = await this.selectCollections(
|
1575
|
-
database,
|
1576
|
-
this.controller!.database,
|
1577
|
-
`Select collections to wipe from ${database.name}:`,
|
1578
|
-
true,
|
1579
|
-
undefined,
|
1580
|
-
true
|
1581
|
-
);
|
1582
|
-
|
1583
|
-
const collectionNames = collections.map(c => c.name);
|
1584
|
-
const confirmed = await ConfirmationDialogs.confirmCollectionWipe(
|
1585
|
-
database.name,
|
1586
|
-
collectionNames
|
1587
|
-
);
|
1588
|
-
|
1589
|
-
if (confirmed) {
|
1590
|
-
MessageFormatter.info(
|
1591
|
-
`Wiping selected collections from ${database.name}...`,
|
1592
|
-
{ prefix: "Wipe" }
|
1593
|
-
);
|
1594
|
-
for (const collection of collections) {
|
1595
|
-
await this.controller!.wipeCollection(database, collection);
|
1596
|
-
MessageFormatter.success(
|
1597
|
-
`Collection ${collection.name} wiped successfully`,
|
1598
|
-
{ prefix: "Wipe" }
|
1599
|
-
);
|
1600
|
-
}
|
1601
|
-
} else {
|
1602
|
-
MessageFormatter.info(
|
1603
|
-
`Wipe operation cancelled for ${database.name}`,
|
1604
|
-
{ prefix: "Wipe" }
|
1605
|
-
);
|
1606
|
-
}
|
1607
|
-
}
|
1608
|
-
MessageFormatter.success("Wipe collections operation completed", { prefix: "Wipe" });
|
1609
|
-
}
|
1610
|
-
|
1611
|
-
private async generateSchemas(): Promise<void> {
|
1612
|
-
console.log(chalk.yellow("Generating schemas..."));
|
1613
|
-
|
1614
|
-
// Prompt user for schema type preference
|
1615
|
-
const { schemaType } = await inquirer.prompt([
|
1616
|
-
{
|
1617
|
-
type: "list",
|
1618
|
-
name: "schemaType",
|
1619
|
-
message: "What type of schemas would you like to generate?",
|
1620
|
-
choices: [
|
1621
|
-
{ name: "TypeScript (Zod) schemas", value: "zod" },
|
1622
|
-
{ name: "JSON schemas", value: "json" },
|
1623
|
-
{ name: "Both TypeScript and JSON schemas", value: "both" },
|
1624
|
-
],
|
1625
|
-
default: "both",
|
1626
|
-
},
|
1627
|
-
]);
|
1628
|
-
|
1629
|
-
// Get the config folder path (where the config file is located)
|
1630
|
-
const configFolderPath = this.controller!.getAppwriteFolderPath();
|
1631
|
-
if (!configFolderPath) {
|
1632
|
-
MessageFormatter.error("Failed to get config folder path", undefined, { prefix: "Schemas" });
|
1633
|
-
return;
|
1634
|
-
}
|
1635
|
-
|
1636
|
-
// Create SchemaGenerator with the correct base path and generate schemas
|
1637
|
-
const schemaGenerator = new SchemaGenerator(this.controller!.config!, configFolderPath);
|
1638
|
-
schemaGenerator.generateSchemas({ format: schemaType, verbose: true });
|
1639
|
-
|
1640
|
-
MessageFormatter.success("Schema generation completed", { prefix: "Schemas" });
|
1641
|
-
}
|
1642
|
-
|
1643
|
-
private async generateConstants(): Promise<void> {
|
1644
|
-
console.log(chalk.yellow("Generating cross-language constants..."));
|
1645
|
-
|
1646
|
-
if (!this.controller?.config) {
|
1647
|
-
MessageFormatter.error("No configuration found", undefined, { prefix: "Constants" });
|
1648
|
-
return;
|
1649
|
-
}
|
1650
|
-
|
1651
|
-
// Prompt for languages
|
1652
|
-
const { languages } = await inquirer.prompt([
|
1653
|
-
{
|
1654
|
-
type: "checkbox",
|
1655
|
-
name: "languages",
|
1656
|
-
message: "Select languages for constants generation:",
|
1657
|
-
choices: [
|
1658
|
-
{ name: "TypeScript", value: "typescript", checked: true },
|
1659
|
-
{ name: "JavaScript", value: "javascript" },
|
1660
|
-
{ name: "Python", value: "python" },
|
1661
|
-
{ name: "PHP", value: "php" },
|
1662
|
-
{ name: "Dart", value: "dart" },
|
1663
|
-
{ name: "JSON", value: "json" },
|
1664
|
-
{ name: "Environment Variables", value: "env" },
|
1665
|
-
],
|
1666
|
-
validate: (input) => {
|
1667
|
-
if (input.length === 0) {
|
1668
|
-
return "Please select at least one language";
|
1669
|
-
}
|
1670
|
-
return true;
|
1671
|
-
},
|
1672
|
-
},
|
1673
|
-
]);
|
1674
|
-
|
1675
|
-
// Determine default output directory based on config location
|
1676
|
-
const configPath = this.controller!.getAppwriteFolderPath();
|
1677
|
-
const defaultOutputDir = configPath
|
1678
|
-
? path.join(configPath, "constants")
|
1679
|
-
: path.join(process.cwd(), "constants");
|
1680
|
-
|
1681
|
-
// Prompt for output directory
|
1682
|
-
const { outputDir } = await inquirer.prompt([
|
1683
|
-
{
|
1684
|
-
type: "input",
|
1685
|
-
name: "outputDir",
|
1686
|
-
message: "Output directory for constants files:",
|
1687
|
-
default: defaultOutputDir,
|
1688
|
-
validate: (input) => {
|
1689
|
-
if (!input.trim()) {
|
1690
|
-
return "Output directory cannot be empty";
|
1691
|
-
}
|
1692
|
-
return true;
|
1693
|
-
},
|
1694
|
-
},
|
1695
|
-
]);
|
1696
|
-
|
1697
|
-
try {
|
1698
|
-
const { ConstantsGenerator } = await import("./utils/constantsGenerator.js");
|
1699
|
-
const generator = new ConstantsGenerator(this.controller.config);
|
1700
|
-
|
1701
|
-
MessageFormatter.info(`Generating constants for: ${languages.join(", ")}`, { prefix: "Constants" });
|
1702
|
-
await generator.generateFiles(languages, outputDir);
|
1703
|
-
|
1704
|
-
MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
|
1705
|
-
} catch (error) {
|
1706
|
-
MessageFormatter.error("Failed to generate constants", error instanceof Error ? error : new Error(String(error)), { prefix: "Constants" });
|
1707
|
-
}
|
1708
|
-
}
|
1709
|
-
|
1710
|
-
private async importData(): Promise<void> {
|
1711
|
-
console.log(chalk.yellow("Importing data..."));
|
1712
|
-
|
1713
|
-
const { doBackup } = await inquirer.prompt([
|
1714
|
-
{
|
1715
|
-
type: "confirm",
|
1716
|
-
name: "doBackup",
|
1717
|
-
message: "Do you want to perform a backup before importing?",
|
1718
|
-
default: true,
|
1719
|
-
},
|
1720
|
-
]);
|
1721
|
-
|
1722
|
-
const databases = await this.selectDatabases(
|
1723
|
-
await fetchAllDatabases(this.controller!.database!),
|
1724
|
-
"Select databases to import data into:",
|
1725
|
-
true
|
1726
|
-
);
|
1727
|
-
|
1728
|
-
const collections = await this.selectCollections(
|
1729
|
-
databases[0],
|
1730
|
-
this.controller!.database!,
|
1731
|
-
"Select collections to import data into (leave empty for all):",
|
1732
|
-
true
|
1733
|
-
);
|
1734
|
-
|
1735
|
-
const { shouldWriteFile } = await inquirer.prompt([
|
1736
|
-
{
|
1737
|
-
type: "confirm",
|
1738
|
-
name: "shouldWriteFile",
|
1739
|
-
message: "Do you want to write the imported data to a file?",
|
1740
|
-
default: false,
|
1741
|
-
},
|
1742
|
-
]);
|
1743
|
-
|
1744
|
-
const options = {
|
1745
|
-
databases,
|
1746
|
-
collections: collections.map((c) => c.name),
|
1747
|
-
doBackup,
|
1748
|
-
importData: true,
|
1749
|
-
shouldWriteFile,
|
1750
|
-
};
|
1751
|
-
|
1752
|
-
try {
|
1753
|
-
await this.controller!.importData(options);
|
1754
|
-
console.log(chalk.green("Data import completed successfully."));
|
1755
|
-
} catch (error) {
|
1756
|
-
console.error(chalk.red("Error importing data:"), error);
|
1757
|
-
}
|
1758
|
-
}
|
1759
|
-
|
1760
|
-
private async transferData(): Promise<void> {
|
1761
|
-
if (!this.controller!.database) {
|
1762
|
-
throw new Error(
|
1763
|
-
"Database is not initialized, is the config file correct & created?"
|
1764
|
-
);
|
1765
|
-
}
|
1766
|
-
|
1767
|
-
const { isRemote } = await inquirer.prompt([
|
1768
|
-
{
|
1769
|
-
type: "confirm",
|
1770
|
-
name: "isRemote",
|
1771
|
-
message: "Is this a remote transfer?",
|
1772
|
-
default: false,
|
1773
|
-
},
|
1774
|
-
]);
|
1775
|
-
|
1776
|
-
let sourceClient = this.controller!.database;
|
1777
|
-
let targetClient: Databases;
|
1778
|
-
let sourceDatabases: Models.Database[];
|
1779
|
-
let targetDatabases: Models.Database[];
|
1780
|
-
let remoteOptions:
|
1781
|
-
| {
|
1782
|
-
transferEndpoint: string;
|
1783
|
-
transferProject: string;
|
1784
|
-
transferKey: string;
|
1785
|
-
}
|
1786
|
-
| undefined;
|
1787
|
-
|
1788
|
-
if (isRemote) {
|
1789
|
-
remoteOptions = await inquirer.prompt([
|
1790
|
-
{
|
1791
|
-
type: "input",
|
1792
|
-
name: "transferEndpoint",
|
1793
|
-
message: "Enter the remote endpoint:",
|
1794
|
-
},
|
1795
|
-
{
|
1796
|
-
type: "input",
|
1797
|
-
name: "transferProject",
|
1798
|
-
message: "Enter the remote project ID:",
|
1799
|
-
},
|
1800
|
-
{
|
1801
|
-
type: "input",
|
1802
|
-
name: "transferKey",
|
1803
|
-
message: "Enter the remote API key:",
|
1804
|
-
},
|
1805
|
-
]);
|
1806
|
-
|
1807
|
-
const remoteClient = getClient(
|
1808
|
-
remoteOptions!.transferEndpoint,
|
1809
|
-
remoteOptions!.transferProject,
|
1810
|
-
remoteOptions!.transferKey
|
1811
|
-
);
|
1812
|
-
targetClient = new Databases(remoteClient);
|
1813
|
-
|
1814
|
-
sourceDatabases = await fetchAllDatabases(sourceClient);
|
1815
|
-
targetDatabases = await fetchAllDatabases(targetClient);
|
1816
|
-
} else {
|
1817
|
-
targetClient = sourceClient;
|
1818
|
-
const allDatabases = await fetchAllDatabases(sourceClient);
|
1819
|
-
sourceDatabases = targetDatabases = allDatabases;
|
1820
|
-
}
|
1821
|
-
|
1822
|
-
const fromDbs = await this.selectDatabases(
|
1823
|
-
sourceDatabases,
|
1824
|
-
"Select the source database:",
|
1825
|
-
false
|
1826
|
-
);
|
1827
|
-
const fromDb = fromDbs[0];
|
1828
|
-
if (!fromDb) {
|
1829
|
-
throw new Error("No source database selected");
|
1830
|
-
}
|
1831
|
-
const availableDbs = targetDatabases.filter((db) => db.$id !== fromDb.$id);
|
1832
|
-
const targetDbs = await this.selectDatabases(
|
1833
|
-
availableDbs,
|
1834
|
-
"Select the target database:",
|
1835
|
-
false
|
1836
|
-
);
|
1837
|
-
const targetDb = targetDbs[0];
|
1838
|
-
if (!targetDb) {
|
1839
|
-
throw new Error("No target database selected");
|
1840
|
-
}
|
1841
|
-
|
1842
|
-
const selectedCollections = await this.selectCollections(
|
1843
|
-
fromDb,
|
1844
|
-
sourceClient,
|
1845
|
-
"Select collections to transfer:",
|
1846
|
-
true,
|
1847
|
-
false // don't prefer local for transfers
|
1848
|
-
);
|
1849
|
-
|
1850
|
-
const { transferStorage } = await inquirer.prompt([
|
1851
|
-
{
|
1852
|
-
type: "confirm",
|
1853
|
-
name: "transferStorage",
|
1854
|
-
message: "Do you want to transfer storage as well?",
|
1855
|
-
default: false,
|
1856
|
-
},
|
1857
|
-
]);
|
1858
|
-
|
1859
|
-
let sourceBucket, targetBucket;
|
1860
|
-
|
1861
|
-
if (transferStorage) {
|
1862
|
-
const sourceStorage = new Storage(this.controller!.appwriteServer!);
|
1863
|
-
const targetStorage = isRemote
|
1864
|
-
? new Storage(
|
1865
|
-
getClient(
|
1866
|
-
remoteOptions!.transferEndpoint,
|
1867
|
-
remoteOptions!.transferProject,
|
1868
|
-
remoteOptions!.transferKey
|
1869
|
-
)
|
1870
|
-
)
|
1871
|
-
: sourceStorage;
|
1872
|
-
|
1873
|
-
const sourceBuckets = await listBuckets(sourceStorage);
|
1874
|
-
const targetBuckets = isRemote
|
1875
|
-
? await listBuckets(targetStorage)
|
1876
|
-
: sourceBuckets;
|
1877
|
-
|
1878
|
-
const sourceBucketPicked = await this.selectBuckets(
|
1879
|
-
sourceBuckets.buckets,
|
1880
|
-
"Select the source bucket:",
|
1881
|
-
false
|
1882
|
-
);
|
1883
|
-
const targetBucketPicked = await this.selectBuckets(
|
1884
|
-
targetBuckets.buckets,
|
1885
|
-
"Select the target bucket:",
|
1886
|
-
false
|
1887
|
-
);
|
1888
|
-
sourceBucket = sourceBucketPicked[0];
|
1889
|
-
targetBucket = targetBucketPicked[0];
|
1890
|
-
}
|
1891
|
-
|
1892
|
-
let transferOptions: TransferOptions = {
|
1893
|
-
fromDb,
|
1894
|
-
targetDb,
|
1895
|
-
isRemote,
|
1896
|
-
collections:
|
1897
|
-
selectedCollections.length > 0
|
1898
|
-
? selectedCollections.map((c) => c.$id)
|
1899
|
-
: undefined,
|
1900
|
-
sourceBucket,
|
1901
|
-
targetBucket,
|
1902
|
-
};
|
1903
|
-
|
1904
|
-
if (isRemote && remoteOptions) {
|
1905
|
-
transferOptions = {
|
1906
|
-
...transferOptions,
|
1907
|
-
...remoteOptions,
|
1908
|
-
};
|
1909
|
-
}
|
1910
|
-
|
1911
|
-
console.log(chalk.yellow("Transferring data..."));
|
1912
|
-
await this.controller!.transferData(transferOptions);
|
1913
|
-
console.log(chalk.green("Data transfer completed."));
|
1914
|
-
}
|
1915
|
-
|
1916
|
-
private getLocalCollections(): Models.Collection[] {
|
1917
|
-
const configCollections = this.controller!.config?.collections || [];
|
1918
|
-
// @ts-expect-error - appwrite invalid types
|
1919
|
-
return configCollections.map((c) => ({
|
1920
|
-
$id: c.$id || ulid(),
|
1921
|
-
$createdAt: DateTime.now().toISO(),
|
1922
|
-
$updatedAt: DateTime.now().toISO(),
|
1923
|
-
name: c.name,
|
1924
|
-
enabled: c.enabled || true,
|
1925
|
-
documentSecurity: c.documentSecurity || false,
|
1926
|
-
attributes: c.attributes || [],
|
1927
|
-
indexes: c.indexes || [],
|
1928
|
-
$permissions: PermissionToAppwritePermission(c.$permissions) || [],
|
1929
|
-
databaseId: c.databaseId!,
|
1930
|
-
}));
|
1931
|
-
}
|
1932
|
-
|
1933
|
-
private getLocalDatabases(): Models.Database[] {
|
1934
|
-
const configDatabases = this.controller!.config?.databases || [];
|
1935
|
-
return configDatabases.map((db) => ({
|
1936
|
-
$id: db.$id || ulid(),
|
1937
|
-
$createdAt: DateTime.now().toISO(),
|
1938
|
-
$updatedAt: DateTime.now().toISO(),
|
1939
|
-
name: db.name,
|
1940
|
-
enabled: true,
|
1941
|
-
}));
|
1942
|
-
}
|
1943
|
-
|
1944
|
-
private async reloadConfig(): Promise<void> {
|
1945
|
-
MessageFormatter.progress("Reloading configuration files...", { prefix: "Config" });
|
1946
|
-
try {
|
1947
|
-
await this.controller!.reloadConfig();
|
1948
|
-
MessageFormatter.success("Configuration files reloaded successfully", { prefix: "Config" });
|
1949
|
-
} catch (error) {
|
1950
|
-
MessageFormatter.error("Failed to reload configuration files", error instanceof Error ? error : new Error(String(error)), { prefix: "Config" });
|
1951
|
-
}
|
1952
|
-
}
|
1953
|
-
|
1954
|
-
private async updateFunctionSpec(): Promise<void> {
|
1955
|
-
const remoteFunctions = await listFunctions(
|
1956
|
-
this.controller!.appwriteServer!,
|
1957
|
-
[Query.limit(1000)]
|
1958
|
-
);
|
1959
|
-
const localFunctions = this.getLocalFunctions();
|
1960
|
-
|
1961
|
-
const allFunctions = [
|
1962
|
-
...remoteFunctions.functions,
|
1963
|
-
...localFunctions.filter(
|
1964
|
-
(f) => !remoteFunctions.functions.some((rf) => rf.name === f.name)
|
1965
|
-
),
|
1966
|
-
];
|
1967
|
-
|
1968
|
-
const functionsToUpdate = await inquirer.prompt([
|
1969
|
-
{
|
1970
|
-
type: "checkbox",
|
1971
|
-
name: "functionId",
|
1972
|
-
message: "Select functions to update:",
|
1973
|
-
choices: allFunctions.map((f) => ({
|
1974
|
-
name: `${f.name} (${f.$id})${
|
1975
|
-
localFunctions.some((lf) => lf.name === f.name)
|
1976
|
-
? " (Local)"
|
1977
|
-
: " (Remote)"
|
1978
|
-
}`,
|
1979
|
-
value: f.$id,
|
1980
|
-
})),
|
1981
|
-
loop: true,
|
1982
|
-
},
|
1983
|
-
]);
|
1984
|
-
|
1985
|
-
const specifications = await listSpecifications(
|
1986
|
-
this.controller!.appwriteServer!
|
1987
|
-
);
|
1988
|
-
const { specification } = await inquirer.prompt([
|
1989
|
-
{
|
1990
|
-
type: "list",
|
1991
|
-
name: "specification",
|
1992
|
-
message: "Select new specification:",
|
1993
|
-
choices: specifications.specifications.map((s) => ({
|
1994
|
-
name: `${s.slug}`,
|
1995
|
-
value: s.slug,
|
1996
|
-
})),
|
1997
|
-
},
|
1998
|
-
]);
|
1999
|
-
|
2000
|
-
try {
|
2001
|
-
for (const functionId of functionsToUpdate.functionId) {
|
2002
|
-
await this.controller!.updateFunctionSpecifications(
|
2003
|
-
functionId,
|
2004
|
-
specification
|
2005
|
-
);
|
2006
|
-
console.log(
|
2007
|
-
chalk.green(
|
2008
|
-
`Successfully updated function specification to ${specification}`
|
2009
|
-
)
|
2010
|
-
);
|
2011
|
-
}
|
2012
|
-
} catch (error) {
|
2013
|
-
console.error(chalk.red("Error updating function specification:"), error);
|
2014
|
-
}
|
2015
|
-
}
|
2016
|
-
|
2017
|
-
private async detectConfigurationType(): Promise<void> {
|
2018
|
-
try {
|
2019
|
-
// Check for YAML config first
|
2020
|
-
const yamlConfigPath = findYamlConfig(this.currentDir);
|
2021
|
-
if (yamlConfigPath) {
|
2022
|
-
this.isUsingTypeScriptConfig = false;
|
2023
|
-
MessageFormatter.info("Using YAML configuration", { prefix: "Config" });
|
2024
|
-
return;
|
2025
|
-
}
|
2026
|
-
|
2027
|
-
// Then check for TypeScript config
|
2028
|
-
const configPath = findAppwriteConfig(this.currentDir);
|
2029
|
-
if (configPath) {
|
2030
|
-
const tsConfigPath = join(configPath, 'appwriteConfig.ts');
|
2031
|
-
if (fs.existsSync(tsConfigPath)) {
|
2032
|
-
this.isUsingTypeScriptConfig = true;
|
2033
|
-
MessageFormatter.info("TypeScript configuration detected", { prefix: "Config" });
|
2034
|
-
MessageFormatter.info("Consider migrating to YAML for better organization", { prefix: "Config" });
|
2035
|
-
return;
|
2036
|
-
}
|
2037
|
-
}
|
2038
|
-
|
2039
|
-
// No config found
|
2040
|
-
this.isUsingTypeScriptConfig = false;
|
2041
|
-
MessageFormatter.info("No configuration file found", { prefix: "Config" });
|
2042
|
-
} catch (error) {
|
2043
|
-
// Silently handle detection errors and continue
|
2044
|
-
this.isUsingTypeScriptConfig = false;
|
2045
|
-
}
|
2046
|
-
}
|
2047
|
-
|
2048
|
-
private buildChoicesList(): string[] {
|
2049
|
-
const allChoices = Object.values(CHOICES);
|
1028
|
+
private buildChoicesList(): string[] {
|
1029
|
+
const allChoices = Object.values(CHOICES);
|
2050
1030
|
|
2051
1031
|
if (this.isUsingTypeScriptConfig) {
|
2052
1032
|
// Place migration option at the top when TS config is detected
|
@@ -2060,302 +1040,4 @@ export class InteractiveCLI {
|
|
2060
1040
|
}
|
2061
1041
|
}
|
2062
1042
|
|
2063
|
-
private async migrateTypeScriptConfig(): Promise<void> {
|
2064
|
-
try {
|
2065
|
-
MessageFormatter.info("Starting TypeScript to YAML configuration migration...", { prefix: "Migration" });
|
2066
|
-
|
2067
|
-
// Perform the migration
|
2068
|
-
await migrateConfig(this.currentDir);
|
2069
|
-
|
2070
|
-
// Reset the detection flag
|
2071
|
-
this.isUsingTypeScriptConfig = false;
|
2072
|
-
|
2073
|
-
// Reset the controller to pick up the new config
|
2074
|
-
this.controller = undefined;
|
2075
|
-
|
2076
|
-
MessageFormatter.success("Migration completed successfully!", { prefix: "Migration" });
|
2077
|
-
MessageFormatter.info("Your configuration has been migrated to the .appwrite directory structure", { prefix: "Migration" });
|
2078
|
-
MessageFormatter.info("You can now use YAML configuration for easier management", { prefix: "Migration" });
|
2079
|
-
|
2080
|
-
} catch (error) {
|
2081
|
-
MessageFormatter.error("Migration failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" });
|
2082
|
-
}
|
2083
|
-
}
|
2084
|
-
|
2085
|
-
private async comprehensiveTransfer(): Promise<void> {
|
2086
|
-
MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
|
2087
|
-
|
2088
|
-
try {
|
2089
|
-
// Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)
|
2090
|
-
await this.initControllerIfNeeded();
|
2091
|
-
|
2092
|
-
// Check if user has an appwrite config for easier setup
|
2093
|
-
const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
|
2094
|
-
this.controller?.config?.appwriteProject &&
|
2095
|
-
this.controller?.config?.appwriteKey;
|
2096
|
-
|
2097
|
-
let sourceConfig: any;
|
2098
|
-
let targetConfig: any;
|
2099
|
-
|
2100
|
-
if (hasAppwriteConfig) {
|
2101
|
-
// Offer to use existing config for source
|
2102
|
-
const { useConfigForSource } = await inquirer.prompt([
|
2103
|
-
{
|
2104
|
-
type: "confirm",
|
2105
|
-
name: "useConfigForSource",
|
2106
|
-
message: "Use your current appwriteConfig as the source?",
|
2107
|
-
default: true,
|
2108
|
-
},
|
2109
|
-
]);
|
2110
|
-
|
2111
|
-
if (useConfigForSource) {
|
2112
|
-
sourceConfig = {
|
2113
|
-
sourceEndpoint: this.controller!.config!.appwriteEndpoint,
|
2114
|
-
sourceProject: this.controller!.config!.appwriteProject,
|
2115
|
-
sourceKey: this.controller!.config!.appwriteKey,
|
2116
|
-
};
|
2117
|
-
MessageFormatter.info(`Using config source: ${sourceConfig.sourceEndpoint}`, { prefix: "Transfer" });
|
2118
|
-
} else {
|
2119
|
-
// Get source configuration manually
|
2120
|
-
sourceConfig = await inquirer.prompt([
|
2121
|
-
{
|
2122
|
-
type: "input",
|
2123
|
-
name: "sourceEndpoint",
|
2124
|
-
message: "Enter the source Appwrite endpoint:",
|
2125
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
2126
|
-
},
|
2127
|
-
{
|
2128
|
-
type: "input",
|
2129
|
-
name: "sourceProject",
|
2130
|
-
message: "Enter the source project ID:",
|
2131
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
2132
|
-
},
|
2133
|
-
{
|
2134
|
-
type: "password",
|
2135
|
-
name: "sourceKey",
|
2136
|
-
message: "Enter the source API key:",
|
2137
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
2138
|
-
},
|
2139
|
-
]);
|
2140
|
-
}
|
2141
|
-
|
2142
|
-
// Offer to use existing config for target
|
2143
|
-
const { useConfigForTarget } = await inquirer.prompt([
|
2144
|
-
{
|
2145
|
-
type: "confirm",
|
2146
|
-
name: "useConfigForTarget",
|
2147
|
-
message: "Use your current appwriteConfig as the target?",
|
2148
|
-
default: false,
|
2149
|
-
},
|
2150
|
-
]);
|
2151
|
-
|
2152
|
-
if (useConfigForTarget) {
|
2153
|
-
targetConfig = {
|
2154
|
-
targetEndpoint: this.controller!.config!.appwriteEndpoint,
|
2155
|
-
targetProject: this.controller!.config!.appwriteProject,
|
2156
|
-
targetKey: this.controller!.config!.appwriteKey,
|
2157
|
-
};
|
2158
|
-
MessageFormatter.info(`Using config target: ${targetConfig.targetEndpoint}`, { prefix: "Transfer" });
|
2159
|
-
} else {
|
2160
|
-
// Get target configuration manually
|
2161
|
-
targetConfig = await inquirer.prompt([
|
2162
|
-
{
|
2163
|
-
type: "input",
|
2164
|
-
name: "targetEndpoint",
|
2165
|
-
message: "Enter the target Appwrite endpoint:",
|
2166
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
2167
|
-
},
|
2168
|
-
{
|
2169
|
-
type: "input",
|
2170
|
-
name: "targetProject",
|
2171
|
-
message: "Enter the target project ID:",
|
2172
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
2173
|
-
},
|
2174
|
-
{
|
2175
|
-
type: "password",
|
2176
|
-
name: "targetKey",
|
2177
|
-
message: "Enter the target API key:",
|
2178
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
2179
|
-
},
|
2180
|
-
]);
|
2181
|
-
}
|
2182
|
-
} else {
|
2183
|
-
// No appwrite config found, get both configurations manually
|
2184
|
-
MessageFormatter.info("No appwriteConfig found, please enter source and target configurations manually", { prefix: "Transfer" });
|
2185
|
-
|
2186
|
-
// Get source configuration
|
2187
|
-
sourceConfig = await inquirer.prompt([
|
2188
|
-
{
|
2189
|
-
type: "input",
|
2190
|
-
name: "sourceEndpoint",
|
2191
|
-
message: "Enter the source Appwrite endpoint:",
|
2192
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
2193
|
-
},
|
2194
|
-
{
|
2195
|
-
type: "input",
|
2196
|
-
name: "sourceProject",
|
2197
|
-
message: "Enter the source project ID:",
|
2198
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
2199
|
-
},
|
2200
|
-
{
|
2201
|
-
type: "password",
|
2202
|
-
name: "sourceKey",
|
2203
|
-
message: "Enter the source API key:",
|
2204
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
2205
|
-
},
|
2206
|
-
]);
|
2207
|
-
|
2208
|
-
// Get target configuration
|
2209
|
-
targetConfig = await inquirer.prompt([
|
2210
|
-
{
|
2211
|
-
type: "input",
|
2212
|
-
name: "targetEndpoint",
|
2213
|
-
message: "Enter the target Appwrite endpoint:",
|
2214
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
2215
|
-
},
|
2216
|
-
{
|
2217
|
-
type: "input",
|
2218
|
-
name: "targetProject",
|
2219
|
-
message: "Enter the target project ID:",
|
2220
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
2221
|
-
},
|
2222
|
-
{
|
2223
|
-
type: "password",
|
2224
|
-
name: "targetKey",
|
2225
|
-
message: "Enter the target API key:",
|
2226
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
2227
|
-
},
|
2228
|
-
]);
|
2229
|
-
}
|
2230
|
-
|
2231
|
-
// Get transfer options
|
2232
|
-
const transferOptions = await inquirer.prompt([
|
2233
|
-
{
|
2234
|
-
type: "checkbox",
|
2235
|
-
name: "transferTypes",
|
2236
|
-
message: "Select what to transfer:",
|
2237
|
-
choices: [
|
2238
|
-
{ name: "👥 Users", value: "users", checked: true },
|
2239
|
-
{ name: "👥 Teams", value: "teams", checked: true },
|
2240
|
-
{ name: "🗄️ Databases", value: "databases", checked: true },
|
2241
|
-
{ name: "📦 Storage Buckets", value: "buckets", checked: true },
|
2242
|
-
{ name: "⚡ Functions", value: "functions", checked: true },
|
2243
|
-
],
|
2244
|
-
validate: (input) => input.length > 0 || "Select at least one transfer type",
|
2245
|
-
},
|
2246
|
-
{
|
2247
|
-
type: "list",
|
2248
|
-
name: "concurrencyLimit",
|
2249
|
-
message: "Select concurrency limit:",
|
2250
|
-
choices: [
|
2251
|
-
{ name: "5 (Conservative) - Users: 2, Files: 1", value: 5 },
|
2252
|
-
{ name: "10 (Balanced) - Users: 5, Files: 2", value: 10 },
|
2253
|
-
{ name: "15 - Users: 7, Files: 3", value: 15 },
|
2254
|
-
{ name: "20 - Users: 10, Files: 5", value: 20 },
|
2255
|
-
{ name: "25 - Users: 12, Files: 6", value: 25 },
|
2256
|
-
{ name: "30 - Users: 15, Files: 7", value: 30 },
|
2257
|
-
{ name: "35 - Users: 17, Files: 8", value: 35 },
|
2258
|
-
{ name: "40 - Users: 20, Files: 10", value: 40 },
|
2259
|
-
{ name: "45 - Users: 22, Files: 11", value: 45 },
|
2260
|
-
{ name: "50 - Users: 25, Files: 12", value: 50 },
|
2261
|
-
{ name: "55 - Users: 27, Files: 13", value: 55 },
|
2262
|
-
{ name: "60 - Users: 30, Files: 15", value: 60 },
|
2263
|
-
{ name: "65 - Users: 32, Files: 16", value: 65 },
|
2264
|
-
{ name: "70 - Users: 35, Files: 17", value: 70 },
|
2265
|
-
{ name: "75 - Users: 37, Files: 18", value: 75 },
|
2266
|
-
{ name: "80 - Users: 40, Files: 20", value: 80 },
|
2267
|
-
{ name: "85 - Users: 42, Files: 21", value: 85 },
|
2268
|
-
{ name: "90 - Users: 45, Files: 22", value: 90 },
|
2269
|
-
{ name: "95 - Users: 47, Files: 23", value: 95 },
|
2270
|
-
{ name: "100 (Aggressive) - Users: 50, Files: 25", value: 100 },
|
2271
|
-
],
|
2272
|
-
default: 10,
|
2273
|
-
},
|
2274
|
-
{
|
2275
|
-
type: "confirm",
|
2276
|
-
name: "dryRun",
|
2277
|
-
message: "Run in dry-run mode (no actual changes)?",
|
2278
|
-
default: false,
|
2279
|
-
},
|
2280
|
-
]);
|
2281
|
-
|
2282
|
-
// Confirmation
|
2283
|
-
const { confirmed } = await inquirer.prompt([
|
2284
|
-
{
|
2285
|
-
type: "confirm",
|
2286
|
-
name: "confirmed",
|
2287
|
-
message: `Are you sure you want to ${transferOptions.dryRun ? "dry-run" : "perform"} comprehensive transfer from ${sourceConfig.sourceEndpoint} to ${targetConfig.targetEndpoint}?`,
|
2288
|
-
default: false,
|
2289
|
-
},
|
2290
|
-
]);
|
2291
|
-
|
2292
|
-
if (!confirmed) {
|
2293
|
-
MessageFormatter.info("Transfer cancelled by user", { prefix: "Transfer" });
|
2294
|
-
return;
|
2295
|
-
}
|
2296
|
-
|
2297
|
-
// Password preservation information
|
2298
|
-
if (transferOptions.transferTypes.includes("users") && !transferOptions.dryRun) {
|
2299
|
-
MessageFormatter.info("User Password Transfer Information:", { prefix: "Transfer" });
|
2300
|
-
MessageFormatter.info("✅ Users with hashed passwords (Argon2, Bcrypt, Scrypt, MD5, SHA, PHPass) will preserve their passwords", { prefix: "Transfer" });
|
2301
|
-
MessageFormatter.info("⚠️ Users without hash information will receive temporary passwords and need to reset", { prefix: "Transfer" });
|
2302
|
-
MessageFormatter.info("🔒 All user data (preferences, labels, verification status) will be preserved", { prefix: "Transfer" });
|
2303
|
-
|
2304
|
-
const { continueWithUsers } = await inquirer.prompt([
|
2305
|
-
{
|
2306
|
-
type: "confirm",
|
2307
|
-
name: "continueWithUsers",
|
2308
|
-
message: "Continue with user transfer?",
|
2309
|
-
default: true,
|
2310
|
-
},
|
2311
|
-
]);
|
2312
|
-
|
2313
|
-
if (!continueWithUsers) {
|
2314
|
-
// Remove users from transfer types
|
2315
|
-
transferOptions.transferTypes = transferOptions.transferTypes.filter((type: string) => type !== "users");
|
2316
|
-
if (transferOptions.transferTypes.length === 0) {
|
2317
|
-
MessageFormatter.info("No transfer types selected, cancelling", { prefix: "Transfer" });
|
2318
|
-
return;
|
2319
|
-
}
|
2320
|
-
}
|
2321
|
-
}
|
2322
|
-
|
2323
|
-
// Execute comprehensive transfer
|
2324
|
-
const comprehensiveTransferOptions: ComprehensiveTransferOptions = {
|
2325
|
-
sourceEndpoint: sourceConfig.sourceEndpoint,
|
2326
|
-
sourceProject: sourceConfig.sourceProject,
|
2327
|
-
sourceKey: sourceConfig.sourceKey,
|
2328
|
-
targetEndpoint: targetConfig.targetEndpoint,
|
2329
|
-
targetProject: targetConfig.targetProject,
|
2330
|
-
targetKey: targetConfig.targetKey,
|
2331
|
-
transferUsers: transferOptions.transferTypes.includes("users"),
|
2332
|
-
transferTeams: transferOptions.transferTypes.includes("teams"),
|
2333
|
-
transferDatabases: transferOptions.transferTypes.includes("databases"),
|
2334
|
-
transferBuckets: transferOptions.transferTypes.includes("buckets"),
|
2335
|
-
transferFunctions: transferOptions.transferTypes.includes("functions"),
|
2336
|
-
concurrencyLimit: transferOptions.concurrencyLimit,
|
2337
|
-
dryRun: transferOptions.dryRun,
|
2338
|
-
};
|
2339
|
-
|
2340
|
-
const transfer = new ComprehensiveTransfer(comprehensiveTransferOptions);
|
2341
|
-
const results = await transfer.execute();
|
2342
|
-
|
2343
|
-
// Display results
|
2344
|
-
if (transferOptions.dryRun) {
|
2345
|
-
MessageFormatter.success("Dry run completed successfully!", { prefix: "Transfer" });
|
2346
|
-
} else {
|
2347
|
-
MessageFormatter.success("Comprehensive transfer completed!", { prefix: "Transfer" });
|
2348
|
-
if (transferOptions.transferTypes.includes("users") && results.users.transferred > 0) {
|
2349
|
-
MessageFormatter.info("Users with preserved password hashes can log in with their original passwords", { prefix: "Transfer" });
|
2350
|
-
MessageFormatter.info("Users with temporary passwords will need to reset their passwords", { prefix: "Transfer" });
|
2351
|
-
}
|
2352
|
-
if (transferOptions.transferTypes.includes("teams") && results.teams.transferred > 0) {
|
2353
|
-
MessageFormatter.info("Team memberships have been transferred and may require user acceptance of invitations", { prefix: "Transfer" });
|
2354
|
-
}
|
2355
|
-
}
|
2356
|
-
|
2357
|
-
} catch (error) {
|
2358
|
-
MessageFormatter.error("Comprehensive transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
2359
|
-
}
|
2360
|
-
}
|
2361
1043
|
}
|