Add Tiny product structure sync
This commit is contained in:
115
src/index.ts
115
src/index.ts
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user