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.

316 lines (300 loc) 11.6 kB
import { type Databases, type Storage } from "node-appwrite"; import type { AppwriteConfig } from "appwrite-utils"; import { validationRules, type ValidationRules, type AttributeMappings, } from "appwrite-utils"; import { converterFunctions, type ConverterFunctions } from "appwrite-utils"; import { convertObjectBySchema } from "../utils/dataConverters.js"; import { type AfterImportActions } from "appwrite-utils"; import { afterImportActions } from "./afterImportActions.js"; import { logger } from "../shared/logging.js"; import { tryAwaitWithRetry } from "../utils/helperFunctions.js"; export class ImportDataActions { private db: Databases; private storage: Storage; private config: AppwriteConfig; private converterDefinitions: ConverterFunctions; private validityRuleDefinitions: ValidationRules; private afterImportActionsDefinitions: AfterImportActions; constructor( db: Databases, storage: Storage, config: AppwriteConfig, converterDefinitions: ConverterFunctions, validityRuleDefinitions: ValidationRules, afterImportActionsDefinitions: AfterImportActions ) { this.db = db; this.storage = storage; this.config = config; this.converterDefinitions = converterDefinitions; this.validityRuleDefinitions = validityRuleDefinitions; this.afterImportActionsDefinitions = afterImportActionsDefinitions; } /** * Runs converter functions on the item based on the provided attribute mappings. * * @param item - The item to be transformed. * @param attributeMappings - The mappings that define how each attribute should be transformed. * @returns The transformed item. */ runConverterFunctions(item: any, attributeMappings: AttributeMappings) { const conversionSchema = attributeMappings.reduce((schema, mapping) => { schema[mapping.targetKey] = (originalValue: any) => { if (!mapping.converters) { return originalValue; } return mapping.converters?.reduce((value, converterName) => { let shouldProcessAsArray = false; if ( (converterName.includes("[Arr]") || converterName.includes("[arr]")) && Array.isArray(value) ) { shouldProcessAsArray = true; converterName = converterName .replace("[Arr]", "") .replace("[arr]", ""); } else if ( (!Array.isArray(value) && converterName.includes("[Arr]")) || converterName.includes("[arr]") ) { converterName = converterName .replace("[Arr]", "") .replace("[arr]", ""); } const converterFunction = converterFunctions[ converterName as keyof typeof converterFunctions ]; if (converterFunction) { if (Array.isArray(value) && !shouldProcessAsArray) { return value.map((item) => converterFunction(item)); } else { return converterFunction(value); } } else { logger.warn( `Converter function '${converterName}' is not defined.` ); return value; } }, originalValue); }; return schema; }, {} as Record<string, (value: any) => any>); // Convert the item using the constructed schema const convertedItem = convertObjectBySchema(item, conversionSchema); // Merge the converted item back into the original item object Object.assign(item, convertedItem); return item; } /** * Validates a single data item based on defined validation rules. * @param item The data item to validate. * @param attributeMap The attribute mappings for the data item. * @param context The context for resolving templated parameters in validation rules. * @returns A promise that resolves to true if the item is valid, false otherwise. */ validateItem( item: any, attributeMap: AttributeMappings, context: { [key: string]: any } ): boolean { for (const mapping of attributeMap) { const { validationActions } = mapping; if ( !validationActions || !Array.isArray(validationActions) || !validationActions.length ) { return true; // Assume items without validation actions as valid. } for (const ruleDef of validationActions) { const { action, params } = ruleDef; const validationRule = validationRules[action as keyof typeof validationRules]; if (!validationRule) { logger.warn(`Validation rule '${action}' is not defined.`); continue; // Optionally, consider undefined rules as a validation failure. } // Resolve templated parameters const resolvedParams = params.map((param: any) => this.resolveTemplate(param, context, item) ); // Apply the validation rule let isValid = false; if (Array.isArray(item)) { isValid = item.every((item) => (validationRule as any)(item, ...resolvedParams) ); } else { isValid = (validationRule as any)(item, ...resolvedParams); } if (!isValid) { logger.error( `Validation failed for rule '${action}' with params ${params.join( ", " )}` ); return false; // Stop validation on first failure } } } return true; // The item passed all validations } async executeAfterImportActions( item: any, attributeMap: AttributeMappings, context: { [key: string]: any } ): Promise<void> { for (const mapping of attributeMap) { const { postImportActions } = mapping; if (!postImportActions || !Array.isArray(postImportActions)) { continue; // Skip to the next attribute if no actions are defined } for (const actionDef of postImportActions) { const { action, params } = actionDef; console.log( `Executing post-import action '${action}' for attribute '${ mapping.targetKey }' with params ${params.join(", ")}...` ); try { await tryAwaitWithRetry( async () => await this.executeAction(action, params, context, item) ); } catch (error) { logger.error( `Failed to execute post-import action '${action}' for attribute '${mapping.targetKey}':`, error ); } } } } async executeAction( actionName: string, params: any[], // Accepts any type, including objects context: { [key: string]: any }, item: any ): Promise<void> { const actionMethod = afterImportActions[actionName as keyof typeof afterImportActions]; if (typeof actionMethod === "function") { try { // Resolve parameters, handling both strings and objects const resolvedParams = params.map((param) => { // Directly resolve each param, whether it's an object or a string return this.resolveTemplate(param, context, item); }); // Execute the action with resolved parameters // Parameters are passed as-is, with objects treated as single parameters console.log( `Executing action '${actionName}' from context with params:`, resolvedParams ); logger.info( `Executing action '${actionName}' from context: ${JSON.stringify( context, null, 2 )} with params:`, resolvedParams ); await (actionMethod as any)(this.config, ...resolvedParams); } catch (error: any) { logger.error( `Error executing action '${actionName}' with context:`, context, error ); throw new Error( `Execution failed for action '${actionName}': ${error.message}` ); } } else { logger.warn(`Action '${actionName}' is not defined.`); throw new Error(`Action '${actionName}' is not defined.`); } } /** * Resolves a templated string or object using the provided context and current data item. * If the template is a string that starts and ends with "{}", it replaces it with the corresponding value from item or context. * If the template is an object, it recursively resolves its properties. * @param template The templated string or object. * @param context The context for resolving the template. * @param item The current data item being processed. */ resolveTemplate( template: any, context: { [key: string]: any }, item: any ): any { // Function to recursively resolve paths, including handling [any] notation const resolvePath = (path: string, currentContext: any): any => { const anyKeyRegex = /\[any\]/g; let pathParts = path.split(".").filter(Boolean); return pathParts.reduce((acc, part, index) => { // Handle [any] part by iterating over all elements if it's an object or an array if (part === "[any]") { if (Array.isArray(acc)) { return acc .map((item) => item[pathParts[index + 1]]) .filter((item) => item !== undefined); } else if (typeof acc === "object") { return Object.values(acc) .map((item: any) => item[pathParts[index + 1]]) .filter((item) => item !== undefined); } } else { return acc?.[part]; } }, currentContext); }; if (typeof template === "string") { // Matches placeholders in the template const regex = /\{([^}]+)\}/g; let match; let resolvedString = template; while ((match = regex.exec(template)) !== null) { const path = match[1]; // Resolve the path, handling [any] notation and arrays/objects const resolvedValue = resolvePath(path, { ...context, ...item }); if (resolvedValue !== undefined) { // If it's an array (from [any] notation), join the values; adjust as needed const value = Array.isArray(resolvedValue) ? resolvedValue.join(", ") : resolvedValue; resolvedString = resolvedString.replace(match[0], value); } else { logger.warn( `Failed to resolve ${template} in context: `, JSON.stringify({ ...context, ...item }, null, 2) ); } } // console.log(`Resolved string: ${resolvedString}`); return resolvedString; } else if (typeof template === "object" && template !== null) { // Recursively resolve templates for each property in the object const resolvedObject: any = Array.isArray(template) ? [] : {}; for (const key in template) { const resolvedValue = this.resolveTemplate( template[key], context, item ); if (resolvedValue !== undefined) { // Only assign if resolvedValue is not undefined resolvedObject[key] = resolvedValue; } } return resolvedObject; } // console.log(`Template is not a string or object: ${template}`); return template; } }