const { v4: uuidv4 } = require('uuid'); module.exports = (sequelize, DataTypes) => { const Category = sequelize.define('Category', { id: { type: DataTypes.CHAR(36), primaryKey: true, defaultValue: () => uuidv4(), allowNull: false, comment: 'UUID primary key' }, name: { type: DataTypes.STRING(100), allowNull: false, unique: { msg: 'Category name already exists' }, validate: { notEmpty: { msg: 'Category name cannot be empty' }, len: { args: [2, 100], msg: 'Category name must be between 2 and 100 characters' } }, comment: 'Category name' }, slug: { type: DataTypes.STRING(100), allowNull: false, unique: { msg: 'Category slug already exists' }, validate: { notEmpty: { msg: 'Slug cannot be empty' }, is: { args: /^[a-z0-9]+(?:-[a-z0-9]+)*$/, msg: 'Slug must be lowercase alphanumeric with hyphens only' } }, comment: 'URL-friendly slug' }, description: { type: DataTypes.TEXT, allowNull: true, comment: 'Category description' }, icon: { type: DataTypes.STRING(255), allowNull: true, comment: 'Icon URL or class' }, color: { type: DataTypes.STRING(20), allowNull: true, validate: { is: { args: /^#[0-9A-F]{6}$/i, msg: 'Color must be a valid hex color (e.g., #FF5733)' } }, comment: 'Display color (hex format)' }, isActive: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true, field: 'is_active', comment: 'Category active status' }, guestAccessible: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: false, field: 'guest_accessible', comment: 'Whether guests can access this category' }, // Statistics questionCount: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'question_count', validate: { min: 0 }, comment: 'Total number of questions in this category' }, quizCount: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'quiz_count', validate: { min: 0 }, comment: 'Total number of quizzes taken in this category' }, // Display order displayOrder: { type: DataTypes.INTEGER, allowNull: false, defaultValue: 0, field: 'display_order', comment: 'Display order (lower numbers first)' } }, { sequelize, modelName: 'Category', tableName: 'categories', timestamps: true, underscored: true, indexes: [ { unique: true, fields: ['name'] }, { unique: true, fields: ['slug'] }, { fields: ['is_active'] }, { fields: ['guest_accessible'] }, { fields: ['display_order'] }, { fields: ['is_active', 'guest_accessible'] } ] }); // Helper function to generate slug from name Category.generateSlug = function(name) { return name .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') // Remove special characters .replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens }; // Instance methods Category.prototype.incrementQuestionCount = async function() { this.questionCount += 1; await this.save(); }; Category.prototype.decrementQuestionCount = async function() { if (this.questionCount > 0) { this.questionCount -= 1; await this.save(); } }; Category.prototype.incrementQuizCount = async function() { this.quizCount += 1; await this.save(); }; // Class methods Category.findActiveCategories = async function(includeGuestOnly = false) { const where = { isActive: true }; if (includeGuestOnly) { where.guestAccessible = true; } return await this.findAll({ where, order: [['displayOrder', 'ASC'], ['name', 'ASC']] }); }; Category.findBySlug = async function(slug) { return await this.findOne({ where: { slug, isActive: true } }); }; Category.getGuestAccessibleCategories = async function() { return await this.findAll({ where: { isActive: true, guestAccessible: true }, order: [['displayOrder', 'ASC'], ['name', 'ASC']] }); }; Category.getCategoriesWithStats = async function() { return await this.findAll({ where: { isActive: true }, attributes: [ 'id', 'name', 'slug', 'description', 'icon', 'color', 'questionCount', 'quizCount', 'guestAccessible', 'displayOrder' ], order: [['displayOrder', 'ASC'], ['name', 'ASC']] }); }; // Hooks Category.beforeValidate((category) => { // Auto-generate slug from name if not provided if (!category.slug && category.name) { category.slug = Category.generateSlug(category.name); } // Ensure UUID is set if (!category.id) { category.id = uuidv4(); } }); Category.beforeCreate((category) => { // Ensure slug is generated even if validation was skipped if (!category.slug && category.name) { category.slug = Category.generateSlug(category.name); } }); Category.beforeUpdate((category) => { // Regenerate slug if name changed if (category.changed('name') && !category.changed('slug')) { category.slug = Category.generateSlug(category.name); } }); // Define associations Category.associate = function(models) { // Category has many questions if (models.Question) { Category.hasMany(models.Question, { foreignKey: 'categoryId', as: 'questions' }); } // Category has many quiz sessions if (models.QuizSession) { Category.hasMany(models.QuizSession, { foreignKey: 'categoryId', as: 'quizSessions' }); } // Category belongs to many guest settings (for guest-accessible categories) if (models.GuestSettings) { Category.belongsToMany(models.GuestSettings, { through: 'guest_settings_categories', foreignKey: 'categoryId', otherKey: 'guestSettingsId', as: 'guestSettings' }); } }; return Category; };