Session 29: Content generation templates — slate threads, POTD, recaps, matchup previews (1660 tests)
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* /api/content (Session 29)
|
||||
*
|
||||
* Structured content objects generated from live VYNDR data, degrading
|
||||
* gracefully by data level. Read-only, zero-credit (reads cached data).
|
||||
* `?format=text` additionally returns post-ready plain text.
|
||||
*
|
||||
* GET /api/content/slate/:sport → daily slate thread
|
||||
* GET /api/content/potd/:sport → prop (or game) of the day
|
||||
* GET /api/content/recap/:sport → results recap (needs resolved grades)
|
||||
* GET /api/content/preview/:sport/:gameId → matchup preview for one game
|
||||
*/
|
||||
|
||||
const express = require('express');
|
||||
const template = require('../services/contentTemplateService');
|
||||
const formatter = require('../services/contentFormatter');
|
||||
const { cacheGet } = require('../utils/redis');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
const MISSION_HEADER = { 'X-VYNDR-Mission': 'Every post is a free ad' };
|
||||
const SUPPORTED = new Set(['nba', 'wnba', 'mlb', 'soccer', 'nfl', 'nhl']);
|
||||
|
||||
function guard(req, res) {
|
||||
const sport = String(req.params.sport || '').toLowerCase();
|
||||
if (!SUPPORTED.has(sport)) {
|
||||
res.status(404).set(MISSION_HEADER).json({ error: `No content for sport: ${sport}` });
|
||||
return null;
|
||||
}
|
||||
return sport;
|
||||
}
|
||||
|
||||
router.get('/slate/:sport', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
try {
|
||||
const data = await template.collectSlateData(sport);
|
||||
const thread = template.generateSlateThread(sport, data);
|
||||
const body = { ...thread };
|
||||
if (req.query.format === 'text') body.text = formatter.formatSlateThread(thread);
|
||||
return res.set(MISSION_HEADER).json(body);
|
||||
} catch (err) {
|
||||
console.error(`[content/slate/${sport}]`, err.message);
|
||||
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/potd/:sport', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
try {
|
||||
const data = await template.collectSlateData(sport);
|
||||
const potd = template.generatePOTD(sport, data);
|
||||
const body = { ...potd };
|
||||
if (req.query.format === 'text') body.text = formatter.formatPOTD(potd);
|
||||
return res.set(MISSION_HEADER).json(body);
|
||||
} catch (err) {
|
||||
console.error(`[content/potd/${sport}]`, err.message);
|
||||
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/recap/:sport', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
try {
|
||||
// Resolved grades land in a daily cache when the grading flow settles.
|
||||
const utc = new Date().toISOString().split('T')[0];
|
||||
const cache = (await cacheGet(`results:${sport}:${utc}`)) ?? (await cacheGet(`results:${sport}`));
|
||||
const resolved = Array.isArray(cache) ? cache : Array.isArray(cache?.grades) ? cache.grades : [];
|
||||
const recap = template.generateResultsRecap(sport, resolved);
|
||||
const body = { ...recap };
|
||||
if (req.query.format === 'text') body.text = formatter.formatRecap(recap);
|
||||
return res.set(MISSION_HEADER).json(body);
|
||||
} catch (err) {
|
||||
console.error(`[content/recap/${sport}]`, err.message);
|
||||
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
|
||||
}
|
||||
});
|
||||
|
||||
router.get('/preview/:sport/:gameId', async (req, res) => {
|
||||
const sport = guard(req, res);
|
||||
if (!sport) return undefined;
|
||||
const { gameId } = req.params;
|
||||
try {
|
||||
const data = await template.collectSlateData(sport);
|
||||
const game = (data.schedule || []).find((g) => String(g.id) === String(gameId));
|
||||
if (!game) {
|
||||
return res.status(404).set(MISSION_HEADER).json({ error: 'Game not on today\'s schedule.' });
|
||||
}
|
||||
const lines = template.__internals.findGameLinesFor(game, data.gameLines);
|
||||
const preview = template.generateMatchupPreview(game, lines, data.streaks);
|
||||
return res.set(MISSION_HEADER).json(preview);
|
||||
} catch (err) {
|
||||
console.error(`[content/preview/${sport}]`, err.message);
|
||||
return res.status(500).set(MISSION_HEADER).json({ error: 'Content generation failed' });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user