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

Automatically serialize belongsToMany into relation meta attribute #74

Open
wants to merge 2 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
41 changes: 41 additions & 0 deletions spec/bookshelf-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,47 @@ describe('Bookshelf relations', () => {

});

it('should add relationships object with meta data for belongsTO', () => {
let model: Model = bookshelf.Model.forge<any>({id: '5', attr: 'value1'});

let relation1: Model = bookshelf.Model.forge<any>({id: '10', attr: 'value2'});
(relation1 as any).pivot = bookshelf.Model.forge<any>({id: '10', attr: 'test'});

let relation2: Model = bookshelf.Model.forge<any>({id: '11', attr: 'value3'});
(model as any).relations['related-model'] = bookshelf.Collection.forge<any>([relation1, relation2]);

let result: any = mapper.map(model, 'model', { relations: { included: false }});

let expected: any = {
data: {
id: '5',
type: 'models',
attributes: {
attr: 'value1'
},
relationships: {
'related-model': {
data: [{
id: '10',
type: 'related-models'
}, {
id: '11',
type: 'related-models'
}],
meta: {
data: [
{ id: '10', attr: 'test' }
]
}
}
}
}
};

expect(_.matches(expected)(result)).toBe(true);

});

Copy link
Collaborator

Choose a reason for hiding this comment

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

Add a test for the attribute omission done

it('should put the single related object in the included array', () => {
let model: Model = bookshelf.Model.forge<any>({id: '5', atrr: 'value'});
(model as any).relations['related-model'] = bookshelf.Model.forge<any>({id: '10', attr2: 'value2'});
Expand Down
1 change: 1 addition & 0 deletions src/bookshelf/extras.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export interface Attributes {
export interface Model extends BModel<any> {
id: any;
attributes: Attributes;
pivot: Model;
relations: RelationsObject;
}

Expand Down
4 changes: 2 additions & 2 deletions src/bookshelf/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export default class Bookshelf implements Mapper {
// Set default values for the options
defaultsDeep(bookOpts, {relations: { included: true }, enableLinks: true, omitAttrs: []});

let info: Information = { bookOpts, linkOpts };
let info: Information = { bookOpts, linkOpts, serialOpts: this.serialOpts };

let template: SerialOpts = processData(info, data);

Expand All @@ -51,7 +51,7 @@ export default class Bookshelf implements Mapper {
assign(template, { typeForAttribute }, this.serialOpts);

// Return the data in JSON API format
let json: any = toJSON(data);
let json: any = toJSON(data, bookOpts);
return new Serializer(type, template).serialize(json);
}
}
52 changes: 44 additions & 8 deletions src/bookshelf/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

'use strict';

import { assign, clone, cloneDeep, differenceWith, includes, intersection, isNil,
escapeRegExp, forOwn, has, keys, mapValues, merge, reduce } from 'lodash';
import { assign, clone, cloneDeep, differenceWith, get, includes, intersection, isArray, isNil, isEmpty,
escapeRegExp, forEach, forOwn, has, keys, mapValues, merge, pick, reduce } from 'lodash';

import { SerialOpts } from 'jsonapi-serializer';
import { LinkOpts } from '../links';
Expand All @@ -21,14 +21,15 @@ import { BookOpts, Data, Model, isModel, isCollection } from './extras';
export interface Information {
bookOpts: BookOpts;
linkOpts: LinkOpts;
serialOpts?: SerialOpts
}

/**
* Start the data processing with top level information,
* then handle resources recursively in processSample
*/
export function processData(info: Information, data: Data): SerialOpts {
let { bookOpts: { enableLinks }, linkOpts }: Information = info;
let { bookOpts: { enableLinks }, linkOpts, serialOpts }: Information = info;

let template: SerialOpts = processSample(info, sample(data));

Expand All @@ -40,12 +41,13 @@ export function processData(info: Information, data: Data): SerialOpts {
return template;
}


/**
* Recursively adds data-related properties to the
* template to be sent to the serializer
*/
function processSample(info: Information, sample: Model): SerialOpts {
let { bookOpts, linkOpts }: Information = info;
let { bookOpts, linkOpts, serialOpts }: Information = info;
let { enableLinks }: BookOpts = bookOpts;

Copy link
Collaborator

Choose a reason for hiding this comment

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

Adding it here, like:

const defaultRelationshipMeta = function (relation, models) {
  if (isArray(models)) { ... }
}
const { relationshipMeta = defaultRelationshipMeta } = seralOpts;

let template: SerialOpts = {};
Expand All @@ -72,13 +74,28 @@ function processSample(info: Information, sample: Model): SerialOpts {
relTemplate.included = false;
}

// Add a relation meta function that will add pivot data if it exists
relTemplate.relationshipMeta = { data: get(serialOpts, 'relationshipMeta') || relationshipMeta };

template[relName] = relTemplate;
template.attributes.push(relName);
});

return template;
}

function relationshipMeta(relation, models) {
Copy link
Collaborator

Choose a reason for hiding this comment

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

I would not declare this relationshipMeta function here, because it is just the default function to use in case none was passed.

I would declare it where it is being used as a default.

if (isArray(models)) {
return reduce(models, (result: Array<Object>, rel: Model): Array<Object> => {
if (rel.pivot) {
result.push(rel.pivot);
}

return result;
}, []);
}
}

/**
* Convert any data into a model representing
* a complete sample to be used in the template generation
Expand Down Expand Up @@ -183,7 +200,7 @@ function includeAllowed(bookOpts: BookOpts, relName: string): boolean {
* Convert a bookshelf model or collection to
* json adding the id attribute if missing
*/
export function toJSON(data: Data): any {
export function toJSON(data: Data, bookOpts: BookOpts): any {

let json: any = null;

Expand All @@ -194,13 +211,32 @@ export function toJSON(data: Data): any {
if (!has(json, 'id')) { json.id = data.id; }

// Loop over model relations to call toJSON recursively on them
forOwn(data.relations, function (relData: Data, relName: string): void {
json[relName] = toJSON(relData);
forOwn(data.relations, function (relData: any, relName: string): void {

// When a Bookshelf Model is serialized with `{ shallow: true }`, the `pivot` data is not not passed along and therefore,
// a function passed to the serializer will not have the necessary data to create any meta data.
// That said, we need to pass along the pivot data when serializing the model to JSON.
forEach(relData.models, (rel: Model) => {

if (rel.pivot) {
// Run the pivot data through the omit attrs function to remove anything unwanted.
// NOTE: Bookshelf returns the pivot table keys by default so `omitAttrs` is a good idea here.
let attrs: Object = pick(rel.pivot.attributes, getAttrsList(rel.pivot, bookOpts));

// If there are attrs that don't meet the `omitAttrs` criteria, then
// add those attrs plus the model id to the pivot payload
if (!isEmpty(attrs)) {
rel.attributes['pivot'] = assign(attrs, { [rel.idAttribute]: rel.id });
}
}
});

json[relName] = toJSON(relData, bookOpts);
});

} else if (isCollection(data)) {
// Run a recursive toJSON on each model of the collection
json = data.map(toJSON);
json = data.map((model) => toJSON(model, bookOpts));
}

return json;
Expand Down