appwrite-utils-cli 1.7.9 → 1.8.2
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 +14 -199
- package/README.md +87 -30
- package/dist/adapters/AdapterFactory.js +5 -25
- package/dist/adapters/DatabaseAdapter.d.ts +17 -2
- package/dist/adapters/LegacyAdapter.d.ts +2 -1
- package/dist/adapters/LegacyAdapter.js +212 -16
- package/dist/adapters/TablesDBAdapter.d.ts +2 -12
- package/dist/adapters/TablesDBAdapter.js +261 -57
- package/dist/cli/commands/databaseCommands.js +4 -3
- package/dist/cli/commands/functionCommands.js +17 -8
- package/dist/collections/attributes.js +447 -125
- package/dist/collections/methods.js +197 -186
- package/dist/collections/tableOperations.d.ts +86 -0
- package/dist/collections/tableOperations.js +434 -0
- package/dist/collections/transferOperations.d.ts +3 -2
- package/dist/collections/transferOperations.js +93 -12
- package/dist/config/yamlConfig.d.ts +221 -88
- package/dist/examples/yamlTerminologyExample.d.ts +1 -1
- package/dist/examples/yamlTerminologyExample.js +6 -3
- package/dist/functions/fnConfigDiscovery.d.ts +3 -0
- package/dist/functions/fnConfigDiscovery.js +108 -0
- package/dist/interactiveCLI.js +18 -15
- package/dist/main.js +211 -73
- package/dist/migrations/appwriteToX.d.ts +88 -23
- package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
- package/dist/migrations/comprehensiveTransfer.js +83 -6
- package/dist/migrations/dataLoader.d.ts +227 -69
- package/dist/migrations/dataLoader.js +3 -3
- package/dist/migrations/importController.js +3 -3
- package/dist/migrations/relationships.d.ts +8 -2
- package/dist/migrations/services/ImportOrchestrator.js +3 -3
- package/dist/migrations/transfer.js +159 -37
- package/dist/shared/attributeMapper.d.ts +20 -0
- package/dist/shared/attributeMapper.js +203 -0
- package/dist/shared/selectionDialogs.js +8 -4
- package/dist/storage/schemas.d.ts +354 -92
- package/dist/utils/configDiscovery.js +4 -3
- package/dist/utils/versionDetection.d.ts +0 -4
- package/dist/utils/versionDetection.js +41 -173
- package/dist/utils/yamlConverter.js +89 -16
- package/dist/utils/yamlLoader.d.ts +1 -1
- package/dist/utils/yamlLoader.js +6 -2
- package/dist/utilsController.js +56 -19
- package/package.json +4 -4
- package/src/adapters/AdapterFactory.ts +119 -143
- package/src/adapters/DatabaseAdapter.ts +18 -3
- package/src/adapters/LegacyAdapter.ts +236 -105
- package/src/adapters/TablesDBAdapter.ts +773 -643
- package/src/cli/commands/databaseCommands.ts +13 -12
- package/src/cli/commands/functionCommands.ts +23 -14
- package/src/collections/attributes.ts +2054 -1611
- package/src/collections/methods.ts +208 -293
- package/src/collections/tableOperations.ts +506 -0
- package/src/collections/transferOperations.ts +218 -144
- package/src/examples/yamlTerminologyExample.ts +10 -5
- package/src/functions/fnConfigDiscovery.ts +103 -0
- package/src/interactiveCLI.ts +25 -20
- package/src/main.ts +549 -194
- package/src/migrations/comprehensiveTransfer.ts +126 -50
- package/src/migrations/dataLoader.ts +3 -3
- package/src/migrations/importController.ts +3 -3
- package/src/migrations/services/ImportOrchestrator.ts +3 -3
- package/src/migrations/transfer.ts +148 -131
- package/src/shared/attributeMapper.ts +229 -0
- package/src/shared/selectionDialogs.ts +29 -25
- package/src/utils/configDiscovery.ts +9 -3
- package/src/utils/versionDetection.ts +74 -228
- package/src/utils/yamlConverter.ts +94 -17
- package/src/utils/yamlLoader.ts +11 -4
- package/src/utilsController.ts +80 -30
package/dist/interactiveCLI.js
CHANGED
|
@@ -2,7 +2,7 @@ import inquirer from "inquirer";
|
|
|
2
2
|
import { UtilsController } from "./utilsController.js";
|
|
3
3
|
import { fetchAllCollections } from "./collections/methods.js";
|
|
4
4
|
import { listBuckets, createBucket } from "./storage/methods.js";
|
|
5
|
-
import { Databases, Storage, Client, Compression, Query, Functions, } from "node-appwrite";
|
|
5
|
+
import { Databases, Storage, Client, Compression, Query, Functions, DatabaseType, } from "node-appwrite";
|
|
6
6
|
import { PermissionToAppwritePermission, RuntimeSchema, permissionSchema, } from "appwrite-utils";
|
|
7
7
|
import { ulid } from "ulidx";
|
|
8
8
|
import chalk from "chalk";
|
|
@@ -192,10 +192,10 @@ export class InteractiveCLI {
|
|
|
192
192
|
const configDatabases = this.getLocalDatabases();
|
|
193
193
|
const allDatabases = [...databases, ...configDatabases]
|
|
194
194
|
.reduce((acc, db) => {
|
|
195
|
-
// Local config takes precedence - if a database with same name exists, use local version
|
|
196
|
-
const existingIndex = acc.findIndex((d) => d.name === db.name);
|
|
195
|
+
// Local config takes precedence - if a database with same name or ID exists, use local version
|
|
196
|
+
const existingIndex = acc.findIndex((d) => d.name === db.name || d.$id === db.$id);
|
|
197
197
|
if (existingIndex >= 0) {
|
|
198
|
-
if (configDatabases.some((cdb) => cdb.name === db.name)) {
|
|
198
|
+
if (configDatabases.some((cdb) => cdb.name === db.name || cdb.$id === db.$id)) {
|
|
199
199
|
acc[existingIndex] = db; // Replace with local version
|
|
200
200
|
}
|
|
201
201
|
}
|
|
@@ -204,14 +204,14 @@ export class InteractiveCLI {
|
|
|
204
204
|
}
|
|
205
205
|
return acc;
|
|
206
206
|
}, []);
|
|
207
|
-
const hasLocalAndRemote = allDatabases.some((db) => configDatabases.some((c) => c.name === db.name)) &&
|
|
208
|
-
allDatabases.some((db) => !configDatabases.some((c) => c.name === db.name));
|
|
207
|
+
const hasLocalAndRemote = allDatabases.some((db) => configDatabases.some((c) => c.name === db.name || c.$id === db.$id)) &&
|
|
208
|
+
allDatabases.some((db) => !configDatabases.some((c) => c.name === db.name || c.$id === db.$id));
|
|
209
209
|
const choices = allDatabases
|
|
210
210
|
.sort((a, b) => a.name.localeCompare(b.name))
|
|
211
211
|
.map((db) => ({
|
|
212
212
|
name: db.name +
|
|
213
213
|
(hasLocalAndRemote
|
|
214
|
-
? configDatabases.some((c) => c.name === db.name)
|
|
214
|
+
? configDatabases.some((c) => c.name === db.name || c.$id === db.$id)
|
|
215
215
|
? " (Local)"
|
|
216
216
|
: " (Remote)"
|
|
217
217
|
: ""),
|
|
@@ -245,20 +245,20 @@ export class InteractiveCLI {
|
|
|
245
245
|
}
|
|
246
246
|
let allCollections = preferLocal
|
|
247
247
|
? remoteCollections.reduce((acc, remoteCollection) => {
|
|
248
|
-
if (!acc.some((c) => c.name === remoteCollection.name)) {
|
|
248
|
+
if (!acc.some((c) => c.name === remoteCollection.name || c.$id === remoteCollection.$id)) {
|
|
249
249
|
acc.push(remoteCollection);
|
|
250
250
|
}
|
|
251
251
|
return acc;
|
|
252
252
|
}, [...configCollections])
|
|
253
253
|
: [
|
|
254
254
|
...remoteCollections,
|
|
255
|
-
...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
|
|
255
|
+
...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name || rc.$id === c.$id)),
|
|
256
256
|
];
|
|
257
257
|
if (shouldFilterByDatabase) {
|
|
258
258
|
// Show collections that EITHER exist in the remote database OR have matching local databaseId metadata
|
|
259
259
|
allCollections = allCollections.filter((c) => {
|
|
260
260
|
// Include if it exists remotely in this database
|
|
261
|
-
const existsInRemoteDb = remoteCollections.some((rc) => rc.name === c.name);
|
|
261
|
+
const existsInRemoteDb = remoteCollections.some((rc) => rc.name === c.name || rc.$id === c.$id);
|
|
262
262
|
// Include if local metadata claims it belongs to this database
|
|
263
263
|
const hasMatchingLocalMetadata = c.databaseId === database.$id;
|
|
264
264
|
return existsInRemoteDb || hasMatchingLocalMetadata;
|
|
@@ -266,8 +266,8 @@ export class InteractiveCLI {
|
|
|
266
266
|
}
|
|
267
267
|
// Filter out system tables (those starting with underscore)
|
|
268
268
|
allCollections = allCollections.filter((collection) => !collection.$id.startsWith('_'));
|
|
269
|
-
const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name)) &&
|
|
270
|
-
allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name));
|
|
269
|
+
const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name || c.$id === coll.$id)) &&
|
|
270
|
+
allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name || c.$id === coll.$id));
|
|
271
271
|
// Enhanced choice display with type indicators
|
|
272
272
|
const choices = allCollections
|
|
273
273
|
.sort((a, b) => {
|
|
@@ -280,7 +280,7 @@ export class InteractiveCLI {
|
|
|
280
280
|
return a.name.localeCompare(b.name);
|
|
281
281
|
})
|
|
282
282
|
.map((collection) => {
|
|
283
|
-
const localCollection = configCollections.find((c) => c.name === collection.name);
|
|
283
|
+
const localCollection = configCollections.find((c) => c.name === collection.name || c.$id === collection.$id);
|
|
284
284
|
const isLocal = !!localCollection;
|
|
285
285
|
const isTable = localCollection?._isFromTablesDir || collection._isFromTablesDir || false;
|
|
286
286
|
const sourceFolder = localCollection?._sourceFolder || collection._sourceFolder || 'collections';
|
|
@@ -350,6 +350,8 @@ export class InteractiveCLI {
|
|
|
350
350
|
else if (tablesCount > 0) {
|
|
351
351
|
MessageFormatter.info(`📊 ${tablesCount} tables available from tables/ folder`, { prefix: "Collections" });
|
|
352
352
|
}
|
|
353
|
+
// Show current database context clearly before view mode selection
|
|
354
|
+
MessageFormatter.info(`DB: ${database.name}`, { prefix: "Collections" });
|
|
353
355
|
// Ask user if they want to filter by database or show all
|
|
354
356
|
const { filterChoice } = await inquirer.prompt([
|
|
355
357
|
{
|
|
@@ -499,7 +501,7 @@ export class InteractiveCLI {
|
|
|
499
501
|
// Combine functions, preferring local ones
|
|
500
502
|
const allFunctions = [
|
|
501
503
|
...localFunctions,
|
|
502
|
-
...remoteFunctions.functions.filter((rf) => !localFunctions.some((lf) => lf.name === rf.name)),
|
|
504
|
+
...remoteFunctions.functions.filter((rf) => !localFunctions.some((lf) => lf.name === rf.name || lf.$id === rf.$id)),
|
|
503
505
|
];
|
|
504
506
|
const { selectedFunctions } = await inquirer.prompt([
|
|
505
507
|
{
|
|
@@ -507,7 +509,7 @@ export class InteractiveCLI {
|
|
|
507
509
|
name: "selectedFunctions",
|
|
508
510
|
message,
|
|
509
511
|
choices: allFunctions.map((f) => ({
|
|
510
|
-
name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name)
|
|
512
|
+
name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name || lf.$id === f.$id)
|
|
511
513
|
? " (Local)"
|
|
512
514
|
: " (Remote)"}`,
|
|
513
515
|
value: f,
|
|
@@ -773,6 +775,7 @@ export class InteractiveCLI {
|
|
|
773
775
|
$updatedAt: DateTime.now().toISO(),
|
|
774
776
|
name: db.name,
|
|
775
777
|
enabled: true,
|
|
778
|
+
type: "tablesdb",
|
|
776
779
|
}));
|
|
777
780
|
}
|
|
778
781
|
/**
|
package/dist/main.js
CHANGED
|
@@ -18,9 +18,9 @@ import { logger } from "./shared/logging.js";
|
|
|
18
18
|
import path from "path";
|
|
19
19
|
import fs from "fs";
|
|
20
20
|
import { createRequire } from "node:module";
|
|
21
|
-
import { loadAppwriteProjectConfig, findAppwriteProjectConfig, projectConfigToAppwriteConfig } from "./utils/projectConfig.js";
|
|
22
|
-
import { hasSessionAuth, getAvailableSessions, getAuthenticationStatus } from "./utils/sessionAuth.js";
|
|
23
|
-
import { findYamlConfig, loadYamlConfigWithSession } from "./config/yamlConfig.js";
|
|
21
|
+
import { loadAppwriteProjectConfig, findAppwriteProjectConfig, projectConfigToAppwriteConfig, } from "./utils/projectConfig.js";
|
|
22
|
+
import { hasSessionAuth, getAvailableSessions, getAuthenticationStatus, } from "./utils/sessionAuth.js";
|
|
23
|
+
import { findYamlConfig, loadYamlConfigWithSession, } from "./config/yamlConfig.js";
|
|
24
24
|
const require = createRequire(import.meta.url);
|
|
25
25
|
if (!globalThis.require) {
|
|
26
26
|
globalThis.require = require;
|
|
@@ -94,9 +94,9 @@ async function performEnhancedSync(controller, parsedArgv) {
|
|
|
94
94
|
const allowNewOnly = !syncExisting;
|
|
95
95
|
// Select databases
|
|
96
96
|
const selectedDatabaseIds = await SelectionDialogs.selectDatabases(availableDatabases, configuredDatabases, {
|
|
97
|
-
showSelectAll:
|
|
97
|
+
showSelectAll: false,
|
|
98
98
|
allowNewOnly,
|
|
99
|
-
defaultSelected:
|
|
99
|
+
defaultSelected: []
|
|
100
100
|
});
|
|
101
101
|
if (selectedDatabaseIds.length === 0) {
|
|
102
102
|
MessageFormatter.warning("No databases selected for sync", { prefix: "Sync" });
|
|
@@ -116,9 +116,9 @@ async function performEnhancedSync(controller, parsedArgv) {
|
|
|
116
116
|
const configuredTables = controller.config.collections || [];
|
|
117
117
|
// Select tables for this database
|
|
118
118
|
const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(databaseId, database.name, availableTables, configuredTables, {
|
|
119
|
-
showSelectAll:
|
|
119
|
+
showSelectAll: false,
|
|
120
120
|
allowNewOnly,
|
|
121
|
-
defaultSelected:
|
|
121
|
+
defaultSelected: []
|
|
122
122
|
});
|
|
123
123
|
tableSelectionsMap.set(databaseId, selectedTableIds);
|
|
124
124
|
if (selectedTableIds.length === 0) {
|
|
@@ -137,10 +137,10 @@ async function performEnhancedSync(controller, parsedArgv) {
|
|
|
137
137
|
// you'd fetch this from the Appwrite API
|
|
138
138
|
const availableBuckets = configuredBuckets; // Placeholder
|
|
139
139
|
selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(selectedDatabaseIds, availableBuckets, configuredBuckets, {
|
|
140
|
-
showSelectAll:
|
|
140
|
+
showSelectAll: false,
|
|
141
141
|
allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
|
|
142
142
|
groupByDatabase: true,
|
|
143
|
-
defaultSelected:
|
|
143
|
+
defaultSelected: []
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
catch (error) {
|
|
@@ -185,24 +185,28 @@ function checkMigrationConditions(configPath) {
|
|
|
185
185
|
if (!fs.existsSync(collectionsPath)) {
|
|
186
186
|
return {
|
|
187
187
|
allowed: false,
|
|
188
|
-
reason: "No collections/ folder found. Migration requires existing collections to migrate."
|
|
188
|
+
reason: "No collections/ folder found. Migration requires existing collections to migrate.",
|
|
189
189
|
};
|
|
190
190
|
}
|
|
191
191
|
// Check if collections/ folder has YAML files
|
|
192
|
-
const collectionFiles = fs
|
|
192
|
+
const collectionFiles = fs
|
|
193
|
+
.readdirSync(collectionsPath)
|
|
194
|
+
.filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
|
|
193
195
|
if (collectionFiles.length === 0) {
|
|
194
196
|
return {
|
|
195
197
|
allowed: false,
|
|
196
|
-
reason: "No YAML files found in collections/ folder. Migration requires existing collection YAML files."
|
|
198
|
+
reason: "No YAML files found in collections/ folder. Migration requires existing collection YAML files.",
|
|
197
199
|
};
|
|
198
200
|
}
|
|
199
201
|
// Check if tables/ folder exists and has YAML files
|
|
200
202
|
if (fs.existsSync(tablesPath)) {
|
|
201
|
-
const tableFiles = fs
|
|
203
|
+
const tableFiles = fs
|
|
204
|
+
.readdirSync(tablesPath)
|
|
205
|
+
.filter((file) => file.endsWith(".yaml") || file.endsWith(".yml"));
|
|
202
206
|
if (tableFiles.length > 0) {
|
|
203
207
|
return {
|
|
204
208
|
allowed: false,
|
|
205
|
-
reason: `Tables folder already exists with ${tableFiles.length} YAML file(s). Migration appears to have already been completed
|
|
209
|
+
reason: `Tables folder already exists with ${tableFiles.length} YAML file(s). Migration appears to have already been completed.`,
|
|
206
210
|
};
|
|
207
211
|
}
|
|
208
212
|
}
|
|
@@ -433,16 +437,23 @@ async function main() {
|
|
|
433
437
|
// Enhanced config creation with session and project file support
|
|
434
438
|
let directConfig = undefined;
|
|
435
439
|
// Show authentication status on startup if no config provided
|
|
436
|
-
if (!argv.config &&
|
|
440
|
+
if (!argv.config &&
|
|
441
|
+
!argv.endpoint &&
|
|
442
|
+
!argv.projectId &&
|
|
443
|
+
!argv.apiKey &&
|
|
444
|
+
!argv.useSession &&
|
|
445
|
+
!argv.sessionCookie) {
|
|
437
446
|
if (hasAnyValidSessions) {
|
|
438
447
|
MessageFormatter.info(`Found ${availableSessions.length} available session(s)`, { prefix: "Auth" });
|
|
439
|
-
availableSessions.forEach(session => {
|
|
440
|
-
MessageFormatter.info(` \u2022 ${session.projectId} (${session.email ||
|
|
448
|
+
availableSessions.forEach((session) => {
|
|
449
|
+
MessageFormatter.info(` \u2022 ${session.projectId} (${session.email || "unknown"}) at ${session.endpoint}`, { prefix: "Auth" });
|
|
441
450
|
});
|
|
442
451
|
MessageFormatter.info("Use --session to enable session authentication", { prefix: "Auth" });
|
|
443
452
|
}
|
|
444
453
|
else {
|
|
445
|
-
MessageFormatter.info("No active Appwrite sessions found", {
|
|
454
|
+
MessageFormatter.info("No active Appwrite sessions found", {
|
|
455
|
+
prefix: "Auth",
|
|
456
|
+
});
|
|
446
457
|
MessageFormatter.info("\u2022 Run 'appwrite login' to authenticate with session", { prefix: "Auth" });
|
|
447
458
|
MessageFormatter.info("\u2022 Or provide --apiKey for API key authentication", { prefix: "Auth" });
|
|
448
459
|
}
|
|
@@ -457,7 +468,11 @@ async function main() {
|
|
|
457
468
|
}
|
|
458
469
|
}
|
|
459
470
|
// Priority 2: CLI arguments override project config
|
|
460
|
-
if (argv.endpoint ||
|
|
471
|
+
if (argv.endpoint ||
|
|
472
|
+
argv.projectId ||
|
|
473
|
+
argv.apiKey ||
|
|
474
|
+
argv.useSession ||
|
|
475
|
+
argv.sessionCookie) {
|
|
461
476
|
directConfig = {
|
|
462
477
|
...directConfig,
|
|
463
478
|
appwriteEndpoint: argv.endpoint || directConfig?.appwriteEndpoint,
|
|
@@ -482,7 +497,9 @@ async function main() {
|
|
|
482
497
|
MessageFormatter.warning("Session authentication requested but no valid session found", { prefix: "Auth" });
|
|
483
498
|
const availableSessions = getAvailableSessions();
|
|
484
499
|
if (availableSessions.length > 0) {
|
|
485
|
-
MessageFormatter.info(`Available sessions: ${availableSessions
|
|
500
|
+
MessageFormatter.info(`Available sessions: ${availableSessions
|
|
501
|
+
.map((s) => `${s.projectId} (${s.email || "unknown"})`)
|
|
502
|
+
.join(", ")}`, { prefix: "Auth" });
|
|
486
503
|
MessageFormatter.info("Use --session flag to enable session authentication", { prefix: "Auth" });
|
|
487
504
|
}
|
|
488
505
|
else {
|
|
@@ -503,11 +520,16 @@ async function main() {
|
|
|
503
520
|
// 3. Auto-detect session authentication when possible
|
|
504
521
|
let finalDirectConfig = directConfig;
|
|
505
522
|
if ((argv.useSession || argv.sessionCookie) &&
|
|
506
|
-
(!directConfig ||
|
|
523
|
+
(!directConfig ||
|
|
524
|
+
!directConfig.appwriteEndpoint ||
|
|
525
|
+
!directConfig.appwriteProject)) {
|
|
507
526
|
// Don't pass incomplete directConfig - let UtilsController load YAML config normally
|
|
508
527
|
finalDirectConfig = null;
|
|
509
528
|
}
|
|
510
|
-
else if (finalDirectConfig &&
|
|
529
|
+
else if (finalDirectConfig &&
|
|
530
|
+
!finalDirectConfig.appwriteKey &&
|
|
531
|
+
!argv.useSession &&
|
|
532
|
+
!argv.sessionCookie) {
|
|
511
533
|
// Auto-detect session authentication when no API key provided
|
|
512
534
|
if (sessionAuthAvailable) {
|
|
513
535
|
MessageFormatter.info("No API key provided, but session authentication is available", { prefix: "Auth" });
|
|
@@ -539,10 +561,14 @@ async function main() {
|
|
|
539
561
|
if (argv.generateConstants) {
|
|
540
562
|
const { ConstantsGenerator } = await import("./utils/constantsGenerator.js");
|
|
541
563
|
if (!controller.config) {
|
|
542
|
-
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
564
|
+
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
565
|
+
prefix: "Constants",
|
|
566
|
+
});
|
|
543
567
|
return;
|
|
544
568
|
}
|
|
545
|
-
const languages = argv
|
|
569
|
+
const languages = argv
|
|
570
|
+
.constantsLanguages.split(",")
|
|
571
|
+
.map((l) => l.trim());
|
|
546
572
|
// Determine output directory - use config folder/constants by default, or custom path if specified
|
|
547
573
|
let outputDir;
|
|
548
574
|
if (argv.constantsOutput === "auto") {
|
|
@@ -560,13 +586,17 @@ async function main() {
|
|
|
560
586
|
const generator = new ConstantsGenerator(controller.config);
|
|
561
587
|
await generator.generateFiles(languages, outputDir);
|
|
562
588
|
operationStats.generatedConstants = languages.length;
|
|
563
|
-
MessageFormatter.success(`Constants generated in ${outputDir}`, {
|
|
589
|
+
MessageFormatter.success(`Constants generated in ${outputDir}`, {
|
|
590
|
+
prefix: "Constants",
|
|
591
|
+
});
|
|
564
592
|
return;
|
|
565
593
|
}
|
|
566
594
|
if (argv.migrateCollectionsToTables) {
|
|
567
595
|
try {
|
|
568
596
|
if (!controller.config) {
|
|
569
|
-
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
597
|
+
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
598
|
+
prefix: "Migration",
|
|
599
|
+
});
|
|
570
600
|
return;
|
|
571
601
|
}
|
|
572
602
|
// Get the config path from the controller or use .appwrite in current directory
|
|
@@ -587,18 +617,22 @@ async function main() {
|
|
|
587
617
|
const migrationCheck = checkMigrationConditions(configPath);
|
|
588
618
|
if (!migrationCheck.allowed) {
|
|
589
619
|
MessageFormatter.error(`Migration not allowed: ${migrationCheck.reason}`, undefined, { prefix: "Migration" });
|
|
590
|
-
MessageFormatter.info("Migration requirements:", {
|
|
620
|
+
MessageFormatter.info("Migration requirements:", {
|
|
621
|
+
prefix: "Migration",
|
|
622
|
+
});
|
|
591
623
|
MessageFormatter.info(" • Configuration must be loaded (use --config or have .appwrite/ folder)", { prefix: "Migration" });
|
|
592
624
|
MessageFormatter.info(" • collections/ folder must exist with YAML files", { prefix: "Migration" });
|
|
593
625
|
MessageFormatter.info(" • tables/ folder must not exist or be empty", { prefix: "Migration" });
|
|
594
626
|
return;
|
|
595
627
|
}
|
|
596
628
|
const { migrateCollectionsToTables } = await import("./config/configMigration.js");
|
|
597
|
-
MessageFormatter.info("Starting collections to tables migration...", {
|
|
629
|
+
MessageFormatter.info("Starting collections to tables migration...", {
|
|
630
|
+
prefix: "Migration",
|
|
631
|
+
});
|
|
598
632
|
const result = migrateCollectionsToTables(controller.config, {
|
|
599
633
|
strategy: "full_migration",
|
|
600
634
|
validateResult: true,
|
|
601
|
-
dryRun: false
|
|
635
|
+
dryRun: false,
|
|
602
636
|
});
|
|
603
637
|
if (result.success) {
|
|
604
638
|
operationStats.migratedCollections = result.changes.length;
|
|
@@ -619,17 +653,29 @@ async function main() {
|
|
|
619
653
|
// Provide better guidance based on available authentication methods
|
|
620
654
|
const availableSessions = getAvailableSessions();
|
|
621
655
|
if (availableSessions.length > 0) {
|
|
622
|
-
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
623
|
-
|
|
624
|
-
|
|
656
|
+
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
657
|
+
prefix: "CLI",
|
|
658
|
+
});
|
|
659
|
+
MessageFormatter.info("Available authentication options:", {
|
|
660
|
+
prefix: "Auth",
|
|
661
|
+
});
|
|
662
|
+
MessageFormatter.info("• Session authentication: Add --session flag", {
|
|
663
|
+
prefix: "Auth",
|
|
664
|
+
});
|
|
625
665
|
MessageFormatter.info("• API key authentication: Add --apiKey YOUR_API_KEY", { prefix: "Auth" });
|
|
626
|
-
MessageFormatter.info(`• Available sessions: ${availableSessions
|
|
666
|
+
MessageFormatter.info(`• Available sessions: ${availableSessions
|
|
667
|
+
.map((s) => `${s.projectId} (${s.email || "unknown"})`)
|
|
668
|
+
.join(", ")}`, { prefix: "Auth" });
|
|
627
669
|
}
|
|
628
670
|
else {
|
|
629
|
-
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
671
|
+
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
672
|
+
prefix: "CLI",
|
|
673
|
+
});
|
|
630
674
|
MessageFormatter.info("Authentication options:", { prefix: "Auth" });
|
|
631
675
|
MessageFormatter.info("• Login with Appwrite CLI: Run 'appwrite login' then use --session flag", { prefix: "Auth" });
|
|
632
|
-
MessageFormatter.info("• Use API key: Add --apiKey YOUR_API_KEY", {
|
|
676
|
+
MessageFormatter.info("• Use API key: Add --apiKey YOUR_API_KEY", {
|
|
677
|
+
prefix: "Auth",
|
|
678
|
+
});
|
|
633
679
|
MessageFormatter.info("• Create config file: Run with --setup to initialize project configuration", { prefix: "Auth" });
|
|
634
680
|
}
|
|
635
681
|
return;
|
|
@@ -640,13 +686,15 @@ async function main() {
|
|
|
640
686
|
const { AdapterFactory } = await import("./adapters/AdapterFactory.js");
|
|
641
687
|
const { listBackups } = await import("./shared/backupTracking.js");
|
|
642
688
|
if (!controller.config) {
|
|
643
|
-
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
689
|
+
MessageFormatter.error("No Appwrite configuration found", undefined, {
|
|
690
|
+
prefix: "Backups",
|
|
691
|
+
});
|
|
644
692
|
return;
|
|
645
693
|
}
|
|
646
694
|
const { adapter } = await AdapterFactory.create({
|
|
647
695
|
appwriteEndpoint: controller.config.appwriteEndpoint,
|
|
648
696
|
appwriteProject: controller.config.appwriteProject,
|
|
649
|
-
appwriteKey: controller.config.appwriteKey
|
|
697
|
+
appwriteKey: controller.config.appwriteKey,
|
|
650
698
|
});
|
|
651
699
|
const databases = parsedArgv.dbIds
|
|
652
700
|
? await controller.getDatabasesByIds(parsedArgv.dbIds.split(","))
|
|
@@ -699,11 +747,16 @@ async function main() {
|
|
|
699
747
|
await controller.updateFunctionSpecifications(parsedArgv.functionId, parsedArgv.specification);
|
|
700
748
|
}
|
|
701
749
|
// Add default databases if not specified (only if we need them for operations)
|
|
702
|
-
const needsDatabases = options.doBackup ||
|
|
703
|
-
options.
|
|
704
|
-
options.
|
|
705
|
-
|
|
706
|
-
|
|
750
|
+
const needsDatabases = options.doBackup ||
|
|
751
|
+
options.wipeDatabase ||
|
|
752
|
+
options.wipeDocumentStorage ||
|
|
753
|
+
options.wipeUsers ||
|
|
754
|
+
options.wipeCollections ||
|
|
755
|
+
options.importData ||
|
|
756
|
+
parsedArgv.sync ||
|
|
757
|
+
parsedArgv.transfer;
|
|
758
|
+
if (needsDatabases &&
|
|
759
|
+
(!options.databases || options.databases.length === 0)) {
|
|
707
760
|
const allDatabases = await fetchAllDatabases(controller.database);
|
|
708
761
|
options.databases = allDatabases;
|
|
709
762
|
}
|
|
@@ -736,58 +789,62 @@ async function main() {
|
|
|
736
789
|
else {
|
|
737
790
|
// Interactive selection
|
|
738
791
|
const inquirer = (await import("inquirer")).default;
|
|
739
|
-
const answer = await inquirer.prompt([
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
792
|
+
const answer = await inquirer.prompt([
|
|
793
|
+
{
|
|
794
|
+
type: "list",
|
|
795
|
+
name: "trackingDb",
|
|
796
|
+
message: "Select database to store backup tracking metadata:",
|
|
797
|
+
choices: allDatabases.map((db) => ({
|
|
744
798
|
name: `${db.name} (${db.$id})`,
|
|
745
|
-
value: db.$id
|
|
746
|
-
}))
|
|
747
|
-
}
|
|
799
|
+
value: db.$id,
|
|
800
|
+
})),
|
|
801
|
+
},
|
|
802
|
+
]);
|
|
748
803
|
trackingDatabaseId = answer.trackingDb;
|
|
749
804
|
}
|
|
750
805
|
}
|
|
751
806
|
// Ensure trackingDatabaseId is defined before proceeding
|
|
752
807
|
if (!trackingDatabaseId) {
|
|
753
|
-
throw new Error(
|
|
808
|
+
throw new Error("Tracking database ID is required for comprehensive backup");
|
|
754
809
|
}
|
|
755
|
-
MessageFormatter.info(`Using tracking database: ${trackingDatabaseId}`, {
|
|
810
|
+
MessageFormatter.info(`Using tracking database: ${trackingDatabaseId}`, {
|
|
811
|
+
prefix: "Backup",
|
|
812
|
+
});
|
|
756
813
|
// Create adapter for backup tracking
|
|
757
814
|
const { adapter } = await AdapterFactory.create({
|
|
758
815
|
appwriteEndpoint: controller.config.appwriteEndpoint,
|
|
759
816
|
appwriteProject: controller.config.appwriteProject,
|
|
760
817
|
appwriteKey: controller.config.appwriteKey,
|
|
761
|
-
sessionCookie: controller.config.sessionCookie
|
|
818
|
+
sessionCookie: controller.config.sessionCookie,
|
|
762
819
|
});
|
|
763
820
|
const result = await comprehensiveBackup(controller.config, controller.database, controller.storage, adapter, {
|
|
764
821
|
trackingDatabaseId,
|
|
765
|
-
backupFormat: parsedArgv.backupFormat ||
|
|
822
|
+
backupFormat: parsedArgv.backupFormat || "zip",
|
|
766
823
|
parallelDownloads: parsedArgv.parallelDownloads || 10,
|
|
767
824
|
onProgress: (message) => {
|
|
768
825
|
MessageFormatter.info(message, { prefix: "Backup" });
|
|
769
|
-
}
|
|
826
|
+
},
|
|
770
827
|
});
|
|
771
828
|
operationStats.comprehensiveBackup = 1;
|
|
772
829
|
operationStats.databasesBackedUp = result.databaseBackups.length;
|
|
773
830
|
operationStats.bucketsBackedUp = result.bucketBackups.length;
|
|
774
831
|
operationStats.totalBackupSize = result.totalSizeBytes;
|
|
775
|
-
if (result.status ===
|
|
832
|
+
if (result.status === "completed") {
|
|
776
833
|
MessageFormatter.success(`Comprehensive backup completed successfully (ID: ${result.backupId})`, { prefix: "Backup" });
|
|
777
834
|
}
|
|
778
|
-
else if (result.status ===
|
|
835
|
+
else if (result.status === "partial") {
|
|
779
836
|
MessageFormatter.warning(`Comprehensive backup completed with errors (ID: ${result.backupId})`, { prefix: "Backup" });
|
|
780
|
-
result.errors.forEach(err => MessageFormatter.warning(err, { prefix: "Backup" }));
|
|
837
|
+
result.errors.forEach((err) => MessageFormatter.warning(err, { prefix: "Backup" }));
|
|
781
838
|
}
|
|
782
839
|
else {
|
|
783
840
|
MessageFormatter.error(`Comprehensive backup failed (ID: ${result.backupId})`, undefined, { prefix: "Backup" });
|
|
784
|
-
result.errors.forEach(err => MessageFormatter.error(err, undefined, { prefix: "Backup" }));
|
|
841
|
+
result.errors.forEach((err) => MessageFormatter.error(err, undefined, { prefix: "Backup" }));
|
|
785
842
|
}
|
|
786
843
|
}
|
|
787
844
|
if (options.doBackup && options.databases) {
|
|
788
845
|
MessageFormatter.info(`Creating backups for ${options.databases.length} database(s) in ${parsedArgv.backupFormat} format`, { prefix: "Backup" });
|
|
789
846
|
for (const db of options.databases) {
|
|
790
|
-
await controller.backupDatabase(db, parsedArgv.backupFormat ||
|
|
847
|
+
await controller.backupDatabase(db, parsedArgv.backupFormat || "json");
|
|
791
848
|
}
|
|
792
849
|
operationStats.backups = options.databases.length;
|
|
793
850
|
MessageFormatter.success(`Backup completed for ${options.databases.length} database(s)`, { prefix: "Backup" });
|
|
@@ -797,10 +854,10 @@ async function main() {
|
|
|
797
854
|
options.wipeUsers ||
|
|
798
855
|
options.wipeCollections) {
|
|
799
856
|
// Confirm destructive operations
|
|
800
|
-
const databaseNames = options.databases?.map(db => db.name) || [];
|
|
857
|
+
const databaseNames = options.databases?.map((db) => db.name) || [];
|
|
801
858
|
const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, {
|
|
802
859
|
includeStorage: options.wipeDocumentStorage,
|
|
803
|
-
includeUsers: options.wipeUsers
|
|
860
|
+
includeUsers: options.wipeUsers,
|
|
804
861
|
});
|
|
805
862
|
if (!confirmed) {
|
|
806
863
|
MessageFormatter.info("Operation cancelled by user", { prefix: "CLI" });
|
|
@@ -842,7 +899,7 @@ async function main() {
|
|
|
842
899
|
const dbCollections = await fetchAllCollections(db.$id, controller.database);
|
|
843
900
|
const collectionsToWipe = dbCollections.filter((c) => options.collections.includes(c.$id));
|
|
844
901
|
// Confirm collection wipe
|
|
845
|
-
const collectionNames = collectionsToWipe.map(c => c.name);
|
|
902
|
+
const collectionNames = collectionsToWipe.map((c) => c.name);
|
|
846
903
|
const collectionConfirmed = await ConfirmationDialogs.confirmCollectionWipe(db.name, collectionNames);
|
|
847
904
|
if (collectionConfirmed) {
|
|
848
905
|
for (const collection of collectionsToWipe) {
|
|
@@ -853,7 +910,10 @@ async function main() {
|
|
|
853
910
|
}
|
|
854
911
|
}
|
|
855
912
|
// Show wipe operation summary
|
|
856
|
-
if (wipeStats.databases > 0 ||
|
|
913
|
+
if (wipeStats.databases > 0 ||
|
|
914
|
+
wipeStats.collections > 0 ||
|
|
915
|
+
wipeStats.users > 0 ||
|
|
916
|
+
wipeStats.buckets > 0) {
|
|
857
917
|
operationStats.wipedDatabases = wipeStats.databases;
|
|
858
918
|
operationStats.wipedCollections = wipeStats.collections;
|
|
859
919
|
operationStats.wipedUsers = wipeStats.users;
|
|
@@ -861,12 +921,84 @@ async function main() {
|
|
|
861
921
|
}
|
|
862
922
|
}
|
|
863
923
|
if (parsedArgv.push) {
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
924
|
+
await controller.init();
|
|
925
|
+
if (!controller.database || !controller.config) {
|
|
926
|
+
MessageFormatter.error("Database or config not initialized", undefined, { prefix: "Push" });
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
// Fetch available DBs
|
|
930
|
+
const availableDatabases = await fetchAllDatabases(controller.database);
|
|
931
|
+
if (availableDatabases.length === 0) {
|
|
932
|
+
MessageFormatter.warning("No databases found in remote project", { prefix: "Push" });
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
// Determine selected DBs
|
|
936
|
+
let selectedDbIds = [];
|
|
937
|
+
if (parsedArgv.dbIds) {
|
|
938
|
+
selectedDbIds = parsedArgv.dbIds.split(/[,\s]+/).filter(Boolean);
|
|
939
|
+
}
|
|
940
|
+
else {
|
|
941
|
+
selectedDbIds = await SelectionDialogs.selectDatabases(availableDatabases, controller.config.databases || [], { showSelectAll: false, allowNewOnly: false, defaultSelected: [] });
|
|
942
|
+
}
|
|
943
|
+
if (selectedDbIds.length === 0) {
|
|
944
|
+
MessageFormatter.warning("No databases selected for push", { prefix: "Push" });
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
// Build DatabaseSelection[] with tableIds per DB
|
|
948
|
+
const databaseSelections = [];
|
|
949
|
+
const allConfigItems = controller.config.collections || controller.config.tables || [];
|
|
950
|
+
for (const dbId of selectedDbIds) {
|
|
951
|
+
const db = availableDatabases.find(d => d.$id === dbId);
|
|
952
|
+
if (!db)
|
|
953
|
+
continue;
|
|
954
|
+
// Filter config items eligible for this DB according to databaseId/databaseIds rule
|
|
955
|
+
const eligibleConfigItems = allConfigItems.filter(item => {
|
|
956
|
+
const one = item.databaseId;
|
|
957
|
+
const many = item.databaseIds;
|
|
958
|
+
if (Array.isArray(many) && many.length > 0)
|
|
959
|
+
return many.includes(dbId);
|
|
960
|
+
if (one)
|
|
961
|
+
return one === dbId;
|
|
962
|
+
return true; // eligible everywhere if unspecified
|
|
963
|
+
});
|
|
964
|
+
// Fetch available tables from remote for selection context
|
|
965
|
+
const availableTables = await fetchAllCollections(dbId, controller.database);
|
|
966
|
+
// Determine selected table IDs
|
|
967
|
+
let selectedTableIds = [];
|
|
968
|
+
if (parsedArgv.collectionIds) {
|
|
969
|
+
const ids = parsedArgv.collectionIds.split(/[,\s]+/).filter(Boolean);
|
|
970
|
+
// Only allow IDs that are in eligible config items
|
|
971
|
+
const eligibleIds = new Set(eligibleConfigItems.map((c) => c.$id || c.id));
|
|
972
|
+
selectedTableIds = ids.filter(id => eligibleIds.has(id));
|
|
973
|
+
}
|
|
974
|
+
else {
|
|
975
|
+
selectedTableIds = await SelectionDialogs.selectTablesForDatabase(dbId, db.name, availableTables, eligibleConfigItems, { showSelectAll: false, allowNewOnly: true, defaultSelected: [] });
|
|
976
|
+
}
|
|
977
|
+
databaseSelections.push({
|
|
978
|
+
databaseId: db.$id,
|
|
979
|
+
databaseName: db.name,
|
|
980
|
+
tableIds: selectedTableIds,
|
|
981
|
+
tableNames: [],
|
|
982
|
+
isNew: false,
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
|
|
986
|
+
MessageFormatter.warning("No tables/collections selected for push", { prefix: "Push" });
|
|
987
|
+
return;
|
|
988
|
+
}
|
|
989
|
+
const pushSummary = {
|
|
990
|
+
databases: databaseSelections.length,
|
|
991
|
+
collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
|
|
992
|
+
details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
|
|
993
|
+
};
|
|
994
|
+
const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
|
|
995
|
+
if (!confirmed) {
|
|
996
|
+
MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
await controller.selectivePush(databaseSelections, []);
|
|
1000
|
+
operationStats.pushedDatabases = databaseSelections.length;
|
|
1001
|
+
operationStats.pushedCollections = databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0);
|
|
870
1002
|
}
|
|
871
1003
|
else if (parsedArgv.sync) {
|
|
872
1004
|
// Enhanced SYNC: Pull from remote with intelligent configuration detection
|
|
@@ -905,7 +1037,9 @@ async function main() {
|
|
|
905
1037
|
MessageFormatter.info(`Starting database transfer from ${parsedArgv.fromDbId} to ${parsedArgv.toDbId}`, { prefix: "Transfer" });
|
|
906
1038
|
fromDb = (await controller.getDatabasesByIds([parsedArgv.fromDbId]))?.[0];
|
|
907
1039
|
if (!fromDb) {
|
|
908
|
-
MessageFormatter.error("Source database not found", undefined, {
|
|
1040
|
+
MessageFormatter.error("Source database not found", undefined, {
|
|
1041
|
+
prefix: "Transfer",
|
|
1042
|
+
});
|
|
909
1043
|
return;
|
|
910
1044
|
}
|
|
911
1045
|
if (isRemote) {
|
|
@@ -920,14 +1054,18 @@ async function main() {
|
|
|
920
1054
|
const remoteDbs = await fetchAllDatabases(targetDatabases);
|
|
921
1055
|
toDb = remoteDbs.find((db) => db.$id === parsedArgv.toDbId);
|
|
922
1056
|
if (!toDb) {
|
|
923
|
-
MessageFormatter.error("Target database not found", undefined, {
|
|
1057
|
+
MessageFormatter.error("Target database not found", undefined, {
|
|
1058
|
+
prefix: "Transfer",
|
|
1059
|
+
});
|
|
924
1060
|
return;
|
|
925
1061
|
}
|
|
926
1062
|
}
|
|
927
1063
|
else {
|
|
928
1064
|
toDb = (await controller.getDatabasesByIds([parsedArgv.toDbId]))?.[0];
|
|
929
1065
|
if (!toDb) {
|
|
930
|
-
MessageFormatter.error("Target database not found", undefined, {
|
|
1066
|
+
MessageFormatter.error("Target database not found", undefined, {
|
|
1067
|
+
prefix: "Transfer",
|
|
1068
|
+
});
|
|
931
1069
|
return;
|
|
932
1070
|
}
|
|
933
1071
|
}
|