diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..7fecdaf0 --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +test/homedir/** \ No newline at end of file diff --git a/package.json b/package.json index 24f4e299..1fe78a0d 100644 --- a/package.json +++ b/package.json @@ -57,9 +57,11 @@ "koa-session": "^3.1.0", "ldapjs": "^1.0.0", "lodash": "^4.9.0", + "md5": "^2.2.1", "minimist": "^1.2.0", "nano": "^6.1.5", "nunjucks": "^2.3.0", + "object-hash": "^1.1.4", "passport-facebook": "^2.0.0", "passport-github": "^1.0.0", "passport-google": "^0.3.0", diff --git a/src/config/db.js b/src/config/db.js index 32d2bb30..e42805c3 100644 --- a/src/config/db.js +++ b/src/config/db.js @@ -19,12 +19,25 @@ if (homeDir) { let databaseConfig = {}; try { databaseConfig = require(path.join(databasePath, 'config')); + var designDocNames = {}; + databaseConfig.designDocNames = []; + if (databaseConfig.customDesign && databaseConfig.customDesign.views) { + var views = databaseConfig.customDesign.views; + for (var key in views) { + if (views[key].designDoc) { + designDocNames[key] = views[key].designDoc; + } + } + } + databaseConfig.designDocNames = designDocNames; } catch (e) { // database config is not mandatory } if (!databaseConfig.import) { databaseConfig.import = {}; } + + databaseConfig.designDocNames = databaseConfig.designDocNames || {}; readImportConfig(databasePath, databaseConfig); dbConfig[database] = databaseConfig; } diff --git a/src/design/app.js b/src/design/app.js index 92744b07..c9ca8400 100644 --- a/src/design/app.js +++ b/src/design/app.js @@ -24,9 +24,29 @@ const mapTpl = function (doc) { customMap(doc); }.toString(); -module.exports = function getDesignDoc(custom) { +module.exports = function (custom) { custom = custom || {}; + processViews(custom); + + if (custom.designDoc === 'app') { + return { + _id: constants.DESIGN_DOC_ID, + language: 'javascript', + version: constants.DESIGN_DOC_VERSION, + customVersion: custom.version, + filters: Object.assign({}, custom.filters, filters), + updates: Object.assign({}, custom.updates, updates), + views: Object.assign({}, custom.views, views), + validate_doc_update: validateDocUpdate, + lists: Object.assign({}, custom.lists) + }; + } else { + return custom; + } +}; + +function processViews(custom) { if (custom.views) { for (const viewName in custom.views) { const view = custom.views[viewName]; @@ -45,16 +65,4 @@ module.exports = function getDesignDoc(custom) { } } } - - return { - _id: constants.DESIGN_DOC_ID, - language: 'javascript', - version: constants.DESIGN_DOC_VERSION, - customVersion: custom.version, - filters: Object.assign({}, custom.filters, filters), - updates: Object.assign({}, custom.updates, updates), - views: Object.assign({}, custom.views, views), - validate_doc_update: validateDocUpdate, - lists: Object.assign({}, custom.lists) - }; -}; +} diff --git a/src/index.js b/src/index.js index 529de09f..b1f2c187 100644 --- a/src/index.js +++ b/src/index.js @@ -4,6 +4,7 @@ const debug = require('./util/debug')('main'); const _ = require('lodash'); const extend = require('extend'); const nano = require('nano'); +const objHash = require('object-hash'); const CouchError = require('./util/CouchError'); const constants = require('./constants'); @@ -904,24 +905,102 @@ async function checkSecurity(db, admin) { } async function checkDesignDoc(db, custom) { - debug.trace('check design doc'); + var toUpdate = new Set(); + debug.trace('check _design/app design doc'); const doc = await nanoPromise.getDocument(db, constants.DESIGN_DOC_ID); if (doc === null) { + toUpdate.add(constants.DESIGN_DOC_NAME); debug.trace('design doc missing'); - return await createDesignDoc(db, null, custom); - } - if ( + } else if ( (!doc.version || doc.version < constants.DESIGN_DOC_VERSION) || (custom && typeof custom.version === 'number' && (!doc.customVersion || doc.customVersion < custom.version)) ) { debug.trace('design doc needs update'); - return await createDesignDoc(db, doc._rev, custom); + toUpdate.add(constants.DESIGN_DOC_NAME); + } + + debug.trace('check other custom design docs'); + var viewNames = Object.keys(custom.views); + if (viewNames.indexOf(constants.DESIGN_DOC_NAME) > -1) { + let idx = viewNames.indexOf(constants.DESIGN_DOC_NAME); + viewNames.splice(idx, 1); + } + var designNames = viewNames.map(vn => custom.views[vn].designDoc); + var uniqDesignNames = _.uniq(designNames); + uniqDesignNames = uniqDesignNames.filter(d => d && d !== constants.DESIGN_DOC_NAME); + var designDocs = await Promise.all(uniqDesignNames.map(name => nanoPromise.getDocument(db, `_design/${name}`))); + uniqDesignNames.push(constants.DESIGN_DOC_NAME); + designDocs.push(doc); + + for (var i = 0; i < viewNames.length; i++) { + let view = custom.views[viewNames[i]]; + var hash = objHash(view); + var dbView = getDBView(viewNames[i]); + if (!dbView) { + if (view.designDoc) { + debug.trace(`design doc ${view.designDoc} not found, will create it`); + toUpdate.add(view.designDoc); + } + } else { + + if (dbView.hash !== hash) { + if (view.designDoc) { + debug.trace(`design doc ${view.designDoc} changed, will update it`); + toUpdate.add(view.designDoc); + } + } + } + view.hash = hash; + } + + debug.trace(`Update ${toUpdate.size} design documents`); + for (var designName of toUpdate.keys()) { + var idx = uniqDesignNames.indexOf(designName); + if (idx > -1 || designName === constants.DESIGN_DOC_NAME) { + await createDesignDoc(db, designDocs[idx] && designDocs[idx]._rev || null, getNewDesignDoc(designName)); + } else { + debug.trace('Expected to be unreachable'); + } } + + function getDBView(viewName) { + for (var i = 0; i < designDocs.length; i++) { + if (designDocs[i] && designDocs[i].views && designDocs[i].views[viewName]) { + return designDocs[i].views[viewName]; + } + } + return null; + } + + function getNewDesignDoc(designName) { + if (designName === constants.DESIGN_DOC_NAME) { + var designDoc = Object.assign({}, custom); + } else { + designDoc = {}; + } + designDoc.views = {}; + designDoc.designDoc = designName; + for (var i = 0; i < viewNames.length; i++) { + var viewName = viewNames[i]; + if (custom.views[viewName].designDoc === designName) { + designDoc.views[viewName] = custom.views[viewName]; + } else if (!custom.views[viewName].designDoc && designName === constants.DESIGN_DOC_NAME) { + designDoc.views[viewName] = custom.views[viewName]; + designDoc.version = custom.version; + designDoc.updat; + } + } + designDoc._id = '_design/' + designName; + return designDoc; + } + + // For each design doc, check if any of the associated view has a different hash + } async function createDesignDoc(db, revID, custom) { debug.trace('create design doc'); - const designDoc = getDesignDoc(custom); + var designDoc = getDesignDoc(custom); if (revID) { designDoc._rev = revID; } diff --git a/src/util/nanoPromise.js b/src/util/nanoPromise.js index ccdb79ac..62d7816e 100644 --- a/src/util/nanoPromise.js +++ b/src/util/nanoPromise.js @@ -3,6 +3,7 @@ const constants = require('../constants'); const debug = require('./debug')('nano'); const hasOwnProperty = Object.prototype.hasOwnProperty; +const getConfig = require('../config/config').getConfig; exports.authenticate = function (nano, user, password) { return new Promise((resolve, reject) => { @@ -90,7 +91,9 @@ exports.queryView = function (db, view, params = {}, options = {}) { return new Promise((resolve, reject) => { debug.trace(`queryView ${view}`); cleanOptions(params); - db.view(constants.DESIGN_DOC_NAME, view, params, function (err, body) { + var config = getConfig(db.config.db); + var designDoc = config.designDocNames && config.designDocNames[view] || constants.DESIGN_DOC_NAME; + db.view(designDoc, view, params, function (err, body) { if (err) return reject(err); if (options.onlyValue) { resolve(body.rows.map(row => row.value)); diff --git a/test/basic.js b/test/basic.js index 3c7a1ce3..eb81b7f1 100644 --- a/test/basic.js +++ b/test/basic.js @@ -1,6 +1,8 @@ 'use strict'; const Couch = require('..'); +const nanoPromise = require('../src/util/nanoPromise'); +const assert = require('assert'); process.on('unhandledRejection', function (err) { throw err; @@ -21,3 +23,31 @@ describe('basic initialization tests', function () { }).should.be.rejectedWith('database option is mandatory'); }); }); + +describe('basic initialization with custom design docs', function () { + let couch; + it('should load the design doc files at initialization', function () { + couch = new Couch({database: 'test3'}); + return couch._initPromise.then(function () { + var app = nanoPromise.getDocument(couch._db, '_design/app') + .then(app => { + assert.notEqual(app, null); + assert.ok(app.views.test); + assert.ok(app.filters.abc); + }); + var custom = nanoPromise.getDocument(couch._db, '_design/custom') + .then(custom => { + assert.notEqual(custom, null); + assert.ok(custom.views.testCustom); + }); + + return Promise.all([app, custom]); + }); + }); + + it('should query a custom design document', function () { + return couch.queryEntriesByUser('a@a.com', 'testCustom').then(data => { + data.should.have.length(0); + }); + }); +}); diff --git a/test/homedir/test-import-db/config.js b/test/homedir/test-import-db/config.js index 82e60c29..28298fba 100644 --- a/test/homedir/test-import-db/config.js +++ b/test/homedir/test-import-db/config.js @@ -1,7 +1,6 @@ 'use strict'; module.exports = { - database: 'jdx', defaultEntry: { molecule: function () { return { diff --git a/test/homedir/test3/config.js b/test/homedir/test3/config.js new file mode 100644 index 00000000..f2bcce13 --- /dev/null +++ b/test/homedir/test3/config.js @@ -0,0 +1,26 @@ +'use strict'; + +module.exports = { + customDesign: { + version: 6, + views: { + test: { + map: function (doc) { + emit(doc._id); + } + }, + testCustom: { + map: function (doc) { + emit(doc._id); + }, + designDoc: 'custom' + } + }, + updates: {}, + filters: { + abc: function (doc) { + return doc.$type === 'log'; + } + } + } +};