/** * Schema Analyzer for AppWrite Collections * * Analyzes AppWrite collections to identify missing userId attributes and permission issues. * Validates attribute properties and provides comprehensive reporting. * * Requirements: 1.1, 1.5 */ /** * @typedef {Object} UserIdAttributeProperties * @property {string} type - Attribute type (should be 'string') * @property {number} size - Maximum character length (should be 255) * @property {boolean} required - Whether the attribute is required * @property {boolean} array - Whether the attribute is an array */ /** * @typedef {Object} CollectionPermissions * @property {string[]} create - Create permissions * @property {string[]} read - Read permissions * @property {string[]} update - Update permissions * @property {string[]} delete - Delete permissions */ /** * @typedef {Object} CollectionAnalysisResult * @property {string} collectionId - Collection identifier * @property {boolean} exists - Whether the collection exists * @property {boolean} hasUserId - Whether userId attribute exists * @property {UserIdAttributeProperties|null} userIdProperties - Properties of userId attribute * @property {CollectionPermissions} permissions - Current collection permissions * @property {string[]} issues - List of identified issues * @property {'critical'|'warning'|'info'} severity - Issue severity level */ export class SchemaAnalyzer { /** * @param {Object} appWriteManager - AppWrite manager instance */ constructor(appWriteManager) { this.appWriteManager = appWriteManager; this.databases = appWriteManager.databases; } /** * Analyzes a single collection's schema for userId attribute and permissions * @param {string} collectionId - Collection to analyze * @returns {Promise} Analysis result */ async analyzeCollection(collectionId) { const result = { collectionId, exists: false, hasUserId: false, userIdProperties: null, permissions: { create: [], read: [], update: [], delete: [] }, issues: [], severity: 'info', analyzedAt: new Date() }; try { // Check if collection exists by trying to get its attributes const collection = await this.databases.getCollection( this.appWriteManager.config.databaseId, collectionId ); result.exists = true; // Get collection attributes to check for userId const attributes = collection.attributes || []; const userIdAttribute = attributes.find(attr => attr.key === 'userId'); if (userIdAttribute) { result.hasUserId = true; result.userIdProperties = { type: userIdAttribute.type, size: userIdAttribute.size, required: userIdAttribute.required, array: userIdAttribute.array || false, key: userIdAttribute.key, status: userIdAttribute.status }; // Validate attribute properties const isValid = await this.validateAttributeProperties(userIdAttribute); if (!isValid) { result.issues.push('userId attribute has incorrect properties'); result.severity = 'warning'; } } else { result.hasUserId = false; result.issues.push('userId attribute is missing'); result.severity = 'critical'; } // Check permissions result.permissions = await this.checkPermissions(collectionId); // Validate permissions const expectedPermissions = { create: ['users'], read: ['user:$userId'], update: ['user:$userId'], delete: ['user:$userId'] }; for (const [action, expected] of Object.entries(expectedPermissions)) { const current = result.permissions[action] || []; if (!this._arraysEqual(current, expected)) { result.issues.push(`${action} permissions are incorrect`); if (result.severity === 'info') { result.severity = 'warning'; } } } } catch (error) { if (error.code === 404) { result.exists = false; result.issues.push('Collection does not exist'); result.severity = 'critical'; } else { result.issues.push(`Analysis failed: ${error.message}`); result.severity = 'critical'; throw error; } } return result; } /** * Analyzes all required collections for schema issues * @returns {Promise} Array of analysis results */ async analyzeAllCollections() { const collectionIds = Object.values(this.appWriteManager.config.collections); const results = []; for (const collectionId of collectionIds) { try { const result = await this.analyzeCollection(collectionId); results.push(result); } catch (error) { // Create error result for failed analysis const errorResult = { collectionId, exists: false, hasUserId: false, userIdProperties: null, permissions: { create: [], read: [], update: [], delete: [] }, issues: [`Analysis failed: ${error.message}`], severity: 'critical', analyzedAt: new Date() }; results.push(errorResult); } } // Sort results by severity (critical first, then warning, then info) results.sort((a, b) => { const severityOrder = { critical: 0, warning: 1, info: 2 }; return severityOrder[a.severity] - severityOrder[b.severity]; }); return results; } /** * Validates that userId attribute has correct properties * @param {Object} attribute - Attribute object from AppWrite * @returns {boolean} Whether attribute properties are correct */ async validateAttributeProperties(attribute) { if (!attribute) return false; const expectedProperties = { type: 'string', size: 255, required: true, array: false }; return ( attribute.type === expectedProperties.type && attribute.size === expectedProperties.size && attribute.required === expectedProperties.required && (attribute.array || false) === expectedProperties.array ); } /** * Checks collection permissions for proper configuration * @param {string} collectionId - Collection to check * @returns {Promise} Current permissions */ async checkPermissions(collectionId) { try { const collection = await this.databases.getCollection( this.appWriteManager.config.databaseId, collectionId ); return { create: collection.documentSecurity ? collection.$permissions?.create || [] : [], read: collection.documentSecurity ? collection.$permissions?.read || [] : [], update: collection.documentSecurity ? collection.$permissions?.update || [] : [], delete: collection.documentSecurity ? collection.$permissions?.delete || [] : [] }; } catch (error) { console.warn(`Failed to check permissions for collection ${collectionId}:`, error.message); return { create: [], read: [], update: [], delete: [] }; } } /** * Helper method to compare arrays for equality * @param {Array} arr1 - First array * @param {Array} arr2 - Second array * @returns {boolean} Whether arrays are equal * @private */ _arraysEqual(arr1, arr2) { if (arr1.length !== arr2.length) return false; const sorted1 = [...arr1].sort(); const sorted2 = [...arr2].sort(); return sorted1.every((val, index) => val === sorted2[index]); } /** * Categorizes analysis results by severity level * @param {CollectionAnalysisResult[]} results - Analysis results to categorize * @returns {Object} Categorized results with counts */ categorizeIssuesBySeverity(results) { const categorized = { critical: [], warning: [], info: [], counts: { critical: 0, warning: 0, info: 0, total: results.length } }; for (const result of results) { categorized[result.severity].push(result); categorized.counts[result.severity]++; } return categorized; } /** * Generates comprehensive analysis report with collection names and details * @param {CollectionAnalysisResult[]} results - Analysis results * @returns {Object} Comprehensive analysis report */ generateComprehensiveReport(results) { const categorized = this.categorizeIssuesBySeverity(results); const timestamp = new Date(); const report = { timestamp, totalCollections: results.length, summary: { collectionsWithIssues: results.filter(r => r.issues.length > 0).length, collectionsWithoutUserId: results.filter(r => !r.hasUserId).length, collectionsWithIncorrectPermissions: results.filter(r => r.issues.some(issue => issue.includes('permissions')) ).length, nonExistentCollections: results.filter(r => !r.exists).length }, categorized, detailedResults: results.map(result => ({ collectionId: result.collectionId, status: result.severity, exists: result.exists, hasUserId: result.hasUserId, issues: result.issues, userIdProperties: result.userIdProperties, permissions: result.permissions, analyzedAt: result.analyzedAt })), recommendations: this._generateRecommendations(categorized) }; return report; } /** * Generates recommendations based on analysis results * @param {Object} categorized - Categorized analysis results * @returns {string[]} Array of recommendations * @private */ _generateRecommendations(categorized) { const recommendations = []; if (categorized.counts.critical > 0) { recommendations.push( `${categorized.counts.critical} collection(s) have critical issues that must be addressed immediately` ); const missingCollections = categorized.critical.filter(r => !r.exists); if (missingCollections.length > 0) { recommendations.push( `Create missing collections: ${missingCollections.map(r => r.collectionId).join(', ')}` ); } const missingUserId = categorized.critical.filter(r => r.exists && !r.hasUserId); if (missingUserId.length > 0) { recommendations.push( `Add userId attribute to collections: ${missingUserId.map(r => r.collectionId).join(', ')}` ); } } if (categorized.counts.warning > 0) { recommendations.push( `${categorized.counts.warning} collection(s) have warnings that should be reviewed` ); const permissionIssues = categorized.warning.filter(r => r.issues.some(issue => issue.includes('permissions')) ); if (permissionIssues.length > 0) { recommendations.push( `Review and fix permissions for collections: ${permissionIssues.map(r => r.collectionId).join(', ')}` ); } } if (categorized.counts.critical === 0 && categorized.counts.warning === 0) { recommendations.push('All collections are properly configured'); } return recommendations; } }