From 8ab2a1c94f0e7a45ff15a955f31ea4bbcce81b24 Mon Sep 17 00:00:00 2001 From: Konstantin Tsabolov <konstantin.tsabolov@spherity.com> Date: Tue, 23 Jan 2024 16:07:10 +0100 Subject: [PATCH] feat: implement proof-manager base CRUD --- apps/proof-manager/jest.config.js | 5 +- apps/proof-manager/package.json | 4 +- apps/proof-manager/src/application.ts | 30 +- apps/proof-manager/src/config/http.config.ts | 4 +- apps/proof-manager/src/config/nats.config.ts | 6 +- apps/proof-manager/src/config/ssi.config.ts | 2 +- apps/proof-manager/src/config/validation.ts | 12 +- apps/proof-manager/src/main.ts | 31 +- .../__tests__/proofs.controller.spec.ts | 158 ++++++ .../proofs/__tests__/proofs.module.spec.ts | 35 ++ .../proofs/__tests__/proofs.service.spec.ts | 170 ++++++ .../src/proofs/dto/get-by-id.dto.ts | 7 + .../src/proofs/dto/register.dto.ts | 115 ++++ .../src/proofs/proofs.controller.ts | 492 ++++++++++++++++++ .../proof-manager/src/proofs/proofs.module.ts | 10 + .../src/proofs/proofs.service.ts | 84 +++ 16 files changed, 1135 insertions(+), 30 deletions(-) create mode 100644 apps/proof-manager/src/proofs/__tests__/proofs.controller.spec.ts create mode 100644 apps/proof-manager/src/proofs/__tests__/proofs.module.spec.ts create mode 100644 apps/proof-manager/src/proofs/__tests__/proofs.service.spec.ts create mode 100644 apps/proof-manager/src/proofs/dto/get-by-id.dto.ts create mode 100644 apps/proof-manager/src/proofs/dto/register.dto.ts create mode 100644 apps/proof-manager/src/proofs/proofs.controller.ts create mode 100644 apps/proof-manager/src/proofs/proofs.module.ts create mode 100644 apps/proof-manager/src/proofs/proofs.service.ts diff --git a/apps/proof-manager/jest.config.js b/apps/proof-manager/jest.config.js index c4d3f93..ccdd468 100644 --- a/apps/proof-manager/jest.config.js +++ b/apps/proof-manager/jest.config.js @@ -7,7 +7,7 @@ export default { moduleFileExtensions: ['js', 'ts'], testEnvironment: 'node', transform: { - '^.+\\.(ts|js)$': [ + '^.+\\.(js|ts)$': [ '@swc/jest', { ...swcConfig, @@ -31,10 +31,9 @@ export default { : ['text-summary', 'html'], coveragePathIgnorePatterns: [ '<rootDir>/node_modules/', - '<rootDir>/test/', '<rootDir>/coverage/', '<rootDir>/dist/', - '<rootDir>/**/test', + '__tests__', '@types', '.dto.(t|j)s', '.enum.ts', diff --git a/apps/proof-manager/package.json b/apps/proof-manager/package.json index 819f35c..c78bda6 100644 --- a/apps/proof-manager/package.json +++ b/apps/proof-manager/package.json @@ -16,8 +16,7 @@ "prisma:generate": "prisma generate --schema=./src/prisma/schema.prisma", "prisma:migrate": "prisma migrate deploy --schema=./src/prisma/schema.prisma", "prisma:studio": "prisma studio", - "start": "nest start", - "start:dev": "nest start --watch --preserveWatchOutput", + "start": "nest start --watch --preserveWatchOutput", "test": "jest", "test:watch": "jest --watch", "test:cov": "jest --coverage", @@ -34,6 +33,7 @@ "@nestjs/swagger": "^7.1.16", "@ocm/shared": "workspace:*", "class-validator": "^0.14.0", + "class-transformer": "^0.5.1", "express": "^4.17.3", "joi": "^17.11.0", "nats": "^2.18.0", diff --git a/apps/proof-manager/src/application.ts b/apps/proof-manager/src/application.ts index a7e650c..0c71d7c 100644 --- a/apps/proof-manager/src/application.ts +++ b/apps/proof-manager/src/application.ts @@ -1,4 +1,5 @@ import type { ConfigType } from '@nestjs/config'; +import type { ClientProvider } from '@nestjs/microservices'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; @@ -11,6 +12,7 @@ import { httpConfig } from './config/http.config.js'; import { natsConfig } from './config/nats.config.js'; import { ssiConfig } from './config/ssi.config.js'; import { validationSchema } from './config/validation.js'; +import { ProofsModule } from './proofs/proofs.module.js'; @Module({ imports: [ @@ -32,12 +34,21 @@ import { validationSchema } from './config/validation.js'; { name: NATS_CLIENT, inject: [natsConfig.KEY], - useFactory: (config: ConfigType<typeof natsConfig>) => ({ - transport: Transport.NATS, - options: { - url: config.url as string, - }, - }), + useFactory: (config: ConfigType<typeof natsConfig>) => { + const provider: Required<ClientProvider> = { + transport: Transport.NATS, + options: { + servers: config.url as string, + }, + }; + + if ('user' in config && 'password' in config) { + provider.options.user = config.user as string; + provider.options.pass = config.password as string; + } + + return provider; + }, }, ], }), @@ -57,7 +68,12 @@ import { validationSchema } from './config/validation.js'; }, }), - RouterModule.register([{ module: HealthModule, path: '/health' }]), + ProofsModule, + + RouterModule.register([ + { module: HealthModule, path: '/health' }, + { module: ProofsModule, path: '/proofs' }, + ]), ], }) export class Application {} diff --git a/apps/proof-manager/src/config/http.config.ts b/apps/proof-manager/src/config/http.config.ts index 0bb4d4c..fc63fd9 100644 --- a/apps/proof-manager/src/config/http.config.ts +++ b/apps/proof-manager/src/config/http.config.ts @@ -1,6 +1,6 @@ import { registerAs } from '@nestjs/config'; export const httpConfig = registerAs('http', () => ({ - host: process.env.HOST, - port: Number(process.env.PORT), + host: process.env.HOST || '0.0.0.0', + port: Number(process.env.PORT) || 3000, })); diff --git a/apps/proof-manager/src/config/nats.config.ts b/apps/proof-manager/src/config/nats.config.ts index 023e923..194053c 100644 --- a/apps/proof-manager/src/config/nats.config.ts +++ b/apps/proof-manager/src/config/nats.config.ts @@ -1,6 +1,8 @@ import { registerAs } from '@nestjs/config'; export const natsConfig = registerAs('nats', () => ({ - url: process.env.NATS_URL, - monitoringUrl: process.env.NATS_MONITORING_URL, + url: process.env.NATS_URL || 'nats://localhost:4222', + user: process.env.NATS_USER, + password: process.env.NATS_PASSWORD, + monitoringUrl: process.env.NATS_MONITORING_URL || 'http://localhost:8222', })); diff --git a/apps/proof-manager/src/config/ssi.config.ts b/apps/proof-manager/src/config/ssi.config.ts index 1779919..408bf27 100644 --- a/apps/proof-manager/src/config/ssi.config.ts +++ b/apps/proof-manager/src/config/ssi.config.ts @@ -1,5 +1,5 @@ import { registerAs } from '@nestjs/config'; export const ssiConfig = registerAs('ssi', () => ({ - agentUrl: process.env.SSI_AGENT_URL, + agentUrl: process.env.SSI_AGENT_URL || 'http://localhost:3010', })); diff --git a/apps/proof-manager/src/config/validation.ts b/apps/proof-manager/src/config/validation.ts index a7fe2f8..fef0d7c 100644 --- a/apps/proof-manager/src/config/validation.ts +++ b/apps/proof-manager/src/config/validation.ts @@ -1,11 +1,13 @@ import Joi from 'joi'; export const validationSchema = Joi.object({ - HTTP_HOST: Joi.string().default('0.0.0.0'), - HTTP_PORT: Joi.number().default(3000), + HTTP_HOST: Joi.string(), + HTTP_PORT: Joi.number(), - NATS_URL: Joi.string().uri().default('nats://localhost:4222'), - NATS_MONITORING_URL: Joi.string().uri().default('http://localhost:8222'), + NATS_URL: Joi.string().uri(), + NATS_USER: Joi.string().optional(), + NATS_PASSWORD: Joi.string().optional(), + NATS_MONITORING_URL: Joi.string().uri(), - SSI_AGENT_URL: Joi.string().default('http://localhost:3010'), + SSI_AGENT_URL: Joi.string().uri(), }); diff --git a/apps/proof-manager/src/main.ts b/apps/proof-manager/src/main.ts index 84f089d..fe4aad7 100644 --- a/apps/proof-manager/src/main.ts +++ b/apps/proof-manager/src/main.ts @@ -1,24 +1,36 @@ /* c8 ignore start */ -import type { MicroserviceOptions } from '@nestjs/microservices'; +import type { ConfigType } from '@nestjs/config'; +import type { MicroserviceOptions, NatsOptions } from '@nestjs/microservices'; -import { VersioningType } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; +import { Logger, VersioningType } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { Transport } from '@nestjs/microservices'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { Application } from './application.js'; +import { httpConfig } from './config/http.config.js'; +import { natsConfig } from './config/nats.config.js'; const app = await NestFactory.create(Application); -const configService = app.get(ConfigService); app.enableCors(); -app.connectMicroservice<MicroserviceOptions>({ +const { url, user, password } = app.get(natsConfig.KEY) as ConfigType< + typeof natsConfig +>; + +const microserviceOptions: Required<NatsOptions> = { transport: Transport.NATS, options: { - servers: [configService.get('nats').url], + servers: [url], }, -}); +}; + +if (user && password) { + microserviceOptions.options.user = user; + microserviceOptions.options.pass = password; +} + +app.connectMicroservice<MicroserviceOptions>(microserviceOptions); app.enableVersioning({ defaultVersion: ['1'], @@ -36,5 +48,8 @@ const document = SwaggerModule.createDocument(app, swaggerConfig); SwaggerModule.setup('/swagger', app, document); await app.startAllMicroservices(); -await app.listen(configService.get('PORT') || 3000); +const { host, port } = app.get(httpConfig.KEY) as ConfigType<typeof httpConfig>; +await app.listen(port as number, host as string); + +Logger.log(`Application is running on: ${await app.getUrl()}`); /* c8 ignore stop */ diff --git a/apps/proof-manager/src/proofs/__tests__/proofs.controller.spec.ts b/apps/proof-manager/src/proofs/__tests__/proofs.controller.spec.ts new file mode 100644 index 0000000..93b991f --- /dev/null +++ b/apps/proof-manager/src/proofs/__tests__/proofs.controller.spec.ts @@ -0,0 +1,158 @@ +import type { TestingModule } from '@nestjs/testing'; +import type { + EventAnonCredsProofsDeleteById, + EventAnonCredsProofsGetAll, + EventAnonCredsProofsGetById, + EventDidcommAnonCredsProofsRequest, +} from '@ocm/shared'; + +import { Test } from '@nestjs/testing'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { ProofsController } from '../proofs.controller.js'; +import { ProofsService } from '../proofs.service.js'; + +describe('ProofsController', () => { + const natsClientMock = {}; + + let controller: ProofsController; + let service: ProofsService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [ProofsController], + providers: [ + { provide: NATS_CLIENT, useValue: natsClientMock }, + ProofsService, + ], + }).compile(); + + controller = module.get<ProofsController>(ProofsController); + service = module.get<ProofsService>(ProofsService); + }); + + describe('find', () => { + it('should return a list of schemas', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const expectedResult: EventAnonCredsProofsGetAll['data'] = []; + + jest.spyOn(service, 'find').mockReturnValue(of(expectedResult)); + + controller + .find({ tenantId }) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('getById', () => { + it('should return a schema by id', (done) => { + const unsubscribe$ = new Subject<void>(); + const proofRecordId = 'exampleProofRecordId'; + const tenantId = 'exampleTenantId'; + const expectedResult = {} as NonNullable< + EventAnonCredsProofsGetById['data'] + >; + + jest.spyOn(service, 'getById').mockReturnValue(of(expectedResult)); + + controller + .get({ proofRecordId }, { tenantId }) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + + it('should throw a NotFoundException if the service returned null', (done) => { + const unsubscribe$ = new Subject<void>(); + const proofRecordId = 'exampleProofRecordId'; + const tenantId = 'exampleTenantId'; + + jest.spyOn(service, 'getById').mockReturnValue(of(null)); + + controller + .get({ proofRecordId }, { tenantId }) + .pipe(takeUntil(unsubscribe$)) + .subscribe({ + error: (error) => { + expect(error.status).toBe(404); + expect(error.message).toBe( + `Presentation proof with id ${proofRecordId} not found`, + ); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }, + }); + }); + }); + + describe('request', () => { + it('should return a proof record', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const name = 'exampleName'; + const connectionId = 'exampleConnectionId'; + const requestedAttributes = {}; + const requestedPredicates = {}; + const expectedResult = {} as EventDidcommAnonCredsProofsRequest['data']; + + jest.spyOn(service, 'request').mockReturnValue(of(expectedResult)); + + controller + .request( + { tenantId }, + { name, connectionId, requestedAttributes, requestedPredicates }, + ) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('delete', () => { + it('should return a proof record', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'exampleTenantId'; + const proofRecordId = 'exampleProofRecordId'; + const expectedResult = {} as EventAnonCredsProofsDeleteById['data']; + + jest.spyOn(service, 'delete').mockReturnValue(of(expectedResult)); + + controller + .delete({ proofRecordId }, { tenantId }) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); +}); diff --git a/apps/proof-manager/src/proofs/__tests__/proofs.module.spec.ts b/apps/proof-manager/src/proofs/__tests__/proofs.module.spec.ts new file mode 100644 index 0000000..b6a2059 --- /dev/null +++ b/apps/proof-manager/src/proofs/__tests__/proofs.module.spec.ts @@ -0,0 +1,35 @@ +import { ClientsModule } from '@nestjs/microservices'; +import { Test } from '@nestjs/testing'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { ProofsController } from '../proofs.controller.js'; +import { ProofsModule } from '../proofs.module.js'; +import { ProofsService } from '../proofs.service.js'; + +describe('Proofs Module', () => { + let proofsController: ProofsController; + let proofsService: ProofsService; + + beforeEach(async () => { + const moduleRef = await Test.createTestingModule({ + imports: [ + ClientsModule.registerAsync({ + isGlobal: true, + clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }], + }), + ProofsModule, + ], + }).compile(); + + proofsController = moduleRef.get<ProofsController>(ProofsController); + proofsService = moduleRef.get<ProofsService>(ProofsService); + }); + + it('should be defined', () => { + expect(proofsController).toBeDefined(); + expect(proofsController).toBeInstanceOf(ProofsController); + + expect(proofsService).toBeDefined(); + expect(proofsService).toBeInstanceOf(ProofsService); + }); +}); diff --git a/apps/proof-manager/src/proofs/__tests__/proofs.service.spec.ts b/apps/proof-manager/src/proofs/__tests__/proofs.service.spec.ts new file mode 100644 index 0000000..d12e7c0 --- /dev/null +++ b/apps/proof-manager/src/proofs/__tests__/proofs.service.spec.ts @@ -0,0 +1,170 @@ +import type { TestingModule } from '@nestjs/testing'; + +import { Test } from '@nestjs/testing'; +import { + EventAnonCredsProofsDeleteById, + EventAnonCredsProofsGetAll, + EventAnonCredsProofsGetById, + EventDidcommAnonCredsProofsRequest, +} from '@ocm/shared'; +import { Subject, of, takeUntil } from 'rxjs'; + +import { NATS_CLIENT } from '../../common/constants.js'; +import { ProofsService } from '../proofs.service.js'; + +describe('ProofsService', () => { + let service: ProofsService; + const natsClientMock = { send: jest.fn() }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { provide: NATS_CLIENT, useValue: natsClientMock }, + ProofsService, + ], + }).compile(); + + service = module.get<ProofsService>(ProofsService); + + jest.resetAllMocks(); + }); + + describe('getAll', () => { + it('should return the data from NATS client', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'mocked tenantId'; + const expectedResult: EventAnonCredsProofsGetAll['data'] = []; + + natsClientMock.send.mockReturnValueOnce( + of(new EventAnonCredsProofsGetAll([], tenantId)), + ); + + service + .find(tenantId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsProofsGetAll.token, + { tenantId }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('getById', () => { + it('should return the data from NATS client', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'mocked tenantId'; + const proofRecordId = 'mocked id'; + const expectedResult = {} as EventAnonCredsProofsGetById['data']; + + natsClientMock.send.mockReturnValueOnce( + of(new EventAnonCredsProofsGetById(expectedResult, tenantId)), + ); + + service + .getById(tenantId, proofRecordId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsProofsGetById.token, + { tenantId, proofRecordId }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('request', () => { + it('should return the data from NATS client', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'mocked tenantId'; + const name = 'mocked name'; + const connectionId = 'mocked connectionId'; + const requestedAttributes = {}; + const requestedPredicates = {}; + const expectedResult = { + name, + connectionId, + requestedAttributes, + requestedPredicates, + } as unknown as EventDidcommAnonCredsProofsRequest['data']; + + natsClientMock.send.mockReturnValueOnce( + of(new EventDidcommAnonCredsProofsRequest(expectedResult, tenantId)), + ); + + service + .request( + tenantId, + name, + connectionId, + requestedAttributes, + requestedPredicates, + ) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventDidcommAnonCredsProofsRequest.token, + { + name, + connectionId, + requestedAttributes, + requestedPredicates, + tenantId, + }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); + + describe('delete', () => { + it('should return the data from NATS client', (done) => { + const unsubscribe$ = new Subject<void>(); + const tenantId = 'mocked tenantId'; + const proofRecordId = 'mocked id'; + const expectedResult = {} as EventAnonCredsProofsGetById['data']; + + natsClientMock.send.mockReturnValueOnce( + of(new EventAnonCredsProofsGetById(expectedResult, tenantId)), + ); + + service + .delete(tenantId, proofRecordId) + .pipe(takeUntil(unsubscribe$)) + .subscribe((result) => { + expect(natsClientMock.send).toHaveBeenCalledWith( + EventAnonCredsProofsDeleteById.token, + { tenantId, proofRecordId }, + ); + + expect(result).toStrictEqual(expectedResult); + + unsubscribe$.next(); + unsubscribe$.complete(); + + done(); + }); + }); + }); +}); diff --git a/apps/proof-manager/src/proofs/dto/get-by-id.dto.ts b/apps/proof-manager/src/proofs/dto/get-by-id.dto.ts new file mode 100644 index 0000000..43d5b40 --- /dev/null +++ b/apps/proof-manager/src/proofs/dto/get-by-id.dto.ts @@ -0,0 +1,7 @@ +import { IsNotEmpty, IsString } from 'class-validator'; + +export class GetByIdParams { + @IsString() + @IsNotEmpty() + public readonly proofRecordId: string; +} diff --git a/apps/proof-manager/src/proofs/dto/register.dto.ts b/apps/proof-manager/src/proofs/dto/register.dto.ts new file mode 100644 index 0000000..3f9653c --- /dev/null +++ b/apps/proof-manager/src/proofs/dto/register.dto.ts @@ -0,0 +1,115 @@ +import { Type } from 'class-transformer'; +import { + IsArray, + IsEnum, + IsNotEmpty, + IsNumber, + IsObject, + IsOptional, + IsString, + ValidateNested, +} from 'class-validator'; + +export class RequestPayload { + @IsString() + @IsNotEmpty() + public readonly name: string; + + @IsString() + @IsNotEmpty() + public readonly connectionId: string; + + @IsObject() + @ValidateNested({ each: true }) + @Type(() => RequestedAttribute) + public readonly requestedAttributes: Record<string, RequestedAttribute>; + + @IsObject() + @ValidateNested({ each: true }) + @Type(() => RequestedPredicate) + public readonly requestedPredicates: Record<string, RequestedPredicate>; +} + +class RequestRestriction { + @IsString() + @IsNotEmpty() + @IsOptional() + public schema_id?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public schema_issuer_id?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public schema_name?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public schema_version?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public issuer_id?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public cred_def_id?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public rev_reg_id?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public schema_issuer_did?: string; + + @IsString() + @IsNotEmpty() + @IsOptional() + public issuer_did?: string; + + [key: `attr::${string}::marker`]: '1' | '0'; + [key: `attr::${string}::value`]: string; +} + +class RequestedAttribute { + @IsArray() + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + public names: string[]; + + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => RequestRestriction) + public restrictions?: RequestRestriction[]; +} + +const predicateType = ['>=', '>', '<=', '<'] as const; + +class RequestedPredicate { + @IsString() + @IsNotEmpty() + public name: string; + + @IsString() + @IsEnum(predicateType) + public predicateType: (typeof predicateType)[number]; + + @IsNumber() + public predicateValue: number; + + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => RequestRestriction) + public restrictions?: RequestRestriction[]; +} diff --git a/apps/proof-manager/src/proofs/proofs.controller.ts b/apps/proof-manager/src/proofs/proofs.controller.ts new file mode 100644 index 0000000..c7602b7 --- /dev/null +++ b/apps/proof-manager/src/proofs/proofs.controller.ts @@ -0,0 +1,492 @@ +import { + Body, + Controller, + Delete, + Get, + HttpCode, + HttpStatus, + NotFoundException, + Param, + Post, + Query, + UseInterceptors, + UsePipes, + ValidationPipe, +} from '@nestjs/common'; +import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { MultitenancyParams, ResponseFormatInterceptor } from '@ocm/shared'; +import { of, switchMap } from 'rxjs'; + +import { GetByIdParams } from './dto/get-by-id.dto.js'; +import { RequestPayload } from './dto/register.dto.js'; +import { ProofsService } from './proofs.service.js'; + +@Controller() +@UsePipes(new ValidationPipe({ transform: true, whitelist: true })) +@UseInterceptors(ResponseFormatInterceptor) +@ApiTags('Presentation Proofs') +export class ProofsController { + public constructor(private readonly service: ProofsService) {} + + @Get() + @ApiOperation({ + summary: 'Fetch a list of presentation proofs', + description: + 'This call provides a list of presentation proofs for a given tenant', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Presentation proofs fetched successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Presentation proofs fetched successfully': { + value: { + statusCode: 200, + message: 'Presentation proofs fetched successfully', + data: [], + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Tenant not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error', + content: { + 'application/json': { + schema: {}, + examples: { + 'Internal server error': { + value: { + statusCode: 500, + message: 'Internal server error', + data: null, + }, + }, + }, + }, + }, + }) + public find( + @Query() { tenantId }: MultitenancyParams, + ): ReturnType<ProofsService['find']> { + return this.service.find(tenantId); + } + + @Get(':id') + @ApiOperation({ + summary: 'Fetch a presentation proof by id', + description: + 'This call provides a presentation proof for a given tenant and id', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Presentation proof fetched successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Presentation proof fetched successfully': { + value: { + statusCode: 200, + message: 'Presentation proof fetched successfully', + data: {}, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Tenant not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid presentation proof id', + content: { + 'application/json': { + schema: {}, + examples: { + 'Invalid presentation proof id': { + value: { + statusCode: 400, + message: 'Invalid presentation proof id', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Presentation proof not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Presentation proof not found': { + value: { + statusCode: 404, + message: 'Presentation proof not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error', + content: { + 'application/json': { + schema: {}, + examples: { + 'Internal server error': { + value: { + statusCode: 500, + message: 'Internal server error', + data: null, + }, + }, + }, + }, + }, + }) + public get( + @Param() { proofRecordId }: GetByIdParams, + @Query() { tenantId }: MultitenancyParams, + ): ReturnType<ProofsService['getById']> { + return this.service.getById(tenantId, proofRecordId).pipe( + switchMap((proofRecord) => { + if (!proofRecord) { + throw new NotFoundException( + `Presentation proof with id ${proofRecordId} not found`, + ); + } + + return of(proofRecord); + }), + ); + } + + @Post() + @ApiOperation({ + summary: 'Request a presentation proof', + description: 'This call requests a presentation proof for a given tenant', + }) + @ApiResponse({ + status: HttpStatus.CREATED, + description: 'Presentation proof requested successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Presentation proof requested successfully': { + value: { + statusCode: 201, + message: 'Presentation proof requested successfully', + data: {}, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Tenant not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid request payload', + content: { + 'application/json': { + schema: {}, + examples: { + 'Invalid request payload': { + value: { + statusCode: 400, + message: 'Invalid request payload', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error', + content: { + 'application/json': { + schema: {}, + examples: { + 'Internal server error': { + value: { + statusCode: 500, + message: 'Internal server error', + data: null, + }, + }, + }, + }, + }, + }) + @ApiBody({ + schema: { + type: 'object', + properties: { + name: { + type: 'string', + example: 'Proof of Vaccination', + }, + connectionId: { + type: 'string', + example: '1234567890', + }, + requestedAttributes: { + type: 'object', + additionalProperties: { + type: 'object', + properties: { + names: { + type: 'array', + items: { + type: 'string', + }, + }, + restrictions: { + type: 'array', + items: { + type: 'object', + properties: { + schema_id: { type: 'string' }, + schema_issuer_id: { type: 'string' }, + schema_name: { type: 'string' }, + schema_version: { type: 'string' }, + issuer_id: { type: 'string' }, + cred_def_id: { type: 'string' }, + rev_reg_id: { type: 'string' }, + schema_issuer_did: { type: 'string' }, + issuer_did: { type: 'string' }, + }, + patternProperties: { + '^attr::.*?::marker$': { enum: ['1', '0'] }, + '^attr::.*?::value$': { type: 'string' }, + }, + additionalProperties: { + type: 'string', + anyOf: [{ enum: ['1', '0'] }, { type: 'string' }], + }, + }, + }, + }, + required: ['names'], + }, + }, + requestedPredicates: { + type: 'object', + properties: { + name: { type: 'string' }, + predicateType: { enum: ['>=', '>', '<=', '<'] }, + predicateValue: { type: 'number' }, + restrictions: { + type: 'array', + items: { + type: 'object', + properties: { + schema_id: { type: 'string' }, + schema_issuer_id: { type: 'string' }, + schema_name: { type: 'string' }, + schema_version: { type: 'string' }, + issuer_id: { type: 'string' }, + cred_def_id: { type: 'string' }, + rev_reg_id: { type: 'string' }, + schema_issuer_did: { type: 'string' }, + issuer_did: { type: 'string' }, + }, + patternProperties: { + '^attr::.*?::marker$': { enum: ['1', '0'] }, + '^attr::.*?::value$': { type: 'string' }, + }, + additionalProperties: { + type: 'string', + anyOf: [{ enum: ['1', '0'] }, { type: 'string' }], + }, + }, + }, + }, + required: ['name', 'predicateType', 'predicateValue'], + }, + }, + required: [ + 'name', + 'connectionId', + 'requestedAttributes', + 'requestedPredicates', + ], + }, + }) + public request( + @Query() { tenantId }: MultitenancyParams, + @Body() + { + name, + connectionId, + requestedAttributes, + requestedPredicates, + }: RequestPayload, + ): ReturnType<ProofsService['request']> { + return this.service.request( + tenantId, + name, + connectionId, + requestedAttributes, + requestedPredicates, + ); + } + + @Delete(':id') + @HttpCode(HttpStatus.OK) + @ApiOperation({ + summary: 'Delete a presentation proof', + description: 'This call deletes a presentation proof for a given tenant', + }) + @ApiResponse({ + status: HttpStatus.OK, + description: 'Presentation proof deleted successfully', + content: { + 'application/json': { + schema: {}, + examples: { + 'Presentation proof deleted successfully': { + value: { + statusCode: 200, + message: 'Presentation proof deleted successfully', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.NOT_FOUND, + description: 'Tenant not found', + content: { + 'application/json': { + schema: {}, + examples: { + 'Tenant not found': { + value: { + statusCode: 404, + message: 'Tenant not found', + data: null, + }, + }, + 'Presentation proof not found': { + value: { + statusCode: 404, + message: 'Presentation proof not found', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.BAD_REQUEST, + description: 'Invalid presentation proof id', + content: { + 'application/json': { + schema: {}, + examples: { + 'Invalid presentation proof id': { + value: { + statusCode: 400, + message: 'Invalid presentation proof id', + data: null, + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: HttpStatus.INTERNAL_SERVER_ERROR, + description: 'Internal server error', + content: { + 'application/json': { + schema: {}, + examples: { + 'Internal server error': { + value: { + statusCode: 500, + message: 'Internal server error', + data: null, + }, + }, + }, + }, + }, + }) + public delete( + @Param() { proofRecordId }: GetByIdParams, + @Query() { tenantId }: MultitenancyParams, + ): ReturnType<ProofsService['delete']> { + return this.service.delete(tenantId, proofRecordId); + } +} diff --git a/apps/proof-manager/src/proofs/proofs.module.ts b/apps/proof-manager/src/proofs/proofs.module.ts new file mode 100644 index 0000000..29690bb --- /dev/null +++ b/apps/proof-manager/src/proofs/proofs.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { ProofsController } from './proofs.controller.js'; +import { ProofsService } from './proofs.service.js'; + +@Module({ + providers: [ProofsService], + controllers: [ProofsController], +}) +export class ProofsModule {} diff --git a/apps/proof-manager/src/proofs/proofs.service.ts b/apps/proof-manager/src/proofs/proofs.service.ts new file mode 100644 index 0000000..8e69544 --- /dev/null +++ b/apps/proof-manager/src/proofs/proofs.service.ts @@ -0,0 +1,84 @@ +import type { + EventAnonCredsProofsDeleteByIdInput, + EventAnonCredsProofsGetAllInput, + EventAnonCredsProofsGetByIdInput, + EventDidcommAnonCredsProofsRequestInput, +} from '@ocm/shared'; + +import { Inject, Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { + EventAnonCredsProofsDeleteById, + EventAnonCredsProofsGetById, + EventDidcommAnonCredsProofsRequest, + EventAnonCredsProofsGetAll, +} from '@ocm/shared'; +import { map, type Observable } from 'rxjs'; + +import { NATS_CLIENT } from '../common/constants.js'; + +@Injectable() +export class ProofsService { + public constructor( + @Inject(NATS_CLIENT) private readonly natsClient: ClientProxy, + ) {} + + public find( + tenantId: string, + ): Observable<EventAnonCredsProofsGetAll['data']> { + return this.natsClient + .send< + EventAnonCredsProofsGetAll, + EventAnonCredsProofsGetAllInput + >(EventAnonCredsProofsGetAll.token, { tenantId }) + .pipe(map((result) => result.data)); + } + + public getById( + tenantId: string, + proofRecordId: string, + ): Observable<EventAnonCredsProofsGetById['data']> { + return this.natsClient + .send<EventAnonCredsProofsGetById, EventAnonCredsProofsGetByIdInput>( + EventAnonCredsProofsGetById.token, + { + tenantId, + proofRecordId, + }, + ) + .pipe(map((results) => results.data)); + } + + public request( + tenantId: string, + name: EventDidcommAnonCredsProofsRequestInput['name'], + connectionId: EventDidcommAnonCredsProofsRequestInput['connectionId'], + requestedAttributes: EventDidcommAnonCredsProofsRequestInput['requestedAttributes'], + requestedPredicates: EventDidcommAnonCredsProofsRequestInput['requestedPredicates'], + ): Observable<EventDidcommAnonCredsProofsRequest['data']> { + return this.natsClient + .send< + EventDidcommAnonCredsProofsRequest, + EventDidcommAnonCredsProofsRequestInput + >(EventDidcommAnonCredsProofsRequest.token, { + tenantId, + name, + connectionId, + requestedAttributes, + requestedPredicates, + }) + .pipe(map((results) => results.data)); + } + + public delete( + tenantId: string, + proofRecordId: string, + ): Observable<EventAnonCredsProofsDeleteById['data']> { + return this.natsClient + .send< + EventAnonCredsProofsDeleteById, + EventAnonCredsProofsDeleteByIdInput + >(EventAnonCredsProofsDeleteById.token, { tenantId, proofRecordId }) + .pipe(map((results) => results.data)); + } +} -- GitLab