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.

262 lines (261 loc) 11.7 kB
import { promises as fs } from "fs"; import path from "path"; import { existsSync } from "fs"; import { MessageFormatter } from "../shared/messageFormatter.js"; import { ConfirmationDialogs } from "../shared/confirmationDialogs.js"; import yaml from "js-yaml"; export async function migrateConfig(workingDir) { try { // Look for appwriteConfig.ts files in the working directory and subdirectories const configFiles = await findAppwriteConfigFiles(workingDir); if (configFiles.length === 0) { MessageFormatter.info("No appwriteConfig.ts files found to migrate", { prefix: "Migration" }); return; } MessageFormatter.info(`Found ${configFiles.length} appwriteConfig.ts file(s) to migrate`, { prefix: "Migration" }); for (const configFile of configFiles) { await migrateConfigFile(configFile, workingDir); } MessageFormatter.success("Migration completed successfully", { prefix: "Migration" }); } catch (error) { MessageFormatter.error("Migration failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" }); throw error; } } async function findAppwriteConfigFiles(dir) { const configFiles = []; const checkDir = async (currentDir) => { try { const entries = await fs.readdir(currentDir, { withFileTypes: true }); for (const entry of entries) { const fullPath = path.join(currentDir, entry.name); if (entry.isDirectory() && !entry.name.startsWith('.') && entry.name !== 'node_modules') { await checkDir(fullPath); } else if (entry.isFile() && entry.name === 'appwriteConfig.ts') { configFiles.push(fullPath); } } } catch (error) { // Ignore directory access errors } }; await checkDir(dir); return configFiles; } async function migrateConfigFile(configFilePath, workingDir) { const configDir = path.dirname(configFilePath); const appwriteDir = path.join(path.dirname(configDir), '.appwrite'); MessageFormatter.info(`Migrating ${path.relative(workingDir, configFilePath)}`, { prefix: "Migration" }); // Check if .appwrite directory already exists if (existsSync(appwriteDir)) { const shouldOverwrite = await ConfirmationDialogs.confirmOverwrite(`.appwrite directory already exists at ${path.relative(workingDir, appwriteDir)}`); if (!shouldOverwrite) { MessageFormatter.info("Skipping migration for this config", { prefix: "Migration" }); return; } } // Load and parse the TypeScript config const config = await parseTypeScriptConfig(configFilePath); // Create .appwrite directory await fs.mkdir(appwriteDir, { recursive: true }); // Convert config to YAML and save const yamlConfig = convertToYAMLConfig(config); const yamlContent = yaml.dump(yamlConfig, { indent: 2, lineWidth: 120, noRefs: true }); await fs.writeFile(path.join(appwriteDir, 'config.yaml'), yamlContent); // Copy all directories except collections and schemas (we handle collections separately, skip schemas entirely) const entries = await fs.readdir(configDir, { withFileTypes: true }); for (const entry of entries) { if (entry.isDirectory() && entry.name !== 'collections' && entry.name !== 'schemas') { const sourcePath = path.join(configDir, entry.name); const targetPath = path.join(appwriteDir, entry.name); await fs.cp(sourcePath, targetPath, { recursive: true }); MessageFormatter.info(`Copied ${entry.name}/ to .appwrite/${entry.name}/`, { prefix: "Migration" }); } } // Convert TypeScript collections to YAML collections const collectionsPath = path.join(configDir, 'collections'); if (existsSync(collectionsPath)) { const targetCollectionsPath = path.join(appwriteDir, 'collections'); await fs.mkdir(targetCollectionsPath, { recursive: true }); const collectionFiles = await fs.readdir(collectionsPath); for (const file of collectionFiles) { if (file.endsWith('.ts')) { await convertCollectionToYaml(path.join(collectionsPath, file), targetCollectionsPath); } } MessageFormatter.info(`Converted TypeScript collections to YAML in .appwrite/collections/`, { prefix: "Migration" }); } // Keep original config file in place (no backup needed since we're not deleting it) MessageFormatter.success(`Migration completed for ${path.relative(workingDir, configFilePath)}`, { prefix: "Migration" }); } async function parseTypeScriptConfig(configFilePath) { try { // Use tsx to import the TypeScript config file directly const { register } = await import("tsx/esm/api"); const { pathToFileURL } = await import("node:url"); const unregister = register(); try { const configUrl = pathToFileURL(configFilePath).href; const configModule = await import(configUrl); const config = configModule.default?.default || configModule.default || configModule; if (!config) { throw new Error("Failed to load config from TypeScript file"); } return config; } finally { unregister(); } } catch (error) { MessageFormatter.error("Could not load TypeScript config", error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" }); throw new Error('Failed to load TypeScript configuration file. Please ensure it exports a valid config object.'); } } function convertToYAMLConfig(config) { // Convert the config to the nested YAML structure const yamlConfig = { appwrite: { endpoint: config.appwriteEndpoint, project: config.appwriteProject, key: config.appwriteKey }, logging: { enabled: config.logging?.enabled ?? false, level: config.logging?.level ?? "info", console: config.logging?.console ?? false, logDirectory: "./logs" }, backups: { enabled: config.enableBackups ?? false, interval: config.backupInterval ?? 3600, retention: config.backupRetention ?? 30, cleanup: config.enableBackupCleanup ?? false }, data: { enableMockData: config.enableMockData ?? false, documentBucketId: config.documentBucketId ?? "documents", usersCollectionName: config.usersCollectionName ?? "Users", importDirectory: "importData" }, schemas: { outputDirectory: "schemas", yamlSchemaDirectory: ".yaml_schemas" }, migrations: { enabled: true }, databases: (config.databases || []).map(db => ({ id: db.$id, name: db.name, collections: [] // Collections will be handled separately })), buckets: (config.buckets || []).map(bucket => ({ id: bucket.$id, name: bucket.name, permissions: bucket.$permissions?.map((p) => ({ permission: p.permission, target: p.target })) || [], fileSecurity: bucket.fileSecurity ?? false, enabled: bucket.enabled ?? true, maximumFileSize: bucket.maximumFileSize ?? 30000000, allowedFileExtensions: bucket.allowedFileExtensions || [], compression: bucket.compression || "gzip", encryption: bucket.encryption ?? false, antivirus: bucket.antivirus ?? false })), functions: (config.functions || []).map((func) => ({ id: func.$id, name: func.name, runtime: func.runtime, execute: func.execute || [], events: func.events || [], schedule: func.schedule || "", timeout: func.timeout ?? 15, enabled: func.enabled ?? true, logging: func.logging ?? false, entrypoint: func.entrypoint || "src/main.js", commands: func.commands || "", scopes: func.scopes || [], specification: func.specification || "s-1vcpu-512mb" })) }; return yamlConfig; } async function convertCollectionToYaml(tsFilePath, targetDir) { try { // Load the TypeScript collection using tsx const { register } = await import("tsx/esm/api"); const { pathToFileURL } = await import("node:url"); const unregister = register(); try { const configUrl = pathToFileURL(tsFilePath).href; const collectionModule = await import(configUrl); const collection = collectionModule.default?.default || collectionModule.default || collectionModule; if (!collection) { throw new Error("Failed to load collection from TypeScript file"); } // Convert collection to YAML format const yamlCollection = { name: collection.name, id: collection.$id, documentSecurity: collection.documentSecurity ?? false, enabled: collection.enabled ?? true, permissions: (collection.permissions || collection.$permissions || []).map((p) => ({ permission: p.permission, target: p.target })), attributes: (collection.attributes || []).map((attr) => ({ key: attr.key, type: attr.type, size: attr.size, required: attr.required ?? false, array: attr.array, default: attr.xdefault || attr.default, min: attr.min, max: attr.max, elements: attr.elements, relatedCollection: attr.relatedCollection, relationType: attr.relationType, twoWay: attr.twoWay, twoWayKey: attr.twoWayKey, onDelete: attr.onDelete, side: attr.side })), indexes: (collection.indexes || []).map((idx) => ({ key: idx.key, type: idx.type, attributes: idx.attributes, orders: idx.orders })), importDefs: collection.importDefs || [] }; // Remove undefined values const cleanYamlCollection = JSON.parse(JSON.stringify(yamlCollection, (key, value) => value === undefined ? undefined : value)); // Write YAML file const fileName = path.basename(tsFilePath, '.ts') + '.yaml'; const targetPath = path.join(targetDir, fileName); const yamlContent = yaml.dump(cleanYamlCollection, { indent: 2, lineWidth: 120, noRefs: true }); await fs.writeFile(targetPath, yamlContent); MessageFormatter.info(`Converted ${path.basename(tsFilePath)} to ${fileName}`, { prefix: "Migration" }); } finally { unregister(); } } catch (error) { MessageFormatter.error(`Failed to convert collection ${path.basename(tsFilePath)}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Migration" }); } }