diff --git a/README.md b/README.md index cd9202e..a8de1f5 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ Serializer.register(type, options); * A _function_ with one argument `function(extraData) { ... }` or with two arguments `function(data, extraData) { ... }` * **relationships** (optional): An object defining some relationships * relationship: The property in data to use as a relationship - * **type**: The type to use for serializing the relationship (type need to be register) + * **type**: A _string_ or a _function_ `function(relationshipData, data) { ... }` for the type to use for serializing the relationship (type need to be register). * **alternativeKey** (optional): An alternative key (string or path) to use if relationship key not exist (example: 'author_id' as an alternative key for 'author' relationship). See [issue #12](https://github.com/danivek/json-api-serializer/issues/12). * **schema** (optional): A custom schema for serializing the relationship. If no schema define, it use the default one. * **links** (optional): Describes the links for the relationship. It can be: diff --git a/lib/JSONAPISerializer.js b/lib/JSONAPISerializer.js index c0fd122..8a8def0 100644 --- a/lib/JSONAPISerializer.js +++ b/lib/JSONAPISerializer.js @@ -40,7 +40,7 @@ module.exports = class JSONAPISerializer { whitelist: joi.array().items(joi.string()).single().default([]), links: joi.alternatives([joi.func(), joi.object()]).default({}), relationships: joi.object().pattern(/.+/, joi.object({ - type: joi.string().required(), + type: joi.alternatives([joi.func(), joi.string()]).required(), alternativeKey: joi.string(), schema: joi.string().default('default'), links: joi.alternatives([joi.func(), joi.object()]).default({}), @@ -604,7 +604,6 @@ module.exports = class JSONAPISerializer { Object.keys(options.relationships).forEach((relationship) => { const rOptions = options.relationships[relationship]; - const schema = rOptions.schema || 'default'; // Support alternativeKey options for relationships let relationshipKey = relationship; @@ -612,18 +611,10 @@ module.exports = class JSONAPISerializer { relationshipKey = rOptions.alternativeKey; } - if (!this.schemas[rOptions.type]) { - throw new Error(`No type registered for "${rOptions.type}" on "${relationship}" relationship`); - } - - if (!this.schemas[rOptions.type][schema]) { - throw new Error(`No schema "${rOptions.schema}" registered for type "${rOptions.type}" on "${relationship}" relationship`); - } - let serializeRelationship = { links: this.processOptionsValues(data, extraData, rOptions.links), meta: this.processOptionsValues(data, extraData, rOptions.meta), - data: this.serializeRelationship(rOptions.type, _.get(data, relationshipKey), this.schemas[rOptions.type][schema], included, extraData), + data: this.serializeRelationship(rOptions.type, rOptions.schema, _.get(data, relationshipKey), included, data, extraData), }; // Avoid empty relationship object @@ -633,6 +624,7 @@ module.exports = class JSONAPISerializer { }; } + // Convert case relationship = (options.convertCase) ? this._convertCase(relationship, options.convertCase) : relationship; _.set(serializedRelationships, relationship, serializeRelationship); @@ -647,14 +639,17 @@ module.exports = class JSONAPISerializer { * @see {@link http://jsonapi.org/format/#document-resource-object-linkage} * @method JSONAPISerializer#serializeRelationship * @private - * @param {string} rType the relationship's type. + * @param {string|Function} rType the relationship's type. + * @param {string} rSchema the relationship's schema * @param {Object|Object[]} rData relationship's data. - * @param {Object} rOptions relationship's configuration options. * @param {Object[]} included. + * @param {Object} the entire resource's data. * @param {Object} extraData additional data. * @return {Object|Object[]} serialized relationship data. */ - serializeRelationship(rType, rData, rOptions, included, extraData) { + serializeRelationship(rType, rSchema, rData, included, data, extraData) { + const schema = rSchema || 'default'; + // No relationship data if (rData === undefined) { return undefined; @@ -668,13 +663,27 @@ module.exports = class JSONAPISerializer { // To-many relationships if (Array.isArray(rData)) { - return rData.map(d => this.serializeRelationship(rType, d, rOptions, included, extraData)); + return rData.map(d => this.serializeRelationship(rType, schema, d, included, data, extraData)); + } + + // Resolve relationship type + const type = (typeof rType === 'function') ? rType(rData, data) : rType; + + if (!type) { + throw new Error(`No type can be resolved from relationship's data: ${JSON.stringify(rData)}`); + } + + if (!this.schemas[type]) { + throw new Error(`No type registered for "${type}"`); + } + + if (!this.schemas[type][schema]) { + throw new Error(`No schema "${schema}" registered for type "${type}"`); } // To-one relationship - const serializedRelationship = { - type: rType, - }; + const rOptions = this.schemas[type][schema]; + const serializedRelationship = { type }; // Support for unpopulated relationships (an id, or array of ids) if (!_.isObjectLike(rData)) { @@ -685,7 +694,7 @@ module.exports = class JSONAPISerializer { } else { // Relationship has been populated serializedRelationship.id = rData[rOptions.id].toString(); - included.push(this.serializeData(rType, rData, rOptions, included, extraData)); + included.push(this.serializeData(type, rData, rOptions, included, extraData)); } return serializedRelationship; } diff --git a/test/unit/JSONAPISerializer.test.js b/test/unit/JSONAPISerializer.test.js index 399a333..a71d4b4 100644 --- a/test/unit/JSONAPISerializer.test.js +++ b/test/unit/JSONAPISerializer.test.js @@ -263,30 +263,62 @@ describe('JSONAPISerializer', function() { }, }); + it('should throw an error if type as not been registered', function(done) { + const included = []; + const SerializerError = new JSONAPISerializer(); + + expect(function() { + SerializerError.serializeRelationship('people', 'default', '1', included); + }).to.throw(Error, 'No type registered for "people"'); + done(); + }); + + it('should throw an error if custom schema as not been registered on a relationship', function(done) { + const included = []; + const SerializerError = new JSONAPISerializer(); + SerializerError.register('people'); + + expect(function() { + SerializerError.serializeRelationship('people', 'custom', '1', included); + }).to.throw(Error, 'No schema "custom" registered for type "people"'); + done(); + }); + + it('should throw an error if no type can be resolved', function(done) { + const included = []; + const SerializerError = new JSONAPISerializer(); + const typeFn = (data) => data.notype; + + expect(function() { + SerializerError.serializeRelationship(typeFn, 'default', '1', included); + }).to.throw(Error, 'No type can be resolved from relationship\'s data: "1"'); + done(); + }); + it('should return undefined for an undefined relationship data', function(done) { - const serializedRelationshipData = Serializer.serializeRelationship('articles', undefined); + const serializedRelationshipData = Serializer.serializeRelationship('articles', 'default', undefined); expect(serializedRelationshipData).to.eql(undefined); done(); }); it('should return null for an empty single relationship data', function(done) { - const serializedRelationshipData = Serializer.serializeRelationship('articles', {}); + const serializedRelationshipData = Serializer.serializeRelationship('articles', 'default', {}); expect(serializedRelationshipData).to.eql(null); done(); }); it('should return empty array for an empty array of relationship data', function(done) { - const serializedRelationshipData = Serializer.serializeRelationship('articles', []); + const serializedRelationshipData = Serializer.serializeRelationship('articles', 'default', []); expect(serializedRelationshipData).to.eql([]); done(); }); it('should return serialized relationship data and populated included for a to one populated relationship', function(done) { const included = []; - const serializedRelationshipData = Serializer.serializeRelationship('authors', { + const serializedRelationshipData = Serializer.serializeRelationship('authors', 'default', { id: '1', name: 'Author 1', - }, Serializer.schemas.authors.default, included); + }, included); expect(serializedRelationshipData).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData).to.have.property('id').to.be.a('string').to.eql('1'); expect(included).to.have.lengthOf(1); @@ -295,13 +327,13 @@ describe('JSONAPISerializer', function() { it('should return serialized relationship data and populated included for a to many populated relationships', function(done) { const included = []; - const serializedRelationshipData = Serializer.serializeRelationship('authors', [{ + const serializedRelationshipData = Serializer.serializeRelationship('authors', 'default', [{ id: '1', name: 'Author 1', }, { id: '2', name: 'Author 2', - }], Serializer.schemas.authors.default, included); + }], included); expect(serializedRelationshipData).to.be.instanceof(Array).to.have.lengthOf(2); expect(serializedRelationshipData[0]).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData[0]).to.have.property('id').to.be.a('string').to.eql('1'); @@ -313,9 +345,9 @@ describe('JSONAPISerializer', function() { it('should return type of string for a to one populated relationship with non string id', function(done) { const included = []; - const serializedRelationshipData = Serializer.serializeRelationship('authors', { + const serializedRelationshipData = Serializer.serializeRelationship('authors', 'default', { id: 1 - }, Serializer.schemas.authors.default, included); + }, included); expect(serializedRelationshipData).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData).to.have.property('id').to.be.a('string').to.eql('1'); done(); @@ -323,7 +355,7 @@ describe('JSONAPISerializer', function() { it('should return serialized relationship data and empty included for a to one unpopulated relationship', function(done) { const included = []; - const serializedRelationshipData = Serializer.serializeRelationship('authors', '1', Serializer.schemas.authors.default, included); + const serializedRelationshipData = Serializer.serializeRelationship('authors', 'default', '1', included); expect(serializedRelationshipData).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData).to.have.property('id').to.be.a('string').to.eql('1'); expect(included).to.have.lengthOf(0); @@ -332,7 +364,7 @@ describe('JSONAPISerializer', function() { it('should return serialized relationship data and empty included for a to many unpopulated relationship', function(done) { const included = []; - const serializedRelationshipData = Serializer.serializeRelationship('authors', ['1', '2'], Serializer.schemas.authors.default, included); + const serializedRelationshipData = Serializer.serializeRelationship('authors', 'default', ['1', '2'], included); expect(serializedRelationshipData).to.be.instanceof(Array).to.have.lengthOf(2); expect(serializedRelationshipData[0]).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData[0]).to.have.property('id').to.be.a('string').to.eql('1'); @@ -344,14 +376,14 @@ describe('JSONAPISerializer', function() { it('should return type of string for a to one unpopulated relationship with non string id', function(done) { const included = []; - const serializedRelationshipData = Serializer.serializeRelationship('authors', 1, Serializer.schemas.authors.default, included); + const serializedRelationshipData = Serializer.serializeRelationship('authors', 'default', 1, included); expect(serializedRelationshipData).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData).to.have.property('id').to.be.a('string').to.eql('1'); done(); }); it('should return serialized relationship with unpopulated relationship with mongoDB BSON ObjectID', function(done) { - const serializedRelationshipData = Serializer.serializeRelationship('authors', new ObjectID(), Serializer.schemas.authors.default, []); + const serializedRelationshipData = Serializer.serializeRelationship('authors', 'default', new ObjectID(), []); expect(serializedRelationshipData).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData).to.have.property('id').to.be.a('string'); done(); @@ -365,11 +397,11 @@ describe('JSONAPISerializer', function() { }); const included = []; - const serializedRelationshipData = Serializer2.serializeRelationship('authors', { + const serializedRelationshipData = Serializer2.serializeRelationship('authors', 'only-name', { id: '1', name: 'Author 1', gender: 'male' - }, Serializer2.schemas.authors['only-name'], included); + }, included); expect(serializedRelationshipData).to.have.property('type').to.eql('authors'); expect(serializedRelationshipData).to.have.property('id').to.eql('1'); expect(included).to.have.lengthOf(1); @@ -380,6 +412,47 @@ describe('JSONAPISerializer', function() { expect(included[0].attributes).to.not.have.property('gender'); done(); }); + + it('should serialize relationship with dynamic type', function(done) { + const included = []; + const typeFn = (data) => data.type; + + const Serializer = new JSONAPISerializer(); + Serializer.register('people'); + Serializer.register('author'); + + const data = [{ + type: 'people', + id: '1', + name: 'Roman Nelson' + }, { + type: 'author', + id: '1', + firstName: 'Kaley', + lastName: 'Maggio' + }] + + const serializedRelationships = Serializer.serializeRelationship(typeFn, 'default', data, included); + expect(serializedRelationships).to.deep.equal([ + { type: 'people', id: '1' }, + { type: 'author', id: '1' } + ]); + expect(included).to.deep.equal([ + { + type: 'people', + id: '1', + attributes: { type: 'people', name: 'Roman Nelson' }, + relationships: undefined, + links: undefined + }, { + type: 'author', + id: '1', + attributes: { type: 'author', firstName: 'Kaley', lastName: 'Maggio' }, + relationships: undefined, + links: undefined } + ]); + done(); + }); }); describe('serializeRelationships', function() { @@ -496,51 +569,6 @@ describe('JSONAPISerializer', function() { expect(serializedRelationships.author).to.have.property('links').to.be.undefined; done(); }); - - it('should throw an error if type as not been registered on a relationship', function(done) { - const included = []; - const Serializer = new JSONAPISerializer(); - - Serializer.register('article', { - relationships: { - author: { - type: 'people', - } - } - }); - - expect(function() { - Serializer.serializeRelationships({ - id: '1', - author: '1' - }, Serializer.schemas.article.default, included); - }).to.throw(Error, 'No type registered for "people" on "author" relationship'); - done(); - }); - - it('should throw an error if custom schema as not been registered on a relationship', function(done) { - const included = []; - const Serializer = new JSONAPISerializer(); - - Serializer.register('article', { - relationships: { - author: { - type: 'people', - schema: 'custom' - } - } - }); - - Serializer.register('people'); - - expect(function() { - Serializer.serializeRelationships({ - id: '1', - author: '1' - }, Serializer.schemas.article.default, included); - }).to.throw(Error, 'No schema "custom" registered for type "people" on "author" relationship'); - done(); - }); }); describe('serializeAttributes', function() {