nconf icon indicating copy to clipboard operation
nconf copied to clipboard

Nconf mutates defaults

Open dvisztempacct opened this issue 5 years ago • 4 comments

For some reason, nconf mutates the value passed to defaults:

$ cat ./index.js
const nconf = require('nconf')
const defaults = {
  foo: {
    bar: 'default-foobar'
  }
}
nconf.argv()
  .env('__')
  .defaults(defaults)
nconf.get()
console.log(defaults.foo.bar == 'default-foobar')
$ node ./index
true
$ foo__bar=nope node ./index
false

dvisztempacct avatar Apr 05 '19 20:04 dvisztempacct

 $ npm ls nconf
[email protected] /home/hdon/nconf-bug
└── [email protected]

dvisztempacct avatar Apr 05 '19 20:04 dvisztempacct

Confirmed the issue here as well.

It's a bug in /lib/nconf/stores/memory.js(200). If the value being merged is an object and a key of the same name doesn't exist in the target, the object reference is assigned rather than a copy. That works fine for a single merge, but once a subsequent merge happens nconf will start rewriting the referenced object.

The same appears to be true for arrays.

The code will need to be modified so that object and array values that don't exist in the target are deep cloned.

ebickle avatar Jun 13 '19 15:06 ebickle

So I think i'm running into this, but want to confirm. I'm creating a defaults object and one of the default values is a function that auto-generates a password.

I'm usiing .env() .argv() and .defaults , and then after all of that, I'm using nconf.use('memory') and then mapping over each key to make sure all the keys are in the memory store. Regardless of what order i call .defaults() it's generating a new password each time, instead of using the value found in the env store.

I'm intended to write a submit a PR for a new format called shellvars, but until then I want to confirm this is due to this bug. Can you check out the code below and confirm?

Toggle to view code
#!/usr/bin/env node
const path = require('path')
require('dotenv').config({
  path: path.join(__dirname, '..', '.env')
})
const nconf = require('nconf')
const generatePassword = require('password-generator')
const fs = require('fs')

const configKeys = ['POSTGRES_DB', 'POSTGRES_USER', 'POSTGRES_PASSWORD', 'DB_HOST', 'DB_PORT', 'sslmode'] const defaults = { 'POSTGRES_DB': 'my_db', 'POSTGRES_USER': 'mydbadmin', 'POSTGRES_PASSWORD': generatePassword(12, false), 'DB_HOST': 'localhost', 'DB_PORT': 5432, 'sslmode': 'disable' } // local env vars into config object, // only loading keys specified, // and casting values to types if possible nconf .defaults(defaults) .env(configKeys) .argv({ parseValues: true })

const config = configKeys.reduce((accumulator,value)=>{ return { ...accumulator, [value]: nconf.get(value) } },{}) //DATABASE_URL used by pgweb const DB_URL = postgres://${config.POSTGRES_USER}:${config.POSTGRES_PASSWORD}@postgres:${config.DB_PORT}/${config.POSTGRES_DB}?sslmode=${config.sslmode} // POSTGRES_URL used by server api const PG_URL = postgres://${config.POSTGRES_USER}:${config.POSTGRES_PASSWORD}@${config.DB_HOST}:5432/${config.POSTGRES_DB}

nconf.use('memory') // map merged configs into a single store and set POSTGRES_URL and DATABASE_URL to that config store Object.keys(configKeys).map( key => nconf.set(key, nconf.get(key))) nconf.set('DATABASE_URL', DB_URL) nconf.set('POSTGRES_URL', PG_URL) // load and save config store to disk in shellvars format nconf.load((err,data) => { if (err) { console.error('error loading config into memory') return } let config = Object.keys(data) .filter(key => key.length >= 5) .reduce((accumulator,value,index) => { return ${accumulator}${value}=${data[value]}\n },'')

fs.writeFile( path.resolve(__dirname, '.env'), config, err => {
  if (err) console.error('Error saving cc-server config', err.code, err.message)
  console.log('cc-server config saved')
})

})

LongLiveCHIEF avatar Apr 07 '20 13:04 LongLiveCHIEF

For those looking for a quick and dirty fix:

Replace:

  if (typeof target[key] !== 'object' || Array.isArray(target[key])) {
    target[key] = value;
    return true;
  }

with

  if (typeof target[key] !== 'object' || Array.isArray(target[key])) {
    target[key] = JSON.parse(JSON.stringify(value));
    return true;
  }

in memory.js

mmadson avatar Aug 22 '24 00:08 mmadson