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.
231 lines (230 loc) • 11.3 kB
JavaScript
import {} from "node-appwrite";
import { validationRules, } from "appwrite-utils";
import { converterFunctions } from "appwrite-utils";
import { convertObjectBySchema } from "../utils/dataConverters.js";
import {} from "appwrite-utils";
import { afterImportActions } from "./afterImportActions.js";
import { logger } from "../shared/logging.js";
import { tryAwaitWithRetry } from "../utils/helperFunctions.js";
export class ImportDataActions {
db;
storage;
config;
converterDefinitions;
validityRuleDefinitions;
afterImportActionsDefinitions;
constructor(db, storage, config, converterDefinitions, validityRuleDefinitions, afterImportActionsDefinitions) {
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, attributeMappings) {
const conversionSchema = attributeMappings.reduce((schema, mapping) => {
schema[mapping.targetKey] = (originalValue) => {
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];
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;
}, {});
// 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, attributeMap, context) {
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];
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) => this.resolveTemplate(param, context, item));
// Apply the validation rule
let isValid = false;
if (Array.isArray(item)) {
isValid = item.every((item) => validationRule(item, ...resolvedParams));
}
else {
isValid = validationRule(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, attributeMap, context) {
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, params, // Accepts any type, including objects
context, item) {
const actionMethod = afterImportActions[actionName];
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(this.config, ...resolvedParams);
}
catch (error) {
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, context, item) {
// Function to recursively resolve paths, including handling [any] notation
const resolvePath = (path, currentContext) => {
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) => 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 = 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;
}
}