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.

518 lines (517 loc) 23.4 kB
import { Client, Databases, Query, Storage, Users, } from "node-appwrite"; import {} from "appwrite-utils"; import { loadConfig, loadConfigWithPath, findAppwriteConfig, findFunctionsDir, } from "./utils/loadConfigs.js"; import { UsersController } from "./users/methods.js"; import { AppwriteToX } from "./migrations/appwriteToX.js"; import { ImportController } from "./migrations/importController.js"; import { ImportDataActions } from "./migrations/importDataActions.js"; import { setupMigrationDatabase, ensureDatabasesExist, wipeOtherDatabases, ensureCollectionsExist, } from "./databases/setup.js"; import { createOrUpdateCollections, wipeDatabase, generateSchemas, fetchAllCollections, wipeCollection, } from "./collections/methods.js"; import { backupDatabase, ensureDatabaseConfigBucketsExist, initOrGetBackupStorage, wipeDocumentStorage, } from "./storage/methods.js"; import path from "path"; import { converterFunctions, validationRules, } from "appwrite-utils"; import { afterImportActions } from "./migrations/afterImportActions.js"; import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./migrations/transfer.js"; import { getClient } from "./utils/getClientFromConfig.js"; import { fetchAllDatabases } from "./databases/methods.js"; import { listFunctions, updateFunctionSpecifications, } from "./functions/methods.js"; import chalk from "chalk"; import { deployLocalFunction } from "./functions/deployments.js"; import fs from "node:fs"; import { configureLogging, updateLogger } from "./shared/logging.js"; import { MessageFormatter, Messages } from "./shared/messageFormatter.js"; import { SchemaGenerator } from "./shared/schemaGenerator.js"; import { findYamlConfig } from "./config/yamlConfig.js"; export class UtilsController { appwriteFolderPath; appwriteConfigPath; config; appwriteServer; database; storage; converterDefinitions = converterFunctions; validityRuleDefinitions = validationRules; afterImportActionsDefinitions = afterImportActions; constructor(currentUserDir, directConfig) { const basePath = currentUserDir; if (directConfig) { let hasErrors = false; if (!directConfig.appwriteEndpoint) { MessageFormatter.error("Appwrite endpoint is required", undefined, { prefix: "Config" }); hasErrors = true; } if (!directConfig.appwriteProject) { MessageFormatter.error("Appwrite project is required", undefined, { prefix: "Config" }); hasErrors = true; } if (!directConfig.appwriteKey) { MessageFormatter.error("Appwrite key is required", undefined, { prefix: "Config" }); hasErrors = true; } if (!hasErrors) { // Only set config if we have all required fields this.appwriteFolderPath = basePath; this.config = { appwriteEndpoint: directConfig.appwriteEndpoint, appwriteProject: directConfig.appwriteProject, appwriteKey: directConfig.appwriteKey, enableBackups: false, backupInterval: 0, backupRetention: 0, enableBackupCleanup: false, enableMockData: false, documentBucketId: "", usersCollectionName: "", useMigrations: true, databases: [], buckets: [], functions: [], logging: { enabled: false, level: "info", console: false, }, }; } } else { // Try to find config file const appwriteConfigFound = findAppwriteConfig(basePath); if (!appwriteConfigFound) { MessageFormatter.warning("No appwriteConfig.ts found and no direct configuration provided", { prefix: "Config" }); return; } this.appwriteConfigPath = appwriteConfigFound; this.appwriteFolderPath = appwriteConfigFound; // For YAML configs, findAppwriteConfig already returns the correct directory } } async init() { if (!this.config) { if (this.appwriteFolderPath && this.appwriteConfigPath) { MessageFormatter.progress("Loading config from file...", { prefix: "Config" }); try { const { config, actualConfigPath } = await loadConfigWithPath(this.appwriteFolderPath); this.config = config; MessageFormatter.info(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" }); } catch (error) { MessageFormatter.error("Failed to load config from file", undefined, { prefix: "Config" }); return; } } else { MessageFormatter.error("No configuration available", undefined, { prefix: "Config" }); return; } } // Configure logging based on config if (this.config.logging) { configureLogging(this.config.logging); updateLogger(); } this.appwriteServer = new Client(); this.appwriteServer .setEndpoint(this.config.appwriteEndpoint) .setProject(this.config.appwriteProject) .setKey(this.config.appwriteKey); this.database = new Databases(this.appwriteServer); this.storage = new Storage(this.appwriteServer); this.config.appwriteClient = this.appwriteServer; } async reloadConfig() { if (!this.appwriteFolderPath) { MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" }); return; } this.config = await loadConfig(this.appwriteFolderPath); if (!this.config) { console.log(chalk.red("Failed to load config")); return; } // Configure logging based on updated config if (this.config.logging) { configureLogging(this.config.logging); updateLogger(); } this.appwriteServer = new Client(); this.appwriteServer .setEndpoint(this.config.appwriteEndpoint) .setProject(this.config.appwriteProject) .setKey(this.config.appwriteKey); this.database = new Databases(this.appwriteServer); this.storage = new Storage(this.appwriteServer); this.config.appwriteClient = this.appwriteServer; } async setupMigrationDatabase() { await this.init(); if (!this.config) { MessageFormatter.error("Config not initialized", undefined, { prefix: "Controller" }); return; } await setupMigrationDatabase(this.config); } async ensureDatabaseConfigBucketsExist(databases = []) { await this.init(); if (!this.storage) { MessageFormatter.error("Storage not initialized", undefined, { prefix: "Controller" }); return; } if (!this.config) { MessageFormatter.error("Config not initialized", undefined, { prefix: "Controller" }); return; } await ensureDatabaseConfigBucketsExist(this.storage, this.config, databases); } async ensureDatabasesExist(databases) { await this.init(); if (!this.config) { MessageFormatter.error("Config not initialized", undefined, { prefix: "Controller" }); return; } await this.setupMigrationDatabase(); await this.ensureDatabaseConfigBucketsExist(databases); await ensureDatabasesExist(this.config, databases); } async ensureCollectionsExist(database, collections) { await this.init(); if (!this.config) { MessageFormatter.error("Config not initialized", undefined, { prefix: "Controller" }); return; } await ensureCollectionsExist(this.config, database, collections); } async getDatabasesByIds(ids) { await this.init(); if (!this.database) { MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" }); return; } if (ids.length === 0) return []; const dbs = await this.database.list([ Query.limit(500), Query.equal("$id", ids), ]); return dbs.databases; } async wipeOtherDatabases(databasesToKeep) { await this.init(); if (!this.database) { MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" }); return; } await wipeOtherDatabases(this.database, databasesToKeep, this.config?.useMigrations ?? true); } async wipeUsers() { await this.init(); if (!this.config || !this.database) { console.log(chalk.red("Config or database not initialized")); return; } const usersController = new UsersController(this.config, this.database); await usersController.wipeUsers(); } async backupDatabase(database) { await this.init(); if (!this.database || !this.storage || !this.config) { console.log(chalk.red("Database, storage, or config not initialized")); return; } await backupDatabase(this.config, this.database, database.$id, this.storage); } async listAllFunctions() { await this.init(); if (!this.appwriteServer) { console.log(chalk.red("Appwrite server not initialized")); return []; } const { functions } = await listFunctions(this.appwriteServer, [ Query.limit(1000), ]); return functions; } async findFunctionDirectories() { if (!this.appwriteFolderPath) { MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" }); return new Map(); } const functionsDir = findFunctionsDir(this.appwriteFolderPath); if (!functionsDir) { console.log(chalk.red("Failed to find functions directory")); return new Map(); } const functionDirMap = new Map(); const entries = fs.readdirSync(functionsDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory()) { const functionPath = path.join(functionsDir, entry.name); // Match with config functions by name if (this.config?.functions) { const matchingFunc = this.config.functions.find((f) => f.name.toLowerCase() === entry.name.toLowerCase()); if (matchingFunc) { functionDirMap.set(matchingFunc.name, functionPath); } } } } return functionDirMap; } async deployFunction(functionName, functionPath, functionConfig) { await this.init(); if (!this.appwriteServer) { console.log(chalk.red("Appwrite server not initialized")); return; } if (!functionConfig) { functionConfig = this.config?.functions?.find((f) => f.name === functionName); } if (!functionConfig) { console.log(chalk.red(`Function ${functionName} not found in config`)); return; } await deployLocalFunction(this.appwriteServer, functionName, functionConfig, functionPath); } async syncFunctions() { await this.init(); if (!this.appwriteServer) { console.log(chalk.red("Appwrite server not initialized")); return; } const localFunctions = this.config?.functions || []; const remoteFunctions = await listFunctions(this.appwriteServer, [ Query.limit(1000), ]); for (const localFunction of localFunctions) { MessageFormatter.progress(`Syncing function ${localFunction.name}...`, { prefix: "Functions" }); await this.deployFunction(localFunction.name); } MessageFormatter.success("All functions synchronized successfully!", { prefix: "Functions" }); } async wipeDatabase(database, wipeBucket = false) { await this.init(); if (!this.database) throw new Error("Database not initialized"); await wipeDatabase(this.database, database.$id); if (wipeBucket) { await this.wipeBucketFromDatabase(database); } } async wipeBucketFromDatabase(database) { // Check configured bucket in database config const configuredBucket = this.config?.databases?.find((db) => db.$id === database.$id)?.bucket; if (configuredBucket?.$id) { await this.wipeDocumentStorage(configuredBucket.$id); } // Also check for document bucket ID pattern if (this.config?.documentBucketId) { const documentBucketId = `${this.config.documentBucketId}_${database.$id .toLowerCase() .trim() .replace(/\s+/g, "")}`; try { await this.wipeDocumentStorage(documentBucketId); } catch (error) { // Ignore if bucket doesn't exist if (error?.type !== "storage_bucket_not_found") { throw error; } } } } async wipeCollection(database, collection) { await this.init(); if (!this.database) throw new Error("Database not initialized"); await wipeCollection(this.database, database.$id, collection.$id); } async wipeDocumentStorage(bucketId) { await this.init(); if (!this.storage) throw new Error("Storage not initialized"); await wipeDocumentStorage(this.storage, bucketId); } async createOrUpdateCollectionsForDatabases(databases, collections = []) { await this.init(); if (!this.database || !this.config) throw new Error("Database or config not initialized"); for (const database of databases) { if (!this.config.useMigrations && database.$id === "migrations") continue; await this.createOrUpdateCollections(database, undefined, collections); } } async createOrUpdateCollections(database, deletedCollections, collections = []) { await this.init(); if (!this.database || !this.config) throw new Error("Database or config not initialized"); await createOrUpdateCollections(this.database, database.$id, this.config, deletedCollections, collections); } async generateSchemas() { await this.init(); if (!this.config) { MessageFormatter.error("Config not initialized", undefined, { prefix: "Controller" }); return; } if (!this.appwriteFolderPath) { MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" }); return; } await generateSchemas(this.config, this.appwriteFolderPath); } async importData(options = {}) { await this.init(); if (!this.database) { MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" }); return; } if (!this.storage) { MessageFormatter.error("Storage not initialized", undefined, { prefix: "Controller" }); return; } if (!this.config) { MessageFormatter.error("Config not initialized", undefined, { prefix: "Controller" }); return; } if (!this.appwriteFolderPath) { MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" }); return; } const importDataActions = new ImportDataActions(this.database, this.storage, this.config, this.converterDefinitions, this.validityRuleDefinitions, this.afterImportActionsDefinitions); const importController = new ImportController(this.config, this.database, this.storage, this.appwriteFolderPath, importDataActions, options, options.databases); await importController.run(options.collections); } async synchronizeConfigurations(databases, config) { await this.init(); if (!this.storage) { MessageFormatter.error("Storage not initialized", undefined, { prefix: "Controller" }); return; } const configToUse = config || this.config; if (!configToUse) { MessageFormatter.error("Config not initialized", undefined, { prefix: "Controller" }); return; } if (!this.appwriteFolderPath) { MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" }); return; } const appwriteToX = new AppwriteToX(configToUse, this.appwriteFolderPath, this.storage); await appwriteToX.toSchemas(databases); // Update the controller's config with the synchronized collections this.config = appwriteToX.updatedConfig; // Write the updated config back to disk const generator = new SchemaGenerator(this.config, this.appwriteFolderPath); const yamlConfigPath = findYamlConfig(this.appwriteFolderPath); const isYamlProject = !!yamlConfigPath; await generator.updateConfig(this.config, isYamlProject); } async syncDb(databases = [], collections = []) { await this.init(); if (!this.database) { MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" }); return; } if (databases.length === 0) { const allDatabases = await fetchAllDatabases(this.database); databases = allDatabases; } await this.ensureDatabasesExist(databases); await this.ensureDatabaseConfigBucketsExist(databases); await this.createOrUpdateCollectionsForDatabases(databases, collections); } getAppwriteFolderPath() { return this.appwriteFolderPath; } async transferData(options) { let sourceClient = this.database; let targetClient; let sourceDatabases = []; let targetDatabases = []; if (!sourceClient) { console.log(chalk.red("Source database not initialized")); return; } if (options.isRemote) { if (!options.transferEndpoint || !options.transferProject || !options.transferKey) { console.log(chalk.red("Remote transfer options are missing")); return; } const remoteClient = getClient(options.transferEndpoint, options.transferProject, options.transferKey); targetClient = new Databases(remoteClient); sourceDatabases = await fetchAllDatabases(sourceClient); targetDatabases = await fetchAllDatabases(targetClient); } else { targetClient = sourceClient; sourceDatabases = targetDatabases = await fetchAllDatabases(sourceClient); } // Always perform database transfer if databases are specified if (options.fromDb && options.targetDb) { const fromDb = sourceDatabases.find((db) => db.$id === options.fromDb.$id); const targetDb = targetDatabases.find((db) => db.$id === options.targetDb.$id); if (!fromDb || !targetDb) { console.log(chalk.red("Source or target database not found")); return; } if (options.isRemote && targetClient) { await transferDatabaseLocalToRemote(sourceClient, options.transferEndpoint, options.transferProject, options.transferKey, fromDb.$id, targetDb.$id); } else { await transferDatabaseLocalToLocal(sourceClient, fromDb.$id, targetDb.$id); } } if (options.transferUsers) { if (!options.isRemote) { console.log(chalk.yellow("User transfer is only supported for remote transfers. Skipping...")); } else if (!this.appwriteServer) { console.log(chalk.red("Appwrite server not initialized")); return; } else { MessageFormatter.progress("Starting user transfer...", { prefix: "Transfer" }); const localUsers = new Users(this.appwriteServer); await transferUsersLocalToRemote(localUsers, options.transferEndpoint, options.transferProject, options.transferKey); MessageFormatter.success("User transfer completed", { prefix: "Transfer" }); } } // Handle storage transfer if (this.storage && (options.sourceBucket || options.fromDb)) { const sourceBucketId = options.sourceBucket?.$id || (options.fromDb && this.config?.documentBucketId && `${this.config.documentBucketId}_${options.fromDb.$id .toLowerCase() .trim() .replace(/\s+/g, "")}`); const targetBucketId = options.targetBucket?.$id || (options.targetDb && this.config?.documentBucketId && `${this.config.documentBucketId}_${options.targetDb.$id .toLowerCase() .trim() .replace(/\s+/g, "")}`); if (sourceBucketId && targetBucketId) { MessageFormatter.progress(`Starting storage transfer from ${sourceBucketId} to ${targetBucketId}`, { prefix: "Transfer" }); if (options.isRemote) { await transferStorageLocalToRemote(this.storage, options.transferEndpoint, options.transferProject, options.transferKey, sourceBucketId, targetBucketId); } else { await transferStorageLocalToLocal(this.storage, sourceBucketId, targetBucketId); } } } MessageFormatter.success("Transfer completed", { prefix: "Transfer" }); } async updateFunctionSpecifications(functionId, specification) { await this.init(); if (!this.appwriteServer) throw new Error("Appwrite server not initialized"); MessageFormatter.progress(`Updating function specifications for ${functionId} to ${specification}`, { prefix: "Functions" }); await updateFunctionSpecifications(this.appwriteServer, functionId, specification); MessageFormatter.success(`Successfully updated function specifications for ${functionId} to ${specification}`, { prefix: "Functions" }); } }