-
Notifications
You must be signed in to change notification settings - Fork 868
Support denormalizing from immutable entities #228
Support denormalizing from immutable entities #228
Conversation
Object { | ||
"id": 1, | ||
"name": "Milo", | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems that immutable data structures are converted to plain objects when serialized into snapshots, see: jestjs/jest#1622. In this case, the Array should be an actual array since the test uses [1,2]
as input but each object is actually an Immutable.Map
(which I manually verified is the case).
Nice work @jcarbo! For the most part this looks good. I have a few nit requests, I think, but I'd like to spend more time with it once I'm back (currently traveling till next week) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will need rebase, as denormalize has changed a bit to handle recursive schema definitions.
I'm also really concerned about edge-cases here, since the fallback checks are very much not fool-proof.
Couldn't we set a flag at the top level of the denormalize
method that can pass down whether the entities provided are immutable or not? Is it safe to assume when one entity is immutable, that all are immutable as well?
src/schemas/Entity.js
Outdated
const entity = typeof entityOrId === 'object' ? entityOrId : entities[this.key][entityOrId]; | ||
const entityCopy = { ...entity }; | ||
const entity = typeof entityOrId === 'object' ? entityOrId : getIn(entities, [ this.key, entityOrId ]); | ||
let entityCopy = isImmutable(entity) ? entity : { ...entity }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Avoid using let
, please
src/schemas/Entity.js
Outdated
Object.keys(this.schema).forEach((key) => { | ||
if (entityCopy.hasOwnProperty(key)) { | ||
if (hasIn(entityCopy, [ key ])) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we use the isImmutable(entity)
method before doing getIn
, hasIn
, and setIn
to avoid unnecessary overhead?
src/schemas/ImmutableUtils.js
Outdated
* @return {bool} | ||
*/ | ||
export function isImmutable(object) { | ||
return !!(object && object.getIn); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
edge-case: what if someone has a key, getIn
on their entity? (also true for setIn
and hasIn
.
src/schemas/Object.js
Outdated
@@ -13,14 +15,14 @@ export const normalize = (schema, input, parent, key, visit, addEntity) => { | |||
}; | |||
|
|||
export const denormalize = (schema, input, unvisit, entities) => { | |||
const object = { ...input }; | |||
let inputCopy = isImmutable(input) ? input : { ...input }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: avoid let
statements.
src/schemas/Object.js
Outdated
@@ -1,3 +1,5 @@ | |||
import { getIn, hasIn, isImmutable, setIn } from './ImmutableUtils'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: import * as ImmutableUtils from './ImmutableUtils';
15a013a
to
4debf74
Compare
@paularmstrong - thanks for reviewing! I rebased and got the tests back to green. Also, reading through your comments it seems that your (valid) concerns were around performance/overhead and robustness. The commit I just pushed up takes a slightly different approach and should address your concerns:
With these changes, the compressed size increased by another ~90 bytes. Still not that bad imho. |
d2da777
to
72365c1
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good. A couple nits marked inline, but I don't think we'll need any major changes. I'll test more when I get some time.
src/index.js
Outdated
return entityOrId; | ||
} | ||
|
||
if (ImmutableUtils.isImmutable(entities)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could be sent in as a flag, since entities
is a wrapped argument in the getEntities
function. This would save on this conditional over every entity iteration. Something like...
const getEntities = (entities, visitedEntities) => {
const isImmutable = ImmutableUtils.isImmutable(entities);
return (schema, entityOrId) => {
// ...
const entity = getEntity(entityOrId, schemaKey, entities, isImmutable);
// ...
};
};
src/schemas/Entity.js
Outdated
this._getId = typeof idAttribute === 'function' ? idAttribute : (input) => input[idAttribute]; | ||
this._getId = typeof idAttribute === 'function' ? | ||
idAttribute : | ||
(input) => ImmutableUtils.isImmutable(input) ? input.get(idAttribute) : input[idAttribute]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we avoid nested ternaries, please? They get difficult to read.
src/schemas/ImmutableUtils.js
Outdated
return Object.keys(schema).reduce((object, key) => { | ||
// Immutable maps cast keys to strings on write so we need to ensure | ||
// we're accessing them using string keys. | ||
const stringKey = typeof key === 'string' ? key : key.toString(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be safe to just force casting it:
const stringKey = `${key}`;
Just pushed those changes 👍 |
Friendly nudge on this when you get a chance :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for the slowness and thanks for the nudge! We recently rolled new mobile.twitter.com out to a ton more people!.
Just one little nit request inline. Can you add a case for it? I can get this merged and deployed today if so!
src/schemas/ImmutableUtils.js
Outdated
export function isImmutable(object) { | ||
return !!(object && ( | ||
object.hasOwnProperty('__ownerID') || // Immutable.Map | ||
(object._map && object._map.object.hasOwnProperty('__ownerID')) // Immutable.Record |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This condition is never hit by tests. Can you add a case for this?
Good call on that missing coverage, the conditional was actually broken! |
@@ -58,16 +59,30 @@ const unvisit = (input, schema, getDenormalizedEntity) => { | |||
return method(schema, input, unvisit, getDenormalizedEntity); | |||
} | |||
|
|||
if (input === undefined || input === null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wanted to call your attention to this change added in the last commit. This came up in the test with immutable records because it always instantiates all fields defined in the schema, using the default value if you didn't give it pass it one. E.g.
> Menu = new Immutable.Record({ id: null, food: null })
[Function: Record]
> menuData = { id: 2 }
{ id: 2 }
> nonRecordMenu = menuData
{ id: 2 }
> recordMenu = new Menu(menuData)
Record { "id": 2, "food": null }
So while denormalizing the record, it's going to try to unvisit food: null
which currently throws an unexpected error in _getId
This assumes that you're not going to have undefined
or null
resolve to anything during the denormalization process. Is that an ok assumption to make?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's a good one and similar to #225. This is good. Thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another related thing I just realized: this currently throws that same unexpected error if an entity is not found:
const mySchema = new schema.Entity('tacos');
const entities = {
tacos: {
1: { id: 1, type: 'foo' }
}
};
> denormalize(3, mySchema, entities)
TypeError: Cannot read property 'id' of undefined
at EntitySchema._getId (~/normalizr/dist/normalizr.js:226:65)
at EntitySchema.getId (~/normalizr/dist/normalizr.js:245:19)
at ~/normalizr/dist/normalizr.js:635:21
at EntitySchema.denormalize (~/normalizr/dist/normalizr.js:273:20)
at unvisit (~/normalizr/dist/normalizr.js:612:17)
at Object.denormalize$1 [as denormalize] (~/normalizr/dist/normalizr.js:651:10)
at repl:1:3
at realRunInThisContextScript (vm.js:22:35)
at sigintHandlersWrap (vm.js:98:12)
at ContextifyScript.Script.runInThisContext (vm.js:24:12)
I kinda feel like that should just return undefined
. In my apps, I think I'd rather just ignore missing data, especially when denormalizing nested data.
I could also see it being configurable so you can handle how you want (e.g. initialize with empty object, return null,undefined, throw error)
denormalize(3, mySchema, entities, { onNotFound: (id, schema) => undefined })
denormalize(3, mySchema, entities, { onNotFound: (id, schema) => { id } })
denormalize(3, mySchema, entities, { onNotFound: (id, schema) => throw new Error(`could not find ${schema.name} with id='${id}'` })
Thoughts? If you agree I'd probably remove this line and just handle that below if an entity is not found in the store.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there was a PR for this and it was randomly closed... #229. Makes sense to fix. We don't ever run across that case, but I could see it being possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, already merged this PR... so maybe a new one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, my brain isn't working today... I'd rather stay away from the extra configuration, as you proposed. I'd expect the application to handle receiving null
for an entity elsewhere than pushing more into normalizr.
Nice job on this! Looks like we've actually increased the performance since the initial release of normalizr2 and this addition doesn't have any ill effects! We used to be right at about 200%. Now looking at 220%.
|
This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs. |
Fixes #222
Problem
First I want to say that I've been using and following this repo for quite a while and it's great to see
denormalize
finally land!It seems that most places I've seen that suggest using
normalizr
also suggest using an immutable store for entities. However, in doing so you lose the ability to usedenormalize
. Addingimmutable
as a dependency would add complexity and bloat for users who are not using immutable data.Solution
Update
denormalize
to handle both mutable and immutable entities without actually requiringimmutable
as a dependency.Support is handled by using simple
getIn
,setIn
,hasIn
methods that defer to the immutable implementations if the object is immutable otherwise implement the same behavior on plain javascript objects/arrays.immutable-js
is required as a dev dependency since it's needed for specs. I just updated the existing specs to also test with immutable entities.The best part is that it only adds ~70 bytes (compressed) and ~40 bytes (minified + compressed) and doesn't add any external dependencies.
TODO
Size Diff
Before
After