diff --git a/__tests__/serializers/serializeResourceObject.test.ts b/__tests__/serializers/serializeResourceObject.test.ts index ed080e7..30a487b 100644 --- a/__tests__/serializers/serializeResourceObject.test.ts +++ b/__tests__/serializers/serializeResourceObject.test.ts @@ -1,5 +1,464 @@ +import { Chance } from 'chance'; +import { linksSymbol } from '../../src/decorators/links'; +import { metaSymbol } from '../../src/decorators/meta'; +import { idSymbol } from '../../src/decorators/id'; +import { relationshipsSymbol } from '../../src/decorators/relationship'; +import { resourceSymbol } from '../../src/decorators/resource'; +import { serializeResourceObject } from '../../src/serializers/serializeResourceObject'; +import { serializeResourceRelationshipObject } from '../../src/serializers/serializeResourceRelationshipObject'; +import { collect } from '../../src/serializers/utils/collect'; +import { getMetadataBySymbol } from '../../src/serializers/utils/getMetadataBySymbol'; + +jest.mock('../../src/serializers/utils/getMetadataBySymbol'); +const getMetadataBySymbolMocked = jest.mocked(getMetadataBySymbol); + +jest.mock('../../src/serializers/utils/collect'); +const collectMocked = jest.mocked(collect); + +jest.mock('../../src/serializers/serializeResourceRelationshipObject'); +const serializeResourceRelationshipObjectMocked = jest.mocked( + serializeResourceRelationshipObject, +); + +jest.mock('../../src/serializers/utils/assertMetadataIsPresent'); + describe('`serializeResourceObject`', () => { - it('', () => { - expect(true).toBe(true); + let chance: Chance.Chance; + + beforeEach(() => { + chance = new Chance(); + }); + + describe('`relationships`', () => { + describe('when the related class instance is an array', () => { + it('should throw an error if an element in the array is not an object', () => { + const key = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === relationshipsSymbol) { + return [[key, '']]; + } + + return undefined; + }, + ); + + const classInstance = { + [key]: ['not-an-object'], + }; + + expect(() => serializeResourceObject(classInstance)).toThrow( + `Failed to serialize resource relationship object for ${key} becuase not all elements in the array are objects.`, + ); + }); + + it('should serialize defined resource relationship objects and return them', () => { + const key = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + if (symbol === relationshipsSymbol) { + return [[key, '']]; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + return undefined; + }); + + const relatedClassInstance = { + a: chance.string(), + }; + + const secondRelatedClassInstance = { + c: chance.string(), + }; + + const classInstance = { + [key]: [relatedClassInstance, secondRelatedClassInstance], + }; + + const b = chance.string(); + + serializeResourceRelationshipObjectMocked.mockImplementation( + (classInstance) => + ({ + ...classInstance, + b, + // biome-ignore lint/suspicious/noExplicitAny: okay for mock + }) as any, + ); + + const result = serializeResourceObject(classInstance); + + expect(result.relationships).toEqual({ + [key]: [ + { + ...relatedClassInstance, + b, + }, + { + ...secondRelatedClassInstance, + b, + }, + ], + }); + }); + }); + + describe('when the related class instance is a single object', () => { + it('should throw an error if the related class instance is not an object', () => { + const key = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === relationshipsSymbol) { + return [[key, '']]; + } + + return undefined; + }, + ); + + const classInstance = { + [key]: 'not-an-object', + }; + + expect(() => serializeResourceObject(classInstance)).toThrow( + `Failed to serialize resource relationship object for ${key} because the value is not an object.`, + ); + }); + + it('should serialize the resource relationship object and return it', () => { + const key = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + if (symbol === relationshipsSymbol) { + return [[key, '']]; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + return undefined; + }); + + const relatedClassInstance = { + a: chance.string(), + }; + + const classInstance = { + [key]: relatedClassInstance, + }; + + const b = chance.string(); + + serializeResourceRelationshipObjectMocked.mockImplementation( + (classInstance) => + ({ + ...classInstance, + b, + // biome-ignore lint/suspicious/noExplicitAny: okay for mock + }) as any, + ); + + const result = serializeResourceObject(classInstance); + + expect(result.relationships).toEqual({ + [key]: { + ...relatedClassInstance, + b, + }, + }); + }); + }); + + it('should return without the `relationships` field if no relationships are found', () => { + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + return undefined; + }); + + const result = serializeResourceObject({}); + + expect(result.relationships).toBeUndefined(); + }); + + it('should not serialize undefined or null class instances', () => { + const key = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + if (symbol === relationshipsSymbol) { + return [[key, '']]; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + return undefined; + }); + + const result = serializeResourceObject({ + [key]: undefined, + [key]: null, + }); + + expect(serializeResourceRelationshipObjectMocked).not.toHaveBeenCalled(); + + expect(result.relationships).toBeUndefined(); + }); + }); + + describe('`type`', () => { + it('should get the resource type and return it', () => { + const type = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return type; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation( + (_object: object, _symbol: symbol) => 'some-collected-value', + ); + + const result = serializeResourceObject({}); + + expect(result.type).toBe(type); + }); + + it('should throw an error if no type is found on the class instance', () => { + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + return undefined; + }, + ); + + expect(() => serializeResourceObject({})).toThrow( + 'Failed to serialize resource object because the provided class instance is not a resource.', + ); + }); + }); + + describe('`id`', () => { + it('should get the id from the class instance and return it', () => { + const id = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return id; + } + + return undefined; + }); + + const result = serializeResourceObject({}); + + expect(result.id).toBe(id); + }); + + it('should throw an error if no id is found on the class instance', () => { + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation( + (_object: object, _symbol: symbol) => undefined, + ); + + expect(() => serializeResourceObject({})).toThrow( + 'Failed to serialize resource object because the provided class instance does not have an id.', + ); + }); + }); + + describe('`links`', () => { + it('should collect the links from the class instance and return it', () => { + const links = { + self: chance.url(), + related: chance.url(), + }; + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + if (symbol === linksSymbol) { + return links; + } + + return undefined; + }); + + const result = serializeResourceObject({}); + + expect(result.links).toEqual(links); + }); + + it('should return without the `links` field if no links are found', () => { + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + return undefined; + }); + + const result = serializeResourceObject({}); + + expect(result.links).toBeUndefined(); + }); + }); + + describe('`meta`', () => { + it('should collect the meta from the class instance and return it', () => { + const meta = { + some: chance.string(), + random: chance.string(), + }; + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + if (symbol === metaSymbol) { + return meta; + } + + return undefined; + }); + + const result = serializeResourceObject({}); + + expect(result.meta).toEqual(meta); + }); + + it('should return without the `meta` field if no meta is found', () => { + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + return undefined; + }); + + const result = serializeResourceObject({}); + + expect(result.meta).toBeUndefined(); + }); }); }); diff --git a/__tests__/serializers/serializeResourceRelationshipObject.test.ts b/__tests__/serializers/serializeResourceRelationshipObject.test.ts index a1a0d87..9a6c5cf 100644 --- a/__tests__/serializers/serializeResourceRelationshipObject.test.ts +++ b/__tests__/serializers/serializeResourceRelationshipObject.test.ts @@ -1,5 +1,198 @@ +import { Chance } from "chance"; +import { collect } from "../../src/serializers/utils/collect"; +import { getMetadataBySymbol } from "../../src/serializers/utils/getMetadataBySymbol"; +import { idSymbol } from '../../src/decorators/id'; +import { resourceSymbol } from '../../src/decorators/resource'; +import { serializeResourceRelationshipObject } from "../../src/serializers/serializeResourceRelationshipObject"; +import { linksSymbol } from "../../src/decorators/links"; +import { metaSymbol } from "../../src/decorators/meta"; + +jest.mock('../../src/serializers/utils/getMetadataBySymbol'); +const getMetadataBySymbolMocked = jest.mocked(getMetadataBySymbol); + +jest.mock('../../src/serializers/utils/collect'); +const collectMocked = jest.mocked(collect); + describe('`serializeResourceRelationshipObject`', () => { - it('', () => { - expect(true).toBe(true); + let chance: Chance.Chance; + + beforeEach(() => { + chance = new Chance(); + }) + + describe('`data`', () => { + describe('`type`', () => { + it('should get the type from the class instance and return it', () => { + const type = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return type; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation( + (_object: object, symbol: symbol) => { + if(symbol === idSymbol) { + return 'id'; + } + + return undefined; + } + ); + + const result = serializeResourceRelationshipObject({}); + + expect(result.data).toHaveProperty('type', type); + }); + + it('should throw an error if no type is found on the class instance', () => { + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return undefined; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation( + (_object: object, symbol: symbol) => { + if(symbol === idSymbol) { + return 'id'; + } + + return undefined; + } + ); + + expect(() => serializeResourceRelationshipObject({})).toThrow( + 'Failed to serialize resource relationship object because the provided class instance is not a resource.', + ); + }); + }); + + describe('`id`', () => { + it('should get the id from the class instance and return it', () => { + const id = chance.string(); + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation( + (_object: object, symbol: symbol) => { + if(symbol === idSymbol) { + return id; + } + + return undefined; + } + ); + + const result = serializeResourceRelationshipObject({}); + + expect(result.data).toHaveProperty('id', id); + }); + + it('should throw an error if no id is found on the class instance', () => { + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation( + (_object: object, symbol: symbol) => undefined + ); + + expect(() => serializeResourceRelationshipObject({})).toThrow( + 'Failed to serialize resource relationship object because the provided class instance does not have an id field.', + ); + }); + }); +}); + + describe('`links`', () => { + it('should get the links from the class instance and return them', () => { + const links = { + self: chance.url() + } + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + if (symbol === linksSymbol) { + return links; + } + + return undefined; + }); + + const result = serializeResourceRelationshipObject({}); + + expect(result.links).toEqual(links); + }); + }) + + describe('`meta`', () => { + it('should get the meta from the class instance and return them', () => { + const meta = { + self: chance.url() + } + + getMetadataBySymbolMocked.mockImplementation( + (_: object, symbol: symbol) => { + if (symbol === resourceSymbol) { + return 'type'; + } + + return undefined; + }, + ); + + collectMocked.mockImplementation((_: object, symbol: symbol) => { + if (symbol === idSymbol) { + return 'id'; + } + + if (symbol === metaSymbol) { + return meta; + } + + return undefined; + }); + + const result = serializeResourceRelationshipObject({}); + + expect(result.meta).toEqual(meta); + }); }); }); diff --git a/__tests__/serializers/utils/clean.test.ts b/__tests__/serializers/utils/clean.test.ts index 777c068..34d1944 100644 --- a/__tests__/serializers/utils/clean.test.ts +++ b/__tests__/serializers/utils/clean.test.ts @@ -141,6 +141,7 @@ describe('`clean`', () => { a: { b: [], c: chance.string(), + d: [chance.string(), {}], }, d: chance.string(), }; @@ -150,6 +151,7 @@ describe('`clean`', () => { expect(result).toEqual({ a: { c: object.a.c, + d: [object.a.d[0]], }, d: object.d, }); diff --git a/src/serializers/serializeResourceObject.ts b/src/serializers/serializeResourceObject.ts index 6228a4a..bdb0605 100644 --- a/src/serializers/serializeResourceObject.ts +++ b/src/serializers/serializeResourceObject.ts @@ -31,35 +31,38 @@ export const serializeResourceObject = ( const relationships = relationshipTuples.reduce( (acc, [key]) => { - const relatedClassInstance = classInstance[key]; + const relatedClassInstance_s = classInstance[key]; - if (relatedClassInstance === null || relatedClassInstance === undefined) { + if ( + relatedClassInstance_s === null || + relatedClassInstance_s === undefined + ) { return acc; } - if (Array.isArray(relatedClassInstance)) { - if (relatedClassInstance.every(isObject)) { - acc[key] = relatedClassInstance.map((classInstance) => - serializeResourceRelationshipObject(classInstance), - ); - - return acc; - } - + if (!isObject(relatedClassInstance_s)) { throw new Error( - `Failed to serialize resource relationship object for ${key.toString()} becuase not all elements in the array are objects.`, + `Failed to serialize resource relationship object for ${key.toString()} because the value is not an object.`, ); } - if (isObject(relatedClassInstance)) { - acc[key] = serializeResourceRelationshipObject(relatedClassInstance); + if (Array.isArray(relatedClassInstance_s)) { + if (!relatedClassInstance_s.every(isObject)) { + throw new Error( + `Failed to serialize resource relationship object for ${key.toString()} becuase not all elements in the array are objects.`, + ); + } + + acc[key] = relatedClassInstance_s.map((classInstance) => + serializeResourceRelationshipObject(classInstance), + ); return acc; } - throw new Error( - `Failed to serialize resource relationship object for ${key.toString()} because the value is not an object.`, - ); + acc[key] = serializeResourceRelationshipObject(relatedClassInstance_s); + + return acc; }, {} as Record< keyof I, @@ -75,11 +78,11 @@ export const serializeResourceObject = ( ); } - const id = getMetadataBySymbol(classInstance, idSymbol); + const id = collect(classInstance, idSymbol); if (id === undefined) { throw new Error( - 'Failed to serialize resource object because the provided class instance does not have an id field.', + 'Failed to serialize resource object because the provided class instance does not have an id.', ); } diff --git a/src/serializers/utils/clean.ts b/src/serializers/utils/clean.ts index 77341b6..c4f952c 100644 --- a/src/serializers/utils/clean.ts +++ b/src/serializers/utils/clean.ts @@ -15,6 +15,24 @@ export const clean = (object: O): O => { return acc; } + if (Array.isArray(value)) { + const cleanedArray = value + .map((item) => (isObject(item) ? clean(item) : item)) + .filter( + (item) => + item !== undefined && + (!isObject(item) || Object.keys(item).length !== 0), + ); + + if (cleanedArray.length === 0) { + return acc; + } + + acc[key] = cleanedArray as typeof value; + + return acc; + } + if (isObject(value)) { if (Object.keys(value).length === 0) { return acc;