Skip to content
This repository has been archived by the owner on Mar 20, 2022. It is now read-only.

Can normalize entities keyed with their ID #155

Merged
merged 1 commit into from
Nov 9, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,12 @@ const article = new Schema('articles', { idAttribute: 'slug' });
function generateSlug(entity) { /* ... */ }
const article = new Schema('articles', { idAttribute: generateSlug });

// The function is passed both the current value and its key
// This is helpful in occasions where your data is keyed by id, but doesn't contain id inside the value
const keyedByIdJson = { 1: { ... }, 2: { ... }, 3: { ... } };
function generateSlug(entity, id) { return id; }
const article = new Schema('articles', { idAttribute: generateSlug });

// You can also specify meta properties to be used for customizing the output in assignEntity (see below)
const article = new Schema('articles', { idAttribute: 'slug', meta: { removeProps: ['publisher'] }});

Expand Down
4 changes: 2 additions & 2 deletions src/EntitySchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ export default class EntitySchema {
return this._key;
}

getId(entity) {
return this._getId(entity);
getId(entity, key) {
return this._getId(entity, key);
}

getIdAttribute() {
Expand Down
34 changes: 21 additions & 13 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ function defaultAssignEntity(normalized, key, entity) {
normalized[key] = entity;
}

function visitObject(obj, schema, bag, options) {
function visitObject(obj, schema, bag, options, collectionKey) {
const { assignEntity = defaultAssignEntity } = options;

const defaults = schema && schema.getDefaults && schema.getDefaults();
Expand All @@ -17,7 +17,7 @@ function visitObject(obj, schema, bag, options) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
const resolvedSchema = typeof schema[key] === 'function' ? schema[key].call(null, obj) : schema[key];
const entity = visit(obj[key], resolvedSchema, bag, options);
const entity = visit(obj[key], resolvedSchema, bag, options, collectionKey);
assignEntity.call(null, normalized, key, entity, obj, schema);
if (schemaAssignEntity) {
schemaAssignEntity.call(null, normalized, key, entity, obj, schema);
Expand All @@ -28,13 +28,13 @@ function visitObject(obj, schema, bag, options) {
}

function defaultMapper(iterableSchema, itemSchema, bag, options) {
return (obj) => visit(obj, itemSchema, bag, options);
return (obj, key) => visit(obj, itemSchema, bag, options, key);
}

function polymorphicMapper(iterableSchema, itemSchema, bag, options) {
return (obj) => {
return (obj, key) => {
const schemaKey = iterableSchema.getSchemaKey(obj);
const result = visit(obj, itemSchema[schemaKey], bag, options);
const result = visit(obj, itemSchema[schemaKey], bag, options, key);
return { id: result, schema: schemaKey };
};
}
Expand All @@ -47,7 +47,7 @@ function visitIterable(obj, iterableSchema, bag, options) {
return obj.map(curriedItemMapper);
} else {
return Object.keys(obj).reduce(function (objMap, key) {
objMap[key] = curriedItemMapper(obj[key]);
objMap[key] = curriedItemMapper(obj[key], key);
return objMap;
}, {});
}
Expand Down Expand Up @@ -76,11 +76,11 @@ function defaultMergeIntoEntity(entityA, entityB, entityKey) {
}
}

function visitEntity(entity, entitySchema, bag, options) {
function visitEntity(entity, entitySchema, bag, options, collectionKey) {
const { mergeIntoEntity = defaultMergeIntoEntity } = options;

const entityKey = entitySchema.getKey();
const id = entitySchema.getId(entity);
const id = entitySchema.getId(entity, collectionKey);

if (!bag.hasOwnProperty(entityKey)) {
bag[entityKey] = {};
Expand All @@ -91,28 +91,35 @@ function visitEntity(entity, entitySchema, bag, options) {
}

let stored = bag[entityKey][id];
let normalized = visitObject(entity, entitySchema, bag, options);
let normalized = visitObject(entity, entitySchema, bag, options, collectionKey);
mergeIntoEntity(stored, normalized, entityKey);

return id;
}

function visit(obj, schema, bag, options) {
function visit(obj, schema, bag, options, collectionKey) {
if (!isObject(obj) || !isObject(schema)) {
return obj;
}

if (schema instanceof EntitySchema) {
return visitEntity(obj, schema, bag, options);
return visitEntity(obj, schema, bag, options, collectionKey);
} else if (schema instanceof IterableSchema) {
return visitIterable(obj, schema, bag, options);
} else if (schema instanceof UnionSchema) {
return visitUnion(obj, schema, bag, options);
} else {
return visitObject(obj, schema, bag, options);
return visitObject(obj, schema, bag, options, collectionKey);
}
}

function normalizeResult(result) {
Copy link
Owner

Choose a reason for hiding this comment

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

Why do we need this function? When is the result coming back as an object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Result comes back as an object because the new functionality is added to an iterable schema (arrayOf(foo) or keyedObjectOf(foo), as mentioned above). Thus, visit() returns the result of visitIterable(), which is an object mapping keys to the result of curriedItemMapper(obj[key], key).

When we use the function (obj, key) => key, this ultimately returns an object something like { 1: '1', 2: '2', 3: '3' } (whereas something like obj => obj would return { 1: foo, 2: bar, 3: baz } or something). Here, the values of the object don't represent objects, so we convert into an array of keys after double checking that all the keys do equal all their values.

Copy link

@specialunderwear specialunderwear Oct 5, 2016

Choose a reason for hiding this comment

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

It does happen that the result can not be normalized, if you specify idAttribute for example, your key will not be your id, and the result can not be further normalized. So every time you don;t want the key to be the ID, but you still need it to compute the id, you will not get an array, but the object instead.

if (isObject(result) && isEqual(Object.keys(result), Object.keys(result).map(key => result[key]))) {
return Object.keys(result);
}
return result;
}

export function arrayOf(schema, options) {
return new IterableSchema(schema, options);
}
Expand All @@ -139,8 +146,9 @@ export function normalize(obj, schema, options = {}) {
let bag = {};
let result = visit(obj, schema, bag, options);


return {
entities: bag,
result
result: normalizeResult(result)
};
}
35 changes: 34 additions & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1950,4 +1950,37 @@ describe('normalizr', function () {
}).should.throw();
});

});
it('can normalize iterables keyed with their id', function () {
var user = new Schema('users', {
idAttribute: function(obj, key) {
return key;
}
})

var input = {
1: {
name: 'Adam'
},
4: {
name: 'Jeremy'
}
};

Object.freeze(input);

normalize(input, valuesOf(user)).should.eql({
result: ['1', '4'],
entities: {
users: {
'1': {
name: 'Adam'
},
'4': {
name: 'Jeremy'
}
}
}
});
});

});