3 Commits
27 changed files with 576 additions and 342 deletions
+9 -2
View File
@@ -13,7 +13,9 @@ import { buildContext } from './src/shared/middleware/context.middleware.js';
const app = express();
// Middlewares
app.use(helmet());
app.use(helmet({
crossOriginResourcePolicy: { policy: 'cross-origin' },
}));
app.use(cors({
origin: process.env.CORS_ORIGIN || '*',
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS', 'PATCH'],
@@ -38,7 +40,12 @@ app.get('/health', (req, res) => {
});
// Centralized Feature Router Loader
app.use('/uploads', express.static('uploads'));
app.use('/uploads', express.static('uploads', {
setHeaders: (res) => {
res.setHeader('Cross-Origin-Resource-Policy', 'cross-origin');
res.setHeader('Access-Control-Allow-Origin', '*');
}
}));
registerRoutes(app);
// Global Error Handler
@@ -92,7 +92,7 @@ export class CatalogRepository {
{
model: models.Attribute,
as: 'variantAxes',
through: { attributes: [] },
through: { attributes: ['id', 'required', 'display_order', 'active'] },
required: false,
include: [
{
+111 -13
View File
@@ -101,6 +101,22 @@ export class CatalogService {
throw new ApiError(404, 'Product Family not found');
}
await this.attachCounts(record);
if (record.variantAxes && Array.isArray(record.variantAxes)) {
for (const axisAttr of record.variantAxes) {
const throughId = axisAttr.FamilyVariantAxis ? axisAttr.FamilyVariantAxis.id : null;
if (throughId) {
const suggestedVals = await models.FamilyVariantAxisValue.findAll({
where: { axis_id: throughId },
order: [['sort_order', 'ASC']]
});
axisAttr.setDataValue('suggestedValues', suggestedVals.map(v => v.value));
} else {
axisAttr.setDataValue('suggestedValues', []);
}
}
}
return record;
}
@@ -189,13 +205,15 @@ export class CatalogService {
// 4. Variant Strategy Validation & Deduplication
let variantAxes = [];
if (data.variantAxes && Array.isArray(data.variantAxes)) {
const uniqueAxes = [...new Set(data.variantAxes)];
const attributesSet = new Set(attributes);
const invalidAxes = uniqueAxes.filter(axis => !attributesSet.has(axis));
const invalidAxes = data.variantAxes.filter(axis => {
const attrId = typeof axis === 'string' ? axis : (axis.attributeId || axis.attribute_id || axis.id);
return !attrId || !attributesSet.has(attrId);
});
if (invalidAxes.length > 0) {
throw new Error('Variant axes must be a subset of family attributes');
}
variantAxes = uniqueAxes;
variantAxes = data.variantAxes;
}
// 5. Asset Family Validation & Deduplication
@@ -271,13 +289,23 @@ export class CatalogService {
throw new Error(`Total completeness rules weight must equal 100% (currently ${totalWeight}%)`);
}
// Category Resolution
const rawCategoryId = data.category || data.category_id || data.categoryId || null;
let categoryId = null;
if (rawCategoryId) {
const catObj = await models.Categorie.findByPk(rawCategoryId, { transaction });
if (catObj) {
categoryId = catObj.id;
}
}
// 9. Save Catalog
const createData = {
code: data.code,
name: data.name,
description: data.description,
status: data.status || 'draft',
category_id: null,
category_id: categoryId,
workflow_code: workflowCode,
completeness_rules: completenessRules,
attribute_set_id: attributeSetId
@@ -295,11 +323,32 @@ export class CatalogService {
}
// Save variant axis relationships
for (const attributeId of variantAxes) {
await models.FamilyVariantAxis.create({
for (let i = 0; i < variantAxes.length; i++) {
const item = variantAxes[i];
const attrId = typeof item === 'string' ? item : (item.attributeId || item.attribute_id || item.id);
const required = typeof item === 'object' && item.required !== undefined ? !!item.required : true;
const active = typeof item === 'object' && item.active !== undefined ? !!item.active : true;
const displayOrder = typeof item === 'object' && item.display_order !== undefined ? item.display_order : i;
const axisRecord = await models.FamilyVariantAxis.create({
family_id: record.id,
attribute_id: attributeId
attribute_id: attrId,
required,
display_order: displayOrder,
active
}, { transaction });
const suggestedValues = typeof item === 'object' ? (item.suggestedValues || item.suggested_values || []) : [];
for (let j = 0; j < suggestedValues.length; j++) {
const valItem = suggestedValues[j];
const valText = typeof valItem === 'string' ? valItem : valItem.value;
if (!valText) continue;
await models.FamilyVariantAxisValue.create({
axis_id: axisRecord.id,
value: valText,
sort_order: typeof valItem === 'object' && valItem.sort_order !== undefined ? valItem.sort_order : j
}, { transaction });
}
}
// Save asset requirements
@@ -420,14 +469,16 @@ export class CatalogService {
// 4. Variant Strategy Validation & Deduplication
let variantAxes = null;
if (data.variantAxes && Array.isArray(data.variantAxes)) {
const uniqueAxes = [...new Set(data.variantAxes)];
const targetAttributes = attributes || (await record.getAttributes({ transaction })).map(a => a.id);
const attributesSet = new Set(targetAttributes);
const invalidAxes = uniqueAxes.filter(axis => !attributesSet.has(axis));
const invalidAxes = data.variantAxes.filter(axis => {
const attrId = typeof axis === 'string' ? axis : (axis.attributeId || axis.attribute_id || axis.id);
return !attrId || !attributesSet.has(attrId);
});
if (invalidAxes.length > 0) {
throw new Error('Variant axes must be a subset of family attributes');
}
variantAxes = uniqueAxes;
variantAxes = data.variantAxes;
}
// 5. Asset Family Validation & Deduplication
@@ -509,6 +560,22 @@ export class CatalogService {
throw new Error(`Total completeness rules weight must equal 100% (currently ${totalWeight}%)`);
}
// Category Resolution for Update
const rawCategoryId = data.hasOwnProperty('category') ? data.category : (data.hasOwnProperty('category_id') ? data.category_id : (data.hasOwnProperty('categoryId') ? data.categoryId : undefined));
let categoryId = undefined;
if (rawCategoryId !== undefined) {
if (rawCategoryId) {
const catObj = await models.Categorie.findByPk(rawCategoryId, { transaction });
if (catObj) {
categoryId = catObj.id;
} else {
categoryId = null;
}
} else {
categoryId = null;
}
}
// 9. Update Catalog
const updateData = {
name: data.name || record.name,
@@ -518,6 +585,9 @@ export class CatalogService {
completeness_rules: completenessRules,
attribute_set_id: attributeSetId
};
if (categoryId !== undefined) {
updateData.category_id = categoryId;
}
await record.update(updateData, { transaction });
@@ -535,12 +605,40 @@ export class CatalogService {
}
if (variantAxes !== null) {
// Find existing axis records to clean up suggested values
const existingAxes = await models.FamilyVariantAxis.findAll({ where: { family_id: id }, transaction });
const existingAxisIds = existingAxes.map(a => a.id);
if (existingAxisIds.length > 0) {
await models.FamilyVariantAxisValue.destroy({ where: { axis_id: existingAxisIds }, transaction });
}
await models.FamilyVariantAxis.destroy({ where: { family_id: id }, transaction });
for (const attributeId of variantAxes) {
await models.FamilyVariantAxis.create({
for (let i = 0; i < variantAxes.length; i++) {
const item = variantAxes[i];
const attrId = typeof item === 'string' ? item : (item.attributeId || item.attribute_id || item.id);
const required = typeof item === 'object' && item.required !== undefined ? !!item.required : true;
const active = typeof item === 'object' && item.active !== undefined ? !!item.active : true;
const displayOrder = typeof item === 'object' && item.display_order !== undefined ? item.display_order : i;
const axisRecord = await models.FamilyVariantAxis.create({
family_id: id,
attribute_id: attributeId
attribute_id: attrId,
required,
display_order: displayOrder,
active
}, { transaction });
const suggestedValues = typeof item === 'object' ? (item.suggestedValues || item.suggested_values || []) : [];
for (let j = 0; j < suggestedValues.length; j++) {
const valItem = suggestedValues[j];
const valText = typeof valItem === 'string' ? valItem : valItem.value;
if (!valText) continue;
await models.FamilyVariantAxisValue.create({
axis_id: axisRecord.id,
value: valText,
sort_order: typeof valItem === 'object' && valItem.sort_order !== undefined ? valItem.sort_order : j
}, { transaction });
}
}
await AuditService.log({ action: 'VARIANT_AXES_CHANGED', resource: 'Catalog', resourceId: id, userId: context.userId || 'system', details: variantAxes });
}
@@ -39,7 +39,16 @@ export const createValidation = [
.isArray()
.withMessage('Allowed units must be an array')
.custom((value) => value.every(val => typeof val === 'string'))
.withMessage('Allowed units must be an array of unit IDs')
.withMessage('Allowed units must be an array of unit IDs'),
body('category')
.optional({ nullable: true })
.isString(),
body('category_id')
.optional({ nullable: true })
.isString(),
body('categoryId')
.optional({ nullable: true })
.isString()
];
export const updateValidation = [
@@ -85,7 +94,16 @@ export const updateValidation = [
.isArray()
.withMessage('Allowed units must be an array')
.custom((value) => value.every(val => typeof val === 'string'))
.withMessage('Allowed units must be an array of unit IDs')
.withMessage('Allowed units must be an array of unit IDs'),
body('category')
.optional({ nullable: true })
.isString(),
body('category_id')
.optional({ nullable: true })
.isString(),
body('categoryId')
.optional({ nullable: true })
.isString()
];
export const deleteValidation = [
@@ -1,20 +1,52 @@
import { Model, DataTypes } from 'sequelize';
export class FamilyVariantAxis extends Model {
static associate(models) {}
static associate(models) {
FamilyVariantAxis.belongsTo(models.Catalog, {
foreignKey: 'family_id',
as: 'family'
});
FamilyVariantAxis.belongsTo(models.Attribute, {
foreignKey: 'attribute_id',
as: 'attribute'
});
FamilyVariantAxis.hasMany(models.FamilyVariantAxisValue, {
foreignKey: 'axis_id',
as: 'suggestedValues'
});
}
}
export default (sequelize) => {
FamilyVariantAxis.init({
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
family_id: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true
allowNull: false
},
attribute_id: {
type: DataTypes.UUID,
allowNull: false
},
required: {
type: DataTypes.BOOLEAN,
allowNull: false,
primaryKey: true
defaultValue: true
},
display_order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
},
active: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: true
}
}, {
sequelize,
@@ -0,0 +1,42 @@
import { Model, DataTypes } from 'sequelize';
export class FamilyVariantAxisValue extends Model {
static associate(models) {
FamilyVariantAxisValue.belongsTo(models.FamilyVariantAxis, {
foreignKey: 'axis_id',
as: 'axis'
});
}
}
export default (sequelize) => {
FamilyVariantAxisValue.init({
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
axis_id: {
type: DataTypes.UUID,
allowNull: false
},
value: {
type: DataTypes.TEXT,
allowNull: false
},
sort_order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
}
}, {
sequelize,
modelName: 'FamilyVariantAxisValue',
tableName: 'family_variant_axis_values',
timestamps: true,
underscored: true
});
return FamilyVariantAxisValue;
};
@@ -99,6 +99,39 @@ export class ProductController {
next(error);
}
}
getVariantValues = async (req, res, next) => {
try {
const data = await service.getVariantValues(req.params.id, req.context);
return res.status(200).json({ success: true, data });
} catch (error) {
next(error);
}
}
saveVariantValues = async (req, res, next) => {
try {
const data = await service.saveVariantValues(req.params.id, req.body.values, req.context);
return res.status(200).json({ success: true, data });
} catch (error) {
next(error);
}
}
generateVariants = async (req, res, next) => {
try {
const result = await service.generateVariants(req.params.id, req.body, req.context);
return res.status(200).json({
success: true,
variants: result.variants,
generatedCount: result.generatedCount,
warnings: result.warnings
});
} catch (error) {
next(error);
}
}
}
export default new ProductController();
@@ -171,4 +171,27 @@ router.post(
controller.restore
);
router.get(
'/:id/variant-values',
authenticate,
authorize(['products.items']),
controller.getVariantValues
);
router.post(
'/:id/variant-values',
authenticate,
authorize(['products.items']),
controller.saveVariantValues
);
router.post(
'/:id/generate-variants',
authenticate,
authorize(['products.items']),
audit('GENERATE_VARIANTS'),
controller.generateVariants
);
export default router;
@@ -850,6 +850,67 @@ export class ProductService {
return restored;
}
async getVariantValues(productId, context = {}) {
const values = await models.ProductVariantValue.findAll({
where: { product_id: productId },
include: [
{
model: models.Attribute,
as: 'axis',
attributes: ['id', 'code', 'name', 'type']
}
],
order: [['sort_order', 'ASC'], ['created_at', 'ASC']]
});
return values;
}
async saveVariantValues(productId, values = [], context = {}) {
const transaction = await sequelize.transaction();
try {
await models.ProductVariantValue.destroy({
where: { product_id: productId },
transaction
});
const records = [];
for (let i = 0; i < values.length; i++) {
const item = values[i];
if (!item.axis_id || !item.value) continue;
const created = await models.ProductVariantValue.create({
product_id: productId,
axis_id: item.axis_id,
value: item.value,
sort_order: item.sort_order !== undefined ? item.sort_order : i
}, { transaction });
records.push(created);
}
await transaction.commit();
return records;
} catch (error) {
await transaction.rollback();
throw error;
}
}
async generateVariants(productId, data, context = {}) {
// Import variantService dynamically to avoid circular dependencies
const variantService = (await import('../../variants/variants/variant.service.js')).default;
const result = await variantService.generateBatch({
productId,
axes: data.axes,
skuTemplate: data.skuTemplate
}, context);
return {
variants: result.variants,
generatedCount: result.created ? result.created.length : 0,
warnings: result.skipped ? result.skipped.map(s => `Skipped combination ${JSON.stringify(s.combination)}: ${s.reason}`) : []
};
}
}
export default new ProductService();
@@ -0,0 +1,50 @@
import { Model, DataTypes } from 'sequelize';
export class ProductVariantValue extends Model {
static associate(models) {
ProductVariantValue.belongsTo(models.Product, {
foreignKey: 'product_id',
as: 'product'
});
ProductVariantValue.belongsTo(models.Attribute, {
foreignKey: 'axis_id',
as: 'axis'
});
}
}
export default (sequelize) => {
ProductVariantValue.init({
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
product_id: {
type: DataTypes.UUID,
allowNull: false
},
axis_id: {
type: DataTypes.UUID,
allowNull: false
},
value: {
type: DataTypes.TEXT,
allowNull: false
},
sort_order: {
type: DataTypes.INTEGER,
allowNull: false,
defaultValue: 0
}
}, {
sequelize,
modelName: 'ProductVariantValue',
tableName: 'product_variant_values',
timestamps: true,
underscored: true
});
return ProductVariantValue;
};
-2
View File
@@ -1,10 +1,8 @@
import { Router } from 'express';
import settingsRouter from './settings/setting.routes.js';
import themeRouter from './theme/routes/theme.routes.js';
const router = Router();
router.use('/settings/theme', themeRouter);
router.use('/settings', settingsRouter);
export default router;
@@ -3,6 +3,7 @@ import controller from './setting.controller.js';
import { authenticate } from '../../../shared/middleware/auth.middleware.js';
import { validate } from '../../../shared/middleware/validation.middleware.js';
import { authorize } from '../../../shared/middleware/permission.middleware.js';
import { audit } from '../../../shared/middleware/audit.middleware.js';
import {
createValidation,
updateValidation,
@@ -58,20 +59,11 @@ router.get(
* @swagger
* /api/v1/settings:
* post:
* summary: Create a new setting
* summary: Create a setting
* tags: [Settings]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* responses:
* 201:
* description: Created
* description: Success
*/
router.post(
'/',
@@ -79,6 +71,7 @@ router.post(
authorize(['settings.users']),
createValidation,
validate,
audit('CREATE_SETTING'),
controller.create
);
@@ -86,23 +79,12 @@ router.post(
* @swagger
* /api/v1/settings/{id}:
* put:
* summary: Update an existing setting
* summary: Update a setting
* tags: [Settings]
* parameters:
* - in: path
* name: id
* required: true
* schema:
* type: string
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* responses:
* 200:
* description: Success
@@ -113,6 +95,7 @@ router.put(
authorize(['settings.users']),
updateValidation,
validate,
audit('UPDATE_SETTING'),
controller.update
);
@@ -126,8 +109,6 @@ router.put(
* - in: path
* name: id
* required: true
* schema:
* type: string
* responses:
* 200:
* description: Success
@@ -138,6 +119,7 @@ router.delete(
authorize(['settings.users']),
deleteValidation,
validate,
audit('DELETE_SETTING'),
controller.delete
);
@@ -1,11 +0,0 @@
export const DEFAULT_THEME = 'royal-purple';
export const ALLOWED_THEME_CODES = [
'royal-purple',
'forest-green',
'ocean-blue',
'sunset-orange',
'dark'
];
export const SYSTEM_THEMES = ['royal-purple', 'dark'];
@@ -1,31 +0,0 @@
import service from '../services/theme.service.js';
export class ThemeController {
getCurrentTheme = async (req, res, next) => {
try {
const userId = req.user.user_id || req.user.id;
const data = await service.getCurrentTheme(userId);
return res.status(200).json({ success: true, data });
} catch (error) {
next(error);
}
}
updateCurrentTheme = async (req, res, next) => {
try {
const userId = req.user.user_id || req.user.id;
const { themeCode } = req.body;
const requestContext = {
method: req.method,
ip: req.ip,
userAgent: req.headers['user-agent']
};
await service.updateCurrentTheme(userId, themeCode, requestContext);
return res.status(200).json({ success: true, message: 'Theme updated successfully.' });
} catch (error) {
next(error);
}
}
}
export default new ThemeController();
-5
View File
@@ -1,5 +0,0 @@
export { default as themeRoutes } from './routes/theme.routes.js';
export { default as themeController } from './controllers/theme.controller.js';
export { default as themeService } from './services/theme.service.js';
export { default as themeRepository } from './repositories/theme.repository.js';
export { default as userThemeModel } from './models/userTheme.model.js';
@@ -1,39 +0,0 @@
import { Model, DataTypes } from 'sequelize';
export class UserTheme extends Model {
static associate(models) {
UserTheme.belongsTo(models.User, {
foreignKey: 'user_id',
as: 'user',
onDelete: 'CASCADE'
});
}
}
export default (sequelize) => {
UserTheme.init({
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
allowNull: false
},
user_id: {
type: DataTypes.INTEGER,
allowNull: false,
unique: true
},
theme_code: {
type: DataTypes.STRING(50),
allowNull: false
}
}, {
sequelize,
modelName: 'UserTheme',
tableName: 'user_themes',
timestamps: true,
underscored: true
});
return UserTheme;
};
@@ -1,26 +0,0 @@
import { models } from '../../../../shared/database/models.js';
export class ThemeRepository {
async findByUserId(userId, options = {}) {
return await models.UserTheme.findOne({
where: { user_id: userId },
...options
});
}
async create(data, options = {}) {
return await models.UserTheme.create(data, options);
}
async update(userId, themeCode, options = {}) {
const record = await this.findByUserId(userId, options);
if (!record) return null;
return await record.update({ theme_code: themeCode }, options);
}
async save(userThemeInstance, options = {}) {
return await userThemeInstance.save(options);
}
}
export default new ThemeRepository();
@@ -1,43 +0,0 @@
import { Router } from 'express';
import controller from '../controllers/theme.controller.js';
import { authenticate } from '../../../../shared/middleware/auth.middleware.js';
import { validate } from '../../../../shared/middleware/validation.middleware.js';
import { themeValidation } from '../validators/theme.validation.js';
const router = Router();
/**
* @swagger
* /api/v1/settings/theme:
* get:
* summary: Retrieve user theme preference
* tags: [Theme]
* responses:
* 200:
* description: Success
*/
router.get(
'/',
authenticate,
controller.getCurrentTheme
);
/**
* @swagger
* /api/v1/settings/theme:
* put:
* summary: Update user theme preference
* tags: [Theme]
* responses:
* 200:
* description: Success
*/
router.put(
'/',
authenticate,
themeValidation,
validate,
controller.updateCurrentTheme
);
export default router;
@@ -1,72 +0,0 @@
import repository from '../repositories/theme.repository.js';
import { ALLOWED_THEME_CODES, DEFAULT_THEME } from '../constants/theme.constants.js';
import { sequelize } from '../../../../shared/database/models.js';
import { AuditService } from '../../../../shared/services/audit.service.js';
import { ApiError } from '../../../../utils/helpers/ApiError.utils.js';
export class ThemeService {
async getCurrentTheme(userId) {
if (!userId) {
return { themeCode: DEFAULT_THEME };
}
const preference = await repository.findByUserId(userId);
return {
themeCode: preference ? preference.theme_code : DEFAULT_THEME
};
}
validateTheme(themeCode) {
if (!ALLOWED_THEME_CODES.includes(themeCode)) {
throw new ApiError(400, `Invalid theme code: "${themeCode}"`);
}
}
async updateCurrentTheme(userId, themeCode, requestContext = {}) {
if (!userId) {
throw new ApiError(401, 'Unauthorized');
}
this.validateTheme(themeCode);
const transaction = await sequelize.transaction();
try {
const existing = await repository.findByUserId(userId, { transaction });
const oldTheme = existing ? existing.theme_code : DEFAULT_THEME;
let result;
if (existing) {
result = await existing.update({ theme_code: themeCode }, { transaction });
} else {
result = await repository.create({
user_id: userId,
theme_code: themeCode
}, { transaction });
}
await transaction.commit();
// Log the theme change via the existing Audit System.
// We pass a dummy 'method' in details to satisfy AuditService validation checks.
await AuditService.log({
action: 'UPDATE_THEME_PREFERENCE',
resource: 'settings',
resourceId: result.id,
userId: userId,
old_value: { theme_code: oldTheme },
new_value: { theme_code: themeCode },
details: {
method: requestContext.method || 'PUT',
ip: requestContext.ip || '127.0.0.1',
userAgent: requestContext.userAgent || 'system'
}
});
return result;
} catch (error) {
await transaction.rollback();
throw error;
}
}
}
export default new ThemeService();
@@ -1,12 +0,0 @@
import { body } from 'express-validator';
import { ALLOWED_THEME_CODES } from '../constants/theme.constants.js';
export const themeValidation = [
body('themeCode')
.notEmpty()
.withMessage('themeCode is required')
.isString()
.withMessage('themeCode must be a string')
.isIn(ALLOWED_THEME_CODES)
.withMessage(`themeCode must be one of: ${ALLOWED_THEME_CODES.join(', ')}`)
];
@@ -0,0 +1,169 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
// 1. Extend or recreate family_variant_axes to ensure id, required, display_order, active exist
const familyAxesExists = await queryInterface.describeTable('family_variant_axes').catch(() => null);
if (familyAxesExists) {
if (!familyAxesExists.id) {
await queryInterface.addColumn('family_variant_axes', 'id', {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
allowNull: true
});
}
if (!familyAxesExists.required) {
await queryInterface.addColumn('family_variant_axes', 'required', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
});
}
if (!familyAxesExists.display_order) {
await queryInterface.addColumn('family_variant_axes', 'display_order', {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
});
}
if (!familyAxesExists.active) {
await queryInterface.addColumn('family_variant_axes', 'active', {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
});
}
} else {
await queryInterface.createTable('family_variant_axes', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
family_id: {
type: Sequelize.UUID,
allowNull: false,
references: { model: 'catalogs', key: 'id' },
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
attribute_id: {
type: Sequelize.UUID,
allowNull: false,
references: { model: 'attributes', key: 'id' },
onUpdate: 'CASCADE',
onDelete: 'RESTRICT'
},
required: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
},
display_order: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
},
active: {
type: Sequelize.BOOLEAN,
allowNull: false,
defaultValue: true
},
created_at: {
type: Sequelize.DATE,
allowNull: false
},
updated_at: {
type: Sequelize.DATE,
allowNull: false
}
});
}
// 2. Create family_variant_axis_values table
const familyAxisValuesExists = await queryInterface.describeTable('family_variant_axis_values').catch(() => null);
if (!familyAxisValuesExists) {
await queryInterface.createTable('family_variant_axis_values', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
axis_id: {
type: Sequelize.UUID,
allowNull: false
},
value: {
type: Sequelize.TEXT,
allowNull: false
},
sort_order: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
},
created_at: {
type: Sequelize.DATE,
allowNull: false
},
updated_at: {
type: Sequelize.DATE,
allowNull: false
}
});
await queryInterface.addIndex('family_variant_axis_values', ['axis_id'], { name: 'idx_fam_var_axis_val_axis' }).catch(() => null);
}
// 3. Create product_variant_values table
const productVariantValuesExists = await queryInterface.describeTable('product_variant_values').catch(() => null);
if (!productVariantValuesExists) {
await queryInterface.createTable('product_variant_values', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
product_id: {
type: Sequelize.UUID,
allowNull: false,
references: { model: 'products', key: 'id' },
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
axis_id: {
type: Sequelize.UUID,
allowNull: false,
references: { model: 'attributes', key: 'id' },
onUpdate: 'CASCADE',
onDelete: 'RESTRICT'
},
value: {
type: Sequelize.TEXT,
allowNull: false
},
sort_order: {
type: Sequelize.INTEGER,
allowNull: false,
defaultValue: 0
},
created_at: {
type: Sequelize.DATE,
allowNull: false
},
updated_at: {
type: Sequelize.DATE,
allowNull: false
}
});
await queryInterface.addIndex('product_variant_values', ['product_id'], { name: 'idx_prod_var_val_prod' }).catch(() => null);
}
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('product_variant_values').catch(() => null);
await queryInterface.dropTable('family_variant_axis_values').catch(() => null);
}
};
@@ -1,45 +0,0 @@
'use strict';
module.exports = {
up: async (queryInterface, Sequelize) => {
await queryInterface.createTable('user_themes', {
id: {
type: Sequelize.UUID,
defaultValue: Sequelize.UUIDV4,
primaryKey: true,
allowNull: false
},
user_id: {
type: Sequelize.INTEGER,
allowNull: false,
unique: true,
references: {
model: 'users',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE'
},
theme_code: {
type: Sequelize.STRING(50),
allowNull: false
},
created_at: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
},
updated_at: {
allowNull: false,
type: Sequelize.DATE,
defaultValue: Sequelize.literal('CURRENT_TIMESTAMP')
}
});
await queryInterface.addIndex('user_themes', ['user_id']);
},
down: async (queryInterface, Sequelize) => {
await queryInterface.dropTable('user_themes');
}
};
+9 -3
View File
@@ -18,11 +18,17 @@ const storage = multer.diskStorage({
});
export const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'application/pdf', 'text/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'];
if (allowedTypes.includes(file.mimetype)) {
const allowedTypes = [
'image/jpeg', 'image/jpg', 'image/png', 'image/webp', 'image/gif', 'image/svg+xml', 'image/avif', 'image/bmp',
'video/mp4', 'video/webm', 'video/quicktime',
'application/pdf', 'text/csv', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'application/vnd.ms-excel', 'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
];
if (allowedTypes.includes(file.mimetype) || file.mimetype.startsWith('image/') || file.mimetype.startsWith('video/')) {
cb(null, true);
} else {
cb(new Error('Invalid file type'), false);
cb(new Error(`Invalid file type: ${file.mimetype}`), false);
}
};
+5 -6
View File
@@ -16,10 +16,6 @@ export const associateModels = () => {
});
// Explicit RBAC Associations
if (models.User && models.UserTheme) {
models.User.hasOne(models.UserTheme, { foreignKey: 'user_id', as: 'theme' });
}
if (models.Tenant && models.User) {
models.User.belongsTo(models.Tenant, { foreignKey: 'tenant_id', as: 'tenant' });
models.Tenant.hasMany(models.User, { foreignKey: 'tenant_id', as: 'users' });
@@ -133,7 +129,6 @@ import brandModelInit from '../../features/brands/brands/brand.model.js';
import unitModelInit from '../../features/brands/units/unit.model.js';
import settingModelInit from '../../features/settings/settings/setting.model.js';
import auditLogModelInit from '../../features/auditLogs/auditLogs/auditLog.model.js';
import userThemeModelInit from '../../features/settings/theme/models/userTheme.model.js';
// Attribute Management Models
import attributeModelInit from '../../features/attributes/attributes/attribute.model.js';
@@ -149,6 +144,7 @@ import attributeSetGroupModelInit from '../../features/attributes/attributeSets/
// Product Family Bridge Models
import familyAttributeModelInit from '../../features/catalogs/catalogs/familyAttribute.model.js';
import familyVariantAxisModelInit from '../../features/catalogs/catalogs/familyVariantAxis.model.js';
import familyVariantAxisValueModelInit from '../../features/catalogs/catalogs/familyVariantAxisValue.model.js';
import familyAssetRequirementModelInit from '../../features/catalogs/catalogs/familyAssetRequirement.model.js';
import familyChannelModelInit from '../../features/catalogs/catalogs/familyChannel.model.js';
@@ -178,6 +174,7 @@ import channelModelInit from '../../features/channels/channels/channel.model.js'
import channelTypeModelInit from '../../features/channels/channelTypes/channelType.model.js';
import workflowModelInit from '../../features/workflows/workflow.model.js';
import productAttributeValueModelInit from '../../features/products/products/productAttributeValue.model.js';
import productVariantValueModelInit from '../../features/products/products/productVariantValue.model.js';
import productCompletenessModelInit from '../../features/products/products/productCompleteness.model.js';
export const initializeDatabaseModels = () => {
@@ -196,7 +193,6 @@ export const initializeDatabaseModels = () => {
registerModel('Unit', unitModelInit);
registerModel('Setting', settingModelInit);
registerModel('AuditLog', auditLogModelInit);
registerModel('UserTheme', userThemeModelInit);
// Attribute Management
registerModel('Attribute', attributeModelInit);
@@ -212,6 +208,7 @@ export const initializeDatabaseModels = () => {
// Product Family Bridges
registerModel('FamilyAttribute', familyAttributeModelInit);
registerModel('FamilyVariantAxis', familyVariantAxisModelInit);
registerModel('FamilyVariantAxisValue', familyVariantAxisValueModelInit);
registerModel('FamilyAssetRequirement', familyAssetRequirementModelInit);
registerModel('FamilyChannel', familyChannelModelInit);
@@ -220,6 +217,7 @@ export const initializeDatabaseModels = () => {
registerModel('ChannelType', channelTypeModelInit);
registerModel('WorkflowRegistry', workflowModelInit);
registerModel('ProductAttributeValue', productAttributeValueModelInit);
registerModel('ProductVariantValue', productVariantValueModelInit);
registerModel('ProductCompleteness', productCompletenessModelInit);
// Variants
@@ -247,3 +245,4 @@ export const initializeDatabaseModels = () => {
};
export { models, sequelize };
Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB