feat(local): add tenant starter catalog seeder
This commit is contained in:
+3
-2
@@ -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
|
||||
}
|
||||
}
|
||||
Executable
+110
@@ -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);
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user