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.
285 lines (258 loc) • 8.9 kB
text/typescript
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 {
type AppwriteFunction,
type FunctionScope,
type Specification,
type Runtime as AppwriteUtilsRuntime,
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?: string[]): string[] => {
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: Client,
queries?: string[],
search?: string
) => {
const functions = new Functions(client);
const functionsList = await functions.list(queries, search);
return functionsList;
};
export const getFunction = async (client: Client, functionId: string) => {
const functions = new Functions(client);
const functionResponse = await functions.get(functionId);
return functionResponse;
};
export const downloadLatestFunctionDeployment = async (
client: Client,
functionId: string,
basePath: string = 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: Client, functionId: string) => {
const functions = new Functions(client);
const functionResponse = await functions.delete(functionId);
return functionResponse;
};
export const createFunction = async (
client: Client,
functionConfig: AppwriteFunction
) => {
const functions = new Functions(client);
const functionResponse = await functions.create(
functionConfig.$id,
functionConfig.name,
functionConfig.runtime as 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: Client,
functionId: string,
specification: 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 as AppwriteUtilsRuntime,
scopes: functionFound.scopes as FunctionScope[],
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: Client) => {
const functions = new Functions(client);
const specifications = await functions.listSpecifications();
return specifications;
};
export const listFunctionDeployments = async (
client: Client,
functionId: string,
queries?: string[]
) => {
const functions = new Functions(client);
const deployments = await functions.listDeployments(functionId, queries);
return deployments;
};
export const updateFunction = async (
client: Client,
functionConfig: AppwriteFunction
) => {
const functions = new Functions(client);
const functionResponse = await functions.update(
functionConfig.$id,
functionConfig.name,
functionConfig.runtime as 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: "typescript-node" | "uv" | "count-docs-in-collection" | "hono-typescript",
functionName: string,
basePath: string = "./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: string, targetPath: string) => {
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;
};