Skip to content

Instantly share code, notes, and snippets.

@eiriklv
Last active July 16, 2017 13:43
Show Gist options
  • Save eiriklv/430ef0c1578b5cca59d8688eb6b9a3a5 to your computer and use it in GitHub Desktop.
Save eiriklv/430ef0c1578b5cca59d8688eb6b9a3a5 to your computer and use it in GitHub Desktop.
Populating graphs using proxies
/**
* 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));
/**
* 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