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/dist/interactiveCLI.js
CHANGED
@@ -1,31 +1,31 @@
|
|
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 { Databases, Storage, Client, Compression, Query, Functions, } from "node-appwrite";
|
8
|
-
import {
|
9
|
-
import { ComprehensiveTransfer } from "./migrations/comprehensiveTransfer.js";
|
10
|
-
import { AppwriteFunctionSchema, parseAttribute, PermissionToAppwritePermission, RuntimeSchema, permissionSchema, } from "appwrite-utils";
|
6
|
+
import { PermissionToAppwritePermission, RuntimeSchema, permissionSchema, } from "appwrite-utils";
|
11
7
|
import { ulid } from "ulidx";
|
12
8
|
import chalk from "chalk";
|
13
9
|
import { DateTime } from "luxon";
|
14
|
-
import {
|
15
|
-
import { deployLocalFunction } from "./functions/deployments.js";
|
10
|
+
import { getFunction, downloadLatestFunctionDeployment, listFunctions, } from "./functions/methods.js";
|
16
11
|
import { join } from "node:path";
|
17
12
|
import path from "path";
|
18
13
|
import fs from "node:fs";
|
19
14
|
import os from "node:os";
|
20
|
-
import { SchemaGenerator } from "./shared/schemaGenerator.js";
|
21
|
-
import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
|
22
15
|
import { MessageFormatter } from "./shared/messageFormatter.js";
|
23
|
-
import { migrateConfig } from "./utils/configMigration.js";
|
24
16
|
import { findAppwriteConfig } from "./utils/loadConfigs.js";
|
25
|
-
import { findYamlConfig
|
17
|
+
import { findYamlConfig } from "./config/yamlConfig.js";
|
18
|
+
// Import command modules
|
19
|
+
import { configCommands } from "./cli/commands/configCommands.js";
|
20
|
+
import { databaseCommands } from "./cli/commands/databaseCommands.js";
|
21
|
+
import { functionCommands } from "./cli/commands/functionCommands.js";
|
22
|
+
import { transferCommands } from "./cli/commands/transferCommands.js";
|
23
|
+
import { schemaCommands } from "./cli/commands/schemaCommands.js";
|
26
24
|
var CHOICES;
|
27
25
|
(function (CHOICES) {
|
28
26
|
CHOICES["MIGRATE_CONFIG"] = "\uD83D\uDD04 Migrate TypeScript config to YAML (.appwrite structure)";
|
27
|
+
CHOICES["VALIDATE_CONFIG"] = "\u2705 Validate configuration (collections/tables conflicts)";
|
28
|
+
CHOICES["MIGRATE_COLLECTIONS_TO_TABLES"] = "\uD83D\uDD00 Migrate collections to tables format";
|
29
29
|
CHOICES["CREATE_COLLECTION_CONFIG"] = "\uD83D\uDCC4 Create collection config file";
|
30
30
|
CHOICES["CREATE_FUNCTION"] = "\u26A1 Create a new function, from scratch or using a template";
|
31
31
|
CHOICES["DEPLOY_FUNCTION"] = "\uD83D\uDE80 Deploy function(s)";
|
@@ -77,75 +77,80 @@ export class InteractiveCLI {
|
|
77
77
|
]);
|
78
78
|
switch (action) {
|
79
79
|
case CHOICES.MIGRATE_CONFIG:
|
80
|
-
await
|
80
|
+
await configCommands.migrateTypeScriptConfig(this);
|
81
|
+
break;
|
82
|
+
case CHOICES.VALIDATE_CONFIG:
|
83
|
+
await configCommands.validateConfiguration(this);
|
84
|
+
break;
|
85
|
+
case CHOICES.MIGRATE_COLLECTIONS_TO_TABLES:
|
86
|
+
await configCommands.migrateCollectionsToTables(this);
|
81
87
|
break;
|
82
88
|
case CHOICES.CREATE_COLLECTION_CONFIG:
|
83
|
-
await
|
89
|
+
await configCommands.createCollectionConfig(this);
|
84
90
|
break;
|
85
91
|
case CHOICES.CREATE_FUNCTION:
|
86
92
|
await this.initControllerIfNeeded();
|
87
|
-
await
|
93
|
+
await functionCommands.createFunction(this);
|
88
94
|
break;
|
89
95
|
case CHOICES.DEPLOY_FUNCTION:
|
90
96
|
await this.initControllerIfNeeded();
|
91
|
-
await
|
97
|
+
await functionCommands.deployFunction(this);
|
92
98
|
break;
|
93
99
|
case CHOICES.DELETE_FUNCTION:
|
94
100
|
await this.initControllerIfNeeded();
|
95
|
-
await
|
101
|
+
await functionCommands.deleteFunction(this);
|
96
102
|
break;
|
97
103
|
case CHOICES.SETUP_DIRS_FILES:
|
98
|
-
await setupDirsFiles(
|
104
|
+
await schemaCommands.setupDirsFiles(this, false);
|
99
105
|
break;
|
100
106
|
case CHOICES.SETUP_DIRS_FILES_WITH_EXAMPLE_DATA:
|
101
|
-
await setupDirsFiles(
|
107
|
+
await schemaCommands.setupDirsFiles(this, true);
|
102
108
|
break;
|
103
109
|
case CHOICES.SYNCHRONIZE_CONFIGURATIONS:
|
104
110
|
await this.initControllerIfNeeded();
|
105
|
-
await
|
111
|
+
await databaseCommands.synchronizeConfigurations(this);
|
106
112
|
break;
|
107
113
|
case CHOICES.SYNC_DB:
|
108
114
|
await this.initControllerIfNeeded();
|
109
|
-
await
|
115
|
+
await databaseCommands.syncDb(this);
|
110
116
|
break;
|
111
117
|
case CHOICES.TRANSFER_DATA:
|
112
118
|
await this.initControllerIfNeeded();
|
113
|
-
await
|
119
|
+
await transferCommands.transferData(this);
|
114
120
|
break;
|
115
121
|
case CHOICES.COMPREHENSIVE_TRANSFER:
|
116
|
-
await
|
122
|
+
await transferCommands.comprehensiveTransfer(this);
|
117
123
|
break;
|
118
124
|
case CHOICES.BACKUP_DATABASE:
|
119
125
|
await this.initControllerIfNeeded();
|
120
|
-
await
|
126
|
+
await databaseCommands.backupDatabase(this);
|
121
127
|
break;
|
122
128
|
case CHOICES.WIPE_DATABASE:
|
123
129
|
await this.initControllerIfNeeded();
|
124
|
-
await
|
130
|
+
await databaseCommands.wipeDatabase(this);
|
125
131
|
break;
|
126
132
|
case CHOICES.WIPE_COLLECTIONS:
|
127
133
|
await this.initControllerIfNeeded();
|
128
|
-
await
|
134
|
+
await databaseCommands.wipeCollections(this);
|
129
135
|
break;
|
130
136
|
case CHOICES.GENERATE_SCHEMAS:
|
131
137
|
await this.initControllerIfNeeded();
|
132
|
-
await
|
138
|
+
await schemaCommands.generateSchemas(this);
|
133
139
|
break;
|
134
140
|
case CHOICES.GENERATE_CONSTANTS:
|
135
141
|
await this.initControllerIfNeeded();
|
136
|
-
await
|
142
|
+
await schemaCommands.generateConstants(this);
|
137
143
|
break;
|
138
144
|
case CHOICES.IMPORT_DATA:
|
139
145
|
await this.initControllerIfNeeded();
|
140
|
-
await
|
146
|
+
await schemaCommands.importData(this);
|
141
147
|
break;
|
142
148
|
case CHOICES.RELOAD_CONFIG:
|
143
|
-
await
|
144
|
-
await this.reloadConfig();
|
149
|
+
await configCommands.reloadConfigWithSessionPreservation(this);
|
145
150
|
break;
|
146
151
|
case CHOICES.UPDATE_FUNCTION_SPEC:
|
147
152
|
await this.initControllerIfNeeded();
|
148
|
-
await
|
153
|
+
await functionCommands.updateFunctionSpec(this);
|
149
154
|
break;
|
150
155
|
case CHOICES.EXIT:
|
151
156
|
MessageFormatter.success("Goodbye!");
|
@@ -158,6 +163,27 @@ export class InteractiveCLI {
|
|
158
163
|
this.controller = new UtilsController(this.currentDir, directConfig);
|
159
164
|
await this.controller.init();
|
160
165
|
}
|
166
|
+
else {
|
167
|
+
// Extract session info from existing controller before reinitializing
|
168
|
+
const sessionInfo = this.controller.getSessionInfo();
|
169
|
+
if (sessionInfo.hasSession && directConfig) {
|
170
|
+
// Create enhanced directConfig with session preservation
|
171
|
+
const enhancedDirectConfig = {
|
172
|
+
...directConfig,
|
173
|
+
sessionCookie: this.controller.sessionCookie,
|
174
|
+
sessionMetadata: this.controller.sessionMetadata
|
175
|
+
};
|
176
|
+
// Reinitialize with session preservation
|
177
|
+
this.controller = new UtilsController(this.currentDir, enhancedDirectConfig);
|
178
|
+
await this.controller.init();
|
179
|
+
}
|
180
|
+
else if (directConfig) {
|
181
|
+
// Standard reinitialize without session
|
182
|
+
this.controller = new UtilsController(this.currentDir, directConfig);
|
183
|
+
await this.controller.init();
|
184
|
+
}
|
185
|
+
// If no directConfig provided, keep existing controller
|
186
|
+
}
|
161
187
|
}
|
162
188
|
async selectDatabases(databases, message, multiSelect = true) {
|
163
189
|
await this.initControllerIfNeeded();
|
@@ -175,11 +201,7 @@ export class InteractiveCLI {
|
|
175
201
|
acc.push(db);
|
176
202
|
}
|
177
203
|
return acc;
|
178
|
-
}, [])
|
179
|
-
.filter((db) => {
|
180
|
-
const useMigrations = this.controller?.config?.useMigrations ?? true;
|
181
|
-
return useMigrations || db.name.toLowerCase() !== "migrations";
|
182
|
-
});
|
204
|
+
}, []);
|
183
205
|
const hasLocalAndRemote = allDatabases.some((db) => configDatabases.some((c) => c.name === db.name)) &&
|
184
206
|
allDatabases.some((db) => !configDatabases.some((c) => c.name === db.name));
|
185
207
|
const choices = allDatabases
|
@@ -192,11 +214,7 @@ export class InteractiveCLI {
|
|
192
214
|
: " (Remote)"
|
193
215
|
: ""),
|
194
216
|
value: db,
|
195
|
-
}))
|
196
|
-
.filter((db) => {
|
197
|
-
const useMigrations = this.controller?.config?.useMigrations ?? true;
|
198
|
-
return useMigrations || db.name.toLowerCase() !== "migrations";
|
199
|
-
});
|
217
|
+
}));
|
200
218
|
const { selectedDatabases } = await inquirer.prompt([
|
201
219
|
{
|
202
220
|
type: multiSelect ? "checkbox" : "list",
|
@@ -217,7 +235,7 @@ export class InteractiveCLI {
|
|
217
235
|
Query.equal("name", database.name),
|
218
236
|
]);
|
219
237
|
if (dbExists.total === 0) {
|
220
|
-
|
238
|
+
MessageFormatter.warning(`Database "${database.name}" does not exist, using only local collection/table options`, { prefix: "Database" });
|
221
239
|
shouldFilterByDatabase = false;
|
222
240
|
}
|
223
241
|
else {
|
@@ -235,27 +253,75 @@ export class InteractiveCLI {
|
|
235
253
|
...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
|
236
254
|
];
|
237
255
|
if (shouldFilterByDatabase) {
|
238
|
-
//
|
239
|
-
// but still filter remote collections by selected database.
|
256
|
+
// Enhanced filtering for tables with optional databaseId
|
240
257
|
allCollections = allCollections.filter((c) => {
|
258
|
+
// For remote collections, they should match the selected database
|
259
|
+
if (remoteCollections.some((rc) => rc.name === c.name)) {
|
260
|
+
return c.databaseId === database.$id;
|
261
|
+
}
|
262
|
+
// For local collections/tables:
|
263
|
+
// - Collections without databaseId are kept (backward compatibility)
|
264
|
+
// - Tables with databaseId must match the selected database
|
265
|
+
// - Tables without databaseId are kept (fallback for misconfigured tables)
|
241
266
|
if (!c.databaseId)
|
242
267
|
return true;
|
243
268
|
return c.databaseId === database.$id;
|
244
269
|
});
|
245
270
|
}
|
271
|
+
// Filter out system tables (those starting with underscore)
|
272
|
+
allCollections = allCollections.filter((collection) => !collection.$id.startsWith('_'));
|
246
273
|
const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name)) &&
|
247
274
|
allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name));
|
275
|
+
// Enhanced choice display with type indicators
|
248
276
|
const choices = allCollections
|
249
|
-
.sort((a, b) =>
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
277
|
+
.sort((a, b) => {
|
278
|
+
// Sort by type first (collections before tables), then by name
|
279
|
+
const aIsTable = a._isFromTablesDir || false;
|
280
|
+
const bIsTable = b._isFromTablesDir || false;
|
281
|
+
if (aIsTable !== bIsTable) {
|
282
|
+
return aIsTable ? 1 : -1; // Collections first, then tables
|
283
|
+
}
|
284
|
+
return a.name.localeCompare(b.name);
|
285
|
+
})
|
286
|
+
.map((collection) => {
|
287
|
+
const localCollection = configCollections.find((c) => c.name === collection.name);
|
288
|
+
const isLocal = !!localCollection;
|
289
|
+
const isTable = localCollection?._isFromTablesDir || collection._isFromTablesDir || false;
|
290
|
+
const sourceFolder = localCollection?._sourceFolder || collection._sourceFolder || 'collections';
|
291
|
+
let typeIndicator = '';
|
292
|
+
let locationIndicator = '';
|
293
|
+
// Type indicator
|
294
|
+
if (isTable) {
|
295
|
+
typeIndicator = chalk.cyan('[Table]');
|
296
|
+
}
|
297
|
+
else {
|
298
|
+
typeIndicator = chalk.green('[Collection]');
|
299
|
+
}
|
300
|
+
// Location indicator
|
301
|
+
if (hasLocalAndRemote) {
|
302
|
+
if (isLocal) {
|
303
|
+
locationIndicator = chalk.gray(`(Local/${sourceFolder})`);
|
304
|
+
}
|
305
|
+
else {
|
306
|
+
locationIndicator = chalk.gray('(Remote)');
|
307
|
+
}
|
308
|
+
}
|
309
|
+
else if (isLocal) {
|
310
|
+
locationIndicator = chalk.gray(`(${sourceFolder}/)`);
|
311
|
+
}
|
312
|
+
// Database indicator for tables with explicit databaseId
|
313
|
+
let dbIndicator = '';
|
314
|
+
if (isTable && collection.databaseId && shouldFilterByDatabase) {
|
315
|
+
const matchesCurrentDb = collection.databaseId === database.$id;
|
316
|
+
if (!matchesCurrentDb) {
|
317
|
+
dbIndicator = chalk.yellow(` [DB: ${collection.databaseId}]`);
|
318
|
+
}
|
319
|
+
}
|
320
|
+
return {
|
321
|
+
name: `${typeIndicator} ${collection.name} ${locationIndicator}${dbIndicator}`,
|
322
|
+
value: collection,
|
323
|
+
};
|
324
|
+
});
|
259
325
|
const { selectedCollections } = await inquirer.prompt([
|
260
326
|
{
|
261
327
|
type: multiSelect ? "checkbox" : "list",
|
@@ -263,11 +329,39 @@ export class InteractiveCLI {
|
|
263
329
|
message: chalk.blue(message),
|
264
330
|
choices,
|
265
331
|
loop: true,
|
266
|
-
pageSize:
|
332
|
+
pageSize: 15, // Increased page size to accommodate additional info
|
267
333
|
},
|
268
334
|
]);
|
269
335
|
return selectedCollections;
|
270
336
|
}
|
337
|
+
/**
|
338
|
+
* Enhanced collection/table selection with better guidance for mixed scenarios
|
339
|
+
*/
|
340
|
+
async selectCollectionsAndTables(database, databasesClient, message, multiSelect = true, preferLocal = false, shouldFilterByDatabase = false) {
|
341
|
+
const configCollections = this.getLocalCollections();
|
342
|
+
const collectionsCount = configCollections.filter(c => !c._isFromTablesDir).length;
|
343
|
+
const tablesCount = configCollections.filter(c => c._isFromTablesDir).length;
|
344
|
+
// Provide context about what's available
|
345
|
+
if (collectionsCount > 0 && tablesCount > 0) {
|
346
|
+
MessageFormatter.info(`\n📋 Available items for database "${database.name}":`, { prefix: "Collections" });
|
347
|
+
MessageFormatter.info(` Collections: ${collectionsCount} (from collections/ folder)`, { prefix: "Collections" });
|
348
|
+
MessageFormatter.info(` Tables: ${tablesCount} (from tables/ folder)`, { prefix: "Collections" });
|
349
|
+
if (shouldFilterByDatabase) {
|
350
|
+
const filteredTables = configCollections.filter(c => c._isFromTablesDir && (!c.databaseId || c.databaseId === database.$id)).length;
|
351
|
+
if (filteredTables !== tablesCount) {
|
352
|
+
MessageFormatter.warning(` Note: ${filteredTables}/${tablesCount} tables match this database`, { prefix: "Collections" });
|
353
|
+
}
|
354
|
+
}
|
355
|
+
MessageFormatter.info('', { prefix: "Collections" });
|
356
|
+
}
|
357
|
+
else if (collectionsCount > 0) {
|
358
|
+
MessageFormatter.info(`📁 ${collectionsCount} collections available from collections/ folder\n`, { prefix: "Collections" });
|
359
|
+
}
|
360
|
+
else if (tablesCount > 0) {
|
361
|
+
MessageFormatter.info(`📊 ${tablesCount} tables available from tables/ folder\n`, { prefix: "Collections" });
|
362
|
+
}
|
363
|
+
return this.selectCollections(database, databasesClient, message, multiSelect, preferLocal, shouldFilterByDatabase);
|
364
|
+
}
|
271
365
|
getTemplateDefaults(template) {
|
272
366
|
const defaults = {
|
273
367
|
"typescript-node": {
|
@@ -276,6 +370,12 @@ export class InteractiveCLI {
|
|
276
370
|
commands: "npm install && npm run build",
|
277
371
|
specification: "s-0.5vcpu-512mb",
|
278
372
|
},
|
373
|
+
"hono-typescript": {
|
374
|
+
runtime: "node-21.0",
|
375
|
+
entrypoint: "src/index.ts",
|
376
|
+
commands: "npm install && npm run build",
|
377
|
+
specification: "s-0.5vcpu-512mb",
|
378
|
+
},
|
279
379
|
"uv": {
|
280
380
|
runtime: "python-3.12",
|
281
381
|
entrypoint: "src/index.py",
|
@@ -296,98 +396,6 @@ export class InteractiveCLI {
|
|
296
396
|
specification: "s-0.5vcpu-512mb",
|
297
397
|
};
|
298
398
|
}
|
299
|
-
async createFunction() {
|
300
|
-
const { name } = await inquirer.prompt([
|
301
|
-
{
|
302
|
-
type: "input",
|
303
|
-
name: "name",
|
304
|
-
message: "Function name:",
|
305
|
-
validate: (input) => input.length > 0,
|
306
|
-
},
|
307
|
-
]);
|
308
|
-
const { template } = await inquirer.prompt([
|
309
|
-
{
|
310
|
-
type: "list",
|
311
|
-
name: "template",
|
312
|
-
message: "Select a template:",
|
313
|
-
choices: [
|
314
|
-
{ name: "TypeScript Node.js", value: "typescript-node" },
|
315
|
-
{ name: "Python with UV", value: "uv" },
|
316
|
-
{ name: "Count Documents in Collection", value: "count-docs-in-collection" },
|
317
|
-
{ name: "None (Empty Function)", value: "none" },
|
318
|
-
],
|
319
|
-
},
|
320
|
-
]);
|
321
|
-
// Get template defaults
|
322
|
-
const templateDefaults = this.getTemplateDefaults(template);
|
323
|
-
const { runtime } = await inquirer.prompt([
|
324
|
-
{
|
325
|
-
type: "list",
|
326
|
-
name: "runtime",
|
327
|
-
message: "Select runtime:",
|
328
|
-
choices: Object.values(RuntimeSchema.Values),
|
329
|
-
default: templateDefaults.runtime,
|
330
|
-
},
|
331
|
-
]);
|
332
|
-
const specifications = await listSpecifications(this.controller.appwriteServer);
|
333
|
-
const { specification } = await inquirer.prompt([
|
334
|
-
{
|
335
|
-
type: "list",
|
336
|
-
name: "specification",
|
337
|
-
message: "Select specification:",
|
338
|
-
choices: [
|
339
|
-
{ name: "None", value: undefined },
|
340
|
-
...specifications.specifications.map((s) => ({
|
341
|
-
name: s.slug,
|
342
|
-
value: s.slug,
|
343
|
-
})),
|
344
|
-
],
|
345
|
-
default: templateDefaults.specification,
|
346
|
-
},
|
347
|
-
]);
|
348
|
-
const functionConfig = {
|
349
|
-
$id: ulid(),
|
350
|
-
name,
|
351
|
-
runtime,
|
352
|
-
events: [],
|
353
|
-
execute: ["any"],
|
354
|
-
enabled: true,
|
355
|
-
logging: true,
|
356
|
-
entrypoint: templateDefaults.entrypoint,
|
357
|
-
commands: templateDefaults.commands,
|
358
|
-
specification: specification || templateDefaults.specification,
|
359
|
-
scopes: [],
|
360
|
-
timeout: 15,
|
361
|
-
schedule: "",
|
362
|
-
installationId: "",
|
363
|
-
providerRepositoryId: "",
|
364
|
-
providerBranch: "",
|
365
|
-
providerSilentMode: false,
|
366
|
-
providerRootDirectory: "",
|
367
|
-
templateRepository: "",
|
368
|
-
templateOwner: "",
|
369
|
-
templateRootDirectory: "",
|
370
|
-
};
|
371
|
-
if (template !== "none") {
|
372
|
-
await createFunctionTemplate(template, name, "./functions");
|
373
|
-
}
|
374
|
-
// Add to in-memory config
|
375
|
-
if (!this.controller.config.functions) {
|
376
|
-
this.controller.config.functions = [];
|
377
|
-
}
|
378
|
-
this.controller.config.functions.push(functionConfig);
|
379
|
-
// If using YAML config, also add to YAML file
|
380
|
-
const yamlConfigPath = findYamlConfig(this.currentDir);
|
381
|
-
if (yamlConfigPath) {
|
382
|
-
try {
|
383
|
-
await addFunctionToYamlConfig(yamlConfigPath, functionConfig);
|
384
|
-
}
|
385
|
-
catch (error) {
|
386
|
-
MessageFormatter.warning(`Function created but failed to update YAML config: ${error instanceof Error ? error.message : error}`, { prefix: "Functions" });
|
387
|
-
}
|
388
|
-
}
|
389
|
-
MessageFormatter.success("Function created successfully!", { prefix: "Functions" });
|
390
|
-
}
|
391
399
|
async findFunctionInSubdirectories(basePaths, functionName) {
|
392
400
|
// Common locations to check first
|
393
401
|
const commonPaths = basePaths.flatMap((basePath) => [
|
@@ -408,7 +416,7 @@ export class InteractiveCLI {
|
|
408
416
|
try {
|
409
417
|
const stats = await fs.promises.stat(path);
|
410
418
|
if (stats.isDirectory()) {
|
411
|
-
|
419
|
+
MessageFormatter.success(`Found function at common location: ${path}`, { prefix: "Functions" });
|
412
420
|
return path;
|
413
421
|
}
|
414
422
|
}
|
@@ -417,7 +425,7 @@ export class InteractiveCLI {
|
|
417
425
|
}
|
418
426
|
}
|
419
427
|
// If not found in common locations, do recursive search
|
420
|
-
|
428
|
+
MessageFormatter.info("Function not found in common locations, searching subdirectories...", { prefix: "Functions" });
|
421
429
|
const queue = [...basePaths];
|
422
430
|
const searched = new Set();
|
423
431
|
while (queue.length > 0) {
|
@@ -444,7 +452,7 @@ export class InteractiveCLI {
|
|
444
452
|
// Check if any variation of the entry name matches any variation of the function name
|
445
453
|
const hasMatch = [...functionNameVariations].some((fnVar) => [...entryNameVariations].includes(fnVar));
|
446
454
|
if (hasMatch) {
|
447
|
-
|
455
|
+
MessageFormatter.success(`Found function at: ${fullPath}`, { prefix: "Functions" });
|
448
456
|
return fullPath;
|
449
457
|
}
|
450
458
|
queue.push(fullPath);
|
@@ -452,150 +460,11 @@ export class InteractiveCLI {
|
|
452
460
|
}
|
453
461
|
}
|
454
462
|
catch (error) {
|
455
|
-
|
463
|
+
MessageFormatter.warning(`Error reading directory ${currentPath}: ${error}`, { prefix: "Functions" });
|
456
464
|
}
|
457
465
|
}
|
458
466
|
return null;
|
459
467
|
}
|
460
|
-
async deployFunction() {
|
461
|
-
await this.initControllerIfNeeded();
|
462
|
-
if (!this.controller?.config) {
|
463
|
-
console.log(chalk.red("Failed to initialize controller or load config"));
|
464
|
-
return;
|
465
|
-
}
|
466
|
-
const functions = await this.selectFunctions("Select function(s) to deploy:", true, true);
|
467
|
-
if (!functions?.length) {
|
468
|
-
console.log(chalk.red("No function selected"));
|
469
|
-
return;
|
470
|
-
}
|
471
|
-
for (const functionConfig of functions) {
|
472
|
-
if (!functionConfig) {
|
473
|
-
console.log(chalk.red("Invalid function configuration"));
|
474
|
-
return;
|
475
|
-
}
|
476
|
-
// Ensure functions array exists
|
477
|
-
if (!this.controller.config.functions) {
|
478
|
-
this.controller.config.functions = [];
|
479
|
-
}
|
480
|
-
const functionNameLower = functionConfig.name
|
481
|
-
.toLowerCase()
|
482
|
-
.replace(/\s+/g, "-");
|
483
|
-
// Debug logging
|
484
|
-
console.log(chalk.blue(`🔍 Function deployment debug:`));
|
485
|
-
console.log(chalk.gray(` Function name: ${functionConfig.name}`));
|
486
|
-
console.log(chalk.gray(` Function ID: ${functionConfig.$id}`));
|
487
|
-
console.log(chalk.gray(` Config dirPath: ${functionConfig.dirPath || 'undefined'}`));
|
488
|
-
if (functionConfig.dirPath) {
|
489
|
-
const expandedPath = functionConfig.dirPath.startsWith('~/')
|
490
|
-
? functionConfig.dirPath.replace('~', os.homedir())
|
491
|
-
: functionConfig.dirPath;
|
492
|
-
console.log(chalk.gray(` Expanded dirPath: ${expandedPath}`));
|
493
|
-
}
|
494
|
-
console.log(chalk.gray(` Appwrite folder: ${this.controller.getAppwriteFolderPath()}`));
|
495
|
-
console.log(chalk.gray(` Current working dir: ${process.cwd()}`));
|
496
|
-
// Helper function to expand tilde in paths
|
497
|
-
const expandTildePath = (path) => {
|
498
|
-
if (path.startsWith('~/')) {
|
499
|
-
return path.replace('~', os.homedir());
|
500
|
-
}
|
501
|
-
return path;
|
502
|
-
};
|
503
|
-
// Check locations in priority order:
|
504
|
-
const priorityLocations = [
|
505
|
-
// 1. Config dirPath if specified (with tilde expansion)
|
506
|
-
functionConfig.dirPath ? expandTildePath(functionConfig.dirPath) : undefined,
|
507
|
-
// 2. Appwrite config folder/functions/name
|
508
|
-
join(this.controller.getAppwriteFolderPath(), "functions", functionNameLower),
|
509
|
-
// 3. Current working directory/functions/name
|
510
|
-
join(process.cwd(), "functions", functionNameLower),
|
511
|
-
// 4. Current working directory/name
|
512
|
-
join(process.cwd(), functionNameLower),
|
513
|
-
].filter((val) => val !== undefined); // Remove undefined entries (in case dirPath is undefined)
|
514
|
-
console.log(chalk.blue(`🔍 Priority locations to check:`));
|
515
|
-
priorityLocations.forEach((loc, i) => {
|
516
|
-
console.log(chalk.gray(` ${i + 1}. ${loc}`));
|
517
|
-
});
|
518
|
-
let functionPath = null;
|
519
|
-
// Check each priority location
|
520
|
-
for (const location of priorityLocations) {
|
521
|
-
console.log(chalk.gray(` Checking: ${location} - ${fs.existsSync(location) ? 'EXISTS' : 'NOT FOUND'}`));
|
522
|
-
if (fs.existsSync(location)) {
|
523
|
-
console.log(chalk.green(`✅ Found function at: ${location}`));
|
524
|
-
functionPath = location;
|
525
|
-
break;
|
526
|
-
}
|
527
|
-
}
|
528
|
-
// If not found in priority locations, do a broader search
|
529
|
-
if (!functionPath) {
|
530
|
-
console.log(chalk.yellow(`Function not found in primary locations, searching subdirectories...`));
|
531
|
-
// Search in both appwrite config directory and current working directory
|
532
|
-
functionPath = await this.findFunctionInSubdirectories([this.controller.getAppwriteFolderPath(), process.cwd()], functionNameLower);
|
533
|
-
}
|
534
|
-
if (!functionPath) {
|
535
|
-
const { shouldDownload } = await inquirer.prompt([
|
536
|
-
{
|
537
|
-
type: "confirm",
|
538
|
-
name: "shouldDownload",
|
539
|
-
message: "Function not found locally. Would you like to download the latest deployment?",
|
540
|
-
default: false,
|
541
|
-
},
|
542
|
-
]);
|
543
|
-
if (shouldDownload) {
|
544
|
-
try {
|
545
|
-
console.log(chalk.blue("Downloading latest deployment..."));
|
546
|
-
const { path: downloadedPath, function: remoteFunction } = await downloadLatestFunctionDeployment(this.controller.appwriteServer, functionConfig.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
|
547
|
-
console.log(chalk.green(`✨ Function downloaded to ${downloadedPath}`));
|
548
|
-
functionPath = downloadedPath;
|
549
|
-
functionConfig.dirPath = downloadedPath;
|
550
|
-
const existingIndex = this.controller.config.functions.findIndex((f) => f?.$id === remoteFunction.$id);
|
551
|
-
if (existingIndex >= 0) {
|
552
|
-
this.controller.config.functions[existingIndex].dirPath =
|
553
|
-
downloadedPath;
|
554
|
-
}
|
555
|
-
await this.controller.reloadConfig();
|
556
|
-
}
|
557
|
-
catch (error) {
|
558
|
-
console.error(chalk.red("Failed to download function deployment:"), error);
|
559
|
-
return;
|
560
|
-
}
|
561
|
-
}
|
562
|
-
else {
|
563
|
-
console.log(chalk.red(`Function ${functionConfig.name} not found locally. Cannot deploy.`));
|
564
|
-
return;
|
565
|
-
}
|
566
|
-
}
|
567
|
-
if (!this.controller.appwriteServer) {
|
568
|
-
console.log(chalk.red("Appwrite server not initialized"));
|
569
|
-
return;
|
570
|
-
}
|
571
|
-
try {
|
572
|
-
await deployLocalFunction(this.controller.appwriteServer, functionConfig.name, {
|
573
|
-
...functionConfig,
|
574
|
-
dirPath: functionPath,
|
575
|
-
}, functionPath);
|
576
|
-
MessageFormatter.success("Function deployed successfully!", { prefix: "Functions" });
|
577
|
-
}
|
578
|
-
catch (error) {
|
579
|
-
console.error(chalk.red("Failed to deploy function:"), error);
|
580
|
-
}
|
581
|
-
}
|
582
|
-
}
|
583
|
-
async deleteFunction() {
|
584
|
-
const functions = await this.selectFunctions("Select functions to delete:", true, false);
|
585
|
-
if (!functions.length) {
|
586
|
-
console.log(chalk.red("No functions selected"));
|
587
|
-
return;
|
588
|
-
}
|
589
|
-
for (const func of functions) {
|
590
|
-
try {
|
591
|
-
await deleteFunction(this.controller.appwriteServer, func.$id);
|
592
|
-
console.log(chalk.green(`✨ Function ${func.name} deleted successfully!`));
|
593
|
-
}
|
594
|
-
catch (error) {
|
595
|
-
console.error(chalk.red(`Failed to delete function ${func.name}:`), error);
|
596
|
-
}
|
597
|
-
}
|
598
|
-
}
|
599
468
|
async selectFunctions(message, multiple = true, includeRemote = false) {
|
600
469
|
const remoteFunctions = includeRemote
|
601
470
|
? await listFunctions(this.controller.appwriteServer, [
|
@@ -673,18 +542,6 @@ export class InteractiveCLI {
|
|
673
542
|
]);
|
674
543
|
return selectedBuckets;
|
675
544
|
}
|
676
|
-
async createCollectionConfig() {
|
677
|
-
const { collectionName } = await inquirer.prompt([
|
678
|
-
{
|
679
|
-
type: "input",
|
680
|
-
name: "collectionName",
|
681
|
-
message: chalk.blue("Enter the name of the collection:"),
|
682
|
-
validate: (input) => input.trim() !== "" || "Collection name cannot be empty.",
|
683
|
-
},
|
684
|
-
]);
|
685
|
-
console.log(chalk.green(`Creating collection config file for '${collectionName}'...`));
|
686
|
-
createEmptyCollection(collectionName);
|
687
|
-
}
|
688
545
|
async configureBuckets(config, databases) {
|
689
546
|
const { storage } = this.controller;
|
690
547
|
if (!storage) {
|
@@ -866,548 +723,6 @@ export class InteractiveCLI {
|
|
866
723
|
antivirus: bucketAntivirus,
|
867
724
|
}, bucketId.length > 0 ? bucketId : ulid());
|
868
725
|
}
|
869
|
-
async syncDb() {
|
870
|
-
console.log(chalk.blue("Pushing local configuration to Appwrite..."));
|
871
|
-
const databases = await this.selectDatabases(this.getLocalDatabases(), chalk.blue("Select local databases to push:"), true);
|
872
|
-
if (!databases.length) {
|
873
|
-
console.log(chalk.yellow("No databases selected. Skipping database sync."));
|
874
|
-
return;
|
875
|
-
}
|
876
|
-
const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true, // prefer local
|
877
|
-
true // filter by selected database
|
878
|
-
);
|
879
|
-
const { syncFunctions } = await inquirer.prompt([
|
880
|
-
{
|
881
|
-
type: "confirm",
|
882
|
-
name: "syncFunctions",
|
883
|
-
message: "Do you want to push local functions to remote?",
|
884
|
-
default: false,
|
885
|
-
},
|
886
|
-
]);
|
887
|
-
try {
|
888
|
-
// First sync databases and collections
|
889
|
-
await this.controller.syncDb(databases, collections);
|
890
|
-
console.log(chalk.green("Database and collections pushed successfully"));
|
891
|
-
// Then handle functions if requested
|
892
|
-
if (syncFunctions && this.controller.config?.functions?.length) {
|
893
|
-
const functions = await this.selectFunctions(chalk.blue("Select local functions to push:"), true, true // prefer local
|
894
|
-
);
|
895
|
-
for (const func of functions) {
|
896
|
-
try {
|
897
|
-
await this.controller.deployFunction(func.name);
|
898
|
-
console.log(chalk.green(`Function ${func.name} deployed successfully`));
|
899
|
-
}
|
900
|
-
catch (error) {
|
901
|
-
console.error(chalk.red(`Failed to deploy function ${func.name}:`), error);
|
902
|
-
}
|
903
|
-
}
|
904
|
-
}
|
905
|
-
console.log(chalk.green("Local configuration push completed successfully!"));
|
906
|
-
}
|
907
|
-
catch (error) {
|
908
|
-
console.error(chalk.red("Failed to push local configuration:"), error);
|
909
|
-
throw error;
|
910
|
-
}
|
911
|
-
}
|
912
|
-
async synchronizeConfigurations() {
|
913
|
-
console.log(chalk.blue("Synchronizing configurations..."));
|
914
|
-
await this.controller.init();
|
915
|
-
// Sync databases, collections, and buckets
|
916
|
-
const { syncDatabases } = await inquirer.prompt([
|
917
|
-
{
|
918
|
-
type: "confirm",
|
919
|
-
name: "syncDatabases",
|
920
|
-
message: "Do you want to synchronize databases, collections, and their buckets?",
|
921
|
-
default: true,
|
922
|
-
},
|
923
|
-
]);
|
924
|
-
if (syncDatabases) {
|
925
|
-
const remoteDatabases = await fetchAllDatabases(this.controller.database);
|
926
|
-
// Use the controller's synchronizeConfigurations method which handles collections properly
|
927
|
-
console.log(chalk.blue("Pulling collections and generating collection files..."));
|
928
|
-
await this.controller.synchronizeConfigurations(remoteDatabases);
|
929
|
-
// Also configure buckets for any new databases
|
930
|
-
const localDatabases = this.controller.config?.databases || [];
|
931
|
-
const updatedConfig = await this.configureBuckets({
|
932
|
-
...this.controller.config,
|
933
|
-
databases: [
|
934
|
-
...localDatabases,
|
935
|
-
...remoteDatabases.filter((rd) => !localDatabases.some((ld) => ld.name === rd.name)),
|
936
|
-
],
|
937
|
-
});
|
938
|
-
this.controller.config = updatedConfig;
|
939
|
-
}
|
940
|
-
// Then sync functions
|
941
|
-
const { syncFunctions } = await inquirer.prompt([
|
942
|
-
{
|
943
|
-
type: "confirm",
|
944
|
-
name: "syncFunctions",
|
945
|
-
message: "Do you want to synchronize functions?",
|
946
|
-
default: true,
|
947
|
-
},
|
948
|
-
]);
|
949
|
-
if (syncFunctions) {
|
950
|
-
const remoteFunctions = await this.controller.listAllFunctions();
|
951
|
-
const localFunctions = this.controller.config?.functions || [];
|
952
|
-
const allFunctions = [
|
953
|
-
...remoteFunctions,
|
954
|
-
...localFunctions.filter((f) => !remoteFunctions.some((rf) => rf.$id === f.$id)),
|
955
|
-
];
|
956
|
-
for (const func of allFunctions) {
|
957
|
-
const hasLocal = localFunctions.some((lf) => lf.$id === func.$id);
|
958
|
-
const hasRemote = remoteFunctions.some((rf) => rf.$id === func.$id);
|
959
|
-
if (hasLocal && hasRemote) {
|
960
|
-
// First try to find the function locally
|
961
|
-
let functionPath = join(this.controller.getAppwriteFolderPath(), "functions", func.name);
|
962
|
-
if (!fs.existsSync(functionPath)) {
|
963
|
-
console.log(chalk.yellow(`Function not found in primary location, searching subdirectories...`));
|
964
|
-
const foundPath = await this.findFunctionInSubdirectories([this.controller.getAppwriteFolderPath(), process.cwd()], func.name);
|
965
|
-
if (foundPath) {
|
966
|
-
console.log(chalk.green(`Found function at: ${foundPath}`));
|
967
|
-
functionPath = foundPath;
|
968
|
-
}
|
969
|
-
}
|
970
|
-
const { preference } = await inquirer.prompt([
|
971
|
-
{
|
972
|
-
type: "list",
|
973
|
-
name: "preference",
|
974
|
-
message: `Function "${func.name}" ${functionPath ? "found at " + functionPath : "not found locally"}. What would you like to do?`,
|
975
|
-
choices: [
|
976
|
-
...(functionPath
|
977
|
-
? [
|
978
|
-
{
|
979
|
-
name: "Keep local version (deploy to remote)",
|
980
|
-
value: "local",
|
981
|
-
},
|
982
|
-
]
|
983
|
-
: []),
|
984
|
-
{ name: "Use remote version (download)", value: "remote" },
|
985
|
-
{ name: "Update config only", value: "config" },
|
986
|
-
{ name: "Skip this function", value: "skip" },
|
987
|
-
],
|
988
|
-
},
|
989
|
-
]);
|
990
|
-
if (preference === "local" && functionPath) {
|
991
|
-
await this.controller.deployFunction(func.name);
|
992
|
-
}
|
993
|
-
else if (preference === "remote") {
|
994
|
-
await downloadLatestFunctionDeployment(this.controller.appwriteServer, func.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
|
995
|
-
}
|
996
|
-
else if (preference === "config") {
|
997
|
-
const remoteFunction = await getFunction(this.controller.appwriteServer, func.$id);
|
998
|
-
const newFunction = {
|
999
|
-
$id: remoteFunction.$id,
|
1000
|
-
name: remoteFunction.name,
|
1001
|
-
runtime: remoteFunction.runtime,
|
1002
|
-
execute: remoteFunction.execute || [],
|
1003
|
-
events: remoteFunction.events || [],
|
1004
|
-
schedule: remoteFunction.schedule || "",
|
1005
|
-
timeout: remoteFunction.timeout || 15,
|
1006
|
-
enabled: remoteFunction.enabled !== false,
|
1007
|
-
logging: remoteFunction.logging !== false,
|
1008
|
-
entrypoint: remoteFunction.entrypoint || "src/index.ts",
|
1009
|
-
commands: remoteFunction.commands || "npm install",
|
1010
|
-
scopes: (remoteFunction.scopes || []),
|
1011
|
-
installationId: remoteFunction.installationId,
|
1012
|
-
providerRepositoryId: remoteFunction.providerRepositoryId,
|
1013
|
-
providerBranch: remoteFunction.providerBranch,
|
1014
|
-
providerSilentMode: remoteFunction.providerSilentMode,
|
1015
|
-
providerRootDirectory: remoteFunction.providerRootDirectory,
|
1016
|
-
specification: remoteFunction.specification,
|
1017
|
-
};
|
1018
|
-
const existingIndex = this.controller.config.functions.findIndex((f) => f.$id === remoteFunction.$id);
|
1019
|
-
if (existingIndex >= 0) {
|
1020
|
-
this.controller.config.functions[existingIndex] = newFunction;
|
1021
|
-
}
|
1022
|
-
else {
|
1023
|
-
this.controller.config.functions.push(newFunction);
|
1024
|
-
}
|
1025
|
-
console.log(chalk.green(`Updated config for function: ${func.name}`));
|
1026
|
-
}
|
1027
|
-
}
|
1028
|
-
else if (hasLocal) {
|
1029
|
-
// Similar check for local-only functions
|
1030
|
-
let functionPath = join(this.controller.getAppwriteFolderPath(), "functions", func.name);
|
1031
|
-
if (!fs.existsSync(functionPath)) {
|
1032
|
-
const foundPath = await this.findFunctionInSubdirectories([this.controller.getAppwriteFolderPath(), process.cwd()], func.name);
|
1033
|
-
if (foundPath) {
|
1034
|
-
functionPath = foundPath;
|
1035
|
-
}
|
1036
|
-
}
|
1037
|
-
const { action } = await inquirer.prompt([
|
1038
|
-
{
|
1039
|
-
type: "list",
|
1040
|
-
name: "action",
|
1041
|
-
message: `Function "${func.name}" ${functionPath ? "found at " + functionPath : "not found locally"}. What would you like to do?`,
|
1042
|
-
choices: [
|
1043
|
-
...(functionPath
|
1044
|
-
? [
|
1045
|
-
{
|
1046
|
-
name: "Deploy to remote",
|
1047
|
-
value: "deploy",
|
1048
|
-
},
|
1049
|
-
]
|
1050
|
-
: []),
|
1051
|
-
{ name: "Skip this function", value: "skip" },
|
1052
|
-
],
|
1053
|
-
},
|
1054
|
-
]);
|
1055
|
-
if (action === "deploy" && functionPath) {
|
1056
|
-
await this.controller.deployFunction(func.name);
|
1057
|
-
}
|
1058
|
-
}
|
1059
|
-
else if (hasRemote) {
|
1060
|
-
const { action } = await inquirer.prompt([
|
1061
|
-
{
|
1062
|
-
type: "list",
|
1063
|
-
name: "action",
|
1064
|
-
message: `Function "${func.name}" exists only remotely. What would you like to do?`,
|
1065
|
-
choices: [
|
1066
|
-
{ name: "Update config only", value: "config" },
|
1067
|
-
{ name: "Download locally", value: "download" },
|
1068
|
-
{ name: "Skip this function", value: "skip" },
|
1069
|
-
],
|
1070
|
-
},
|
1071
|
-
]);
|
1072
|
-
if (action === "download") {
|
1073
|
-
await downloadLatestFunctionDeployment(this.controller.appwriteServer, func.$id, join(this.controller.getAppwriteFolderPath(), "functions"));
|
1074
|
-
}
|
1075
|
-
else if (action === "config") {
|
1076
|
-
const remoteFunction = await getFunction(this.controller.appwriteServer, func.$id);
|
1077
|
-
const newFunction = {
|
1078
|
-
$id: remoteFunction.$id,
|
1079
|
-
name: remoteFunction.name,
|
1080
|
-
runtime: remoteFunction.runtime,
|
1081
|
-
execute: remoteFunction.execute || [],
|
1082
|
-
events: remoteFunction.events || [],
|
1083
|
-
schedule: remoteFunction.schedule || "",
|
1084
|
-
timeout: remoteFunction.timeout || 15,
|
1085
|
-
enabled: remoteFunction.enabled !== false,
|
1086
|
-
logging: remoteFunction.logging !== false,
|
1087
|
-
entrypoint: remoteFunction.entrypoint || "src/index.ts",
|
1088
|
-
commands: remoteFunction.commands || "npm install",
|
1089
|
-
scopes: (remoteFunction.scopes || []),
|
1090
|
-
installationId: remoteFunction.installationId,
|
1091
|
-
providerRepositoryId: remoteFunction.providerRepositoryId,
|
1092
|
-
providerBranch: remoteFunction.providerBranch,
|
1093
|
-
providerSilentMode: remoteFunction.providerSilentMode,
|
1094
|
-
providerRootDirectory: remoteFunction.providerRootDirectory,
|
1095
|
-
specification: remoteFunction.specification,
|
1096
|
-
};
|
1097
|
-
this.controller.config.functions =
|
1098
|
-
this.controller.config.functions || [];
|
1099
|
-
this.controller.config.functions.push(newFunction);
|
1100
|
-
console.log(chalk.green(`Added config for remote function: ${func.name}`));
|
1101
|
-
}
|
1102
|
-
}
|
1103
|
-
}
|
1104
|
-
// Schema generation and collection file writing is handled by controller.synchronizeConfigurations()
|
1105
|
-
}
|
1106
|
-
console.log(chalk.green("✨ Configurations synchronized successfully!"));
|
1107
|
-
}
|
1108
|
-
async backupDatabase() {
|
1109
|
-
if (!this.controller.database) {
|
1110
|
-
throw new Error("Database is not initialized, is the config file correct & created?");
|
1111
|
-
}
|
1112
|
-
const databases = await fetchAllDatabases(this.controller.database);
|
1113
|
-
const selectedDatabases = await this.selectDatabases(databases, "Select databases to backup:");
|
1114
|
-
for (const db of selectedDatabases) {
|
1115
|
-
console.log(chalk.yellow(`Backing up database: ${db.name}`));
|
1116
|
-
await this.controller.backupDatabase(db);
|
1117
|
-
}
|
1118
|
-
MessageFormatter.success("Database backup completed", { prefix: "Backup" });
|
1119
|
-
}
|
1120
|
-
async wipeDatabase() {
|
1121
|
-
if (!this.controller.database || !this.controller.storage) {
|
1122
|
-
throw new Error("Database or Storage is not initialized, is the config file correct & created?");
|
1123
|
-
}
|
1124
|
-
const databases = await fetchAllDatabases(this.controller.database);
|
1125
|
-
const storage = await listBuckets(this.controller.storage);
|
1126
|
-
const selectedDatabases = await this.selectDatabases(databases, "Select databases to wipe:");
|
1127
|
-
const { selectedStorage } = await inquirer.prompt([
|
1128
|
-
{
|
1129
|
-
type: "checkbox",
|
1130
|
-
name: "selectedStorage",
|
1131
|
-
message: "Select storage buckets to wipe:",
|
1132
|
-
choices: storage.buckets.map((s) => ({ name: s.name, value: s.$id })),
|
1133
|
-
},
|
1134
|
-
]);
|
1135
|
-
const { wipeUsers } = await inquirer.prompt([
|
1136
|
-
{
|
1137
|
-
type: "confirm",
|
1138
|
-
name: "wipeUsers",
|
1139
|
-
message: "Do you want to wipe users as well?",
|
1140
|
-
default: false,
|
1141
|
-
},
|
1142
|
-
]);
|
1143
|
-
const databaseNames = selectedDatabases.map(db => db.name);
|
1144
|
-
const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, {
|
1145
|
-
includeStorage: selectedStorage.length > 0,
|
1146
|
-
includeUsers: wipeUsers
|
1147
|
-
});
|
1148
|
-
if (confirmed) {
|
1149
|
-
MessageFormatter.info("Starting wipe operation...", { prefix: "Wipe" });
|
1150
|
-
for (const db of selectedDatabases) {
|
1151
|
-
await this.controller.wipeDatabase(db);
|
1152
|
-
}
|
1153
|
-
for (const bucketId of selectedStorage) {
|
1154
|
-
await this.controller.wipeDocumentStorage(bucketId);
|
1155
|
-
}
|
1156
|
-
if (wipeUsers) {
|
1157
|
-
await this.controller.wipeUsers();
|
1158
|
-
}
|
1159
|
-
MessageFormatter.success("Wipe operation completed", { prefix: "Wipe" });
|
1160
|
-
}
|
1161
|
-
else {
|
1162
|
-
MessageFormatter.info("Wipe operation cancelled", { prefix: "Wipe" });
|
1163
|
-
}
|
1164
|
-
}
|
1165
|
-
async wipeCollections() {
|
1166
|
-
if (!this.controller.database) {
|
1167
|
-
throw new Error("Database is not initialized, is the config file correct & created?");
|
1168
|
-
}
|
1169
|
-
const databases = await fetchAllDatabases(this.controller.database);
|
1170
|
-
const selectedDatabases = await this.selectDatabases(databases, "Select the database(s) containing the collections to wipe:", true);
|
1171
|
-
for (const database of selectedDatabases) {
|
1172
|
-
const collections = await this.selectCollections(database, this.controller.database, `Select collections to wipe from ${database.name}:`, true, undefined, true);
|
1173
|
-
const collectionNames = collections.map(c => c.name);
|
1174
|
-
const confirmed = await ConfirmationDialogs.confirmCollectionWipe(database.name, collectionNames);
|
1175
|
-
if (confirmed) {
|
1176
|
-
MessageFormatter.info(`Wiping selected collections from ${database.name}...`, { prefix: "Wipe" });
|
1177
|
-
for (const collection of collections) {
|
1178
|
-
await this.controller.wipeCollection(database, collection);
|
1179
|
-
MessageFormatter.success(`Collection ${collection.name} wiped successfully`, { prefix: "Wipe" });
|
1180
|
-
}
|
1181
|
-
}
|
1182
|
-
else {
|
1183
|
-
MessageFormatter.info(`Wipe operation cancelled for ${database.name}`, { prefix: "Wipe" });
|
1184
|
-
}
|
1185
|
-
}
|
1186
|
-
MessageFormatter.success("Wipe collections operation completed", { prefix: "Wipe" });
|
1187
|
-
}
|
1188
|
-
async generateSchemas() {
|
1189
|
-
console.log(chalk.yellow("Generating schemas..."));
|
1190
|
-
// Prompt user for schema type preference
|
1191
|
-
const { schemaType } = await inquirer.prompt([
|
1192
|
-
{
|
1193
|
-
type: "list",
|
1194
|
-
name: "schemaType",
|
1195
|
-
message: "What type of schemas would you like to generate?",
|
1196
|
-
choices: [
|
1197
|
-
{ name: "TypeScript (Zod) schemas", value: "zod" },
|
1198
|
-
{ name: "JSON schemas", value: "json" },
|
1199
|
-
{ name: "Both TypeScript and JSON schemas", value: "both" },
|
1200
|
-
],
|
1201
|
-
default: "both",
|
1202
|
-
},
|
1203
|
-
]);
|
1204
|
-
// Get the config folder path (where the config file is located)
|
1205
|
-
const configFolderPath = this.controller.getAppwriteFolderPath();
|
1206
|
-
if (!configFolderPath) {
|
1207
|
-
MessageFormatter.error("Failed to get config folder path", undefined, { prefix: "Schemas" });
|
1208
|
-
return;
|
1209
|
-
}
|
1210
|
-
// Create SchemaGenerator with the correct base path and generate schemas
|
1211
|
-
const schemaGenerator = new SchemaGenerator(this.controller.config, configFolderPath);
|
1212
|
-
schemaGenerator.generateSchemas({ format: schemaType, verbose: true });
|
1213
|
-
MessageFormatter.success("Schema generation completed", { prefix: "Schemas" });
|
1214
|
-
}
|
1215
|
-
async generateConstants() {
|
1216
|
-
console.log(chalk.yellow("Generating cross-language constants..."));
|
1217
|
-
if (!this.controller?.config) {
|
1218
|
-
MessageFormatter.error("No configuration found", undefined, { prefix: "Constants" });
|
1219
|
-
return;
|
1220
|
-
}
|
1221
|
-
// Prompt for languages
|
1222
|
-
const { languages } = await inquirer.prompt([
|
1223
|
-
{
|
1224
|
-
type: "checkbox",
|
1225
|
-
name: "languages",
|
1226
|
-
message: "Select languages for constants generation:",
|
1227
|
-
choices: [
|
1228
|
-
{ name: "TypeScript", value: "typescript", checked: true },
|
1229
|
-
{ name: "JavaScript", value: "javascript" },
|
1230
|
-
{ name: "Python", value: "python" },
|
1231
|
-
{ name: "PHP", value: "php" },
|
1232
|
-
{ name: "Dart", value: "dart" },
|
1233
|
-
{ name: "JSON", value: "json" },
|
1234
|
-
{ name: "Environment Variables", value: "env" },
|
1235
|
-
],
|
1236
|
-
validate: (input) => {
|
1237
|
-
if (input.length === 0) {
|
1238
|
-
return "Please select at least one language";
|
1239
|
-
}
|
1240
|
-
return true;
|
1241
|
-
},
|
1242
|
-
},
|
1243
|
-
]);
|
1244
|
-
// Determine default output directory based on config location
|
1245
|
-
const configPath = this.controller.getAppwriteFolderPath();
|
1246
|
-
const defaultOutputDir = configPath
|
1247
|
-
? path.join(configPath, "constants")
|
1248
|
-
: path.join(process.cwd(), "constants");
|
1249
|
-
// Prompt for output directory
|
1250
|
-
const { outputDir } = await inquirer.prompt([
|
1251
|
-
{
|
1252
|
-
type: "input",
|
1253
|
-
name: "outputDir",
|
1254
|
-
message: "Output directory for constants files:",
|
1255
|
-
default: defaultOutputDir,
|
1256
|
-
validate: (input) => {
|
1257
|
-
if (!input.trim()) {
|
1258
|
-
return "Output directory cannot be empty";
|
1259
|
-
}
|
1260
|
-
return true;
|
1261
|
-
},
|
1262
|
-
},
|
1263
|
-
]);
|
1264
|
-
try {
|
1265
|
-
const { ConstantsGenerator } = await import("./utils/constantsGenerator.js");
|
1266
|
-
const generator = new ConstantsGenerator(this.controller.config);
|
1267
|
-
MessageFormatter.info(`Generating constants for: ${languages.join(", ")}`, { prefix: "Constants" });
|
1268
|
-
await generator.generateFiles(languages, outputDir);
|
1269
|
-
MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" });
|
1270
|
-
}
|
1271
|
-
catch (error) {
|
1272
|
-
MessageFormatter.error("Failed to generate constants", error instanceof Error ? error : new Error(String(error)), { prefix: "Constants" });
|
1273
|
-
}
|
1274
|
-
}
|
1275
|
-
async importData() {
|
1276
|
-
console.log(chalk.yellow("Importing data..."));
|
1277
|
-
const { doBackup } = await inquirer.prompt([
|
1278
|
-
{
|
1279
|
-
type: "confirm",
|
1280
|
-
name: "doBackup",
|
1281
|
-
message: "Do you want to perform a backup before importing?",
|
1282
|
-
default: true,
|
1283
|
-
},
|
1284
|
-
]);
|
1285
|
-
const databases = await this.selectDatabases(await fetchAllDatabases(this.controller.database), "Select databases to import data into:", true);
|
1286
|
-
const collections = await this.selectCollections(databases[0], this.controller.database, "Select collections to import data into (leave empty for all):", true);
|
1287
|
-
const { shouldWriteFile } = await inquirer.prompt([
|
1288
|
-
{
|
1289
|
-
type: "confirm",
|
1290
|
-
name: "shouldWriteFile",
|
1291
|
-
message: "Do you want to write the imported data to a file?",
|
1292
|
-
default: false,
|
1293
|
-
},
|
1294
|
-
]);
|
1295
|
-
const options = {
|
1296
|
-
databases,
|
1297
|
-
collections: collections.map((c) => c.name),
|
1298
|
-
doBackup,
|
1299
|
-
importData: true,
|
1300
|
-
shouldWriteFile,
|
1301
|
-
};
|
1302
|
-
try {
|
1303
|
-
await this.controller.importData(options);
|
1304
|
-
console.log(chalk.green("Data import completed successfully."));
|
1305
|
-
}
|
1306
|
-
catch (error) {
|
1307
|
-
console.error(chalk.red("Error importing data:"), error);
|
1308
|
-
}
|
1309
|
-
}
|
1310
|
-
async transferData() {
|
1311
|
-
if (!this.controller.database) {
|
1312
|
-
throw new Error("Database is not initialized, is the config file correct & created?");
|
1313
|
-
}
|
1314
|
-
const { isRemote } = await inquirer.prompt([
|
1315
|
-
{
|
1316
|
-
type: "confirm",
|
1317
|
-
name: "isRemote",
|
1318
|
-
message: "Is this a remote transfer?",
|
1319
|
-
default: false,
|
1320
|
-
},
|
1321
|
-
]);
|
1322
|
-
let sourceClient = this.controller.database;
|
1323
|
-
let targetClient;
|
1324
|
-
let sourceDatabases;
|
1325
|
-
let targetDatabases;
|
1326
|
-
let remoteOptions;
|
1327
|
-
if (isRemote) {
|
1328
|
-
remoteOptions = await inquirer.prompt([
|
1329
|
-
{
|
1330
|
-
type: "input",
|
1331
|
-
name: "transferEndpoint",
|
1332
|
-
message: "Enter the remote endpoint:",
|
1333
|
-
},
|
1334
|
-
{
|
1335
|
-
type: "input",
|
1336
|
-
name: "transferProject",
|
1337
|
-
message: "Enter the remote project ID:",
|
1338
|
-
},
|
1339
|
-
{
|
1340
|
-
type: "input",
|
1341
|
-
name: "transferKey",
|
1342
|
-
message: "Enter the remote API key:",
|
1343
|
-
},
|
1344
|
-
]);
|
1345
|
-
const remoteClient = getClient(remoteOptions.transferEndpoint, remoteOptions.transferProject, remoteOptions.transferKey);
|
1346
|
-
targetClient = new Databases(remoteClient);
|
1347
|
-
sourceDatabases = await fetchAllDatabases(sourceClient);
|
1348
|
-
targetDatabases = await fetchAllDatabases(targetClient);
|
1349
|
-
}
|
1350
|
-
else {
|
1351
|
-
targetClient = sourceClient;
|
1352
|
-
const allDatabases = await fetchAllDatabases(sourceClient);
|
1353
|
-
sourceDatabases = targetDatabases = allDatabases;
|
1354
|
-
}
|
1355
|
-
const fromDbs = await this.selectDatabases(sourceDatabases, "Select the source database:", false);
|
1356
|
-
const fromDb = fromDbs[0];
|
1357
|
-
if (!fromDb) {
|
1358
|
-
throw new Error("No source database selected");
|
1359
|
-
}
|
1360
|
-
const availableDbs = targetDatabases.filter((db) => db.$id !== fromDb.$id);
|
1361
|
-
const targetDbs = await this.selectDatabases(availableDbs, "Select the target database:", false);
|
1362
|
-
const targetDb = targetDbs[0];
|
1363
|
-
if (!targetDb) {
|
1364
|
-
throw new Error("No target database selected");
|
1365
|
-
}
|
1366
|
-
const selectedCollections = await this.selectCollections(fromDb, sourceClient, "Select collections to transfer:", true, false // don't prefer local for transfers
|
1367
|
-
);
|
1368
|
-
const { transferStorage } = await inquirer.prompt([
|
1369
|
-
{
|
1370
|
-
type: "confirm",
|
1371
|
-
name: "transferStorage",
|
1372
|
-
message: "Do you want to transfer storage as well?",
|
1373
|
-
default: false,
|
1374
|
-
},
|
1375
|
-
]);
|
1376
|
-
let sourceBucket, targetBucket;
|
1377
|
-
if (transferStorage) {
|
1378
|
-
const sourceStorage = new Storage(this.controller.appwriteServer);
|
1379
|
-
const targetStorage = isRemote
|
1380
|
-
? new Storage(getClient(remoteOptions.transferEndpoint, remoteOptions.transferProject, remoteOptions.transferKey))
|
1381
|
-
: sourceStorage;
|
1382
|
-
const sourceBuckets = await listBuckets(sourceStorage);
|
1383
|
-
const targetBuckets = isRemote
|
1384
|
-
? await listBuckets(targetStorage)
|
1385
|
-
: sourceBuckets;
|
1386
|
-
const sourceBucketPicked = await this.selectBuckets(sourceBuckets.buckets, "Select the source bucket:", false);
|
1387
|
-
const targetBucketPicked = await this.selectBuckets(targetBuckets.buckets, "Select the target bucket:", false);
|
1388
|
-
sourceBucket = sourceBucketPicked[0];
|
1389
|
-
targetBucket = targetBucketPicked[0];
|
1390
|
-
}
|
1391
|
-
let transferOptions = {
|
1392
|
-
fromDb,
|
1393
|
-
targetDb,
|
1394
|
-
isRemote,
|
1395
|
-
collections: selectedCollections.length > 0
|
1396
|
-
? selectedCollections.map((c) => c.$id)
|
1397
|
-
: undefined,
|
1398
|
-
sourceBucket,
|
1399
|
-
targetBucket,
|
1400
|
-
};
|
1401
|
-
if (isRemote && remoteOptions) {
|
1402
|
-
transferOptions = {
|
1403
|
-
...transferOptions,
|
1404
|
-
...remoteOptions,
|
1405
|
-
};
|
1406
|
-
}
|
1407
|
-
console.log(chalk.yellow("Transferring data..."));
|
1408
|
-
await this.controller.transferData(transferOptions);
|
1409
|
-
console.log(chalk.green("Data transfer completed."));
|
1410
|
-
}
|
1411
726
|
getLocalCollections() {
|
1412
727
|
const configCollections = this.controller.config?.collections || [];
|
1413
728
|
// @ts-expect-error - appwrite invalid types
|
@@ -1422,6 +737,8 @@ export class InteractiveCLI {
|
|
1422
737
|
indexes: c.indexes || [],
|
1423
738
|
$permissions: PermissionToAppwritePermission(c.$permissions) || [],
|
1424
739
|
databaseId: c.databaseId,
|
740
|
+
_isFromTablesDir: c._isFromTablesDir || false,
|
741
|
+
_sourceFolder: c._isFromTablesDir ? 'tables' : 'collections',
|
1425
742
|
}));
|
1426
743
|
}
|
1427
744
|
getLocalDatabases() {
|
@@ -1434,58 +751,29 @@ export class InteractiveCLI {
|
|
1434
751
|
enabled: true,
|
1435
752
|
}));
|
1436
753
|
}
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
catch (error) {
|
1444
|
-
MessageFormatter.error("Failed to reload configuration files", error instanceof Error ? error : new Error(String(error)), { prefix: "Config" });
|
754
|
+
/**
|
755
|
+
* Extract session information from current controller for preservation
|
756
|
+
*/
|
757
|
+
extractSessionFromController() {
|
758
|
+
if (!this.controller?.config) {
|
759
|
+
return undefined;
|
1445
760
|
}
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
const allFunctions = [
|
1451
|
-
...remoteFunctions.functions,
|
1452
|
-
...localFunctions.filter((f) => !remoteFunctions.functions.some((rf) => rf.name === f.name)),
|
1453
|
-
];
|
1454
|
-
const functionsToUpdate = await inquirer.prompt([
|
1455
|
-
{
|
1456
|
-
type: "checkbox",
|
1457
|
-
name: "functionId",
|
1458
|
-
message: "Select functions to update:",
|
1459
|
-
choices: allFunctions.map((f) => ({
|
1460
|
-
name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name)
|
1461
|
-
? " (Local)"
|
1462
|
-
: " (Remote)"}`,
|
1463
|
-
value: f.$id,
|
1464
|
-
})),
|
1465
|
-
loop: true,
|
1466
|
-
},
|
1467
|
-
]);
|
1468
|
-
const specifications = await listSpecifications(this.controller.appwriteServer);
|
1469
|
-
const { specification } = await inquirer.prompt([
|
1470
|
-
{
|
1471
|
-
type: "list",
|
1472
|
-
name: "specification",
|
1473
|
-
message: "Select new specification:",
|
1474
|
-
choices: specifications.specifications.map((s) => ({
|
1475
|
-
name: `${s.slug}`,
|
1476
|
-
value: s.slug,
|
1477
|
-
})),
|
1478
|
-
},
|
1479
|
-
]);
|
1480
|
-
try {
|
1481
|
-
for (const functionId of functionsToUpdate.functionId) {
|
1482
|
-
await this.controller.updateFunctionSpecifications(functionId, specification);
|
1483
|
-
console.log(chalk.green(`Successfully updated function specification to ${specification}`));
|
1484
|
-
}
|
761
|
+
const sessionInfo = this.controller.getSessionInfo();
|
762
|
+
const config = this.controller.config;
|
763
|
+
if (!config.appwriteEndpoint || !config.appwriteProject) {
|
764
|
+
return undefined;
|
1485
765
|
}
|
1486
|
-
|
1487
|
-
|
766
|
+
const result = {
|
767
|
+
appwriteEndpoint: config.appwriteEndpoint,
|
768
|
+
appwriteProject: config.appwriteProject,
|
769
|
+
appwriteKey: config.appwriteKey
|
770
|
+
};
|
771
|
+
// Add session data if available
|
772
|
+
if (sessionInfo.hasSession) {
|
773
|
+
result.sessionCookie = this.controller.sessionCookie;
|
774
|
+
result.sessionMetadata = this.controller.sessionMetadata;
|
1488
775
|
}
|
776
|
+
return result;
|
1489
777
|
}
|
1490
778
|
async detectConfigurationType() {
|
1491
779
|
try {
|
@@ -1530,283 +818,4 @@ export class InteractiveCLI {
|
|
1530
818
|
return allChoices.filter(choice => choice !== CHOICES.MIGRATE_CONFIG);
|
1531
819
|
}
|
1532
820
|
}
|
1533
|
-
async migrateTypeScriptConfig() {
|
1534
|
-
try {
|
1535
|
-
MessageFormatter.info("Starting TypeScript to YAML configuration migration...", { prefix: "Migration" });
|
1536
|
-
// Perform the migration
|
1537
|
-
await migrateConfig(this.currentDir);
|
1538
|
-
// Reset the detection flag
|
1539
|
-
this.isUsingTypeScriptConfig = false;
|
1540
|
-
// Reset the controller to pick up the new config
|
1541
|
-
this.controller = undefined;
|
1542
|
-
MessageFormatter.success("Migration completed successfully!", { prefix: "Migration" });
|
1543
|
-
MessageFormatter.info("Your configuration has been migrated to the .appwrite directory structure", { prefix: "Migration" });
|
1544
|
-
MessageFormatter.info("You can now use YAML configuration for easier management", { prefix: "Migration" });
|
1545
|
-
}
|
1546
|
-
catch (error) {
|
1547
|
-
MessageFormatter.error("Migration failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" });
|
1548
|
-
}
|
1549
|
-
}
|
1550
|
-
async comprehensiveTransfer() {
|
1551
|
-
MessageFormatter.info("Starting comprehensive transfer configuration...", { prefix: "Transfer" });
|
1552
|
-
try {
|
1553
|
-
// Initialize controller to optionally load config if available (supports both YAML and TypeScript configs)
|
1554
|
-
await this.initControllerIfNeeded();
|
1555
|
-
// Check if user has an appwrite config for easier setup
|
1556
|
-
const hasAppwriteConfig = this.controller?.config?.appwriteEndpoint &&
|
1557
|
-
this.controller?.config?.appwriteProject &&
|
1558
|
-
this.controller?.config?.appwriteKey;
|
1559
|
-
let sourceConfig;
|
1560
|
-
let targetConfig;
|
1561
|
-
if (hasAppwriteConfig) {
|
1562
|
-
// Offer to use existing config for source
|
1563
|
-
const { useConfigForSource } = await inquirer.prompt([
|
1564
|
-
{
|
1565
|
-
type: "confirm",
|
1566
|
-
name: "useConfigForSource",
|
1567
|
-
message: "Use your current appwriteConfig as the source?",
|
1568
|
-
default: true,
|
1569
|
-
},
|
1570
|
-
]);
|
1571
|
-
if (useConfigForSource) {
|
1572
|
-
sourceConfig = {
|
1573
|
-
sourceEndpoint: this.controller.config.appwriteEndpoint,
|
1574
|
-
sourceProject: this.controller.config.appwriteProject,
|
1575
|
-
sourceKey: this.controller.config.appwriteKey,
|
1576
|
-
};
|
1577
|
-
MessageFormatter.info(`Using config source: ${sourceConfig.sourceEndpoint}`, { prefix: "Transfer" });
|
1578
|
-
}
|
1579
|
-
else {
|
1580
|
-
// Get source configuration manually
|
1581
|
-
sourceConfig = await inquirer.prompt([
|
1582
|
-
{
|
1583
|
-
type: "input",
|
1584
|
-
name: "sourceEndpoint",
|
1585
|
-
message: "Enter the source Appwrite endpoint:",
|
1586
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
1587
|
-
},
|
1588
|
-
{
|
1589
|
-
type: "input",
|
1590
|
-
name: "sourceProject",
|
1591
|
-
message: "Enter the source project ID:",
|
1592
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
1593
|
-
},
|
1594
|
-
{
|
1595
|
-
type: "password",
|
1596
|
-
name: "sourceKey",
|
1597
|
-
message: "Enter the source API key:",
|
1598
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
1599
|
-
},
|
1600
|
-
]);
|
1601
|
-
}
|
1602
|
-
// Offer to use existing config for target
|
1603
|
-
const { useConfigForTarget } = await inquirer.prompt([
|
1604
|
-
{
|
1605
|
-
type: "confirm",
|
1606
|
-
name: "useConfigForTarget",
|
1607
|
-
message: "Use your current appwriteConfig as the target?",
|
1608
|
-
default: false,
|
1609
|
-
},
|
1610
|
-
]);
|
1611
|
-
if (useConfigForTarget) {
|
1612
|
-
targetConfig = {
|
1613
|
-
targetEndpoint: this.controller.config.appwriteEndpoint,
|
1614
|
-
targetProject: this.controller.config.appwriteProject,
|
1615
|
-
targetKey: this.controller.config.appwriteKey,
|
1616
|
-
};
|
1617
|
-
MessageFormatter.info(`Using config target: ${targetConfig.targetEndpoint}`, { prefix: "Transfer" });
|
1618
|
-
}
|
1619
|
-
else {
|
1620
|
-
// Get target configuration manually
|
1621
|
-
targetConfig = await inquirer.prompt([
|
1622
|
-
{
|
1623
|
-
type: "input",
|
1624
|
-
name: "targetEndpoint",
|
1625
|
-
message: "Enter the target Appwrite endpoint:",
|
1626
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
1627
|
-
},
|
1628
|
-
{
|
1629
|
-
type: "input",
|
1630
|
-
name: "targetProject",
|
1631
|
-
message: "Enter the target project ID:",
|
1632
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
1633
|
-
},
|
1634
|
-
{
|
1635
|
-
type: "password",
|
1636
|
-
name: "targetKey",
|
1637
|
-
message: "Enter the target API key:",
|
1638
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
1639
|
-
},
|
1640
|
-
]);
|
1641
|
-
}
|
1642
|
-
}
|
1643
|
-
else {
|
1644
|
-
// No appwrite config found, get both configurations manually
|
1645
|
-
MessageFormatter.info("No appwriteConfig found, please enter source and target configurations manually", { prefix: "Transfer" });
|
1646
|
-
// Get source configuration
|
1647
|
-
sourceConfig = await inquirer.prompt([
|
1648
|
-
{
|
1649
|
-
type: "input",
|
1650
|
-
name: "sourceEndpoint",
|
1651
|
-
message: "Enter the source Appwrite endpoint:",
|
1652
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
1653
|
-
},
|
1654
|
-
{
|
1655
|
-
type: "input",
|
1656
|
-
name: "sourceProject",
|
1657
|
-
message: "Enter the source project ID:",
|
1658
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
1659
|
-
},
|
1660
|
-
{
|
1661
|
-
type: "password",
|
1662
|
-
name: "sourceKey",
|
1663
|
-
message: "Enter the source API key:",
|
1664
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
1665
|
-
},
|
1666
|
-
]);
|
1667
|
-
// Get target configuration
|
1668
|
-
targetConfig = await inquirer.prompt([
|
1669
|
-
{
|
1670
|
-
type: "input",
|
1671
|
-
name: "targetEndpoint",
|
1672
|
-
message: "Enter the target Appwrite endpoint:",
|
1673
|
-
validate: (input) => input.trim() !== "" || "Endpoint cannot be empty",
|
1674
|
-
},
|
1675
|
-
{
|
1676
|
-
type: "input",
|
1677
|
-
name: "targetProject",
|
1678
|
-
message: "Enter the target project ID:",
|
1679
|
-
validate: (input) => input.trim() !== "" || "Project ID cannot be empty",
|
1680
|
-
},
|
1681
|
-
{
|
1682
|
-
type: "password",
|
1683
|
-
name: "targetKey",
|
1684
|
-
message: "Enter the target API key:",
|
1685
|
-
validate: (input) => input.trim() !== "" || "API key cannot be empty",
|
1686
|
-
},
|
1687
|
-
]);
|
1688
|
-
}
|
1689
|
-
// Get transfer options
|
1690
|
-
const transferOptions = await inquirer.prompt([
|
1691
|
-
{
|
1692
|
-
type: "checkbox",
|
1693
|
-
name: "transferTypes",
|
1694
|
-
message: "Select what to transfer:",
|
1695
|
-
choices: [
|
1696
|
-
{ name: "👥 Users", value: "users", checked: true },
|
1697
|
-
{ name: "👥 Teams", value: "teams", checked: true },
|
1698
|
-
{ name: "🗄️ Databases", value: "databases", checked: true },
|
1699
|
-
{ name: "📦 Storage Buckets", value: "buckets", checked: true },
|
1700
|
-
{ name: "⚡ Functions", value: "functions", checked: true },
|
1701
|
-
],
|
1702
|
-
validate: (input) => input.length > 0 || "Select at least one transfer type",
|
1703
|
-
},
|
1704
|
-
{
|
1705
|
-
type: "list",
|
1706
|
-
name: "concurrencyLimit",
|
1707
|
-
message: "Select concurrency limit:",
|
1708
|
-
choices: [
|
1709
|
-
{ name: "5 (Conservative) - Users: 2, Files: 1", value: 5 },
|
1710
|
-
{ name: "10 (Balanced) - Users: 5, Files: 2", value: 10 },
|
1711
|
-
{ name: "15 - Users: 7, Files: 3", value: 15 },
|
1712
|
-
{ name: "20 - Users: 10, Files: 5", value: 20 },
|
1713
|
-
{ name: "25 - Users: 12, Files: 6", value: 25 },
|
1714
|
-
{ name: "30 - Users: 15, Files: 7", value: 30 },
|
1715
|
-
{ name: "35 - Users: 17, Files: 8", value: 35 },
|
1716
|
-
{ name: "40 - Users: 20, Files: 10", value: 40 },
|
1717
|
-
{ name: "45 - Users: 22, Files: 11", value: 45 },
|
1718
|
-
{ name: "50 - Users: 25, Files: 12", value: 50 },
|
1719
|
-
{ name: "55 - Users: 27, Files: 13", value: 55 },
|
1720
|
-
{ name: "60 - Users: 30, Files: 15", value: 60 },
|
1721
|
-
{ name: "65 - Users: 32, Files: 16", value: 65 },
|
1722
|
-
{ name: "70 - Users: 35, Files: 17", value: 70 },
|
1723
|
-
{ name: "75 - Users: 37, Files: 18", value: 75 },
|
1724
|
-
{ name: "80 - Users: 40, Files: 20", value: 80 },
|
1725
|
-
{ name: "85 - Users: 42, Files: 21", value: 85 },
|
1726
|
-
{ name: "90 - Users: 45, Files: 22", value: 90 },
|
1727
|
-
{ name: "95 - Users: 47, Files: 23", value: 95 },
|
1728
|
-
{ name: "100 (Aggressive) - Users: 50, Files: 25", value: 100 },
|
1729
|
-
],
|
1730
|
-
default: 10,
|
1731
|
-
},
|
1732
|
-
{
|
1733
|
-
type: "confirm",
|
1734
|
-
name: "dryRun",
|
1735
|
-
message: "Run in dry-run mode (no actual changes)?",
|
1736
|
-
default: false,
|
1737
|
-
},
|
1738
|
-
]);
|
1739
|
-
// Confirmation
|
1740
|
-
const { confirmed } = await inquirer.prompt([
|
1741
|
-
{
|
1742
|
-
type: "confirm",
|
1743
|
-
name: "confirmed",
|
1744
|
-
message: `Are you sure you want to ${transferOptions.dryRun ? "dry-run" : "perform"} comprehensive transfer from ${sourceConfig.sourceEndpoint} to ${targetConfig.targetEndpoint}?`,
|
1745
|
-
default: false,
|
1746
|
-
},
|
1747
|
-
]);
|
1748
|
-
if (!confirmed) {
|
1749
|
-
MessageFormatter.info("Transfer cancelled by user", { prefix: "Transfer" });
|
1750
|
-
return;
|
1751
|
-
}
|
1752
|
-
// Password preservation information
|
1753
|
-
if (transferOptions.transferTypes.includes("users") && !transferOptions.dryRun) {
|
1754
|
-
MessageFormatter.info("User Password Transfer Information:", { prefix: "Transfer" });
|
1755
|
-
MessageFormatter.info("✅ Users with hashed passwords (Argon2, Bcrypt, Scrypt, MD5, SHA, PHPass) will preserve their passwords", { prefix: "Transfer" });
|
1756
|
-
MessageFormatter.info("⚠️ Users without hash information will receive temporary passwords and need to reset", { prefix: "Transfer" });
|
1757
|
-
MessageFormatter.info("🔒 All user data (preferences, labels, verification status) will be preserved", { prefix: "Transfer" });
|
1758
|
-
const { continueWithUsers } = await inquirer.prompt([
|
1759
|
-
{
|
1760
|
-
type: "confirm",
|
1761
|
-
name: "continueWithUsers",
|
1762
|
-
message: "Continue with user transfer?",
|
1763
|
-
default: true,
|
1764
|
-
},
|
1765
|
-
]);
|
1766
|
-
if (!continueWithUsers) {
|
1767
|
-
// Remove users from transfer types
|
1768
|
-
transferOptions.transferTypes = transferOptions.transferTypes.filter((type) => type !== "users");
|
1769
|
-
if (transferOptions.transferTypes.length === 0) {
|
1770
|
-
MessageFormatter.info("No transfer types selected, cancelling", { prefix: "Transfer" });
|
1771
|
-
return;
|
1772
|
-
}
|
1773
|
-
}
|
1774
|
-
}
|
1775
|
-
// Execute comprehensive transfer
|
1776
|
-
const comprehensiveTransferOptions = {
|
1777
|
-
sourceEndpoint: sourceConfig.sourceEndpoint,
|
1778
|
-
sourceProject: sourceConfig.sourceProject,
|
1779
|
-
sourceKey: sourceConfig.sourceKey,
|
1780
|
-
targetEndpoint: targetConfig.targetEndpoint,
|
1781
|
-
targetProject: targetConfig.targetProject,
|
1782
|
-
targetKey: targetConfig.targetKey,
|
1783
|
-
transferUsers: transferOptions.transferTypes.includes("users"),
|
1784
|
-
transferTeams: transferOptions.transferTypes.includes("teams"),
|
1785
|
-
transferDatabases: transferOptions.transferTypes.includes("databases"),
|
1786
|
-
transferBuckets: transferOptions.transferTypes.includes("buckets"),
|
1787
|
-
transferFunctions: transferOptions.transferTypes.includes("functions"),
|
1788
|
-
concurrencyLimit: transferOptions.concurrencyLimit,
|
1789
|
-
dryRun: transferOptions.dryRun,
|
1790
|
-
};
|
1791
|
-
const transfer = new ComprehensiveTransfer(comprehensiveTransferOptions);
|
1792
|
-
const results = await transfer.execute();
|
1793
|
-
// Display results
|
1794
|
-
if (transferOptions.dryRun) {
|
1795
|
-
MessageFormatter.success("Dry run completed successfully!", { prefix: "Transfer" });
|
1796
|
-
}
|
1797
|
-
else {
|
1798
|
-
MessageFormatter.success("Comprehensive transfer completed!", { prefix: "Transfer" });
|
1799
|
-
if (transferOptions.transferTypes.includes("users") && results.users.transferred > 0) {
|
1800
|
-
MessageFormatter.info("Users with preserved password hashes can log in with their original passwords", { prefix: "Transfer" });
|
1801
|
-
MessageFormatter.info("Users with temporary passwords will need to reset their passwords", { prefix: "Transfer" });
|
1802
|
-
}
|
1803
|
-
if (transferOptions.transferTypes.includes("teams") && results.teams.transferred > 0) {
|
1804
|
-
MessageFormatter.info("Team memberships have been transferred and may require user acceptance of invitations", { prefix: "Transfer" });
|
1805
|
-
}
|
1806
|
-
}
|
1807
|
-
}
|
1808
|
-
catch (error) {
|
1809
|
-
MessageFormatter.error("Comprehensive transfer failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
1810
|
-
}
|
1811
|
-
}
|
1812
821
|
}
|