Skip to content
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
112 changes: 87 additions & 25 deletions model/fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,70 +5,96 @@ module.exports = fetch;
function fetch(options) {
const object = this;
const config = _.merge({}, object.__config, options);
const request = build_request({ config, object });
const axios = object.__axios;
const main_request = axios(build_request_options({ config, object }))
.then(res => res.data);
let requests = [ main_request ];

return axios(request).then(response => {
const related = (response.data.included || []).reduce((result, item) => {
result[item.id] = item;
return result;
}, {});
if (config.parallel_relationships) {
// Concatenate related requests.
requests = requests.concat(build_related_requests({ axios, config, object }));
}

object.hydrate({ data: response.data.data, related });
return Promise.all(requests).then(responses => {
const main = responses.shift();
const data = main.data;
const related = {}

if (config.parallel_relationships) {
responses.forEach(response => {
if (response.__relationship) {
data.relationships[response.__relationship] = response.data.data;
} else if (response.__related_object) {
const item = response.data.data;
related[item.id] = item;
}
});
} else {
(main.included || []).forEach(item => {
related[item.id] = item;
});
}

object.hydrate({ data, related });
object.__saved = true;
object.__new = false;

return object;
});
}

function build_request({ config, object }) {
function build_request_options({ config, object }) {
const request = {
method: 'get',
url: `${config.base_url}${object.endpoint}/${object.id}`,
}
const include_param = get_include_param({ object, related: config.related });

if (include_param) {
request.url += `?include=${include_param}`;
// If relationships are not being fetched in parallel, try to include them
// via query string param.
if (!config.parallel_relationships) {
const include_param = get_related_fields({ object, related: config.related }).join(',');

if (include_param) {
request.url += `?include=${include_param}`;
}
}

return request;
}

function get_include_param({ object, related }) {
let include_param = '';
function get_related_fields({ object, related }) {
let related_fields = [];

if (!related) {
return include_param;
return related_fields;
}

switch (typeof related) {
case 'string':
include_param += calculate_related_paths({ object, prop: related });
related_fields.push(calculate_related_paths({ object, prop: related }));
break;

case 'object':
if (Array.isArray(related)) {
include_param += related
.map(item => get_include_param({ object, related: item }))
related_fields = related_fields.concat(related
.map(item => get_related_fields({ object, related: item }))
.map(item => item[0])
.filter(item => item)
.join(',');
)
}
// @TODO: Support non-iterable objects (for nested relationships?)
break;

case 'boolean':
if (related) {
include_param += Object.keys(object.__maps)
.map(item => get_include_param({ object, related: item }))
.filter(item => item)
.join(',');
}
related_fields = related_fields.concat(Object.keys(object.__maps)
.map(item => get_related_fields({ object, related: item }))
.map(item => item[0])
.filter(item => item)
)
break;
}

return include_param;
return related_fields;
}

function calculate_related_paths({ object, prop }) {
Expand All @@ -81,3 +107,39 @@ function calculate_related_paths({ object, prop }) {

return directive.replace(/^relationships\./, '');
}

/**
* Construct an array of parallel requests to fetch related objects and the
* relationships themselves.
*
* @return Array - An array of promises.
*/
function build_related_requests({ axios, config, object }) {
return get_related_fields({ object, related: config.related })

// Reduce the array of related fields into an array of requests;
// two requests per related field:
// - fetching the related object
// - fetching the relationship itself (sometimes this includes metadata
// about the relationship)
.reduce((arr, item) => {

// Request relationship data.
const rel_req = build_request_options({ config, object });
rel_req.url += `/relationships/${item}`;
arr.push(axios(rel_req).then(response => {
response.__relationship = item;
return response;
}));

// Request related object data.
const obj_req = build_request_options({ config, object });
obj_req.url += `/${item}`;
arr.push(axios(obj_req).then(response => {
response.__related_object = item;
return response;
}));

return arr;
}, []);
}
6 changes: 4 additions & 2 deletions model/get.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,17 @@ function get({ object, prop }) {

switch (type) {
case 'attributes':
result = _.get(object, `__data.${map}`);
result = _.get(object, `__data.${map}`, null);
break;
case 'relationships': {
const reference = _.get(object, `__data.${map}.data`);
const related = _.get(object, '__related');
if (Array.isArray(reference)) {
result = reference.map(ref => get_related({ parent: object, reference: ref, related }));
} else {
} else if (reference) {
result = get_related({ parent: object, reference, related });
} else {
result = null;
}
break;
}
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"chai": "^4.1.2",
"eslint": "^4.19.1",
"mocha": "^5.1.1",
"pluralize": "^7.0.0",
"pre-push": "^0.1.1",
"should": "^13.2.1",
"sinon": "^4.5.0",
Expand Down
15 changes: 3 additions & 12 deletions test/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,11 @@

const expect = require('chai').expect;
const config = require('../config');
const config_object = require('./fixtures/config')();
const options = require('./fixtures/config/options');
const symbol = Symbol.for('Jsonmonger.config');

describe('config() method', () => {
let config_object, options, symbol;

before(() => {
options = {
base_url: 'https://some.contrived.url',
}

config_object = config(options);

symbol = Symbol.for('Jsonmonger.config');
});

it('should create a global symbol', () => {
expect(config_object).to.deep.equal(options);
expect(config_object).to.deep.equal(global[symbol]);
Expand Down
96 changes: 96 additions & 0 deletions test/fixtures/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
const _ = require('lodash');
const pluralize = require('pluralize');
const url = require('url');
const base = './data';

module.exports = req => {
return new Promise((resolve, reject) => {
const request_url = url.parse(req.url, true, true);
const { type, id, relationship, field } = parse_path(request_url.pathname);
const main_data = require(`${base}/${type}/${id}`);
let data;
let included = [];

if (relationship && field) {
data = _.get(main_data, `relationships.${field}`, {}).data || null;
} else if (field) {
data = get_path_relationship({ field, main_data });
} else {
data = main_data;
included = get_query_param_relationships({ data, included, request_url });
}

if (data instanceof Error) {
return reject(data);
}

return resolve(JSON.stringify({ data, included }));
})
}

function parse_path(path) {
// Make array of path segments, filtering out empty segments.
const segments = path.split('/').filter(seg => seg);
const relationship = segments[2] === 'relationships' && segments[3];

return {
type: segments[0],
id: segments[1],
relationship,
field: relationship ? segments[3] : segments[2],
}
}

function get_relationships({ data, key }) {
return _.get(data, `relationships.${key}`);
}

function get_query_param_relationships({ data, included, request_url }) {
if (_.get(request_url, 'query.include')) {
const query_fields = request_url.query.include.split(',');
return _.flattenDeep(included.concat(query_fields
.map(key => get_relationships({ data, key }))
.filter(relationship => relationship)
.map(relationship => {
if (Array.isArray(relationship.data)) {
return relationship.data.map(item => {
try {
return require(`${base}/${pluralize.plural(item.type)}/${item.id}`);
} catch (e) {
console.error(e.message);
return null;
}
}).filter(item => item);
} else {
try {
return require(`${base}/${pluralize.plural(relationship.data.type)}/${relationship.data.id}`);
} catch (e) {
console.error(e.message);
return null;
}
}
}).filter(item => item)
));
}
}

function get_path_relationship({ field, main_data }) {
const relationship = _.get(main_data, `relationships.${field}`);
if (Array.isArray(relationship.data)) {
return relationship.data.map(item => {
try {
return require(`${base}/${pluralize.plural(item.type)}/${item.id}`);
} catch (e) {
console.error(e.message);
return null;
}
}).filter(item => item);
} else {
try {
return require(`${base}/${pluralize.plural(relationship.data.type)}/${relationship.data.id}`);
} catch (e) {
console.error(e.message);
return null;
}
}
}
16 changes: 14 additions & 2 deletions test/fixtures/config/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
module.exports = {
schema: require('./schema'),
const config = require('../../../config');
const options = require('./options');

module.exports = () => {
try {
return config(options);
} catch (e) {
console.log(e.message);
if (e.message !== 'Jsonmonger Error: Global configuration cannot be set more than once.') {
throw e;
} else {
return options;
}
}
}
3 changes: 3 additions & 0 deletions test/fixtures/config/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
base_url: 'https://some.contrived.url',
}
15 changes: 0 additions & 15 deletions test/fixtures/config/schema/blockquote.js

This file was deleted.

4 changes: 0 additions & 4 deletions test/fixtures/config/schema/image.js

This file was deleted.

9 changes: 0 additions & 9 deletions test/fixtures/config/schema/index.js

This file was deleted.

4 changes: 0 additions & 4 deletions test/fixtures/config/schema/paragraph.js

This file was deleted.

6 changes: 0 additions & 6 deletions test/fixtures/config/schema/person.js

This file was deleted.

1 change: 0 additions & 1 deletion test/fixtures/config/schema/person_sparse.js

This file was deleted.

8 changes: 0 additions & 8 deletions test/fixtures/config/schema/post.js

This file was deleted.

5 changes: 0 additions & 5 deletions test/fixtures/config/schema/taxonomy.js

This file was deleted.

Loading