diff --git a/README.md b/README.md index 28e95d6..45cf667 100644 --- a/README.md +++ b/README.md @@ -365,6 +365,22 @@ const result = Serializer.serialize('article', data, 'default', {count: 2}, true Some others examples are available in [tests folders](https://github.com/danivek/json-api-serializer/blob/master/test/) +#### Serialize a relationship endpoint + +To serialize a [relationship endpoint](https://jsonapi.org/format/#fetching-relationships) (e.g. `GET /articles/1/relationships/tags`), use `serializeRelationshipData(type, data, [schema], [extraData])`. It returns a top-level document whose `data` contains [resource identifier objects](https://jsonapi.org/format/#document-resource-identifier-objects) only (`{ type, id }`), without `attributes` or nested `relationships`. + +```javascript +Serializer.serializeRelationshipData('tag', [{ id: '2', name: 'foo' }, { id: '3', name: 'bar' }]); +// { +// "data": [ +// { "type": "tag", "id": "2" }, +// { "type": "tag", "id": "3" } +// ] +// } +``` + +`data` can be full resource objects (only the `id` is read) or bare ids, a single value or an array. Empty to-one relationships serialize to `null` and empty to-many relationships to `[]`. + ### Deserialize input data (can be an simple object or an array of objects) diff --git a/index.d.ts b/index.d.ts index 12d810e..61de86b 100755 --- a/index.d.ts +++ b/index.d.ts @@ -52,6 +52,7 @@ declare class JSONAPISerializer { serialize(type: string | DynamicTypeOptions, data: any, schema?: string, extraData?: any, excludeData?: boolean, overrideSchemaOptions?: { [type: string]: Partial }): any; serializeAsync(type: string | DynamicTypeOptions, data: any, extraData?: any): Promise; serializeAsync(type: string | DynamicTypeOptions, data: any, schema?: string, extraData?: any, excludeData?: boolean, overrideSchemaOptions?: { [type: string]: Partial }): Promise; + serializeRelationshipData(type: string | DynamicTypeOptions, data: any, schema?: string, extraData?: any): any; deserialize(type: string | DynamicTypeOptions, data: any, schema?: string): any; deserializeAsync(type: string | DynamicTypeOptions, data: any, schema?: string): Promise; serializeError(error: Error | Error[] | ErrorWithStatus | ErrorWithStatus[] | { [key: string]: any } | { [key: string]: any }[]): any; diff --git a/lib/JSONAPISerializer.js b/lib/JSONAPISerializer.js index 103be1e..cea3357 100644 --- a/lib/JSONAPISerializer.js +++ b/lib/JSONAPISerializer.js @@ -123,6 +123,26 @@ module.exports = class JSONAPISerializer { }; } + /** + * Serialize the linkage for a relationship endpoint (e.g. `GET /articles/1/relationships/tags`). + * Produces a top-level document whose `data` is resource identifier objects only + * (`{ type, id }`), without `attributes` or nested `relationships`. + * @see {@link https://jsonapi.org/format/#fetching-relationships} + * @see {@link https://jsonapi.org/format/#document-resource-identifier-objects} + * @function JSONAPISerializer#serializeRelationshipData + * @param {string|Function} type the related resource's type. + * @param {object|object[]|string|string[]} data related resource(s) or their id(s). + * @param {string} [schema='default'] the related resource's schema name. + * @param {object} [extraData] additional data passed to type-resolving functions. + * @returns {object} a top-level document with linkage-only `data`. + */ + serializeRelationshipData(type, data, schema = 'default', extraData = {}) { + // Discard included: a relationship endpoint returns linkage only, not related resources. + return { + data: this.serializeRelationship(type, schema, data, new Map(), undefined, extraData), + }; + } + /** * Asynchronously serialize input data to a JSON API compliant response. * Input data can be a simple object or an array of objects. diff --git a/test/unit/JSONAPISerializer.test.js b/test/unit/JSONAPISerializer.test.js index 3a7810a..4edb7aa 100644 --- a/test/unit/JSONAPISerializer.test.js +++ b/test/unit/JSONAPISerializer.test.js @@ -1356,6 +1356,53 @@ describe('JSONAPISerializer', function() { }); }); + describe('serializeRelationshipData', function() { + const Serializer = new JSONAPISerializer(); + Serializer.register('tag', {}); + + it('should serialize to-many linkage as resource identifier objects only', function(done) { + const tags = [{id: '2', name: 'foo'}, {id: '3', name: 'bar'}]; + + const serializedData = Serializer.serializeRelationshipData('tag', tags); + + expect(serializedData.data).to.be.instanceof(Array).to.have.length(2); + expect(serializedData.data[0]).to.eql({type: 'tag', id: '2'}); + expect(serializedData.data[1]).to.eql({type: 'tag', id: '3'}); + expect(serializedData.data[0]).to.not.have.property('attributes'); + expect(serializedData.included).to.be.undefined; + done(); + }); + + it('should serialize to-one linkage as a single resource identifier object', function(done) { + const serializedData = Serializer.serializeRelationshipData('tag', {id: '2', name: 'foo'}); + + expect(serializedData.data).to.eql({type: 'tag', id: '2'}); + expect(serializedData.data).to.not.have.property('attributes'); + expect(serializedData.included).to.be.undefined; + done(); + }); + + it('should serialize linkage from bare ids', function(done) { + const serializedData = Serializer.serializeRelationshipData('tag', ['2', '3']); + + expect(serializedData.data[0]).to.eql({type: 'tag', id: '2'}); + expect(serializedData.data[1]).to.eql({type: 'tag', id: '3'}); + done(); + }); + + it('should serialize empty to-many linkage as an empty array', function(done) { + const serializedData = Serializer.serializeRelationshipData('tag', []); + expect(serializedData.data).to.eql([]); + done(); + }); + + it('should serialize empty to-one linkage as null', function(done) { + const serializedData = Serializer.serializeRelationshipData('tag', null); + expect(serializedData.data).to.eql(null); + done(); + }); + }); + describe('serializeAsync', function() { const Serializer = new JSONAPISerializer(); const dataArray = [{