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.

359 lines (358 loc) 15.2 kB
import { z } from "zod"; import {} from "appwrite-utils"; import { MessageFormatter } from "../shared/messageFormatter.js"; import chalk from "chalk"; /** * Main configuration validation function * Validates the dual collections/tables configuration schema */ export function validateCollectionsTablesConfig(config) { const errors = []; const warnings = []; const suggestions = []; // Basic structure validation const structureErrors = validateBasicStructure(config); errors.push(...structureErrors.filter(e => e.severity === "error")); warnings.push(...structureErrors.filter(e => e.severity === "warning")); // Naming conflicts validation const namingConflicts = detectNamingConflicts(config); errors.push(...namingConflicts.filter(e => e.severity === "error")); warnings.push(...namingConflicts.filter(e => e.severity === "warning")); // Database reference validation const dbRefErrors = validateDatabaseReferences(config); errors.push(...dbRefErrors.filter(e => e.severity === "error")); warnings.push(...dbRefErrors.filter(e => e.severity === "warning")); // Schema consistency validation const consistencyErrors = validateSchemaConsistency(config); errors.push(...consistencyErrors.filter(e => e.severity === "error")); warnings.push(...consistencyErrors.filter(e => e.severity === "warning")); suggestions.push(...consistencyErrors.filter(e => e.severity === "info")); return { isValid: errors.length === 0, errors, warnings, suggestions }; } /** * Validates basic structure of collections and tables arrays */ function validateBasicStructure(config) { const errors = []; // Ensure arrays exist if (!config.collections && !config.tables) { errors.push({ type: "missing_required_field", message: "No collections or tables defined", details: "Configuration must include either collections or tables array", suggestion: "Add collections array for legacy Databases API or tables array for TablesDB API", severity: "warning" }); } // Validate collections array structure if (config.collections) { config.collections.forEach((item, index) => { if (!item.name && !item.$id) { errors.push({ type: "missing_required_field", message: `Collection at index ${index} missing required name or $id`, details: "Each collection must have either a name or $id field", suggestion: "Add name field to the collection definition", affectedItems: [`collections[${index}]`], severity: "error" }); } }); } // Validate tables array structure if (config.tables) { config.tables.forEach((item, index) => { if (!item.name && !item.tableId && !item.$id) { errors.push({ type: "missing_required_field", message: `Table at index ${index} missing required name, tableId, or $id`, details: "Each table must have at least a name, tableId, or $id field", suggestion: "Add name field to the table definition", affectedItems: [`tables[${index}]`], severity: "error" }); } }); } return errors; } /** * Detects naming conflicts between collections and tables */ export function detectNamingConflicts(config) { const errors = []; const conflicts = findNamingConflicts(config); conflicts.forEach(conflict => { errors.push({ type: "naming_conflict", message: `Naming conflict detected: '${conflict.name}'`, details: conflict.message, suggestion: conflict.type === "name" ? "Rename one of the conflicting items to avoid confusion" : "Use different IDs to ensure unique identification", affectedItems: [ conflict.collectionsIndex !== undefined ? `collections[${conflict.collectionsIndex}]` : undefined, conflict.tablesIndex !== undefined ? `tables[${conflict.tablesIndex}]` : undefined ].filter(Boolean), severity: "error" }); }); return errors; } /** * Find naming conflicts between collections and tables */ export function findNamingConflicts(config) { const conflicts = []; const collections = config.collections || []; const tables = config.tables || []; // Create maps for efficient lookup const collectionsByName = new Map(); const collectionsByI = new Map(); const tablesByName = new Map(); const tablesById = new Map(); // Index collections collections.forEach((collection, index) => { if (collection.name) { collectionsByName.set(collection.name.toLowerCase(), index); } if (collection.$id) { collectionsByI.set(collection.$id.toLowerCase(), index); } }); // Check tables for conflicts tables.forEach((table, tableIndex) => { const tableName = table.name?.toLowerCase(); const tableId = (table.tableId || table.$id)?.toLowerCase(); // Check name conflicts if (tableName && collectionsByName.has(tableName)) { conflicts.push({ name: table.name, collectionsIndex: collectionsByName.get(tableName), tablesIndex: tableIndex, type: "name", message: `Table '${table.name}' has the same name as a collection` }); } // Check ID conflicts if (tableId && collectionsByI.has(tableId)) { conflicts.push({ name: tableId, collectionsIndex: collectionsByI.get(tableId), tablesIndex: tableIndex, type: "id", message: `Table '${tableId}' has the same ID as a collection` }); } }); return conflicts; } /** * Validates database ID references in collections and tables */ export function validateDatabaseReferences(config) { const errors = []; const validDatabaseIds = new Set((config.databases || []).map(db => db.$id)); // Validate collection database references if (config.collections) { config.collections.forEach((collection, index) => { if (collection.databaseId && !validDatabaseIds.has(collection.databaseId)) { errors.push({ type: "invalid_database_reference", message: `Collection '${collection.name || collection.$id}' references invalid database '${collection.databaseId}'`, details: `Database '${collection.databaseId}' is not defined in the databases array`, suggestion: `Add database with $id '${collection.databaseId}' to the databases array or use a valid database ID`, affectedItems: [`collections[${index}].databaseId`], severity: "error" }); } }); } // Validate table database references if (config.tables) { config.tables.forEach((table, index) => { if (table.databaseId && !validDatabaseIds.has(table.databaseId)) { errors.push({ type: "invalid_database_reference", message: `Table '${table.name || table.tableId}' references invalid database '${table.databaseId}'`, details: `Database '${table.databaseId}' is not defined in the databases array`, suggestion: `Add database with $id '${table.databaseId}' to the databases array or use a valid database ID`, affectedItems: [`tables[${index}].databaseId`], severity: "error" }); } }); } return errors; } /** * Validates schema consistency between collections and tables */ export function validateSchemaConsistency(config) { const errors = []; // Check for duplicate definitions within the same array if (config.collections) { const duplicates = findDuplicatesInArray(config.collections, 'collections'); errors.push(...duplicates); } if (config.tables) { const duplicates = findDuplicatesInArray(config.tables, 'tables'); errors.push(...duplicates); } // Suggest using tables for newer Appwrite versions if (config.collections && config.collections.length > 0 && (!config.tables || config.tables.length === 0)) { errors.push({ type: "schema_inconsistency", message: "Using collections without tables", details: "Consider migrating to tables for compatibility with Appwrite 1.8+ and TablesDB API", suggestion: "Use the migration utilities to convert collections to tables format", severity: "info" }); } // Warn about mixed usage without apiMode configuration if (config.collections && config.collections.length > 0 && config.tables && config.tables.length > 0 && config.apiMode === "auto") { errors.push({ type: "schema_inconsistency", message: "Mixed collections and tables with auto API mode", details: "Using both collections and tables with auto API mode may lead to unpredictable behavior", suggestion: "Set apiMode to 'legacy' or 'tablesdb' explicitly, or migrate all collections to tables", severity: "warning" }); } return errors; } /** * Find duplicate definitions within a single array */ function findDuplicatesInArray(items, arrayName) { const errors = []; const namesSeen = new Set(); const idsSeen = new Set(); items.forEach((item, index) => { const name = item.name?.toLowerCase(); const rawId = ('$id' in item && item.$id) || ('tableId' in item && item.tableId); const id = rawId && typeof rawId === 'string' ? rawId.toLowerCase() : undefined; // Check name duplicates if (name) { if (namesSeen.has(name)) { errors.push({ type: "duplicate_definition", message: `Duplicate name '${item.name}' in ${arrayName} array`, details: `The name '${item.name}' appears multiple times in the ${arrayName} array`, suggestion: "Use unique names for each item in the array", affectedItems: [`${arrayName}[${index}].name`], severity: "error" }); } else { namesSeen.add(name); } } // Check ID duplicates if (id) { if (idsSeen.has(id)) { errors.push({ type: "duplicate_definition", message: `Duplicate ID '${id}' in ${arrayName} array`, details: `The ID '${id}' appears multiple times in the ${arrayName} array`, suggestion: "Use unique IDs for each item in the array", affectedItems: [`${arrayName}[${index}].${arrayName === 'tables' ? 'tableId' : '$id'}`], severity: "error" }); } else { idsSeen.add(id); } } }); return errors; } /** * Reports validation results with formatted output */ export function reportValidationResults(result, options = {}) { const { verbose = false } = options; if (result.isValid) { MessageFormatter.success("Configuration validation passed", { prefix: "Validation" }); if (result.warnings.length > 0) { MessageFormatter.info(`Found ${result.warnings.length} warnings`, { prefix: "Validation" }); if (verbose) { result.warnings.forEach(warning => { MessageFormatter.warning(warning.message, { prefix: "Warning" }); if (warning.details) { console.log(chalk.gray(` Details: ${warning.details}`)); } if (warning.suggestion) { console.log(chalk.blue(` Suggestion: ${warning.suggestion}`)); } }); } } if (result.suggestions.length > 0 && verbose) { MessageFormatter.info(`Found ${result.suggestions.length} suggestions`, { prefix: "Validation" }); result.suggestions.forEach(suggestion => { MessageFormatter.info(suggestion.message, { prefix: "Suggestion" }); if (suggestion.details) { console.log(chalk.gray(` Details: ${suggestion.details}`)); } if (suggestion.suggestion) { console.log(chalk.blue(` Recommendation: ${suggestion.suggestion}`)); } }); } } else { MessageFormatter.error(`Configuration validation failed with ${result.errors.length} errors`, undefined, { prefix: "Validation" }); result.errors.forEach(error => { MessageFormatter.error(error.message, undefined, { prefix: "Error" }); if (error.details) { console.log(chalk.gray(` Details: ${error.details}`)); } if (error.suggestion) { console.log(chalk.blue(` Suggestion: ${error.suggestion}`)); } if (error.affectedItems?.length) { console.log(chalk.gray(` Affected: ${error.affectedItems.join(", ")}`)); } }); if (result.warnings.length > 0) { MessageFormatter.warning(`Also found ${result.warnings.length} warnings`, { prefix: "Validation" }); if (verbose) { result.warnings.forEach(warning => { MessageFormatter.warning(warning.message, { prefix: "Warning" }); if (warning.details) { console.log(chalk.gray(` Details: ${warning.details}`)); } if (warning.suggestion) { console.log(chalk.blue(` Suggestion: ${warning.suggestion}`)); } }); } } } } /** * Validation with strict mode for enhanced checking */ export function validateWithStrictMode(config, strictMode = false) { const result = validateCollectionsTablesConfig(config); if (strictMode) { // In strict mode, promote warnings to errors const promotedErrors = result.warnings.map(warning => ({ ...warning, severity: "error" })); return { isValid: result.errors.length === 0 && result.warnings.length === 0, errors: [...result.errors, ...promotedErrors], warnings: [], suggestions: result.suggestions }; } return result; }