genesis
This commit is contained in:
266
category-syncer.js
Normal file
266
category-syncer.js
Normal file
@@ -0,0 +1,266 @@
|
||||
import { EventEmitter } from 'events';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { createConnection } from './database.js';
|
||||
|
||||
class CategorySyncer extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
if (CategorySyncer.instance) {
|
||||
return CategorySyncer.instance;
|
||||
}
|
||||
|
||||
this.isSyncing = false;
|
||||
this.queuedSync = false;
|
||||
this.cacheDir = process.env.CACHE_LOCATION || '.';
|
||||
this.lastTreeString = null;
|
||||
|
||||
CategorySyncer.instance = this;
|
||||
}
|
||||
|
||||
async triggerSync() {
|
||||
if (this.isSyncing) {
|
||||
if (this.queuedSync) {
|
||||
console.log('🚫 Sync already in progress and next sync already queued. Ignoring.');
|
||||
return;
|
||||
}
|
||||
console.log('⏳ Sync already in progress. Queuing next sync.');
|
||||
this.queuedSync = true;
|
||||
return;
|
||||
}
|
||||
|
||||
await this._doSync();
|
||||
}
|
||||
|
||||
async _doSync() {
|
||||
this.isSyncing = true;
|
||||
const startTime = Date.now();
|
||||
console.log('🚀 Starting sync...');
|
||||
|
||||
try {
|
||||
await this._syncFromDb();
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ Sync completed successfully in ${duration}ms.`);
|
||||
} catch (err) {
|
||||
console.error('❌ Sync failed:', err);
|
||||
} finally {
|
||||
this.isSyncing = false;
|
||||
if (this.queuedSync) {
|
||||
console.log('🔄 Processing queued sync...');
|
||||
this.queuedSync = false;
|
||||
// Use setImmediate to allow stack to clear/event loop to tick
|
||||
setImmediate(() => this.triggerSync());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async _syncFromDb() {
|
||||
let pool;
|
||||
try {
|
||||
pool = await createConnection();
|
||||
|
||||
// Fetch categories
|
||||
const categoriesResult = await pool.request().query(`
|
||||
SELECT kKategorie, kOberKategorie, nSort
|
||||
FROM tkategorie
|
||||
`);
|
||||
|
||||
// Fetch names
|
||||
const namesResult = await pool.request().query(`
|
||||
SELECT kKategorie, cName
|
||||
FROM tKategorieSprache
|
||||
WHERE kSprache = ${process.env.JTL_SPRACHE_ID} AND kShop = ${process.env.JTL_SHOP_ID}
|
||||
`);
|
||||
|
||||
// Fetch article counts
|
||||
const articleCountsResult = await pool.request().query(`
|
||||
SELECT ka.kKategorie, COUNT(a.kArtikel) as count
|
||||
FROM tkategorieartikel ka
|
||||
JOIN tArtikel a ON ka.kArtikel = a.kArtikel
|
||||
WHERE a.cAktiv = 'Y'
|
||||
GROUP BY ka.kKategorie
|
||||
`);
|
||||
|
||||
// Fetch images (kBild)
|
||||
const imagesResult = await pool.request().query(`
|
||||
SELECT kKategorie, kBild
|
||||
FROM (
|
||||
SELECT kKategorie, kBild, ROW_NUMBER() OVER (PARTITION BY kKategorie ORDER BY nNr ASC) as rn
|
||||
FROM tKategoriebildPlattform
|
||||
WHERE kShop = ${process.env.JTL_SHOP_ID} AND kPlattform = ${process.env.JTL_PLATTFORM_ID}
|
||||
) t
|
||||
WHERE rn = 1
|
||||
`);
|
||||
|
||||
const categories = categoriesResult.recordset;
|
||||
const names = namesResult.recordset;
|
||||
const articleCounts = articleCountsResult.recordset;
|
||||
const images = imagesResult.recordset;
|
||||
|
||||
let tree = this._buildTree(categories, names, articleCounts, images);
|
||||
|
||||
// Keep unpruned tree for image sync
|
||||
const unprunedTree = tree;
|
||||
|
||||
// Ensure directory exists
|
||||
await fs.mkdir(this.cacheDir, { recursive: true });
|
||||
|
||||
const treeString = JSON.stringify(tree, null, 2);
|
||||
const changed = this.lastTreeString !== treeString;
|
||||
|
||||
if (changed) {
|
||||
// Generate translation template BEFORE pruning (to include all categories)
|
||||
const translationTemplate = this._buildTranslationTemplate(tree);
|
||||
const templatePath = path.join(this.cacheDir, 'categories_translation_template.txt');
|
||||
await fs.writeFile(templatePath, this._formatTranslationTemplate(translationTemplate));
|
||||
console.log(`💾 Translation template saved to ${templatePath}`);
|
||||
|
||||
// Now prune for the main tree
|
||||
tree = this._pruneTree(tree);
|
||||
|
||||
const filePath = path.join(this.cacheDir, 'category_tree.json');
|
||||
await fs.writeFile(filePath, JSON.stringify(tree, null, 2));
|
||||
console.log(`💾 Category tree saved to ${filePath}`);
|
||||
|
||||
this.lastTreeString = treeString;
|
||||
console.log('📢 Tree updated.');
|
||||
} else {
|
||||
console.log('🤷 No changes detected in category tree.');
|
||||
}
|
||||
|
||||
this.emit('synced', { tree, unprunedTree, changed });
|
||||
|
||||
} finally {
|
||||
if (pool) {
|
||||
await pool.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_buildTree(categories, names, articleCounts, images) {
|
||||
// Create a map for quick lookup of names
|
||||
const nameMap = new Map();
|
||||
names.forEach(n => nameMap.set(n.kKategorie, n.cName));
|
||||
|
||||
// Create a map for article counts
|
||||
const countMap = new Map();
|
||||
articleCounts.forEach(c => countMap.set(c.kKategorie, c.count));
|
||||
|
||||
// Create a map for images
|
||||
const imageMap = new Map();
|
||||
images.forEach(i => imageMap.set(i.kKategorie, i.kBild));
|
||||
|
||||
// Create a map for category nodes
|
||||
const categoryMap = new Map();
|
||||
|
||||
// Initialize all nodes
|
||||
categories.forEach(cat => {
|
||||
categoryMap.set(cat.kKategorie, {
|
||||
kKategorie: cat.kKategorie,
|
||||
cName: nameMap.get(cat.kKategorie) || `Unknown (${cat.kKategorie})`, // Fallback if name missing
|
||||
articleCount: countMap.get(cat.kKategorie) || 0,
|
||||
kBild: imageMap.get(cat.kKategorie) || null,
|
||||
children: [],
|
||||
nSort: cat.nSort || 0 // Store nSort temporarily
|
||||
});
|
||||
});
|
||||
|
||||
const rootNodes = [];
|
||||
|
||||
// Build hierarchy
|
||||
categories.forEach(cat => {
|
||||
const node = categoryMap.get(cat.kKategorie);
|
||||
if (cat.kOberKategorie === 0) {
|
||||
rootNodes.push(node);
|
||||
} else {
|
||||
const parent = categoryMap.get(cat.kOberKategorie);
|
||||
if (parent) {
|
||||
parent.children.push(node);
|
||||
} else {
|
||||
// Handle orphan nodes if necessary, or ignore
|
||||
// console.warn(`Orphan category found: ${cat.kKategorie}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const rootId = process.env.ROOT_CATEGORY_ID ? parseInt(process.env.ROOT_CATEGORY_ID) : null;
|
||||
let resultNodes = rootNodes;
|
||||
|
||||
if (rootId) {
|
||||
const specificRoot = categoryMap.get(rootId);
|
||||
// Return the children of the specified root, not the root itself
|
||||
resultNodes = specificRoot ? specificRoot.children : [];
|
||||
}
|
||||
|
||||
// Sort children and remove nSort
|
||||
for (const node of categoryMap.values()) {
|
||||
node.children.sort((a, b) => a.nSort - b.nSort);
|
||||
}
|
||||
|
||||
// Sort root nodes if returning multiple
|
||||
resultNodes.sort((a, b) => a.nSort - b.nSort);
|
||||
|
||||
// Remove nSort property from all nodes
|
||||
for (const node of categoryMap.values()) {
|
||||
delete node.nSort;
|
||||
}
|
||||
|
||||
return resultNodes;
|
||||
}
|
||||
|
||||
_pruneTree(nodes) {
|
||||
// Filter out nodes that are empty (no articles) and have no valid children
|
||||
return nodes.filter(node => {
|
||||
// Recursively prune children
|
||||
if (node.children && node.children.length > 0) {
|
||||
node.children = this._pruneTree(node.children);
|
||||
}
|
||||
|
||||
// Keep node if it has articles OR has remaining children
|
||||
const hasArticles = node.articleCount > 0;
|
||||
const hasChildren = node.children && node.children.length > 0;
|
||||
|
||||
return hasArticles || hasChildren;
|
||||
});
|
||||
}
|
||||
|
||||
_buildTranslationTemplate(nodes) {
|
||||
return nodes.map(node => {
|
||||
const result = { name: node.cName };
|
||||
if (node.children && node.children.length > 0) {
|
||||
result.children = this._buildTranslationTemplate(node.children);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
_formatTranslationTemplate(nodes, indent = 0) {
|
||||
const spaces = ' '.repeat(indent);
|
||||
const innerSpaces = ' '.repeat(indent + 1);
|
||||
|
||||
if (nodes.length === 0) return '[]';
|
||||
|
||||
const lines = ['['];
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
const isLast = index === nodes.length - 1;
|
||||
|
||||
if (node.children && node.children.length > 0) {
|
||||
// Node with children - multi-line format
|
||||
lines.push(`${innerSpaces}{`);
|
||||
lines.push(`${innerSpaces} "name": "${node.name}",`);
|
||||
lines.push(`${innerSpaces} "children": ${this._formatTranslationTemplate(node.children, indent + 2)}`);
|
||||
lines.push(`${innerSpaces}}${isLast ? '' : ','}`);
|
||||
} else {
|
||||
// Leaf node - single line format
|
||||
lines.push(`${innerSpaces}{ "name": "${node.name}" }${isLast ? '' : ','}`);
|
||||
}
|
||||
});
|
||||
|
||||
lines.push(`${spaces}]`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
}
|
||||
|
||||
const instance = new CategorySyncer();
|
||||
export default instance;
|
||||
Reference in New Issue
Block a user