Publish Container / docker (push) Successful in 14m9s
src/App.tsx (Scripture Index book map) and server/routes/bible-passage.js (BSB/bolls.life passage fetch) each hand-maintained their own independent 66-book list with a different key format (3-letter USFM id vs bolls.life's numeric id), so a future alias/rename fix in one would silently not apply to the other. Add shared/bible-books.js as the single canonical list (name + aliases + USFM id, in canonical order); the server derives its numeric bolls.life ids from the array position instead of hand-maintaining a second table. Requires allowJs + including shared/ in tsconfig.app.json so the client can import the plain-JS module alongside its TypeScript sources. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016Yvx4rPVy2HWe7ubuk3X3J
76 lines
2.5 KiB
JavaScript
76 lines
2.5 KiB
JavaScript
import { BIBLE_BOOKS } from '../../shared/bible-books.js'
|
|
|
|
const passageCache = new Map()
|
|
|
|
// Keyed by lowercase, whitespace-stripped book name/alias (matching the format
|
|
// the admin's "Bible Book" field and the /api/bible-passage `book` query param
|
|
// both use), mapped to bolls.life's numeric book id (canonical order, 1-66).
|
|
// Derived from the shared BIBLE_BOOKS list so it can't drift from the client's
|
|
// Scripture Index book map (src/App.tsx's SI_BOOK_MAP).
|
|
const BOLLS_BOOK_MAP = {}
|
|
BIBLE_BOOKS.forEach((b, index) => {
|
|
const bookNum = index + 1
|
|
for (const key of [b.name.toLowerCase(), ...(b.aliases ?? [])]) {
|
|
BOLLS_BOOK_MAP[key.replace(/\s+/g, '')] = bookNum
|
|
}
|
|
})
|
|
|
|
function parseReference(reference) {
|
|
const stripped = reference.replace(/^[a-z]+/i, '')
|
|
const match = stripped.match(/^(\d+):(\d+)(?:-(\d+))?$/)
|
|
if (!match) return null
|
|
return {
|
|
chapter: parseInt(match[1], 10),
|
|
verseStart: parseInt(match[2], 10),
|
|
verseEnd: match[3] ? parseInt(match[3], 10) : parseInt(match[2], 10),
|
|
}
|
|
}
|
|
|
|
export function register(app) {
|
|
app.get('/api/bible-passage', async (req, res) => {
|
|
const book = String(req.query.book ?? '').trim().toLowerCase().replace(/[\s-]/g, '')
|
|
const reference = String(req.query.reference ?? '').trim()
|
|
|
|
const bookNum = BOLLS_BOOK_MAP[book]
|
|
if (!bookNum) {
|
|
res.status(400).json({ message: 'Unknown book.' })
|
|
return
|
|
}
|
|
|
|
const parsed = parseReference(reference)
|
|
if (!parsed) {
|
|
res.status(400).json({ message: 'Invalid reference format. Expected format: chapter:verseStart-verseEnd' })
|
|
return
|
|
}
|
|
|
|
const cacheKey = `BSB:${bookNum}:${parsed.chapter}:${parsed.verseStart}:${parsed.verseEnd}`
|
|
if (passageCache.has(cacheKey)) {
|
|
res.json({ text: passageCache.get(cacheKey) })
|
|
return
|
|
}
|
|
|
|
try {
|
|
const verseNums = []
|
|
for (let v = parsed.verseStart; v <= parsed.verseEnd; v++) verseNums.push(v)
|
|
|
|
const results = await Promise.all(
|
|
verseNums.map(v =>
|
|
fetch(`https://bolls.life/get-verse/BSB/${bookNum}/${parsed.chapter}/${v}/`, { signal: AbortSignal.timeout(8000) })
|
|
.then(r => r.ok ? r.json() : Promise.reject(new Error(`${r.status}`)))
|
|
)
|
|
)
|
|
|
|
if (results.length === 0) {
|
|
res.status(404).json({ message: 'Passage not found.' })
|
|
return
|
|
}
|
|
|
|
const text = results.map(v => `v. ${v.verse} ${String(v.text).trim()}`).join('\n\n')
|
|
passageCache.set(cacheKey, text)
|
|
res.json({ text })
|
|
} catch {
|
|
res.status(502).json({ message: 'Unable to fetch passage right now.' })
|
|
}
|
|
})
|
|
}
|