UNPKG

ephrem

Version:

Ephrem is a light-weight API wrapper for API.Bible, built using NodeJS and Typescript. Ephrem validates bible references and fetches scripture text corresponding to the references.

370 lines 10.9 kB
import axios from "axios"; import axiosRetry from "axios-retry"; import * as fs from "node:fs"; import path from "node:path"; import { BaseEphremError, createDataDir, ephremPaths, normalizeBookName, removePunctuation } from "./utils.js"; const API_BIBLE_BASE_URL = "https://api.scripture.api.bible"; const API_BIBLE_TIMEOUT = 1e4; const ISO_693_3_REGEX = /^[a-z]{3}$/; const BIBLES_DATA_PATH = path.join(ephremPaths.data, "bibles.json"); const ABB_TO_ID_MAPPING_PATH = path.join( ephremPaths.data, "bibles-map.json" ); const BOOKS_DATA_PATH = path.join(ephremPaths.data, "books.json"); const NAMES_TO_BIBLES_PATH = path.join( ephremPaths.data, "book-names-to-bibles.json" ); class ApiBibleKeyNotFoundError extends BaseEphremError { constructor() { super("API.Bible Key not found. Please provide a valid API key."); this.name = "ApiBibleKeyNotFoundError"; this.context = {}; } } const hasApiBibleKey = () => { return process.env.API_BIBLE_API_KEY !== void 0; }; class InvalidLanguageIDError extends BaseEphremError { context; constructor(languageId) { super("Language ID does not match ISO 639-3 format (lower case)"); this.name = "InvalidLanguageIDError"; this.context = { languageId }; } } class BiblesNotAvailableError extends BaseEphremError { context; constructor(languageId) { super( "No Bibles were available for the given language using the specified API.Bible key" ); this.name = "BiblesNotAvailableError"; this.context = { languageId }; } } class BiblesFetchError extends BaseEphremError { constructor(message, languageId, statusCode, statusText) { super(message); this.statusCode = statusCode; this.statusText = statusText; this.name = "BiblesFetchError"; this.context = { languageId, statusCode, statusText }; } context; } class BooksFetchError extends BaseEphremError { constructor(message, bibleId, statusCode, statusText) { super(message); this.statusCode = statusCode; this.statusText = statusText; this.name = "BooksFetchError"; this.context = { bibleId, statusCode, statusText }; } context; } class PassageFetchError extends BaseEphremError { constructor(message, passageId, bibleId, passageOptions, statusCode, statusText) { super(message); this.statusCode = statusCode; this.statusText = statusText; this.name = "PassageFetchError"; this.context = { bibleId, passageId, passageOptions, statusCode, statusText }; } context; } const convertPassageOptionsForApi = (options) => { const { contentType, includeChapterNumbers, includeNotes, includeTitles, includeVerseNumbers, includeVerseSpans } = options; const precursor = { "content-type": contentType, "include-chapter-numbers": includeChapterNumbers, "include-notes": includeNotes, "include-titles": includeTitles, "include-verse-numbers": includeVerseNumbers, "include-verse-spans": includeVerseSpans }; return Object.fromEntries( Object.entries(precursor).filter(([, value]) => value !== void 0) ); }; axiosRetry(axios, { retries: 5, retryDelay: (retryCount, error) => axiosRetry.exponentialDelay(retryCount, error, 1e3), shouldResetTimeout: true }); const getDefaultApiHeader = () => { if (!hasApiBibleKey()) { throw new ApiBibleKeyNotFoundError(); } return { Accept: "application/json", "api-key": process.env.API_BIBLE_API_KEY }; }; const getBiblesFromApiConfig = (normalizedLanguageId) => { if (!ISO_693_3_REGEX.test(normalizedLanguageId)) { throw new InvalidLanguageIDError(normalizedLanguageId); } return { baseURL: API_BIBLE_BASE_URL, headers: getDefaultApiHeader(), params: { "include-full-details": false, language: normalizedLanguageId }, timeout: API_BIBLE_TIMEOUT }; }; const handleGetBiblesFromApiError = (error, languageId) => { let errorMessage = "An unexpected error occurred while fetching Bibles from API.Bible."; let statusCode = void 0; let statusText = void 0; if (axios.isAxiosError(error)) { const response = error.response; if (response?.status) { statusCode = response.status; } if (response?.statusText) { statusText = response.statusText; } if (statusCode === 400) { errorMessage = "Not authorized to retrieve any Bibles or invalid `language` provided."; } else if (statusCode === 401) { errorMessage = "Unauthorized for API access. Missing or Invalid API Key provided."; } } return new BiblesFetchError(errorMessage, languageId, statusCode, statusText); }; const getBiblesFromApi = async (normalizedLanguageId) => { const config = getBiblesFromApiConfig(normalizedLanguageId); let bibles = []; try { const response = await axios.get("/v1/bibles", config); bibles = response.data.data; } catch (error) { throw handleGetBiblesFromApiError(error, normalizedLanguageId); } if (bibles.length === 0) { throw new BiblesNotAvailableError(normalizedLanguageId); } return bibles; }; const getBiblesInLanguages = async (languageIds) => { const normalizedLanguageIds = languageIds.map( (languageId) => removePunctuation(languageId).trim().toLowerCase() ); const uniqueLanguageIds = [...new Set(normalizedLanguageIds)]; const bibles = await Promise.all( uniqueLanguageIds.map( (normalizedLanguageId) => getBiblesFromApi(normalizedLanguageId) ) ); return bibles.flat(); }; const writeBiblesMap = async (bibles) => { const biblesMap = bibles.reduce((acc, bible) => { acc[bible.abbreviation] = bible.id; acc[bible.abbreviationLocal] = bible.id; return acc; }, {}); await fs.promises.writeFile( ABB_TO_ID_MAPPING_PATH, JSON.stringify(biblesMap, null, 2) ); }; const writeBiblesData = async (bibles) => { const biblesData = bibles.reduce((acc, bible) => { acc[bible.id] = bible; return acc; }, {}); await fs.promises.writeFile( BIBLES_DATA_PATH, JSON.stringify(biblesData, null, 2) ); }; const setupBibles = async (languageIds) => { const bibles = await getBiblesInLanguages(languageIds); await writeBiblesMap(bibles); await writeBiblesData(bibles); return ephremPaths.data; }; const handleGetBooksFromApiError = (error, bibleId) => { let errorMessage = "An unexpected error occurred while fetching Books from API.Bible."; let statusCode = void 0; let statusText = void 0; if (axios.isAxiosError(error)) { const response = error.response; if (response?.status) { statusCode = response.status; } if (response?.statusText) { statusText = response.statusText; } if (statusCode === 400) { errorMessage = "Not authorized to retrieve any Bibles or invalid `language` provided."; } else if (statusCode === 401) { errorMessage = "Unauthorized for API access. Missing or Invalid API Key provided."; } } return new BooksFetchError(errorMessage, bibleId, statusCode, statusText); }; const getBooksFromApiConfig = () => { if (!hasApiBibleKey()) { throw new ApiBibleKeyNotFoundError(); } return { baseURL: API_BIBLE_BASE_URL, headers: getDefaultApiHeader(), params: { "include-chapters": false }, timeout: API_BIBLE_TIMEOUT }; }; const getBooksFromApi = async (bibleId) => { const config = getBooksFromApiConfig(); try { const response = await axios.get( `/v1/bibles/${bibleId}/books`, config ); return response.data.data; } catch (error) { throw handleGetBooksFromApiError(error, bibleId); } }; const getBooksFromBibles = async (bibleIds) => { const books = await Promise.all( bibleIds.map((bibleId) => getBooksFromApi(bibleId)) ); return books.flat(); }; const getBookNamesToBibles = (books) => { const bookNamesToBibles = {}; for (const book of books) { const bookName = normalizeBookName(book.name); if (!(bookName in bookNamesToBibles)) { bookNamesToBibles[bookName] = {}; } if (!(book.bibleId in bookNamesToBibles[bookName])) { bookNamesToBibles[bookName][book.bibleId] = book.id; } } return bookNamesToBibles; }; const writeBookNamesToBibles = async (books) => { const bookNamesToBibles = getBookNamesToBibles(books); await fs.promises.writeFile( NAMES_TO_BIBLES_PATH, JSON.stringify(bookNamesToBibles, null, 2) ); }; const setupBooks = async () => { const biblesData = JSON.parse( await fs.promises.readFile(BIBLES_DATA_PATH, "utf-8") ); const bibleIds = Object.keys(biblesData); const books = await getBooksFromBibles(bibleIds); await fs.promises.writeFile(BOOKS_DATA_PATH, JSON.stringify(books, null, 2)); await writeBookNamesToBibles(books); return ephremPaths.data; }; const setupEphrem = async (languageIds) => { await createDataDir(); await setupBibles(languageIds); await setupBooks(); return ephremPaths.data; }; const getPassageFromApiConfig = (passageOptions) => { if (!hasApiBibleKey()) { throw new ApiBibleKeyNotFoundError(); } return { baseURL: API_BIBLE_BASE_URL, headers: getDefaultApiHeader(), params: convertPassageOptionsForApi(passageOptions), timeout: API_BIBLE_TIMEOUT }; }; const handleGetPassageFromApiError = (error, passageId, bibleId, passageOptions) => { let errorMessage = "An unexpected error occurred while fetching Passage from API.Bible."; let statusCode = void 0; let statusText = void 0; if (axios.isAxiosError(error)) { const response = error.response; if (response?.status) { statusCode = response.status; } if (response?.statusText) { statusText = response.statusText; } if (statusCode === 400) { errorMessage = "Not authorized to retrieve any Bibles or invalid `language` provided."; } else if (statusCode === 401) { errorMessage = "Unauthorized for API access. Missing or Invalid API Key provided."; } } return new PassageFetchError( errorMessage, passageId, bibleId, passageOptions, statusCode, statusText ); }; const getPassageFromApi = async (passageId, bibleId, passageOptions) => { const config = getPassageFromApiConfig(passageOptions); try { const response = await axios.get( `/v1/bibles/${bibleId}/passages/${passageId}`, config ); return response.data; } catch (error) { throw handleGetPassageFromApiError( error, passageId, bibleId, passageOptions ); } }; const getBibleAbbreviationsFilepath = () => ABB_TO_ID_MAPPING_PATH; export { ABB_TO_ID_MAPPING_PATH, ApiBibleKeyNotFoundError, BIBLES_DATA_PATH, BOOKS_DATA_PATH, BiblesFetchError, BiblesNotAvailableError, BooksFetchError, InvalidLanguageIDError, NAMES_TO_BIBLES_PATH, PassageFetchError, getBibleAbbreviationsFilepath, getPassageFromApi, hasApiBibleKey, setupEphrem }; //# sourceMappingURL=api-bible.js.map