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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
47 changes: 28 additions & 19 deletions lib/JSONAPISerializer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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({}),
Expand Down Expand Up @@ -604,26 +604,17 @@ 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;
if (!data[relationship] && rOptions.alternativeKey) {
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
Expand All @@ -633,6 +624,7 @@ module.exports = class JSONAPISerializer {
};
}

// Convert case
relationship = (options.convertCase) ? this._convertCase(relationship, options.convertCase) : relationship;

_.set(serializedRelationships, relationship, serializeRelationship);
Expand All @@ -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;
Expand All @@ -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)) {
Expand All @@ -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;
}
Expand Down
148 changes: 88 additions & 60 deletions test/unit/JSONAPISerializer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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');
Expand All @@ -313,17 +345,17 @@ 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();
});

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);
Expand All @@ -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');
Expand All @@ -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();
Expand All @@ -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);
Expand All @@ -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() {
Expand Down Expand Up @@ -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() {
Expand Down