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
JavaScript
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