From 00cf7aed76b9ba413a6f71139bd142b47c3a9883 Mon Sep 17 00:00:00 2001 From: Mahir-Mohamed Date: Tue, 8 Sep 2026 14:23:58 +0530 Subject: [PATCH] feat: improve product catalog backend configuration --- .../attributeGroup.repository.js | 4 +- .../attributeSets/attributeSet.repository.js | 4 +- .../attributes/attribute.repository.js | 1 + .../attributes/attribute.service.js | 2 +- .../authentication/access/role.repository.js | 1 + .../authentication/users/user.repository.js | 1 + .../brands/brands/brand.repository.js | 1 + src/features/brands/units/unit.repository.js | 1 + .../catalogs/catalogs/catalog.controller.js | 1 + .../catalogs/catalogs/catalog.repository.js | 4 +- .../catalogs/catalogs/catalog.service.js | 31 +++++++-- .../catalogs/catalogs/catalog.validation.js | 24 ++++++- .../channelTypes/channelType.repository.js | 6 +- .../channels/channels/channel.repository.js | 6 +- .../integrations/workers/outbox.worker.js | 17 ++++- .../assetFamilies/assetFamily.repository.js | 6 +- .../media/assetTypes/assetType.repository.js | 6 +- .../products/products/product.repository.js | 66 +------------------ .../variants/variants/variant.repository.js | 2 +- src/features/workflows/workflow.repository.js | 6 +- src/shared/config/database.config.cjs | 12 ++++ 21 files changed, 116 insertions(+), 86 deletions(-) diff --git a/src/features/attributes/attributeGroups/attributeGroup.repository.js b/src/features/attributes/attributeGroups/attributeGroup.repository.js index 0b99816..c4076fd 100644 --- a/src/features/attributes/attributeGroups/attributeGroup.repository.js +++ b/src/features/attributes/attributeGroups/attributeGroup.repository.js @@ -14,8 +14,8 @@ export class AttributeGroupRepository { through: { attributes: ['display_order'] } } ], - order: [ - ['name', 'ASC'] + order: options.order || [ + ['created_at', 'DESC'] ] }); } diff --git a/src/features/attributes/attributeSets/attributeSet.repository.js b/src/features/attributes/attributeSets/attributeSet.repository.js index c01b006..fe89973 100644 --- a/src/features/attributes/attributeSets/attributeSet.repository.js +++ b/src/features/attributes/attributeSets/attributeSet.repository.js @@ -28,8 +28,8 @@ export class AttributeSetRepository { ] } ], - order: [ - ['name', 'ASC'] + order: options.order || [ + ['created_at', 'DESC'] ] }); } diff --git a/src/features/attributes/attributes/attribute.repository.js b/src/features/attributes/attributes/attribute.repository.js index 8400844..4e623d7 100644 --- a/src/features/attributes/attributes/attribute.repository.js +++ b/src/features/attributes/attributes/attribute.repository.js @@ -4,6 +4,7 @@ import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AttributeRepository { async findAll(options = {}, context = {}) { const queryOptions = { + order: [['created_at', 'DESC']], ...options, where: applyTenantScope(options.where || {}, context) }; diff --git a/src/features/attributes/attributes/attribute.service.js b/src/features/attributes/attributes/attribute.service.js index 1f07650..14d0618 100644 --- a/src/features/attributes/attributes/attribute.service.js +++ b/src/features/attributes/attributes/attribute.service.js @@ -54,7 +54,7 @@ export class AttributeService { } // Sorting - let order = [['display_order', 'ASC']]; + let order = [['created_at', 'DESC']]; if (query.sortBy) { const direction = query.sortDir?.toUpperCase() === 'DESC' ? 'DESC' : 'ASC'; if (query.sortBy === 'name') order = [['name', direction]]; diff --git a/src/features/authentication/access/role.repository.js b/src/features/authentication/access/role.repository.js index b3b6f9c..7725f77 100644 --- a/src/features/authentication/access/role.repository.js +++ b/src/features/authentication/access/role.repository.js @@ -15,6 +15,7 @@ export class RoleRepository { }; return await models.Role.findAll({ + order: [['created_at', 'DESC']], include: [ { model: models.PermissionNode, diff --git a/src/features/authentication/users/user.repository.js b/src/features/authentication/users/user.repository.js index 76fd526..9caaaeb 100644 --- a/src/features/authentication/users/user.repository.js +++ b/src/features/authentication/users/user.repository.js @@ -8,6 +8,7 @@ export class UserRepository { ? { tenant_id: context.tenantId } : {}; return await models.User.findAll({ + order: [['created_at', 'DESC']], attributes: { exclude: ['password_hash'] }, include: [ { diff --git a/src/features/brands/brands/brand.repository.js b/src/features/brands/brands/brand.repository.js index 3c918bc..c8080c0 100644 --- a/src/features/brands/brands/brand.repository.js +++ b/src/features/brands/brands/brand.repository.js @@ -4,6 +4,7 @@ import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class BrandRepository { async findAll(options = {}, context = {}) { const queryOptions = { + order: [['created_at', 'DESC']], ...options, where: applyTenantScope(options.where || {}, context) }; diff --git a/src/features/brands/units/unit.repository.js b/src/features/brands/units/unit.repository.js index 785f879..783e505 100644 --- a/src/features/brands/units/unit.repository.js +++ b/src/features/brands/units/unit.repository.js @@ -4,6 +4,7 @@ import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class UnitRepository { async findAll(options = {}, context = {}) { const queryOptions = { + order: [['created_at', 'DESC']], ...options, where: applyTenantScope(options.where || {}, context) }; diff --git a/src/features/catalogs/catalogs/catalog.controller.js b/src/features/catalogs/catalogs/catalog.controller.js index 81f7b6f..250c216 100644 --- a/src/features/catalogs/catalogs/catalog.controller.js +++ b/src/features/catalogs/catalogs/catalog.controller.js @@ -18,6 +18,7 @@ export class CatalogController { : (raw.category_id || ''), attributeSetId: raw.attribute_set_id || (raw.attributeSet ? raw.attributeSet.id : null) || '', workflowCode: raw.workflow_code || 'standard', + productType: raw.completeness_rules?.productType || raw.completenessRules?.productType || raw.productType || raw.product_type || null, completenessRules: raw.completeness_rules || {}, allowedBrands: raw.allowedBrands || [], allowedUnits: raw.allowedUnits || [], diff --git a/src/features/catalogs/catalogs/catalog.repository.js b/src/features/catalogs/catalogs/catalog.repository.js index 94b3182..71f1f11 100644 --- a/src/features/catalogs/catalogs/catalog.repository.js +++ b/src/features/catalogs/catalogs/catalog.repository.js @@ -57,8 +57,8 @@ export class CatalogRepository { ] } ], - order: [ - ['name', 'ASC'] + order: options.order || [ + ['created_at', 'DESC'] ], ...queryOptions }); diff --git a/src/features/catalogs/catalogs/catalog.service.js b/src/features/catalogs/catalogs/catalog.service.js index ea28ca5..b6fb6fa 100644 --- a/src/features/catalogs/catalogs/catalog.service.js +++ b/src/features/catalogs/catalogs/catalog.service.js @@ -243,11 +243,15 @@ export class CatalogService { const completenessRules = data.completenessRules || data.completeness_rules || {}; completenessRules.allowedBrands = data.allowedBrands || data.allowed_brands || []; completenessRules.allowedUnits = data.allowedUnits || data.allowed_units || []; + const incomingProductType = data.productType || data.product_type || data.type; + if (incomingProductType) { + completenessRules.productType = incomingProductType; + } let totalWeight = 0; let hasRules = false; for (const [key, val] of Object.entries(completenessRules)) { - if (key === 'allowedBrands' || key === 'allowedUnits') continue; + if (key === 'allowedBrands' || key === 'allowedUnits' || key === 'productType') continue; const weight = Number(val); if (isNaN(weight)) { throw new Error(`Completeness rule weight for "${key}" must be a number`); @@ -515,11 +519,17 @@ export class CatalogService { if (data.hasOwnProperty('allowedUnits') || data.hasOwnProperty('allowed_units')) { completenessRules.allowedUnits = data.allowedUnits || data.allowed_units || []; } + if (data.hasOwnProperty('productType') || data.hasOwnProperty('product_type') || data.hasOwnProperty('type')) { + const pType = data.productType || data.product_type || data.type; + if (pType) { + completenessRules.productType = pType; + } + } let totalWeight = 0; let hasRules = false; for (const [key, val] of Object.entries(completenessRules)) { - if (key === 'allowedBrands' || key === 'allowedUnits') continue; + if (key === 'allowedBrands' || key === 'allowedUnits' || key === 'productType') continue; const weight = Number(val); if (isNaN(weight)) { throw new Error(`Completeness rule weight for "${key}" must be a number`); @@ -748,6 +758,7 @@ export class CatalogService { if (!family) throw new Error('Product Family not found'); let groups = []; + let attributeSetObj = null; if (family.attribute_set_id) { const setRecord = await models.AttributeSet.findByPk(family.attribute_set_id, { include: [ @@ -770,8 +781,13 @@ export class CatalogService { } ] }); - if (setRecord && setRecord.groups) { - groups = setRecord.groups; + if (setRecord) { + const setObj = setRecord.toJSON + ? setRecord.toJSON() + : JSON.parse(JSON.stringify(setRecord)); + + attributeSetObj = setObj; + groups = setObj.groups || []; } } @@ -826,7 +842,9 @@ export class CatalogService { name: family.name, description: family.description, category: family.category, - attributeSet: family.attributeSet, + attributeSet: attributeSetObj || family.attributeSet || null, + attribute_set_id: family.attribute_set_id || null, + attributeSetId: family.attribute_set_id || null, groups, attributes: family.attributes || [], variantAxes: family.variantAxes || [], @@ -837,7 +855,8 @@ export class CatalogService { workflow: workflow, allowedBrands: completenessRules.allowedBrands || [], allowedUnits: completenessRules.allowedUnits || [], - completenessRules: completenessRules + completenessRules: completenessRules, + productType: completenessRules.productType || null }; } diff --git a/src/features/catalogs/catalogs/catalog.validation.js b/src/features/catalogs/catalogs/catalog.validation.js index cfc0788..b3ad843 100644 --- a/src/features/catalogs/catalogs/catalog.validation.js +++ b/src/features/catalogs/catalogs/catalog.validation.js @@ -48,7 +48,17 @@ export const createValidation = [ .isString(), body('categoryId') .optional({ nullable: true }) - .isString() + .isString(), + body('productType') + .optional({ nullable: true }) + .isIn(['simple', 'variant']) + .withMessage('Product type must be either simple or variant'), + body('product_type') + .optional({ nullable: true }) + .isIn(['simple', 'variant']), + body('type') + .optional({ nullable: true }) + .isIn(['simple', 'variant']) ]; export const updateValidation = [ @@ -103,7 +113,17 @@ export const updateValidation = [ .isString(), body('categoryId') .optional({ nullable: true }) - .isString() + .isString(), + body('productType') + .optional({ nullable: true }) + .isIn(['simple', 'variant']) + .withMessage('Product type must be either simple or variant'), + body('product_type') + .optional({ nullable: true }) + .isIn(['simple', 'variant']), + body('type') + .optional({ nullable: true }) + .isIn(['simple', 'variant']) ]; export const deleteValidation = [ diff --git a/src/features/channels/channelTypes/channelType.repository.js b/src/features/channels/channelTypes/channelType.repository.js index f073029..05b732b 100644 --- a/src/features/channels/channelTypes/channelType.repository.js +++ b/src/features/channels/channelTypes/channelType.repository.js @@ -4,7 +4,11 @@ import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class ChannelTypeRepository { async findAll(options = {}, context = {}) { const where = applyTenantScope(options.where || {}, context); - return await models.ChannelType.findAll({ ...options, where }); + return await models.ChannelType.findAll({ + order: [['created_at', 'DESC']], + ...options, + where + }); } async findById(id, options = {}, context = {}) { diff --git a/src/features/channels/channels/channel.repository.js b/src/features/channels/channels/channel.repository.js index 10fd789..5a2e1f2 100644 --- a/src/features/channels/channels/channel.repository.js +++ b/src/features/channels/channels/channel.repository.js @@ -4,7 +4,11 @@ import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class ChannelRepository { async findAll(options = {}, context = {}) { const where = applyTenantScope(options.where || {}, context); - return await models.Channel.findAll({ ...options, where }); + return await models.Channel.findAll({ + order: [['created_at', 'DESC']], + ...options, + where + }); } async findById(id, options = {}, context = {}) { diff --git a/src/features/integrations/workers/outbox.worker.js b/src/features/integrations/workers/outbox.worker.js index 40389fb..53ca1fc 100644 --- a/src/features/integrations/workers/outbox.worker.js +++ b/src/features/integrations/workers/outbox.worker.js @@ -79,6 +79,21 @@ export const processOutboxEvents = async () => { } }; +let isProcessing = false; + +export const guardedProcessOutboxEvents = async () => { + if (isProcessing) { + return; + } + + isProcessing = true; + try { + await processOutboxEvents(); + } finally { + isProcessing = false; + } +}; + export const startOutboxWorker = (intervalMs = 10000) => { - setInterval(processOutboxEvents, intervalMs); + setInterval(guardedProcessOutboxEvents, intervalMs); }; diff --git a/src/features/media/assetFamilies/assetFamily.repository.js b/src/features/media/assetFamilies/assetFamily.repository.js index 849848d..6c8219b 100644 --- a/src/features/media/assetFamilies/assetFamily.repository.js +++ b/src/features/media/assetFamilies/assetFamily.repository.js @@ -14,7 +14,11 @@ export class AssetFamilyRepository { async findAll(options = {}, context = {}) { const where = applyTenantScope(options.where || {}, context); - const queryOptions = { ...options, where }; + const queryOptions = { + order: [['created_at', 'DESC']], + ...options, + where + }; try { return await models.AssetFamily.findAll(queryOptions); } catch (err) { diff --git a/src/features/media/assetTypes/assetType.repository.js b/src/features/media/assetTypes/assetType.repository.js index 87836e0..fd27b7f 100644 --- a/src/features/media/assetTypes/assetType.repository.js +++ b/src/features/media/assetTypes/assetType.repository.js @@ -4,7 +4,11 @@ import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class AssetTypeRepository { async findAll(options = {}, context = {}) { const where = applyTenantScope(options.where || {}, context); - return await models.AssetType.findAll({ ...options, where }); + return await models.AssetType.findAll({ + order: [['created_at', 'DESC']], + ...options, + where + }); } async findById(id, options = {}, context = {}) { diff --git a/src/features/products/products/product.repository.js b/src/features/products/products/product.repository.js index 491a130..e01bda6 100644 --- a/src/features/products/products/product.repository.js +++ b/src/features/products/products/product.repository.js @@ -4,6 +4,7 @@ import { applyTenantScope } from '../../../utils/helpers/common.helper.js'; export class ProductRepository { async findAll(options = {}, context = {}) { const queryOptions = { + order: [['created_at', 'DESC']], ...options, where: applyTenantScope(options.where || {}, context) }; @@ -62,79 +63,16 @@ export class ProductRepository { as: 'family', required: false, include: [ - { - model: models.Attribute, - as: 'attributes', - through: { attributes: [] }, - required: false, - include: [ - { - model: models.AttributeOption, - as: 'optionsList', - attributes: ['id', 'code', 'label', 'sort_order', 'status'], - required: false - } - ] - }, { model: models.Attribute, as: 'variantAxes', through: { attributes: [] }, - required: false, - include: [ - { - model: models.AttributeOption, - as: 'optionsList', - attributes: ['id', 'code', 'label', 'sort_order', 'status'], - required: false - } - ] - }, - { - model: models.AssetFamily, - as: 'assetRequirements', - through: { attributes: [] }, - required: false - }, - { - model: models.FamilyChannel, - as: 'channels', required: false }, { model: models.Categorie, as: 'category', - attributes: ['id', 'name', 'code'], - required: false - }, - { - model: models.AttributeSet, - as: 'attributeSet', - required: false, - include: [ - { - model: models.AttributeGroup, - as: 'groups', - through: { attributes: [] }, - required: false, - include: [ - { - model: models.Attribute, - as: 'attributes', - through: { attributes: [] }, - required: false, - include: [ - { - model: models.AttributeOption, - as: 'optionsList', - attributes: ['id', 'code', 'label', 'sort_order', 'status'], - required: false - } - ] - } - ] - } - ] + attributes: ['id', 'name'] } ] }, diff --git a/src/features/variants/variants/variant.repository.js b/src/features/variants/variants/variant.repository.js index 28aac3e..6d9c6f9 100644 --- a/src/features/variants/variants/variant.repository.js +++ b/src/features/variants/variants/variant.repository.js @@ -44,7 +44,7 @@ export class VariantRepository { async findAll(options = {}, context = {}) { const where = applyTenantScope(options.where || {}, context); return await models.Variant.findAll({ - order: [['sku', 'ASC']], + order: options.order || [['created_at', 'DESC']], include: options.include || getDefaultVariantIncludes(), ...options, where diff --git a/src/features/workflows/workflow.repository.js b/src/features/workflows/workflow.repository.js index 799c24c..dc52dbf 100644 --- a/src/features/workflows/workflow.repository.js +++ b/src/features/workflows/workflow.repository.js @@ -4,7 +4,11 @@ import { applyTenantScope } from '../../utils/helpers/common.helper.js'; export class WorkflowRepository { async findAll(options = {}, context = {}) { const where = applyTenantScope(options.where || {}, context); - return await models.WorkflowRegistry.findAll({ ...options, where }); + return await models.WorkflowRegistry.findAll({ + order: [['created_at', 'DESC']], + ...options, + where + }); } async findById(id, options = {}, context = {}) { diff --git a/src/shared/config/database.config.cjs b/src/shared/config/database.config.cjs index 2a5266f..0dc7fac 100644 --- a/src/shared/config/database.config.cjs +++ b/src/shared/config/database.config.cjs @@ -20,6 +20,12 @@ module.exports = { port: process.env.DB_PORT || 5432, dialect: process.env.DB_DIALECT || "postgres", logging: false, + pool: { + max: 15, + min: 2, + acquire: 30000, + idle: 10000, + }, }, development: { @@ -30,6 +36,12 @@ module.exports = { port: process.env.DB_PORT || 5432, dialect: process.env.DB_DIALECT || "postgres", logging: false, + pool: { + max: 15, + min: 2, + acquire: 30000, + idle: 10000, + }, }, test: {