From 8b173aba535d013e8d6d3f28c2092be1214e4f0c Mon Sep 17 00:00:00 2001 From: James Dixon Date: Thu, 22 Sep 2016 23:46:20 -0600 Subject: [PATCH 1/2] adds ability to serialize belongsToMany relations into meta --- spec/bookshelf-spec.ts | 41 +++++++++++++++++++++++++++++++++ src/bookshelf/extras.ts | 1 + src/bookshelf/index.ts | 4 ++-- src/bookshelf/utils.ts | 50 ++++++++++++++++++++++++++++++++++------- 4 files changed, 86 insertions(+), 10 deletions(-) diff --git a/spec/bookshelf-spec.ts b/spec/bookshelf-spec.ts index f33b112..c6782f7 100644 --- a/spec/bookshelf-spec.ts +++ b/spec/bookshelf-spec.ts @@ -955,6 +955,47 @@ describe('Bookshelf relations', () => { }); + it('should add relationships object with meta data for belongsTO', () => { + let model: Model = bookshelf.Model.forge({id: '5', attr: 'value1'}); + + let relation1: Model = bookshelf.Model.forge({id: '10', attr: 'value2'}); + (relation1 as any).pivot = bookshelf.Model.forge({id: '10', attr: 'test'}); + + let relation2: Model = bookshelf.Model.forge({id: '11', attr: 'value3'}); + (model as any).relations['related-model'] = bookshelf.Collection.forge([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); + + }); + it('should put the single related object in the included array', () => { let model: Model = bookshelf.Model.forge({id: '5', atrr: 'value'}); (model as any).relations['related-model'] = bookshelf.Model.forge({id: '10', attr2: 'value2'}); diff --git a/src/bookshelf/extras.ts b/src/bookshelf/extras.ts index 889eede..9d12597 100644 --- a/src/bookshelf/extras.ts +++ b/src/bookshelf/extras.ts @@ -28,6 +28,7 @@ export interface Attributes { export interface Model extends BModel { id: any; attributes: Attributes; + pivot: Model; relations: RelationsObject; } diff --git a/src/bookshelf/index.ts b/src/bookshelf/index.ts index 71f1dec..2ffbbd7 100644 --- a/src/bookshelf/index.ts +++ b/src/bookshelf/index.ts @@ -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); @@ -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); } } diff --git a/src/bookshelf/utils.ts b/src/bookshelf/utils.ts index 425fdd8..e01f2f3 100644 --- a/src/bookshelf/utils.ts +++ b/src/bookshelf/utils.ts @@ -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, isNil, isEmpty, + escapeRegExp, forEach, forOwn, has, keys, mapValues, merge, pick, reduce } from 'lodash'; import { SerialOpts } from 'jsonapi-serializer'; import { LinkOpts } from '../links'; @@ -21,6 +21,7 @@ import { BookOpts, Data, Model, isModel, isCollection } from './extras'; export interface Information { bookOpts: BookOpts; linkOpts: LinkOpts; + serialOpts?: SerialOpts } /** @@ -28,7 +29,7 @@ export interface 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)); @@ -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; let template: SerialOpts = {}; @@ -72,6 +74,9 @@ 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); }); @@ -79,6 +84,16 @@ function processSample(info: Information, sample: Model): SerialOpts { return template; } +function relationshipMeta(relation: Model, models: Array) { + return reduce(models, (result: Array, rel: Model): Array => { + 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 @@ -183,7 +198,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; @@ -194,13 +209,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; From b3474c173172f39c279bbcec47f08234e040a22b Mon Sep 17 00:00:00 2001 From: James Dixon Date: Fri, 23 Sep 2016 14:45:06 -0600 Subject: [PATCH 2/2] ignore pivot when relation isn't a belongsToMany --- src/bookshelf/utils.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/bookshelf/utils.ts b/src/bookshelf/utils.ts index e01f2f3..5111d0b 100644 --- a/src/bookshelf/utils.ts +++ b/src/bookshelf/utils.ts @@ -6,7 +6,7 @@ 'use strict'; -import { assign, clone, cloneDeep, differenceWith, get, includes, intersection, isNil, isEmpty, +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'; @@ -84,14 +84,16 @@ function processSample(info: Information, sample: Model): SerialOpts { return template; } -function relationshipMeta(relation: Model, models: Array) { - return reduce(models, (result: Array, rel: Model): Array => { - if (rel.pivot) { - result.push(rel.pivot); - } +function relationshipMeta(relation, models) { + if (isArray(models)) { + return reduce(models, (result: Array, rel: Model): Array => { + if (rel.pivot) { + result.push(rel.pivot); + } - return result; - }, []); + return result; + }, []); + } } /**