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
This commit is contained in:
348
src/AppWriteSchemaAnalyzer.js
Normal file
348
src/AppWriteSchemaAnalyzer.js
Normal file
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user