correction
This commit is contained in:
@@ -19,7 +19,7 @@ export default (sequelize) => {
|
||||
allowNull: false
|
||||
},
|
||||
changed_by: {
|
||||
type: DataTypes.UUID,
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
changes: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export default (sequelize) => {
|
||||
allowNull: false
|
||||
},
|
||||
changed_by: {
|
||||
type: DataTypes.UUID,
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
changes: {
|
||||
|
||||
@@ -5,13 +5,13 @@ function serializeAttribute(attr) {
|
||||
const json = attr.toJSON ? attr.toJSON() : { ...attr };
|
||||
|
||||
// Mapping to camelCase
|
||||
json.isRequired = json.is_required ?? false;
|
||||
json.isUnique = json.is_unique ?? false;
|
||||
json.isLocalizable = json.is_localizable ?? false;
|
||||
json.isVariantEligible = json.is_variant_eligible ?? false;
|
||||
json.isSearchable = json.is_searchable ?? false;
|
||||
json.isFilterable = json.is_filterable ?? false;
|
||||
json.isChannelSpecific = json.is_channel_specific ?? false;
|
||||
json.isRequired = json.is_required ?? json.isRequired ?? false;
|
||||
json.isUnique = json.is_unique ?? json.isUnique ?? false;
|
||||
json.isLocalizable = json.is_localizable ?? json.isLocalizable ?? false;
|
||||
json.isVariantEligible = json.is_variant_eligible ?? json.isVariantEligible ?? false;
|
||||
json.isSearchable = json.is_searchable ?? json.isSearchable ?? false;
|
||||
json.isFilterable = json.is_filterable ?? json.isFilterable ?? false;
|
||||
json.isChannelSpecific = json.is_channel_specific ?? json.isChannelSpecific ?? false;
|
||||
json.minLength = json.min_length ?? 0;
|
||||
json.maxLength = json.max_length ?? 255;
|
||||
json.regexPattern = json.regex_pattern ?? null;
|
||||
|
||||
@@ -119,15 +119,15 @@ export default (sequelize) => {
|
||||
defaultValue: 0
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.UUID,
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.UUID,
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
deleted_by: {
|
||||
type: DataTypes.UUID,
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
help_text: {
|
||||
|
||||
@@ -19,7 +19,7 @@ export default (sequelize) => {
|
||||
allowNull: false
|
||||
},
|
||||
changed_by: {
|
||||
type: DataTypes.UUID,
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
changes: {
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { Model, DataTypes } from 'sequelize';
|
||||
|
||||
export class Role extends Model {
|
||||
static associate(models) {}
|
||||
static associate(models) { }
|
||||
}
|
||||
|
||||
export default (sequelize) => {
|
||||
Role.init({
|
||||
id: {
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
type: DataTypes.INTEGER,
|
||||
primaryKey: true,
|
||||
autoIncrement: true
|
||||
},
|
||||
tenant_id: {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: true
|
||||
},
|
||||
role_code: {
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
type: DataTypes.STRING(50),
|
||||
allowNull: false
|
||||
},
|
||||
role_name: {
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
type: DataTypes.STRING(100),
|
||||
allowNull: false
|
||||
},
|
||||
description: {
|
||||
type: DataTypes.TEXT
|
||||
type: DataTypes.TEXT
|
||||
},
|
||||
role_type: {
|
||||
type: DataTypes.STRING(20),
|
||||
defaultValue: 'tenant',
|
||||
allowNull: false
|
||||
type: DataTypes.STRING(20),
|
||||
defaultValue: 'tenant',
|
||||
allowNull: false
|
||||
},
|
||||
is_system_role: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false
|
||||
},
|
||||
status: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: true
|
||||
},
|
||||
created_by: {
|
||||
type: DataTypes.INTEGER
|
||||
type: DataTypes.INTEGER
|
||||
},
|
||||
updated_by: {
|
||||
type: DataTypes.INTEGER
|
||||
type: DataTypes.INTEGER
|
||||
}
|
||||
}, {
|
||||
sequelize,
|
||||
|
||||
@@ -7,6 +7,13 @@ export class RoleRepository {
|
||||
const tenantFilter = context.userType !== 'platform' && context.tenantId
|
||||
? { tenant_id: context.tenantId }
|
||||
: {};
|
||||
|
||||
const { where: optionsWhere, ...restOptions } = options;
|
||||
const whereClause = {
|
||||
...(optionsWhere || {}),
|
||||
...tenantFilter
|
||||
};
|
||||
|
||||
return await models.Role.findAll({
|
||||
include: [
|
||||
{
|
||||
@@ -15,11 +22,8 @@ export class RoleRepository {
|
||||
through: { attributes: ['can_view', 'can_create', 'can_edit', 'can_delete', 'can_alter', 'can_import', 'can_export'] }
|
||||
}
|
||||
],
|
||||
...options,
|
||||
where: {
|
||||
...(options.where || {}),
|
||||
...tenantFilter
|
||||
}
|
||||
...restOptions,
|
||||
where: whereClause
|
||||
});
|
||||
}
|
||||
|
||||
@@ -48,7 +52,7 @@ export class RoleRepository {
|
||||
roleData.tenant_id = context.tenantId;
|
||||
}
|
||||
const role = await models.Role.create(roleData, { transaction });
|
||||
|
||||
|
||||
if (permissions && permissions.length > 0) {
|
||||
const validPermissions = permissions.filter(p => p.node_id != null);
|
||||
if (validPermissions.length > 0) {
|
||||
@@ -66,7 +70,7 @@ export class RoleRepository {
|
||||
await models.RolePermission.bulkCreate(rolePermissions, { transaction });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
await transaction.commit();
|
||||
return await this.findById(role.id, {}, context);
|
||||
} catch (error) {
|
||||
@@ -137,9 +141,9 @@ export class RoleRepository {
|
||||
// Delete associations first
|
||||
await models.RolePermission.destroy({ where: { role_id: id }, transaction });
|
||||
await models.UserRole.destroy({ where: { role_id: id }, transaction });
|
||||
|
||||
|
||||
await role.destroy({ transaction });
|
||||
|
||||
|
||||
await transaction.commit();
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import repository from './role.repository.js';
|
||||
import { models } from '../../../shared/database/models.js';
|
||||
import { ApiError } from '../../../utils/helpers/ApiError.utils.js';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
export class RoleService {
|
||||
async getAll(query = {}, context = {}) {
|
||||
@@ -8,13 +9,17 @@ export class RoleService {
|
||||
}
|
||||
|
||||
async getByTenant(tenantId, context = {}) {
|
||||
if (!tenantId || tenantId === 'platform' || tenantId === 'null' || tenantId === 'undefined') {
|
||||
return await repository.findAll({ where: { tenant_id: null } }, context);
|
||||
}
|
||||
if (tenantId === 'all') {
|
||||
return await repository.findAll({}, context);
|
||||
}
|
||||
const numericId = isNaN(Number(tenantId)) ? tenantId : Number(tenantId);
|
||||
return await repository.findAll({
|
||||
where: tenantId
|
||||
? { tenant_id: tenantId }
|
||||
: { tenant_id: null }
|
||||
where: {
|
||||
tenant_id: numericId
|
||||
}
|
||||
}, context);
|
||||
}
|
||||
|
||||
@@ -60,6 +65,15 @@ export class RoleService {
|
||||
}
|
||||
|
||||
async delete(id, context = {}) {
|
||||
const role = await repository.findById(id, {}, context);
|
||||
if (!role) {
|
||||
throw new ApiError(404, 'Role not found');
|
||||
}
|
||||
|
||||
if (role.is_system_role || role.role_code === 'SUPER_ADMIN' || role.role_code === 'SUPERADMIN' || role.role_name?.toLowerCase().includes('super admin')) {
|
||||
throw new ApiError(403, 'Forbidden: Super Admin and System roles cannot be deleted');
|
||||
}
|
||||
|
||||
const deleted = await repository.delete(id, context);
|
||||
if (!deleted) {
|
||||
throw new ApiError(404, 'Role not found');
|
||||
|
||||
@@ -82,6 +82,22 @@ export class UserService {
|
||||
}
|
||||
|
||||
async delete(id, context = {}) {
|
||||
const user = await repository.findById(id, {}, context);
|
||||
if (!user) {
|
||||
throw new ApiError(404, 'User not found');
|
||||
}
|
||||
|
||||
const isSuperAdmin = user.email === 'admin@admin.com' || (user.roles && user.roles.some(r =>
|
||||
r.role_code === 'SUPER_ADMIN' ||
|
||||
r.role_code === 'SUPERADMIN' ||
|
||||
r.role_name?.toLowerCase().includes('super admin') ||
|
||||
r.is_system_role
|
||||
));
|
||||
|
||||
if (isSuperAdmin) {
|
||||
throw new ApiError(403, 'Forbidden: Super Admin user cannot be deleted');
|
||||
}
|
||||
|
||||
const deleted = await repository.delete(id, context);
|
||||
if (!deleted) {
|
||||
throw new ApiError(404, 'User not found');
|
||||
|
||||
@@ -9,53 +9,83 @@ export class CatalogService {
|
||||
if (!record) return null;
|
||||
const id = record.id;
|
||||
|
||||
// Count associated products
|
||||
const productCount = await models.Product.count({
|
||||
where: { family_id: id },
|
||||
transaction
|
||||
});
|
||||
// Run primary counts and relation queries in parallel
|
||||
const [
|
||||
productCount,
|
||||
attributeCount,
|
||||
variantAxisCount,
|
||||
assetRequirementCount,
|
||||
channelCount,
|
||||
attributeRelations
|
||||
] = await Promise.all([
|
||||
models.Product.count({ where: { family_id: id }, transaction }),
|
||||
models.FamilyAttribute.count({ where: { family_id: id }, transaction }),
|
||||
models.FamilyVariantAxis.count({ where: { family_id: id }, transaction }),
|
||||
models.FamilyAssetRequirement.count({ where: { family_id: id }, transaction }),
|
||||
models.FamilyChannel.count({ where: { family_id: id }, transaction }),
|
||||
models.FamilyAttribute.findAll({ where: { family_id: id }, transaction })
|
||||
]);
|
||||
|
||||
// Count associated attributes
|
||||
const attributeCount = await models.FamilyAttribute.count({
|
||||
where: { family_id: id },
|
||||
transaction
|
||||
});
|
||||
|
||||
// Count associated variant axes
|
||||
const variantAxisCount = await models.FamilyVariantAxis.count({
|
||||
where: { family_id: id },
|
||||
transaction
|
||||
});
|
||||
|
||||
// Count associated asset requirements
|
||||
const assetRequirementCount = await models.FamilyAssetRequirement.count({
|
||||
where: { family_id: id },
|
||||
transaction
|
||||
});
|
||||
|
||||
// Count associated channels
|
||||
const channelCount = await models.FamilyChannel.count({
|
||||
where: { family_id: id },
|
||||
transaction
|
||||
});
|
||||
|
||||
// Count unique attribute groups
|
||||
const attributeRelations = await models.FamilyAttribute.findAll({
|
||||
where: { family_id: id },
|
||||
transaction
|
||||
});
|
||||
const attributeIds = attributeRelations.map(ar => ar.attribute_id);
|
||||
let attributeGroupsCount = 0;
|
||||
if (attributeIds.length > 0) {
|
||||
const groupRelations = await models.AttributeGroupAttribute.findAll({
|
||||
where: { attribute_id: attributeIds },
|
||||
attributes: ['group_id'],
|
||||
raw: true,
|
||||
transaction
|
||||
});
|
||||
const uniqueGroups = new Set(groupRelations.map(gr => gr.group_id));
|
||||
attributeGroupsCount = uniqueGroups.size;
|
||||
|
||||
// Extract allowedBrands and allowedUnits from completeness_rules
|
||||
const rules = record.completeness_rules || {};
|
||||
let brandIds = rules.allowedBrands || [];
|
||||
if (typeof brandIds === 'string') {
|
||||
try { brandIds = JSON.parse(brandIds); } catch (e) { brandIds = []; }
|
||||
}
|
||||
let unitIds = rules.allowedUnits || [];
|
||||
if (typeof unitIds === 'string') {
|
||||
try { unitIds = JSON.parse(unitIds); } catch (e) { unitIds = []; }
|
||||
}
|
||||
|
||||
// Run sub-queries concurrently via Promise.all
|
||||
const [groupRelations, completenessRecords, allowedBrands, allowedUnits] = await Promise.all([
|
||||
attributeIds.length > 0
|
||||
? models.AttributeGroupAttribute.findAll({
|
||||
where: { attribute_id: attributeIds },
|
||||
attributes: ['group_id'],
|
||||
raw: true,
|
||||
transaction
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
|
||||
productCount > 0
|
||||
? models.Product.findAll({
|
||||
where: { family_id: id },
|
||||
attributes: ['id'],
|
||||
transaction
|
||||
}).then(products => {
|
||||
const pIds = products.map(p => p.id);
|
||||
return pIds.length > 0
|
||||
? models.ProductCompleteness.findAll({
|
||||
where: { product_id: pIds, channel: 'default' },
|
||||
attributes: ['percentage'],
|
||||
transaction
|
||||
})
|
||||
: Promise.resolve([]);
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
|
||||
Array.isArray(brandIds) && brandIds.length > 0
|
||||
? models.Brand.findAll({
|
||||
where: { id: brandIds },
|
||||
attributes: ['id', 'name', 'code', 'status'],
|
||||
transaction
|
||||
})
|
||||
: Promise.resolve([]),
|
||||
|
||||
Array.isArray(unitIds) && unitIds.length > 0
|
||||
? models.Unit.findAll({
|
||||
where: { id: unitIds },
|
||||
attributes: ['id', 'name', 'code', 'symbol', 'status'],
|
||||
transaction
|
||||
})
|
||||
: Promise.resolve([])
|
||||
]);
|
||||
|
||||
const uniqueGroups = new Set(groupRelations.map(gr => gr.group_id));
|
||||
const attributeGroupsCount = uniqueGroups.size;
|
||||
|
||||
record.setDataValue('productCount', productCount);
|
||||
record.setDataValue('attributeCount', attributeCount);
|
||||
@@ -65,63 +95,16 @@ export class CatalogService {
|
||||
record.setDataValue('attributeGroups', attributeGroupsCount);
|
||||
record.setDataValue('canDelete', productCount === 0);
|
||||
|
||||
// Calculate average completeness for the family
|
||||
let completeness = 100;
|
||||
if (productCount > 0) {
|
||||
const products = await models.Product.findAll({
|
||||
where: { family_id: id },
|
||||
attributes: ['id'],
|
||||
transaction
|
||||
});
|
||||
const productIds = products.map(p => p.id);
|
||||
const completenessRecords = await models.ProductCompleteness.findAll({
|
||||
where: {
|
||||
product_id: productIds,
|
||||
channel: 'default'
|
||||
},
|
||||
attributes: ['percentage'],
|
||||
transaction
|
||||
});
|
||||
if (completenessRecords.length > 0) {
|
||||
const totalPct = completenessRecords.reduce((sum, c) => sum + c.percentage, 0);
|
||||
completeness = Math.round(totalPct / completenessRecords.length);
|
||||
} else {
|
||||
completeness = 0;
|
||||
}
|
||||
if (productCount > 0 && completenessRecords.length > 0) {
|
||||
const totalPct = completenessRecords.reduce((sum, c) => sum + c.percentage, 0);
|
||||
completeness = Math.round(totalPct / completenessRecords.length);
|
||||
} else if (productCount > 0) {
|
||||
completeness = 0;
|
||||
}
|
||||
record.setDataValue('completeness', completeness);
|
||||
|
||||
// Load full Brand and Unit objects from completeness_rules IDs
|
||||
const rules = record.completeness_rules || {};
|
||||
let brandIds = rules.allowedBrands || [];
|
||||
if (typeof brandIds === 'string') {
|
||||
try { brandIds = JSON.parse(brandIds); } catch (e) { brandIds = []; }
|
||||
}
|
||||
if (Array.isArray(brandIds) && brandIds.length > 0) {
|
||||
const allowedBrands = await models.Brand.findAll({
|
||||
where: { id: brandIds },
|
||||
attributes: ['id', 'name', 'code', 'status'],
|
||||
transaction
|
||||
});
|
||||
record.setDataValue('allowedBrands', allowedBrands);
|
||||
} else {
|
||||
record.setDataValue('allowedBrands', []);
|
||||
}
|
||||
|
||||
let unitIds = rules.allowedUnits || [];
|
||||
if (typeof unitIds === 'string') {
|
||||
try { unitIds = JSON.parse(unitIds); } catch (e) { unitIds = []; }
|
||||
}
|
||||
if (Array.isArray(unitIds) && unitIds.length > 0) {
|
||||
const allowedUnits = await models.Unit.findAll({
|
||||
where: { id: unitIds },
|
||||
attributes: ['id', 'name', 'code', 'symbol', 'status'],
|
||||
transaction
|
||||
});
|
||||
record.setDataValue('allowedUnits', allowedUnits);
|
||||
} else {
|
||||
record.setDataValue('allowedUnits', []);
|
||||
}
|
||||
record.setDataValue('allowedBrands', allowedBrands);
|
||||
record.setDataValue('allowedUnits', allowedUnits);
|
||||
|
||||
return record;
|
||||
}
|
||||
@@ -135,10 +118,8 @@ export class CatalogService {
|
||||
// Find all records
|
||||
const records = await repository.findAll({ where }, context);
|
||||
|
||||
// Attach counts
|
||||
for (const record of records) {
|
||||
await this.attachCounts(record);
|
||||
}
|
||||
// Attach counts concurrently using Promise.all
|
||||
await Promise.all(records.map(record => this.attachCounts(record)));
|
||||
|
||||
return records;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
// 1. Alter attributes table
|
||||
const attributesTable = await queryInterface.describeTable('attributes').catch(() => null);
|
||||
if (attributesTable) {
|
||||
if (attributesTable.created_by) {
|
||||
await queryInterface.removeColumn('attributes', 'created_by');
|
||||
}
|
||||
if (attributesTable.updated_by) {
|
||||
await queryInterface.removeColumn('attributes', 'updated_by');
|
||||
}
|
||||
if (attributesTable.deleted_by) {
|
||||
await queryInterface.removeColumn('attributes', 'deleted_by');
|
||||
}
|
||||
|
||||
await queryInterface.addColumn('attributes', 'created_by', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true
|
||||
});
|
||||
await queryInterface.addColumn('attributes', 'updated_by', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true
|
||||
});
|
||||
await queryInterface.addColumn('attributes', 'deleted_by', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
|
||||
// 2. Alter history tables
|
||||
const historyTables = ['attribute_history', 'attribute_set_history', 'attribute_group_history'];
|
||||
for (const table of historyTables) {
|
||||
const tableInfo = await queryInterface.describeTable(table).catch(() => null);
|
||||
if (tableInfo) {
|
||||
if (tableInfo.changed_by) {
|
||||
await queryInterface.removeColumn(table, 'changed_by');
|
||||
}
|
||||
await queryInterface.addColumn(table, 'changed_by', {
|
||||
type: Sequelize.INTEGER,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
// Revert attributes table
|
||||
const attributesTable = await queryInterface.describeTable('attributes').catch(() => null);
|
||||
if (attributesTable) {
|
||||
if (attributesTable.created_by) {
|
||||
await queryInterface.removeColumn('attributes', 'created_by');
|
||||
}
|
||||
if (attributesTable.updated_by) {
|
||||
await queryInterface.removeColumn('attributes', 'updated_by');
|
||||
}
|
||||
if (attributesTable.deleted_by) {
|
||||
await queryInterface.removeColumn('attributes', 'deleted_by');
|
||||
}
|
||||
|
||||
await queryInterface.addColumn('attributes', 'created_by', {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true
|
||||
});
|
||||
await queryInterface.addColumn('attributes', 'updated_by', {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true
|
||||
});
|
||||
await queryInterface.addColumn('attributes', 'deleted_by', {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
|
||||
// Revert history tables
|
||||
const historyTables = ['attribute_history', 'attribute_set_history', 'attribute_group_history'];
|
||||
for (const table of historyTables) {
|
||||
const tableInfo = await queryInterface.describeTable(table).catch(() => null);
|
||||
if (tableInfo) {
|
||||
if (tableInfo.changed_by) {
|
||||
await queryInterface.removeColumn(table, 'changed_by');
|
||||
}
|
||||
await queryInterface.addColumn(table, 'changed_by', {
|
||||
type: Sequelize.UUID,
|
||||
allowNull: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Reference in New Issue
Block a user