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
JavaScript
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;
};