From 5ac5b2d0a04f08999bdf9f41caccd2303850585d Mon Sep 17 00:00:00 2001 From: Edward Date: Fri, 1 Sep 2023 17:46:01 +0100 Subject: [PATCH] feat: add polymprphic serializer Use default serializer when none defined for child type --- package.json | 3 +- src/classes/polymorphic-serialiser.ts | 84 +++++++++++++++++++ src/index.ts | 1 + test/issue-65.test.ts | 113 ++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 src/classes/polymorphic-serialiser.ts create mode 100644 test/issue-65.test.ts diff --git a/package.json b/package.json index a17049e..6d787e3 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "lint:eslint": "eslint .", "playground": "ts-node ./benchmarks/playground.benchmark", "prepare": "husky install", - "test": "jest --runInBand --verbose --coverage" + "test": "jest --runInBand --verbose --coverage", + "test:watch": "jest --runInBand --verbose --watch" }, "publishConfig": { "access": "public", diff --git a/src/classes/polymorphic-serialiser.ts b/src/classes/polymorphic-serialiser.ts new file mode 100644 index 0000000..fc57355 --- /dev/null +++ b/src/classes/polymorphic-serialiser.ts @@ -0,0 +1,84 @@ +import { DataDocument } from '../interfaces/json-api.interface'; +import { SerializerOptions } from '../interfaces/serializer.interface'; +import ResourceIdentifier from '../models/resource-identifier.model'; +import Resource from '../models/resource.model'; +import { Dictionary, nullish, SingleOrArray } from '../types/global.types'; +import { Helpers } from '../utils/serializer.utils'; +import Relator from './relator'; +import Serializer from './serializer'; + +export default class PolymorphicSerializer< + PrimaryType extends Dictionary +> extends Serializer { + private serialisers: Record; + + private key: keyof PrimaryType; + + public constructor( + commonName: string, + key: keyof PrimaryType, + serializers: Record + ) { + super(commonName); + this.serialisers = serializers; + this.key = key; + } + + public async serialize( + data: SingleOrArray | nullish, + options?: Partial> + ): Promise>> { + if (Array.isArray(data)) { + data.map((d) => { + return this.serializeSingle(d, options); + }); + } else if (data) { + return this.serializeSingle(data, options); + } + + return Object.values(this.serialisers)[0].serialize(data, options); + } + + public createIdentifier( + data: PrimaryType, + options?: SerializerOptions + ): ResourceIdentifier { + const serializer = this.getSerializerForData(data); + if (serializer) { + return serializer.createIdentifier(data, options); + } + return super.createIdentifier(data, options); + } + + public async createResource( + data: PrimaryType, + options?: Partial>, + helpers?: Helpers, + relatorDataCache?: Map, Dictionary[]> + ): Promise> { + const serializer = this.getSerializerForData(data); + if (serializer) { + return serializer.createResource(data, options, helpers, relatorDataCache); + } + return super.createResource(data, options, helpers, relatorDataCache); + } + + private async serializeSingle( + data: PrimaryType, + options?: Partial> + ) { + const serializer = this.getSerializerForData(data); + if (serializer) { + return serializer.serialize(data, options); + } + return super.serialize(data, options); + } + + private getSerializerForData(data: PrimaryType): Serializer | null { + if (this.serialisers[data[this.key]]) { + return this.serialisers[data[this.key]]; + } + + return null; + } +} diff --git a/src/index.ts b/src/index.ts index 91ab78b..9883eec 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export { default as Relator } from './classes/relator'; export { default as JapiError } from './models/error.model'; export { default as ErrorSerializer } from './classes/error-serializer'; export { default as Serializer } from './classes/serializer'; +export { default as PolymorphicSerializer } from './classes/polymorphic-serialiser'; export * from './interfaces/cache.interface'; export * from './interfaces/error-serializer.interface'; export * from './interfaces/error.interface'; diff --git a/test/issue-65.test.ts b/test/issue-65.test.ts new file mode 100644 index 0000000..d26531c --- /dev/null +++ b/test/issue-65.test.ts @@ -0,0 +1,113 @@ +import { PolymorphicSerializer, Relator, Serializer } from '../lib'; +import ResourceIdentifier from '../lib/models/resource-identifier.model'; +import Resource from '../lib/models/resource.model'; + +describe('Issue #65 - Polymorphic relator', () => { + class Model { + id: string; + + children: Child[]; + } + + abstract class Child { + public type: string; + + constructor(public id: string) {} + } + + class Child1 extends Child { + constructor(id: string, public child1: string) { + super(id); + this.type = 'type:Child1'; + } + } + + class Child2 extends Child { + constructor(id: string, public child2: string) { + super(id); + this.type = 'type:Child2'; + } + } + + class Child3 extends Child { + constructor(id: string, public child2: string) { + super(id); + this.type = 'type:Child3'; + } + } + + it('should work non-polymorphicly', async () => { + const model: Model = new Model(); + const child1: Child1 = new Child1('1', 'child1'); + const child2: Child2 = new Child2('2', 'child2'); + + model.id = '1'; + model.children = [child1, child2]; + + const Child1Serializer = new Serializer('Child'); + + const relator = new Relator( + async (obj) => obj.children, + () => Child1Serializer, + { relatedName: 'children' } + ); + + const ModelSerializer = new Serializer('Model', { + relators: [relator], + projection: { + children: 0, + }, + }); + + const data = (await ModelSerializer.serialize(model)) as { data: Resource }; + + expect(data.data).toBeInstanceOf(Resource); + expect(data.data.relationships?.children.data).toHaveLength(2); + expect(data.data.relationships?.children.data?.[0].id).toEqual('1'); + expect(data.data.relationships?.children.data?.[1].id).toEqual('2'); + + expect(data.data.relationships?.children.data?.[0].type).toEqual('Child'); + expect(data.data.relationships?.children.data?.[1].type).toEqual('Child'); + }); + + it('should work polymorphicly', async () => { + const model: Model = new Model(); + const child1: Child1 = new Child1('1', 'child1'); + const child2: Child2 = new Child2('2', 'child2'); + const child3: Child2 = new Child3('3', 'child3'); + + model.id = '1'; + model.children = [child1, child2, child3]; + + const Child1Serializer = new Serializer('Child1'); + const Child2Serializer = new Serializer('Child2'); + + const PolySerializer = new PolymorphicSerializer('Child', 'type', { + 'type:Child1': Child1Serializer, + 'type:Child2': Child2Serializer, + }); + + const relator = new Relator(async (obj) => obj.children, PolySerializer, { + relatedName: 'children', + }); + + const ModelSerializer = new Serializer('Model', { + relators: [relator], + projection: { + children: 0, + }, + }); + + const data = (await ModelSerializer.serialize(model)) as { data: Resource }; + + expect(data.data).toBeInstanceOf(Resource); + expect(data.data.relationships?.children.data).toHaveLength(3); + expect(data.data.relationships?.children.data?.[0].id).toEqual('1'); + expect(data.data.relationships?.children.data?.[1].id).toEqual('2'); + expect(data.data.relationships?.children.data?.[2].id).toEqual('3'); + + expect(data.data.relationships?.children.data?.[0].type).toEqual('Child1'); + expect(data.data.relationships?.children.data?.[1].type).toEqual('Child2'); + expect(data.data.relationships?.children.data?.[2].type).toEqual('Child'); + }); +});