Files
ebaysnipeextension/src/AppWriteSchemaAnalyzer.js
Kenso Grimm 216a972fef chore: initialize project repository with core extension files
- Add .gitignore to exclude node_modules, dist, logs, and system files
- Add comprehensive project documentation including README, deployment guide, and development setup
- Add .kiro project specifications for amazon-product-bar-extension, appwrite-cloud-storage, appwrite-userid-repair, blacklist-feature, and enhanced-item-management
- Add .kiro steering documents for product, structure, styling, and tech guidelines
- Add VSCode settings configuration for consistent development environment
- Add manifest.json and babel/vite configuration for extension build setup
- Add complete source code implementation including AppWrite integration, storage managers, UI components, and services
- Add comprehensive test suite with Jest configuration and 30+ test files covering all major modules
- Add test HTML files for integration testing and validation
- Add coverage reports and build validation scripts
- Add AppWrite setup and repair documentation for database schema management
- Add migration guides and responsive accessibility implementation documentation
- Establish foundation for Amazon product bar extension with full feature set including blacklist management, enhanced item workflows, and real-time synchronization
2026-01-12 17:46:42 +01:00

348 lines
13 KiB
JavaScript

/**
* 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<CollectionAnalysisResult>} 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<CollectionAnalysisResult[]>} 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<CollectionPermissions>} 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;
}
}