Add Tiny product structure sync

This commit is contained in:
Cauê Faleiros
2026-07-23 13:57:47 -03:00
parent 831f82ee57
commit bda1e22be3
3 changed files with 663 additions and 0 deletions

View File

@@ -169,6 +169,121 @@ app.get('/api/stop-graphs-backfill', (req: Request, res: Response) => {
});
});
// Hidden endpoint to sync Tiny product structures/composition into Graphs consumption references
app.get('/api/trigger-product-structures-sync', (req: Request, res: Response) => {
const expectedToken = process.env.TINY_WEBHOOK_SECRET;
if (expectedToken && req.query.token !== expectedToken) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const scriptPath = path.join(__dirname, 'scripts', 'sync-product-structures.js');
const env = { ...process.env };
const stopFile = env.PRODUCT_STRUCTURES_STOP_FILE || path.join(process.cwd(), 'product_structures_sync_stop.json');
const optionalEnvMap: Record<string, string> = {
skus: 'PRODUCT_STRUCTURE_SKUS',
productIds: 'PRODUCT_STRUCTURE_PRODUCT_IDS',
inputFile: 'PRODUCT_STRUCTURES_INPUT_FILE',
graphsUrl: 'PRODUCT_STRUCTURES_GRAPHS_API_URL',
graphsApiKey: 'PRODUCT_STRUCTURES_GRAPHS_API_KEY',
dryRun: 'PRODUCT_STRUCTURES_DRY_RUN',
maxProducts: 'PRODUCT_STRUCTURES_MAX_PRODUCTS',
requestDelayMs: 'PRODUCT_STRUCTURES_REQUEST_DELAY_MS',
graphsRetryDelayMs: 'PRODUCT_STRUCTURES_GRAPHS_RETRY_DELAY_MS',
graphsMaxRetries: 'PRODUCT_STRUCTURES_GRAPHS_MAX_RETRIES'
};
for (const [queryKey, envKey] of Object.entries(optionalEnvMap)) {
const value = req.query[queryKey];
if (typeof value === 'string' && value.trim() !== '') {
env[envKey] = value.trim();
}
}
if (fs.existsSync(stopFile)) {
fs.rmSync(stopFile, { force: true });
}
const child = spawn('node', [scriptPath], {
detached: true,
stdio: 'inherit',
env
});
child.on('error', (error) => {
console.error(`[server]: Product structures sync process failed to start: ${error.message}`);
});
child.on('exit', (code, signal) => {
console.log(`[server]: Product structures sync process exited. code=${code ?? 'null'} signal=${signal ?? 'null'}`);
});
child.unref();
console.log('[server]: Product structures sync script manually triggered via HTTP endpoint.');
res.status(200).json({
status: 'STARTED',
message: 'Product structures sync is running in the background. Check Portainer logs for progress.',
options: {
skus: env.PRODUCT_STRUCTURE_SKUS || null,
productIds: env.PRODUCT_STRUCTURE_PRODUCT_IDS || null,
inputFile: env.PRODUCT_STRUCTURES_INPUT_FILE || null,
graphsUrl: env.PRODUCT_STRUCTURES_GRAPHS_API_URL || env.PRODUCTION_ORDERS_GRAPHS_API_URL || env.GRAPHS_PRODUCTION_ORDERS_API_URL || null,
dryRun: env.PRODUCT_STRUCTURES_DRY_RUN || 'false',
maxProducts: env.PRODUCT_STRUCTURES_MAX_PRODUCTS || null,
requestDelayMs: env.PRODUCT_STRUCTURES_REQUEST_DELAY_MS || '2500',
graphsMaxRetries: env.PRODUCT_STRUCTURES_GRAPHS_MAX_RETRIES || '3',
graphsApiKeyConfigured: Boolean(env.PRODUCT_STRUCTURES_GRAPHS_API_KEY || env.PRODUCTION_ORDERS_GRAPHS_API_KEY || env.GRAPHS_PRODUCTION_ORDERS_API_KEY || env.GRAPHS_API_KEY || env.NEXSTAR_GRAPHS_API_KEY || env.API_KEY)
}
});
});
app.get('/api/product-structures-sync-status', (req: Request, res: Response) => {
const expectedToken = process.env.TINY_WEBHOOK_SECRET;
if (expectedToken && req.query.token !== expectedToken) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const stateFile = process.env.PRODUCT_STRUCTURES_STATE_FILE || path.join(process.cwd(), 'product_structures_sync_state.json');
const stopFile = process.env.PRODUCT_STRUCTURES_STOP_FILE || path.join(process.cwd(), 'product_structures_sync_stop.json');
let state = null;
if (fs.existsSync(stateFile)) {
try {
state = JSON.parse(fs.readFileSync(stateFile, 'utf-8'));
} catch (error: any) {
res.status(500).json({ error: `Could not read product structures sync state: ${error.message}` });
return;
}
}
res.status(200).json({
status: state?.status || 'unknown',
stopRequested: fs.existsSync(stopFile),
state
});
});
app.get('/api/stop-product-structures-sync', (req: Request, res: Response) => {
const expectedToken = process.env.TINY_WEBHOOK_SECRET;
if (expectedToken && req.query.token !== expectedToken) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const stopFile = process.env.PRODUCT_STRUCTURES_STOP_FILE || path.join(process.cwd(), 'product_structures_sync_stop.json');
fs.writeFileSync(stopFile, JSON.stringify({
requestedAt: new Date().toISOString()
}, null, 2));
res.status(200).json({
status: 'STOP_REQUESTED',
message: 'Product structures sync will stop after the current wait/request finishes.'
});
});
// Hidden endpoint to download the stock CSV log directly from the browser
app.get('/api/stock-logs/download', (req: Request, res: Response) => {
const expectedToken = process.env.TINY_WEBHOOK_SECRET;