Skip to content

Commit

Permalink
feat(spa): add deployments (#967)
Browse files Browse the repository at this point in the history
Signed-off-by: Timon Masberg <[email protected]>
Co-authored-by: Jasper Herzberg <[email protected]>
  • Loading branch information
timonmasberg and JSPRH authored Dec 29, 2024
1 parent 64926b2 commit 0ddc556
Show file tree
Hide file tree
Showing 162 changed files with 14,536 additions and 7,068 deletions.
10 changes: 5 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,11 @@ generate a new component, run
`npx nx g @nrwl/angular:component --project=spa-<lib> --name=<component name>`.
This will generate a new component in the correct folder. For more information,
check out the [NX documentation]https://nx.dev/more-concepts/nx-and-angular). By
default the Component will be standalone and with `OnPush` Change Detection.
Please try to stick to this as much as possible. You can serve the SPA with
`npm run serve:spa`. Instead of an OAuth Provider, in development environments
you will get redirected to a login page where you can choose a persona. This
will set a JWT in the local storage.
default the Component will be standalone and with `OnPush` Change Detection. Please
try to stick to this as much as possible. You can serve the SPA with `npm run serve:spa`.
Instead of an OAuth Provider, in development environments you will get redirected
to a login page where you can choose a persona. This will set a JWT in the local
storage.

We agreed on supporting only Chromium based browsers with their 2 latest major
versions. This allows us to use features that are available for these browsers
Expand Down
3 changes: 2 additions & 1 deletion apps/api/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ ENV NODE_ENV=production
# Install dependencies separately for caching
COPY ./dist/apps/api/package.json ./dist/apps/api/package-lock.json ./

RUN npm ci --omit=dev
# ci is failing here due to nx bug https://github.com/nrwl/nx/issues/15366 (closed, but still an issue)
RUN npm i --omit=dev

COPY ./dist/apps/api ./

Expand Down
3 changes: 1 addition & 2 deletions apps/spa/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
{
"compilerOptions": {
"target": "es2022",
"target": "ES2022",
"useDefineForClassFields": false,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"allowSyntheticDefaultImports": true
Expand Down
11 changes: 11 additions & 0 deletions libs/api/deployment/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ also throw events regarding the rescue stations, as signing in, signing off and
updating contains cross boundary process logic. The commands are exported and
can be called by the managers, as they act as an API.

## Deployment Assignments

A deployment can have an alert group with corresponding units and stand-alone
units assigned. An alert group has default units and current units. At first, an
alert group will always have the default units as current units. The main
purpose of current units is to have a default selection of units whenever the
user selects an alert-group. If a unit was previously assigned to another
deployment, it will be removed from the previous deployment and added to the new
deployment. The logic is handled in the
[`DeploymentAssignmentService`](./src/lib/core/service/deployment-assignment.service.ts).

## Running unit tests

Run `nx test api-deployment` to execute the unit tests via
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { createMock } from '@golevelup/ts-jest';
import { EventBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';

import { uowMockProvider } from '@kordis/api/test-helpers';

import {
RescueStationDeploymentEntity,
RescueStationStrength,
} from '../entity/rescue-station-deployment.entity';
import { RescueStationsResetEvent } from '../event/rescue-stations-reset.event';
import {
DEPLOYMENT_ASSIGNMENT_REPOSITORY,
DeploymentAssignmentRepository,
} from '../repository/deployment-assignment.repository';
import {
RESCUE_STATION_DEPLOYMENT_REPOSITORY,
RescueStationDeploymentRepository,
} from '../repository/rescue-station-deployment.repository';
import {
ResetRescueStationsCommand,
ResetRescueStationsHandler,
} from './reset-rescue-stations.command';

describe('ResetRescueStationsHandler', () => {
let handler: ResetRescueStationsHandler;
const mockRescueStationDeploymentRepository =
createMock<RescueStationDeploymentRepository>();
const mockDeploymentAssignmentRepository =
createMock<DeploymentAssignmentRepository>();
const mockEventBus = createMock<EventBus>();

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ResetRescueStationsHandler,
{
provide: RESCUE_STATION_DEPLOYMENT_REPOSITORY,
useValue: mockRescueStationDeploymentRepository,
},
{
provide: DEPLOYMENT_ASSIGNMENT_REPOSITORY,
useValue: mockDeploymentAssignmentRepository,
},
{
provide: EventBus,
useValue: mockEventBus,
},
uowMockProvider(),
],
}).compile();

handler = module.get<ResetRescueStationsHandler>(
ResetRescueStationsHandler,
);
});

it('should reset rescue stations and publish event', async () => {
const orgId = 'orgId';
const rescueStations = [
{ id: 'rescueStationId1' },
{ id: 'rescueStationId2' },
] as RescueStationDeploymentEntity[];
mockRescueStationDeploymentRepository.findByOrgId.mockResolvedValue(
rescueStations,
);

const command = new ResetRescueStationsCommand(orgId);
await handler.execute(command);

expect(
mockRescueStationDeploymentRepository.findByOrgId,
).toHaveBeenCalledWith(orgId, undefined, expect.anything());

expect(
mockDeploymentAssignmentRepository.removeAssignmentsOfDeployments,
).toHaveBeenCalledWith(
orgId,
['rescueStationId1', 'rescueStationId2'],
expect.anything(),
);
const expectedStrength = new RescueStationStrength();
expectedStrength.leaders =
expectedStrength.helpers =
expectedStrength.subLeaders =
0;

expect(
mockRescueStationDeploymentRepository.updateAll,
).toHaveBeenCalledWith(
orgId,
{
signedIn: false,
note: '',
strength: expectedStrength,
},
expect.anything(),
);
expect(mockEventBus.publish).toHaveBeenCalledWith(
new RescueStationsResetEvent(orgId),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';
import { plainToInstance } from 'class-transformer';

import { UNIT_OF_WORK_SERVICE, UnitOfWorkService } from '@kordis/api/shared';

import { RescueStationStrength } from '../entity/rescue-station-deployment.entity';
import { RescueStationsResetEvent } from '../event/rescue-stations-reset.event';
import {
DEPLOYMENT_ASSIGNMENT_REPOSITORY,
DeploymentAssignmentRepository,
} from '../repository/deployment-assignment.repository';
import {
RESCUE_STATION_DEPLOYMENT_REPOSITORY,
RescueStationDeploymentRepository,
} from '../repository/rescue-station-deployment.repository';

export class ResetRescueStationsCommand {
constructor(readonly orgId: string) {}
}

@CommandHandler(ResetRescueStationsCommand)
export class ResetRescueStationsHandler
implements ICommandHandler<ResetRescueStationsCommand>
{
constructor(
@Inject(RESCUE_STATION_DEPLOYMENT_REPOSITORY)
private readonly rescueStationDeploymentRepository: RescueStationDeploymentRepository,
@Inject(DEPLOYMENT_ASSIGNMENT_REPOSITORY)
private readonly deploymentsAssignmentsRepository: DeploymentAssignmentRepository,
@Inject(UNIT_OF_WORK_SERVICE)
private readonly uow: UnitOfWorkService,
private readonly eventBus: EventBus,
) {}

async execute({ orgId }: ResetRescueStationsCommand): Promise<void> {
await this.uow.asTransaction(async (uow) => {
const rescueStations =
await this.rescueStationDeploymentRepository.findByOrgId(
orgId,
undefined,
uow,
);

// remove assignments of all rescue stations
await this.deploymentsAssignmentsRepository.removeAssignmentsOfDeployments(
orgId,
rescueStations.map((rescueStation) => rescueStation.id),
uow,
);

// reset all rescue stations (sign out, reset note and strength)
await this.rescueStationDeploymentRepository.updateAll(
orgId,
{
signedIn: false,
note: '',
strength: plainToInstance(RescueStationStrength, {
leaders: 0,
helpers: 0,
subLeaders: 0,
}),
},
uow,
);
}, 3);

this.eventBus.publish(new RescueStationsResetEvent(orgId));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { createMock } from '@golevelup/ts-jest';
import { EventBus } from '@nestjs/cqrs';
import { Test, TestingModule } from '@nestjs/testing';

import { RescueStationNoteUpdatedEvent } from '../event/rescue-station-note-updated.event';
import {
RESCUE_STATION_DEPLOYMENT_REPOSITORY,
RescueStationDeploymentRepository,
} from '../repository/rescue-station-deployment.repository';
import {
UpdateRescueStationNoteCommand,
UpdateRescueStationNoteHandler,
} from './update-rescue-station-note.command';

describe('UpdateRescueStationNoteHandler', () => {
let handler: UpdateRescueStationNoteHandler;
const mockRescueStationDeploymentRepository =
createMock<RescueStationDeploymentRepository>();
const mockEventBus = createMock<EventBus>();

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
UpdateRescueStationNoteHandler,
{
provide: RESCUE_STATION_DEPLOYMENT_REPOSITORY,
useValue: mockRescueStationDeploymentRepository,
},
{
provide: EventBus,
useValue: mockEventBus,
},
],
}).compile();

handler = module.get<UpdateRescueStationNoteHandler>(
UpdateRescueStationNoteHandler,
);
});

it('should update the note and publish an event', async () => {
const orgId = 'orgId';
const rescueStationId = 'rescueStationId';
const note = 'new note';

const command = new UpdateRescueStationNoteCommand(
orgId,
rescueStationId,
note,
);
await handler.execute(command);

expect(
mockRescueStationDeploymentRepository.updateOne,
).toHaveBeenCalledWith(orgId, rescueStationId, { note });
expect(mockEventBus.publish).toHaveBeenCalledWith(
new RescueStationNoteUpdatedEvent(orgId, rescueStationId),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, EventBus, ICommandHandler } from '@nestjs/cqrs';

import { RescueStationNoteUpdatedEvent } from '../event/rescue-station-note-updated.event';
import {
RESCUE_STATION_DEPLOYMENT_REPOSITORY,
RescueStationDeploymentRepository,
} from '../repository/rescue-station-deployment.repository';

export class UpdateRescueStationNoteCommand {
constructor(
readonly orgId: string,
readonly rescueStationId: string,
readonly note: string,
) {}
}

@CommandHandler(UpdateRescueStationNoteCommand)
export class UpdateRescueStationNoteHandler
implements ICommandHandler<UpdateRescueStationNoteCommand>
{
constructor(
@Inject(RESCUE_STATION_DEPLOYMENT_REPOSITORY)
private readonly rescueStationDeploymentRepository: RescueStationDeploymentRepository,
private readonly eventBus: EventBus,
) {}

async execute({
orgId,
rescueStationId,
note,
}: UpdateRescueStationNoteCommand): Promise<void> {
await this.rescueStationDeploymentRepository.updateOne(
orgId,
rescueStationId,
{
note,
},
);

this.eventBus.publish(
new RescueStationNoteUpdatedEvent(orgId, rescueStationId),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export class RescueStationNoteUpdatedEvent {
constructor(
readonly orgId: string,
readonly rescueStationId: string,
) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export class RescueStationsResetEvent {
constructor(readonly orgId: string) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';

import {
UNIT_ASSIGNMENT_REPOSITORY,
UnitAssignmentRepository,
} from '../repository/unit-assignment.repository';
import {
GetAlertGroupAssignedUnitsHandler,
GetAlertGroupAssignedUnitsQuery,
} from './get-alert-group-assigned-units.query';

describe('GetAlertGroupAssignedUnitsHandler', () => {
let handler: GetAlertGroupAssignedUnitsHandler;
const mockUnitAssignmentRepository = createMock<UnitAssignmentRepository>();

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
GetAlertGroupAssignedUnitsHandler,
{
provide: UNIT_ASSIGNMENT_REPOSITORY,
useValue: mockUnitAssignmentRepository,
},
],
}).compile();

handler = module.get<GetAlertGroupAssignedUnitsHandler>(
GetAlertGroupAssignedUnitsHandler,
);
});

it('should get assigned units of alert group', async () => {
const orgId = 'orgId';
const alertGroupId = 'alertGroupId';
const query = new GetAlertGroupAssignedUnitsQuery(orgId, alertGroupId);
await handler.execute(query);

expect(
mockUnitAssignmentRepository.getUnitsOfAlertGroup,
).toHaveBeenCalledWith(orgId, alertGroupId);
});
});
Loading

0 comments on commit 0ddc556

Please sign in to comment.