Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multiselect connection while issuance #629

Merged
merged 10 commits into from
Apr 23, 2024
28 changes: 4 additions & 24 deletions apps/api-gateway/src/issuance/dtos/issuance.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand All @@ -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' })
@IsNotEmpty({ message: 'credentialDefinitionId is required' })
bhavanakarwade marked this conversation as resolved.
Show resolved Hide resolved
@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' })
Expand Down Expand Up @@ -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()
Expand Down
71 changes: 71 additions & 0 deletions apps/api-gateway/src/issuance/dtos/multiConnection.dto.ts
Original file line number Diff line number Diff line change
@@ -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;

}
4 changes: 2 additions & 2 deletions apps/api-gateway/src/issuance/issuance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ import {
ClientDetails,
FileParameter,
IssuanceDto,
IssueCredentialDto,
OOBCredentialDtoWithEmail,
OOBIssueCredentialDto,
PreviewFileDetails
Expand All @@ -63,6 +62,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/multiConnection.dto';
bhavanakarwade marked this conversation as resolved.
Show resolved Hide resolved

@Controller()
@UseFilters(CustomExceptionFilter)
Expand Down Expand Up @@ -527,7 +527,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,
Expand Down
11 changes: 5 additions & 6 deletions apps/api-gateway/src/issuance/issuance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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/multiConnection.dto';

@Injectable()
export class IssuanceService extends BaseService {
Expand All @@ -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<object> {

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<{
Expand Down
4 changes: 2 additions & 2 deletions apps/connection/src/connection.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ export class ConnectionRepository {
break;
}

const agentDetails = await this.prisma.connections.upsert({
const connectionDetails = await this.prisma.connections.upsert({
bhavanakarwade marked this conversation as resolved.
Show resolved Hide resolved
bhavanakarwade marked this conversation as resolved.
Show resolved Hide resolved
where: {
connectionId: connectionDto?.id
},
Expand All @@ -169,7 +169,7 @@ export class ConnectionRepository {
orgId: organisationId
}
});
return agentDetails;
return connectionDetails;
} catch (error) {
this.logger.error(`Error in saveConnectionWebhook: ${error.message} `);
throw error;
Expand Down
9 changes: 6 additions & 3 deletions apps/issuance/interfaces/issuance.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,23 @@ 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;
goalCode?: string,
parentThreadId?: string,
willConfirm?: boolean,
label?: string

}

interface IIndy {
Expand Down
2 changes: 1 addition & 1 deletion apps/issuance/src/issuance.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export class IssuanceController {
constructor(private readonly issuanceService: IssuanceService) { }

@MessagePattern({ cmd: 'send-credential-create-offer' })
async sendCredentialCreateOffer(payload: IIssuance): Promise<ICreateOfferResponse> {
async sendCredentialCreateOffer(payload: IIssuance): Promise<PromiseSettledResult<ICreateOfferResponse>[]> {
return this.issuanceService.sendCredentialCreateOffer(payload);
}

Expand Down
83 changes: 42 additions & 41 deletions apps/issuance/src/issuance.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ export class IssuanceService {
@Inject(CACHE_MANAGER) private cacheService: Cache
) { }


async sendCredentialCreateOffer(payload: IIssuance): Promise<ICreateOfferResponse> {
async sendCredentialCreateOffer(payload: IIssuance): Promise<PromiseSettledResult<ICreateOfferResponse>[]> {

try {
const { orgId, credentialDefinitionId, comment, connectionId, attributes } = payload || {};
const { orgId, credentialDefinitionId, comment, credentialData } = payload || {};

const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails(
credentialDefinitionId
Expand All @@ -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);

Expand All @@ -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<ICreateOfferResponse>[] = [];

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;
Expand Down