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/seeding/dev/01/04_rolle.json b/seeding/dev/01/04_rolle.json index eb1d89673..b7f45cc04 100644 --- a/seeding/dev/01/04_rolle.json +++ b/seeding/dev/01/04_rolle.json @@ -116,6 +116,15 @@ "MIGRATION_DURCHFUEHREN" ], "serviceProviderIds": [] + }, + { + "id": 6, + "administeredBySchulstrukturknoten": 0, + "name": "Schulbegleitung", + "rollenart": "LERN", + "merkmale": [], + "systemrechte": [], + "serviceProviderIds": [] } ] } 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 d8427fa43..7c42e2954 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'; @@ -39,16 +39,11 @@ import { PersonRenamedEvent } from '../../../shared/events/person-renamed.event. import { Person } from '../../person/domain/person.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), + ]); } function getPerson(): Person { @@ -157,7 +152,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(), @@ -168,7 +163,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) => { @@ -184,7 +186,7 @@ describe('Email Event Handler', () => { get currentAddress(): Option { return 'test@schule-sh.de'; }, - id: emailId, + personId: fakePersonId, emailAddresses: emailAddresses, }), }; @@ -203,7 +205,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(), @@ -214,7 +216,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) => { @@ -255,7 +264,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>({ @@ -390,8 +408,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}`); @@ -503,7 +519,7 @@ describe('Email Event Handler', () => { get currentAddress(): Option { return 'test@schule-sh.de'; }, - id: faker.string.uuid(), + personId: faker.string.uuid(), emailAddresses: [], }), }; diff --git a/src/modules/email/domain/email-event-handler.ts b/src/modules/email/domain/email-event-handler.ts index 015bfbf6b..946acde4a 100644 --- a/src/modules/email/domain/email-event-handler.ts +++ b/src/modules/email/domain/email-event-handler.ts @@ -10,7 +10,6 @@ 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'; @@ -19,6 +18,7 @@ import { EmailAddressNotFoundError } from '../error/email-address-not-found.erro import { PersonRenamedEvent } from '../../../shared/events/person-renamed.event.js'; import { PersonRepository } from '../../person/persistence/person.repository.js'; import { Person } from '../../person/domain/person.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 2376a87b6..66b121d86 100644 --- a/src/modules/email/domain/email.spec.ts +++ b/src/modules/email/domain/email.spec.ts @@ -46,15 +46,16 @@ describe('Email Aggregate', () => { beforeEach(() => { jest.resetAllMocks(); newEmail = emailFactory.createNew(faker.string.uuid()); - firstEmailAddress = new EmailAddress(faker.string.uuid(), faker.internet.email(), false); - emailAddresses = [firstEmailAddress]; - existingEmail = emailFactory.construct( + firstEmailAddress = new EmailAddress( faker.string.uuid(), faker.date.past(), faker.date.recent(), faker.string.uuid(), - emailAddresses, + faker.internet.email(), + false, ); + emailAddresses = [firstEmailAddress]; + existingEmail = emailFactory.construct(faker.string.uuid(), emailAddresses); newNames = { vorname: faker.person.firstName(), familienname: faker.person.lastName(), @@ -64,6 +65,19 @@ describe('Email Aggregate', () => { describe('enable', () => { describe('when emailAddresses are already present on aggregate', () => { it('should return successfully', async () => { + /* const emailAddressId: EmailAddressID = faker.string.uuid(); + const emailAddresses: EmailAddress[] = [ + new EmailAddress( + emailAddressId, + faker.date.past(), + faker.date.recent(), + faker.string.uuid(), + faker.internet.email(), + false, + ), + ]; + const existingEmail: Email = emailFactory.construct(faker.string.uuid(), emailAddresses); +*/ const result: Result> = await existingEmail.enable(); expect(result.ok).toBeTruthy(); @@ -188,17 +202,14 @@ describe('Email Aggregate', () => { describe('when emailAddresses exist and at least one is enabled', () => { it('should return true ', () => { const emailAddress: EmailAddress = new EmailAddress( - faker.string.uuid(), - faker.internet.email(), - true, - ); - const email: Email = emailFactory.construct( faker.string.uuid(), faker.date.past(), faker.date.recent(), faker.string.uuid(), - [emailAddress], + faker.internet.email(), + true, ); + const email: Email = emailFactory.construct(faker.string.uuid(), [emailAddress]); const result: boolean = email.isEnabled(); expect(result).toBeTruthy(); @@ -216,17 +227,14 @@ describe('Email Aggregate', () => { describe('when emailAddresses exist and at least one is enabled', () => { it('should return the emailAddress-address string', () => { const emailAddress: EmailAddress = new EmailAddress( - faker.string.uuid(), - faker.internet.email(), - true, - ); - const email: Email = emailFactory.construct( faker.string.uuid(), faker.date.past(), faker.date.recent(), faker.string.uuid(), - [emailAddress], + faker.internet.email(), + true, ); + const email: Email = emailFactory.construct(faker.string.uuid(), [emailAddress]); const result: Option = email.currentAddress; expect(result).toBeDefined(); diff --git a/src/modules/email/domain/email.ts b/src/modules/email/domain/email.ts index 34e8f9632..5580e187c 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'; @@ -12,9 +12,6 @@ export type EmailAddressProperties = { 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, @@ -26,27 +23,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>> { @@ -68,9 +54,6 @@ export class Email { return { ok: true, value: new Email( - this.id, - this.createdAt, - this.updatedAt, this.personId, this.emailGeneratorService, this.personRepository, @@ -108,22 +91,21 @@ export class Email { error: new EmailInvalidError(), }; } - const newEmailAddress: EmailAddress = new EmailAddress(undefined, generatedAddress.value, true); + const newEmailAddress: EmailAddress = new EmailAddress( + undefined, + undefined, + undefined, + this.personId, + generatedAddress.value, + true, + ); let newAddresses: EmailAddress[] = [newEmailAddress]; if (this.emailAddresses) { newAddresses = newAddresses.concat(this.emailAddresses); } return { ok: true, - value: new Email( - this.id, - this.createdAt, - this.updatedAt, - this.personId, - this.emailGeneratorService, - this.personRepository, - newAddresses, - ), + value: new Email(this.personId, this.emailGeneratorService, this.personRepository, newAddresses), }; } diff --git a/src/modules/email/email.module.ts b/src/modules/email/email.module.ts index 805e189b7..0e4e16f55 100644 --- a/src/modules/email/email.module.ts +++ b/src/modules/email/email.module.ts @@ -4,36 +4,15 @@ import { EmailRepo } from './persistence/email.repo.js'; import { EmailFactory } from './domain/email.factory.js'; import { EmailGeneratorService } from './domain/email-generator.service.js'; import { EmailEventHandler } from './domain/email-event-handler.js'; -import { RolleRepo } from '../rolle/repo/rolle.repo.js'; -import { ServiceProviderRepo } from '../service-provider/repo/service-provider.repo.js'; -import { RolleFactory } from '../rolle/domain/rolle.factory.js'; -import { OrganisationRepository } from '../organisation/persistence/organisation.repository.js'; import { EmailServiceRepo } from './persistence/email-service.repo.js'; import { EventService } from '../../core/eventbus/index.js'; -import { PersonRepository } from '../person/persistence/person.repository.js'; -import { KeycloakUserService } from '../keycloak-administration/index.js'; -import { KeycloakAdministrationService } from '../keycloak-administration/domain/keycloak-admin-client.service.js'; -import { KeycloakAdminClient } from '@s3pweb/keycloak-admin-client-cjs'; -import { KeycloakAdministrationModule } from '../keycloak-administration/keycloak-administration.module.js'; +import { PersonModule } from '../person/person.module.js'; +import { RolleModule } from '../rolle/rolle.module.js'; +import { ServiceProviderModule } from '../service-provider/service-provider.module.js'; @Module({ - imports: [KeycloakAdministrationModule, LoggerModule.register(EmailModule.name)], - providers: [ - KeycloakUserService, - KeycloakAdministrationService, - KeycloakAdminClient, - PersonRepository, - RolleRepo, - RolleFactory, - ServiceProviderRepo, - OrganisationRepository, - EmailRepo, - EmailServiceRepo, - EmailFactory, - EmailGeneratorService, - EmailEventHandler, - EventService, - ], + imports: [PersonModule, RolleModule, ServiceProviderModule, LoggerModule.register(EmailModule.name)], + providers: [EmailRepo, EmailServiceRepo, EmailFactory, EmailGeneratorService, EmailEventHandler, EventService], exports: [EmailRepo, EmailFactory, EmailGeneratorService, EmailEventHandler], }) export class EmailModule {} 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/keycloak-administration/domain/keycloak-user.service.ts b/src/modules/keycloak-administration/domain/keycloak-user.service.ts index 3440666fe..16d58b50e 100644 --- a/src/modules/keycloak-administration/domain/keycloak-user.service.ts +++ b/src/modules/keycloak-administration/domain/keycloak-user.service.ts @@ -87,9 +87,10 @@ export class KeycloakUserService { } let algorithm: string; let hashIterations: number | undefined; + let passwordValue: string; if (hashedPassword.startsWith('{BCRYPT}')) { algorithm = 'bcrypt'; - const parts: string[] = hashedPassword.split('$'); + const parts: string[] = hashedPassword.split('$'); //Only Everything After and including the First $ if (parts.length < 4 || !parts[2]) { return { ok: false, @@ -97,6 +98,7 @@ export class KeycloakUserService { }; } hashIterations = parseInt(parts[2]); + passwordValue = hashedPassword.substring(hashedPassword.indexOf('$')); } else if (hashedPassword.startsWith('{crypt}')) { algorithm = 'crypt'; const parts: string[] = hashedPassword.split('$'); @@ -107,6 +109,7 @@ export class KeycloakUserService { }; } hashIterations = undefined; + passwordValue = hashedPassword.substring(hashedPassword.indexOf('$')); } else { return { ok: false, @@ -144,7 +147,7 @@ export class KeycloakUserService { algorithm: algorithm, }), secretData: JSON.stringify({ - value: hashedPassword, + value: passwordValue, }), type: 'password', }, diff --git a/src/modules/person/domain/person.ts b/src/modules/person/domain/person.ts index b2a96d622..174d4ba95 100644 --- a/src/modules/person/domain/person.ts +++ b/src/modules/person/domain/person.ts @@ -27,7 +27,6 @@ export type PersonCreationParams = { username?: string; password?: string; personalnummer?: string; - email?: string; }; export class Person { diff --git a/src/modules/person/persistence/person.repository.ts b/src/modules/person/persistence/person.repository.ts index 57db6f254..5fd707023 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 { @@ -322,15 +322,11 @@ export class PersonRepository { return person; } + // This method in principle should be located in email.repo. It is here to avoid a circular reference. 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/modules/person/person-api.module.ts b/src/modules/person/person-api.module.ts index 6efab4d8b..41d53efd9 100644 --- a/src/modules/person/person-api.module.ts +++ b/src/modules/person/person-api.module.ts @@ -6,13 +6,10 @@ import { PersonModule } from './person.module.js'; import { PersonFrontendController } from './api/person.frontend.controller.js'; import { PersonenkontextUc } from '../personenkontext/api/personenkontext.uc.js'; import { PersonenKontextModule } from '../personenkontext/personenkontext.module.js'; -import { UsernameGeneratorService } from './domain/username-generator.service.js'; -import { PersonRepository } from './persistence/person.repository.js'; import { RolleModule } from '../rolle/rolle.module.js'; import { OrganisationModule } from '../organisation/organisation.module.js'; import { KeycloakAdministrationModule } from '../keycloak-administration/keycloak-administration.module.js'; import { DBiamPersonenuebersichtController } from './api/personenuebersicht/dbiam-personenuebersicht.controller.js'; -import { DBiamPersonenkontextRepo } from '../personenkontext/persistence/dbiam-personenkontext.repo.js'; import { PersonInfoController } from './api/person-info.controller.js'; import { PersonApiMapper } from './mapper/person-api.mapper.js'; import { DBiamPersonController } from './api/dbiam-person.controller.js'; @@ -29,14 +26,7 @@ import { EventModule } from '../../core/eventbus/event.module.js'; LoggerModule.register(PersonApiModule.name), EventModule, ], - providers: [ - PersonApiMapperProfile, - PersonenkontextUc, - UsernameGeneratorService, - PersonRepository, - DBiamPersonenkontextRepo, - PersonApiMapper, - ], + providers: [PersonApiMapperProfile, PersonenkontextUc, PersonApiMapper], controllers: [ PersonController, PersonFrontendController, diff --git a/src/modules/rolle/api/rolle.controller.integration-spec.ts b/src/modules/rolle/api/rolle.controller.integration-spec.ts index 0c1af824b..3d492e642 100644 --- a/src/modules/rolle/api/rolle.controller.integration-spec.ts +++ b/src/modules/rolle/api/rolle.controller.integration-spec.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker'; import { EntityManager, MikroORM } from '@mikro-orm/core'; -import { INestApplication } from '@nestjs/common'; -import { APP_PIPE } from '@nestjs/core'; +import { CallHandler, ExecutionContext, INestApplication } from '@nestjs/common'; +import { APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; import request, { Response } from 'supertest'; import { App } from 'supertest/types.js'; @@ -30,6 +30,11 @@ import { RolleWithServiceProvidersResponse } from './rolle-with-serviceprovider. import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; import { PagedResponse } from '../../../shared/paging/index.js'; import { ServiceProviderIdNameResponse } from './serviceprovider-id-name.response.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Observable } from 'rxjs'; +import { PersonPermissionsRepo } from '../../authentication/domain/person-permission.repo.js'; +import { PassportUser } from '../../authentication/types/user.js'; +import { Request } from 'express'; describe('Rolle API', () => { let app: INestApplication; @@ -37,6 +42,7 @@ describe('Rolle API', () => { let em: EntityManager; let rolleRepo: RolleRepo; let serviceProviderRepo: ServiceProviderRepo; + let personpermissionsRepoMock: DeepMocked; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -51,6 +57,24 @@ describe('Rolle API', () => { provide: APP_PIPE, useClass: GlobalValidationPipe, }, + { + provide: APP_INTERCEPTOR, + useValue: { + intercept(context: ExecutionContext, next: CallHandler): Observable { + const req: Request = context.switchToHttp().getRequest(); + req.passportUser = createMock({ + async personPermissions() { + return personpermissionsRepoMock.loadPersonPermissions(''); + }, + }); + return next.handle(); + }, + }, + }, + { + provide: PersonPermissionsRepo, + useValue: createMock(), + }, OrganisationRepository, RolleFactory, ServiceProviderRepo, @@ -61,6 +85,7 @@ describe('Rolle API', () => { em = module.get(EntityManager); rolleRepo = module.get(RolleRepo); serviceProviderRepo = module.get(ServiceProviderRepo); + personpermissionsRepoMock = module.get(PersonPermissionsRepo); await DatabaseTestModule.setupDatabase(module.get(MikroORM)); app = module.createNestApplication(); await app.init(); @@ -211,6 +236,18 @@ describe('Rolle API', () => { expect(pagedResponse.items).toHaveLength(3); }); + it('should return no rollen', async () => { + const response: Response = await request(app.getHttpServer() as App) + .get('/rolle') + .send(); + + expect(response.status).toBe(200); + expect(response.body).toBeInstanceOf(Object); + const pagedResponse: PagedResponse = + response.body as PagedResponse; + expect(pagedResponse.items).toHaveLength(0); + }); + it('should return rollen with the given queried name', async () => { const testRolle: { name: string } = await rolleRepo.save(DoFactory.createRolle(false)); diff --git a/src/modules/rolle/api/rolle.controller.spec.ts b/src/modules/rolle/api/rolle.controller.spec.ts index 851871b11..1a55a8441 100644 --- a/src/modules/rolle/api/rolle.controller.spec.ts +++ b/src/modules/rolle/api/rolle.controller.spec.ts @@ -14,6 +14,7 @@ import { FindRolleByIdParams } from './find-rolle-by-id.params.js'; import { OrganisationService } from '../../organisation/domain/organisation.service.js'; import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; import { RolleNameQueryParams } from './rolle-name-query.param.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; describe('Rolle API with mocked ServiceProviderRepo', () => { let rolleRepoMock: DeepMocked; @@ -84,11 +85,14 @@ describe('Rolle API with mocked ServiceProviderRepo', () => { const params: RolleNameQueryParams = { searchStr: faker.string.alpha(), }; + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + //mock getRollenByName rolleRepoMock.findByName.mockResolvedValueOnce(undefined); //mock call to get sp (direct in controller-method) serviceProviderRepoMock.findById.mockResolvedValueOnce(undefined); - await expect(rolleController.findRollen(params)).resolves.not.toThrow(Error); + await expect(rolleController.findRollen(params, permissions)).resolves.not.toThrow(Error); }); }); }); diff --git a/src/modules/rolle/api/rolle.controller.ts b/src/modules/rolle/api/rolle.controller.ts index 4bd325dde..6e73ebb4b 100644 --- a/src/modules/rolle/api/rolle.controller.ts +++ b/src/modules/rolle/api/rolle.controller.ts @@ -49,6 +49,8 @@ import { ServiceProviderResponse } from '../../service-provider/api/service-prov import { SchulConnexError } from '../../../shared/error/schul-connex.error.js'; import { RolleExceptionFilter } from './rolle-exception-filter.js'; import { Paged, PagedResponse, PagingHeadersObject } from '../../../shared/paging/index.js'; +import { Permissions } from '../../authentication/api/permissions.decorator.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; @UseFilters(new SchulConnexValidationErrorFilter(), new RolleExceptionFilter()) @ApiTags('rolle') @@ -75,18 +77,16 @@ export class RolleController { @ApiInternalServerErrorResponse({ description: 'Internal server error while getting all rollen.' }) public async findRollen( @Query() queryParams: RolleNameQueryParams, + @Permissions() permissions: PersonPermissions, ): Promise> { - let rollen: Option[]>; - - if (queryParams.searchStr) { - rollen = await this.rolleRepo.findByName(queryParams.searchStr, queryParams.limit, queryParams.offset); - } else { - rollen = await this.rolleRepo.find(queryParams.limit, queryParams.offset); - } - - const serviceProviders: ServiceProvider[] = await this.serviceProviderRepo.find(); + const rollen: Option[]> = await this.rolleRepo.findRollenAuthorized( + permissions, + queryParams.searchStr, + queryParams.limit, + queryParams.offset, + ); - if (!rollen) { + if (!rollen || rollen.length === 0) { const pagedRolleWithServiceProvidersResponse: Paged = { total: 0, offset: 0, @@ -95,6 +95,7 @@ export class RolleController { }; return new PagedResponse(pagedRolleWithServiceProvidersResponse); } + const serviceProviders: ServiceProvider[] = await this.serviceProviderRepo.find(); const rollenWithServiceProvidersResponses: RolleWithServiceProvidersResponse[] = rollen.map( (r: Rolle) => { diff --git a/src/modules/rolle/repo/rolle.repo.spec.ts b/src/modules/rolle/repo/rolle.repo.spec.ts index 1c3d67e3d..54a7df4f0 100644 --- a/src/modules/rolle/repo/rolle.repo.spec.ts +++ b/src/modules/rolle/repo/rolle.repo.spec.ts @@ -16,6 +16,9 @@ import { ServiceProviderRepo } from '../../service-provider/repo/service-provide import { ServiceProvider } from '../../service-provider/domain/service-provider.js'; import { EventService } from '../../../core/eventbus/index.js'; import { OrganisationRepository } from '../../organisation/persistence/organisation.repository.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { OrganisationID } from '../../../shared/types/index.js'; describe('RolleRepo', () => { let module: TestingModule; @@ -118,7 +121,74 @@ describe('RolleRepo', () => { expect(rolle).toBeNull(); }); }); + describe('findRollenAuthorized', () => { + it('should return no rollen because there are none', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(0); + }); + + it('should return the rollen when permissions are sufficient', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save(DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId })); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(1); + }); + + it('should return empty array when permissions are insufficient', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save(DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId })); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(0); + }); + + it('should filter rollen based on search string and permissions', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save( + DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId, name: 'Test' }), + ); + await sut.save( + DoFactory.createRolle(false, { + administeredBySchulstrukturknoten: organisationId, + name: 'AnotherName', + }), + ); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, 'Test', 10, 0); + + expect(rolleResult?.length).toBe(1); + }); + + it('should return all rollen when no search string is provided and permissions are sufficient', async () => { + const organisationId: OrganisationID = faker.string.uuid(); + await sut.save(DoFactory.createRolle(false, { administeredBySchulstrukturknoten: organisationId })); + + const permissions: DeepMocked = createMock(); + permissions.getOrgIdsWithSystemrecht.mockResolvedValueOnce([organisationId]); + + const rolleResult: Option[]> = await sut.findRollenAuthorized(permissions, undefined, 10, 0); + + expect(rolleResult?.length).toBe(1); + }); + }); describe('findByName', () => { it('should return the rolle', async () => { const rolle: Rolle = await sut.save(DoFactory.createRolle(false)); diff --git a/src/modules/rolle/repo/rolle.repo.ts b/src/modules/rolle/repo/rolle.repo.ts index de63c2275..7b72a8896 100644 --- a/src/modules/rolle/repo/rolle.repo.ts +++ b/src/modules/rolle/repo/rolle.repo.ts @@ -7,8 +7,9 @@ import { RolleMerkmalEntity } from '../entity/rolle-merkmal.entity.js'; import { RolleEntity } from '../entity/rolle.entity.js'; import { RolleFactory } from '../domain/rolle.factory.js'; import { RolleServiceProviderEntity } from '../entity/rolle-service-provider.entity.js'; -import { RolleID } from '../../../shared/types/index.js'; +import { OrganisationID, RolleID } from '../../../shared/types/index.js'; import { RolleSystemrechtEntity } from '../entity/rolle-systemrecht.entity.js'; +import { PersonPermissions } from '../../authentication/domain/person-permissions.js'; /** * @deprecated Not for use outside of rolle-repo, export will be removed at a later date @@ -129,6 +130,42 @@ export class RolleRepo { return rollen.map((rolle: RolleEntity) => mapEntityToAggregate(rolle, this.rolleFactory)); } + public async findRollenAuthorized( + permissions: PersonPermissions, + searchStr?: string, + limit?: number, + offset?: number, + ): Promise[]>> { + let rollen: Option; + if (searchStr) { + rollen = await this.em.find( + this.entityName, + { name: { $ilike: '%' + searchStr + '%' } }, + { populate: ['merkmale', 'systemrechte', 'serviceProvider'] as const, limit: limit, offset: offset }, + ); + } else { + rollen = await this.em.findAll(this.entityName, { + populate: ['merkmale', 'systemrechte', 'serviceProvider'] as const, + limit: limit, + offset: offset, + }); + } + if (rollen.length === 0) { + return []; + } + + const orgIdsWithRecht: OrganisationID[] = await permissions.getOrgIdsWithSystemrecht( + [RollenSystemRecht.ROLLEN_VERWALTEN], + true, + ); + + const filteredRollen: RolleEntity[] = rollen.filter((rolle: RolleEntity) => + orgIdsWithRecht.includes(rolle.administeredBySchulstrukturknoten), + ); + + return filteredRollen.map((rolle: RolleEntity) => mapEntityToAggregate(rolle, this.rolleFactory)); + } + public async exists(id: RolleID): Promise { const rolle: Option> = await this.em.findOne( RolleEntity, diff --git a/src/modules/rolle/rolle.module.ts b/src/modules/rolle/rolle.module.ts index 0f466d70d..d5813194f 100644 --- a/src/modules/rolle/rolle.module.ts +++ b/src/modules/rolle/rolle.module.ts @@ -2,13 +2,12 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '../../core/logging/logger.module.js'; import { RolleRepo } from './repo/rolle.repo.js'; import { ServiceProviderModule } from '../service-provider/service-provider.module.js'; -import { ServiceProviderRepo } from '../service-provider/repo/service-provider.repo.js'; import { RolleFactory } from './domain/rolle.factory.js'; import { OrganisationModule } from '../organisation/organisation.module.js'; @Module({ imports: [ServiceProviderModule, LoggerModule.register(RolleModule.name), OrganisationModule], - providers: [RolleRepo, RolleFactory, ServiceProviderRepo], + providers: [RolleRepo, RolleFactory], exports: [RolleRepo, RolleFactory], }) export class RolleModule {} 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;