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.

331 lines (330 loc) 16.7 kB
import inquirer from "inquirer"; import { join } from "node:path"; import fs from "node:fs"; import os from "node:os"; import { ulid } from "ulidx"; import chalk from "chalk"; import { Query } from "node-appwrite"; import { MessageFormatter } from "../../shared/messageFormatter.js"; import { createFunctionTemplate, deleteFunction, downloadLatestFunctionDeployment, listFunctions, listSpecifications, } from "../../functions/methods.js"; import { deployLocalFunction } from "../../functions/deployments.js"; import { discoverFnConfigs, mergeDiscoveredFunctions } from "../../functions/fnConfigDiscovery.js"; import { addFunctionToYamlConfig, findYamlConfig } from "../../config/yamlConfig.js"; import { RuntimeSchema } from "appwrite-utils"; export const functionCommands = { async createFunction(cli) { const { name } = await inquirer.prompt([ { type: "input", name: "name", message: "Function name:", validate: (input) => input.length > 0, }, ]); const { template } = await inquirer.prompt([ { type: "list", name: "template", message: "Select a template:", choices: [ { name: "TypeScript Node.js", value: "typescript-node" }, { name: "TypeScript with Hono Web Framework", value: "hono-typescript" }, { name: "Python with UV", value: "uv" }, { name: "Count Documents in Collection", value: "count-docs-in-collection" }, { name: "None (Empty Function)", value: "none" }, ], }, ]); // Get template defaults const templateDefaults = cli.getTemplateDefaults(template); const { runtime } = await inquirer.prompt([ { type: "list", name: "runtime", message: "Select runtime:", choices: Object.values(RuntimeSchema.Values), default: templateDefaults.runtime, }, ]); const specifications = await listSpecifications(cli.controller.appwriteServer); const { specification } = await inquirer.prompt([ { type: "list", name: "specification", message: "Select specification:", choices: [ { name: "None", value: undefined }, ...specifications.specifications.map((s) => ({ name: s.slug, value: s.slug, })), ], default: templateDefaults.specification, }, ]); const functionConfig = { $id: ulid(), name, runtime, events: [], execute: ["any"], enabled: true, logging: true, entrypoint: templateDefaults.entrypoint, commands: templateDefaults.commands, specification: specification || templateDefaults.specification, scopes: [], timeout: 15, schedule: "", installationId: "", providerRepositoryId: "", providerBranch: "", providerSilentMode: false, providerRootDirectory: "", templateRepository: "", templateOwner: "", templateRootDirectory: "", }; if (template !== "none") { await createFunctionTemplate(template, name, "./functions"); } // Add to in-memory config if (!cli.controller.config.functions) { cli.controller.config.functions = []; } cli.controller.config.functions.push(functionConfig); // If using YAML config, also add to YAML file const yamlConfigPath = findYamlConfig(cli.currentDir); if (yamlConfigPath) { try { await addFunctionToYamlConfig(yamlConfigPath, functionConfig); } catch (error) { MessageFormatter.warning(`Function created but failed to update YAML config: ${error instanceof Error ? error.message : error}`, { prefix: "Functions" }); } } MessageFormatter.success("Function created successfully!", { prefix: "Functions" }); }, async deployFunction(cli) { await cli.initControllerIfNeeded(); if (!cli.controller?.config) { MessageFormatter.error("Failed to initialize controller or load config", undefined, { prefix: "Functions" }); return; } // Discover per-function .fnconfig.yaml definitions and merge with central list for selection // No global prompt; we'll handle conflicts per-function if both exist. let discovered = []; let central = cli.controller.config.functions || []; try { discovered = discoverFnConfigs(cli.currentDir); const merged = mergeDiscoveredFunctions(central, discovered); cli.controller.config.functions = merged; } catch { } const functions = await cli.selectFunctions("Select function(s) to deploy:", true, true); if (!functions?.length) { MessageFormatter.error("No function selected", undefined, { prefix: "Functions" }); return; } for (const functionConfig of functions) { if (!functionConfig) { MessageFormatter.error("Invalid function configuration", undefined, { prefix: "Functions" }); return; } // Resolve effective config for this function (prefer per-function choice if both sources exist) const byIdOrName = (arr) => arr.find((f) => f?.$id === functionConfig.$id || f?.name === functionConfig.name); const centralDef = byIdOrName(central); const discoveredDef = byIdOrName(discovered); let effectiveConfig = functionConfig; if (centralDef && discoveredDef) { try { const answer = await inquirer.prompt([ { type: 'list', name: 'cfgChoice', message: `Multiple configs found for '${functionConfig.name}'. Which to use?`, choices: [ { name: 'config.yaml (central)', value: 'central' }, { name: '.fnconfig.yaml (local file)', value: 'fnconfig' }, { name: 'Merge (.fnconfig overrides central)', value: 'merge' }, ], default: 'fnconfig' } ]); if (answer.cfgChoice === 'central') effectiveConfig = centralDef; else if (answer.cfgChoice === 'fnconfig') effectiveConfig = discoveredDef; else effectiveConfig = { ...centralDef, ...discoveredDef }; } catch { } } // Ensure functions array exists if (!cli.controller.config.functions) { cli.controller.config.functions = []; } const functionNameLower = effectiveConfig.name .toLowerCase() .replace(/\s+/g, "-"); // Debug logging MessageFormatter.info(`🔍 Function deployment debug:`, { prefix: "Functions" }); MessageFormatter.info(` Function name: ${effectiveConfig.name}`, { prefix: "Functions" }); MessageFormatter.info(` Function ID: ${effectiveConfig.$id}`, { prefix: "Functions" }); MessageFormatter.info(` Config dirPath: ${effectiveConfig.dirPath || 'undefined'}`, { prefix: "Functions" }); if (effectiveConfig.dirPath) { const expandedPath = effectiveConfig.dirPath.startsWith('~/') ? effectiveConfig.dirPath.replace('~', os.homedir()) : effectiveConfig.dirPath; MessageFormatter.info(` Expanded dirPath: ${expandedPath}`, { prefix: "Functions" }); } MessageFormatter.info(` Appwrite folder: ${cli.controller.getAppwriteFolderPath()}`, { prefix: "Functions" }); MessageFormatter.info(` Current working dir: ${process.cwd()}`, { prefix: "Functions" }); // Resolve config dirPath relative to central YAML if it's relative const yamlConfigPath = findYamlConfig(cli.currentDir); const yamlBaseDir = yamlConfigPath ? require('node:path').dirname(yamlConfigPath) : process.cwd(); const expandTildePath = (p) => (p?.startsWith('~/') ? p.replace('~', os.homedir()) : p); // Check locations in priority order: const priorityLocations = [ // 1. Config dirPath if specified (with tilde expansion) effectiveConfig.dirPath ? (require('node:path').isAbsolute(expandTildePath(effectiveConfig.dirPath)) ? expandTildePath(effectiveConfig.dirPath) : require('node:path').resolve(yamlBaseDir, expandTildePath(effectiveConfig.dirPath))) : undefined, // 2. Appwrite config folder/functions/name join(cli.controller.getAppwriteFolderPath(), "functions", functionNameLower), // 3. Current working directory/functions/name join(process.cwd(), "functions", functionNameLower), // 4. Current working directory/name join(process.cwd(), functionNameLower), ].filter((val) => val !== undefined); MessageFormatter.info(`🔍 Priority locations to check:`, { prefix: "Functions" }); priorityLocations.forEach((loc, i) => { MessageFormatter.info(` ${i + 1}. ${loc}`, { prefix: "Functions" }); }); let functionPath = null; // Check each priority location for (const location of priorityLocations) { MessageFormatter.info(` Checking: ${location} - ${fs.existsSync(location) ? 'EXISTS' : 'NOT FOUND'}`, { prefix: "Functions" }); if (fs.existsSync(location)) { MessageFormatter.success(`✅ Found function at: ${location}`, { prefix: "Functions" }); functionPath = location; break; } } // If not found in priority locations, do a broader search if (!functionPath) { MessageFormatter.info(`Function not found in primary locations, searching subdirectories...`, { prefix: "Functions" }); // Search in both appwrite config directory and current working directory functionPath = await cli.findFunctionInSubdirectories([cli.controller.getAppwriteFolderPath(), process.cwd()], functionNameLower); } if (!functionPath) { const { shouldDownload } = await inquirer.prompt([ { type: "confirm", name: "shouldDownload", message: "Function not found locally. Would you like to download the latest deployment?", default: false, }, ]); if (shouldDownload) { try { MessageFormatter.progress("Downloading latest deployment...", { prefix: "Functions" }); const { path: downloadedPath, function: remoteFunction } = await downloadLatestFunctionDeployment(cli.controller.appwriteServer, effectiveConfig.$id, join(cli.controller.getAppwriteFolderPath(), "functions")); MessageFormatter.success(`✨ Function downloaded to ${downloadedPath}`, { prefix: "Functions" }); functionPath = downloadedPath; effectiveConfig.dirPath = downloadedPath; const existingIndex = cli.controller.config.functions.findIndex((f) => f?.$id === remoteFunction.$id); if (existingIndex >= 0) { cli.controller.config.functions[existingIndex].dirPath = downloadedPath; } await cli.reloadConfigWithSessionPreservation(); } catch (error) { MessageFormatter.error("Failed to download function deployment", error instanceof Error ? error : new Error(String(error)), { prefix: "Functions" }); return; } } else { MessageFormatter.error(`Function ${effectiveConfig.name} not found locally. Cannot deploy.`, undefined, { prefix: "Functions" }); return; } } if (!cli.controller.appwriteServer) { MessageFormatter.error("Appwrite server not initialized", undefined, { prefix: "Functions" }); return; } try { await deployLocalFunction(cli.controller.appwriteServer, effectiveConfig.name, { ...effectiveConfig, dirPath: functionPath, }, functionPath); MessageFormatter.success("Function deployed successfully!", { prefix: "Functions" }); } catch (error) { MessageFormatter.error("Failed to deploy function", error instanceof Error ? error : new Error(String(error)), { prefix: "Functions" }); } } }, async deleteFunction(cli) { const functions = await cli.selectFunctions("Select functions to delete:", true, false); if (!functions.length) { MessageFormatter.error("No functions selected", undefined, { prefix: "Functions" }); return; } for (const func of functions) { try { await deleteFunction(cli.controller.appwriteServer, func.$id); MessageFormatter.success(`✨ Function ${func.name} deleted successfully!`, { prefix: "Functions" }); } catch (error) { MessageFormatter.error(`Failed to delete function ${func.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Functions" }); } } }, async updateFunctionSpec(cli) { const remoteFunctions = await listFunctions(cli.controller.appwriteServer, [Query.limit(1000)]); const localFunctions = cli.getLocalFunctions(); const allFunctions = [ ...remoteFunctions.functions, ...localFunctions.filter((f) => !remoteFunctions.functions.some((rf) => rf.name === f.name)), ]; const functionsToUpdate = await inquirer.prompt([ { type: "checkbox", name: "functionId", message: "Select functions to update:", choices: allFunctions.map((f) => ({ name: `${f.name} (${f.$id})${localFunctions.some((lf) => lf.name === f.name) ? " (Local)" : " (Remote)"}`, value: f.$id, })), loop: true, }, ]); const specifications = await listSpecifications(cli.controller.appwriteServer); const { specification } = await inquirer.prompt([ { type: "list", name: "specification", message: "Select new specification:", choices: specifications.specifications.map((s) => ({ name: `${s.slug}`, value: s.slug, })), }, ]); try { for (const functionId of functionsToUpdate.functionId) { await cli.controller.updateFunctionSpecifications(functionId, specification); MessageFormatter.success(`Successfully updated function specification to ${specification}`, { prefix: "Functions" }); } } catch (error) { MessageFormatter.error("Error updating function specification", error instanceof Error ? error : new Error(String(error)), { prefix: "Functions" }); } } };