biblesdk
Version:
Typescript client for Bible SDK API
315 lines (310 loc) • 10.5 kB
JavaScript
;
var lruCache = require('lru-cache');
// This file is auto-generated. Do not edit directly.
var BASE_URL = "https://biblesdk.com/api";
var httpCache = new lruCache.LRUCache({
// max number of items in cache
max: 500,
// how long items can live in the cache in ms
ttl: 1e3 * 60 * 5,
// return stale items before removing from cache?
allowStale: true
});
async function fancyFetch(input, init, timeoutMs = 3e4, maxRetries = 5, baseDelay = 200, maxDelay = 3e4) {
let delay = baseDelay;
const controller = new AbortController();
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const attemptTimer = setTimeout(() => controller.abort(), timeoutMs);
try {
const res = await fetch(input, { ...init, signal: controller.signal });
if (!res.ok && res.status >= 500 && attempt < maxRetries) {
throw new Error(`HTTP ${res.status}`);
}
clearTimeout(attemptTimer);
return res;
} catch (err) {
clearTimeout(attemptTimer);
if (attempt === maxRetries)
throw err;
delay = Math.min(maxDelay, Math.random() * (delay * 3 - baseDelay) + baseDelay);
await new Promise((res) => setTimeout(res, delay));
}
}
throw new Error(`fetch failed after ${maxRetries} attempts`);
}
async function httpGet(url, headers) {
const cacheKey = `${url}-${JSON.stringify(headers)}`;
if (httpCache.has(cacheKey)) {
return httpCache.get(cacheKey);
}
const response = await fancyFetch(`${BASE_URL}${url}`, {
method: "GET",
headers: {
...headers,
"Content-Type": "application/json"
}
});
const data = response.json();
httpCache.set(cacheKey, data);
return data;
}
// src/client.ts
function parsePaginationLink(link) {
const match = link.match(
/^\/api\/books\/([^/]+)\/chapters\/([^/]+)\/verses(?:\?([^#]+))?/
);
if (match != null) {
const [_, bookParam, chapterParam, query] = match;
if (bookParam != null && chapterParam != null && query != null) {
const params = new URLSearchParams(query);
const takeParam = params.get("take");
const cursorParam = params.get("cursor");
if (takeParam != null && cursorParam != null) {
const take = parseInt(takeParam);
const cursor = parseInt(cursorParam);
const chapter = parseInt(chapterParam);
if (bookParam.length === 3 && chapter > 0 && take > 0 && cursor >= 0) {
return {
book: bookParam,
chapter,
take,
cursor
};
}
}
}
}
return null;
}
function parsePhrases(data, book, chapter) {
const phrases = [];
for (const p of data ?? []) {
if (p.text == null || p.usfm == null || p.position == null) {
throw new Error(`Failed to parse phrases for book ${book}, chapter ${chapter}, verse ${p.verse}, missing required phrase data`);
}
phrases.push({
book,
text: p.text,
usfm: p.usfm,
position: p.position,
verse: p.verse ?? null,
verse_position: p.verse_position ?? null,
chapter
});
}
return phrases;
}
function parsePhrasesWithConcordanceInfo(data, book, chapter) {
const phrases = parsePhrases(data, book, chapter);
const phrasesWithConcordanceInfo = [];
for (const p of phrases) {
const phraseIndex = data.findIndex((d) => d.position === p.position);
if (phraseIndex === -1 || data[phraseIndex] == null) {
throw new Error(`Failed to parse phrases with concordance info for book ${book}, chapter ${chapter}, verse ${p.verse}, missing required phrase data`);
}
phrasesWithConcordanceInfo.push({
...p,
strongs_number: data[phraseIndex].strongs_number ?? null,
strongs_type: data[phraseIndex].strongs_type ?? null,
transliteration: data[phraseIndex].transliteration ?? null,
definition: data[phraseIndex].definition ?? null,
hebrew_word: data[phraseIndex].hebrew_word ?? null,
greek_word: data[phraseIndex].greek_word ?? null
});
}
return phrasesWithConcordanceInfo;
}
async function parseVerseRange(book, chapter, verseRange) {
const [startVerse, endVerse] = verseRange;
const chapterMetadata = await getChapterMetadata(book, chapter);
if (endVerse > chapterMetadata.verses) {
throw new Error(`Invalid verse range: end verse ${endVerse} out of range for book ${book}, chapter ${chapter}, which has (${chapterMetadata.verses}) verses`);
}
if (startVerse < 1 || startVerse > chapterMetadata.verses) {
throw new Error(`Invalid verse range: start verse ${startVerse} out of range for book ${book}, chapter ${chapter}, which has (${chapterMetadata.verses}) verses`);
}
if (startVerse > endVerse) {
throw new Error(`Invalid verse range: ${startVerse} must be less than or equal to requested end verse ${endVerse} for book ${book}, chapter ${chapter}`);
}
return {
startVerse,
endVerse
};
}
function joinPhrases(phrases) {
const output = [];
for (let i = 0; i < phrases.length; i++) {
const rawPhrase = phrases[i];
if (rawPhrase != null) {
output.push(rawPhrase.text.replace(/ {2,}/g, " "));
}
}
return output.join("");
}
async function listBooks() {
const data = await httpGet("/books");
if (data.books == null || data.books.length === 0) {
throw new Error("Failed to list books, no books found");
}
const books = [];
for (const b of data.books ?? []) {
if (b.code == null || b.name == null || b.chapters == null || b.position == null) {
throw new Error("Failed to list books, missing required book data");
}
books.push({
code: b.code,
name: b.name,
chapters: b.chapters,
position: b.position
});
}
return books;
}
async function listChapters(book) {
const data = await httpGet(`/books/${book}/chapters`);
if (data.chapters == null || data.chapters.length === 0) {
throw new Error(`Failed to list chapters for book ${book}, no chapters found`);
}
const chapters = [];
for (const c of data.chapters ?? []) {
if (c.chapter == null || c.position == null || c.verses == null) {
throw new Error(`Failed to list chapters for book ${book}, missing required chapter data`);
}
chapters.push({
book,
chapter: c.chapter,
position: c.position,
verses: c.verses
});
}
return chapters;
}
async function getBookMetadata(book) {
const data = await httpGet(`/books/${book}`);
if (data.code == null || data.name == null || data.chapters == null || data.position == null) {
throw new Error(`Failed to get book metadata for book ${book}, missing required book data`);
}
return {
code: data.code,
name: data.name,
chapters: data.chapters,
position: data.position
};
}
async function getChapterMetadata(book, chapter) {
const data = await httpGet(`/books/${book}/chapters/${chapter}`);
if (data.chapter == null || data.position == null || data.verses == null) {
throw new Error(`Failed to get chapter metadata for book ${book}, chapter ${chapter}, missing required chapter data`);
}
return {
book,
chapter: data.chapter,
position: data.position,
verses: data.verses
};
}
async function read(book, chapter, cursor = 0, take = 50, withConcordanceInfo = false) {
let url = `/books/${book}/chapters/${chapter}/verses?cursor=${cursor}&take=${take}`;
if (withConcordanceInfo === true) {
url += `&concordance=true`;
}
const data = await httpGet(url);
if (data.phrases == null || data.phrases.length === 0) {
throw new Error(`Failed to read chapter ${chapter} for book ${book}, no phrases found`);
}
if (data.links == null) {
throw new Error(`Error while reading book ${book}, chapter ${chapter}, no next or prev links available`);
}
let phrases = [];
if (withConcordanceInfo === true) {
phrases = parsePhrasesWithConcordanceInfo(data.phrases, book, chapter);
} else {
phrases = parsePhrases(data.phrases, book, chapter);
}
let n = null;
let p = null;
if (data.links.next != null) {
n = parsePaginationLink(data.links.next);
}
if (data.links.prev != null) {
p = parsePaginationLink(data.links.prev);
}
const output = {
phrases,
next: null,
prev: null
};
if (n != null) {
output.next = () => read(n.book, n.chapter, n.cursor, n.take);
}
if (p != null) {
output.prev = () => read(p.book, p.chapter, p.cursor, p.take);
}
return output;
}
async function getPhrases(book, chapter, verseRange, withConcordanceInfo = false) {
const { startVerse, endVerse } = await parseVerseRange(book, chapter, verseRange);
const url = `/books/${book}/chapters/${chapter}/verses/${startVerse}-${endVerse}`;
if (withConcordanceInfo === true) {
const data = await httpGet(`${url}?concordance=true`);
return parsePhrasesWithConcordanceInfo(data, book, chapter);
} else {
const data = await httpGet(url);
return parsePhrases(data, book, chapter);
}
}
async function getVerses(book, chapter, verseRange) {
const phrases = await getPhrases(book, chapter, verseRange);
const verses = [];
let currentVerse = {
number: 1,
phrases: []
};
for (const p of phrases) {
if (p.verse == null) {
continue;
}
if (p.verse > currentVerse.number) {
verses.push({
book,
chapter,
verse: currentVerse.number,
text: joinPhrases(currentVerse.phrases)
});
currentVerse.number = p.verse;
currentVerse.phrases = [];
}
currentVerse.phrases.push(p);
}
return verses;
}
async function getSearchResults(query) {
const data = await httpGet(`/search?query=${query}`);
if (data.matches == null || data.matches.length === 0) {
throw new Error(`Failed to retrieve search results for query "${query}"`);
}
const matches = [];
for (const m of data.matches) {
if (m.book == null || m.chapter == null || m.verse == null || m.score == null) {
throw new Error(`Unexpected missing data in search results for query ${query}`);
}
matches.push({
book: m.book,
chapter: m.chapter,
verse: m.verse,
score: m.score
});
}
return matches;
}
exports.getBookMetadata = getBookMetadata;
exports.getChapterMetadata = getChapterMetadata;
exports.getPhrases = getPhrases;
exports.getSearchResults = getSearchResults;
exports.getVerses = getVerses;
exports.joinPhrases = joinPhrases;
exports.listBooks = listBooks;
exports.listChapters = listChapters;
exports.read = read;
//# sourceMappingURL=out.js.map
//# sourceMappingURL=index.js.map