├── .github └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── cli.js ├── index.js ├── package-lock.json ├── package.json └── test ├── fixtures ├── commonjs │ └── index.js ├── config.js ├── config.json └── config │ └── couchdb-configure │ └── foo └── index.js /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-18.04 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v2 13 | with: 14 | fetch-depth: 0 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 14.17 19 | - name: Install dependencies 20 | run: npm ci 21 | - name: Release 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 25 | run: npx semantic-release 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | cluster: 7 | runs-on: ubuntu-latest 8 | 9 | strategy: 10 | matrix: 11 | node-version: [10.x, 12.x, 14.x, 15.x] 12 | couchdb-version: [2, 3] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: Set up CouchDB 21 | uses: iamssen/couchdb-github-action@master 22 | with: 23 | couchdb-version: ${{ matrix.couchdb-version }} 24 | - run: npm ci 25 | - run: COUCH=http://admin:password@127.0.0.1:5984 npm test 26 | 27 | 28 | single-node: 29 | runs-on: ubuntu-latest 30 | 31 | strategy: 32 | matrix: 33 | node-version: [10.x, 12.x, 14.x, 15.x] 34 | couchdb-version: [1.7] 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: Use Node.js ${{ matrix.node-version }} 39 | uses: actions/setup-node@v1 40 | with: 41 | node-version: ${{ matrix.node-version }} 42 | - name: Set up CouchDB 43 | uses: cobot/couchdb-action@v4 44 | with: 45 | couchdb version: ${{ matrix.couchdb-version }} 46 | - run: npm ci 47 | - run: npm test 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Licensed under the Apache License, Version 2.0 (the "License"); you 2 | may not use this file except in compliance with the License. You may 3 | obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software 8 | distributed under the License is distributed on an "AS IS" BASIS, 9 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 10 | implied. See the License for the specific language governing 11 | permissions and limitations under the License. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CouchDB Configure 2 | Configure CouchDB database server from filesystem. 3 | 4 | ## API 5 | 6 | ```js 7 | configure(url, source[, options], callback) 8 | ``` 9 | 10 | * `url` - CouchDB server URL 11 | * `source` - Can be a Couchapp Directory Tree, JSON file or CommonJS/Node module. Please see [couchdb-compile](https://github.com/jo/couchdb-compile) for in depth information about source handling. 12 | * `callback` - called when done with a `response` object describing the status of all operations. 13 | 14 | ### Example 15 | 16 | ```js 17 | var configure = require('couchdb-configure') 18 | configure('http://localhost:5984', 'couchdb/config.json', function(error, response) { 19 | // here we go 20 | }) 21 | ``` 22 | 23 | ## CLI 24 | 25 | ```sh 26 | couchdb-configure URL [SOURCE] 27 | ``` 28 | 29 | When `SOURCE` is omitted, the current directory will be used. 30 | 31 | 32 | ### Example 33 | 34 | ```sh 35 | couchdb-configure http://localhost:5984 couchdb/config.json 36 | ``` 37 | 38 | ## Tests 39 | ```sh 40 | npm test 41 | ``` 42 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const configure = require('./') 4 | 5 | const args = process.argv.slice(2) 6 | if (!args.length) { 7 | console.log('Usage: \ncouchdb-configure URL [SOURCE]') 8 | process.exit() 9 | } 10 | 11 | const url = args[0] 12 | const source = args[1] || process.cwd() 13 | 14 | configure(url, source, function (error, response) { 15 | if (error) return console.error(error) 16 | 17 | console.log(JSON.stringify(response, null, ' ')) 18 | }) 19 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const nanoOption = require('nano-option') 2 | const async = require('async') 3 | const compile = require('couchdb-compile') 4 | const assert = require('assert') 5 | 6 | module.exports = function configure (url, source, callback) { 7 | const couch = nanoOption(url) 8 | 9 | assert(typeof couch.request === 'function', 10 | 'URL must point to the root of a CouchDB server (not to a database).') 11 | 12 | async.waterfall([ 13 | (done) => { 14 | async.series([ 15 | (done) => { 16 | couch.request({ path: '' }, (error, result) => { 17 | if (error) { return done(error) } 18 | return done(null, result.version) 19 | }) 20 | }, 21 | (done) => { 22 | async.waterfall([ 23 | compile.bind(null, source, { index: true }), 24 | (config, done) => { 25 | const settings = Object.keys(config) 26 | .reduce(function (memo, key) { 27 | if (typeof config[key] !== 'object') return memo 28 | 29 | const section = Object.keys(config[key]) 30 | .map(function (k) { 31 | return { 32 | path: encodeURIComponent(key) + '/' + encodeURIComponent(k), 33 | value: config[key][k].toString() 34 | } 35 | }) 36 | 37 | return memo.concat(section) 38 | }, []) 39 | return done(null, settings) 40 | } 41 | ], done) 42 | } 43 | ], done) 44 | }, 45 | ([version, settings], done) => { 46 | function writeConfig (configPath, done) { 47 | async.map(settings, (setting, done) => { 48 | couch.request({ 49 | method: 'PUT', 50 | path: configPath + setting.path, 51 | body: setting.value 52 | }, function (error, oldValue) { 53 | if (error) return done(error) 54 | 55 | done(null, { 56 | path: setting.path, 57 | value: setting.value, 58 | oldValue: oldValue 59 | }) 60 | }) 61 | }, done) 62 | } 63 | if (version > '2') { 64 | couch.request({ 65 | path: '_membership' 66 | }, function (error, result) { 67 | if (error) { return done(error) } else { 68 | const configTasks = result.all_nodes.map((node) => { 69 | return writeConfig.bind(null, `_node/${node}/_config/`) 70 | }) 71 | return async.series(configTasks, function (error, responses) { 72 | done(error, responses[0]) 73 | }) 74 | } 75 | }) 76 | } else { 77 | return writeConfig('_config/', done) 78 | } 79 | }, 80 | (responses, done) => { 81 | const response = responses.reduce(function (memo, response) { 82 | memo[response.path] = { 83 | ok: true, 84 | value: response.value 85 | } 86 | if (response.oldValue === response.value) { 87 | memo[response.path].unchanged = true 88 | } 89 | 90 | return memo 91 | }, {}) 92 | return done(null, response) 93 | } 94 | ], callback) 95 | } 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "couchdb-configure", 3 | "description": "Configure CouchDB from file or directory.", 4 | "main": "index.js", 5 | "bin": "cli.js", 6 | "scripts": { 7 | "test": "tap --reporter=spec --lines 80 --functions 80 --branches 55 --statements 75 test/index.js", 8 | "pretest": "standard", 9 | "semantic-release": "semantic-release" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/jo/couchdb-configure.git" 14 | }, 15 | "keywords": [ 16 | "couchdb", 17 | "bootstrap" 18 | ], 19 | "author": "Johannes J. Schmidt (http://die-tf.de/)", 20 | "license": "Apache-2.0", 21 | "bugs": { 22 | "url": "https://github.com/jo/couchdb-configure/issues" 23 | }, 24 | "homepage": "https://github.com/jo/couchdb-configure#readme", 25 | "dependencies": { 26 | "async": "^3.2.1", 27 | "couchdb-compile": "^1.11.2", 28 | "nano-option": "^2.2.0" 29 | }, 30 | "devDependencies": { 31 | "nano": "^9.0.5", 32 | "semantic-release": "^19.0.3", 33 | "standard": "^16.0.4", 34 | "tap": "^15.0.10" 35 | }, 36 | "version": "0.0.0-development", 37 | "release": { 38 | "branches": [ 39 | "main" 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/fixtures/commonjs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'couchdb-configure': { 3 | bar: 'baz' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'couchdb-configure': { 3 | baz: 'foo' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "couchdb-configure": { 3 | "foo": "bar" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/config/couchdb-configure/foo: -------------------------------------------------------------------------------- 1 | bar 2 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const test = require('tap').test 2 | const path = require('path') 3 | const async = require('async') 4 | const nano = require('nano') 5 | 6 | const configure = require('../') 7 | 8 | const url = process.env.COUCH || 'http://localhost:5984' 9 | const couch = nano(url) 10 | 11 | // get couchdb version to set _config path 12 | const configPaths = [] 13 | couch.request({ path: '' }, (error, result) => { 14 | if (error) { throw error } 15 | if (result.version > '2') { 16 | couch.request({ path: '_membership' }, (error, result) => { 17 | if (error) { throw error } 18 | for (const node of result.all_nodes) { 19 | configPaths.push(`_node/${node}/_config/`) 20 | } 21 | }) 22 | } else { 23 | configPaths.push('_config/') 24 | } 25 | }) 26 | 27 | function setConfig (options, done) { 28 | async.map(configPaths, (configPath, done) => { 29 | const path = `${configPath}/${options.path}` 30 | couch.request({ ...options, path }, done) 31 | }, done) 32 | } 33 | 34 | // There is an issue with section deletion in CouchDB. 35 | // You cannot delete an entire section: 36 | // $ curl -XDELETE http://localhost:5984/_config/couchdb-bootstrap 37 | // {"error":"method_not_allowed","reason":"Only GET,PUT,DELETE allowed"} 38 | function clear (callback) { 39 | setConfig({ path: 'couchdb-configure' }, function (error, configs) { 40 | if (error) return callback(error) 41 | const config = configs[0] 42 | 43 | async.map(Object.keys(config), function (key, next) { 44 | setConfig({ 45 | method: 'DELETE', 46 | path: `couchdb-configure/${encodeURIComponent(key)}` 47 | }, next) 48 | }, callback) 49 | }) 50 | } 51 | 52 | test('use url', function (t) { 53 | configure(url, path.join(__dirname, 'fixtures', 'config.json'), function (error, responses) { 54 | t.error(error) 55 | t.end() 56 | }) 57 | }) 58 | 59 | test('use url with trailing slash', function (t) { 60 | configure(url + '/', path.join(__dirname, 'fixtures', 'config.json'), function (error, responses) { 61 | t.error(error) 62 | t.end() 63 | }) 64 | }) 65 | 66 | test('use nano object', function (t) { 67 | configure(couch, path.join(__dirname, 'fixtures', 'config.json'), function (error, responses) { 68 | t.error(error) 69 | t.end() 70 | }) 71 | }) 72 | 73 | test('configure from json', function (t) { 74 | clear(function (error) { 75 | t.error(error) 76 | 77 | configure(url, path.join(__dirname, 'fixtures', 'config.json'), function (error, responses) { 78 | t.error(error) 79 | 80 | setConfig({ 81 | path: 'couchdb-configure/foo' 82 | }, function (error, configs) { 83 | const config = configs[0] 84 | t.error(error) 85 | t.equal(config, 'bar') 86 | t.end() 87 | }) 88 | }) 89 | }) 90 | }) 91 | 92 | test('configure from commonjs', function (t) { 93 | clear(function (error) { 94 | t.error(error) 95 | 96 | configure(url, path.join(__dirname, 'fixtures', 'config.js'), function (error, responses) { 97 | t.error(error) 98 | 99 | setConfig({ 100 | path: 'couchdb-configure/baz' 101 | }, function (error, configs) { 102 | const config = configs[0] 103 | t.error(error) 104 | t.equal(config, 'foo') 105 | t.end() 106 | }) 107 | }) 108 | }) 109 | }) 110 | 111 | test('configure from commonjs/index', function (t) { 112 | clear(function (error) { 113 | t.error(error) 114 | 115 | configure(url, path.join(__dirname, 'fixtures', 'commonjs'), function (error, responses) { 116 | t.error(error) 117 | 118 | setConfig({ 119 | path: 'couchdb-configure/bar' 120 | }, function (error, configs) { 121 | const config = configs[0] 122 | t.error(error) 123 | t.equal(config, 'baz') 124 | t.end() 125 | }) 126 | }) 127 | }) 128 | }) 129 | 130 | test('configure from filesystem', function (t) { 131 | clear(function (error) { 132 | t.error(error) 133 | 134 | configure(url, path.join(__dirname, 'fixtures', 'config'), function (error, responses) { 135 | t.error(error) 136 | 137 | setConfig({ 138 | path: 'couchdb-configure/foo' 139 | }, function (error, configs) { 140 | const config = configs[0] 141 | t.error(error) 142 | t.equal(config, 'bar') 143 | t.end() 144 | }) 145 | }) 146 | }) 147 | }) 148 | --------------------------------------------------------------------------------