Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/multi level includes #287

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ cache:
notifications:
email: false
node_js:
- '10'
- '8'
- '6'
- '4'
before_script:
- npm prune
- 'npm i -g jsinspect'
Expand Down
6 changes: 0 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -566,12 +566,6 @@ context.
The name of the property that is the primary key for the model. This is usually just
`id` unless defined differently in a model.json file.

###### `options.requestedIncludes`
The relationships that the user has requested be side loaded with the request.
For example, for the request `GET /api/posts?include=comments` options.requestedIncludes
would be `'comments'`.
- Type: `string` or `array`
- eg: `'comments'` or `['posts', 'comments']`

###### `options.host`
The host part of the url including any port information.
Expand Down
31 changes: 1 addition & 30 deletions lib/serialize.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,7 @@ module.exports = function (app, defaults) {
relatedModelPlural,
relatedModelPath,
relations,
model,
requestedIncludes
model

if (utils.shouldNotApplyJsonApi(ctx, defaults)) {
return next()
Expand Down Expand Up @@ -103,41 +102,13 @@ module.exports = function (app, defaults) {
}
}

// If we're sideloading, we need to add the includes
if (ctx.req.isSideloadingRelationships) {
requestedIncludes = utils.setRequestedIncludes(
ctx.req.remotingContext.args.filter.include
)
}

if (model.definition.settings.scope) {
// bring requestedIncludes in array form
if (typeof requestedIncludes === 'undefined') {
requestedIncludes = []
} else if (typeof requestedIncludes === 'string') {
requestedIncludes = [requestedIncludes]
}

// add include from model
var include = model.definition.settings.scope.include

if (typeof include === 'string') {
requestedIncludes.push(include)
} else if (_.isArray(include)) {
requestedIncludes = requestedIncludes.concat(
utils.setRequestedIncludes(include)
)
}
}

options = {
app: app,
model: model,
modelPath: path,
method: ctx.method.name,
meta: ctx.meta ? utils.clone(ctx.meta) : null,
primaryKeyField: primaryKeyField,
requestedIncludes: requestedIncludes,
host: defaults.host || utils.hostFromContext(ctx),
dataLinks: {
self: function (item) {
Expand Down
106 changes: 64 additions & 42 deletions lib/serializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,10 @@ function defaultSerialize (options, cb) {
resultData.data = result
if (options.meta) resultData.meta = options.meta

/**
* If we're requesting to sideload relationships...
*/
if (options.requestedIncludes) {
try {
handleIncludes(
resultData,
options.requestedIncludes,
options.relationships,
options
)
} catch (err) {
cb(err)
}
try {
handleIncludes(resultData, options.relationships, options)
} catch (err) {
cb(err)
}

options.results = resultData
Expand Down Expand Up @@ -339,34 +329,21 @@ function makeLinks (links, item) {
}

/**
* Handles serializing the requested includes to a seperate included property
* per JSON API spec.
* From resources, it returns an array of related resources, and turn the
* embedded resource into relationships with id/type couple.
* @private
* @memberOf {Serializer}
* @param {Object} resp
* @param {Array<String>|String}
* @param {Array} resources
* @param {Object} relations
* @param {Object} options
* @throws {Error}
* @return {undefined}
* @return {Array}
*/
function handleIncludes (resp, includes, relations, options) {
function getEmbeddedAndSubstituteEmbeddedForIds (resources, relations, options) {
var app = options.app

relations = utils.setIncludedRelations(relations, app)

var resources = _.isArray(resp.data) ? resp.data : [resp.data]

if (typeof includes === 'string') {
includes = [includes]
}

if (!_.isArray(includes)) {
throw RelUtils.getInvalidIncludesError(
'JSON API unable to detect valid includes'
)
}

var embedded = resources.map(function subsituteEmbeddedForIds (resource) {
return includes.map(function (include) {
return resources.map(function subsituteEmbeddedForIds (resource) {
return Object.keys(relations).map(function (include) {
var relation = relations[include]
var includedRelations = relations[include].relations
var propertyKey = relation.keyFrom
Expand All @@ -378,6 +355,7 @@ function handleIncludes (resp, includes, relations, options) {
} else {
plural = utils.pluralForModel(relation.modelTo)
}

var embeds = []

// If relation is belongsTo then pk and fk are the other way around
Expand All @@ -391,9 +369,11 @@ function handleIncludes (resp, includes, relations, options) {
)
}

resource.relationships[include] = resource.relationships[include] || {}

if (resource.relationships[include] && resource.attributes[include]) {
if (
resource.relationships &&
resource.relationships[include] &&
resource.attributes[include]
) {
if (_.isArray(resource.attributes[include])) {
embeds = resource.attributes[include].map(function (rel) {
rel = utils.clone(rel)
Expand All @@ -406,7 +386,14 @@ function handleIncludes (resp, includes, relations, options) {
options
)
})
embeds = _.compact(embeds)

var subEmbed = getEmbeddedAndSubstituteEmbeddedForIds(
embeds,
utils.setIncludedRelations(relations[include].relations || {}, app),
options
)
embeds = embeds.concat(subEmbed)

const included = resource.attributes[include]
resource.relationships[include].data = included.map(relData => {
return {
Expand All @@ -429,15 +416,47 @@ function handleIncludes (resp, includes, relations, options) {
id: String(resource.attributes[include][propertyKey]),
type: plural
}

embeds.push(compoundIncludes)

var subEmbeds = getEmbeddedAndSubstituteEmbeddedForIds(
embeds,
utils.setIncludedRelations(relations[include].relations || {}, app),
options
)
embeds = embeds.concat(subEmbeds)
}
delete resource.attributes[relation.keyFrom]
delete resource.attributes[relation.keyTo]
delete resource.attributes[include]
}
return embeds
return _.compact(embeds)
})
})
}

/**
* Handles serializing the requested includes to a seperate included property
* per JSON API spec.
* @private
* @memberOf {Serializer}
* @param {Object} resp
* @param {Array<String>|String}
* @throws {Error}
* @return {undefined}
*/
function handleIncludes (resp, relations, options) {
var app = options.app

relations = utils.setIncludedRelations(relations, app)

var resources = _.isArray(resp.data) ? resp.data : [resp.data]

var embedded = getEmbeddedAndSubstituteEmbeddedForIds(
resources,
relations,
options
)

if (embedded.length) {
// This array may contain duplicate models if the same item is referenced multiple times in 'data'
Expand All @@ -458,7 +477,10 @@ function handleIncludes (resp, includes, relations, options) {
unique.push(d)
}
})
resp.included = unique

if (unique.length) {
resp.included = unique
}
}
}

Expand Down
21 changes: 0 additions & 21 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ module.exports = {
shouldNotApplyJsonApi: shouldNotApplyJsonApi,
shouldApplyJsonApi: shouldApplyJsonApi,
relationFkOnModelFrom: relationFkOnModelFrom,
setRequestedIncludes: setRequestedIncludes,
setIncludedRelations: setIncludedRelations
}

Expand Down Expand Up @@ -278,23 +277,3 @@ function setIncludedRelations (relations, app) {
}
return relations
}

function setRequestedIncludes (include) {
if (!include) return undefined

if (typeof include === 'string') {
return include
}
if (include instanceof Array) {
return include.map(function (inc) {
if (typeof inc === 'string') {
return inc
}

if (inc instanceof Object) {
return inc.relation
}
})
}
return include
}
Loading