diff --git a/backend/libs/typescript-sdk/src/exception.ts b/backend/libs/typescript-sdk/src/exception.ts new file mode 100644 index 0000000..1f06e24 --- /dev/null +++ b/backend/libs/typescript-sdk/src/exception.ts @@ -0,0 +1,15 @@ +export class Exception extends Error { + constructor(message: string, private readonly causation: Error | undefined = undefined) { + super(message); + this.stack = causation ? Exception.errorCausedBy(this, causation).stack : this.stack; + } + + causedBy(causation: Error): Exception { + return new Exception(Exception.errorCausedBy(this, causation).message, causation); + } + + private static errorCausedBy(error: Error, causation: Error): Error { + error.stack += '\nCaused by: \n' + causation.message + '\n' + causation.stack; + return error; + } +} diff --git a/backend/libs/typescript-sdk/src/index.ts b/backend/libs/typescript-sdk/src/index.ts new file mode 100644 index 0000000..3d5b914 --- /dev/null +++ b/backend/libs/typescript-sdk/src/index.ts @@ -0,0 +1 @@ +export * from './exception'; diff --git a/backend/libs/typescript-sdk/tsconfig.lib.json b/backend/libs/typescript-sdk/tsconfig.lib.json new file mode 100644 index 0000000..960f5d0 --- /dev/null +++ b/backend/libs/typescript-sdk/tsconfig.lib.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true, + "outDir": "../../dist/libs/typescript-sdk" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/backend/nest-cli.json b/backend/nest-cli.json index 11d886a..0917a12 100755 --- a/backend/nest-cli.json +++ b/backend/nest-cli.json @@ -55,6 +55,15 @@ "compilerOptions": { "tsConfigPath": "libs/eventstore-projections/tsconfig.lib.json" } + }, + "typescript-sdk": { + "type": "library", + "root": "libs/typescript-sdk", + "entryFile": "index", + "sourceRoot": "libs/typescript-sdk/src", + "compilerOptions": { + "tsConfigPath": "libs/typescript-sdk/tsconfig.lib.json" + } } }, "compilerOptions": { diff --git a/backend/package-lock.json b/backend/package-lock.json index d8e9bf7..7e56d42 100755 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -8190,6 +8190,11 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz", "integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg==" }, + "monet": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/monet/-/monet-0.9.1.tgz", + "integrity": "sha512-GaZw308g6t0WD+U2mGujwJckGp2b8AGGVuB6PiIwkXbq6rJeiqddre2mdbO4PxjyILPi+7GgtRkkfr6dBYXWtA==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index 4bfcfd9..23495f6 100755 --- a/backend/package.json +++ b/backend/package.json @@ -51,6 +51,7 @@ "class-transformer": "^0.2.3", "class-validator": "^0.12.2", "moment": "^2.25.3", + "monet": "^0.9.1", "pg": "^8.2.0", "postgres": "^1.0.2", "reflect-metadata": "^0.1.13", @@ -115,10 +116,12 @@ "@coders-board-library/axios-utils/(.*)": "/libs/axios-utils/src/$1", "@coders-board-library/axios-utils": "/libs/axios-utils/src", "@coders-board-library/eventstore-projections/(.*)": "/libs/eventstore-projections/src/$1", - "@coders-board-library/eventstore-projections": "/libs/eventstore-projections/src" + "@coders-board-library/eventstore-projections": "/libs/eventstore-projections/src", + "@coders-board-library/typescript-sdk/(.*)": "/libs/typescript-sdk/src/$1", + "@coders-board-library/typescript-sdk": "/libs/typescript-sdk/src" } }, "eslintIgnore": [ "**/*.projection.*" ] -} +} \ No newline at end of file diff --git a/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/applicant-invitation.v1.write-side-controller.ts b/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/applicant-invitation.v1.write-side-controller.ts index 90056ec..e39f5ef 100644 --- a/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/applicant-invitation.v1.write-side-controller.ts +++ b/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/applicant-invitation.v1.write-side-controller.ts @@ -9,6 +9,7 @@ import { ApiCreatedResponse, ApiNoContentResponse, ApiTags } from '@nestjs/swagg import { InviteApplicant } from '../../../application/internal-command/invite-applicant.internal-command'; import { CancelApplicantInvitation } from '../../../application/internal-command/cancel-applicant-invitation.internal-command'; import { v4 as uuid } from 'uuid'; +import { CancelApplicantInvitationRequestParams } from './request-params/cancel-applicant-invitation.request-params'; @ApiTags('inviting-applicants') @Controller('/rest-api/v1/applicant-invitations') @@ -36,7 +37,7 @@ export class ApplicantInvitationV1WriteSideController { }) @HttpCode(204) @Post(':invitationId/cancellation') - postApplicantInvitationCancellation(@Param('invitationId') invitationId: string) { - return this.internalCommandBus.sendAndWait(new CancelApplicantInvitation(invitationId)); + postApplicantInvitationCancellation(@Param() params: CancelApplicantInvitationRequestParams) { + return this.internalCommandBus.sendAndWait(new CancelApplicantInvitation(params.invitationId)); } } diff --git a/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/request/cancel-applicant-invitation.request-body.ts b/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/request-params/cancel-applicant-invitation.request-params.ts similarity index 61% rename from backend/src/inviting-applicants/write-side/presentation/rest-api/v1/request/cancel-applicant-invitation.request-body.ts rename to backend/src/inviting-applicants/write-side/presentation/rest-api/v1/request-params/cancel-applicant-invitation.request-params.ts index b8b5564..00c4c22 100644 --- a/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/request/cancel-applicant-invitation.request-body.ts +++ b/backend/src/inviting-applicants/write-side/presentation/rest-api/v1/request-params/cancel-applicant-invitation.request-params.ts @@ -1,8 +1,9 @@ -import { IsDefined, IsNotEmpty } from 'class-validator'; +import { IsDefined, IsNotEmpty, IsUUID } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -export class CancelApplicantInvitationRequestBody { +export class CancelApplicantInvitationRequestParams { @ApiProperty() + @IsUUID() @IsDefined() @IsNotEmpty() readonly invitationId: string; diff --git a/backend/src/main.ts b/backend/src/main.ts index ba5994e..b03627b 100755 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { INestApplication, ValidationPipe } from '@nestjs/common'; +import { InternalCommandInvalidSchemaExceptionFilter } from './shared-kernel/write-side/presentation/rest-api/nestjs-exception-filter/internal-command-invalid-schema.exception-filter'; function setupSwagger(app: INestApplication) { const options = new DocumentBuilder() @@ -21,6 +22,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); setupValidationPipe(app); setupSwagger(app); + app.useGlobalFilters(new InternalCommandInvalidSchemaExceptionFilter()); await app.listen(process.env.CODERSBOARD_SERVER_PORT || 4000); } diff --git a/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-invalid-schema.exception.ts b/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-invalid-schema.exception.ts new file mode 100644 index 0000000..6a0cf91 --- /dev/null +++ b/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-invalid-schema.exception.ts @@ -0,0 +1,15 @@ +import { InternalCommand } from './internal-command'; +import { Exception } from '@coders-board-library/typescript-sdk/exception'; +import { ValidationError } from 'class-validator'; + +export class InternalCommandInvalidSchemaException extends Exception { + constructor(command: InternalCommand, readonly validationErrors: ValidationError[]) { + super( + `Internal command ${ + command.constructor.name + } rejected! Schema doesn't match. \n Validation errors: ${validationErrors.map( + (it, index) => `\n${index + 1}. ${it}`, + )}`, + ); + } +} diff --git a/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-rejected.exception.ts b/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-rejected.exception.ts new file mode 100644 index 0000000..74bbeb9 --- /dev/null +++ b/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-rejected.exception.ts @@ -0,0 +1,8 @@ +import { InternalCommand } from './internal-command'; +import { Exception } from '@coders-board-library/typescript-sdk/exception'; + +export class InternalCommandRejectedException extends Exception { + constructor(command: InternalCommand, causation: Error | undefined) { + super(`Internal command ${command.constructor.name} rejected!`, causation); + } +} diff --git a/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-sender.ts b/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-sender.ts index ed25053..8a6cf1d 100644 --- a/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-sender.ts +++ b/backend/src/shared-kernel/write-side/application/internal-command-sender/internal-command-sender.ts @@ -6,5 +6,5 @@ export interface InternalCommandSender { sendAndWait(command: InternalCommand): Promise; //TODO: Add inbox and ack - sendAndForget(command: InternalCommand): void; + sendAndForget(command: InternalCommand): Promise; } diff --git a/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/class-validatior-command-sender.ts b/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/class-validatior-command-sender.ts index cec82ea..d995a77 100644 --- a/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/class-validatior-command-sender.ts +++ b/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/class-validatior-command-sender.ts @@ -1,15 +1,25 @@ import { InternalCommandSender } from '../../application/internal-command-sender/internal-command-sender'; import { InternalCommand } from '../../application/internal-command-sender/internal-command'; import { validateOrReject } from 'class-validator'; +import { InternalCommandRejectedException } from '../../application/internal-command-sender/internal-command-rejected.exception'; +import { InternalCommandInvalidSchemaException } from '../../application/internal-command-sender/internal-command-invalid-schema.exception'; export class ClassValidatorInternalCommandSender implements InternalCommandSender { constructor(private readonly commandSender: InternalCommandSender) {} sendAndWait(command: InternalCommand): Promise { - return validateOrReject(command).then(() => this.commandSender.sendAndWait(command)); + return validateOrReject(command) + .catch(validationError => { + throw new InternalCommandInvalidSchemaException(command, validationError); + }) + .then(() => this.commandSender.sendAndWait(command)); } - sendAndForget(command: T) { - return validateOrReject(command).then(() => this.commandSender.sendAndForget(command)); + sendAndForget(command: T): Promise { + return validateOrReject(command) + .catch(validationError => { + throw new InternalCommandInvalidSchemaException(command, validationError); + }) + .then(() => this.commandSender.sendAndForget(command)); } } diff --git a/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/nest-js-internal-command-sender.ts b/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/nest-js-internal-command-sender.ts index f8b9a2a..529d56e 100644 --- a/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/nest-js-internal-command-sender.ts +++ b/backend/src/shared-kernel/write-side/infrastructure/internal-command-sender/nest-js-internal-command-sender.ts @@ -9,7 +9,8 @@ export class NestJsInternalCommandSender implements InternalCommandSender { return this.commandBus.execute(command); } - sendAndForget(command: T) { + sendAndForget(command: T): Promise { this.commandBus.execute(command).then(); + return Promise.resolve(); } } diff --git a/backend/src/shared-kernel/write-side/presentation/rest-api/nestjs-exception-filter/internal-command-invalid-schema.exception-filter.ts b/backend/src/shared-kernel/write-side/presentation/rest-api/nestjs-exception-filter/internal-command-invalid-schema.exception-filter.ts new file mode 100644 index 0000000..817e064 --- /dev/null +++ b/backend/src/shared-kernel/write-side/presentation/rest-api/nestjs-exception-filter/internal-command-invalid-schema.exception-filter.ts @@ -0,0 +1,18 @@ +import { ExceptionFilter, Catch, ArgumentsHost, HttpStatus } from '@nestjs/common'; +import { InternalCommandInvalidSchemaException } from '../../../application/internal-command-sender/internal-command-invalid-schema.exception'; + +@Catch(InternalCommandInvalidSchemaException) +export class InternalCommandInvalidSchemaExceptionFilter implements ExceptionFilter { + catch(exception: InternalCommandInvalidSchemaException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); + + response.status(HttpStatus.BAD_REQUEST).json({ + statusCode: HttpStatus.BAD_REQUEST, + timestamp: new Date().toISOString(), + path: request.url, + message: exception.validationErrors, //TODO: Flatten from NestJS validation pipe! + }); + } +} diff --git a/backend/test-integration/jest-integration.json b/backend/test-integration/jest-integration.json index d4b6f89..0bcbd04 100755 --- a/backend/test-integration/jest-integration.json +++ b/backend/test-integration/jest-integration.json @@ -28,6 +28,8 @@ "@coders-board-library/axios-utils/(.*)": "/../libs/axios-utils/src/$1", "@coders-board-library/axios-utils": "/../libs/axios-utils/src", "@coders-board-library/eventstore-projections/(.*)": "/../libs/eventstore-projections/src/$1", - "@coders-board-library/eventstore-projections": "/../libs/eventstore-projections/src" + "@coders-board-library/eventstore-projections": "/../libs/eventstore-projections/src", + "@coders-board-library/typescript-sdk/(.*)": "/../libs/typescript-sdk/src/$1", + "@coders-board-library/typescript-sdk": "/../libs/typescript-sdk/src" } } \ No newline at end of file diff --git a/backend/test-module/jest-module.json b/backend/test-module/jest-module.json index 881706b..2cf7446 100755 --- a/backend/test-module/jest-module.json +++ b/backend/test-module/jest-module.json @@ -28,6 +28,8 @@ "@coders-board-library/axios-utils/(.*)": "/../libs/axios-utils/src/$1", "@coders-board-library/axios-utils": "/../libs/axios-utils/src", "@coders-board-library/eventstore-projections/(.*)": "/../libs/eventstore-projections/src/$1", - "@coders-board-library/eventstore-projections": "/../libs/eventstore-projections/src" + "@coders-board-library/eventstore-projections": "/../libs/eventstore-projections/src", + "@coders-board-library/typescript-sdk/(.*)": "/../libs/typescript-sdk/src/$1", + "@coders-board-library/typescript-sdk": "/../libs/typescript-sdk/src" } } \ No newline at end of file diff --git a/backend/test-unit/jest-unit.json b/backend/test-unit/jest-unit.json index 31493dd..f65cff0 100755 --- a/backend/test-unit/jest-unit.json +++ b/backend/test-unit/jest-unit.json @@ -28,6 +28,8 @@ "@coders-board-library/axios-utils/(.*)": "/../libs/axios-utils/src/$1", "@coders-board-library/axios-utils": "/../libs/axios-utils/src", "@coders-board-library/eventstore-projections/(.*)": "/../libs/eventstore-projections/src/$1", - "@coders-board-library/eventstore-projections": "/../libs/eventstore-projections/src" + "@coders-board-library/eventstore-projections": "/../libs/eventstore-projections/src", + "@coders-board-library/typescript-sdk/(.*)": "/../libs/typescript-sdk/src/$1", + "@coders-board-library/typescript-sdk": "/../libs/typescript-sdk/src" } } \ No newline at end of file diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 5d4f92e..2fe8a57 100755 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -46,6 +46,12 @@ ], "@coders-board-library/eventstore-projections/*": [ "libs/eventstore-projections/src/*" + ], + "@coders-board-library/typescript-sdk": [ + "libs/typescript-sdk/src" + ], + "@coders-board-library/typescript-sdk/*": [ + "libs/typescript-sdk/src/*" ] } },