Skip to content

Commit

Permalink
feat: serialize properties decorated with @serialize() (#251)
Browse files Browse the repository at this point in the history
* feat: serialize properties decorated with @serialize()

* feat: serialize object array properties decorated with @serialize()

* feat: re-initialize properties decorated with @serialize() with their appropriate class

* feat: re-initialize object array properties decorated with @serialize() with their appropriate class

* chore: code coverage

* chore: removed superfluous comment

* fix: added some fixture data because its absense broke the integration tests

* fix: eslint

* fix: apparently I broke a bunch of tests and did not see it

* fix: another attempt, hopefully this fixes the mess I made
  • Loading branch information
danieleisenhardt committed Aug 23, 2021
1 parent 110c1f5 commit 6109c26
Show file tree
Hide file tree
Showing 11 changed files with 299 additions and 12 deletions.
36 changes: 36 additions & 0 deletions src/AbstractFirestoreRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CollectionReference,
Transaction,
} from '@google-cloud/firestore';
import { serializeKey } from './Decorators/Serialize';
import { ValidationError } from './Errors/ValidationError';

import {
Expand Down Expand Up @@ -114,6 +115,40 @@ export abstract class AbstractFirestoreRepository<T extends IEntity>
});
};

protected initializeSerializedObjects(entity: T) {
Object.keys(entity).forEach(propertyKey => {
if (Reflect.getMetadata(serializeKey, entity, propertyKey) !== undefined) {
const constructor = Reflect.getMetadata(serializeKey, entity, propertyKey);
const data = entity as unknown as { [k: string]: unknown };
const subData = data[propertyKey] as { [k: string]: unknown };

if (Array.isArray(subData)) {
(entity as unknown as { [key: string]: unknown })[propertyKey] = subData.map(value => {
const subEntity = new constructor();

for (const i in value) {
subEntity[i] = value[i];
}

this.initializeSerializedObjects(subEntity);

return subEntity;
});
} else {
const subEntity = new constructor();

for (const i in subData) {
subEntity[i] = subData[i];
}

this.initializeSerializedObjects(subEntity);

(entity as unknown as { [key: string]: unknown })[propertyKey] = subEntity;
}
}
});
}

protected extractTFromDocSnap = (
doc: DocumentSnapshot,
tran?: Transaction,
Expand All @@ -125,6 +160,7 @@ export abstract class AbstractFirestoreRepository<T extends IEntity>
}) as T;

this.initializeSubCollections(entity, tran, tranRefStorage);
this.initializeSerializedObjects(entity);

return entity;
};
Expand Down
26 changes: 20 additions & 6 deletions src/BaseFirestoreRepository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
Coordinates,
FirestoreDocumentReference,
AlbumImage,
Agent,
Website,
} from '../test/fixture';
import { BaseFirestoreRepository } from './BaseFirestoreRepository';
import { Band } from '../test/BandCollection';
Expand Down Expand Up @@ -67,7 +69,7 @@ describe('BaseFirestoreRepository', () => {

it('must not throw any exceptions if a query with no results is limited', async () => {
const oldBands = await bandRepository
.whereLessOrEqualThan('formationYear', 1930)
.whereLessOrEqualThan('formationYear', 1688)
.limit(4)
.find();
expect(oldBands.length).toEqual(0);
Expand All @@ -91,7 +93,7 @@ describe('BaseFirestoreRepository', () => {
describe('orderByAscending', () => {
it('must order repository objects', async () => {
const bands = await bandRepository.orderByAscending('formationYear').find();
expect(bands[0].id).toEqual('pink-floyd');
expect(bands[0].id).toEqual('the-speckled-band');
});

it('must order the objects in a subcollection', async () => {
Expand All @@ -112,7 +114,7 @@ describe('BaseFirestoreRepository', () => {
});

it('must be chainable with limit', async () => {
const bands = await bandRepository.orderByAscending('formationYear').limit(2).find();
const bands = await bandRepository.orderByAscending('formationYear').limit(3).find();
const lastBand = bands[bands.length - 1];
expect(lastBand.id).toEqual('red-hot-chili-peppers');
});
Expand Down Expand Up @@ -432,7 +434,7 @@ describe('BaseFirestoreRepository', () => {

it('must filter with whereNotEqualTo', async () => {
const list = await bandRepository.whereNotEqualTo('name', 'Porcupine Tree').find();
expect(list.length).toEqual(1);
expect(list.length).toEqual(2);
expect(list[0].formationYear).toEqual(1983);
});

Expand All @@ -449,12 +451,12 @@ describe('BaseFirestoreRepository', () => {
it('must filter with whereLessThan', async () => {
const list = await bandRepository.whereLessThan('formationYear', 1983).find();

expect(list.length).toEqual(1);
expect(list.length).toEqual(2);
});

it('must filter with whereLessOrEqualThan', async () => {
const list = await bandRepository.whereLessOrEqualThan('formationYear', 1983).find();
expect(list.length).toEqual(2);
expect(list.length).toEqual(3);
});

it('must filter with whereArrayContains', async () => {
Expand Down Expand Up @@ -863,4 +865,16 @@ describe('BaseFirestoreRepository', () => {
expect(possibleDocWithoutId).not.toBeUndefined();
});
});

describe('deserialization', () => {
it('should correctly initialize a repository with an entity', async () => {
const bandRepositoryWithPath = new BandRepository(Band);
const band = await bandRepositoryWithPath.findById('the-speckled-band');
expect(band.name).toEqual('the Speckled Band');
expect(band.agents[0]).toBeInstanceOf(Agent);
expect(band.agents[0].name).toEqual('Mycroft Holmes');
expect(band.agents[0].website).toBeInstanceOf(Website);
expect(band.agents[0].website.url).toEqual('en.wikipedia.org/wiki/Mycroft_Holmes');
});
});
});
22 changes: 22 additions & 0 deletions src/Decorators/Serialize.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Serialize, serializeKey } from './Serialize';

describe('IgnoreDecorator', () => {
it('should decorate properties', () => {
class Address {
streetName: string;
zipcode: string;
}

class Band {
id: string;
name: string;
@Serialize(Address)
address: Address;
}

const band = new Band();

expect(Reflect.getMetadata(serializeKey, band, 'name')).toBe(undefined);
expect(Reflect.getMetadata(serializeKey, band, 'address')).toBe(Address);
});
});
7 changes: 7 additions & 0 deletions src/Decorators/Serialize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'reflect-metadata';
import { Constructor } from '../types';
export const serializeKey = Symbol('Serialize');

export function Serialize(entityConstructor: Constructor<unknown>) {
return Reflect.metadata(serializeKey, entityConstructor);
}
3 changes: 2 additions & 1 deletion src/Decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './Collection';
export * from './CustomRepository';
export * from './SubCollection';
export * from './Ignore';
export * from './Serialize';
export * from './SubCollection';
6 changes: 3 additions & 3 deletions src/Transaction/BaseFirestoreTransactionRepository.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,14 +316,14 @@ describe('BaseFirestoreTransactionRepository', () => {
await bandRepository.runTransaction(async tran => {
const list = await tran.whereLessThan('formationYear', 1983).find();

expect(list.length).toEqual(1);
expect(list.length).toEqual(2);
});
});

it('must filter with whereLessOrEqualThan', async () => {
await bandRepository.runTransaction(async tran => {
const list = await tran.whereLessOrEqualThan('formationYear', 1983).find();
expect(list.length).toEqual(2);
const list = await tran.whereLessOrEqualThan('formationYear', 1900).find();
expect(list.length).toEqual(1);
});
});

Expand Down
64 changes: 63 additions & 1 deletion src/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Ignore } from './Decorators/Ignore';
import { Ignore, Serialize } from './Decorators';
import { IEntity } from './types';
import { extractAllGetters, serializeEntity } from './utils';

Expand Down Expand Up @@ -75,5 +75,67 @@ describe('Utils', () => {
expect(serializeEntity(rhcp, [])).toHaveProperty('name');
expect(serializeEntity(rhcp, [])).not.toHaveProperty('temporaryName');
});

it('should serialize object properties with the @Serialize() decorator', () => {
class Address {
streetName: string;
number: number;
numberAddition: string;
}

class Band implements IEntity {
id: string;
name: string;
@Serialize(Address)
address: Address;
}

const address = new Address();
address.streetName = 'Baker St.';
address.number = 211;
address.numberAddition = 'B';

const band = new Band();
band.name = 'the Speckled Band';
band.address = address;

expect(serializeEntity(band, [])).toHaveProperty('name');
expect(serializeEntity(band, []).address).not.toBeInstanceOf(Address);
expect(serializeEntity(band, []).address['number']).toBe(211);
});

it('should serialize object array properties with the @Serialize() decorator', () => {
class Address {
streetName: string;
number: number;
numberAddition: string;
}

class Band implements IEntity {
id: string;
name: string;
@Serialize(Address)
addresses: Address[];
}

const address = new Address();
address.streetName = 'Baker St.';
address.number = 211;
address.numberAddition = 'B';

const address2 = new Address();
address2.streetName = 'Baker St.';
address2.number = 211;
address2.numberAddition = 'C';

const band = new Band();
band.name = 'the Speckled Band';
band.addresses = [address, address2];

expect(serializeEntity(band, [])).toHaveProperty('name');
expect(serializeEntity(band, []).addresses[0]).not.toBeInstanceOf(Address);
expect(serializeEntity(band, []).addresses[0]['numberAddition']).toBe('B');
expect(serializeEntity(band, []).addresses[1]['numberAddition']).toBe('C');
});
});
});
17 changes: 16 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ignoreKey } from './Decorators/Ignore';
import { ignoreKey, serializeKey } from './Decorators';
import { SubCollectionMetadata } from './MetadataStorage';
import { IEntity } from '.';

Expand Down Expand Up @@ -61,6 +61,21 @@ export function serializeEntity<T extends IEntity>(
}
});

Object.entries(serializableObj).forEach(([propertyKey, propertyValue]) => {
if (Reflect.getMetadata(serializeKey, obj, propertyKey) !== undefined) {
if (Array.isArray(propertyValue)) {
(serializableObj as { [key: string]: unknown })[propertyKey] = propertyValue.map(element =>
serializeEntity(element, [])
);
} else {
(serializableObj as { [key: string]: unknown })[propertyKey] = serializeEntity(
propertyValue as Partial<T>,
[]
);
}
}
});

return serializableObj;
}

Expand Down
5 changes: 5 additions & 0 deletions test/BandCollection.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { Collection, SubCollection } from '../src/Decorators';
import { Serialize } from '../src/Decorators/Serialize';
import {
Agent,
Album as AlbumEntity,
AlbumImage as AlbumImageEntity,
Coordinates,
Expand Down Expand Up @@ -49,6 +51,9 @@ export class Band {
@Type(() => FirestoreDocumentReference)
relatedBand?: FirestoreDocumentReference;

@Serialize(Agent)
agents: Agent[];

getLastShowYear() {
return this.lastShow.getFullYear();
}
Expand Down
33 changes: 33 additions & 0 deletions test/fixture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { IEntity } from '../src';
import { Serialize } from '../src/Decorators/Serialize';

export class Coordinates {
latitude: number;
Expand All @@ -22,6 +23,16 @@ export class Album {
comment?: string;
}

export class Website {
url: string;
}

export class Agent {
name: string;
@Serialize(Website)
website: Website;
}

export class Band {
id: string;
name: string;
Expand Down Expand Up @@ -181,6 +192,28 @@ export const getInitialData = () => {
},
],
},
{
id: 'the-speckled-band',
name: 'the Speckled Band',
formationYear: 1892,
lastShow: null,
genres: [],
albums: [],
agents: [
{
name: 'Mycroft Holmes',
website: {
url: 'en.wikipedia.org/wiki/Mycroft_Holmes',
},
},
{
name: 'Arthur Conan Doyle',
website: {
url: 'en.wikipedia.org/wiki/Arthur_Conan_Doyle',
},
},
],
},
];
};

Expand Down

0 comments on commit 6109c26

Please sign in to comment.