feat(local): add tenant starter catalog seeder

This commit is contained in:
Inamul-hasan-tec
2026-09-08 16:26:41 +05:30
parent ad416d631a
commit 6b6252db61
3 changed files with 391 additions and 2 deletions
+3 -2
View File
@@ -30,7 +30,8 @@
"db:seed:undo:dev": "cross-env NODE_ENV=development sequelize-cli db:seed:undo:all --config src/shared/config/database.config.cjs --seeders-path src/seeders --models-path src/shared/database",
"db:migrate:test": "cross-env NODE_ENV=test sequelize-cli db:migrate --config src/shared/config/database.config.cjs --migrations-path src/migrations --models-path src/shared/database",
"db:seed:test": "cross-env NODE_ENV=test sequelize-cli db:seed:all --config src/shared/config/database.config.cjs --seeders-path src/seeders --models-path src/shared/database",
"db:seed:undo:test": "cross-env NODE_ENV=test sequelize-cli db:seed:undo:all --config src/shared/config/database.config.cjs --seeders-path src/seeders --models-path src/shared/database"
"db:seed:undo:test": "cross-env NODE_ENV=test sequelize-cli db:seed:undo:all --config src/shared/config/database.config.cjs --seeders-path src/seeders --models-path src/shared/database",
"seed:tenant": "node scripts/seed-tenant-starter.js"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.1113.0",
@@ -68,4 +69,4 @@
"node": ">=18.0.0"
},
"private": true
}
}
+110
View File
@@ -0,0 +1,110 @@
#!/usr/bin/env node
import dotenv from 'dotenv';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
dotenv.config({ path: path.resolve(__dirname, '../.env.development') });
dotenv.config({ path: path.resolve(__dirname, '../.env.local') });
dotenv.config({ path: path.resolve(__dirname, '../.env') });
const { models, initializeDatabaseModels, sequelize } = await import('../src/shared/database/models.js');
initializeDatabaseModels();
const { seedTenantStarterData } = await import('../src/seeders/tenantStarterData.service.js');
async function run() {
const args = process.argv.slice(2);
let targetTenantId = null;
let seedAll = false;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--all') {
seedAll = true;
} else if (args[i] === '--tenant-id' && args[i + 1]) {
targetTenantId = parseInt(args[i + 1], 10);
i++;
} else if (args[i] === '--canonical-id' && args[i + 1]) {
const canonical = args[i + 1].trim().toLowerCase();
const tenant = await models.Tenant.findOne({ where: { canonical_tenant_id: canonical } });
if (!tenant) {
console.error(`Tenant with canonical ID "${canonical}" not found.`);
process.exit(1);
}
targetTenantId = tenant.id;
console.log(`Resolved canonical ID "${canonical}" -> Tenant ID: ${targetTenantId}`);
i++;
} else if (args[i] === '--email' && args[i + 1]) {
const email = args[i + 1].trim().toLowerCase();
const user = await models.User.findOne({ where: { email } });
if (user && user.tenant_id) {
targetTenantId = user.tenant_id;
console.log(`Resolved user "${email}" -> Tenant ID: ${targetTenantId}`);
} else {
const tenant = await models.Tenant.findOne({ where: { contact_email: email } });
if (tenant) {
targetTenantId = tenant.id;
console.log(`Resolved contact email "${email}" -> Tenant ID: ${targetTenantId}`);
} else {
console.error(`Tenant or User with email "${email}" not found.`);
process.exit(1);
}
}
i++;
}
}
let tenantsToSeed = [];
if (targetTenantId) {
const t = await models.Tenant.findByPk(targetTenantId);
if (!t) {
console.error(`Tenant ID ${targetTenantId} not found.`);
process.exit(1);
}
tenantsToSeed = [t];
} else {
// Default to all active tenants
tenantsToSeed = await models.Tenant.findAll({ where: { status: true } });
if (tenantsToSeed.length === 0) {
console.error('No active tenants found in PIM.');
process.exit(1);
}
}
console.log(`\n======================================================`);
console.log(`🌱 PIM TENANT STARTER PACK SEEDER`);
console.log(`Targeting ${tenantsToSeed.length} tenant(s)...`);
console.log(`======================================================\n`);
let successCount = 0;
for (const tenant of tenantsToSeed) {
const tenantLabel = tenant.tenant_name || tenant.tenant_code || `Tenant #${tenant.id}`;
console.log(`▶ Seeding Tenant: "${tenantLabel}" (ID: ${tenant.id}, Canonical: ${tenant.canonical_tenant_id || 'N/A'})...`);
const tx = await sequelize.transaction();
try {
const result = await seedTenantStarterData(tenant.id, { transaction: tx });
await tx.commit();
console.log(` ✅ Units (${result.units.length}): ${result.units.map(u => u.name).join(', ')}`);
console.log(` ✅ Brands (${result.brands.length}): ${result.brands.map(b => b.name).join(', ')}`);
console.log(` ✅ Categories (${result.categories.length}): ${result.categories.map(c => c.name).join(', ')}`);
console.log(` ✅ Families (${result.families.length}): ${result.families.map(f => f.name).join(', ')}`);
console.log(` ✅ Channels (${result.channels.length}): ${result.channels.map(ch => ch.name).join(', ')}`);
console.log(` ✅ Products (${result.products.length}): ${result.products.map(p => p.name).join(', ')}`);
console.log(` 🎉 Successfully seeded starter pack for Tenant ${tenant.id}!\n`);
successCount++;
} catch (err) {
await tx.rollback();
console.error(` ❌ Failed seeding Tenant ${tenant.id}:`, err.message);
}
}
console.log(`======================================================`);
console.log(`✨ Completed: ${successCount}/${tenantsToSeed.length} tenants seeded successfully.`);
console.log(`======================================================\n`);
process.exit(0);
}
run().catch(err => {
console.error('Seeder execution error:', err);
process.exit(1);
});
+278
View File
@@ -0,0 +1,278 @@
import { models } from '../shared/database/models.js';
import { generateUniqueCode } from '../utils/helpers/code.utils.js';
export async function seedTenantStarterData(tenantId, { transaction = null } = {}) {
const tId = Number(tenantId);
if (!tId) {
throw new Error('Valid numeric tenantId is required to seed starter data');
}
const results = {
units: [],
brands: [],
categories: [],
families: [],
channels: [],
products: []
};
// 1. Units
const unitsData = [
{ name: 'Pieces', code: 'pcs', symbol: 'pcs', unitType: 'quantity', description: 'Standard piece count' },
{ name: 'Kilograms', code: 'kg', symbol: 'kg', unitType: 'weight', description: 'Standard metric weight' },
{ name: 'Boxes', code: 'box', symbol: 'box', unitType: 'packaging', description: 'Packaged carton or box' },
{ name: 'Meters', code: 'm', symbol: 'm', unitType: 'length', description: 'Linear length measurement' }
];
for (const u of unitsData) {
let unit = await models.Unit.findOne({
where: { tenant_id: tId, name: u.name },
transaction
});
if (!unit) {
const code = await generateUniqueCode(models.Unit, u.code, 'code', transaction);
unit = await models.Unit.create({
tenant_id: tId,
name: u.name,
code,
symbol: u.symbol,
unitType: u.unitType,
description: u.description,
status: 'active'
}, { transaction });
}
results.units.push(unit);
}
const defaultUnit = results.units[0];
// 2. Brands
const brandsData = [
{ name: 'Maskan Tech', code: 'maskan', description: 'Maskan Enterprise Technology & Hardware' },
{ name: 'Apex Essentials', code: 'apex', description: 'Premium computing peripherals and lifestyle accessories' },
{ name: 'Nova Dynamics', code: 'nova', description: 'Smart connected audio and workspace hardware' }
];
for (const b of brandsData) {
let brand = await models.Brand.findOne({
where: { tenant_id: tId, name: b.name },
transaction
});
if (!brand) {
const code = await generateUniqueCode(models.Brand, b.code, 'code', transaction);
brand = await models.Brand.create({
tenant_id: tId,
name: b.name,
code,
description: b.description,
status: 'active'
}, { transaction });
}
results.brands.push(brand);
}
// 3. Categories
let electronicsCat = await models.Categorie.findOne({
where: { tenant_id: tId, name: 'Electronics' },
transaction
});
if (!electronicsCat) {
const code = await generateUniqueCode(models.Categorie, 'electronics', 'code', transaction);
electronicsCat = await models.Categorie.create({
tenant_id: tId,
name: 'Electronics',
code,
path: '/electronics',
level: 0,
status: 'active'
}, { transaction });
}
results.categories.push(electronicsCat);
let compAccCat = await models.Categorie.findOne({
where: { tenant_id: tId, name: 'Computer Accessories' },
transaction
});
if (!compAccCat) {
const code = await generateUniqueCode(models.Categorie, 'comp_accessories', 'code', transaction);
compAccCat = await models.Categorie.create({
tenant_id: tId,
parent_id: electronicsCat.id,
name: 'Computer Accessories',
code,
path: '/electronics/comp_accessories',
level: 1,
status: 'active'
}, { transaction });
}
results.categories.push(compAccCat);
let audioCat = await models.Categorie.findOne({
where: { tenant_id: tId, name: 'Audio & Sound' },
transaction
});
if (!audioCat) {
const code = await generateUniqueCode(models.Categorie, 'audio_sound', 'code', transaction);
audioCat = await models.Categorie.create({
tenant_id: tId,
parent_id: electronicsCat.id,
name: 'Audio & Sound',
code,
path: '/electronics/audio_sound',
level: 1,
status: 'active'
}, { transaction });
}
results.categories.push(audioCat);
let officeCat = await models.Categorie.findOne({
where: { tenant_id: tId, name: 'Office & Workspace' },
transaction
});
if (!officeCat) {
const code = await generateUniqueCode(models.Categorie, 'office_workspace', 'code', transaction);
officeCat = await models.Categorie.create({
tenant_id: tId,
name: 'Office & Workspace',
code,
path: '/office_workspace',
level: 0,
status: 'active'
}, { transaction });
}
results.categories.push(officeCat);
// 4. Product Families (Catalogs)
const familiesData = [
{ name: 'Computer Peripherals', code: 'computer_peripherals', category_id: compAccCat.id, description: 'Mice, keyboards, docks, and input devices' },
{ name: 'Consumer Audio', code: 'consumer_audio', category_id: audioCat.id, description: 'Headphones, earphones, and audio hardware' },
{ name: 'Workspace Ergonomics', code: 'workspace_ergonomics', category_id: officeCat.id, description: 'Laptop stands, mounts, and desk accessories' }
];
for (const f of familiesData) {
let family = await models.Catalog.findOne({
where: { tenant_id: tId, name: f.name },
transaction
});
if (!family) {
const code = await generateUniqueCode(models.Catalog, f.code, 'code', transaction);
family = await models.Catalog.create({
tenant_id: tId,
name: f.name,
code,
category_id: f.category_id,
description: f.description,
status: 'active'
}, { transaction });
}
results.families.push(family);
}
// 5. Channels
const ecomType = await models.ChannelType.findOne({ where: { code: 'ecommerce' }, transaction });
const mktType = await models.ChannelType.findOne({ where: { code: 'marketplace' }, transaction });
const erpType = await models.ChannelType.findOne({ where: { code: 'erp' }, transaction });
const channelsData = [
{ name: 'Shopify Online Store', code: 'shopify_online_store', type_id: ecomType?.id, description: 'Official online direct-to-consumer store' },
{ name: 'Amazon Marketplace', code: 'amazon_marketplace', type_id: mktType?.id, description: 'Amazon B2C marketplace channel' },
{ name: 'Maskan ERP & POS', code: 'maskan_erp_pos', type_id: erpType?.id, description: 'Central Inventory ERP & Store Point of Sale' }
];
for (const ch of channelsData) {
let channel = await models.Channel.findOne({
where: { tenant_id: tId, name: ch.name },
transaction
});
if (!channel) {
const code = await generateUniqueCode(models.Channel, ch.code, 'code', transaction);
channel = await models.Channel.create({
tenant_id: tId,
name: ch.name,
code,
type_id: ch.type_id,
description: ch.description,
status: 'active',
metadata: { allowPublishing: true }
}, { transaction });
}
results.channels.push(channel);
}
// 6. Products
const sampleProducts = [
{
name: 'Wireless Ergonomic Mouse',
code: 'MSK-WEM-001',
description: 'Precision ergonomic wireless mouse with multi-device Bluetooth and 2.4GHz connectivity.',
category_id: compAccCat.id,
brand_id: results.brands[0].id,
unit_id: defaultUnit.id,
family_id: results.families[0].id
},
{
name: 'RGB Mechanical Keyboard',
code: 'MSK-RMK-002',
description: 'Custom hot-swappable mechanical keyboard with per-key RGB backlighting and aluminum body.',
category_id: compAccCat.id,
brand_id: results.brands[0].id,
unit_id: defaultUnit.id,
family_id: results.families[0].id
},
{
name: 'Ultra-Slim USB-C Multiport Hub',
code: 'MSK-UCH-003',
description: '7-in-1 USB-C Hub with 4K HDMI, 100W Power Delivery, Gigabit Ethernet, and SD card slots.',
category_id: compAccCat.id,
brand_id: results.brands[1].id,
unit_id: defaultUnit.id,
family_id: results.families[0].id
},
{
name: 'Active Noise Cancelling Headphones',
code: 'MSK-ANC-004',
description: 'Over-ear wireless headphones with hybrid active noise cancellation and 40-hour battery life.',
category_id: audioCat.id,
brand_id: results.brands[2].id,
unit_id: defaultUnit.id,
family_id: results.families[1].id
},
{
name: 'Adjustable Aluminum Laptop Stand',
code: 'MSK-ALS-005',
description: 'Ergonomic multi-angle foldable aluminum laptop riser with heat dissipation pads.',
category_id: officeCat.id,
brand_id: results.brands[1].id,
unit_id: defaultUnit.id,
family_id: results.families[2].id
}
];
for (const p of sampleProducts) {
let product = await models.Product.findOne({
where: { tenant_id: tId, name: p.name },
transaction
});
if (!product) {
const code = await generateUniqueCode(models.Product, p.code, 'code', transaction);
const slug = code.toLowerCase().replace(/[^a-z0-9]+/g, '-');
product = await models.Product.create({
tenant_id: tId,
name: p.name,
code,
slug,
description: p.description,
short_description: p.description.slice(0, 100),
status: 'active',
is_active: true,
category_id: p.category_id,
brand_id: p.brand_id,
unit_id: p.unit_id,
family_id: p.family_id
}, { transaction });
}
results.products.push(product);
}
return results;
}