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.

175 lines (174 loc) 8.29 kB
import { AppwriteException, Client, Functions, Query, Runtime, } from "node-appwrite"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import fs from "node:fs"; import { EventTypeSchema, } from "appwrite-utils"; import chalk from "chalk"; import { extract as extractTar } from "tar"; import { MessageFormatter } from "../shared/messageFormatter.js"; import { expandTildePath, normalizeFunctionName } from "./pathResolution.js"; /** * Validates and filters events array for Appwrite functions * - Filters out empty/invalid strings * - Validates against EventTypeSchema * - Limits to 100 items maximum (Appwrite limit) * - Returns empty array if input is invalid */ const validateEvents = (events) => { if (!events || !Array.isArray(events)) return []; return events .filter(event => { if (!event || typeof event !== 'string' || event.trim().length === 0) { return false; } // Validate against EventTypeSchema const result = EventTypeSchema.safeParse(event); if (!result.success) { MessageFormatter.warning(`Invalid event type "${event}" will be filtered out`, { prefix: "Functions" }); return false; } return true; }) .slice(0, 100); }; export const listFunctions = async (client, queries, search) => { const functions = new Functions(client); const functionsList = await functions.list(queries, search); return functionsList; }; export const getFunction = async (client, functionId) => { const functions = new Functions(client); const functionResponse = await functions.get(functionId); return functionResponse; }; export const downloadLatestFunctionDeployment = async (client, functionId, basePath = process.cwd()) => { const functions = new Functions(client); const functionInfo = await getFunction(client, functionId); const functionDeployments = await functions.listDeployments(functionId, [ Query.orderDesc("$createdAt"), ]); if (functionDeployments.deployments.length === 0) { throw new Error("No deployments found for function"); } const latestDeployment = functionDeployments.deployments[0]; const deploymentData = await functions.getDeploymentDownload(functionId, latestDeployment.$id); // Create function directory using provided basePath const functionDir = join(basePath, normalizeFunctionName(functionInfo.name)); await fs.promises.mkdir(functionDir, { recursive: true }); // Create temporary file for tar extraction const tarPath = join(functionDir, "temp.tar.gz"); const uint8Array = new Uint8Array(deploymentData); await fs.promises.writeFile(tarPath, uint8Array); try { // Extract tar file extractTar({ C: functionDir, file: tarPath, sync: true, }); return { path: functionDir, function: functionInfo, deployment: latestDeployment, }; } finally { // Clean up tar file await fs.promises.unlink(tarPath).catch(() => { }); } }; export const deleteFunction = async (client, functionId) => { const functions = new Functions(client); const functionResponse = await functions.delete(functionId); return functionResponse; }; export const createFunction = async (client, functionConfig) => { const functions = new Functions(client); const functionResponse = await functions.create(functionConfig.$id, functionConfig.name, functionConfig.runtime, functionConfig.execute, validateEvents(functionConfig.events), functionConfig.schedule, functionConfig.timeout, functionConfig.enabled, functionConfig.logging, functionConfig.entrypoint, functionConfig.commands, functionConfig.scopes, functionConfig.installationId, functionConfig.providerRepositoryId, functionConfig.providerBranch, functionConfig.providerSilentMode, functionConfig.providerRootDirectory); return functionResponse; }; export const updateFunctionSpecifications = async (client, functionId, specification) => { const curFunction = await listFunctions(client, [ Query.equal("$id", functionId), ]); if (curFunction.functions.length === 0) { throw new Error("Function not found"); } const functionFound = curFunction.functions[0]; try { const functionResponse = await updateFunction(client, { ...functionFound, runtime: functionFound.runtime, scopes: functionFound.scopes, specification: specification, }); return functionResponse; } catch (error) { if (error instanceof AppwriteException && error.message.includes("Invalid `specification`")) { MessageFormatter.error("Error updating function specifications, please try setting the env variable `_FUNCTIONS_CPUS` and `_FUNCTIONS_RAM` to non-zero values", undefined, { prefix: "Functions" }); } else { MessageFormatter.error("Error updating function specifications", error instanceof Error ? error : undefined, { prefix: "Functions" }); throw error; } } }; export const listSpecifications = async (client) => { const functions = new Functions(client); const specifications = await functions.listSpecifications(); return specifications; }; export const listFunctionDeployments = async (client, functionId, queries) => { const functions = new Functions(client); const deployments = await functions.listDeployments(functionId, queries); return deployments; }; export const updateFunction = async (client, functionConfig) => { const functions = new Functions(client); const functionResponse = await functions.update(functionConfig.$id, functionConfig.name, functionConfig.runtime, functionConfig.execute, validateEvents(functionConfig.events), functionConfig.schedule, functionConfig.timeout, functionConfig.enabled, functionConfig.logging, functionConfig.entrypoint, functionConfig.commands, functionConfig.scopes, functionConfig.installationId, functionConfig.providerRepositoryId, functionConfig.providerBranch, functionConfig.providerSilentMode, functionConfig.providerRootDirectory, functionConfig.specification); return functionResponse; }; export const createFunctionTemplate = async (templateType, functionName, basePath = "./functions") => { const expandedBasePath = expandTildePath(basePath); const functionPath = join(expandedBasePath, functionName); const currentFileUrl = import.meta.url; const currentDir = dirname(fileURLToPath(currentFileUrl)); const templatesPath = join(currentDir, "templates", templateType); // Create function directory await fs.promises.mkdir(functionPath, { recursive: true }); // Copy template files recursively const copyTemplateFiles = async (sourcePath, targetPath) => { const entries = await fs.promises.readdir(sourcePath, { withFileTypes: true, }); for (const entry of entries) { const srcPath = join(sourcePath, entry.name); const destPath = join(targetPath, entry.name); if (entry.isDirectory()) { await fs.promises.mkdir(destPath, { recursive: true }); await copyTemplateFiles(srcPath, destPath); } else { let content = await fs.promises.readFile(srcPath, "utf-8"); // Replace template variables content = content .replace(/\{\{functionName\}\}/g, functionName) .replace(/\{\{databaseId\}\}/g, "{{databaseId}}") .replace(/\{\{collectionId\}\}/g, "{{collectionId}}"); await fs.promises.writeFile(destPath, content); } } }; try { await copyTemplateFiles(templatesPath, functionPath); MessageFormatter.success(`Created ${templateType} function template at ${functionPath}`, { prefix: "Functions" }); } catch (error) { MessageFormatter.error("Failed to create function template", error instanceof Error ? error : undefined, { prefix: "Functions" }); throw error; } return functionPath; };