Skip to content

Added app.extend() - #3568

Closed
boycce wants to merge 3 commits into
expressjs:4.xfrom
boycce:app.extend
Closed

Added app.extend()#3568
boycce wants to merge 3 commits into
expressjs:4.xfrom
boycce:app.extend

Conversation

@boycce

@boycceboycce commented Feb 16, 2018

Copy link
Copy Markdown

Added app.extend(), which provides an easy way to update certain properties of a setting.
Example:

app.set('foo', { 'bar' : 1 })
app.extend('foo', { 'baz' : 1 });
app.get('foo');
// => { "bar" : 1, "baz" : 1 }

Related issue #3499

@wesleytodd

Copy link
Copy Markdown
Member

Hey @boycce. This is an interesting addition. Can you provide some real world examples of why you would like this?

Could you also take a look at how you would do this addition considering this planned change: #3218

@boycce

Copy link
Copy Markdown
Author

Hey @wesleytodd I see no issues migrating .extend() along with the other getter/setter methods.

One example would be if you needed to update stored configurations for a particular service while keeping to express's familiar set/get/expand* methods:

app.set(braintree, {
'environment': 'production',
'merchantId': 'os9ksSioSeyH87fhd',
'publicKey': 'kcYsjcSlkfd8Hs2jd',
'privateKey': '1z844211b3d012228dee47279ef30000',
'merchantAccountIdNZD': 'userNZD',
'merchantAccountIdUSD': 'userUSD'
})
app.expand('braintree', {
'privateKey': '3syd7D9l02hh11dB91p02kdufhnw712a'
})

Maybe @brunoscopelliti could also share his use case for needing extend().

@brunoscopelliti

Copy link
Copy Markdown

In some cases I found useful to set related fields under the same object.
As example:

app.set("log_level","verbose");app.set("log_active",true);// vs app.set("log_config",{level: "verbose",active: true});

When I use the latter approach I would like to have a better method to update a single field, than:

app.set("log_config",Object.assign({},app.get("log_config"),{dir: "/db/log/"}));

@wesleytodd

wesleytodd commented Feb 19, 2018

Copy link
Copy Markdown
Member

Hey @boycce, first off, I hope those are not your production configs ;). But second @boycce & @brunoscopelliti, your use cases seem reasonable. I prefer extend over expand, assuming that is not just a typo.

I think this change is something which could be landed on 4.x as well as the 5.x branch. So can you update this PR to point to master for 4.x, then create a PR to store-settings so that I can update #3218 with this?

@dougwilson

Copy link
Copy Markdown
Contributor

Is this handling inherited settings correctly? It seems like it's actually altering the parent property in addition to setting the child property to the same object instance. I feel like the parent object should not be getting modified, so the setting on the parent app should remain the same when the setting on the child app is extended.

@wesleytodd

Copy link
Copy Markdown
Member

I dont think it would set the parent's property because it is prototypicaly inherited, not done by reference. So if it was initially from the parent, it would just result in settings it's own property with a fully copy of the parent and not touching the parent values. I could be wrong though, would be worth having a test around this.

See: https://github.com/expressjs/express/pull/3568/files#diff-5372f626ee15242f1e2c6eb31655b4faR116

@dougwilson

Copy link
Copy Markdown
Contributor

I did test this and it is altering the parent property as well as setting the child property to the same object reference.

@wesleytodd

Copy link
Copy Markdown
Member

Ahh, ok then I miss-read how this works, sorry. Yes I agree it should not modify the parent.

@dougwilson

Copy link
Copy Markdown
Contributor

Here is the test I ran. I feel like this test should pass, but it currently does not:

varassert=require('assert')varexpress=require('express')varapp1=express()varapp2=express()app1.set('log_config',{level: "verbose",active: false})assert.equal(app1.get('log_config').active,false)assert.equal(app2.get('log_config'),undefined)app1.use(app2)assert.equal(app1.get('log_config').active,false)assert.equal(app2.get('log_config').active,false)app2.extend('log_config',{active: true})assert.deepEqual(app1.get('log_config'),{level: "verbose",active: false})assert.deepEqual(app2.get('log_config'),{level: "verbose",active: true})console.log('success')
assert.js:81
throw new assert.AssertionError({
^
AssertionError: { level: 'verbose', active: true } deepEqual { level: 'verbose', active: false }
at Object.<anonymous> (test.js:19:8)
at Module._compile (module.js:570:32)
at Object.Module._extensions..js (module.js:579:10)
at Module.load (module.js:487:32)
at tryModuleLoad (module.js:446:12)
at Function.Module._load (module.js:438:3)
at Module.runMain (module.js:604:10)
at run (bootstrap_node.js:389:7)
at startup (bootstrap_node.js:149:9)
at bootstrap_node.js:504:3

Comment threadlib/application.js Outdated
@boycce
boycce changed the base branch from 5.0 to 4.xFebruary 22, 2018 18:40
Added app.extend(), which provides an easy way to update certain properties of a setting.
`extend` shouldn't be setting the propertry to the same object reference.
@boycce

boycce commented Feb 22, 2018

Copy link
Copy Markdown
Author

Updated to 4.x and removed object referencing.
Would you like me to support Node 3.3 and below concerning Object.assign()

Comment threadlib/application.js Outdated
@wesleytodd

Copy link
Copy Markdown
Member

I left a comment about the isArray usage, but the same does apply to Object.assign.

Added `utils.extendObject()` to support previous Node versons
Removed support for extending arrays and throws an error
Comment threadlib/utils.js
* @private
*/

exports.extendObject = function(target, source) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding more code here, can the dependency utils-merge we already have perform this function? Or if not, what's the difference between this code and utils-merge?

@boycceboycceFeb 23, 2018

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dougwilson, sorry I didn't see this merge util, I was scanning for an extending extend() function or corresponding library 🤐. This passes my tests:

this.settings[setting] = merge(merge({}, this.settings[setting]), val)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, I didn't realize that utils-merge did not support variadic calls. I think this is, sadly, the best way at the moment, but I think we should add to the roadmap "removing utils-merge in favor of object.assign or an equivilant fallback".

@dougwilson

dougwilson commented Feb 22, 2018

Copy link
Copy Markdown
Contributor

What do you think we should do for the case where .extend is called on a setting that is already set but is not an object? Right now it is silently overwritten. Do we think that is the correct thing to do?

Example:

$ node -pe 'require("express")().set("thing","foo").extend("thing",{"foo":"bar"}).get("thing")'
{ foo: 'bar' }

Comment threadlib/application.js
*
* app.set('foo', { 'bar' : 1 })
* app.expand('foo', { 'baz' : 1 });
* app.expand('foo');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@wesleytodd thoughts on this? I'm not sure what the use of having .extend called with one argument returning the current value -- that's already covered with .get and .set I don't think we need a third method to get the value of a setting, right?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Not sure if it should error, but it shouldn't do anything and shouldn't be documented.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

May be useful to throw error that required argument is missing.

@boycceboycceFeb 23, 2018

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will edit this later today.

Comment threadlib/application.js
if (typeof val === 'undefined') {
return this;
} else if (val === null || typeof val !== 'object' || val instanceof Array) {
throw new Error('value needs to be an object');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be a TypeError

Comment threadlib/application.js

if (typeof val === 'undefined') {
return this;
} else if (val === null || typeof val !== 'object' || val instanceof Array) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would recommend Array.isArray instead of instanceof Array. All the instanceof Arrays were removed in Express 1.0.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When looking at node.green, I think in my other comment I mentioned that I thought that wasn't supported in node 0.12, but I think I looked at the wrong field, and it has been so long since I have used 0.12 I couldn't remember what was supported...

http://node.green/#ES2015-subclassing-Array-is-subclassable-Array-isArray-support

@dougwilsondougwilson added this to the 4.17 milestone Feb 22, 2018
@wesleytodd

Copy link
Copy Markdown
Member

What do you think we should do for the case where .extend is called on a setting that is already set but is not an object

I think it should refuse to work on any property which is not a plain object.

@dougwilson

Copy link
Copy Markdown
Contributor

I think it should refuse to work on any property which is not a plain object.

Ah, that is a good point. If a setting is an instance, calling extend here would remove the original prototype, so yea, we don't want to destroy the object. We can always add additional things to extend as semver minors so just only working on plain objects sounds like a good first implementation.

@boycce

Copy link
Copy Markdown
Author

@dougwilson, would you like me to add a isPlainObject export to the utils file, or require it from the lodash module in regards to checking if the object is in fact a plain object from the Object constructor.

I could use something like this:

functionisPlainObject(obj){returntypeofobj==='object'&&obj!==null&&obj.constructor===Object&&Object.prototype.toString.call(obj)==='[object Object]'}

@wesleytodd

Copy link
Copy Markdown
Member

My point was more along the lines of, "don't try to merge arrays" and "don't try to handle complex types". In re-reading it I see where this led, and I think I was making it too complicated.

So adding a dep for this is out of the question IMO. And there is a bunch of valid use cases for merging "object like" things, so the full isPlainObject is also not a good path forward. I would think just doing this is the best:

app.extend=function(setting,val){if(val===null||typeofval!=='object'||Array.isArray(val)){thrownewError('value needs to be an object');}debug('extend "%s" with %o',setting,val);varcurrentSetting=this.settings[setting];if(typeofcurrentSetting==='undefined'){this.settings[setting]=val;}else{this.settings[setting]=extendObject({},currentSetting,val);}returnthis;};

Thoughts?

@boycce

Copy link
Copy Markdown
Author

Sounds good by me.

@dougwilsondougwilson mentioned this pull request Oct 27, 2018
23 tasks
@dougwilsondougwilson removed this from the 4.17 milestone May 10, 2019
@dougwilson

Copy link
Copy Markdown
Contributor

So, this pull request is a bit stale, and looking back, it doesn't seem like there was really a consensus about adding this to core express. As pointed out in the conversation above, there are certainly different extending strategies one can make... And with things like the prototype pollution issues going around and the fact that, at it's core, it's not really much more than app.set('foo', Object.assign(app.get('foo') || {}, { foo: 'bar' })) but without the flexibility to perform your own merge strategies, it just seems more complexity into core than it's worth...

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@boycce@wesleytodd@brunoscopelliti@dougwilson