├── 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 | [](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 |
--------------------------------------------------------------------------------