diff --git a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts index ce511b169..216fcaeec 100644 --- a/apps/api-gateway/src/issuance/dtos/issuance.dto.ts +++ b/apps/api-gateway/src/issuance/dtos/issuance.dto.ts @@ -105,7 +105,7 @@ class Credential { @IsObject() public credentialStatus?: JsonLdCredentialDetailCredentialStatus; } -class Attribute { +export class Attribute { @ApiProperty() @IsString({ message: 'Attribute name should be string' }) @IsNotEmpty({ message: 'Attribute name is required' }) @@ -121,18 +121,15 @@ class Attribute { @ApiProperty({ default: false }) @IsBoolean() @IsOptional() - @IsNotEmpty({ message: 'isRequired property is required' }) isRequired?: boolean = false; } - -class CredentialsIssuanceDto { +export class CredentialsIssuanceDto { @ApiProperty({ example: 'string' }) - @IsNotEmpty({ message: 'Please provide valid credential definition id' }) - @IsString({ message: 'credential definition id should be string' }) + @IsNotEmpty({ message: 'Credential definition Id is required' }) + @IsString({ message: 'Credential definition id should be string' }) @Transform(({ value }) => value.trim()) - @IsOptional() - credentialDefinitionId?: string; + credentialDefinitionId: string; @ApiProperty({ example: 'string' }) @IsNotEmpty({ message: 'Please provide valid comment' }) @@ -280,23 +277,6 @@ class CredentialOffer { } -export class IssueCredentialDto extends OOBIssueCredentialDto { - @ApiProperty({ example: 'string' }) - @IsNotEmpty({ message: 'connectionId is required' }) - @IsString({ message: 'connectionId should be string' }) - @Transform(({ value }) => trim(value)) - connectionId: string; - - @ApiPropertyOptional() - @IsOptional() - @IsString({ message: 'auto accept proof must be in string' }) - @IsNotEmpty({ message: 'please provide valid auto accept proof' }) - @IsEnum(AutoAccept, { - message: `Invalid auto accept credential. It should be one of: ${Object.values(AutoAccept).join(', ')}` - }) - autoAcceptCredential?: string; -} - export class IssuanceDto { @ApiProperty() @IsOptional() diff --git a/apps/api-gateway/src/issuance/dtos/multi-connection.dto.ts b/apps/api-gateway/src/issuance/dtos/multi-connection.dto.ts new file mode 100644 index 000000000..c5b313d86 --- /dev/null +++ b/apps/api-gateway/src/issuance/dtos/multi-connection.dto.ts @@ -0,0 +1,71 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ArrayMaxSize, ArrayMinSize, IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { Transform, Type } from 'class-transformer'; + +import { AutoAccept } from '@credebl/enum/enum'; +import { trim } from '@credebl/common/cast.helper'; +import { Attribute, CredentialsIssuanceDto } from './issuance.dto'; + +class ConnectionAttributes { + @ApiProperty({ example: 'string' }) + @IsNotEmpty({ message: 'connectionId is required' }) + @IsString({ message: 'connectionId should be string' }) + @Transform(({ value }) => trim(value)) + connectionId: string; + + @ApiProperty({ + example: [ + { + value: 'string', + name: 'string' + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @ArrayMinSize(1) + @IsNotEmpty({ message: 'Please provide valid attributes' }) + @Type(() => Attribute) + attributes: Attribute[]; +} + +export class IssueCredentialDto extends CredentialsIssuanceDto { + @ApiProperty({ + example: [ + { + connectionId: 'string', + attributes: [ + { + value: 'string', + name: 'string' + } + ] + } + ] + }) + @IsArray() + @ValidateNested({ each: true }) + @ArrayMinSize(1) + @ArrayMaxSize(Number(process.env.OOB_BATCH_SIZE), { message: `Limit reached (${process.env.OOB_BATCH_SIZE} connections max).` }) + @IsNotEmpty({ message: 'credentialData is required' }) + @Type(() => ConnectionAttributes) + credentialData: ConnectionAttributes[]; + + @ApiPropertyOptional() + @IsOptional() + @IsString({ message: 'auto accept proof must be in string' }) + @IsNotEmpty({ message: 'please provide valid auto accept proof' }) + @IsEnum(AutoAccept, { + message: `Invalid auto accept credential. It should be one of: ${Object.values(AutoAccept).join(', ')}` + }) + autoAcceptCredential?: string; + + @ApiProperty({ + example: false + }) + @IsOptional() + @IsNotEmpty() + @IsBoolean({message: 'isShortenUrl must be boolean'}) + isShortenUrl?: boolean; + +} diff --git a/apps/api-gateway/src/issuance/issuance.controller.ts b/apps/api-gateway/src/issuance/issuance.controller.ts index 615a44446..464f3b5fe 100644 --- a/apps/api-gateway/src/issuance/issuance.controller.ts +++ b/apps/api-gateway/src/issuance/issuance.controller.ts @@ -44,7 +44,6 @@ import { ClientDetails, FileParameter, IssuanceDto, - IssueCredentialDto, OOBCredentialDtoWithEmail, OOBIssueCredentialDto, PreviewFileDetails @@ -64,6 +63,7 @@ import { RpcException } from '@nestjs/microservices'; /* eslint-disable @typescript-eslint/no-unused-vars */ import { user } from '@prisma/client'; import { IGetAllIssuedCredentialsDto } from './dtos/get-all-issued-credentials.dto'; +import { IssueCredentialDto } from './dtos/multi-connection.dto'; @Controller() @UseFilters(CustomExceptionFilter) @@ -528,7 +528,7 @@ export class IssuanceController { issueCredentialDto.orgId = orgId; const getCredentialDetails = await this.issueCredentialService.sendCredentialCreateOffer(issueCredentialDto, user); - + const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.issuance.success.create, diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 934cc80c0..07478a1a2 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -3,9 +3,10 @@ import { Injectable, Inject } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; -import { ClientDetails, FileParameter, IssuanceDto, IssueCredentialDto, OOBCredentialDtoWithEmail, OOBIssueCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; +import { ClientDetails, FileParameter, IssuanceDto, OOBCredentialDtoWithEmail, OOBIssueCredentialDto, PreviewFileDetails } from './dtos/issuance.dto'; import { FileExportResponse, IIssuedCredentialSearchParams, IssueCredentialType, RequestPayload } from './interfaces'; import { IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; +import { IssueCredentialDto } from './dtos/multi-connection.dto'; @Injectable() export class IssuanceService extends BaseService { @@ -17,13 +18,11 @@ export class IssuanceService extends BaseService { super('IssuanceService'); } - sendCredentialCreateOffer(issueCredentialDto: IssueCredentialDto, user: IUserRequest): Promise<{ - response: object; - }> { + sendCredentialCreateOffer(issueCredentialDto: IssueCredentialDto, user: IUserRequest): Promise { - const payload = { attributes: issueCredentialDto.attributes, comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, connectionId: issueCredentialDto.connectionId, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, user }; + const payload = { comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, credentialData: issueCredentialDto.credentialData, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, user }; - return this.sendNats(this.issuanceProxy, 'send-credential-create-offer', payload); + return this.sendNatsMessage(this.issuanceProxy, 'send-credential-create-offer', payload); } sendCredentialOutOfBand(issueCredentialDto: OOBIssueCredentialDto): Promise<{ diff --git a/apps/connection/src/connection.repository.ts b/apps/connection/src/connection.repository.ts index 2b7034878..430304f97 100644 --- a/apps/connection/src/connection.repository.ts +++ b/apps/connection/src/connection.repository.ts @@ -149,7 +149,7 @@ export class ConnectionRepository { break; } - const agentDetails = await this.prisma.connections.upsert({ + return this.prisma.connections.upsert({ where: { connectionId: connectionDto?.id }, @@ -169,7 +169,6 @@ export class ConnectionRepository { orgId: organisationId } }); - return agentDetails; } catch (error) { this.logger.error(`Error in saveConnectionWebhook: ${error.message} `); throw error; diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index 5747d7a69..16095c050 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -11,12 +11,16 @@ export interface IAttributes { value: string; isRequired?: boolean; } + +interface ICredentialsAttributes { + connectionId: string; + attributes: IAttributes[]; +} export interface IIssuance { user?: IUserRequest; credentialDefinitionId: string; comment?: string; - connectionId: string; - attributes: IAttributes[]; + credentialData: ICredentialsAttributes[]; orgId: string; autoAcceptCredential?: AutoAccept, protocolVersion?: string; @@ -24,7 +28,6 @@ export interface IIssuance { parentThreadId?: string, willConfirm?: boolean, label?: string - } interface IIndy { diff --git a/apps/issuance/src/issuance.controller.ts b/apps/issuance/src/issuance.controller.ts index 1c3d0cd85..0750f85b0 100644 --- a/apps/issuance/src/issuance.controller.ts +++ b/apps/issuance/src/issuance.controller.ts @@ -10,7 +10,7 @@ export class IssuanceController { constructor(private readonly issuanceService: IssuanceService) { } @MessagePattern({ cmd: 'send-credential-create-offer' }) - async sendCredentialCreateOffer(payload: IIssuance): Promise { + async sendCredentialCreateOffer(payload: IIssuance): Promise[]> { return this.issuanceService.sendCredentialCreateOffer(payload); } diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 61123836a..e89b330ff 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -50,11 +50,10 @@ export class IssuanceService { @Inject(CACHE_MANAGER) private cacheService: Cache ) { } - - async sendCredentialCreateOffer(payload: IIssuance): Promise { + async sendCredentialCreateOffer(payload: IIssuance): Promise[]> { try { - const { orgId, credentialDefinitionId, comment, connectionId, attributes } = payload || {}; + const { orgId, credentialDefinitionId, comment, credentialData } = payload || {}; const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( credentialDefinitionId @@ -63,26 +62,26 @@ export class IssuanceService { if (schemaResponse?.attributes) { const schemaResponseError = []; const attributesArray: IAttributes[] = JSON.parse(schemaResponse.attributes); - + attributesArray.forEach((attribute) => { - if (attribute.attributeName && attribute.isRequired) { - - payload.attributes.map((attr) => { - if (attr.name === attribute.attributeName && attribute.isRequired && !attr.value) { - schemaResponseError.push( - `Attribute ${attribute.attributeName} is required` - ); - } - return true; - }); - } + if (attribute.attributeName && attribute.isRequired) { + + credentialData.forEach((credential, i) => { + credential.attributes.forEach((attr) => { + if (attr.name === attribute.attributeName && attribute.isRequired && !attr.value) { + schemaResponseError.push( + `Attribute ${attribute.attributeName} is required at position ${i + 1}` + ); + } + }); + }); + } }); + if (0 < schemaResponseError.length) { - throw new BadRequestException(schemaResponseError); - + throw new BadRequestException(schemaResponseError); } - - } + } const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); @@ -99,34 +98,36 @@ export class IssuanceService { } const issuanceMethodLabel = 'create-offer'; - const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId); - const issueData: IIssueData = { - protocolVersion: 'v1', - connectionId, - credentialFormats: { - indy: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - attributes: (attributes).map(({ isRequired, ...rest }) => rest), - credentialDefinitionId + const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId); - } - }, - autoAcceptCredential: payload.autoAcceptCredential || 'always', - comment - }; + const issuancePromises: Promise[] = []; - const credentialCreateOfferDetails: ICreateOfferResponse = await this._sendCredentialCreateOffer(issueData, url, orgId); + for (const credentials of credentialData) { + const { connectionId, attributes } = credentials; + const issueData: IIssueData = { + protocolVersion: 'v1', + connectionId, + credentialFormats: { + indy: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + attributes: (attributes).map(({ isRequired, ...rest }) => rest), + credentialDefinitionId + + } + }, + autoAcceptCredential: payload.autoAcceptCredential || 'always', + comment + }; - if (credentialCreateOfferDetails && 0 < Object.keys(credentialCreateOfferDetails).length) { - delete credentialCreateOfferDetails._tags; - delete credentialCreateOfferDetails.metadata; - delete credentialCreateOfferDetails.credentials; - delete credentialCreateOfferDetails.credentialAttributes; - delete credentialCreateOfferDetails.autoAcceptCredential; + await this.delay(500); + const credentialCreateOfferDetails = this._sendCredentialCreateOffer(issueData, url, orgId); + issuancePromises.push(credentialCreateOfferDetails); } - return credentialCreateOfferDetails; + const results = await Promise.allSettled(issuancePromises); + return results; + } catch (error) { this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); const errorStack = error?.status?.message?.error?.reason || error?.status?.message?.error;