├── server ├── models │ └── user.js ├── v1 │ ├── error.js │ ├── meta.js │ ├── router.js │ ├── mongo.js │ ├── model.js │ └── fetcher.js ├── public │ └── index.html ├── index.js ├── db │ ├── meta.js │ ├── mongo.js │ └── queryMapper.js ├── controllers │ ├── meta.controller.js │ ├── base.controller.js │ └── passage.controller.js ├── parser │ └── parser.js ├── middleware │ └── error-handler.js ├── routes.js └── server.js ├── index.js ├── HISTORY.md ├── .babelrc ├── tests ├── mocks │ └── server.mock.js ├── routes │ └── passage.route.spec.js └── v1 │ ├── server.spec.js.js │ └── fetcher.spec.js ├── .env.example ├── db └── import.sh ├── .travis.yml ├── .gitignore ├── LICENSE ├── package.json └── README.md /server/models/user.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | require("babel-register"); 2 | require('./server'); -------------------------------------------------------------------------------- /server/v1/error.js: -------------------------------------------------------------------------------- 1 | exports.onError = function(message) { 2 | return {message: message}; 3 | } 4 | -------------------------------------------------------------------------------- /server/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Restful Bible API

4 | 5 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // Load environment variables 2 | require('dotenv').config(); 3 | 4 | // Initialize Server 5 | require('./server'); -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | # Release History 2 | 3 | * 0.2.0 4 | * Code rewrite using ES6 classes and async/await 5 | 6 | * 0.1.0 7 | * Initial restful Bible API web service 8 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0"], 3 | "plugins": [ 4 | ["transform-runtime", { 5 | "polyfill": false, 6 | "regenerator": true 7 | }] 8 | ] 9 | } -------------------------------------------------------------------------------- /tests/mocks/server.mock.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import chaiHttp from 'chai-http'; 3 | import server from '../../server/server'; 4 | 5 | chai.use(chaiHttp); 6 | export default chai.request(server); -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=development 2 | NODE_PORT=3333 3 | MONGODB_REPLICA_NODE_1=localhost:27017 4 | MONGODB_SSL=true 5 | MONGODB_DATABASE=bibleapi 6 | MONGODB_COLLECTION=bible 7 | MONGODB_USERNAME=user 8 | MONGODB_PASSWORD=passw0rd -------------------------------------------------------------------------------- /db/import.sh: -------------------------------------------------------------------------------- 1 | # mongoimport --db bibleapi --collection bible --type json --file russian_synodal_updated.json --jsonArray 2 | 3 | # KJV 4 | mongoimport --db bibleapi --collection bible --type json --file kjv.json 5 | 6 | # ASV 7 | mongoimport --db bibleapi --collection bible --type json --file asv.json 8 | -------------------------------------------------------------------------------- /server/db/meta.js: -------------------------------------------------------------------------------- 1 | import { bcv_parser as BcvParcer } from 'bible-passage-reference-parser/js/en_bcv_parser'; 2 | 3 | const getMetaInfo = (translation) => { 4 | const bcv = new BcvParcer; 5 | const translationInfo = bcv.translation_info(translation); 6 | return translationInfo; 7 | }; 8 | 9 | export { getMetaInfo } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: true 3 | node_js: 4 | - "6" 5 | before_install: 6 | - sudo add-apt-repository ppa:ubuntu-toolchain-r/test -y 7 | - sudo apt-get update -q 8 | - sudo apt-get install gcc-4.8 g++-4.8 -y 9 | env: 10 | - TRAVIS=travis CXX=g++-4.8 11 | services: 12 | - mongodb 13 | install: 14 | - npm install 15 | script: 16 | - npm test -------------------------------------------------------------------------------- /server/v1/meta.js: -------------------------------------------------------------------------------- 1 | const bcv_parser = require("bible-passage-reference-parser/js/en_bcv_parser").bcv_parser; 2 | const bcv = new bcv_parser; 3 | 4 | exports.getMeta = function(req, res) { 5 | var translation = bcv.translation_info(req.params.translation); 6 | res.send({translation: translation}); 7 | }; 8 | 9 | exports.getMetaBooks = function(req, res) { 10 | var translation = bcv.translation_info(req.params.translation); 11 | res.send({translation: translation.books}); 12 | }; 13 | -------------------------------------------------------------------------------- /server/controllers/meta.controller.js: -------------------------------------------------------------------------------- 1 | import { getMetaInfo } from '../db/meta'; 2 | import BaseController from './base.controller'; 3 | 4 | class MetaController extends BaseController { 5 | 6 | find = async (req, res, next) => { 7 | try { 8 | const translation = req.params.translation; 9 | const result = getMetaInfo(translation); 10 | res.json( 11 | { 12 | translation: result 13 | } 14 | ); 15 | } catch(err) { 16 | next(err); 17 | } 18 | }; 19 | 20 | } 21 | 22 | export default new MetaController(); -------------------------------------------------------------------------------- /server/v1/router.js: -------------------------------------------------------------------------------- 1 | const meta = require('./meta'); 2 | const model = require('./model'); 3 | 4 | exports.respondIndex = function respondIndex(req, res, next) { 5 | res.set('Content-Type', 'text/html'); 6 | res.end('\n' + 7 | 'BibleAPI\n' + 8 | 'Bible API web service v0.1.0\n'); 9 | next(); 10 | }; 11 | 12 | exports.parsePassage = function parsePassage(req, res, next) { 13 | model.parsePassage(req, res); 14 | next(); 15 | }; 16 | 17 | exports.getMeta = function getMeta(req, res, next) { 18 | meta.getMeta(req, res); 19 | next(); 20 | }; 21 | -------------------------------------------------------------------------------- /server/parser/parser.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { bcv_parser as bcvParser } from 'bible-passage-reference-parser/js/en_bcv_parser'; 3 | const bcv = new bcvParser; 4 | 5 | const parse = (reference) => { 6 | const parsedEntities = bcv.parse(reference).parsed_entities(); 7 | const entities = _.get(parsedEntities, '[0].entities'); 8 | 9 | if (!entities) { 10 | console.error('Passages not found!'); 11 | return null; 12 | } 13 | 14 | return _.map(entities, entity => ({ 15 | type: entity.type, 16 | start: entity.start, 17 | end: entity.end 18 | })); 19 | }; 20 | 21 | export default { parse }; -------------------------------------------------------------------------------- /server/middleware/error-handler.js: -------------------------------------------------------------------------------- 1 | export default function errorHandler(err, req, res, next) { 2 | if (!err) { 3 | return res.sendStatus(500); 4 | } 5 | 6 | const error = { 7 | message: err.message || 'Internal Server Error.', 8 | }; 9 | 10 | if (process.env.NODE_ENV) { 11 | error.stack = err.stack; 12 | } 13 | 14 | if (err.errors) { 15 | error.errors = {}; 16 | const { errors } = err; 17 | for (const type in errors) { 18 | if (type in errors) { 19 | error.errors[type] = errors[type].message; 20 | } 21 | } 22 | } 23 | 24 | res.status(err.status || 500).json(error); 25 | } -------------------------------------------------------------------------------- /server/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | 3 | import MetaController from './controllers/meta.controller'; 4 | import PassageController from './controllers/passage.controller'; 5 | 6 | import errorHandler from './middleware/error-handler'; 7 | 8 | const routes = new Router(); 9 | 10 | routes.get('/favicon.ico', function(req, res) { 11 | res.sendStatus(204); 12 | }); 13 | 14 | routes.get('/random', PassageController.findRandomProverb); 15 | 16 | routes.get('/:reference', PassageController.find); 17 | 18 | routes.get('/meta/:translation', MetaController.find); 19 | 20 | routes.use(errorHandler); 21 | 22 | export default routes; 23 | -------------------------------------------------------------------------------- /server/db/mongo.js: -------------------------------------------------------------------------------- 1 | module.exports = mongoPool => { 2 | return { 3 | getVerses(query) { 4 | return mongoPool.collection(process.env.MONGODB_COLLECTION) 5 | .find(query) 6 | .toArray() 7 | .then(rows => 8 | rows.map((row) => ({ 9 | id: row._id, 10 | book: row.book_id, 11 | chapter: row.chapter, 12 | verse: row.verse, 13 | text: row.text 14 | }))); 15 | }, 16 | getText(query) { 17 | return mongoPool.collection(process.env.MONGODB_COLLECTION) 18 | .find(query) 19 | .toArray() 20 | .then(rows => rows.map(row => row.text).join(' ')); 21 | }, 22 | } 23 | }; -------------------------------------------------------------------------------- /tests/routes/passage.route.spec.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | import server from '../mocks/server.mock'; 3 | 4 | const expect = chai.expect; 5 | 6 | describe.only('GET /', () => { 7 | 8 | describe('200', () => { 9 | 10 | it('should return json', (done) => { 11 | server.get('/api/Gen 1:1') 12 | .end((err, res) => { 13 | expect(res).to.have.status(200); 14 | expect(res.type).to.eql('application/json'); 15 | done(); 16 | }); 17 | }); 18 | 19 | xit('should return the API version', (done) => { 20 | server.get('/') 21 | .end((err, res) => { 22 | expect(res).to.have.status(200); 23 | done(); 24 | }); 25 | }); 26 | 27 | }); 28 | 29 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Commenting this out is preferred by some people, see 24 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 25 | node_modules 26 | 27 | # Users Environment Variables 28 | .lock-wscript 29 | 30 | # IDE directory 31 | .idea 32 | /.env 33 | -------------------------------------------------------------------------------- /server/controllers/base.controller.js: -------------------------------------------------------------------------------- 1 | class BaseController { 2 | filterParams(params, whitelist) { 3 | const filtered = {}; 4 | for (const key in params) { 5 | if (whitelist.indexOf(key) > -1) { 6 | filtered[key] = params[key]; 7 | } 8 | } 9 | return filtered; 10 | } 11 | 12 | formatApiError(err) { 13 | if (!err) { 14 | // eslint-disable-next-line no-console 15 | return console.error('Provide an error'); 16 | } 17 | 18 | const formatted = { 19 | message: err.message, 20 | }; 21 | 22 | if (err.errors) { 23 | formatted.errors = {}; 24 | const errors = err.errors; 25 | for (const type in errors) { 26 | if (errors.hasOwnProperty(type)) { 27 | formatted.errors[type] = errors[type].message; 28 | } 29 | } 30 | } 31 | 32 | return formatted; 33 | } 34 | } 35 | 36 | export default BaseController; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2018 Ruslan Kazakov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /server/v1/mongo.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const mongoClient = require('mongodb').MongoClient; 4 | 5 | const HOST = '127.0.0.1'; 6 | const PORT = '27017'; 7 | const DB_NAME = 'bibleapi'; 8 | const COLLECTION_NAME = 'bible'; 9 | const CONNECTION_STRING = HOST + ':' + PORT +'/' + DB_NAME; 10 | 11 | // if OPENSHIFT env variables are present, use the available connection info: 12 | if (process.env.OPENSHIFT_MONGODB_DB_PASSWORD) { 13 | connectionString = process.env.OPENSHIFT_MONGODB_DB_USERNAME + ":" + 14 | process.env.OPENSHIFT_MONGODB_DB_PASSWORD + "@" + 15 | process.env.OPENSHIFT_MONGODB_DB_HOST + ':' + 16 | process.env.OPENSHIFT_MONGODB_DB_PORT + '/' + 17 | process.env.OPENSHIFT_APP_NAME; 18 | } 19 | 20 | function open(callback) { 21 | mongoClient.connect('mongodb://' + CONNECTION_STRING, function(err, db) { 22 | if (err) { 23 | console.log('Error connecting to the database: ' + err); 24 | throw err; 25 | } 26 | db.collection(COLLECTION_NAME, {strict:true}, function(err, collection) { 27 | if (err) { 28 | console.log('Error retrieving collection: ' + err); 29 | throw err; 30 | } 31 | return callback(null, collection); 32 | }); 33 | }); 34 | } 35 | 36 | exports.open = open; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bibleapi-rest", 3 | "author": "Ruslan Kazakov", 4 | "description": "Bible API", 5 | "license": "MIT", 6 | "version": "0.2.0", 7 | "main": "index.js", 8 | "scripts": { 9 | "start": "babel-node index.js", 10 | "dev": "nodemon --exec babel-node index.js", 11 | "test": "NODE_ENV=test mocha --recursive --reporter spec --compilers js:babel-register tests" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/rkazakov/bibleapi-rest.git" 16 | }, 17 | "keywords": [ 18 | "bible", 19 | "api", 20 | "rest" 21 | ], 22 | "bugs": { 23 | "url": "https://github.com/bibleapi/bibleapi-rest/issues" 24 | }, 25 | "homepage": "https://github.com/bibleapi/bibleapi-rest#readme", 26 | "dependencies": { 27 | "bible-passage-reference-parser": "^2.0.1", 28 | "dotenv": "^4.0.0", 29 | "express": "^4.14.0", 30 | "express-winston": "^2.4.0", 31 | "helmet": "^3.8.0", 32 | "lodash": "^4.17.4", 33 | "mongodb": "^2.2.30", 34 | "winston": "^2.3.1" 35 | }, 36 | "devDependencies": { 37 | "babel-cli": "^6.18.0", 38 | "babel-plugin-transform-class-properties": "^6.24.1", 39 | "babel-plugin-transform-runtime": "^6.23.0", 40 | "babel-preset-es2015": "^6.18.0", 41 | "babel-preset-stage-0": "^6.16.0", 42 | "babel-register": "^6.18.0", 43 | "chai": "^4.1.1", 44 | "chai-http": "^3.0.0", 45 | "mocha": "^3.5.0", 46 | "nodemon": "^1.11.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/controllers/passage.controller.js: -------------------------------------------------------------------------------- 1 | import mongoDb from '../db/mongo'; 2 | import parser from '../parser/parser'; 3 | import queryMapper from '../db/queryMapper'; 4 | import BaseController from './base.controller'; 5 | 6 | class PassageController extends BaseController { 7 | 8 | find = async (req, res, next) => { 9 | try { 10 | const reference = req.params.reference; 11 | const parsedReference = parser.parse(reference); 12 | const passageQuery = queryMapper.mapQuery(parsedReference); 13 | const result = await mongoDb(req.app.mongoPool).getVerses(passageQuery); 14 | res.json( 15 | { 16 | verses: result 17 | } 18 | ); 19 | } catch(err) { 20 | next(err); 21 | } 22 | }; 23 | 24 | findRandomProverb = async (req, res, next) => { 25 | try { 26 | const randomChapter = Math.floor(Math.random() * 30) + 1; 27 | const randomVerse = Math.floor(Math.random() * 20) + 1; 28 | const reference = ['Prov ', randomChapter, ':', randomVerse].join(''); 29 | console.log(reference); 30 | const parsedReference = parser.parse(reference); 31 | const passageQuery = queryMapper.mapQuery(parsedReference); 32 | const result = await mongoDb(req.app.mongoPool).getVerses(passageQuery); 33 | res.json( 34 | { 35 | verses: result 36 | } 37 | ); 38 | } catch(err) { 39 | next(err); 40 | } 41 | }; 42 | 43 | } 44 | 45 | export default new PassageController(); -------------------------------------------------------------------------------- /tests/v1/server.spec.js.js: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'test'; 2 | 3 | var server = require('../../server/server'); 4 | 5 | var chai = require('chai'); 6 | var chaiHttp = require('chai-http'); 7 | 8 | var should = chai.should(); 9 | chai.use(chaiHttp); 10 | 11 | describe.skip('Passages', function() { 12 | it('should list 1 verse on calling fetchBcv', function(done) { 13 | chai.request(server) 14 | .get('/api/v1/Gen1:1') 15 | .end(function(err, res) { 16 | res.should.have.status(200); 17 | res.should.be.json; 18 | res.body.should.be.a('object'); 19 | res.body.should.have.property('verses'); 20 | res.body.should.have.property('text'); 21 | res.body.verses[0].book.should.equal(1); 22 | res.body.verses[0].chapter.should.equal(1); 23 | res.body.verses[0].verse.should.equal(1); 24 | done(); 25 | }); 26 | }); 27 | 28 | it('should list 2 verses on calling fetchRange', function(done) { 29 | chai.request(server) 30 | .get('/api/v1/Gen1:1-2') 31 | .end(function(err, res) { 32 | res.should.have.status(200); 33 | res.should.be.json; 34 | res.body.should.be.a('object'); 35 | res.body.should.have.property('verses'); 36 | res.body.should.have.property('text'); 37 | res.body.should.have.property('verses').with.length(2); 38 | // 1st verse 39 | res.body.verses[0].book.should.equal(1); 40 | res.body.verses[0].chapter.should.equal(1); 41 | res.body.verses[0].verse.should.equal(1); 42 | // 2nd verse 43 | res.body.verses[1].book.should.equal(1); 44 | res.body.verses[1].chapter.should.equal(1); 45 | res.body.verses[1].verse.should.equal(2); 46 | done(); 47 | }); 48 | }); 49 | 50 | }); 51 | -------------------------------------------------------------------------------- /server/server.js: -------------------------------------------------------------------------------- 1 | import helmet from 'helmet'; 2 | import express from 'express'; 3 | import winston from 'winston'; 4 | import { MongoClient } from 'mongodb'; 5 | import expressWinston from 'express-winston'; 6 | import routes from './routes'; 7 | 8 | const host = `${process.env.MONGODB_REPLICA_NODE_1},${process.env.MONGODB_REPLICA_NODE_2},${process.env.MONGODB_REPLICA_NODE_3}`; 9 | const replicaSet = process.env.MONGODB_REPLICA_SET; 10 | const useSsl = process.env.MONGODB_SSL; 11 | const db = process.env.MONGODB_DATABASE; 12 | const user = process.env.MONGODB_USERNAME; 13 | const pass = process.env.MONGODB_PASSWORD; 14 | const mongoDbUrl = `mongodb://${user}:${pass}@${host}/${db}?ssl=${useSsl}&replicaSet=${replicaSet}&authSource=admin`; 15 | 16 | const app = express(); 17 | 18 | MongoClient.connect(mongoDbUrl, (error, mongoPool) => { 19 | 20 | if (error) { 21 | console.error(error); 22 | return; 23 | } 24 | 25 | app.mongoPool = mongoPool; 26 | 27 | // Helmet helps securing Express.js apps by setting various HTTP headers 28 | // https://github.com/helmetjs/helmet 29 | app.use(helmet()); 30 | 31 | // Request logger 32 | // https://github.com/bithavoc/express-winston 33 | app.use(expressWinston.logger({ 34 | transports: [ 35 | new winston.transports.Console({ 36 | json: true, 37 | colorize: true 38 | }) 39 | ], 40 | meta: true, 41 | msg: "HTTP {{req.method}} {{req.url}}", 42 | expressFormat: true, 43 | colorize: true, 44 | ignoreRoute: function (req, res) { return false; } 45 | })); 46 | 47 | // Mount public routes 48 | app.use('/', express.static(`${__dirname}/public`)); 49 | 50 | const apiPrefix = '/api/'; 51 | app.use(apiPrefix, routes); 52 | 53 | app.use(expressWinston.errorLogger({ 54 | transports: [ 55 | new winston.transports.Console({ 56 | json: true, 57 | colorize: true 58 | }) 59 | ] 60 | })); 61 | 62 | const nodePort = 3333; 63 | app.listen(nodePort, () => { 64 | console.info(`Restful API server is listening on port ${nodePort}`); 65 | }); 66 | 67 | }); 68 | 69 | export default app; -------------------------------------------------------------------------------- /server/db/queryMapper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { bcv_parser as BcvParcer } from 'bible-passage-reference-parser/js/en_bcv_parser'; 3 | 4 | const mapQuery = (passages) => { 5 | const bcv = new BcvParcer; 6 | const translationInfo = bcv.translation_info(''); 7 | 8 | const queries = _.map(passages, (passage) => { 9 | 10 | if(passage.type === 'bc') { // Gen 1 11 | return { book_id: passage.start.b, chapter: passage.start.c } 12 | } 13 | else if(passage.type === 'bcv') { // Gen 1:4 14 | return { book_id: passage.start.b, chapter: passage.start.c, verse: passage.start.v } 15 | } 16 | else if(passage.type === 'cv') { // Gen 1:4,6:9 17 | return { book_id: passage.start.b, chapter: passage.start.c, verse: passage.start.v } 18 | } 19 | else if(passage.type === 'range') { 20 | // multiple books in one passage 21 | if (passage.start.b != passage.end.b) { 22 | console.error('One passages cannot contain multiple books!'); 23 | return null; 24 | } 25 | // multiple chapters of one book 26 | else if(passage.end.c - passage.start.c > 0) { 27 | const chapters = _.range(passage.start.c, passage.end.c + 1); 28 | const queries = _.map(chapters, (chapter) => { 29 | let startVerse = 1; 30 | let endVerse = translationInfo.chapters[passage.start.b][chapter-1]; 31 | 32 | if (chapters[0] === chapter) { 33 | startVerse = passage.start.v; 34 | } 35 | if (chapters[_.size(chapters)-1] === chapter) { 36 | endVerse = passage.end.v; 37 | } 38 | return { book_id: passage.start.b, chapter, verse: {$gte: startVerse, $lte: endVerse} } 39 | }); 40 | return {$or: queries}; 41 | } 42 | // one chapter passage 43 | else if(passage.start.c === passage.end.c) { 44 | return { 45 | book_id: passage.start.b, 46 | chapter: passage.start.c, 47 | verse: {$gte: passage.start.v, $lte: passage.end.v} 48 | }; 49 | } 50 | 51 | } 52 | 53 | }); 54 | 55 | return { $and: queries }; 56 | }; 57 | 58 | export default { mapQuery } -------------------------------------------------------------------------------- /server/v1/model.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash'); 4 | const mongo = require('./mongo'); 5 | const fetcher = require('./fetcher'); 6 | const error = require('./error'); 7 | 8 | var bcv_parser = require("bible-passage-reference-parser/js/en_bcv_parser").bcv_parser; 9 | var bcv = new bcv_parser; 10 | 11 | function reformatResults(items, hasMultipleTranslations) { 12 | let result = { 13 | verses: items 14 | }; 15 | 16 | if (!hasMultipleTranslations) { 17 | let text = []; 18 | _(items).forEach(function(item) { 19 | text.push(item.text); 20 | }); 21 | 22 | result.text = text.join(' '); 23 | } 24 | 25 | return result; 26 | } 27 | 28 | function displayResults(res, mongoQuery, hasMultipleTranslations) { 29 | if (mongoQuery.length > 0) { 30 | mongo.open(function(err, collection) { 31 | collection.find({ $or: mongoQuery }, { _id: 0 }) 32 | .sort({chapter: 1, verse: 1}) 33 | .toArray(function(err, items) { 34 | res.charSet('utf-8'); 35 | if(items.length > 0) { 36 | res.send(reformatResults(items, hasMultipleTranslations)); 37 | } 38 | else { 39 | res.send(error.onError('Bible passage is not found.')); 40 | } 41 | }); 42 | }); 43 | } 44 | else { 45 | res.send(error.onError('Bible passage is not found.')); 46 | } 47 | } 48 | 49 | exports.parsePassage = function(req, res) { 50 | let entities = bcv.parse(req.params.passage).entities; 51 | if (entities.length < 1) { 52 | res.send(error.onError('Passage not found.')); 53 | } 54 | 55 | _(entities).each(function(entity) { 56 | // passage contains whole book 57 | if (entity.type === 'b') { 58 | res.send(error.onError('Please specify passage chapter.')); 59 | } 60 | // whole chapter or verse 61 | else if (entity.type === 'bc' || entity.type === 'bcv') { 62 | // bcv has only one passage 63 | const translations = entity.passages[0].translations ? entity.passages[0].translations : []; 64 | fetcher.fetchBcv(entity.passages[0], entity.type, function(err, result) { 65 | let hasMultipleTranslations = false; 66 | if (entity.type === 'bcv' && translations.length > 1) { 67 | hasMultipleTranslations = true; 68 | } 69 | displayResults(res, result, hasMultipleTranslations); 70 | }); 71 | } // range of verses 72 | else if (entity.type === 'range') { 73 | // range has only one passage 74 | fetcher.fetchRange(entity.passages[0], function(err, result) { 75 | displayResults(res, result, false); 76 | }); 77 | } // sequence of passages 78 | else if(entity.type === 'sequence') { 79 | _(entity.passages).each(function(passage) { 80 | // passage sequence includes bcv 81 | if (passage.type === 'bcv' || passage.type === 'integer') { 82 | fetcher.fetchBcv(passage, 'bcv', function(err, result) { 83 | displayResults(res, result, false); 84 | }); 85 | } // passage sequence includes range 86 | else if(passage.type === 'range') { 87 | fetcher.fetchRange(passage, function(err, result) { 88 | displayResults(res, result, false); 89 | }); 90 | } 91 | }); 92 | } 93 | }); 94 | }; 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Bible API REST 2 | 3 | [![Known Vulnerabilities](https://snyk.io/test/github/bibleapi/bibleapi-rest/badge.svg)](https://snyk.io/test/github/bibleapi/bibleapi-rest) 4 | 5 | Bible API RESTful web service developed with Node.js 6 | 7 | ---- 8 | 9 | ## API 10 | 11 | ### Single passage 12 | 13 | ###### Single verse 14 | [http://localhost:3333/api/Gen1:1](http://localhost:3333/api/v1/Gen1:1) 15 | 16 | ``` 17 | { 18 | verses: [ 19 | { 20 | book: 1, 21 | chapter: 1, 22 | verse: 1, 23 | text: "In the beginning God created the heaven and the earth.", 24 | tran: "KJV", 25 | bookRef: "Gen", 26 | bookName: "Genesis" 27 | } 28 | ], 29 | text: "In the beginning God created the heaven and the earth." 30 | } 31 | ``` 32 | 33 | ###### Single chapter 34 | [http://localhost:3333/api/Gen1](http://localhost:3333/api/Gen1) 35 | 36 | ### Single passage range 37 | 38 | ###### Single verse range 39 | [http://localhost:3333/api/Gen1:1-5](http://localhost:3333/api/Gen1:1-5) 40 | 41 | ###### Single chapter range 42 | [http://localhost:3333/api/Gen1-3](http://localhost:3333/api/Gen1-3) 43 | 44 | ###### Single chapter and verses range 45 | [http://localhost:3333/api/Gen1:1-2:5](http://localhost:3333/api/Gen1:1-2:5) 46 | 47 | ### Multiple passages 48 | [http://localhost:3333/api/Gen1;Gen2:1-3:5](http://localhost:3333/api/Gen1;Gen2:1-3:5) 49 | 50 | ### Supported translations 51 | - KJV 52 | - ASV 53 | 54 | #### Single translation 55 | [http://localhost:3333/api/Gen1:1ASV](http://localhost:3333/api/Gen1:1ASV) 56 | 57 | ``` 58 | { 59 | verses: [ 60 | { 61 | book: 1, 62 | chapter: 1, 63 | verse: 1, 64 | text: "In the beginning God created the heaven and the earth.", 65 | tran: "ASV", 66 | bookRef: "Gen", 67 | bookName: "Genesis" 68 | } 69 | ], 70 | text: "In the beginning God created the heaven and the earth." 71 | } 72 | ``` 73 | 74 | #### Multiple translations 75 | [http://localhost:3333/api/Gen1:1KJV;ASV](http://localhost:3333/api/Gen1:1KJV;ASV) 76 | 77 | ``` 78 | { 79 | verses: [ 80 | { 81 | book: 1, 82 | chapter: 1, 83 | verse: 1, 84 | text: "In the beginning God created the heaven and the earth.", 85 | tran: "KJV", 86 | bookRef: "Gen", 87 | bookName: "Genesis" 88 | }, 89 | { 90 | book: 1, 91 | chapter: 1, 92 | verse: 1, 93 | text: "In the beginning God created the heavens and the earth.", 94 | tran: "ASV", 95 | bookRef: "Gen", 96 | bookName: "Genesis" 97 | } 98 | ] 99 | } 100 | ``` 101 | 102 | ### Meta data 103 | [http://localhost:3333/api/meta/KJV](http://localhost:3333/api/meta/KJV) 104 | 105 | ## Development 106 | 107 | ### Prerequisites 108 | - [MongoDB](https://www.mongodb.org) 109 | - [Node.js](https://www.nodejs.org) 110 | 111 | ### Setup 112 | 113 | #### Database 114 | 115 | Import JSON files from this repo: 116 | https://github.com/dev4christ/usfm2json/tree/master/json 117 | 118 | ##### KJV 119 | > mongoimport --db bibleapi --collection bible --type json --file kjv.json 120 | 121 | ##### ASV 122 | > mongoimport --db bibleapi --collection bible --type json --file asv.json 123 | 124 | #### NPM Modules 125 | > npm install 126 | 127 | ### Run 128 | > gulp 129 | 130 | ### Test 131 | > mocha 132 | 133 | 134 | ## Demo 135 | > - [http://bibleapi.ws/Gen1](http://bibleapi.ws/Gen1) 136 | > - [http://bibleapi.ws/Gen1:1](http://bibleapi.ws/Gen1:1) 137 | -------------------------------------------------------------------------------- /server/v1/fetcher.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const bcv_parser = require("bible-passage-reference-parser/js/en_bcv_parser").bcv_parser; 6 | const bcv = new bcv_parser; 7 | 8 | const DEFAULT_TRANSLATION = 'KJV'; 9 | 10 | /* 11 | * Fetches singular chapter or singular verse 12 | */ 13 | exports.fetchBcv = function fetchBcv(passage, type, callback) { 14 | let mongoQuery = []; 15 | const translations = passage.translations; 16 | 17 | if (type === 'bc') { 18 | let translation = translations != null ? translations[0] : DEFAULT_TRANSLATION; 19 | mongoQuery.push({'tran': translation, 'bookRef': passage.start.b, 'chapter': passage.start.c}); 20 | } 21 | else if (type === 'bcv') { 22 | if (translations != null) { 23 | _(translations).each(function(translation) { 24 | mongoQuery.push({'tran': translation.osis, 'bookRef': passage.start.b, 'chapter': passage.start.c, 'verse': passage.start.v}); 25 | }); 26 | } 27 | else { 28 | mongoQuery.push({'tran': DEFAULT_TRANSLATION, 'bookRef': passage.start.b, 'chapter': passage.start.c, 'verse': passage.start.v}); 29 | } 30 | } 31 | 32 | return callback(null, mongoQuery); 33 | }; 34 | 35 | exports.fetchRange = function fetchRange(passage, callback) { 36 | var mongoQuery = []; 37 | var translationInfo = bcv.translation_info(''); 38 | 39 | var pTranslation = DEFAULT_TRANSLATION; 40 | if (passage.translations != null) { 41 | pTranslation = passage.translations[0].osis; 42 | } 43 | 44 | // passage of the same book 45 | if (passage.start.b === passage.end.b) { 46 | // passage of one chapter 47 | if (passage.start.c === passage.end.c) { 48 | mongoQuery.push({'tran': pTranslation, 'bookRef': passage.start.b, 'chapter': passage.start.c, 'verse': {$gte:passage.start.v, $lte:passage.end.v}}); 49 | } 50 | else { // passage of many chapters 51 | var chapters = []; 52 | for (var ch=passage.start.c; ch<=passage.end.c; ch++) { 53 | var startVerse = 1; 54 | var endVerse = translationInfo.chapters[passage.start.b][ch-1]; 55 | 56 | if (ch === passage.start.c) { 57 | if (passage.start.v) { 58 | startVerse = passage.start.v; 59 | } 60 | } 61 | if (ch === passage.end.c) { 62 | if (passage.end.v) { 63 | endVerse = passage.end.v; 64 | } 65 | } 66 | 67 | chapters.push({'tran':pTranslation, 'bookRef':passage.start.b, 'chapter':ch, 'verse':{$gte:startVerse, $lte:endVerse}}); 68 | } 69 | 70 | mongoQuery.push({$or: chapters}); 71 | } 72 | } // passage of many books 73 | else { 74 | var chapters = []; 75 | var startBook = translationInfo.order[passage.start.b]; 76 | var endBook = translationInfo.order[passage.end.b]; 77 | 78 | for (var b=startBook; b<=endBook; b++) { 79 | var bookRef = translationInfo.books[b-1]; 80 | 81 | // passage includes the whole book in the middle 82 | if (b != startBook && b != endBook) { 83 | chapters.push({'tran':pTranslation, 'bookRef':bookRef}); 84 | } 85 | else { // other half-books parts 86 | var startChapter = 1; 87 | var endChapter = translationInfo.chapters[bookRef].length; 88 | 89 | if (b === startBook) { 90 | startChapter = passage.start.c; 91 | } 92 | if (b === endBook) { 93 | endChapter = passage.end.c; 94 | } 95 | 96 | for (var ch=startChapter; ch<=endChapter; ch++) { 97 | var startVerse = 1; 98 | var endVerse = translationInfo.chapters[bookRef][ch-1]; 99 | 100 | // first chapter, first verse in range 101 | if (b === startBook && ch === startChapter) { 102 | startVerse = passage.start.v; 103 | // TODO: fix verse 104 | chapters.push({'tran':pTranslation, 'bookRef':bookRef, 'chapter':startChapter, 'verse':{$gte:startVerse, $lte:endVerse}}); 105 | } // last chapter, last verse in range 106 | else if (b === endBook && ch === endChapter) { 107 | endVerse = passage.end.v; 108 | // TODO: fix verse 109 | chapters.push({'tran':pTranslation, 'bookRef':bookRef, 'chapter':ch, 'verse':{$gte:startVerse, $lte:endVerse}}); 110 | } 111 | else { // other whole chapters in range 112 | chapters.push({'tran':pTranslation, 'bookRef':bookRef, 'chapter':ch}); 113 | } 114 | } 115 | } 116 | } 117 | 118 | mongoQuery.push({$or: chapters}); 119 | } 120 | return callback(null, mongoQuery); 121 | }; 122 | -------------------------------------------------------------------------------- /tests/v1/fetcher.spec.js: -------------------------------------------------------------------------------- 1 | //var sinon = require('sinon'); 2 | 3 | const bcv_parser = require("bible-passage-reference-parser/js/en_bcv_parser").bcv_parser; 4 | const bcv = new bcv_parser; 5 | var fetcher = require('../../server/v1/fetcher'); 6 | 7 | var chai = require('chai'); 8 | var should = chai.should(); 9 | 10 | describe.skip('fetchBcv', function() { 11 | it('should get an empty object from fetchBcv', function(done) { 12 | var passage = 'Gen'; 13 | var entity = bcv.parse(passage).entities[0]; 14 | fetcher.fetchBcv(entity.passages[0], entity.type, function(err, result) { 15 | result.should.be.a('array'); 16 | result.should.have.length(0); 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should get a chapter object from fetchBcv', function(done) { 22 | var passage = 'Gen1'; 23 | var entity = bcv.parse(passage).entities[0]; 24 | fetcher.fetchBcv(entity.passages[0], entity.type, function(err, result) { 25 | result.should.be.a('array'); 26 | result[0].should.have.property('bookRef'); 27 | result[0].bookRef.should.equal('Gen'); 28 | result[0].should.have.property('chapter'); 29 | result[0].chapter.should.equal(1); 30 | done(); 31 | }); 32 | }); 33 | 34 | it('should get a verse object from fetchBcv', function(done) { 35 | var passage = 'Gen1:1'; 36 | var entity = bcv.parse(passage).entities[0]; 37 | fetcher.fetchBcv(entity.passages[0], entity.type, function(err, result) { 38 | result.should.be.a('array'); 39 | result[0].should.have.property('bookRef'); 40 | result[0].bookRef.should.equal('Gen'); 41 | result[0].should.have.property('chapter'); 42 | result[0].chapter.should.equal(1); 43 | result[0].should.have.property('verse'); 44 | result[0].verse.should.equal(1); 45 | done(); 46 | }); 47 | }); 48 | }); 49 | 50 | describe.skip('fetchRange', function() { 51 | it('should get a book object from fetchRange', function(done) { 52 | var passage = 'Gen;Ps'; 53 | var entity = bcv.parse(passage).entities[0]; 54 | 55 | entity.passages.should.have.length(2); 56 | 57 | fetcher.fetchRange(entity.passages[0], function(err, result) { 58 | result.should.be.a('array'); 59 | result[0].should.have.property('bookRef'); 60 | result[0].bookRef.should.equal('Gen'); 61 | }); 62 | 63 | fetcher.fetchRange(entity.passages[1], function(err, result) { 64 | result.should.be.a('array'); 65 | result[0].should.have.property('bookRef'); 66 | result[0].bookRef.should.equal('Ps'); 67 | }); 68 | 69 | done(); 70 | }); 71 | 72 | it('should get a chapter object from fetchRange', function(done) { 73 | var passage = 'Gen1;Ps1'; 74 | var entity = bcv.parse(passage).entities[0]; 75 | 76 | entity.passages.should.have.length(2); 77 | 78 | fetcher.fetchRange(entity.passages[0], function(err, result) { 79 | result.should.be.a('array'); 80 | result[0].should.have.property('bookRef'); 81 | result[0].bookRef.should.equal('Gen'); 82 | result[0].chapter.should.equal(1); 83 | }); 84 | 85 | fetcher.fetchRange(entity.passages[1], function(err, result) { 86 | result.should.be.a('array'); 87 | result[0].should.have.property('bookRef'); 88 | result[0].bookRef.should.equal('Ps'); 89 | result[0].chapter.should.equal(1); 90 | }); 91 | 92 | done(); 93 | }); 94 | 95 | it('should get a verse object from fetchRange', function(done) { 96 | var passage = 'Gen1:2;Ps1:3'; 97 | var entity = bcv.parse(passage).entities[0]; 98 | 99 | entity.passages.should.have.length(2); 100 | 101 | fetcher.fetchRange(entity.passages[0], function(err, result) { 102 | result.should.be.a('array'); 103 | result[0].should.have.property('bookRef'); 104 | result[0].bookRef.should.equal('Gen'); 105 | result[0].chapter.should.equal(1); 106 | result[0].verse.$gte.should.equal(2); 107 | result[0].verse.$lte.should.equal(2); 108 | }); 109 | 110 | fetcher.fetchRange(entity.passages[1], function(err, result) { 111 | result.should.be.a('array'); 112 | result[0].should.have.property('bookRef'); 113 | result[0].bookRef.should.equal('Ps'); 114 | result[0].chapter.should.equal(1); 115 | result[0].verse.$gte.should.equal(3); 116 | result[0].verse.$lte.should.equal(3); 117 | }); 118 | 119 | done(); 120 | }); 121 | 122 | it('should get a passage object from fetchRange', function (done) { 123 | var passage = 'Gen1:2-7;Ps1:3-6'; 124 | var entity = bcv.parse(passage).entities[0]; 125 | 126 | entity.passages.should.have.length(2); 127 | 128 | fetcher.fetchRange(entity.passages[0], function (err, result) { 129 | result.should.be.a('array'); 130 | result[0].should.have.property('bookRef'); 131 | result[0].bookRef.should.equal('Gen'); 132 | result[0].chapter.should.equal(1); 133 | result[0].verse.$gte.should.equal(2); 134 | result[0].verse.$lte.should.equal(7); 135 | }); 136 | 137 | fetcher.fetchRange(entity.passages[1], function (err, result) { 138 | result.should.be.a('array'); 139 | result[0].should.have.property('bookRef'); 140 | result[0].bookRef.should.equal('Ps'); 141 | result[0].chapter.should.equal(1); 142 | result[0].verse.$gte.should.equal(3); 143 | result[0].verse.$lte.should.equal(6); 144 | }); 145 | 146 | done(); 147 | }); 148 | 149 | it('should get a passage object from fetchRange, if the passage is "Gen1:2-33;Ps1:3-8"', function (done) { 150 | var passage = 'Gen1:2-33;Ps1:3-8'; 151 | var entity = bcv.parse(passage).entities[0]; 152 | 153 | entity.passages.should.have.length(2); 154 | 155 | fetcher.fetchRange(entity.passages[0], function (err, result) { 156 | result.should.be.a('array'); 157 | result[0].should.have.property('bookRef'); 158 | result[0].bookRef.should.equal('Gen'); 159 | result[0].chapter.should.equal(1); 160 | result[0].verse.$gte.should.equal(2); 161 | result[0].verse.$lte.should.equal(31); 162 | }); 163 | 164 | fetcher.fetchRange(entity.passages[1], function (err, result) { 165 | result.should.be.a('array'); 166 | result[0].should.have.property('bookRef'); 167 | result[0].bookRef.should.equal('Ps'); 168 | result[0].chapter.should.equal(1); 169 | result[0].verse.$gte.should.equal(3); 170 | result[0].verse.$lte.should.equal(6); 171 | }); 172 | 173 | done(); 174 | }); 175 | 176 | it('should get a passage object from fetchRange, if the passage contains few chapters', function (done) { 177 | var passage = 'Gen1-2;Ps2-3'; 178 | var entity = bcv.parse(passage).entities[0]; 179 | 180 | entity.passages.should.have.length(2); 181 | 182 | fetcher.fetchRange(entity.passages[0], function (err, result) { 183 | result.should.be.a('array'); 184 | result[0].$or[0].should.have.property('bookRef'); 185 | result[0].$or[0].bookRef.should.equal('Gen'); 186 | result[0].$or[0].chapter.should.equal(1); 187 | result[0].$or[0].verse.$gte.should.equal(1); 188 | result[0].$or[0].verse.$lte.should.equal(31); 189 | 190 | result[0].$or[1].should.have.property('bookRef'); 191 | result[0].$or[1].bookRef.should.equal('Gen'); 192 | result[0].$or[1].chapter.should.equal(2); 193 | result[0].$or[1].verse.$gte.should.equal(1); 194 | result[0].$or[1].verse.$lte.should.equal(25); 195 | }); 196 | 197 | fetcher.fetchRange(entity.passages[1], function (err, result) { 198 | result.should.be.a('array'); 199 | result[0].$or[0].should.have.property('bookRef'); 200 | result[0].$or[0].bookRef.should.equal('Ps'); 201 | result[0].$or[0].chapter.should.equal(2); 202 | result[0].$or[0].verse.$gte.should.equal(1); 203 | result[0].$or[0].verse.$lte.should.equal(12); 204 | 205 | result[0].$or[1].should.have.property('bookRef'); 206 | result[0].$or[1].bookRef.should.equal('Ps'); 207 | result[0].$or[1].chapter.should.equal(3); 208 | result[0].$or[1].verse.$gte.should.equal(1); 209 | result[0].$or[1].verse.$lte.should.equal(8); 210 | }); 211 | 212 | done(); 213 | }); 214 | }); 215 | 216 | describe.skip('fetchTranslation', function() { 217 | it('should get a single translation', function(done) { 218 | var passage = 'Gen1:1;ASV'; 219 | var entity = bcv.parse(passage).entities[0]; 220 | fetcher.fetchBcv(entity.passages[0], entity.type, function(err, result) { 221 | result.should.be.a('array'); 222 | result[0].should.have.property('bookRef'); 223 | result[0].bookRef.should.equal('Gen'); 224 | result[0].should.have.property('chapter'); 225 | result[0].chapter.should.equal(1); 226 | result[0].should.have.property('verse'); 227 | result[0].verse.should.equal(1); 228 | result[0].should.have.property('tran'); 229 | result[0].tran.should.equal("ASV"); 230 | }); 231 | 232 | done(); 233 | }); 234 | 235 | it('should get a both translation', function(done) { 236 | var passage = 'Gen1:1;ASV;KJV'; 237 | var entity = bcv.parse(passage).entities[0]; 238 | 239 | 240 | fetcher.fetchBcv(entity.passages[0], entity.type, function(err, result) { 241 | result.should.be.a('array'); 242 | 243 | result[0].should.have.property('bookRef'); 244 | result[0].bookRef.should.equal('Gen'); 245 | result[0].should.have.property('chapter'); 246 | result[0].chapter.should.equal(1); 247 | result[0].should.have.property('verse'); 248 | result[0].verse.should.equal(1); 249 | result[0].should.have.property('tran'); 250 | result[0].tran.should.equal("ASV"); 251 | }); 252 | 253 | fetcher.fetchBcv(entity.passages[0], entity.type, function(err, result) { 254 | result.should.be.a('array'); 255 | 256 | result[1].should.have.property('bookRef'); 257 | result[1].bookRef.should.equal('Gen'); 258 | result[1].should.have.property('chapter'); 259 | result[1].chapter.should.equal(1); 260 | result[1].should.have.property('verse'); 261 | result[1].verse.should.equal(1); 262 | result[1].should.have.property('tran'); 263 | result[1].tran.should.equal("KJV"); 264 | }); 265 | done(); 266 | }); 267 | }); 268 | 269 | --------------------------------------------------------------------------------