Skip to content

Commit efb47ef

Browse files
committed
[fix] Config.set
- When using config.set('path.to.value'), it will set 'path.to' and 'path' This fixes an issue where different .set methods produced different mapped results. - Config now better handles Array value.
1 parent c34418d commit efb47ef

7 files changed

+168
-22
lines changed

lib/Configuration.ts

+60-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { merge, isArray, defaults, union } from 'lodash'
22
import { resolve, dirname } from 'path'
33
import { IllegalAccessError, ConfigValueError } from './errors'
44
import { requireMainFilename } from './utils'
5+
import { Core } from './Core'
56

67
// Proxy Handler for get requests to the configuration
78
const ConfigurationProxyHandler: ProxyHandler<Configuration> = {
@@ -22,15 +23,14 @@ const ConfigurationProxyHandler: ProxyHandler<Configuration> = {
2223
export class Configuration extends Map<any, any> {
2324
public immutable: boolean
2425
public env: {}
25-
2626
/**
2727
* Flattens configuration tree
2828
* Recursive
2929
*/
3030
static flattenTree (tree = { }) {
31-
// Try to flatten and fail if circular
31+
const toReturn: { [key: string]: any } = {}
32+
// Try to flatten and fail if unable to resolve circular object
3233
try {
33-
const toReturn: { [key: string]: any } = {}
3434
Object.entries(tree).forEach(([k, v]) => {
3535
// if (typeof v === 'object' && v !== null) {
3636
if (
@@ -43,6 +43,9 @@ export class Configuration extends Map<any, any> {
4343
toReturn[`${k}.${i}`] = val
4444
})
4545
}
46+
else if (!Core.isNotCircular(v)) {
47+
toReturn[k] = v
48+
}
4649
// If the value is a normal object, keep flattening
4750
else {
4851
const flatObject = Configuration.flattenTree(v)
@@ -57,7 +60,10 @@ export class Configuration extends Map<any, any> {
5760
return toReturn
5861
}
5962
catch (err) {
60-
throw new RangeError('Tree is circular, check that there are no circular references in the config')
63+
if (err !== Core.BreakException) {
64+
throw new RangeError('Tree is circular and can not be resolved, check that there are no circular references in the config')
65+
}
66+
return toReturn
6167
}
6268
}
6369

@@ -132,6 +138,43 @@ export class Configuration extends Map<any, any> {
132138
return new Proxy(this, ConfigurationProxyHandler)
133139
}
134140

141+
/**
142+
* Recursively sets the tree values on the config map
143+
*/
144+
private _reverseFlattenSet(key, value) {
145+
if (/\.[0-9a-z]+$/.test(key)) {
146+
const decedent = (key).match(/\.([0-9a-z]+)$/)[1]
147+
const parent = key.replace(/\.[0-9a-z]+$/, '')
148+
const proto = Array.isArray(value) ? [] : {}
149+
const newParentValue = Core.defaultsDeep({[decedent]: value}, this.get(parent) || proto)
150+
super.set(key, value)
151+
// Recursively reverse flatten the set back up the tree
152+
return this._reverseFlattenSet(parent, newParentValue)
153+
}
154+
else {
155+
// This is as high as it goes
156+
return super.set(key, value)
157+
}
158+
}
159+
/**
160+
* Flattens what is being called to .set
161+
*/
162+
private _flattenSet(key, value) {
163+
if (
164+
value instanceof Object
165+
&& typeof value !== 'function'
166+
&& !Array.isArray(value)
167+
) {
168+
// Flatten the new value
169+
const configEntries = Object.entries(Configuration.flattenTree({[key]: value}))
170+
// Set the flat values
171+
configEntries.forEach(([_key, _value]) => {
172+
return super.set(_key, _value)
173+
})
174+
}
175+
// Reverse flatten up the tree
176+
return this._reverseFlattenSet(key, value)
177+
}
135178
/**
136179
* Throws IllegalAccessError if the configuration has already been set to immutable
137180
* and an attempt to set value occurs.
@@ -140,7 +183,7 @@ export class Configuration extends Map<any, any> {
140183
if (this.immutable === true) {
141184
throw new IllegalAccessError('Cannot set properties directly on config. Use .set(key, value) (immutable)')
142185
}
143-
return super.set(key, value)
186+
return this._flattenSet(key, value)
144187
}
145188

146189
/**
@@ -157,7 +200,18 @@ export class Configuration extends Map<any, any> {
157200
}
158201
// If configAction is set to merge, it will default values over the initial config
159202
else if (hasKey && configAction === 'merge') {
160-
this.set(key, defaults(this.get(key), value))
203+
if (Array.isArray(value)) {
204+
// Do Nothing
205+
}
206+
else if (typeof value === 'number') {
207+
// Do Nothing
208+
}
209+
else if (typeof value === 'string') {
210+
// Do Nothing
211+
}
212+
else {
213+
this.set(key, Core.defaultsDeep(this.get(key), value))
214+
}
161215
}
162216
// If configAction is replaceable, and the key already exists, it's ignored completely
163217
// This is because it was set by a higher level app config

lib/Core.ts

+61-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { union, defaultsDeep } from 'lodash'
1+
import { union, defaultsDeep, isArray, toArray, mergeWith } from 'lodash'
22
import { FabrixApp } from './'
33
import * as mkdirp from 'mkdirp'
44
import { Templates } from './'
@@ -44,7 +44,9 @@ export const Errors = {
4444
}
4545

4646
export const Core = {
47-
47+
// An Exception convenience
48+
BreakException: {},
49+
// Methods reserved so that they are not autobound
4850
reservedMethods: [
4951
'app',
5052
'api',
@@ -273,6 +275,63 @@ export const Core = {
273275
}
274276
},
275277

278+
defaultsDeep: (...args) => {
279+
const output = {}
280+
toArray(args).reverse().forEach(function (item) {
281+
mergeWith(output, item, function (objectValue, sourceValue) {
282+
return isArray(sourceValue) ? sourceValue : undefined
283+
})
284+
})
285+
return output
286+
},
287+
288+
collector: (stack, key, val) => {
289+
let idx: any = stack[stack.length - 1].indexOf(key)
290+
try {
291+
const props: any = Object.keys(val)
292+
if (!props.length) {
293+
throw props
294+
}
295+
props.unshift({idx: idx})
296+
stack.push(props)
297+
}
298+
catch (e) {
299+
while (!(stack[stack.length - 1].length - 2)) {
300+
idx = stack[stack.length - 1][0].idx
301+
stack.pop()
302+
}
303+
304+
if (idx + 1) {
305+
stack[stack.length - 1].splice(idx, 1)
306+
}
307+
}
308+
return val
309+
},
310+
311+
isNotCircular: (obj) => {
312+
let stack = [[]]
313+
314+
try {
315+
return !!JSON.stringify(obj, Core.collector.bind(null, stack))
316+
}
317+
catch (e) {
318+
if (e.message.indexOf('circular') !== -1) {
319+
let idx = 0
320+
let path = ''
321+
let parentProp = ''
322+
while (idx + 1) {
323+
idx = stack.pop()[0].idx
324+
parentProp = stack[stack.length - 1][idx]
325+
if (!parentProp) {
326+
break
327+
}
328+
path = '.' + parentProp + path
329+
}
330+
}
331+
return false
332+
}
333+
},
334+
276335
/**
277336
* Create configured paths if they don't exist
278337
*/

package-lock.json

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fabrix/fabrix",
3-
"version": "1.0.7",
3+
"version": "1.0.8",
44
"description": "Strongly Typed Modern Web Application Framework for Node.js",
55
"keywords": [
66
"framework",

test/integration/fabrixapp.test.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -202,12 +202,14 @@ describe('Fabrix', () => {
202202
spools: [ Testspool ]
203203
},
204204
test: {
205-
val: 1
205+
val: 1,
206+
array: [1, 2, 3]
206207
}
207208
}
208209
}
209210
const app = new FabrixApp(def)
210211
assert.equal(app.config.get('test.val'), 1)
212+
assert.deepEqual(app.config.get('test.array'), [1, 2, 3])
211213
assert.equal(app.config.get('test.otherval'), 1)
212214
})
213215

test/integration/testspool.js

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module.exports = class Testspool extends Spool {
1010
config: {
1111
test: {
1212
val: 0,
13+
array: [3, 4, 5],
1314
otherval: 1
1415
}
1516
},

test/lib/Configuration.test.js

+41-11
Original file line numberDiff line numberDiff line change
@@ -214,22 +214,22 @@ describe('lib.Configuration', () => {
214214

215215
describe('#set', () => {
216216
it('should set the value of a leaf node', () => {
217-
const config = new lib.Configuration(_.cloneDeep(testConfig), { NODE_ENV: 'test' })
217+
const config = new lib.Configuration(_.cloneDeep(testConfig), {NODE_ENV: 'test'})
218218
config.set('customObject.testValue', 'test')
219219

220220
assert.equal(config.get('customObject.testValue'), 'test')
221221
assert.equal(config.get('customObject.testValue'), 'test')
222222
})
223223
it('should set the value of a new, nested leaf node with no pre-existing path', () => {
224-
const config = new lib.Configuration(_.cloneDeep(testConfig), { NODE_ENV: 'test' })
224+
const config = new lib.Configuration(_.cloneDeep(testConfig), {NODE_ENV: 'test'})
225225

226226
assert(!config.get('foo'))
227227
config.set('foo.bar.new.path', 'test')
228228

229229
assert.equal(config.get('foo.bar.new.path'), 'test')
230230
})
231231
it('should throw an error when attempting to set a value after frozen', () => {
232-
const config = new lib.Configuration(_.cloneDeep(testConfig), { NODE_ENV: 'test' })
232+
const config = new lib.Configuration(_.cloneDeep(testConfig), {NODE_ENV: 'test'})
233233
config.freeze()
234234

235235
assert.throws(() => config.set('customObject.string', 'b'), lib.IllegalAccessError)
@@ -238,26 +238,57 @@ describe('lib.Configuration', () => {
238238
// assert.throws(() => config.customObject['string'] = 'c', lib.IllegalAccessError)
239239
})
240240

241+
describe('#set sanity', () => {
242+
it('should set leaves as well as root', () => {
243+
const config = new lib.Configuration(_.cloneDeep(testConfig))
244+
config.set('test', {test2: {test3: 4}})
245+
assert.equal(config.get('test.test2.test3'), 4)
246+
assert.deepEqual(config.get('test.test2'), {test3: 4})
247+
assert.deepEqual(config.get('test'), {test2: {test3: 4}})
248+
249+
config.set('test.test2', {test3: 5})
250+
assert.equal(config.get('test.test2.test3'), 5)
251+
assert.deepEqual(config.get('test.test2'), {test3: 5})
252+
assert.deepEqual(config.get('test'), {test2: {test3: 5}})
253+
254+
config.set('test.test2.test3', 6)
255+
assert.equal(config.get('test.test2.test3'), 6)
256+
assert.deepEqual(config.get('test.test2'), {test3: 6})
257+
assert.deepEqual(config.get('test'), {test2: {test3: 6}})
258+
259+
config.set('test.test2.test3', [1, 2, 3])
260+
assert.deepEqual(config.get('test.test2.test3'), [1 ,2, 3])
261+
assert.deepEqual(config.get('test.test2'), { test3: [1 ,2, 3] })
262+
assert.deepEqual(config.get('test'), { test2: { test3: [1 ,2, 3] } })
263+
})
264+
})
265+
})
266+
describe('#merge', () => {
267+
241268
it('should merge values', () => {
242269
const config = new lib.Configuration(_.cloneDeep(testConfig))
243270
config.merge({
244271
customObject: {
245272
string: 'b',
246273
int: 2,
247-
array: [3, 4, 5],
274+
intArray: [3, 4, 5],
275+
stringArray: ['one', 'two', 'three'],
276+
stringArray2: ['one', 'two', 'three'],
248277
subobj: {
249278
attr: 'b'
250279
},
251280
newValue: 'a'
252281
}
253282
}, 'merge')
254-
// Old Value should still be the same
283+
// Old Value should be replaced?
255284
assert.equal(config.get('customObject.string'), 'a')
256285
assert.equal(config.get('customObject.int'), 1)
257-
assert.deepEqual(config.get('customObject.array'), [1, 2, 3])
286+
assert.deepEqual(config.get('customObject.intArray'), [3, 4, 5])
258287
assert.deepEqual(config.get('customObject.subobj'), {attr: 'a'})
259-
// New Value should be merged in
288+
// New Values should be merged in
260289
assert.equal(config.get('customObject.newValue'), 'a')
290+
assert.deepEqual(config.get('customObject.stringArray'), ['one', 'two', 'three'])
291+
assert.deepEqual(config.get('customObject.stringArray2'), ['one', 'two', 'three'])
261292

262293
})
263294

@@ -325,18 +356,17 @@ describe('lib.Configuration', () => {
325356
assert(obj['main.spools.0'])
326357
assert.equal(obj['settings.foo'], 'bar')
327358
})
328-
})
329-
describe('#flattenTree', () => {
359+
330360
it('circular tree error', () => {
331361
const circle = { test: 'key'}
332362
circle.circle = circle
333-
assert.throws(() => lib.Configuration.flattenTree(circle), Error)
363+
// assert.throws(() => lib.Configuration.flattenTree(circle), Error)
334364
})
335365
})
336366
describe('#merge', () => {
337367
const tree = {
338368
foo: true,
339-
bar: [ 1,2,3 ],
369+
bar: [ 1, 2, 3 ],
340370
level2: {
341371
name: 'alice',
342372
level3: {

0 commit comments

Comments
 (0)