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.

157 lines (156 loc) 7.04 kB
import { AppwriteException, } from "node-appwrite"; import fs from "node:fs"; import path from "node:path"; import { getClientWithAuth } from "./getClientFromConfig.js"; export const toPascalCase = (str) => { return (str // Split the string into words on spaces or camelCase transitions .split(/(?:\s+)|(?:([A-Z][a-z]+))/g) // Filter out empty strings that can appear due to the split regex .filter(Boolean) // Capitalize the first letter of each word and join them together .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .join("")); }; export const toCamelCase = (str) => { return str .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => index === 0 ? word.toLowerCase() : word.toUpperCase()) .replace(/\s+/g, ""); }; export const ensureDirectoryExistence = (filePath) => { const dirname = path.dirname(filePath); if (fs.existsSync(dirname)) { return true; } ensureDirectoryExistence(dirname); fs.mkdirSync(dirname); }; export const writeFileSync = (filePath, content, options) => { ensureDirectoryExistence(filePath); fs.writeFileSync(filePath, content, options); }; export const readFileSync = (filePath) => { return fs.readFileSync(filePath, "utf8"); }; export const existsSync = (filePath) => { return fs.existsSync(filePath); }; export const mkdirSync = (filePath) => { ensureDirectoryExistence(filePath); fs.mkdirSync(filePath); }; export const readdirSync = (filePath) => { return fs.readdirSync(filePath); }; export const areCollectionNamesSame = (a, b) => { return (a.toLowerCase().trim().replace(" ", "") === b.toLowerCase().trim().replace(" ", "")); }; /** * Generates the view URL for a specific file based on the provided endpoint, project ID, bucket ID, file ID, and optional JWT token. * * @param {string} endpoint - the base URL endpoint * @param {string} projectId - the ID of the project * @param {string} bucketId - the ID of the bucket * @param {string} fileId - the ID of the file * @param {Models.Jwt} [jwt] - optional JWT token generated via the Appwrite SDK * @return {string} the generated view URL for the file */ export const getFileViewUrl = (endpoint, projectId, bucketId, fileId, jwt) => { return `${endpoint}/storage/buckets/${bucketId}/files/${fileId}/view?project=${projectId}${jwt ? `&jwt=${jwt.jwt}` : ""}`; }; /** * Generates a download URL for a file based on the provided endpoint, project ID, bucket ID, file ID, and optionally a JWT. * * @param {string} endpoint - The base URL endpoint. * @param {string} projectId - The ID of the project. * @param {string} bucketId - The ID of the bucket. * @param {string} fileId - The ID of the file. * @param {Models.Jwt} [jwt] - Optional JWT object for authentication with Appwrite. * @return {string} The complete download URL for the file. */ export const getFileDownloadUrl = (endpoint, projectId, bucketId, fileId, jwt) => { return `${endpoint}/storage/buckets/${bucketId}/files/${fileId}/download?project=${projectId}${jwt ? `&jwt=${jwt.jwt}` : ""}`; }; export const finalizeByAttributeMap = async (appwriteFolderPath, collection, item) => { const schemaFolderPath = path.join(appwriteFolderPath, "schemas"); const zodSchema = await import(`${schemaFolderPath}/${toCamelCase(collection.name)}.ts`); return zodSchema.parse({ ...item.context, ...item.finalData, }); }; export let numTimesFailedTotal = 0; /** * Tries to execute the given createFunction and retries up to 5 times if it fails. * Only retries on transient errors (network failures, 5xx errors). Does NOT retry validation errors (4xx). * * @param {() => Promise<any>} createFunction - The function to be executed. * @param {number} [attemptNum=0] - The number of attempts made so far (default: 0). * @return {Promise<any>} - A promise that resolves to the result of the createFunction or rejects with an error if it fails after 5 attempts. */ export const tryAwaitWithRetry = async (createFunction, attemptNum = 0, throwError = false) => { try { return await createFunction(); } catch (error) { const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase(); const errorCode = error.code; // Check if this is a validation error that should NOT be retried const isValidationError = errorCode === 400 || errorCode === 409 || errorCode === 422 || errorMessage.includes("already exists") || errorMessage.includes("attribute with the same key") || errorMessage.includes("invalid") && !errorMessage.includes("fetch failed") || errorMessage.includes("conflict") || errorMessage.includes("bad request"); // Check if this is a transient error that SHOULD be retried const isTransientError = errorCode === 522 || errorCode === "522" || // Cloudflare error errorCode >= 500 && errorCode < 600 || // 5xx server errors errorMessage.includes("fetch failed") || // Network failures errorMessage.includes("timeout") || errorMessage.includes("econnrefused") || errorMessage.includes("network error"); // Only retry if it's a transient error AND not a validation error if (isTransientError && !isValidationError) { if (errorCode === 522 || errorCode === "522") { console.log("Cloudflare error. Retrying..."); } else { console.log(`Fetch failed on attempt ${attemptNum}. Retrying...`); } numTimesFailedTotal++; if (attemptNum > 5) { throw error; } await delay(2500); return tryAwaitWithRetry(createFunction, attemptNum + 1); } // For validation errors or non-transient errors, throw immediately if (throwError) { throw error; } console.error("Error during retryAwait function: ", error); // @ts-ignore return Promise.resolve(); } }; export const getAppwriteClient = (endpoint, projectId, apiKey) => { return getClientWithAuth(endpoint, projectId, apiKey); }; export const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); /** * Calculates exponential backoff delay with configurable base and maximum * * @param retryCount - Current retry attempt number (0-indexed) * @param baseDelay - Base delay in milliseconds (default: 2000) * @param maxDelay - Maximum delay cap in milliseconds (default: 30000) * @returns Calculated delay in milliseconds * * @example * calculateExponentialBackoff(0) // Returns 2000 * calculateExponentialBackoff(1) // Returns 4000 * calculateExponentialBackoff(5) // Returns 30000 (capped) */ export const calculateExponentialBackoff = (retryCount, baseDelay = 2000, maxDelay = 30000) => { return Math.min(baseDelay * Math.pow(2, retryCount), maxDelay); };