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.
323 lines (322 loc) • 14.8 kB
JavaScript
import { Client, Functions, Runtime } from "node-appwrite";
import {} from "appwrite-utils";
import { join, relative, resolve, basename } from "node:path";
import fs from "node:fs";
import chalk from "chalk";
import pLimit from "p-limit";
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
// Concurrency limits
const functionLimit = pLimit(5); // Moderate limit for function operations
const queryLimit = pLimit(25); // Higher limit for read operations
export class FunctionManager {
client;
functions;
constructor(client) {
this.client = client;
this.functions = new Functions(client);
}
/**
* Improved function directory detection with multiple strategies
*/
async findFunctionDirectory(functionName, options = {}) {
const { searchPaths = [process.cwd()], caseSensitive = false, allowFuzzyMatch = true, verbose = false } = options;
if (verbose) {
console.log(chalk.blue(`🔍 Searching for function: ${functionName}`));
}
// Normalize function name for comparison
const normalizedName = caseSensitive ? functionName : functionName.toLowerCase();
const nameVariations = this.generateNameVariations(normalizedName);
// Strategy 1: Check standard locations first
const standardPaths = this.getStandardFunctionPaths(searchPaths, functionName);
for (const path of standardPaths) {
if (await this.isValidFunctionDirectory(path)) {
if (verbose) {
console.log(chalk.green(`✓ Found function at standard location: ${path}`));
}
return path;
}
}
// Strategy 2: Recursive directory search with fuzzy matching
if (allowFuzzyMatch) {
for (const searchPath of searchPaths) {
const foundPath = await this.recursiveDirectorySearch(searchPath, nameVariations, { caseSensitive, verbose });
if (foundPath) {
if (verbose) {
console.log(chalk.green(`✓ Found function via fuzzy search: ${foundPath}`));
}
return foundPath;
}
}
}
if (verbose) {
console.log(chalk.yellow(`⚠ Function directory not found: ${functionName}`));
}
return null;
}
generateNameVariations(name) {
const variations = new Set([
name,
name.toLowerCase(),
name.replace(/\s+/g, "-"),
name.replace(/\s+/g, "_"),
name.replace(/[^a-z0-9]/gi, ""),
name.replace(/[-_\s]+/g, ""),
name.replace(/[-_\s]+/g, "-").toLowerCase(),
name.replace(/[-_\s]+/g, "_").toLowerCase(),
]);
return variations;
}
getStandardFunctionPaths(searchPaths, functionName) {
const normalizedName = functionName.toLowerCase().replace(/\s+/g, "-");
const paths = [];
for (const basePath of searchPaths) {
paths.push(join(basePath, "functions", functionName), join(basePath, "functions", normalizedName), join(basePath, "src", "functions", functionName), join(basePath, "src", "functions", normalizedName), join(basePath, functionName), join(basePath, normalizedName), join(basePath, "appwrite", "functions", functionName), join(basePath, "appwrite", "functions", normalizedName));
}
return paths;
}
async recursiveDirectorySearch(searchPath, nameVariations, options) {
const { caseSensitive, verbose } = options;
try {
const stats = await fs.promises.stat(searchPath);
if (!stats.isDirectory())
return null;
const entries = await fs.promises.readdir(searchPath, { withFileTypes: true });
for (const entry of entries) {
if (!entry.isDirectory())
continue;
// Skip common directories that won't contain functions
if (this.shouldSkipDirectory(entry.name))
continue;
const entryPath = join(searchPath, entry.name);
// Check if this directory matches our function name
const entryNameToCheck = caseSensitive ? entry.name : entry.name.toLowerCase();
if (nameVariations.has(entryNameToCheck)) {
if (await this.isValidFunctionDirectory(entryPath)) {
return entryPath;
}
}
// Recursively search subdirectories
const result = await this.recursiveDirectorySearch(entryPath, nameVariations, options);
if (result)
return result;
}
}
catch (error) {
if (verbose) {
console.log(chalk.gray(`Skipping inaccessible directory: ${searchPath}`));
}
}
return null;
}
shouldSkipDirectory(dirName) {
const skipDirs = new Set([
'node_modules',
'.git',
'.vscode',
'.idea',
'dist',
'build',
'coverage',
'.nyc_output',
'tmp',
'temp',
'.cache',
'__pycache__',
'.pytest_cache',
'venv',
'.venv',
'env',
'.env'
]);
return skipDirs.has(dirName) || dirName.startsWith('.');
}
async isValidFunctionDirectory(path) {
try {
const stats = await fs.promises.stat(path);
if (!stats.isDirectory())
return false;
const files = await fs.promises.readdir(path);
// Check for common function indicators
const hasPackageJson = files.includes('package.json');
const hasMainFile = files.some(file => file.match(/^(index|main|app)\.(js|ts|py)$/i) ||
file === 'src' ||
file === 'lib');
const hasRequirements = files.includes('requirements.txt');
const hasPyProject = files.includes('pyproject.toml');
return hasPackageJson || hasMainFile || hasRequirements || hasPyProject;
}
catch {
return false;
}
}
/**
* Enhanced function deployment with better error handling and validation
*/
async deployFunction(functionConfig, functionPath, options = {}) {
const { activate = true, entrypoint = functionConfig.entrypoint || "index.js", commands = functionConfig.commands || "npm install", ignored = ["node_modules", ".git", ".vscode", ".DS_Store", "__pycache__", ".venv"], verbose = false, forceRedeploy = false } = options;
return await functionLimit(async () => {
if (verbose) {
console.log(chalk.blue(`🚀 Deploying function: ${functionConfig.name}`));
console.log(chalk.gray(` Path: ${functionPath}`));
console.log(chalk.gray(` Entrypoint: ${entrypoint}`));
}
// Validate function directory
if (!await this.isValidFunctionDirectory(functionPath)) {
throw new Error(`Invalid function directory: ${functionPath}`);
}
// Ensure function exists
let functionExists = false;
try {
await this.getFunction(functionConfig.$id);
functionExists = true;
}
catch (error) {
if (verbose) {
console.log(chalk.yellow(`Function ${functionConfig.$id} does not exist, creating...`));
}
}
// Create function if it doesn't exist
if (!functionExists) {
await this.createFunction(functionConfig, { verbose });
}
else if (forceRedeploy) {
await this.updateFunction(functionConfig, { verbose });
}
// Execute pre-deploy commands if specified
if (functionConfig.predeployCommands?.length) {
await this.executePredeployCommands(functionConfig.predeployCommands, functionPath, { verbose });
}
// Deploy the function
const deployment = await this.createDeployment(functionConfig.$id, functionPath, { activate, entrypoint, commands, ignored, verbose });
if (verbose) {
console.log(chalk.green(`✅ Function ${functionConfig.name} deployed successfully`));
}
return deployment;
});
}
async createFunction(functionConfig, options = {}) {
const { verbose = false } = options;
if (verbose) {
console.log(chalk.blue(`Creating function: ${functionConfig.name}`));
}
return await tryAwaitWithRetry(async () => {
return await this.functions.create(functionConfig.$id, functionConfig.name, functionConfig.runtime, functionConfig.execute || [], functionConfig.events || [], functionConfig.schedule || "", functionConfig.timeout || 15, functionConfig.enabled !== false, functionConfig.logging !== false, functionConfig.entrypoint, functionConfig.commands, functionConfig.scopes || [], functionConfig.installationId, functionConfig.providerRepositoryId, functionConfig.providerBranch, functionConfig.providerSilentMode, functionConfig.providerRootDirectory);
});
}
async updateFunction(functionConfig, options = {}) {
const { verbose = false } = options;
if (verbose) {
console.log(chalk.blue(`Updating function: ${functionConfig.name}`));
}
return await tryAwaitWithRetry(async () => {
return await this.functions.update(functionConfig.$id, functionConfig.name, functionConfig.runtime, functionConfig.execute || [], functionConfig.events || [], functionConfig.schedule || "", functionConfig.timeout || 15, functionConfig.enabled !== false, functionConfig.logging !== false, functionConfig.entrypoint, functionConfig.commands, functionConfig.scopes || [], functionConfig.installationId, functionConfig.providerRepositoryId, functionConfig.providerBranch, functionConfig.providerSilentMode, functionConfig.providerRootDirectory, functionConfig.specification);
});
}
async executePredeployCommands(commands, workingDir, options = {}) {
const { verbose = false } = options;
const { execSync } = await import("child_process");
const { platform } = await import("node:os");
if (verbose) {
console.log(chalk.blue("Executing pre-deploy commands..."));
}
const isWindows = platform() === "win32";
for (const command of commands) {
if (verbose) {
console.log(chalk.gray(` $ ${command}`));
}
try {
execSync(command, {
cwd: workingDir,
stdio: verbose ? "inherit" : "pipe",
shell: isWindows ? "cmd.exe" : "/bin/sh",
windowsHide: true,
});
}
catch (error) {
console.error(chalk.red(`Failed to execute command: ${command}`));
throw error;
}
}
if (verbose) {
console.log(chalk.green("✓ Pre-deploy commands completed"));
}
}
async createDeployment(functionId, codePath, options = {}) {
const { activate = true, entrypoint = "index.js", commands = "npm install", ignored = [], verbose = false } = options;
const { InputFile } = await import("node-appwrite/file");
const { create: createTarball } = await import("tar");
const tarPath = join(process.cwd(), `function-${functionId}-${Date.now()}.tar.gz`);
try {
if (verbose) {
console.log(chalk.blue("Creating deployment archive..."));
}
// Create tarball
await createTarball({
gzip: true,
file: tarPath,
cwd: codePath,
filter: (path) => {
const relativePath = relative(codePath, join(codePath, path)).toLowerCase();
const shouldIgnore = ignored.some(pattern => relativePath.startsWith(pattern.toLowerCase()) ||
relativePath.includes(`/${pattern.toLowerCase()}`) ||
relativePath.includes(`\\${pattern.toLowerCase()}`));
if (shouldIgnore && verbose) {
console.log(chalk.gray(` Ignoring: ${path}`));
}
return !shouldIgnore;
},
}, ["."]);
// Read and upload
const fileBuffer = await fs.promises.readFile(tarPath);
const fileObject = InputFile.fromBuffer(new Uint8Array(fileBuffer), `function-${functionId}.tar.gz`);
if (verbose) {
console.log(chalk.blue("Uploading deployment..."));
}
const deployment = await tryAwaitWithRetry(async () => {
return await this.functions.createDeployment(functionId, fileObject, activate, entrypoint, commands);
});
return deployment;
}
finally {
// Clean up tarball
try {
await fs.promises.unlink(tarPath);
}
catch {
// Ignore cleanup errors
}
}
}
async getFunction(functionId) {
return await queryLimit(() => tryAwaitWithRetry(async () => await this.functions.get(functionId)));
}
async listFunctions() {
return await queryLimit(() => tryAwaitWithRetry(async () => await this.functions.list()));
}
async deleteFunction(functionId) {
await functionLimit(() => tryAwaitWithRetry(async () => await this.functions.delete(functionId)));
}
/**
* Validate function configuration
*/
validateFunctionConfig(functionConfig) {
const errors = [];
if (!functionConfig.$id) {
errors.push("Function ID is required");
}
if (!functionConfig.name) {
errors.push("Function name is required");
}
if (!functionConfig.runtime) {
errors.push("Function runtime is required");
}
if (!functionConfig.entrypoint) {
errors.push("Function entrypoint is required");
}
// Validate timeout
if (functionConfig.timeout && (functionConfig.timeout < 1 || functionConfig.timeout > 900)) {
errors.push("Function timeout must be between 1 and 900 seconds");
}
return { valid: errors.length === 0, errors };
}
}