From ecff7876641b60b9f366f4cf12ed8d7e3d12cf89 Mon Sep 17 00:00:00 2001 From: luekromanowicz Date: Wed, 4 Oct 2023 09:06:20 +0200 Subject: [PATCH 1/2] submission verification endpoint --- .../src/contracts/contracts.controller.ts | 47 +++++++++++++++---- .../src/contracts/contracts.service.ts | 34 +++++++++++--- .../contract-submission-status-request.dto.ts | 11 +++++ .../dto/contract-submission-status.dto.ts | 15 ++++++ .../entities/contract-submission.entity.ts | 19 ++++++++ .../submission-not-found.exception.ts | 14 ++++++ .../validators/contract-id.validator.ts | 1 - 7 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 apps/scv-gateway/src/contracts/dto/contract-submission-status-request.dto.ts create mode 100644 apps/scv-gateway/src/contracts/dto/contract-submission-status.dto.ts create mode 100644 apps/scv-gateway/src/contracts/exceptions/submission-not-found.exception.ts diff --git a/apps/scv-gateway/src/contracts/contracts.controller.ts b/apps/scv-gateway/src/contracts/contracts.controller.ts index 7593504..44a3396 100644 --- a/apps/scv-gateway/src/contracts/contracts.controller.ts +++ b/apps/scv-gateway/src/contracts/contracts.controller.ts @@ -8,13 +8,18 @@ import { HttpException, HttpStatus, Param, + Res, } from '@nestjs/common'; +import { Response } from 'express'; import { FilesInterceptor } from '@nestjs/platform-express'; import { ContractsService } from './contracts.service'; import { ContractSubmissionDto } from './dto/contract-submission.dto'; import { ApiBody, ApiConsumes, ApiParam, ApiResponse } from '@nestjs/swagger'; import { ContractFilesValidator } from './validators/contract-files.validator'; import { ContractIdDto } from './dto/contract-id.dto'; +import { ContractSubmissionStatusDto } from './dto/contract-submission-status.dto'; +import { VerificationStatus } from './entities/contract-submission.entity'; +import { ContractSubmissionStatusRequestDto } from './dto/contract-submission-status-request.dto'; @Controller('contracts') export class ContractsController { @@ -81,13 +86,39 @@ export class ContractsController { ); } - @Get() - findAll() { - return this.contractsService.findAll(); - } + @Get(':id/check/:submissionId') + @ApiParam({ name: 'id', type: String }) + @ApiParam({ name: 'submissionId', type: String }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Contract is still pending or verified successfully', + type: ContractSubmissionStatusDto, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Contract or submission matching that contract not found', + }) + @ApiResponse({ + status: HttpStatus.UNPROCESSABLE_ENTITY, + description: 'Contract verification failed', + type: ContractSubmissionStatusDto, + }) + async checkSubmissionStatus( + @Param() params: ContractSubmissionStatusRequestDto, + @Res() res: Response, + ) { + const verificationStatus = + await this.contractsService.checkSubmissionStatus( + params.id, + params.submissionId, + ); - // @Get(':id') - // findOne(@Param('id') id: string) { - // return this.contractsService.findOne(+id); - // } + if (verificationStatus.status === VerificationStatus.FAIL) { + res.status(HttpStatus.UNPROCESSABLE_ENTITY); + } else { + res.status(HttpStatus.OK); + } + + res.json(verificationStatus); + } } diff --git a/apps/scv-gateway/src/contracts/contracts.service.ts b/apps/scv-gateway/src/contracts/contracts.service.ts index 17c2755..687f5ad 100644 --- a/apps/scv-gateway/src/contracts/contracts.service.ts +++ b/apps/scv-gateway/src/contracts/contracts.service.ts @@ -2,9 +2,14 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; import { ContractSubmissionDto } from './dto/contract-submission.dto'; -import { ContractSubmission } from './entities/contract-submission.entity'; +import { + ContractSubmission, + VerificationStatus, +} from './entities/contract-submission.entity'; import { ContractSubmissionSourceFile } from './entities/contract-submission-source-file.entity'; import { ContractSubmissionResponseDto } from './dto/contract-submission-response.dto'; +import { SubmissionNotFoundException } from './exceptions/submission-not-found.exception'; +import { ContractSubmissionStatusDto } from './dto/contract-submission-status.dto'; @Injectable() export class ContractsService { @@ -40,11 +45,28 @@ export class ContractsService { return contractSubmissionResponseDto; } - findAll() { - return this.contractSubmissionsRepository.find(); - } + async checkSubmissionStatus( + contractId: string, + submissionId: string, + ): Promise { + const submission = await this.contractSubmissionsRepository.findOne({ + where: { + id: submissionId, + contractId, + }, + }); + + if (!submission) { + throw new SubmissionNotFoundException(submissionId, contractId); + } + + const contractSubmissionStatusDto = new ContractSubmissionStatusDto(); + contractSubmissionStatusDto.status = submission.status; + + if (submission.status === VerificationStatus.FAIL) { + contractSubmissionStatusDto.message = submission.result; + } - findOne(id: string) { - return `This action returns a #${id} contract`; + return contractSubmissionStatusDto; } } diff --git a/apps/scv-gateway/src/contracts/dto/contract-submission-status-request.dto.ts b/apps/scv-gateway/src/contracts/dto/contract-submission-status-request.dto.ts new file mode 100644 index 0000000..cae2e5b --- /dev/null +++ b/apps/scv-gateway/src/contracts/dto/contract-submission-status-request.dto.ts @@ -0,0 +1,11 @@ +import { IsNotEmpty, IsUUID, Validate } from 'class-validator'; +import { ContractIdValidator } from '../validators/contract-id.validator'; + +export class ContractSubmissionStatusRequestDto { + @IsNotEmpty() + @Validate(ContractIdValidator) + id: string; + + @IsUUID() + submissionId: string; +} diff --git a/apps/scv-gateway/src/contracts/dto/contract-submission-status.dto.ts b/apps/scv-gateway/src/contracts/dto/contract-submission-status.dto.ts new file mode 100644 index 0000000..44f59d7 --- /dev/null +++ b/apps/scv-gateway/src/contracts/dto/contract-submission-status.dto.ts @@ -0,0 +1,15 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { VerificationStatus } from '../entities/contract-submission.entity'; + +export class ContractSubmissionStatusDto { + @ApiProperty({ + enum: VerificationStatus, + }) + status: VerificationStatus; + + @ApiProperty({ + required: false, + description: 'Error message if status is FAIL', + }) + message?: string; +} diff --git a/apps/scv-gateway/src/contracts/entities/contract-submission.entity.ts b/apps/scv-gateway/src/contracts/entities/contract-submission.entity.ts index 1e5f250..67f7e5b 100644 --- a/apps/scv-gateway/src/contracts/entities/contract-submission.entity.ts +++ b/apps/scv-gateway/src/contracts/entities/contract-submission.entity.ts @@ -1,6 +1,12 @@ import { Entity, Column, PrimaryGeneratedColumn, OneToMany } from 'typeorm'; import { ContractSubmissionSourceFile } from './contract-submission-source-file.entity'; +export enum VerificationStatus { + PENDING = 'pending', + SUCCESS = 'success', + FAIL = 'fail', +} + @Entity('contract_submissions') export class ContractSubmission { @PrimaryGeneratedColumn('uuid') @@ -18,6 +24,19 @@ export class ContractSubmission { @Column() entryFile: string; + @Column({ type: 'timestamp', default: () => 'CURRENT_TIMESTAMP' }) + submittedAt: Date; + + @Column({ + type: 'enum', + enum: VerificationStatus, + default: VerificationStatus.PENDING, + }) + status: VerificationStatus; + + @Column({ nullable: true }) + result: string; + @OneToMany(() => ContractSubmissionSourceFile, (file) => file.submission, { cascade: true, }) diff --git a/apps/scv-gateway/src/contracts/exceptions/submission-not-found.exception.ts b/apps/scv-gateway/src/contracts/exceptions/submission-not-found.exception.ts new file mode 100644 index 0000000..197f19a --- /dev/null +++ b/apps/scv-gateway/src/contracts/exceptions/submission-not-found.exception.ts @@ -0,0 +1,14 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; + +export class SubmissionNotFoundException extends HttpException { + constructor(submissionId: string, contractId: string | undefined) { + if (!contractId) { + super(`Submission ${submissionId} was not found`, HttpStatus.NOT_FOUND); + return; + } + super( + `Submission ${submissionId} was not found for contract ${contractId}`, + HttpStatus.NOT_FOUND, + ); + } +} diff --git a/apps/scv-gateway/src/contracts/validators/contract-id.validator.ts b/apps/scv-gateway/src/contracts/validators/contract-id.validator.ts index 173b13e..df04e91 100644 --- a/apps/scv-gateway/src/contracts/validators/contract-id.validator.ts +++ b/apps/scv-gateway/src/contracts/validators/contract-id.validator.ts @@ -8,7 +8,6 @@ import { isAddressValid, Encoding } from '@aeternity/aepp-sdk'; @ValidatorConstraint({ name: 'contractId', async: false }) export class ContractIdValidator implements ValidatorConstraintInterface { validate(text: string) { - console.log('validating', text); return isAddressValid(text, Encoding.ContractAddress); } From fac4fa3fb6898ac82c77b650587dbaca14ad82f2 Mon Sep 17 00:00:00 2001 From: luekromanowicz Date: Wed, 4 Oct 2023 10:43:49 +0200 Subject: [PATCH 2/2] add migration --- .../1696408787963-SubmissionVerification.ts | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 apps/scv-gateway/src/database/migrations/1696408787963-SubmissionVerification.ts diff --git a/apps/scv-gateway/src/database/migrations/1696408787963-SubmissionVerification.ts b/apps/scv-gateway/src/database/migrations/1696408787963-SubmissionVerification.ts new file mode 100644 index 0000000..19f98ab --- /dev/null +++ b/apps/scv-gateway/src/database/migrations/1696408787963-SubmissionVerification.ts @@ -0,0 +1,47 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class SubmissionVerification1696408787963 implements MigrationInterface { + name = 'SubmissionVerification1696408787963'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contract_submissions" ADD "submitted_at" TIMESTAMP NOT NULL DEFAULT now()`, + ); + await queryRunner.query( + `CREATE TYPE "public"."contract_submissions_status_enum" AS ENUM('pending', 'success', 'fail')`, + ); + await queryRunner.query( + `ALTER TABLE "contract_submissions" ADD "status" "public"."contract_submissions_status_enum" NOT NULL DEFAULT 'pending'`, + ); + await queryRunner.query( + `ALTER TABLE "contract_submissions" ADD "result" character varying`, + ); + await queryRunner.query( + `ALTER TABLE "contract_source_files" DROP COLUMN "file_path"`, + ); + await queryRunner.query( + `ALTER TABLE "contract_source_files" ADD "file_path" character varying NOT NULL`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "contract_source_files" DROP COLUMN "file_path"`, + ); + await queryRunner.query( + `ALTER TABLE "contract_source_files" ADD "file_path" character varying(127) NOT NULL`, + ); + await queryRunner.query( + `ALTER TABLE "contract_submissions" DROP COLUMN "result"`, + ); + await queryRunner.query( + `ALTER TABLE "contract_submissions" DROP COLUMN "status"`, + ); + await queryRunner.query( + `DROP TYPE "public"."contract_submissions_status_enum"`, + ); + await queryRunner.query( + `ALTER TABLE "contract_submissions" DROP COLUMN "submitted_at"`, + ); + } +}