UNPKG

appwrite-utils-cli

Version:

Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.

471 lines (470 loc) 20.4 kB
#!/usr/bin/env node import yargs from "yargs"; import {} from "yargs"; import { hideBin } from "yargs/helpers"; import { InteractiveCLI } from "./interactiveCLI.js"; import { UtilsController } from "./utilsController.js"; import { Databases, Storage } from "node-appwrite"; import { getClient } from "./utils/getClientFromConfig.js"; import { fetchAllDatabases } from "./databases/methods.js"; import { setupDirsFiles } from "./utils/setupFiles.js"; import { fetchAllCollections } from "./collections/methods.js"; import chalk from "chalk"; import { listSpecifications } from "./functions/methods.js"; import { MessageFormatter } from "./shared/messageFormatter.js"; import { ConfirmationDialogs } from "./shared/confirmationDialogs.js"; import path from "path"; const argv = yargs(hideBin(process.argv)) .option("config", { type: "string", description: "Path to Appwrite configuration file (appwriteConfig.ts)", }) .option("it", { alias: ["interactive", "i"], type: "boolean", description: "Launch interactive CLI mode with guided prompts", }) .option("dbIds", { type: "string", description: "Comma-separated list of database IDs to target (e.g., 'db1,db2,db3')", }) .option("collectionIds", { alias: ["collIds"], type: "string", description: "Comma-separated list of collection IDs to target (e.g., 'users,posts')", }) .option("bucketIds", { type: "string", description: "Comma-separated list of bucket IDs to operate on", }) .option("wipe", { choices: ["all", "docs", "users"], description: "⚠️ DESTRUCTIVE: Wipe data (all: databases+storage+users, docs: documents only, users: user accounts only)", }) .option("wipeCollections", { type: "boolean", description: "⚠️ DESTRUCTIVE: Wipe specific collections (requires --collectionIds)", }) .option("transferUsers", { type: "boolean", description: "Transfer users between projects", }) .option("generate", { type: "boolean", description: "Generate TypeScript schemas and types from your Appwrite database schemas", }) .option("import", { type: "boolean", description: "Import data from importData/ directory into your Appwrite databases", }) .option("backup", { type: "boolean", description: "Create a complete backup of your databases and collections", }) .option("writeData", { type: "boolean", description: "Output converted import data to files for validation before importing", }) .option("push", { type: "boolean", description: "Deploy your local configuration (collections, attributes, indexes) to Appwrite", }) .option("sync", { type: "boolean", description: "Pull and synchronize your local config with the remote Appwrite project schema", }) .option("endpoint", { type: "string", description: "Set the Appwrite endpoint", }) .option("projectId", { type: "string", description: "Set the Appwrite project ID", }) .option("apiKey", { type: "string", description: "Set the Appwrite API key", }) .option("transfer", { type: "boolean", description: "Transfer documents and files between databases, collections, or projects", }) .option("fromDbId", { alias: ["fromDb", "sourceDbId", "sourceDb"], type: "string", description: "Source database ID for transfer operations", }) .option("toDbId", { alias: ["toDb", "targetDbId", "targetDb"], type: "string", description: "Target database ID for transfer operations", }) .option("fromCollectionId", { alias: ["fromCollId", "fromColl"], type: "string", description: "Set the source collection ID for transfer", }) .option("toCollectionId", { alias: ["toCollId", "toColl"], type: "string", description: "Set the destination collection ID for transfer", }) .option("fromBucketId", { type: "string", description: "Set the source bucket ID for transfer", }) .option("toBucketId", { type: "string", description: "Set the destination bucket ID for transfer", }) .option("remoteEndpoint", { type: "string", description: "Set the remote Appwrite endpoint for transfer", }) .option("remoteProjectId", { type: "string", description: "Set the remote Appwrite project ID for transfer", }) .option("remoteApiKey", { type: "string", description: "Set the remote Appwrite API key for transfer", }) .option("setup", { type: "boolean", description: "Initialize project with configuration files and directory structure", }) .option("updateFunctionSpec", { type: "boolean", description: "Update function specifications", }) .option("functionId", { type: "string", description: "Function ID to update", }) .option("specification", { type: "string", description: "New function specification (e.g., 's-1vcpu-1gb')", choices: [ "s-0.5vcpu-512mb", "s-1vcpu-1gb", "s-2vcpu-2gb", "s-2vcpu-4gb", "s-4vcpu-4gb", "s-4vcpu-8gb", "s-8vcpu-4gb", "s-8vcpu-8gb", ], }) .option("migrateConfig", { alias: ["migrate"], type: "boolean", description: "Migrate appwriteConfig.ts to .appwrite structure with YAML configuration", }) .option("generateConstants", { alias: ["constants"], type: "boolean", description: "Generate cross-language constants file with database, collection, bucket, and function IDs", }) .option("constantsLanguages", { type: "string", description: "Comma-separated list of languages for constants (typescript,javascript,python,php,dart,json,env)", default: "typescript", }) .option("constantsOutput", { type: "string", description: "Output directory for generated constants files (default: config-folder/constants)", default: "auto", }) .parse(); async function main() { const startTime = Date.now(); const operationStats = {}; if (argv.it) { const cli = new InteractiveCLI(process.cwd()); await cli.run(); } else { const directConfig = argv.endpoint || argv.projectId || argv.apiKey ? { appwriteEndpoint: argv.endpoint, appwriteProject: argv.projectId, appwriteKey: argv.apiKey, } : undefined; const controller = new UtilsController(process.cwd(), directConfig); await controller.init(); if (argv.setup) { await setupDirsFiles(false, process.cwd()); return; } if (argv.migrateConfig) { const { migrateConfig } = await import("./utils/configMigration.js"); await migrateConfig(process.cwd()); return; } if (argv.generateConstants) { const { ConstantsGenerator } = await import("./utils/constantsGenerator.js"); if (!controller.config) { MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Constants" }); return; } const languages = argv.constantsLanguages.split(",").map(l => l.trim()); // Determine output directory - use config folder/constants by default, or custom path if specified let outputDir; if (argv.constantsOutput === "auto") { // Default case: use config directory + constants, fallback to current directory const configPath = controller.getAppwriteFolderPath(); outputDir = configPath ? path.join(configPath, "constants") : path.join(process.cwd(), "constants"); } else { // Custom output directory specified outputDir = argv.constantsOutput; } MessageFormatter.info(`Generating constants for languages: ${languages.join(", ")}`, { prefix: "Constants" }); const generator = new ConstantsGenerator(controller.config); await generator.generateFiles(languages, outputDir); operationStats.generatedConstants = languages.length; MessageFormatter.success(`Constants generated in ${outputDir}`, { prefix: "Constants" }); return; } if (!controller.config) { MessageFormatter.error("No Appwrite connection found", undefined, { prefix: "CLI" }); return; } const parsedArgv = argv; const options = { databases: parsedArgv.dbIds ? await controller.getDatabasesByIds(parsedArgv.dbIds.split(",")) : undefined, collections: parsedArgv.collectionIds?.split(","), doBackup: parsedArgv.backup, wipeDatabase: parsedArgv.wipe === "all" || parsedArgv.wipe === "docs", wipeDocumentStorage: parsedArgv.wipe === "all" || parsedArgv.wipe === "storage", wipeUsers: parsedArgv.wipe === "all" || parsedArgv.wipe === "users", generateSchemas: parsedArgv.generate, importData: parsedArgv.import, shouldWriteFile: parsedArgv.writeData, wipeCollections: parsedArgv.wipeCollections, transferUsers: parsedArgv.transferUsers, }; if (parsedArgv.updateFunctionSpec) { if (!parsedArgv.functionId || !parsedArgv.specification) { throw new Error("Function ID and specification are required for updating function specs"); } MessageFormatter.info(`Updating function specification for ${parsedArgv.functionId} to ${parsedArgv.specification}`, { prefix: "Functions" }); const specifications = await listSpecifications(controller.appwriteServer); if (!specifications.specifications.some((s) => s.slug === parsedArgv.specification)) { MessageFormatter.error(`Specification ${parsedArgv.specification} not found`, undefined, { prefix: "Functions" }); return; } await controller.updateFunctionSpecifications(parsedArgv.functionId, parsedArgv.specification); } // Add default databases if not specified if (!options.databases || options.databases.length === 0) { const allDatabases = await fetchAllDatabases(controller.database); options.databases = allDatabases.filter((db) => { const useMigrations = controller.config?.useMigrations ?? true; return useMigrations || db.name.toLowerCase() !== "migrations"; }); } // Add default collections if not specified if (!options.collections || options.collections.length === 0) { if (controller.config && controller.config.collections) { options.collections = controller.config.collections.map((c) => c.name); } else { options.collections = []; } } if (options.doBackup && options.databases) { MessageFormatter.info(`Creating backups for ${options.databases.length} database(s)`, { prefix: "Backup" }); for (const db of options.databases) { await controller.backupDatabase(db); } operationStats.backups = options.databases.length; MessageFormatter.success(`Backup completed for ${options.databases.length} database(s)`, { prefix: "Backup" }); } if (options.wipeDatabase || options.wipeDocumentStorage || options.wipeUsers || options.wipeCollections) { // Confirm destructive operations const databaseNames = options.databases?.map(db => db.name) || []; const confirmed = await ConfirmationDialogs.confirmDatabaseWipe(databaseNames, { includeStorage: options.wipeDocumentStorage, includeUsers: options.wipeUsers }); if (!confirmed) { MessageFormatter.info("Operation cancelled by user", { prefix: "CLI" }); return; } let wipeStats = { databases: 0, collections: 0, users: 0, buckets: 0 }; if (parsedArgv.wipe === "all") { if (options.databases) { for (const db of options.databases) { await controller.wipeDatabase(db, true); // true to wipe associated buckets } wipeStats.databases = options.databases.length; } await controller.wipeUsers(); wipeStats.users = 1; } else if (parsedArgv.wipe === "docs") { if (options.databases) { for (const db of options.databases) { await controller.wipeBucketFromDatabase(db); } wipeStats.databases = options.databases.length; } if (parsedArgv.bucketIds) { const bucketIds = parsedArgv.bucketIds.split(","); for (const bucketId of bucketIds) { await controller.wipeDocumentStorage(bucketId); } wipeStats.buckets = bucketIds.length; } } else if (parsedArgv.wipe === "users") { await controller.wipeUsers(); wipeStats.users = 1; } // Handle specific collection wipes if (options.wipeCollections && options.databases) { for (const db of options.databases) { const dbCollections = await fetchAllCollections(db.$id, controller.database); const collectionsToWipe = dbCollections.filter((c) => options.collections.includes(c.$id)); // Confirm collection wipe const collectionNames = collectionsToWipe.map(c => c.name); const collectionConfirmed = await ConfirmationDialogs.confirmCollectionWipe(db.name, collectionNames); if (collectionConfirmed) { for (const collection of collectionsToWipe) { await controller.wipeCollection(db, collection); } wipeStats.collections += collectionsToWipe.length; } } } // Show wipe operation summary if (wipeStats.databases > 0 || wipeStats.collections > 0 || wipeStats.users > 0 || wipeStats.buckets > 0) { operationStats.wipedDatabases = wipeStats.databases; operationStats.wipedCollections = wipeStats.collections; operationStats.wipedUsers = wipeStats.users; operationStats.wipedBuckets = wipeStats.buckets; } } if (parsedArgv.push || parsedArgv.sync) { const databases = options.databases || (await fetchAllDatabases(controller.database)); let collections = []; if (options.collections) { for (const db of databases) { const dbCollections = await fetchAllCollections(db.$id, controller.database); collections = collections.concat(dbCollections.filter((c) => options.collections.includes(c.$id))); } } if (parsedArgv.push) { await controller.syncDb(databases, collections); operationStats.pushedDatabases = databases.length; operationStats.pushedCollections = collections.length; } else if (parsedArgv.sync) { await controller.synchronizeConfigurations(databases); operationStats.syncedDatabases = databases.length; } } if (options.generateSchemas) { await controller.generateSchemas(); operationStats.generatedSchemas = 1; } if (options.importData) { await controller.importData(options); operationStats.importCompleted = 1; } if (parsedArgv.transfer) { const isRemote = !!parsedArgv.remoteEndpoint; let fromDb, toDb; let targetDatabases; let targetStorage; // Only fetch databases if database IDs are provided if (parsedArgv.fromDbId && parsedArgv.toDbId) { MessageFormatter.info(`Starting database transfer from ${parsedArgv.fromDbId} to ${parsedArgv.toDbId}`, { prefix: "Transfer" }); fromDb = (await controller.getDatabasesByIds([parsedArgv.fromDbId]))?.[0]; if (!fromDb) { MessageFormatter.error("Source database not found", undefined, { prefix: "Transfer" }); return; } if (isRemote) { if (!parsedArgv.remoteEndpoint || !parsedArgv.remoteProjectId || !parsedArgv.remoteApiKey) { throw new Error("Remote transfer details are missing"); } const remoteClient = getClient(parsedArgv.remoteEndpoint, parsedArgv.remoteProjectId, parsedArgv.remoteApiKey); targetDatabases = new Databases(remoteClient); targetStorage = new Storage(remoteClient); const remoteDbs = await fetchAllDatabases(targetDatabases); toDb = remoteDbs.find((db) => db.$id === parsedArgv.toDbId); if (!toDb) { MessageFormatter.error("Target database not found", undefined, { prefix: "Transfer" }); return; } } else { toDb = (await controller.getDatabasesByIds([parsedArgv.toDbId]))?.[0]; if (!toDb) { MessageFormatter.error("Target database not found", undefined, { prefix: "Transfer" }); return; } } if (!fromDb || !toDb) { MessageFormatter.error("Source or target database not found", undefined, { prefix: "Transfer" }); return; } } // Handle storage setup let sourceBucket, targetBucket; if (parsedArgv.fromBucketId) { sourceBucket = await controller.storage?.getBucket(parsedArgv.fromBucketId); } if (parsedArgv.toBucketId) { if (isRemote) { if (!targetStorage) { const remoteClient = getClient(parsedArgv.remoteEndpoint, parsedArgv.remoteProjectId, parsedArgv.remoteApiKey); targetStorage = new Storage(remoteClient); } targetBucket = await targetStorage?.getBucket(parsedArgv.toBucketId); } else { targetBucket = await controller.storage?.getBucket(parsedArgv.toBucketId); } } // Validate that at least one transfer type is specified if (!fromDb && !sourceBucket && !options.transferUsers) { throw new Error("No source database or bucket specified for transfer"); } const transferOptions = { isRemote, fromDb, targetDb: toDb, transferEndpoint: parsedArgv.remoteEndpoint, transferProject: parsedArgv.remoteProjectId, transferKey: parsedArgv.remoteApiKey, sourceBucket: sourceBucket, targetBucket: targetBucket, transferUsers: options.transferUsers, }; await controller.transferData(transferOptions); operationStats.transfers = 1; } // Show final operation summary if any operations were performed if (Object.keys(operationStats).length > 0) { const duration = Date.now() - startTime; MessageFormatter.operationSummary("CLI Operations", operationStats, duration); } } } main().catch((error) => { MessageFormatter.error("CLI execution failed", error, { prefix: "CLI" }); process.exit(1); });