From 6b3490ebe7bb80f3e7821fe052e04fc0488fd1a6 Mon Sep 17 00:00:00 2001 From: DPDS93CT Date: Wed, 3 Jul 2024 09:19:33 +0200 Subject: [PATCH] remove EmailEntity --- docs/persistence.md | 33 +++ src/modules/email/domain/email-address.ts | 7 +- .../email/domain/email-event-handler.spec.ts | 54 +++-- .../email/domain/email-event-handler.ts | 2 +- src/modules/email/domain/email.factory.ts | 18 +- src/modules/email/domain/email.spec.ts | 21 +- src/modules/email/domain/email.ts | 51 ++--- .../email/persistence/email-address.entity.ts | 17 +- src/modules/email/persistence/email.entity.ts | 25 --- .../email/persistence/email.repo.spec.ts | 97 +++++---- src/modules/email/persistence/email.repo.ts | 188 ++++++++---------- .../person/persistence/person.repository.ts | 11 +- src/shared/types/aggregate-ids.types.ts | 4 +- 13 files changed, 254 insertions(+), 274 deletions(-) create mode 100644 docs/persistence.md delete mode 100644 src/modules/email/persistence/email.entity.ts diff --git a/docs/persistence.md b/docs/persistence.md new file mode 100644 index 000000000..95766e9fc --- /dev/null +++ b/docs/persistence.md @@ -0,0 +1,33 @@ +# Developer notes on persistence layer + +## Mapping aggregates to entities + +During development many aggregates were created and these aggregates have to be persisted as entities. +Therefore, repositories were built which typically contain at least a _mapAggregateToData_ method and a _mapDataToAggregate_ method. + +### Troubleshooting + +If you encounter following error while writing such a method, 'Type Option is not assignable to type string' e.g. on the id: + +```typescript +function mapAggregateToData(aggregate: Aggregate): RequiredEntityData { + return { + // Don't assign createdAt and updatedAt, they are auto-generated! + id: aggregate.id, //Here following error is shown: Type Option is not assignable to type string + personId: rel(PersonEntity, aggregate.personId), + value: aggregate.value, + }; +} +``` +make sure that id is the primary key for the entity, additional declarations of primary key attributes may result in the error mentioned above, like e.g. in this entity: +```typescript +@Entity({ tableName: 'tableName' }) +export class Entitiy extends TimestampedEntity { + //... + public [PrimaryKeyProp]?: ['anotherProp']; +} +``` + + + + diff --git a/src/modules/email/domain/email-address.ts b/src/modules/email/domain/email-address.ts index 04d14a28f..e4c7d2c97 100644 --- a/src/modules/email/domain/email-address.ts +++ b/src/modules/email/domain/email-address.ts @@ -1,8 +1,11 @@ -import { EmailID } from '../../../shared/types/index.js'; +import { PersonID } from '../../../shared/types/index.js'; export class EmailAddress { public constructor( - public email: Persisted, + public id: Persisted, + public readonly createdAt: Persisted, + public readonly updatedAt: Persisted, + public personId: PersonID, public address: string, public enabled: boolean, ) {} diff --git a/src/modules/email/domain/email-event-handler.spec.ts b/src/modules/email/domain/email-event-handler.spec.ts index 0bacfe3c1..c97194a38 100644 --- a/src/modules/email/domain/email-event-handler.spec.ts +++ b/src/modules/email/domain/email-event-handler.spec.ts @@ -28,7 +28,7 @@ import { PersonRepository } from '../../person/persistence/person.repository.js' import { EventModule, EventService } from '../../../core/eventbus/index.js'; import { EmailFactory } from './email.factory.js'; import { Email } from './email.js'; -import { EmailID, PersonID, RolleID } from '../../../shared/types/index.js'; +import { EmailAddressID, PersonID, RolleID } from '../../../shared/types/index.js'; import { EmailInvalidError } from '../error/email-invalid.error.js'; import { ClassLogger } from '../../../core/logging/class-logger.js'; import { EmailAddress } from './email-address.js'; @@ -37,16 +37,11 @@ import { PersonDeletedEvent } from '../../../shared/events/person-deleted.event. import { EmailAddressNotFoundError } from '../error/email-address-not-found.error.js'; function getEmail(emaiGeneratorService: EmailGeneratorService, personRepository: PersonRepository): Email { - const fakeEmailId: EmailID = faker.string.uuid(); - return Email.construct( - fakeEmailId, - faker.date.past(), - faker.date.recent(), - faker.string.uuid(), - emaiGeneratorService, - personRepository, - [new EmailAddress(fakeEmailId, faker.internet.email(), true)], - ); + const fakePersonId: PersonID = faker.string.uuid(); + const fakeEmailAddressId: string = faker.string.uuid(); + return Email.construct(fakePersonId, emaiGeneratorService, personRepository, [ + new EmailAddress(fakeEmailAddressId, undefined, undefined, fakePersonId, faker.internet.email(), true), + ]); } describe('Email Event Handler', () => { @@ -141,7 +136,7 @@ describe('Email Event Handler', () => { describe('when existing email is found', () => { it('should enable existing email', async () => { const fakePersonId: PersonID = faker.string.uuid(); - const emailId: EmailID = faker.string.uuid(); + const emailAddressId: EmailAddressID = faker.string.uuid(); const event: PersonenkontextCreatedEvent = new PersonenkontextCreatedEvent( fakePersonId, faker.string.uuid(), @@ -152,7 +147,14 @@ describe('Email Event Handler', () => { serviceProviderRepoMock.findByIds.mockResolvedValueOnce(spMap); const emailAddresses: EmailAddress[] = [ - new EmailAddress(emailId, faker.internet.email(), true), + new EmailAddress( + emailAddressId, + faker.date.past(), + faker.date.recent(), + fakePersonId, + faker.internet.email(), + true, + ), ]; // eslint-disable-next-line @typescript-eslint/require-await emailRepoMock.findByPerson.mockImplementationOnce(async (personId: PersonID) => { @@ -168,7 +170,7 @@ describe('Email Event Handler', () => { get currentAddress(): Option { return 'test@schule-sh.de'; }, - id: emailId, + personId: fakePersonId, emailAddresses: emailAddresses, }), }; @@ -187,7 +189,7 @@ describe('Email Event Handler', () => { describe('when existing email is found but enabling results in error', () => { it('should log error', async () => { const fakePersonId: PersonID = faker.string.uuid(); - const emailId: EmailID = faker.string.uuid(); + const emailAddressId: EmailAddressID = faker.string.uuid(); const event: PersonenkontextCreatedEvent = new PersonenkontextCreatedEvent( fakePersonId, faker.string.uuid(), @@ -198,7 +200,14 @@ describe('Email Event Handler', () => { serviceProviderRepoMock.findByIds.mockResolvedValueOnce(spMap); const emailAddresses: EmailAddress[] = [ - new EmailAddress(emailId, faker.internet.email(), true), + new EmailAddress( + emailAddressId, + faker.date.past(), + faker.date.recent(), + fakePersonId, + faker.internet.email(), + true, + ), ]; // eslint-disable-next-line @typescript-eslint/require-await emailRepoMock.findByPerson.mockImplementationOnce(async (personId: PersonID) => { @@ -239,7 +248,16 @@ describe('Email Event Handler', () => { emailFactoryMock.createNew.mockImplementationOnce((personId: PersonID) => { const emailMock: DeepMocked> = createMock>({ - emailAddresses: [new EmailAddress(undefined, faker.internet.email(), true)], + emailAddresses: [ + new EmailAddress( + undefined, + undefined, + undefined, + personId, + faker.internet.email(), + true, + ), + ], personId: personId, }); const emailAddress: EmailAddress = createMock>({ @@ -374,8 +392,6 @@ describe('Email Event Handler', () => { describe('when deletion is successful', () => { it('should log info', async () => { - emailRepoMock.deleteById.mockResolvedValueOnce(true); - await emailEventHandler.asyncPersonDeletedEventHandler(event); expect(loggerMock.info).toHaveBeenCalledWith(`Successfully deactivated email-address:${emailAddress}`); diff --git a/src/modules/email/domain/email-event-handler.ts b/src/modules/email/domain/email-event-handler.ts index e65407361..7d179aa7a 100644 --- a/src/modules/email/domain/email-event-handler.ts +++ b/src/modules/email/domain/email-event-handler.ts @@ -10,12 +10,12 @@ import { ServiceProvider } from '../../service-provider/domain/service-provider. import { ServiceProviderTarget } from '../../service-provider/domain/service-provider.enum.js'; import { EmailFactory } from './email.factory.js'; import { Email } from './email.js'; -import { EmailRepo } from '../persistence/email.repo.js'; import { PersonDeletedEvent } from '../../../shared/events/person-deleted.event.js'; import { DomainError } from '../../../shared/error/index.js'; import { PersonID } from '../../../shared/types/index.js'; import { EmailAddressEntity } from '../persistence/email-address.entity.js'; import { EmailAddressNotFoundError } from '../error/email-address-not-found.error.js'; +import { EmailRepo } from '../persistence/email.repo.js'; @Injectable() export class EmailEventHandler { diff --git a/src/modules/email/domain/email.factory.ts b/src/modules/email/domain/email.factory.ts index 1fd5caa7b..e3ab7e1a8 100644 --- a/src/modules/email/domain/email.factory.ts +++ b/src/modules/email/domain/email.factory.ts @@ -12,22 +12,8 @@ export class EmailFactory { private readonly personRepository: PersonRepository, ) {} - public construct( - id: string, - createdAt: Date, - updatedAt: Date, - personId: PersonID, - emailAddresses: EmailAddress[], - ): Email { - return Email.construct( - id, - createdAt, - updatedAt, - personId, - this.emailGeneratorService, - this.personRepository, - emailAddresses, - ); + public construct(personId: PersonID, emailAddresses: EmailAddress[]): Email { + return Email.construct(personId, this.emailGeneratorService, this.personRepository, emailAddresses); } public createNew(personId: PersonID): Email { diff --git a/src/modules/email/domain/email.spec.ts b/src/modules/email/domain/email.spec.ts index e4b527a37..cf3c68c40 100644 --- a/src/modules/email/domain/email.spec.ts +++ b/src/modules/email/domain/email.spec.ts @@ -8,7 +8,7 @@ import { faker } from '@faker-js/faker'; import { Person } from '../../person/domain/person.js'; import { EmailInvalidError } from '../error/email-invalid.error.js'; import { EmailAddress } from './email-address.js'; -import { EmailID } from '../../../shared/types/index.js'; +import { EmailAddressID } from '../../../shared/types/index.js'; describe('Email Aggregate', () => { let module: TestingModule; @@ -48,17 +48,18 @@ describe('Email Aggregate', () => { describe('enable', () => { describe('when emailAddresses are already present on aggregate', () => { it('should return successfully', async () => { - const emailId: EmailID = faker.string.uuid(); + const emailAddressId: EmailAddressID = faker.string.uuid(); const emailAddresses: EmailAddress[] = [ - new EmailAddress(emailId, faker.internet.email(), false), + new EmailAddress( + emailAddressId, + faker.date.past(), + faker.date.recent(), + faker.string.uuid(), + faker.internet.email(), + false, + ), ]; - const existingEmail: Email = emailFactory.construct( - emailId, - faker.date.past(), - faker.date.recent(), - faker.string.uuid(), - emailAddresses, - ); + const existingEmail: Email = emailFactory.construct(faker.string.uuid(), emailAddresses); const result: Result> = await existingEmail.enable(); diff --git a/src/modules/email/domain/email.ts b/src/modules/email/domain/email.ts index 4f8cf3a9d..c754c5b5b 100644 --- a/src/modules/email/domain/email.ts +++ b/src/modules/email/domain/email.ts @@ -1,4 +1,4 @@ -import { EmailID, PersonID } from '../../../shared/types/index.js'; +import { PersonID } from '../../../shared/types/index.js'; import { EmailGeneratorService } from './email-generator.service.js'; import { PersonRepository } from '../../person/persistence/person.repository.js'; import { Person } from '../../person/domain/person.js'; @@ -7,9 +7,6 @@ import { EmailAddress } from './email-address.js'; export class Email { private constructor( - public readonly id: Persisted, - public readonly createdAt: Persisted, - public readonly updatedAt: Persisted, public readonly personId: PersonID, public readonly emailGeneratorService: EmailGeneratorService, public readonly personRepository: PersonRepository, @@ -21,27 +18,16 @@ export class Email { emailGeneratorService: EmailGeneratorService, personRepository: PersonRepository, ): Email { - return new Email( - undefined, - undefined, - undefined, - personId, - emailGeneratorService, - personRepository, - undefined, - ); + return new Email(personId, emailGeneratorService, personRepository, undefined); } public static construct( - id: string, - createdAt: Date, - updatedAt: Date, personId: PersonID, emailGeneratorService: EmailGeneratorService, personRepository: PersonRepository, - emailAddresses: EmailAddress[], + emailAddresses: EmailAddress[], ): Email { - return new Email(id, createdAt, updatedAt, personId, emailGeneratorService, personRepository, emailAddresses); + return new Email(personId, emailGeneratorService, personRepository, emailAddresses); } public async enable(): Promise>> { @@ -49,15 +35,7 @@ export class Email { this.emailAddresses[0].enabled = true; return { ok: true, - value: new Email( - this.id, - this.createdAt, - this.updatedAt, - this.personId, - this.emailGeneratorService, - this.personRepository, - this.emailAddresses, - ), + value: new Email(this.personId, this.emailGeneratorService, this.personRepository, this.emailAddresses), }; } const person: Option> = await this.personRepository.findById(this.personId); @@ -77,18 +55,17 @@ export class Email { error: new EmailInvalidError(), }; } - const newEmailAddress: EmailAddress = new EmailAddress(undefined, generatedName.value, true); + const newEmailAddress: EmailAddress = new EmailAddress( + undefined, + undefined, + undefined, + this.personId, + generatedName.value, + true, + ); return { ok: true, - value: new Email( - this.id, - this.createdAt, - this.updatedAt, - this.personId, - this.emailGeneratorService, - this.personRepository, - [newEmailAddress], - ), + value: new Email(this.personId, this.emailGeneratorService, this.personRepository, [newEmailAddress]), }; } diff --git a/src/modules/email/persistence/email-address.entity.ts b/src/modules/email/persistence/email-address.entity.ts index b41448a8e..c9521048f 100644 --- a/src/modules/email/persistence/email-address.entity.ts +++ b/src/modules/email/persistence/email-address.entity.ts @@ -1,21 +1,22 @@ -import { BaseEntity, Entity, ManyToOne, PrimaryKeyProp, Property, Rel } from '@mikro-orm/core'; -import { EmailEntity } from './email.entity.js'; +import { Entity, ManyToOne, Property, Ref } from '@mikro-orm/core'; +import { PersonEntity } from '../../person/persistence/person.entity.js'; +import { TimestampedEntity } from '../../../persistence/timestamped.entity.js'; @Entity({ tableName: 'email_address' }) -export class EmailAddressEntity extends BaseEntity { +export class EmailAddressEntity extends TimestampedEntity { @ManyToOne({ columnType: 'uuid', - fieldName: 'email_id', + fieldName: 'person_id', ref: true, - entity: () => EmailEntity, + nullable: true, + deleteRule: 'set null', + entity: () => PersonEntity, }) - public email!: Rel; + public personId!: Ref; @Property({ primary: true, nullable: false, unique: true }) public address!: string; @Property({ nullable: false }) public enabled!: boolean; - - public [PrimaryKeyProp]?: ['address']; } diff --git a/src/modules/email/persistence/email.entity.ts b/src/modules/email/persistence/email.entity.ts deleted file mode 100644 index 6afa74ff7..000000000 --- a/src/modules/email/persistence/email.entity.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { Cascade, Collection, Entity, ManyToOne, OneToMany, Ref } from '@mikro-orm/core'; -import { TimestampedEntity } from '../../../persistence/timestamped.entity.js'; -import { PersonEntity } from '../../person/persistence/person.entity.js'; -import { EmailAddressEntity } from './email-address.entity.js'; - -@Entity({ tableName: 'email' }) -export class EmailEntity extends TimestampedEntity { - @OneToMany({ - entity: () => EmailAddressEntity, - mappedBy: 'email', - orphanRemoval: true, - cascade: [Cascade.PERSIST], - }) - public emailAddresses: Collection = new Collection(this); - - @ManyToOne({ - columnType: 'uuid', - fieldName: 'person_id', - deleteRule: 'set null', - ref: true, - nullable: true, - entity: () => PersonEntity, - }) - public personId!: Ref; -} diff --git a/src/modules/email/persistence/email.repo.spec.ts b/src/modules/email/persistence/email.repo.spec.ts index dab895c78..7d4710f95 100644 --- a/src/modules/email/persistence/email.repo.spec.ts +++ b/src/modules/email/persistence/email.repo.spec.ts @@ -21,8 +21,8 @@ import { EmailServiceRepo } from './email-service.repo.js'; import { EventService } from '../../../core/eventbus/index.js'; import { ClassLogger } from '../../../core/logging/class-logger.js'; import { EmailAddressNotFoundError } from '../error/email-address-not-found.error.js'; -import { EmailAddress } from '../domain/email-address.js'; import { EmailAddressEntity } from './email-address.entity.js'; +import { EmailInvalidError } from '../error/email-invalid.error.js'; describe('EmailRepo', () => { let module: TestingModule; @@ -95,6 +95,10 @@ describe('EmailRepo', () => { return person; } + /* function createEmailAddress(personId: PersonID): EmailAddress { + return new EmailAddress(undefined, undefined, undefined, personId, faker.internet.email(), true); + }*/ + afterAll(async () => { await orm.close(); await module.close(); @@ -110,7 +114,7 @@ describe('EmailRepo', () => { }); describe('findByPerson', () => { - it('should return email by personId', async () => { + it('should return email with email-addresses by personId', async () => { const person: Person = await createPerson(); const email: Email = emailFactory.createNew(person.id); const validEmail: Result> = await email.enable(); @@ -122,7 +126,7 @@ describe('EmailRepo', () => { if (!foundEmail) throw Error(); expect(foundEmail).toBeTruthy(); - expect(foundEmail.id).toStrictEqual(savedEmail.id); + expect(foundEmail.emailAddresses).toHaveLength(1); }); }); @@ -162,7 +166,7 @@ describe('EmailRepo', () => { }); }); - describe('save (create new)', () => { + /*describe('save (create new)', () => { describe('when no emailAddresses are attached', () => { it('should create entity without email-addresses', async () => { const person: Person = await createPerson(); @@ -172,10 +176,57 @@ describe('EmailRepo', () => { expect(savedEmail).toBeInstanceOf(Email); }); }); - }); + });*/ + + describe('save', () => { + describe('when emailAddressEntities are NOT attached to aggregate', () => { + it('should return EmailInvalidError', async () => { + const newEmail: Email = emailFactory.createNew(faker.string.uuid()); + const res: Email | DomainError = await sut.save(newEmail); + + expect(res).toBeInstanceOf(EmailInvalidError); + }); + }); + + describe('when addresses are attached to aggregate and some are already persisted', () => { + it('should use update method and return email aggregate', async () => { + const person: Person = await createPerson(); + const email: Email = emailFactory.createNew(person.id); + + const validEmail: Result> = await email.enable(); + if (!validEmail.ok) throw Error(); + + const persistedValidEmail: Email | DomainError = await sut.save(validEmail.value); + if (persistedValidEmail instanceof DomainError) throw new Error(); + + persistedValidEmail.disable(); + const persistedDisabledEmail: Email | DomainError = await sut.save(persistedValidEmail); + + expect(persistedDisabledEmail).toBeInstanceOf(Email); + }); + }); + + describe('when addresses are attached to aggregate and some are already persisted BUT cannot be found in DB', () => { + it('should return EmailAddressNotFoundError', async () => { + const person: Person = await createPerson(); + const email: Email = emailFactory.createNew(person.id); + + const validEmail: Result> = await email.enable(); + if (!validEmail.ok) throw Error(); + + const persistedValidEmail: Email | DomainError = await sut.save(validEmail.value); + if (persistedValidEmail instanceof DomainError) throw new Error(); + if (!persistedValidEmail.emailAddresses || !persistedValidEmail.emailAddresses[0]) throw new Error(); + + persistedValidEmail.emailAddresses[0].address = faker.internet.email(); - describe('save with id (update)', () => { - describe('when emailAddressEntities can be found in DB', () => { + const persistedChangedEmail: Email | DomainError = await sut.save(persistedValidEmail); + + expect(persistedChangedEmail).toBeInstanceOf(EmailAddressNotFoundError); + }); + }); + + /*describe('when emailAddressEntities can be found in DB', () => { it('should update entity, when id is set', async () => { const person: Person = await createPerson(); const email: Email = emailFactory.createNew(person.id); @@ -186,15 +237,13 @@ describe('EmailRepo', () => { const savedEmail: Email | DomainError = await sut.save(validEmail.value); if (savedEmail instanceof DomainError) throw new Error(); const newEmail: Email = emailFactory.construct( - savedEmail.id, - faker.date.past(), - faker.date.recent(), person.id, [validEmail.value.emailAddresses[0]], ); const updatedMail: Email | DomainError = await sut.save(newEmail); if (updatedMail instanceof DomainError) throw new Error(); const foundEmail: Option> = await sut.findById(updatedMail.id); + expect(foundEmail).toBeTruthy(); expect(foundEmail).toEqual(updatedMail); }); @@ -218,33 +267,9 @@ describe('EmailRepo', () => { [new EmailAddress(faker.string.uuid(), faker.internet.email(), true)], //results in em.findOne returns undefined ); const updatedMail: Email | DomainError = await sut.save(newEmail); - expect(updatedMail).toBeInstanceOf(EmailAddressNotFoundError); - }); - }); - }); - - describe('deleteById', () => { - describe('when email exists', () => { - it('should return true', async () => { - const person: Person = await createPerson(); - const email: Email = emailFactory.createNew(person.id); - const validEmail: Result> = await email.enable(); - - if (!validEmail.ok) throw Error(); - const savedEmail: Email | DomainError = await sut.save(validEmail.value); - if (savedEmail instanceof DomainError) throw new Error(); - const result: boolean = await sut.deleteById(savedEmail.id); - expect(result).toBeTruthy(); - }); - }); - - describe('when email does NOT exist', () => { - it('should return false', async () => { - const result: boolean = await sut.deleteById(faker.string.uuid()); - - expect(result).toBeFalsy(); + expect(updatedMail).toBeInstanceOf(EmailAddressNotFoundError); }); - }); + });*/ }); }); diff --git a/src/modules/email/persistence/email.repo.ts b/src/modules/email/persistence/email.repo.ts index 72b19fffb..b9f4e569d 100644 --- a/src/modules/email/persistence/email.repo.ts +++ b/src/modules/email/persistence/email.repo.ts @@ -1,9 +1,7 @@ -import { Collection, EntityData, EntityManager, EntityName, Loaded, rel, RequiredEntityData } from '@mikro-orm/core'; +import { EntityManager, RequiredEntityData, rel } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; -import { EmailEntity } from './email.entity.js'; import { Email } from '../domain/email.js'; -import { PersonEntity } from '../../person/persistence/person.entity.js'; -import { EmailID, PersonID } from '../../../shared/types/index.js'; +import { PersonID } from '../../../shared/types/index.js'; import { EmailGeneratorService } from '../domain/email-generator.service.js'; import { PersonRepository } from '../../person/persistence/person.repository.js'; import { EmailAddressEntity } from './email-address.entity.js'; @@ -11,62 +9,41 @@ import { EmailAddress } from '../domain/email-address.js'; import { EmailAddressNotFoundError } from '../error/email-address-not-found.error.js'; import { ClassLogger } from '../../../core/logging/class-logger.js'; import { DomainError } from '../../../shared/error/index.js'; +import { PersonEntity } from '../../person/persistence/person.entity.js'; +import { EmailInvalidError } from '../error/email-invalid.error.js'; -function mapEmailAddressAggregateToData( - emailAddress: EmailAddress, - emailId: EmailID, -): RequiredEntityData { +function mapAggregateToData(emailAddress: EmailAddress): RequiredEntityData { return { - email: emailId, + // Don't assign createdAt and updatedAt, they are auto-generated! + id: emailAddress.id, + personId: rel(PersonEntity, emailAddress.personId), address: emailAddress.address, enabled: emailAddress.enabled, }; } -function mapAggregateToData(email: Email): RequiredEntityData { - if (email.emailAddresses) { - const emailAddresses: EntityData[] = email.emailAddresses.map( - (emailAddress: EmailAddress) => { - return { - email: email.id, - address: emailAddress.address, - enabled: emailAddress.enabled, - }; - }, - ); - - return { - personId: rel(PersonEntity, email.personId), - emailAddresses: new Collection(emailAddresses), - }; - } - - return { - personId: rel(PersonEntity, email.personId), - emailAddresses: undefined, - }; +function mapEntityToAggregate(entity: EmailAddressEntity): EmailAddress { + return new EmailAddress( + entity.id, + entity.createdAt, + entity.updatedAt, + entity.personId.id, + entity.address, + entity.enabled, + ); } -function mapEntityToAggregate( - entity: EmailEntity, +function mapEntitiesToEmailAggregate( + personId: PersonID, + entities: EmailAddressEntity[], emailGeneratorService: EmailGeneratorService, personRepository: PersonRepository, ): Email { - const emailAddresses: EmailAddress[] = entity.emailAddresses.map( - (emailAddressEntity: EmailAddressEntity) => { - return new EmailAddress(entity.id, emailAddressEntity.address, emailAddressEntity.enabled); - }, + const emailAddresses: EmailAddress[] = entities.map((entity: EmailAddressEntity) => + mapEntityToAggregate(entity), ); - return Email.construct( - entity.id, - entity.createdAt, - entity.updatedAt, - entity.personId.id, - emailGeneratorService, - personRepository, - emailAddresses, - ); + return Email.construct(personId, emailGeneratorService, personRepository, emailAddresses); } @Injectable() @@ -78,24 +55,15 @@ export class EmailRepo { private readonly personRepository: PersonRepository, ) {} - public get entityName(): EntityName { - return EmailEntity; - } - - public async findById(id: EmailID): Promise>> { - const emailEntity: Option = await this.em.findOne(this.entityName, { id }, {}); - - return emailEntity && mapEntityToAggregate(emailEntity, this.emailGeneratorService, this.personRepository); - } - public async findByPerson(personId: PersonID): Promise>> { - const emailEntity: Option = await this.em.findOne( - EmailEntity, - { personId }, - { populate: ['emailAddresses'] as const }, - ); + const emailAddressEntities: EmailAddressEntity[] = await this.em.find(EmailAddressEntity, { personId }, {}); - return emailEntity && mapEntityToAggregate(emailEntity, this.emailGeneratorService, this.personRepository); + return mapEntitiesToEmailAggregate( + personId, + emailAddressEntities, + this.emailGeneratorService, + this.personRepository, + ); } public async deactivateEmailAddress(emailAddress: string): Promise { @@ -114,66 +82,66 @@ export class EmailRepo { public async save(email: Email): Promise | DomainError> { this.logger.info('save email'); - if (email.id) { - return this.update(email); + if (!email.emailAddresses) { + return new EmailInvalidError(['No email-addresses attached to email aggregate']); + } + + if (email.emailAddresses.some((emailAddress: EmailAddress) => emailAddress.id)) { + return this.update(email.personId, email.emailAddresses); } else { - return this.create(email); + return this.create(email.personId, email.emailAddresses); } } - private async create(email: Email): Promise> { - const emailEntity: EmailEntity = this.em.create(EmailEntity, mapAggregateToData(email)); - await this.em.persistAndFlush(emailEntity); - + private async create( + personId: PersonID, + emailAddresses: EmailAddress[], + ): Promise | DomainError> { //persist the emailAddresses - if (email.emailAddresses) { - for (const emailAddress of email.emailAddresses) { - const emailAddressEntity: EmailAddressEntity = this.em.create( - EmailAddressEntity, - mapEmailAddressAggregateToData(emailAddress, emailEntity.id), - ); - await this.em.persistAndFlush(emailAddressEntity); - } + const emailAddressEntities: EmailAddressEntity[] = []; + for (const emailAddress of emailAddresses) { + const emailAddressEntity: EmailAddressEntity = this.em.create( + EmailAddressEntity, + mapAggregateToData(emailAddress), + ); + emailAddressEntities.push(emailAddressEntity); + await this.em.persistAndFlush(emailAddressEntity); } - return mapEntityToAggregate(emailEntity, this.emailGeneratorService, this.personRepository); + return mapEntitiesToEmailAggregate( + personId, + emailAddressEntities, + this.emailGeneratorService, + this.personRepository, + ); } - private async update(email: Email): Promise | DomainError> { - const emailEntity: Loaded = await this.em.findOneOrFail(EmailEntity, email.id, { - populate: ['emailAddresses'] as const, - }); - - emailEntity.assign(mapAggregateToData(email), {}); - - await this.em.persistAndFlush(emailEntity); - + private async update( + personId: PersonID, + emailAddresses: EmailAddress[], + ): Promise | DomainError> { //update the emailAddresses - if (email.emailAddresses) { - for (const emailAddress of email.emailAddresses) { - const emailAddressEntity: Option = await this.em.findOne(EmailAddressEntity, { - address: emailAddress.address, - }); - - if (emailAddressEntity) { - emailAddressEntity.assign(mapEmailAddressAggregateToData(emailAddress, emailEntity.id), {}); - await this.em.persistAndFlush(emailAddressEntity); - } else { - this.logger.error(`Email-address:${emailAddress.address} could not be found`); - return new EmailAddressNotFoundError(emailAddress.address); - } + const emailAddressEntities: EmailAddressEntity[] = []; + for (const emailAddress of emailAddresses) { + const emailAddressEntity: Option = await this.em.findOne(EmailAddressEntity, { + address: emailAddress.address, + }); + + if (emailAddressEntity) { + emailAddressEntity.assign(mapAggregateToData(emailAddress), {}); + emailAddressEntities.push(emailAddressEntity); + await this.em.persistAndFlush(emailAddressEntity); + } else { + this.logger.error(`Email-address:${emailAddress.address} could not be found`); + return new EmailAddressNotFoundError(emailAddress.address); } } - return mapEntityToAggregate(emailEntity, this.emailGeneratorService, this.personRepository); - } - - public async deleteById(id: EmailID): Promise { - const emailEntity: Option = await this.em.findOne(EmailEntity, { id }); - if (emailEntity) { - await this.em.removeAndFlush(emailEntity); - return true; - } - return false; + return mapEntitiesToEmailAggregate( + personId, + emailAddressEntities, + this.emailGeneratorService, + this.personRepository, + ); } } diff --git a/src/modules/person/persistence/person.repository.ts b/src/modules/person/persistence/person.repository.ts index 57db6f254..0467c7458 100644 --- a/src/modules/person/persistence/person.repository.ts +++ b/src/modules/person/persistence/person.repository.ts @@ -19,7 +19,7 @@ import { PersonEntity } from './person.entity.js'; import { PersonScope } from './person.scope.js'; import { EventService } from '../../../core/eventbus/index.js'; import { PersonDeletedEvent } from '../../../shared/events/person-deleted.event.js'; -import { EmailEntity } from '../../email/persistence/email.entity.js'; +import { EmailAddressEntity } from '../../email/persistence/email-address.entity.js'; export function mapAggregateToData(person: Person): RequiredEntityData { return { @@ -323,14 +323,9 @@ export class PersonRepository { } public async findEmailAddressByPerson(personId: PersonID): Promise { - const emailEntity: Option = await this.em.findOne( - EmailEntity, - { personId }, - { populate: ['emailAddresses'] as const }, - ); - if (!emailEntity) return undefined; + const emailAddressEntities: EmailAddressEntity[] = await this.em.find(EmailAddressEntity, { personId }, {}); - for (const emailAddressEntity of emailEntity.emailAddresses) { + for (const emailAddressEntity of emailAddressEntities) { if (emailAddressEntity.enabled) return emailAddressEntity.address; } return undefined; diff --git a/src/shared/types/aggregate-ids.types.ts b/src/shared/types/aggregate-ids.types.ts index 3aae5ee5e..3941b39b8 100644 --- a/src/shared/types/aggregate-ids.types.ts +++ b/src/shared/types/aggregate-ids.types.ts @@ -15,5 +15,5 @@ export type PersonenkontextID = Flavor; declare const serviceProviderSymbol: unique symbol; export type ServiceProviderID = Flavor; -declare const emailSymbol: unique symbol; -export type EmailID = Flavor; +declare const emailAddressSymbol: unique symbol; +export type EmailAddressID = Flavor;