Skip to content

Commit

Permalink
feat(api): add deployments (#838)
Browse files Browse the repository at this point in the history
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jasper Herzberg <JSPRH@users.noreply.github.com>
  • Loading branch information
3 people authored May 7, 2024
1 parent fd27501 commit c13b7c0
Show file tree
Hide file tree
Showing 72 changed files with 3,950 additions and 34 deletions.
2 changes: 2 additions & 0 deletions apps/api/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { MongooseModule } from '@nestjs/mongoose';
import * as path from 'path';

import { AuthModule } from '@kordis/api/auth';
import { DeploymentModule } from '@kordis/api/deployment';
import { ObservabilityModule } from '@kordis/api/observability';
import { OrganizationModule } from '@kordis/api/organization';
import {
Expand Down Expand Up @@ -36,6 +37,7 @@ const FEATURE_MODULES = [
UsersModule.forRoot(process.env.AUTH_PROVIDER === 'dev' ? 'dev' : 'aadb2c'),
UnitModule,
TetraModule,
DeploymentModule,
];
const SAGA_MODULES = [UnitsSagaModule];
const UTILITY_MODULES = [
Expand Down
18 changes: 18 additions & 0 deletions libs/api/deployment/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"extends": ["../../../.eslintrc.json"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
14 changes: 14 additions & 0 deletions libs/api/deployment/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# API Deployment

Currently we have two types of Deplyoments: a _Rescue Station_ and an
_Operation_. The Rescue Station data holder is this domain, as a Rescue Station
only lives in this domain and has no further purpose, whereas the Operation is a
unique domain. Rescue Stations are managed by the Rescue Station Manager, which
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.

## Running unit tests

Run `nx test api-deployment` to execute the unit tests via
[Jest](https://jestjs.io).
11 changes: 11 additions & 0 deletions libs/api/deployment/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable */
export default {
displayName: 'api-deployment',
preset: '../../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }],
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/api/deployment',
};
20 changes: 20 additions & 0 deletions libs/api/deployment/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "api-deployment",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/api/deployment/src",
"projectType": "library",
"targets": {
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/api/deployment/jest.config.ts"
}
}
},
"tags": []
}
8 changes: 8 additions & 0 deletions libs/api/deployment/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export * from './lib/infra/deployment.module';
export * from './lib/core/exception/deployment-not-found.exception';
export * from './lib/infra/rescue-station.view-model';
export * from './lib/core/query/get-rescue-station-deployment.query';
export * from './lib/infra/rescue-station.view-model';
export * from './lib/core/command/sign-in-rescue-station.command';
export * from './lib/core/command/sign-off-rescue-station.command';
export * from './lib/core/command/update-signed-in-rescue-station.command';
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';

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

import {
RESCUE_STATION_DEPLOYMENT_REPOSITORY,
RescueStationDeploymentRepository,
} from '../repository/rescue-station-deployment.repository';
import { DeploymentAssignmentService } from '../service/deployment-assignment.service';
import { StrengthFromCommandFactory } from '../service/strength-from-command.factory';
import {
SignInRescueStationCommand,
SignInRescueStationHandler,
} from './sign-in-rescue-station.command';

describe('SignInRescueStationHandler', () => {
let handler: SignInRescueStationHandler;
const mockRescueStationDeploymentRepository =
createMock<RescueStationDeploymentRepository>();
const mockDeploymentAssignmentService =
createMock<DeploymentAssignmentService>();

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SignInRescueStationHandler,
StrengthFromCommandFactory,
{
provide: RESCUE_STATION_DEPLOYMENT_REPOSITORY,
useValue: mockRescueStationDeploymentRepository,
},
{
provide: DeploymentAssignmentService,
useValue: mockDeploymentAssignmentService,
},
uowMockProvider(),
],
}).compile();

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

it('should call assignment and update', async () => {
const orgId = 'orgId';
const rescueStationId = 'rescueStationId';
const strength = {
leaders: 1,
subLeaders: 1,
helpers: 1,
};
const note = 'note';
const assignedUnitIds = ['unitId'];
const assignedAlertGroups = [
{ alertGroupId: 'alertGroupId', unitIds: ['unitId'] },
];
const command = new SignInRescueStationCommand(
orgId,
rescueStationId,
strength,
note,
assignedUnitIds,
assignedAlertGroups,
);
await handler.execute(command);

expect(
mockDeploymentAssignmentService.setAssignmentsOfDeployment,
).toHaveBeenCalledWith(
orgId,
rescueStationId,
assignedUnitIds,
assignedAlertGroups,
expect.anything(),
);
expect(mockRescueStationDeploymentRepository.updateOne).toHaveBeenCalled();
});

it('should throw validation exception when strength is invalid', async () => {
const command = new SignInRescueStationCommand(
'orgId',
'rescueStationId',
{
leaders: -1,
subLeaders: 1,
helpers: 1,
},
'note',
['unitId'],
[{ alertGroupId: 'alertGroupId', unitIds: ['unitId'] }],
);

await expect(handler.execute(command)).rejects.toThrow(ValidationException);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

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

import {
RESCUE_STATION_DEPLOYMENT_REPOSITORY,
RescueStationDeploymentRepository,
} from '../repository/rescue-station-deployment.repository';
import { DeploymentAssignmentService } from '../service/deployment-assignment.service';
import { StrengthFromCommandFactory } from '../service/strength-from-command.factory';

export class SignInRescueStationCommand {
constructor(
readonly orgId: string,
readonly rescueStationId: string,
readonly strength: {
readonly leaders: number;
readonly subLeaders: number;
readonly helpers: number;
},
readonly note: string,
readonly assignedUnitIds: string[],
readonly assignedAlertGroups: {
readonly alertGroupId: string;
readonly unitIds: string[];
}[],
) {}
}

@CommandHandler(SignInRescueStationCommand)
export class SignInRescueStationHandler
implements ICommandHandler<SignInRescueStationCommand>
{
constructor(
@Inject(RESCUE_STATION_DEPLOYMENT_REPOSITORY)
private readonly rescueStationDeploymentRepository: RescueStationDeploymentRepository,
private readonly deploymentAssignmentService: DeploymentAssignmentService,
private readonly strengthFactory: StrengthFromCommandFactory,
@Inject(UNIT_OF_WORK_SERVICE)
private readonly uow: UnitOfWorkService,
) {}

async execute(command: SignInRescueStationCommand): Promise<void> {
await this.uow.asTransaction(async (uow) => {
await this.deploymentAssignmentService.setAssignmentsOfDeployment(
command.orgId,
command.rescueStationId,
command.assignedUnitIds,
command.assignedAlertGroups,
uow,
);

const strength = this.strengthFactory.create(command.strength);
await strength.validOrThrow();

await this.rescueStationDeploymentRepository.updateOne(
command.orgId,
command.rescueStationId,
{
signedIn: true,
note: command.note,
strength: strength,
},
uow,
);
}, 3);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { createMock } from '@golevelup/ts-jest';
import { Test, TestingModule } from '@nestjs/testing';

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

import {
DEPLOYMENT_ASSIGNMENT_REPOSITORY,
DeploymentAssignmentRepository,
} from '../repository/deployment-assignment.repository';
import {
RESCUE_STATION_DEPLOYMENT_REPOSITORY,
RescueStationDeploymentRepository,
} from '../repository/rescue-station-deployment.repository';
import {
SignOffRescueStationCommand,
SignOffRescueStationHandler,
} from './sign-off-rescue-station.command';

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

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

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

it('should call specific methods when command is executed', async () => {
const orgId = 'orgId';
const rescueStationId = 'rescueStationId';
const command = new SignOffRescueStationCommand(orgId, rescueStationId);
await handler.execute(command);

expect(
mockDeploymentAssignmentRepository.removeAssignmentsOfDeployment,
).toHaveBeenCalledWith(orgId, rescueStationId, expect.anything());
expect(
mockRescueStationDeploymentRepository.updateOne,
).toHaveBeenCalledWith(
orgId,
rescueStationId,
{
signedIn: false,
note: '',
strength: {
helpers: 0,
leaders: 0,
subLeaders: 0,
},
},
expect.anything(),
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';

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

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

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

@CommandHandler(SignOffRescueStationCommand)
export class SignOffRescueStationHandler
implements ICommandHandler<SignOffRescueStationCommand>
{
constructor(
@Inject(RESCUE_STATION_DEPLOYMENT_REPOSITORY)
private readonly rescueStationDeploymentRepository: RescueStationDeploymentRepository,
@Inject(DEPLOYMENT_ASSIGNMENT_REPOSITORY)
private readonly deploymentAssignmentRepository: DeploymentAssignmentRepository,
@Inject(UNIT_OF_WORK_SERVICE)
private readonly uow: UnitOfWorkService,
) {}

async execute({
orgId,
rescueStationId,
}: SignOffRescueStationCommand): Promise<void> {
await this.uow.asTransaction(async (uow) => {
await this.deploymentAssignmentRepository.removeAssignmentsOfDeployment(
orgId,
rescueStationId,
uow,
);

await this.rescueStationDeploymentRepository.updateOne(
orgId,
rescueStationId,
{
signedIn: false,
note: '',
strength: RescueStationStrength.makeDefault(),
},
uow,
);
});
}
}
Loading

0 comments on commit c13b7c0

Please sign in to comment.