Last active
July 16, 2017 13:43
-
-
Save eiriklv/430ef0c1578b5cca59d8688eb6b9a3a5 to your computer and use it in GitHub Desktop.
Populating graphs using proxies
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Dependencies | |
*/ | |
const { plural } = require('pluralize'); | |
/** | |
* Get an item from an array collection by id | |
*/ | |
const getByIdFromArray = (collection = []) => (id = '') => { | |
return collection.find((item = {}) => item.id === id); | |
} | |
/** | |
* Get an item from an object collection by id | |
*/ | |
const getByIdFromHash = (collection = {}) => (id = '') => { | |
return collection[id]; | |
} | |
/** | |
* Get an item from a collection by id | |
*/ | |
const getByIdFromCollection = (collection = []) => { | |
return ( | |
Array.isArray(collection) ? | |
getByIdFromArray(collection) : | |
getByIdFromHash(collection) | |
); | |
} | |
/** | |
* Object population / linking implemented with ES6 Proxies | |
* | |
* Creates a proxied object which will resolve | |
* to the collections recursively by convention. | |
* | |
* NOTE: Lazy evaluation implemented using ES6 Proxies and getter traps | |
*/ | |
function populateByProxy( | |
depth = 0, | |
collections = {}, | |
object | |
) { | |
/** | |
* Handle invalid input | |
*/ | |
if (!object || typeof object !== 'object') { | |
return object; | |
} | |
/** | |
* List collection keys | |
*/ | |
const collectionKeys = Reflect.ownKeys(collections); | |
/** | |
* Create proxy trap handler for all get calls | |
*/ | |
const handler = { | |
get(target, key, receiver) { | |
/** | |
* Get the accessed value | |
*/ | |
const value = Reflect.get(target, key, receiver); | |
/** | |
* Handle symbol access or reaching bottom depth | |
*/ | |
if (typeof key === 'symbol' || depth <= 0) { | |
return value; | |
} | |
/** | |
* For collection references (array of ids) | |
*/ | |
if (Array.isArray(value) && collectionKeys.includes(key)) { | |
return value | |
.map(getByIdFromCollection(collections[key])) | |
.map(populateByProxy.bind(null, depth - 1, collections)); | |
} | |
/** | |
* For collection references (map of ids) | |
*/ | |
if (typeof value === 'object' && collectionKeys.includes(key)) { | |
return Reflect.ownKeys(value) | |
.map(getByIdFromCollection(collections[key])) | |
.filter(x => x) | |
.map(populateByProxy.bind(null, depth - 1, collections)) | |
.reduce((res, item) => Object.assign({}, res, { [item.id]: item }), {}); | |
} | |
/** | |
* For single value references (single id) | |
*/ | |
if (collectionKeys.includes(plural(key))) { | |
return populateByProxy( | |
depth - 1, | |
collections, | |
getByIdFromCollection(collections[plural(key)])(value) | |
); | |
} | |
/** | |
* Handle nested objects recursively | |
*/ | |
if (value && typeof value === 'object') { | |
return populateByProxy(depth, collections, value); | |
} | |
/** | |
* Handle primitive values | |
*/ | |
return value; | |
}, | |
}; | |
/** | |
* Return a proxied object or a list of proxied object | |
*/ | |
return Array.isArray(object) ? ( | |
object.map((item) => populateByProxy(depth, collections, item)) | |
) : ( | |
new Proxy(object, handler) | |
); | |
} | |
/** | |
* Example use | |
*/ | |
const stories = [{ | |
id: 'story-1', | |
name: 'Blabla', | |
author: { | |
person: 'person-1', | |
}, | |
liked_by: { | |
people: ['person-1', 'person-2'], | |
}, | |
read_by: { | |
people: ['person-1', 'person-2'] | |
}, | |
}]; | |
const people = [{ | |
id: 'person-1', | |
name: 'Per', | |
authored: { | |
stories: ['story-1'], | |
}, | |
read: { | |
stories: ['story-1'], | |
}, | |
liked: { | |
stories: ['story-1'], | |
}, | |
}, { | |
id: 'person-2', | |
name: 'Leif', | |
read: { | |
stories: ['story-1'], | |
}, | |
liked: { | |
stories: ['story-1'], | |
}, | |
}]; | |
/** | |
* Choose an object from the graph | |
*/ | |
const per = people[0]; | |
/** | |
* Create a populated/linked graph object | |
*/ | |
const populatedPer = populateByProxy(3, { stories, people }, per); | |
/** | |
* Only evaluates the fields necessary to do the read | |
*/ | |
console.log(populatedPer.authored.stories[0].name); | |
console.log(populatedPer.authored.stories[0].read_by.people[0].name); | |
/** | |
* Has to evaluate all field (which grows in size exponentially) | |
*/ | |
console.log(JSON.stringify(populatedPer, null, 2)); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Get an item from an array collection by id | |
*/ | |
const getByIdFromArray = (collection = []) => (id = '') => { | |
return collection.find((item = {}) => item.id === id); | |
} | |
/** | |
* Get an item from an object collection by id | |
*/ | |
const getByIdFromHash = (collection = {}) => (id = '') => { | |
return collection[id]; | |
} | |
/** | |
* Get an item from a collection by id | |
*/ | |
const getByIdFromCollection = (collection = []) => { | |
return ( | |
Array.isArray(collection) ? | |
getByIdFromArray(collection) : | |
getByIdFromHash(collection) | |
); | |
} | |
/** | |
* Object population / linking implemented with ES6 Proxies | |
* | |
* Creates a proxied object which will resolve | |
* to the collections recursively by convention. | |
* | |
* NOTE: Lazy evaluation (faster) implemented using ES6 Proxies and getter traps | |
*/ | |
const populateByProxy = module.exports.populateByProxy = function populateByProxy( | |
depth = 0, | |
collections = {}, | |
object | |
) { | |
/** | |
* Handle invalid input | |
*/ | |
if (!object || typeof object !== 'object') { | |
return object; | |
} | |
/** | |
* Check if the object contains the $ref key and populate agains the correct collection | |
*/ | |
if ( | |
typeof object === 'object' && | |
Object.keys(object).includes('$ref') && | |
Object.keys(object).includes('id') | |
) { | |
return populateByProxy( | |
depth - 1, | |
collections, | |
getByIdFromCollection(collections[object.$ref])(object.id) | |
); | |
} | |
/** | |
* Create proxy trap handler for all get calls | |
*/ | |
const handler = { | |
get(target, key, receiver) { | |
/** | |
* Get the accessed value | |
*/ | |
const value = Reflect.get(target, key, receiver); | |
/** | |
* Handle symbol access | |
*/ | |
if (typeof key === 'symbol' || depth <= 0) { | |
return value; | |
} | |
/** | |
* Handle nested objects and arrays | |
*/ | |
if (value && typeof value === 'object') { | |
return populateByProxy(depth, collections, value); | |
} | |
/** | |
* Handle primitive values | |
*/ | |
return value; | |
}, | |
}; | |
/** | |
* Return a proxied object or a list of proxied object | |
*/ | |
return Array.isArray(object) ? ( | |
object.map((item) => populateByProxy(depth, collections, item)) | |
) : ( | |
new Proxy(object, handler) | |
); | |
} | |
/** | |
* Example | |
*/ | |
const stories = [ | |
{ | |
type: 'story', | |
id: 'story-1', | |
author: { $ref: 'people', id: 'person-1' } | |
}, | |
{ | |
type: 'story', | |
id: 'story-2', | |
author: { $ref: 'people', id: 'person-2' } | |
}, | |
]; | |
const people = [ | |
{ | |
type: 'person', | |
id: 'person-1', | |
name: 'John Storywriter', | |
authored: [ | |
{ $ref: 'stories', id: 'story-1' }, | |
], | |
likes: [ | |
{ $ref: 'stories', id: 'story-1' }, | |
{ $ref: 'stories', id: 'story-2' }, | |
], | |
}, | |
{ | |
type: 'person', | |
id: 'person-2', | |
name: 'Peter Telltale', | |
authored: [ | |
{ $ref: 'stories', id: 'story-2' }, | |
], | |
likes: [ | |
{ $ref: 'stories', id: 'story-1' }, | |
{ $ref: 'stories', id: 'story-2' }, | |
], | |
} | |
]; | |
/** | |
* Consolidate the collections into a "graph" | |
*/ | |
const graph = { people, stories }; | |
/** | |
* Choose an entry point to the graph | |
*/ | |
const entry = people[0]; | |
/** | |
* Create ta populated entry point resolved against the graph with a specified depth | |
*/ | |
const populatedItem = populateByProxy(3, graph, entry); | |
console.log(JSON.stringify(populatedItem, null, 2)); | |
/** | |
* { | |
* "type": "person", | |
* "id": "person-1", | |
* "name": "John Storywriter", | |
* "authored": [ | |
* { | |
* "type": "story", | |
* "id": "story-1", | |
* "author": { | |
* "type": "person", | |
* "id": "person-1", | |
* "name": "John Storywriter", | |
* "authored": [ | |
* { | |
* "type": "story", | |
* "id": "story-1", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-1" | |
* } | |
* } | |
* ], | |
* "likes": [ | |
* { | |
* "type": "story", | |
* "id": "story-1", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-1" | |
* } | |
* }, | |
* { | |
* "type": "story", | |
* "id": "story-2", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-2" | |
* } | |
* } | |
* ] | |
* } | |
* } | |
* ], | |
* "likes": [ | |
* { | |
* "type": "story", | |
* "id": "story-1", | |
* "author": { | |
* "type": "person", | |
* "id": "person-1", | |
* "name": "John Storywriter", | |
* "authored": [ | |
* { | |
* "type": "story", | |
* "id": "story-1", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-1" | |
* } | |
* } | |
* ], | |
* "likes": [ | |
* { | |
* "type": "story", | |
* "id": "story-1", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-1" | |
* } | |
* }, | |
* { | |
* "type": "story", | |
* "id": "story-2", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-2" | |
* } | |
* } | |
* ] | |
* } | |
* }, | |
* { | |
* "type": "story", | |
* "id": "story-2", | |
* "author": { | |
* "type": "person", | |
* "id": "person-2", | |
* "name": "Peter Telltale", | |
* "authored": [ | |
* { | |
* "type": "story", | |
* "id": "story-2", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-2" | |
* } | |
* } | |
* ], | |
* "likes": [ | |
* { | |
* "type": "story", | |
* "id": "story-1", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-1" | |
* } | |
* }, | |
* { | |
* "type": "story", | |
* "id": "story-2", | |
* "author": { | |
* "$ref": "people", | |
* "id": "person-2" | |
* } | |
* } | |
* ] | |
* } | |
* } | |
* ] | |
* } | |
*/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment