From 76346367274da484feffefd6b51473af981473cc Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 28 Mar 2025 18:42:18 +0530 Subject: [PATCH 01/14] wip: support nested attributes while creating schema Signed-off-by: bhavanakarwade --- .../api-gateway/src/dtos/create-schema.dto.ts | 167 ++++++++++- apps/ledger/src/schema/enum/schema.enum.ts | 2 +- .../interfaces/schema-payload.interface.ts | 15 + .../src/schema/interfaces/schema.interface.ts | 29 +- apps/ledger/src/schema/schema.service.ts | 270 +++++++++++++++--- libs/enum/src/enum.ts | 2 + libs/validations/arrayKeyword.ts | 3 + libs/validations/exclusiveMaximum.ts | 28 ++ libs/validations/exclusiveMinimum.ts | 28 ++ libs/validations/keyword.ts | 7 + libs/validations/maxItems.ts | 28 ++ libs/validations/maxLength.ts | 28 ++ libs/validations/maximum.ts | 28 ++ libs/validations/minItems.ts | 28 ++ libs/validations/minLength.ts | 28 ++ libs/validations/minimum.ts | 28 ++ libs/validations/multipleOf.ts | 28 ++ libs/validations/numberKeyword.ts | 3 + libs/validations/objectKeyword.ts | 3 + libs/validations/pattern.ts | 33 +++ libs/validations/stringKeyword.ts | 3 + libs/validations/uniqueItems.ts | 28 ++ 22 files changed, 761 insertions(+), 56 deletions(-) create mode 100644 libs/validations/arrayKeyword.ts create mode 100644 libs/validations/exclusiveMaximum.ts create mode 100644 libs/validations/exclusiveMinimum.ts create mode 100644 libs/validations/keyword.ts create mode 100644 libs/validations/maxItems.ts create mode 100644 libs/validations/maxLength.ts create mode 100644 libs/validations/maximum.ts create mode 100644 libs/validations/minItems.ts create mode 100644 libs/validations/minLength.ts create mode 100644 libs/validations/minimum.ts create mode 100644 libs/validations/multipleOf.ts create mode 100644 libs/validations/numberKeyword.ts create mode 100644 libs/validations/objectKeyword.ts create mode 100644 libs/validations/pattern.ts create mode 100644 libs/validations/stringKeyword.ts create mode 100644 libs/validations/uniqueItems.ts diff --git a/apps/api-gateway/src/dtos/create-schema.dto.ts b/apps/api-gateway/src/dtos/create-schema.dto.ts index c74cc2e44..7d3e58b47 100644 --- a/apps/api-gateway/src/dtos/create-schema.dto.ts +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -1,24 +1,24 @@ -import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsPositive, IsString, Min, ValidateIf, ValidateNested } from 'class-validator'; import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; +import { plainToClass, Transform, Type } from 'class-transformer'; import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; import { JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum/enum'; + export class W3CAttributeValue { - class W3CAttributeValue { @ApiProperty() @IsString() @Transform(({ value }) => trim(value)) @IsNotEmpty({ message: 'attributeName is required' }) attributeName: string; - + @ApiProperty() @IsString() @Transform(({ value }) => trim(value)) @IsNotEmpty({ message: 'displayName is required' }) displayName: string; - + @ApiProperty({ description: 'The type of the schema', enum: W3CSchemaDataType, @@ -26,20 +26,161 @@ import { JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum }) @IsEnum(W3CSchemaDataType, { message: 'Schema data type must be a valid type' }) schemaDataType: W3CSchemaDataType; - + @ApiProperty() @IsBoolean() @IsNotEmpty({ message: 'isRequired property is required' }) isRequired: boolean; + + @ApiPropertyOptional({ description: 'Minimum length for string values' }) + @IsOptional() + @IsInt() + @Min(0) + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) + minLength?: number; + + @ApiPropertyOptional({ description: 'Maximum length for string values' }) + @IsOptional() + @IsInt() + @Min(1) + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) + maxLength?: number; + + @ApiPropertyOptional({ description: 'Regular expression pattern for string values' }) + @IsOptional() + @IsString() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) + pattern?: string; + + @ApiPropertyOptional({ description: 'Enumerated values for string type' }) + @IsOptional() + @IsArray() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) + enum?: string[]; - @ApiPropertyOptional({ - description: 'Array of objects with dynamic keys', - isArray: true - }) + @ApiPropertyOptional({ description: 'Content encoding (e.g., base64)' }) + @IsOptional() + @IsString() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) + contentEncoding?: string; + + @ApiPropertyOptional({ description: 'Content media type (e.g., image/png)' }) + @IsOptional() + @IsString() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) + contentMediaType?: string; + + // Number type specific validations + @ApiPropertyOptional({ description: 'Minimum value (inclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) + minimum?: number; + + @ApiPropertyOptional({ description: 'Maximum value (inclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) + maximum?: number; + + @ApiPropertyOptional({ description: 'Minimum value (exclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) + exclusiveMinimum?: number; + + @ApiPropertyOptional({ description: 'Maximum value (exclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) + exclusiveMaximum?: number; + + @ApiPropertyOptional({ description: 'Number must be a multiple of this value' }) + @IsOptional() + @IsNumber() + @IsPositive() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) + multipleOf?: number; + + // Array type specific validations + @ApiPropertyOptional({ description: 'Minimum number of items in array' }) + @IsOptional() + @IsInt() + @Min(0) + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) + minItems?: number; + + @ApiPropertyOptional({ description: 'Maximum number of items in array' }) + @IsOptional() + @IsInt() + @Min(1) + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) + maxItems?: number; + + @ApiPropertyOptional({ description: 'Whether array items must be unique' }) + @IsOptional() + @IsBoolean() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) + uniqueItems?: boolean; + + @ApiPropertyOptional({ description: 'Array of items', type: [W3CAttributeValue] }) + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => W3CAttributeValue) + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) + items?: W3CAttributeValue[]; + + // Object type specific validations + @ApiPropertyOptional({ description: 'Minimum number of properties in object' }) + @IsOptional() + @IsInt() + @Min(0) + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) + minProperties?: number; + + @ApiPropertyOptional({ description: 'Maximum number of properties in object' }) + @IsOptional() + @IsInt() + @Min(1) + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) + maxProperties?: number; + + @ApiPropertyOptional({ description: 'additional properties must be boolean' }) + @IsOptional() + @IsBoolean() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) + additionalProperties?: boolean; + + @ApiPropertyOptional({ description: 'Required properties for object type' }) + @IsOptional() @IsArray() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) + required?: string[]; + + @ApiPropertyOptional({ description: 'Dependent required properties' }) + @IsOptional() + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) + dependentRequired?: Record; + + @ApiPropertyOptional({ description: 'Object with dynamic properties' }) @IsOptional() - nestedAttributes: Record | string>[]; + @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) + @Transform(({ value }) => { + if (value && 'object' === typeof value) { + const result = {}; + Object.entries(value).forEach(([key, propValue]) => { + result[key] = plainToClass(W3CAttributeValue, propValue, { + enableImplicitConversion: false + }); + }); + return result; + } + return value; + }) + properties?: Record; } + class AttributeValue { @ApiProperty() @@ -181,7 +322,5 @@ export class GenericSchemaDTO { return CreateW3CSchemaDto; } }) - schemaPayload:CreateSchemaDto | CreateW3CSchemaDto; - - + schemaPayload:CreateSchemaDto | CreateW3CSchemaDto; } \ No newline at end of file diff --git a/apps/ledger/src/schema/enum/schema.enum.ts b/apps/ledger/src/schema/enum/schema.enum.ts index 247e7050b..1a307a646 100644 --- a/apps/ledger/src/schema/enum/schema.enum.ts +++ b/apps/ledger/src/schema/enum/schema.enum.ts @@ -11,5 +11,5 @@ export enum SortFields { } export enum W3CSchemaVersion { - W3C_SCHEMA_VERSION = 'draft-07' + W3C_SCHEMA_VERSION = 'draft/2020-12' } diff --git a/apps/ledger/src/schema/interfaces/schema-payload.interface.ts b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts index 02bf74de3..db8d09192 100644 --- a/apps/ledger/src/schema/interfaces/schema-payload.interface.ts +++ b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts @@ -88,6 +88,21 @@ export interface SchemaPayload { title: string, } + export interface ISchemaAttributesFormat extends W3CSchemaAttributes{ + order: number, + description: string; + exclusiveMinimum?: number; + multipleOf?: number; + pattern?: string; + minLength?: number; + maxLength?: number; + items?: object[] | string [] | number []; + properties?: object; + format?: string; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + } export interface W3CSchemaPayload { schemaPayload: SchemaPayload, orgId: string, diff --git a/apps/ledger/src/schema/interfaces/schema.interface.ts b/apps/ledger/src/schema/interfaces/schema.interface.ts index cf85b4c70..5604f1f4e 100644 --- a/apps/ledger/src/schema/interfaces/schema.interface.ts +++ b/apps/ledger/src/schema/interfaces/schema.interface.ts @@ -66,11 +66,38 @@ export interface ISchemasWithCount { schemasCount: number; schemasResult: ISchemaData[]; } -interface IW3CAttributeValue { +export interface IW3CAttributeValue { attributeName: string; schemaDataType: W3CSchemaDataType; displayName: string; isRequired: boolean; + minLength?: number; + maxLength?: number; + pattern?: string; + enum?: string[]; + contentEncoding?: string; + contentMediaType?: string; + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + multipleOf?: number; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + items?: IW3CAttributeValue[]; + minProperties?: number; + maxProperties?: number; + additionalProperties?: boolean; + required?: string[]; + dependentRequired?: Record; + properties?: Record; +} + +export interface IProductSchema { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + properties: Record; + required: string[]; } interface IAttributeValue { diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index 47f8708d3..3f62de312 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -10,9 +10,9 @@ import { ClientProxy, RpcException } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; import { SchemaRepository } from './repositories/schema.repository'; import { Prisma, schema } from '@prisma/client'; -import { ISaveSchema, ISchema, ISchemaCredDeffSearchInterface, ISchemaExist, ISchemaSearchCriteria, W3CCreateSchema } from './interfaces/schema-payload.interface'; +import { ISaveSchema, ISchema, ISchemaAttributesFormat, ISchemaCredDeffSearchInterface, ISchemaExist, ISchemaSearchCriteria, W3CCreateSchema } from './interfaces/schema-payload.interface'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { ICreateSchema, ICreateW3CSchema, IGenericSchema, IUpdateSchema, IUserRequestInterface, UpdateSchemaResponse } from './interfaces/schema.interface'; +import { ICreateSchema, ICreateW3CSchema, IGenericSchema, IProductSchema, IUpdateSchema, IUserRequestInterface, IW3CAttributeValue, UpdateSchemaResponse } from './interfaces/schema.interface'; import { CreateSchemaAgentRedirection, GetSchemaAgentRedirection, ISchemaId } from './schema.interface'; import { map } from 'rxjs/operators'; import { JSONSchemaType, LedgerLessConstant, LedgerLessMethods, OrgAgentType, SchemaType, SchemaTypeEnum } from '@credebl/enum/enum'; @@ -27,6 +27,15 @@ import { networkNamespace } from '@credebl/common/common.utils'; import { checkDidLedgerAndNetwork } from '@credebl/common/cast.helper'; import { NATSClient } from '@credebl/common/NATSClient'; import { from } from 'rxjs'; +import UniqueItems from 'libs/validations/uniqueItems'; +import MaxItems from 'libs/validations/maxItems'; +import MinItems from 'libs/validations/minItems'; +import MultipleOf from 'libs/validations/multipleOf'; +import ExclusiveMinimum from 'libs/validations/exclusiveMinimum'; +import Minimum from 'libs/validations/minimum'; +import Pattern from 'libs/validations/pattern'; +import MaxLength from 'libs/validations/maxLength'; +import MinLength from 'libs/validations/minLength'; @Injectable() export class SchemaService extends BaseService { @@ -290,6 +299,7 @@ export class SchemaService extends BaseService { const { tenantId } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); url = `${agentEndPoint}${CommonConstants.SHARED_CREATE_POLYGON_W3C_SCHEMA}${tenantId}`; } + const schemaObject = await this.w3cSchemaBuilder(attributes, schemaName, description); if (!schemaObject) { throw new BadRequestException(ResponseMessages.schema.error.schemaBuilder, { @@ -341,67 +351,247 @@ export class SchemaService extends BaseService { } } - private async w3cSchemaBuilder(attributes, schemaName: string, description: string): Promise { - const schemaAttributeJson = attributes.map((attribute, index) => ({ - [attribute.attributeName]: { - type: attribute.schemaDataType.toLowerCase(), - order: index, - title: attribute.attributeName + private async w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: string, description: string): Promise { + + // Function to apply validations based on attribute properties + const applyValidations = (attribute, propertyObj): ISchemaAttributesFormat => { + const context = { ...propertyObj }; + + // Apply string validations + if ('string' === attribute.schemaDataType.toLowerCase()) { + if (attribute.minLength !== undefined) { + const validation = new MinLength(attribute.minLength); + validation.json(context); + } + + if (attribute.maxLength !== undefined) { + const validation = new MaxLength(attribute.maxLength); + validation.json(context); + } + + if (attribute.pattern !== undefined) { + const validation = new Pattern(attribute.pattern); + validation.json(context); + } } - })); - - // Add the format property to the id key - schemaAttributeJson.unshift({ - id: { - type: 'string', - format: 'uri' + + // Apply number validations + if (['number', 'integer'].includes(attribute.schemaDataType.toLowerCase())) { + if (attribute.minimum !== undefined) { + const validation = new Minimum(attribute.minimum); + validation.json(context); + } + + if (attribute.exclusiveMinimum !== undefined) { + const validation = new ExclusiveMinimum(attribute.exclusiveMinimum); + validation.json(context); + } + + if (attribute.multipleOf !== undefined) { + const validation = new MultipleOf(attribute.multipleOf); + validation.json(context); + } + } + + // Apply array validations + if ('array' === attribute.schemaDataType.toLowerCase()) { + if (attribute.minItems !== undefined) { + const validation = new MinItems(attribute.minItems); + validation.json(context); + } + + if (attribute.maxItems !== undefined) { + const validation = new MaxItems(attribute.maxItems); + validation.json(context); + } + + if (attribute.uniqueItems !== undefined) { + const validation = new UniqueItems(attribute.uniqueItems); + validation.json(context); + } + } + + return context; + }; + + // Function to recursively process attributes + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const processAttributes = (attrs: IW3CAttributeValue[]): IProductSchema => { + if (!Array.isArray(attrs)) { + return { properties: {}, required: [] }; } - }); - const nestedObject = {}; - schemaAttributeJson.forEach((obj) => { - // eslint-disable-next-line prefer-destructuring - const key = Object.keys(obj)[0]; - nestedObject[key] = obj[key]; - }); - - const schemaNameObject = {}; - schemaNameObject[schemaName] = { - 'const': schemaName - }; - const date = new Date().toISOString(); - + const properties = {}; + const required = []; + + attrs.forEach((attribute, index) => { + const { attributeName, schemaDataType, isRequired, displayName } = attribute; + + // Add to required array if isRequired is true + if (isRequired) { + required.push(attributeName); + } + + // Create base property object with common fields + const baseProperty = { + type: schemaDataType.toLowerCase(), + order: index, + title: displayName || attributeName, + description: `${attributeName} field` + }; + + // Handle different attribute types + if (['string', 'number', 'boolean', 'integer'].includes(schemaDataType.toLowerCase())) { + // Apply validations to the base property + properties[attributeName] = applyValidations(attribute, baseProperty); + + } else if ('datetime-local' === schemaDataType.toLowerCase()) { + properties[attributeName] = { + ...baseProperty, + type: 'string', + format: 'date-time' + }; + + } else if ('array' === schemaDataType.toLowerCase() && attribute.items) { + const result = processAttributes(attribute.items); + + properties[attributeName] = { + ...baseProperty, + type: 'array', + items: { + type: 'object', + properties: result.properties + } + }; + + // Apply array-specific validations + properties[attributeName] = applyValidations(attribute, properties[attributeName]); + + // Add required properties to the items schema if any + if (0 < result.required.length) { + properties[attributeName].items.required = result.required; + } + + } else if ('object' === schemaDataType.toLowerCase() && attribute.properties) { + const nestedProperties = {}; + const nestedRequired = []; + + // Process each property in the object + Object.keys(attribute.properties).forEach(propKey => { + const prop = attribute.properties[propKey]; + + // Add to nested required array if isRequired is true + if (prop.isRequired) { + nestedRequired.push(propKey); + } + + // Create base property for nested object + const nestedBaseProperty = { + type: prop.schemaDataType.toLowerCase(), + title: prop.displayName || prop.attributeName, + description: `${prop.attributeName} field` + }; + + if ('array' === prop.schemaDataType.toLowerCase() && prop.items) { + // Handle nested arrays + const result = processAttributes(prop.items); + + nestedProperties[propKey] = { + ...nestedBaseProperty, + type: 'array', + items: { + type: 'object', + properties: result.properties + } + }; + + // Apply array-specific validations + nestedProperties[propKey] = applyValidations(prop, nestedProperties[propKey]); + + // Add required properties to the items schema if any + if (0 < result.required.length) { + nestedProperties[propKey].items.required = result.required; + } + } else { + // Handle basic properties with validations + nestedProperties[propKey] = applyValidations(prop, nestedBaseProperty); + } + }); + + properties[attributeName] = { + ...baseProperty, + type: 'object', + properties: nestedProperties + }; + + // Add required properties to the object schema if any + if (0 < nestedRequired.length) { + properties[attributeName].required = nestedRequired; + } + } + }); + + return { properties, required }; + }; + + // Process all attributes + const result = processAttributes(attributes); + const {properties} = result; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const required = ['id', ...result.required]; + + // Add id property + properties['id'] = { + type: 'string', + format: 'uri' + }; + + const date = new Date().toISOString(); + const schemaNameObject = {}; + schemaNameObject[schemaName] = { + 'const': schemaName + }; + const W3CSchema = { - $schema: 'http://json-schema.org/draft-07/schema#', + $schema: 'https://json-schema.org/draft/2020-12/schema', $id: `${date}-${schemaName}`, + $vocabulary: { + 'https://json-schema.org/draft/2020-12/vocab/core': true, + 'https://json-schema.org/draft/2020-12/vocab/applicator': true, + 'https://json-schema.org/draft/2020-12/vocab/unevaluated': true, + 'https://json-schema.org/draft/2020-12/vocab/validation': true, + 'https://json-schema.org/draft/2020-12/vocab/meta-data': true, + 'https://json-schema.org/draft/2020-12/vocab/format-annotation': true, + 'https://json-schema.org/draft/2020-12/vocab/content': true + }, type: 'object', required: ['@context', 'issuer', 'issuanceDate', 'type', 'credentialSubject'], properties: { '@context': { - $ref: '#/definitions/context' + $ref: '#/$defs/context' }, type: { type: 'array', items: { anyOf: [ { - $ref: '#/definitions/VerifiableCredential' + $ref: '#/$defs/VerifiableCredential' }, { - const: `#/definitions/$${schemaName}` + const: `#/$defs/$${schemaName}` } ] } }, credentialSubject: { - $ref: '#/definitions/credentialSubject' + $ref: '#/$defs/credentialSubject' }, id: { type: 'string', format: 'uri' }, issuer: { - $ref: '#/definitions/uriOrId' + $ref: '#/$defs/uriOrId' }, issuanceDate: { type: 'string', @@ -412,13 +602,13 @@ export class SchemaService extends BaseService { format: 'date-time' }, credentialStatus: { - $ref: '#/definitions/credentialStatus' + $ref: '#/$defs/credentialStatus' }, credentialSchema: { - $ref: '#/definitions/credentialSchema' + $ref: '#/$defs/credentialSchema' } }, - definitions: { + $defs: { context: { type: 'array', items: [ @@ -438,7 +628,7 @@ export class SchemaService extends BaseService { { type: 'array', items: { - $ref: '#/definitions/context' + $ref: '#/$defs/context' } } ] @@ -450,7 +640,7 @@ export class SchemaService extends BaseService { type: 'object', required: ['id'], additionalProperties: false, - properties: nestedObject + properties }, VerifiableCredential: { const: 'VerifiableCredential' diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 0c5adee7a..29dabc308 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -208,6 +208,8 @@ export enum W3CSchemaDataType { STRING = 'string', DATE_TIME = 'datetime-local', ARRAY= 'array', + OBJECT = 'object', + BOOLEAN = 'boolean' } export enum JSONSchemaType { diff --git a/libs/validations/arrayKeyword.ts b/libs/validations/arrayKeyword.ts new file mode 100644 index 000000000..e2ad17116 --- /dev/null +++ b/libs/validations/arrayKeyword.ts @@ -0,0 +1,3 @@ +import Keyword from './keyword'; + +export default class ArrayKeyword extends Keyword {} diff --git a/libs/validations/exclusiveMaximum.ts b/libs/validations/exclusiveMaximum.ts new file mode 100644 index 000000000..bc717ff9d --- /dev/null +++ b/libs/validations/exclusiveMaximum.ts @@ -0,0 +1,28 @@ +import NumberKeyword from './numberKeyword'; + +export default class ExclusiveMaximum extends NumberKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if ('number' === typeof value) { + this._value = value; + } else { + throw new Error('value must be a number'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.exclusiveMaximum = this.value; + return context; + } +} diff --git a/libs/validations/exclusiveMinimum.ts b/libs/validations/exclusiveMinimum.ts new file mode 100644 index 000000000..653c1d1b9 --- /dev/null +++ b/libs/validations/exclusiveMinimum.ts @@ -0,0 +1,28 @@ +import NumberKeyword from './numberKeyword'; + +export default class ExclusiveMinimum extends NumberKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if ('number' === typeof value) { + this._value = value; + } else { + throw new Error('value must be a number'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.exclusiveMinimum = this.value; + return context; + } +} diff --git a/libs/validations/keyword.ts b/libs/validations/keyword.ts new file mode 100644 index 000000000..dc57e4b4c --- /dev/null +++ b/libs/validations/keyword.ts @@ -0,0 +1,7 @@ +export default class Keyword { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context): any { + return context || {}; + } + } + \ No newline at end of file diff --git a/libs/validations/maxItems.ts b/libs/validations/maxItems.ts new file mode 100644 index 000000000..7329cdd5a --- /dev/null +++ b/libs/validations/maxItems.ts @@ -0,0 +1,28 @@ +import ArrayKeyword from './arrayKeyword'; + +export default class MaxItems extends ArrayKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if (0 <= value && Number.isInteger(value)) { + this._value = value; + } else { + throw new Error('value must be an integer and greater than or equal to 0'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.maxItems = this.value; + return context; + } +} diff --git a/libs/validations/maxLength.ts b/libs/validations/maxLength.ts new file mode 100644 index 000000000..b935afb60 --- /dev/null +++ b/libs/validations/maxLength.ts @@ -0,0 +1,28 @@ +import StringKeyword from './stringKeyword'; + +export default class MaxLength extends StringKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + get value(): number { + return this._value; + } + + set value(value: number) { + + if (0 <= value && Number.isInteger(value)) { + this._value = value; + } else { + throw new Error('value must be an integer and greater than or equal to 0'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.maxLength = this.value; + return context; + } +} diff --git a/libs/validations/maximum.ts b/libs/validations/maximum.ts new file mode 100644 index 000000000..c871228d2 --- /dev/null +++ b/libs/validations/maximum.ts @@ -0,0 +1,28 @@ +import NumberKeyword from './numberKeyword'; + +export default class Maximum extends NumberKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if ('number' === typeof value) { + this._value = value; + } else { + throw new Error('value must be a number'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.maximum = this.value; + return context; + } +} diff --git a/libs/validations/minItems.ts b/libs/validations/minItems.ts new file mode 100644 index 000000000..98d4c57bb --- /dev/null +++ b/libs/validations/minItems.ts @@ -0,0 +1,28 @@ +import ArrayKeyword from './arrayKeyword'; + +export default class MinItems extends ArrayKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if (0 <= value && Number.isInteger(value)) { + this._value = value; + } else { + throw new Error('value must be an integer and greater than or equal to 0'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.minItems = this.value; + return context; + } +} diff --git a/libs/validations/minLength.ts b/libs/validations/minLength.ts new file mode 100644 index 000000000..2d97bcda2 --- /dev/null +++ b/libs/validations/minLength.ts @@ -0,0 +1,28 @@ +import StringKeyword from './stringKeyword'; + +export default class MinLength extends StringKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if (0 <= value && Number.isInteger(value)) { + this._value = value; + } else { + throw new Error('value must be an integer and greater than or equal to 0'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.minLength = this.value; + return context; + } +} diff --git a/libs/validations/minimum.ts b/libs/validations/minimum.ts new file mode 100644 index 000000000..05b70ff2a --- /dev/null +++ b/libs/validations/minimum.ts @@ -0,0 +1,28 @@ +import NumberKeyword from './numberKeyword'; + +export default class Minimum extends NumberKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if ('number' === typeof value) { + this._value = value; + } else { + throw new Error('value must be a number'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.minimum = this.value; + return context; + } +} diff --git a/libs/validations/multipleOf.ts b/libs/validations/multipleOf.ts new file mode 100644 index 000000000..bead8736b --- /dev/null +++ b/libs/validations/multipleOf.ts @@ -0,0 +1,28 @@ +import NumberKeyword from './numberKeyword'; + +export default class MultipleOf extends NumberKeyword { + private _value: number; + + constructor(value: number) { + super(); + this.value = value; + } + + get value(): number { + return this._value; + } + + set value(value: number) { + if ('number' === typeof value && 0 < value) { + this._value = value; + } else { + throw new Error('value must be a number greater than 0'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.multipleOf = this.value; + return context; + } +} diff --git a/libs/validations/numberKeyword.ts b/libs/validations/numberKeyword.ts new file mode 100644 index 000000000..7f16cdab5 --- /dev/null +++ b/libs/validations/numberKeyword.ts @@ -0,0 +1,3 @@ +import Keyword from './keyword'; + +export default class NumberKeyword extends Keyword {} diff --git a/libs/validations/objectKeyword.ts b/libs/validations/objectKeyword.ts new file mode 100644 index 000000000..07882c60d --- /dev/null +++ b/libs/validations/objectKeyword.ts @@ -0,0 +1,3 @@ +import Keyword from './keyword'; + +export default class ObjectKeyword extends Keyword {} diff --git a/libs/validations/pattern.ts b/libs/validations/pattern.ts new file mode 100644 index 000000000..556a1e535 --- /dev/null +++ b/libs/validations/pattern.ts @@ -0,0 +1,33 @@ +import StringKeyword from './stringKeyword'; + +export default class Pattern extends StringKeyword { + private _value: string; + + constructor(value: string) { + super(); + this.value = value; + } + + get value(): string { + return this._value; + } + + set value(value: string) { + if ('string' === typeof value) { + try { + new RegExp(value); + this._value = value; + } catch (e) { + throw new Error('value must be a valid regular expression pattern'); + } + } else { + throw new Error('value must be a string'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.pattern = this.value; + return context; + } +} diff --git a/libs/validations/stringKeyword.ts b/libs/validations/stringKeyword.ts new file mode 100644 index 000000000..0c5fd217f --- /dev/null +++ b/libs/validations/stringKeyword.ts @@ -0,0 +1,3 @@ +import Keyword from './keyword'; + +export default class StringKeyword extends Keyword {} \ No newline at end of file diff --git a/libs/validations/uniqueItems.ts b/libs/validations/uniqueItems.ts new file mode 100644 index 000000000..a47d539e6 --- /dev/null +++ b/libs/validations/uniqueItems.ts @@ -0,0 +1,28 @@ +import ArrayKeyword from './arrayKeyword'; + +export default class UniqueItems extends ArrayKeyword { + private _value: boolean; + + constructor(value: boolean) { + super(); + this.value = value; + } + + get value(): boolean { + return this._value; + } + + set value(value: boolean) { + if ('boolean' === typeof value) { + this._value = value; + } else { + throw new Error('value must be a boolean'); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + json(context: Record = {}): Record { + context.uniqueItems = this.value; + return context; + } +} From 940b068a37841b64a2c97bd64c3ab45445716fdd Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Thu, 3 Apr 2025 17:16:32 +0530 Subject: [PATCH 02/14] wip: aligned issuance functionality with nested attributes structure Signed-off-by: bhavanakarwade --- .../src/issuance/issuance.service.ts | 2 +- .../interfaces/issuance.interfaces.ts | 25 ++++ .../libs/helpers/extractAttributes.ts | 132 ++++++++++++++++++ apps/issuance/src/issuance.service.ts | 49 +++---- libs/common/src/common.constant.ts | 2 + 5 files changed, 177 insertions(+), 33 deletions(-) create mode 100644 apps/issuance/libs/helpers/extractAttributes.ts diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 2f1b749ed..543268f6f 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -35,7 +35,7 @@ export class IssuanceService extends BaseService { payload = { attributes: issueCredentialDto.attributes, comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType, isShortenUrl: issueCredentialDto.isShortenUrl, reuseConnection : issueCredentialDto.reuseConnection }; } if (IssueCredentialType.JSONLD === issueCredentialDto.credentialType) { - payload = { credential: issueCredentialDto.credential, options: issueCredentialDto.options, comment: issueCredentialDto.comment, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType, isShortenUrl: issueCredentialDto.isShortenUrl, reuseConnection : issueCredentialDto.reuseConnection }; + payload = { credential: issueCredentialDto.credential, options: issueCredentialDto.options, comment: issueCredentialDto.comment, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType, isShortenUrl: issueCredentialDto.isShortenUrl, reuseConnection : issueCredentialDto.reuseConnection, isValidateSchema: issueCredentialDto.isValidateSchema}; } return this.natsClient.sendNats(this.issuanceProxy, 'send-credential-create-offer-oob', payload); diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index cb30338eb..db85a1e68 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -374,6 +374,31 @@ export interface ISchemaAttributes { isRequired: boolean; } +export interface IW3CAttributeValue extends ISchemaAttributes{ + minLength?: number; + maxLength?: number; + pattern?: string; + enum?: string[]; + contentEncoding?: string; + contentMediaType?: string; + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + multipleOf?: number; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + items?: IW3CAttributeValue[]; + minProperties?: number; + maxProperties?: number; + additionalProperties?: boolean; + required?: string[]; + dependentRequired?: Record; + properties?: Record; +} + + export interface IIssuanceAttributes { [key: string]: string; } diff --git a/apps/issuance/libs/helpers/extractAttributes.ts b/apps/issuance/libs/helpers/extractAttributes.ts new file mode 100644 index 000000000..48f9cbb11 --- /dev/null +++ b/apps/issuance/libs/helpers/extractAttributes.ts @@ -0,0 +1,132 @@ +import { CommonConstants } from '@credebl/common/common.constant'; +import { TemplateIdentifier } from '@credebl/enum/enum'; + +export function extractAttributeNames( + attributeObj, + parentKey: string = '', + result: Set = new Set() +): string[] { + if (Array.isArray(attributeObj)) { + attributeObj.forEach((item) => { + extractAttributeNames(item, parentKey, result); + }); + } else if ('object' === typeof attributeObj && null !== attributeObj) { + let newParentKey = parentKey; + + if (attributeObj.hasOwnProperty('attributeName')) { + newParentKey = parentKey ? `${parentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${attributeObj.attributeName}` : attributeObj.attributeName; + } + + if (attributeObj.hasOwnProperty('items') && Array.isArray(attributeObj.items)) { + attributeObj.items.forEach((item, index) => { + extractAttributeNames(item, `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${index}`, result); + }); + } else if (attributeObj.hasOwnProperty('properties')) { + Object.entries(attributeObj.properties).forEach(([key, value]) => { + const propertyKey = `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${key}`; + extractAttributeNames(value, propertyKey, result); + }); + } else { + Object.entries(attributeObj).forEach(([key, value]) => { + if (!['attributeName', 'items', 'properties'].includes(key)) { + extractAttributeNames(value, newParentKey, result); + } + }); + } + } else { + result.add(parentKey); + } + + return Array.from(result); // Convert Set to an array and return +} + + //Merges objects inside arrays where attributes are split across multiple objects. + function mergeArrayObjects(obj): void { + for (const key in obj) { + if (Array.isArray(obj[key])) { + // Create a map to store merged objects + const mergedArray = []; + + obj[key].forEach((item) => { + if ('object' === typeof item && null !== item) { + Object.keys(item).forEach((k) => { + const match = k.match(/(.*?)(\d+)$/); // Detect keys like "Course Code1" + if (match) { + const baseKey = match[1].trim(); // Extract base name (e.g., "Course Code") + const index = parseInt(match[2]); // Extract index number (e.g., "1") + + // Ensure the indexed object exists in the array + if (!mergedArray[index]) { + mergedArray[index] = {}; + } + + // Assign the correct attribute to the right indexed object + mergedArray[index][baseKey] = item[k]; + } else { + // Directly assign if no index is found + if (!mergedArray[0]) { + mergedArray[0] = {}; + } + mergedArray[0][k] = item[k]; + } + }); + } + }); + + obj[key] = mergedArray; + } else if ('object' === typeof obj[key] && null !== obj[key]) { + mergeArrayObjects(obj[key]); // Recursively merge nested objects + } + } + } + + + export function unflattenCsvRow(row: object): object { + const result: object = {}; + + for (const key in row) { + if (Object.prototype.hasOwnProperty.call(row, key)) { + const keys = TemplateIdentifier.EMAIL_COLUMN === key ? [key] : [...key.split('${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}')]; + + let currentLevel = result; + + for (let i = 0; i < keys.length; i++) { + const part = keys[i]; + + if (i === keys.length - 1) { + // Assign value at the last level + if ('' !== row[key]) { + currentLevel[part] = row[key]; + } + } else { + // Check if the next key is an array index + if (!isNaN(Number(keys[i + 1]))) { + if (!currentLevel[part]) { + currentLevel[part] = []; + } + const index = parseInt(keys[i + 1]); + + // Ensure the indexed object exists and merge attributes + if (!currentLevel[part][index]) { + currentLevel[part][index] = {}; + } + + currentLevel = currentLevel[part][index]; + i++; // Skip the next key since it's an array index + } else { + // Handle object nesting + if (!currentLevel[part]) { + currentLevel[part] = {}; + } + currentLevel = currentLevel[part]; + } + } + } + } + } + + // Merge duplicate indexed keys into a single object inside arrays + mergeArrayObjects(result); + return result; + } + \ No newline at end of file diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 108217f43..272274220 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -39,6 +39,7 @@ import { validateW3CSchemaAttributes } from '../libs/helpers/attributes.validato import { ISchemaDetail } from '@credebl/common/interfaces/schema.interface'; import ContextStorageService, { ContextStorageServiceKey } from '@credebl/context/contextStorageService.interface'; import { NATSClient } from '@credebl/common/NATSClient'; +import { extractAttributeNames, unflattenCsvRow } from '../libs/helpers/extractAttributes'; @Injectable() export class IssuanceService { private readonly logger = new Logger('IssueCredentialService'); @@ -1073,55 +1074,37 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO fileName = `${schemaResponse.tag}-${timestamp}.csv`; } else if (schemaType === SchemaType.W3C_Schema) { - const schemDetails = await this.issuanceRepository.getSchemaDetailsBySchemaIdentifier(templateId); - const {attributes, schemaLedgerId, name} = schemDetails; + const schemaDetails = await this.issuanceRepository.getSchemaDetailsBySchemaIdentifier(templateId); + const {attributes, schemaLedgerId, name} = schemaDetails; schemaResponse = { attributes, schemaLedgerId, name }; if (!schemaResponse) { throw new NotFoundException(ResponseMessages.bulkIssuance.error.invalidIdentifier); } fileName = `${schemaResponse.name}-${timestamp}.csv`; } - const jsonData = []; + const attributesArray = JSON.parse(schemaResponse.attributes); - // Extract the 'attributeName' values from the objects and store them in an array - const attributeNameArray = attributesArray - .filter((attribute) => W3CSchemaDataType.ARRAY !== attribute?.schemaDataType) - .map((attribute) => attribute.attributeName); - - let nestedAttributes = []; - - if (attributesArray.some((attribute) => W3CSchemaDataType.ARRAY === attribute?.schemaDataType)) { - nestedAttributes = attributesArray - .filter((attribute) => W3CSchemaDataType.ARRAY === attribute?.schemaDataType) - .flatMap((attribute) => attribute.nestedAttributes || []) - .flatMap(Object.entries) - .flatMap(([key, value]) => [key, ...Object.values(value)]); - } - - attributeNameArray.unshift(TemplateIdentifier.EMAIL_COLUMN); - - const [csvData, csvFields] = 0 < nestedAttributes.length - ? [jsonData, [...attributeNameArray, ...nestedAttributes]] - : [jsonData, attributeNameArray]; + const csvFields: string[] = [TemplateIdentifier.EMAIL_COLUMN]; + + const flattendData = extractAttributeNames(attributesArray); + csvFields.push(...flattendData); - if (!csvData || !csvFields) { + const jsonData = []; + + if (!csvFields.length) { // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject('Unable to transform schema data for CSV.'); } - - const csv = parse(csvFields, { fields: csvFields }); - + + const csv = parse(jsonData, { fields: csvFields }); const filePath = join(process.cwd(), `uploadedFiles/exports`); - - await createFile(filePath, fileName, csv); const fullFilePath = join(process.cwd(), `uploadedFiles/exports/${fileName}`); this.logger.log('fullFilePath::::::::', fullFilePath); //remove after user if (!checkIfFileOrDirectoryExists(fullFilePath)) { throw new NotFoundException(ResponseMessages.bulkIssuance.error.PathNotFound); } - // https required to download csv from frontend side const filePathToDownload = `${process.env.API_GATEWAY_PROTOCOL_SECURE}://${process.env.UPLOAD_LOGO_HOST}/${fileName}`; return { @@ -1170,6 +1153,8 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO transformheader: (header) => header.toLowerCase().replace('#', '').trim(), complete: (results) => results.data }); + + const nestedObject = parsedData.data.map(row => unflattenCsvRow(row)); if (0 >= parsedData.data.length) { throw new BadRequestException(ResponseMessages.bulkIssuance.error.emptyFile); @@ -1184,7 +1169,7 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO throw new BadRequestException(ResponseMessages.bulkIssuance.error.invalidEmails); } - const fileData: string[][] = parsedData.data.map(Object.values); + const fileData: string[][] = nestedObject.map(Object.values); const fileHeader: string[] = parsedData.meta.fields; const attributesArray = JSON.parse(credentialDetails.attributes); @@ -1218,7 +1203,7 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO return { email_identifier, ...newRow }; }); } else if (type === SchemaType.W3C_Schema && !isValidateSchema) { - validatedData = parsedData.data.map((row) => { + validatedData = nestedObject.map((row) => { const { email_identifier, ...rest } = row; const newRow = { ...rest }; diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index 0d1997315..1abbb5aac 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -141,6 +141,8 @@ export enum CommonConstants { // CREATE KEYS CREATE_POLYGON_SECP256k1_KEY = '/polygon/create-keys', + // Nested attribute separator + NESTED_ATTRIBUTE_SEPARATOR = '~', // ENTITY NAMES ENTITY_NAME_TEMPLATE = 'templates', From 5ce21fbc8409adbaaca43de0990dc7dce368d19e Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 4 Apr 2025 16:05:03 +0530 Subject: [PATCH 03/14] refactor: modify csv to json function Signed-off-by: bhavanakarwade --- .../libs/helpers/attributes.extractor.ts | 197 +++ .../libs/helpers/extractAttributes.ts | 132 -- apps/issuance/src/issuance.service.ts | 1248 +++++++++-------- 3 files changed, 872 insertions(+), 705 deletions(-) create mode 100644 apps/issuance/libs/helpers/attributes.extractor.ts delete mode 100644 apps/issuance/libs/helpers/extractAttributes.ts diff --git a/apps/issuance/libs/helpers/attributes.extractor.ts b/apps/issuance/libs/helpers/attributes.extractor.ts new file mode 100644 index 000000000..cbe125e35 --- /dev/null +++ b/apps/issuance/libs/helpers/attributes.extractor.ts @@ -0,0 +1,197 @@ +import { CommonConstants } from '@credebl/common/common.constant'; +import { TemplateIdentifier } from '@credebl/enum/enum'; + +// Function for extracting attributes from nested structure +export function extractAttributeNames( + attributeObj, + parentKey: string = '', + result: Set = new Set(), + inNestedArray: boolean = false // Track if we're inside a nested array +): string[] { + if (Array.isArray(attributeObj)) { + attributeObj.forEach((item) => { + // For array items, pass through the nested array flag + extractAttributeNames(item, parentKey, result, inNestedArray); + }); + } else if ('object' === typeof attributeObj && null !== attributeObj) { + let newParentKey = parentKey; + + if (attributeObj.hasOwnProperty('attributeName')) { + newParentKey = parentKey + ? `${parentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${attributeObj.attributeName}` + : attributeObj.attributeName; + } + + if (attributeObj.hasOwnProperty('items') && Array.isArray(attributeObj.items)) { + const isNestedArray = parentKey.includes(CommonConstants.NESTED_ATTRIBUTE_SEPARATOR); + + attributeObj.items.forEach((item, index) => { + // For items in nested arrays, always use index 0 + const useIndex = isNestedArray ? 0 : index; + extractAttributeNames( + item, + `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${useIndex}`, + result, + true // Mark that we're now in a nested array context + ); + }); + } else if (attributeObj.hasOwnProperty('properties')) { + Object.entries(attributeObj.properties).forEach(([key, value]) => { + const propertyKey = `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${key}`; + extractAttributeNames(value, propertyKey, result, inNestedArray); + }); + } else { + Object.entries(attributeObj).forEach(([key, value]) => { + if (!['attributeName', 'items', 'properties'].includes(key)) { + extractAttributeNames(value, newParentKey, result, inNestedArray); + } + }); + } + } else { + result.add(parentKey); + } + + return Array.from(result); +} + +//For merging nested objects with numbered keys nto an array of objects +function mergeArrayObjects(obj): void { + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (Array.isArray(obj[key])) { + // Check if the array contains objects with numbered keys + const hasNumericKeys = obj[key].some( + (item) => item && 'object' === typeof item && Object.keys(item).some((k) => k.match(/.*?\d+$/)) + ); + + // Only apply the merging logic if we have numeric keys that need merging + if (hasNumericKeys) { + const mergedArray = []; + obj[key].forEach((item) => { + if ('object' === typeof item && null !== item) { + Object.keys(item).forEach((k) => { + const match = k.match(/(.*?)(\d+)$/); + if (match) { + const baseKey = match[1].trim(); + const index = parseInt(match[2]); + if (!mergedArray[index]) { + mergedArray[index] = {}; + } + mergedArray[index][baseKey] = item[k]; + } else { + if (!mergedArray[0]) { + mergedArray[0] = {}; + } + mergedArray[0][k] = item[k]; + } + }); + } + }); + obj[key] = mergedArray; + } + + // Recursively process array items that are objects + obj[key].forEach((item) => { + if ('object' === typeof item && null !== item) { + mergeArrayObjects(item); + } + }); + } else if ('object' === typeof obj[key] && null !== obj[key]) { + mergeArrayObjects(obj[key]); + } + } + } +} + +// function to converts a flattened CSV row into a nested object. +export function unflattenCsvRow(row: object): object { + const result: object = {}; + const groupedKeys: Record = {}; + + for (const key in row) { + if (Object.prototype.hasOwnProperty.call(row, key)) { + if (TemplateIdentifier.EMAIL_COLUMN === key) { + result[key] = row[key]; + continue; + } + + const keyParts = key.split(`${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}`); + + if (1 < keyParts.length && !isNaN(Number(keyParts[1]))) { + // eslint-disable-next-line prefer-destructuring + const arrayName = keyParts[0]; + // eslint-disable-next-line prefer-destructuring + const arrayIndex = keyParts[1]; + const groupKey = `${arrayName}~${arrayIndex}`; + if (!groupedKeys[groupKey]) { + groupedKeys[groupKey] = []; + } + groupedKeys[groupKey].push(key); + } else { + let currentLevel = result; + for (let i = 0; i < keyParts.length; i++) { + const part = keyParts[i]; + if (i === keyParts.length - 1) { + if ('' !== row[key]) { + currentLevel[part] = row[key]; + } + } else { + if (!currentLevel[part]) { + currentLevel[part] = {}; + } + currentLevel = currentLevel[part]; + } + } + } + } + } + + for (const groupKey in groupedKeys) { + if (Object.prototype.hasOwnProperty.call(groupedKeys, groupKey)) { + const [arrayName, arrayIndex] = groupKey.split('~'); + const keys = groupedKeys[groupKey]; + if (!result[arrayName]) { + result[arrayName] = []; + } + const index = parseInt(arrayIndex); + while (result[arrayName].length <= index) { + result[arrayName].push({}); + } + + for (const key of keys) { + if ('' !== row[key]) { + const keyParts = key.split(`${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}`); + const remainingParts = keyParts.slice(2); + let currentLevel = result[arrayName][index]; + + for (let i = 0; i < remainingParts.length; i++) { + const part = remainingParts[i]; + if (i === remainingParts.length - 1) { + currentLevel[part] = row[key]; + } else { + if (!isNaN(Number(remainingParts[i + 1]))) { + if (!currentLevel[part]) { + currentLevel[part] = []; + } + const nestedIndex = parseInt(remainingParts[i + 1]); + while (currentLevel[part].length <= nestedIndex) { + currentLevel[part].push({}); + } + currentLevel = currentLevel[part][nestedIndex]; + i++; + } else { + if (!currentLevel[part]) { + currentLevel[part] = {}; + } + currentLevel = currentLevel[part]; + } + } + } + } + } + } + } + + mergeArrayObjects(result); + return result; +} diff --git a/apps/issuance/libs/helpers/extractAttributes.ts b/apps/issuance/libs/helpers/extractAttributes.ts deleted file mode 100644 index 48f9cbb11..000000000 --- a/apps/issuance/libs/helpers/extractAttributes.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { CommonConstants } from '@credebl/common/common.constant'; -import { TemplateIdentifier } from '@credebl/enum/enum'; - -export function extractAttributeNames( - attributeObj, - parentKey: string = '', - result: Set = new Set() -): string[] { - if (Array.isArray(attributeObj)) { - attributeObj.forEach((item) => { - extractAttributeNames(item, parentKey, result); - }); - } else if ('object' === typeof attributeObj && null !== attributeObj) { - let newParentKey = parentKey; - - if (attributeObj.hasOwnProperty('attributeName')) { - newParentKey = parentKey ? `${parentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${attributeObj.attributeName}` : attributeObj.attributeName; - } - - if (attributeObj.hasOwnProperty('items') && Array.isArray(attributeObj.items)) { - attributeObj.items.forEach((item, index) => { - extractAttributeNames(item, `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${index}`, result); - }); - } else if (attributeObj.hasOwnProperty('properties')) { - Object.entries(attributeObj.properties).forEach(([key, value]) => { - const propertyKey = `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${key}`; - extractAttributeNames(value, propertyKey, result); - }); - } else { - Object.entries(attributeObj).forEach(([key, value]) => { - if (!['attributeName', 'items', 'properties'].includes(key)) { - extractAttributeNames(value, newParentKey, result); - } - }); - } - } else { - result.add(parentKey); - } - - return Array.from(result); // Convert Set to an array and return -} - - //Merges objects inside arrays where attributes are split across multiple objects. - function mergeArrayObjects(obj): void { - for (const key in obj) { - if (Array.isArray(obj[key])) { - // Create a map to store merged objects - const mergedArray = []; - - obj[key].forEach((item) => { - if ('object' === typeof item && null !== item) { - Object.keys(item).forEach((k) => { - const match = k.match(/(.*?)(\d+)$/); // Detect keys like "Course Code1" - if (match) { - const baseKey = match[1].trim(); // Extract base name (e.g., "Course Code") - const index = parseInt(match[2]); // Extract index number (e.g., "1") - - // Ensure the indexed object exists in the array - if (!mergedArray[index]) { - mergedArray[index] = {}; - } - - // Assign the correct attribute to the right indexed object - mergedArray[index][baseKey] = item[k]; - } else { - // Directly assign if no index is found - if (!mergedArray[0]) { - mergedArray[0] = {}; - } - mergedArray[0][k] = item[k]; - } - }); - } - }); - - obj[key] = mergedArray; - } else if ('object' === typeof obj[key] && null !== obj[key]) { - mergeArrayObjects(obj[key]); // Recursively merge nested objects - } - } - } - - - export function unflattenCsvRow(row: object): object { - const result: object = {}; - - for (const key in row) { - if (Object.prototype.hasOwnProperty.call(row, key)) { - const keys = TemplateIdentifier.EMAIL_COLUMN === key ? [key] : [...key.split('${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}')]; - - let currentLevel = result; - - for (let i = 0; i < keys.length; i++) { - const part = keys[i]; - - if (i === keys.length - 1) { - // Assign value at the last level - if ('' !== row[key]) { - currentLevel[part] = row[key]; - } - } else { - // Check if the next key is an array index - if (!isNaN(Number(keys[i + 1]))) { - if (!currentLevel[part]) { - currentLevel[part] = []; - } - const index = parseInt(keys[i + 1]); - - // Ensure the indexed object exists and merge attributes - if (!currentLevel[part][index]) { - currentLevel[part][index] = {}; - } - - currentLevel = currentLevel[part][index]; - i++; // Skip the next key since it's an array index - } else { - // Handle object nesting - if (!currentLevel[part]) { - currentLevel[part] = {}; - } - currentLevel = currentLevel[part]; - } - } - } - } - } - - // Merge duplicate indexed keys into a single object inside arrays - mergeArrayObjects(result); - return result; - } - \ No newline at end of file diff --git a/apps/issuance/src/issuance.service.ts b/apps/issuance/src/issuance.service.ts index 272274220..bd07d5040 100644 --- a/apps/issuance/src/issuance.service.ts +++ b/apps/issuance/src/issuance.service.ts @@ -2,15 +2,57 @@ /* eslint-disable no-useless-catch */ /* eslint-disable camelcase */ import { CommonService } from '@credebl/common'; -import { BadRequestException, ConflictException, HttpException, HttpStatus, Inject, Injectable, InternalServerErrorException, Logger, NotFoundException } from '@nestjs/common'; +import { + BadRequestException, + ConflictException, + HttpException, + HttpStatus, + Inject, + Injectable, + InternalServerErrorException, + Logger, + NotFoundException +} from '@nestjs/common'; import { IssuanceRepository } from './issuance.repository'; import { IUserRequest } from '@credebl/user-request/user-request.interface'; import { CommonConstants } from '@credebl/common/common.constant'; import { ResponseMessages } from '@credebl/common/response-messages'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { map } from 'rxjs'; -import { BulkPayloadDetails, CredentialOffer, FileUpload, FileUploadData, IAttributes, IBulkPayloadObject, IClientDetails, ICreateOfferResponse, ICredentialPayload, IIssuance, IIssueData, IPattern, IQueuePayload, ISchemaAttributes, ISchemaId, ISendOfferNatsPayload, ImportFileDetails, IssueCredentialWebhookPayload, OutOfBandCredentialOfferPayload, PreviewRequest, SchemaDetails, SendEmailCredentialOffer, TemplateDetailsInterface } from '../interfaces/issuance.interfaces'; -import { AutoAccept, IssuanceProcessState, OrgAgentType, PromiseResult, SchemaType, TemplateIdentifier, W3CSchemaDataType} from '@credebl/enum/enum'; +import { + BulkPayloadDetails, + CredentialOffer, + FileUpload, + FileUploadData, + IAttributes, + IBulkPayloadObject, + IClientDetails, + ICreateOfferResponse, + ICredentialPayload, + IIssuance, + IIssueData, + IPattern, + IQueuePayload, + ISchemaAttributes, + ISchemaId, + ISendOfferNatsPayload, + ImportFileDetails, + IssueCredentialWebhookPayload, + OutOfBandCredentialOfferPayload, + PreviewRequest, + SchemaDetails, + SendEmailCredentialOffer, + TemplateDetailsInterface +} from '../interfaces/issuance.interfaces'; +import { + AutoAccept, + IssuanceProcessState, + OrgAgentType, + PromiseResult, + SchemaType, + TemplateIdentifier, + W3CSchemaDataType +} from '@credebl/enum/enum'; import * as QRCode from 'qrcode'; import { OutOfBandIssuance } from '../templates/out-of-band-issuance.template'; import { EmailDto } from '@credebl/common/dtos/email.dto'; @@ -28,10 +70,21 @@ import { FileUploadStatus, FileUploadType } from 'apps/api-gateway/src/enum'; import { AwsService } from '@credebl/aws'; import { io } from 'socket.io-client'; import { IIssuedCredentialSearchParams, IssueCredentialType } from 'apps/api-gateway/src/issuance/interfaces'; -import { ICredentialOfferResponse, IDeletedIssuanceRecords, IIssuedCredential, IJsonldCredential, IPrettyVc, ISchemaObject } from '@credebl/common/interfaces/issuance.interface'; +import { + ICredentialOfferResponse, + IDeletedIssuanceRecords, + IIssuedCredential, + IJsonldCredential, + IPrettyVc, + ISchemaObject +} from '@credebl/common/interfaces/issuance.interface'; import { OOBIssueCredentialDto } from 'apps/api-gateway/src/issuance/dtos/issuance.dto'; import { RecordType, agent_invitations, organisation, user } from '@prisma/client'; -import { createOobJsonldIssuancePayload, validateAndUpdateIssuanceDates, validateEmail } from '@credebl/common/cast.helper'; +import { + createOobJsonldIssuancePayload, + validateAndUpdateIssuanceDates, + validateEmail +} from '@credebl/common/cast.helper'; import { sendEmail } from '@credebl/common/send-grid-helper-file'; import * as pLimit from 'p-limit'; import { UserActivityRepository } from 'libs/user-activity/repositories'; @@ -39,7 +92,7 @@ import { validateW3CSchemaAttributes } from '../libs/helpers/attributes.validato import { ISchemaDetail } from '@credebl/common/interfaces/schema.interface'; import ContextStorageService, { ContextStorageServiceKey } from '@credebl/context/contextStorageService.interface'; import { NATSClient } from '@credebl/common/NATSClient'; -import { extractAttributeNames, unflattenCsvRow } from '../libs/helpers/extractAttributes'; +import { extractAttributeNames, unflattenCsvRow } from '../libs/helpers/attributes.extractor'; @Injectable() export class IssuanceService { private readonly logger = new Logger('IssueCredentialService'); @@ -57,15 +110,14 @@ export class IssuanceService { @InjectQueue('bulk-issuance') private readonly bulkIssuanceQueue: Queue, @Inject(CACHE_MANAGER) private readonly cacheService: Cache, @Inject(ContextStorageServiceKey) - private readonly contextStorageService: ContextStorageService, - private readonly natsClient : NATSClient - ) { } + private readonly contextStorageService: ContextStorageService, + private readonly natsClient: NATSClient + ) {} async getIssuanceRecords(orgId: string): Promise { try { return await this.issuanceRepository.getIssuanceRecordsCount(orgId); } catch (error) { - this.logger.error( `[getIssuanceRecords ] [NATS call]- error in get issuance records count : ${JSON.stringify(error)}` ); @@ -80,12 +132,12 @@ export class IssuanceService { cause: new Error(), description: ResponseMessages.errorMessages.notFound }); - } + } - const getSchemaDetails = await this.issuanceRepository.getSchemaDetails(schemaUrl); - const schemaAttributes = JSON.parse(getSchemaDetails?.attributes); + const getSchemaDetails = await this.issuanceRepository.getSchemaDetails(schemaUrl); + const schemaAttributes = JSON.parse(getSchemaDetails?.attributes); - return schemaAttributes; + return schemaAttributes; } async sendCredentialCreateOffer(payload: IIssuance): Promise { @@ -93,9 +145,8 @@ export class IssuanceService { const { orgId, credentialDefinitionId, comment, credentialData } = payload || {}; if (payload.credentialType === IssueCredentialType.INDY) { - const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( - credentialDefinitionId - ); + const schemaResponse: SchemaDetails = + await this.issuanceRepository.getCredentialDefinitionDetails(credentialDefinitionId); if (schemaResponse?.attributes) { const schemaResponseError = []; const attributesArray: IAttributes[] = JSON.parse(schemaResponse.attributes); @@ -132,11 +183,10 @@ export class IssuanceService { const issuanceMethodLabel = 'create-offer'; const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId); - if (payload.credentialType === IssueCredentialType.JSONLD) { await validateAndUpdateIssuanceDates(credentialData); } - + const issuancePromises = credentialData.map(async (credentials) => { const { connectionId, attributes, credential, options } = credentials; let issueData; @@ -226,7 +276,6 @@ export class IssuanceService { }; return finalResult; - } catch (error) { this.logger.error(`[sendCredentialCreateOffer] - error in create credentials : ${JSON.stringify(error)}`); const errorStack = error?.status?.message?.error?.reason || error?.status?.message?.error; @@ -245,12 +294,22 @@ export class IssuanceService { async sendCredentialOutOfBand(payload: OOBIssueCredentialDto): Promise<{ response: object }> { try { - - const { orgId, credentialDefinitionId, comment, attributes, protocolVersion, credential, options, credentialType, isShortenUrl, reuseConnection, isValidateSchema } = payload; + const { + orgId, + credentialDefinitionId, + comment, + attributes, + protocolVersion, + credential, + options, + credentialType, + isShortenUrl, + reuseConnection, + isValidateSchema + } = payload; if (credentialType === IssueCredentialType.INDY) { - const schemadetailsResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( - credentialDefinitionId - ); + const schemadetailsResponse: SchemaDetails = + await this.issuanceRepository.getCredentialDefinitionDetails(credentialDefinitionId); if (schemadetailsResponse?.attributes) { const schemadetailsResponseError = []; @@ -258,7 +317,6 @@ export class IssuanceService { attributesArray.forEach((attribute) => { if (attribute.attributeName && attribute.isRequired) { - payload.attributes.forEach((attr) => { if (attr.name === attribute.attributeName && attribute.isRequired && !attr.value) { schemadetailsResponseError.push( @@ -272,7 +330,6 @@ export class IssuanceService { if (0 < schemadetailsResponseError.length) { throw new BadRequestException(schemadetailsResponseError); } - } } @@ -280,10 +337,10 @@ export class IssuanceService { let invitationDid: string | undefined; if (true === reuseConnection) { const data: agent_invitations[] = await this.issuanceRepository.getInvitationDidByOrgId(orgId); - if (data && 0 < data.length) { + if (data && 0 < data.length) { const [firstElement] = data; invitationDid = firstElement?.invitationDid ?? undefined; - } + } } const { agentEndPoint, organisation } = agentDetails; @@ -296,16 +353,14 @@ export class IssuanceService { const issuanceMethodLabel = 'create-offer-oob'; const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId); - let issueData; if (credentialType === IssueCredentialType.INDY) { - issueData = { protocolVersion: protocolVersion || 'v1', credentialFormats: { indy: { // eslint-disable-next-line @typescript-eslint/no-unused-vars - attributes: (attributes).map(({ isRequired, ...rest }) => rest), + attributes: attributes.map(({ isRequired, ...rest }) => rest), credentialDefinitionId } }, @@ -316,9 +371,8 @@ export class IssuanceService { imageUrl: organisation?.logoUrl || payload?.imageUrl || undefined, label: organisation?.name, comment: comment || '', - invitationDid:invitationDid || undefined + invitationDid: invitationDid || undefined }; - } if (credentialType === IssueCredentialType.JSONLD) { @@ -337,7 +391,7 @@ export class IssuanceService { imageUrl: organisation?.logoUrl || payload?.imageUrl || undefined, label: organisation?.name, comment: comment || '', - invitationDid:invitationDid || undefined + invitationDid: invitationDid || undefined }; const payloadAttributes = issueData?.credentialFormats?.jsonld?.credential?.credentialSubject; @@ -371,7 +425,6 @@ export class IssuanceService { message: errorStack?.reason ? errorStack?.reason : errorStack?.message, statusCode: error?.status?.code }); - } else { throw new RpcException(error.response ? error.response : error); } @@ -380,17 +433,19 @@ export class IssuanceService { async storeIssuanceObjectReturnUrl(storeObj: string): Promise { try { - // Set default to false, since currently our invitation are not multi-use - const persistent: boolean = false; - //nats call in agent-service to create an invitation url - const pattern = { cmd: 'store-object-return-url' }; - const payload = { persistent, storeObj }; - const message = await this.natsCall(pattern, payload); - return message.response; - } catch (error) { - this.logger.error(`[storeIssuanceObjectReturnUrl] [NATS call]- error in storing object and returning url : ${JSON.stringify(error)}`); - throw error; - } + // Set default to false, since currently our invitation are not multi-use + const persistent: boolean = false; + //nats call in agent-service to create an invitation url + const pattern = { cmd: 'store-object-return-url' }; + const payload = { persistent, storeObj }; + const message = await this.natsCall(pattern, payload); + return message.response; + } catch (error) { + this.logger.error( + `[storeIssuanceObjectReturnUrl] [NATS call]- error in storing object and returning url : ${JSON.stringify(error)}` + ); + throw error; + } } // Created this function to avoid the impact of actual "natsCall" function for other operations @@ -399,14 +454,16 @@ export class IssuanceService { try { const createOffer = await this.natsClient .send(this.issuanceServiceProxy, pattern, payload) - - .catch(error => { + + .catch((error) => { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( { status: error.statusCode, error: error.message - }, error.error); + }, + error.error + ); }); return createOffer; } catch (error) { @@ -415,25 +472,30 @@ export class IssuanceService { } } - async natsCall(pattern: object, payload: object): Promise<{ + async natsCall( + pattern: object, + payload: object + ): Promise<{ response: string; }> { try { return this.issuanceServiceProxy .send(pattern, payload) .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( { status: error.statusCode, error: error.message - }, error.error); + }, + error.error + ); }); } catch (error) { this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); @@ -447,7 +509,9 @@ export class IssuanceService { const payload: ISendOfferNatsPayload = { issueData, url, orgId }; return await this.natsCallAgent(pattern, payload); } catch (error) { - this.logger.error(`[_sendCredentialCreateOffer] [NATS call]- error in create credentials : ${JSON.stringify(error)}`); + this.logger.error( + `[_sendCredentialCreateOffer] [NATS call]- error in create credentials : ${JSON.stringify(error)}` + ); throw error; } } @@ -458,16 +522,14 @@ export class IssuanceService { issuedCredentialsSearchCriteria: IIssuedCredentialSearchParams ): Promise { try { - let schemaIds; if (issuedCredentialsSearchCriteria?.search) { - const schemaDetails = await this._getSchemaDetailsByName(issuedCredentialsSearchCriteria?.search, orgId); - + if (schemaDetails && 0 < schemaDetails?.length) { - schemaIds = schemaDetails.map(item => item?.schemaLedgerId); + schemaIds = schemaDetails.map((item) => item?.schemaLedgerId); } - } + } const getIssuedCredentialsList = await this.issuanceRepository.getAllIssuedCredentials( user, @@ -482,17 +544,17 @@ export class IssuanceService { let responseWithSchemaName; if (getSchemaDetails) { - responseWithSchemaName = getIssuedCredentialsList?.issuedCredentialsList.map(file => { - const schemaDetail = getSchemaDetails?.find(schema => schema.schemaLedgerId === file.schemaId); + responseWithSchemaName = getIssuedCredentialsList?.issuedCredentialsList.map((file) => { + const schemaDetail = getSchemaDetails?.find((schema) => schema.schemaLedgerId === file.schemaId); return { ...file, schemaName: schemaDetail?.name }; }); - } else { + } else { const getSchemaUrlDetails = await this.getSchemaUrlDetails(getSchemaIds); - responseWithSchemaName = getIssuedCredentialsList?.issuedCredentialsList.map(file => { - const schemaDetail = getSchemaUrlDetails?.find(schema => schema.title); + responseWithSchemaName = getIssuedCredentialsList?.issuedCredentialsList.map((file) => { + const schemaDetail = getSchemaUrlDetails?.find((schema) => schema.title); return { ...file, schemaName: schemaDetail?.title @@ -502,7 +564,8 @@ export class IssuanceService { const issuedCredentialsResponse: IIssuedCredential = { totalItems: getIssuedCredentialsList.issuedCredentialsCount, hasNextPage: - issuedCredentialsSearchCriteria.pageSize * issuedCredentialsSearchCriteria.pageNumber < getIssuedCredentialsList.issuedCredentialsCount, + issuedCredentialsSearchCriteria.pageSize * issuedCredentialsSearchCriteria.pageNumber < + getIssuedCredentialsList.issuedCredentialsCount, hasPreviousPage: 1 < issuedCredentialsSearchCriteria.pageNumber, nextPage: Number(issuedCredentialsSearchCriteria.pageNumber) + 1, previousPage: issuedCredentialsSearchCriteria.pageNumber - 1, @@ -523,10 +586,10 @@ export class IssuanceService { async _getSchemaDetailsByName(schemaName: string, orgId: string): Promise { const pattern = { cmd: 'get-schemas-details-by-name' }; - const payload = {schemaName, orgId}; + const payload = { schemaName, orgId }; const schemaDetails = await this.natsClient .send(this.issuanceServiceProxy, pattern, payload) - + .catch((error) => { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -543,21 +606,24 @@ export class IssuanceService { async getSchemaUrlDetails(schemaUrls: string[]): Promise { const results = []; - + for (const schemaUrl of schemaUrls) { - const schemaRequest = await this.commonService.httpGet(schemaUrl); - if (!schemaRequest) { - throw new NotFoundException(ResponseMessages.schema.error.W3CSchemaNotFOund, { - cause: new Error(), - description: ResponseMessages.errorMessages.notFound - }); - } - results.push(schemaRequest); + const schemaRequest = await this.commonService.httpGet(schemaUrl); + if (!schemaRequest) { + throw new NotFoundException(ResponseMessages.schema.error.W3CSchemaNotFOund, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + results.push(schemaRequest); } return results; -} + } - async _getIssueCredentials(url: string, apiKey: string): Promise<{ + async _getIssueCredentials( + url: string, + apiKey: string + ): Promise<{ response: string; }> { try { @@ -570,9 +636,12 @@ export class IssuanceService { } } - async getIssueCredentialsbyCredentialRecordId(user: IUserRequest, credentialRecordId: string, orgId: string): Promise { + async getIssueCredentialsbyCredentialRecordId( + user: IUserRequest, + credentialRecordId: string, + orgId: string + ): Promise { try { - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); // const platformConfig: platform_config = await this.issuanceRepository.getPlatformConfigDetails(); @@ -583,19 +652,28 @@ export class IssuanceService { const orgAgentType = await this.issuanceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); const issuanceMethodLabel = 'get-issue-credential-by-credential-id'; - const url = await this.getAgentUrl(issuanceMethodLabel, orgAgentType, agentEndPoint, agentDetails?.tenantId, credentialRecordId); - + const url = await this.getAgentUrl( + issuanceMethodLabel, + orgAgentType, + agentEndPoint, + agentDetails?.tenantId, + credentialRecordId + ); const createConnectionInvitation = await this._getIssueCredentialsbyCredentialRecordId(url, orgId); return createConnectionInvitation?.response; } catch (error) { - this.logger.error(`[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}`); + this.logger.error( + `[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}` + ); if (error && error?.status && error?.status?.message && error?.status?.message?.error) { throw new RpcException({ - message: error?.status?.message?.error?.reason || error?.status?.message?.error?.message || error?.status?.message?.error, + message: + error?.status?.message?.error?.reason || + error?.status?.message?.error?.message || + error?.status?.message?.error, statusCode: error?.status?.code }); - } else { throw new RpcException(error.response ? error.response : error); } @@ -607,160 +685,170 @@ export class IssuanceService { const agentDetails = await this.issuanceRepository.saveIssuedCredentialDetails(payload); return agentDetails; } catch (error) { - this.logger.error(`[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}`); + this.logger.error( + `[getIssueCredentialsbyCredentialRecordId] - error in get credentials : ${JSON.stringify(error)}` + ); throw new RpcException(error.response ? error.response : error); } } - async _getIssueCredentialsbyCredentialRecordId(url: string, orgId: string): Promise<{ + async _getIssueCredentialsbyCredentialRecordId( + url: string, + orgId: string + ): Promise<{ response: string; }> { try { const pattern = { cmd: 'agent-get-issued-credentials-by-credentialDefinitionId' }; const payload = { url, orgId }; return await this.natsCall(pattern, payload); - } catch (error) { - this.logger.error(`[_getIssueCredentialsbyCredentialRecordId] [NATS call]- error in fetch credentials : ${JSON.stringify(error)}`); + this.logger.error( + `[_getIssueCredentialsbyCredentialRecordId] [NATS call]- error in fetch credentials : ${JSON.stringify(error)}` + ); throw error; } } -async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayload, platformName?: string, organizationLogoUrl?: string, prettyVc?: IPrettyVc, isValidateSchema?: boolean): Promise { - try { - const { - credentialOffer, - comment, - credentialDefinitionId, - orgId, - protocolVersion, - attributes, - emailId, - credentialType, - isReuseConnection - } = outOfBandCredential; - - if (IssueCredentialType.JSONLD === credentialType) { - await validateAndUpdateIssuanceDates(credentialOffer); - } + async outOfBandCredentialOffer( + outOfBandCredential: OutOfBandCredentialOfferPayload, + platformName?: string, + organizationLogoUrl?: string, + prettyVc?: IPrettyVc, + isValidateSchema?: boolean + ): Promise { + try { + const { + credentialOffer, + comment, + credentialDefinitionId, + orgId, + protocolVersion, + attributes, + emailId, + credentialType, + isReuseConnection + } = outOfBandCredential; + + if (IssueCredentialType.JSONLD === credentialType) { + await validateAndUpdateIssuanceDates(credentialOffer); + } - if (IssueCredentialType.INDY === credentialType) { - const schemaResponse: SchemaDetails = await this.issuanceRepository.getCredentialDefinitionDetails( - credentialDefinitionId - ); + if (IssueCredentialType.INDY === credentialType) { + const schemaResponse: SchemaDetails = + await this.issuanceRepository.getCredentialDefinitionDetails(credentialDefinitionId); - let attributesArray: IAttributes[] = []; - if (schemaResponse?.attributes) { - attributesArray = JSON.parse(schemaResponse.attributes); - } + let attributesArray: IAttributes[] = []; + if (schemaResponse?.attributes) { + attributesArray = JSON.parse(schemaResponse.attributes); + } - if (0 < attributes?.length) { - const attrError = []; - attributesArray.forEach((schemaAttribute, i) => { - if (schemaAttribute.isRequired) { - const attribute = attributes.find((attribute) => attribute.name === schemaAttribute.attributeName); - if (!attribute?.value) { - attrError.push(`attributes.${i}.Attribute ${schemaAttribute.attributeName} is required`); + if (0 < attributes?.length) { + const attrError = []; + attributesArray.forEach((schemaAttribute, i) => { + if (schemaAttribute.isRequired) { + const attribute = attributes.find((attribute) => attribute.name === schemaAttribute.attributeName); + if (!attribute?.value) { + attrError.push(`attributes.${i}.Attribute ${schemaAttribute.attributeName} is required`); + } } + }); + if (0 < attrError.length) { + throw new BadRequestException(attrError); } - }); - if (0 < attrError.length) { - throw new BadRequestException(attrError); } - } - if (0 < credentialOffer?.length) { - const credefError = []; - credentialOffer.forEach((credentialAttribute, index) => { - attributesArray.forEach((schemaAttribute, i) => { - const attribute = credentialAttribute.attributes.find( - (attribute) => attribute.name === schemaAttribute.attributeName - ); - - if (schemaAttribute.isRequired && !attribute?.value) { - credefError.push( - `credentialOffer.${index}.attributes.${i}.Attribute ${schemaAttribute.attributeName} is required` + if (0 < credentialOffer?.length) { + const credefError = []; + credentialOffer.forEach((credentialAttribute, index) => { + attributesArray.forEach((schemaAttribute, i) => { + const attribute = credentialAttribute.attributes.find( + (attribute) => attribute.name === schemaAttribute.attributeName ); - } + + if (schemaAttribute.isRequired && !attribute?.value) { + credefError.push( + `credentialOffer.${index}.attributes.${i}.Attribute ${schemaAttribute.attributeName} is required` + ); + } + }); }); - }); - if (0 < credefError.length) { - throw new BadRequestException(credefError); + if (0 < credefError.length) { + throw new BadRequestException(credefError); + } } } - } - const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); + const agentDetails = await this.issuanceRepository.getAgentEndPoint(orgId); - const { organisation } = agentDetails; - if (!agentDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); - } - const orgAgentType = await this.issuanceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); - - const issuanceMethodLabel = 'create-offer-oob'; - const url = await this.getAgentUrl( - issuanceMethodLabel, - orgAgentType, - agentDetails.agentEndPoint, - agentDetails.tenantId - ); - const organizationDetails = await this.issuanceRepository.getOrganization(orgId); - - if (!organizationDetails) { - throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); - } - const errors = []; - let credentialOfferResponse; - const arraycredentialOfferResponse = []; - const sendEmailCredentialOffer: { - iterator: CredentialOffer; - emailId: string; - index: number; - credentialType: IssueCredentialType; - protocolVersion: string; - isReuseConnection?: boolean; - attributes: IAttributes[]; - credentialDefinitionId: string; - outOfBandCredential: OutOfBandCredentialOfferPayload; - comment: string; - organisation: organisation; - errors: string[]; - url: string; - orgId: string; - organizationDetails: organisation; - platformName?: string; - organizationLogoUrl?: string; - prettyVc?: IPrettyVc; - isValidateSchema?: boolean; - } = { - credentialType, - protocolVersion, - isReuseConnection, - attributes, - credentialDefinitionId, - outOfBandCredential, - comment, - organisation, - errors, - url, - orgId, - isValidateSchema, - organizationDetails, - iterator: undefined, - emailId: emailId || '', - index: 0, - platformName: platformName || null, - organizationLogoUrl: organizationLogoUrl || null, - prettyVc: { - certificate: prettyVc?.certificate, - size: prettyVc?.size, - orientation: prettyVc?.orientation, - height: prettyVc?.height, - width: prettyVc?.width + const { organisation } = agentDetails; + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); } - }; + const orgAgentType = await this.issuanceRepository.getOrgAgentType(agentDetails?.orgAgentTypeId); - if (credentialOffer) { + const issuanceMethodLabel = 'create-offer-oob'; + const url = await this.getAgentUrl( + issuanceMethodLabel, + orgAgentType, + agentDetails.agentEndPoint, + agentDetails.tenantId + ); + const organizationDetails = await this.issuanceRepository.getOrganization(orgId); + if (!organizationDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.organizationNotFound); + } + const errors = []; + let credentialOfferResponse; + const arraycredentialOfferResponse = []; + const sendEmailCredentialOffer: { + iterator: CredentialOffer; + emailId: string; + index: number; + credentialType: IssueCredentialType; + protocolVersion: string; + isReuseConnection?: boolean; + attributes: IAttributes[]; + credentialDefinitionId: string; + outOfBandCredential: OutOfBandCredentialOfferPayload; + comment: string; + organisation: organisation; + errors: string[]; + url: string; + orgId: string; + organizationDetails: organisation; + platformName?: string; + organizationLogoUrl?: string; + prettyVc?: IPrettyVc; + isValidateSchema?: boolean; + } = { + credentialType, + protocolVersion, + isReuseConnection, + attributes, + credentialDefinitionId, + outOfBandCredential, + comment, + organisation, + errors, + url, + orgId, + isValidateSchema, + organizationDetails, + iterator: undefined, + emailId: emailId || '', + index: 0, + platformName: platformName || null, + organizationLogoUrl: organizationLogoUrl || null, + prettyVc: { + certificate: prettyVc?.certificate, + size: prettyVc?.size, + orientation: prettyVc?.orientation, + height: prettyVc?.height, + width: prettyVc?.width + } + }; + + if (credentialOffer) { for (const [index, iterator] of credentialOffer.entries()) { sendEmailCredentialOffer['iterator'] = iterator; sendEmailCredentialOffer['emailId'] = iterator.emailId; @@ -770,209 +858,214 @@ async outOfBandCredentialOffer(outOfBandCredential: OutOfBandCredentialOfferPayl const sendOobOffer = await this.sendEmailForCredentialOffer(sendEmailCredentialOffer); arraycredentialOfferResponse.push(sendOobOffer); - } - if (0 < errors.length) { - throw errors; - } - - return arraycredentialOfferResponse.every((result) => true === result); - } else { - credentialOfferResponse = await this.sendEmailForCredentialOffer(sendEmailCredentialOffer); - return credentialOfferResponse; - } - } catch (error) { - this.logger.error( - `[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}` - ); - if (0 < error?.length) { - const errorStack = error?.map((item) => { - const { statusCode, message, error } = item?.error || item?.response || {}; - return { - statusCode, - message, - error - }; - }); - throw new RpcException({ - error: errorStack, - statusCode: error?.status?.code, - message: ResponseMessages.issuance.error.unableToCreateOOBOffer - }); - } else { - throw new RpcException(error.response ? error.response : error); - } - } -} + } + if (0 < errors.length) { + throw errors; + } -async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialOffer): Promise { - const { - iterator, - emailId, - index, - credentialType, - protocolVersion, - attributes, - credentialDefinitionId, - outOfBandCredential, - comment, - organisation, - errors, - url, - orgId, - organizationDetails, - platformName, - organizationLogoUrl, - isReuseConnection, - isValidateSchema - } = sendEmailCredentialOffer; - const iterationNo = index + 1; - try { - - - let invitationDid: string | undefined; - if (true === isReuseConnection) { - const data: agent_invitations[] = await this.issuanceRepository.getInvitationDidByOrgId(orgId); - if (data && 0 < data.length) { - const [firstElement] = data; - invitationDid = firstElement?.invitationDid ?? undefined; - } + return arraycredentialOfferResponse.every((result) => true === result); + } else { + credentialOfferResponse = await this.sendEmailForCredentialOffer(sendEmailCredentialOffer); + return credentialOfferResponse; + } + } catch (error) { + this.logger.error( + `[outOfBoundCredentialOffer] - error in create out-of-band credentials: ${JSON.stringify(error)}` + ); + if (0 < error?.length) { + const errorStack = error?.map((item) => { + const { statusCode, message, error } = item?.error || item?.response || {}; + return { + statusCode, + message, + error + }; + }); + throw new RpcException({ + error: errorStack, + statusCode: error?.status?.code, + message: ResponseMessages.issuance.error.unableToCreateOOBOffer + }); + } else { + throw new RpcException(error.response ? error.response : error); + } } + } - let outOfBandIssuancePayload; - if (IssueCredentialType.INDY === credentialType) { - - outOfBandIssuancePayload = { - protocolVersion: protocolVersion || 'v1', - credentialFormats: { - indy: { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - attributes: attributes ? attributes : iterator.attributes.map(({ isRequired, ...rest }) => rest), - credentialDefinitionId - } - }, - autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', - comment, - goalCode: outOfBandCredential.goalCode || undefined, - parentThreadId: outOfBandCredential.parentThreadId || undefined, - willConfirm: outOfBandCredential.willConfirm || undefined, - label: organisation?.name, - imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl, - invitationDid: invitationDid || undefined - }; - } + async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialOffer): Promise { + const { + iterator, + emailId, + index, + credentialType, + protocolVersion, + attributes, + credentialDefinitionId, + outOfBandCredential, + comment, + organisation, + errors, + url, + orgId, + organizationDetails, + platformName, + organizationLogoUrl, + isReuseConnection, + isValidateSchema + } = sendEmailCredentialOffer; + const iterationNo = index + 1; + try { + let invitationDid: string | undefined; + if (true === isReuseConnection) { + const data: agent_invitations[] = await this.issuanceRepository.getInvitationDidByOrgId(orgId); + if (data && 0 < data.length) { + const [firstElement] = data; + invitationDid = firstElement?.invitationDid ?? undefined; + } + } - if (IssueCredentialType.JSONLD === credentialType) { - outOfBandIssuancePayload = { - protocolVersion: 'v2', - credentialFormats: { - jsonld: { - credential: iterator.credential, - options: iterator.options - } - }, - // For Educreds - autoAcceptCredential: AutoAccept.Always, - comment, - goalCode: outOfBandCredential.goalCode || undefined, - parentThreadId: outOfBandCredential.parentThreadId || undefined, - willConfirm: outOfBandCredential.willConfirm || undefined, - label: organisation?.name, - imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl, - invitationDid: invitationDid || undefined - }; + let outOfBandIssuancePayload; + if (IssueCredentialType.INDY === credentialType) { + outOfBandIssuancePayload = { + protocolVersion: protocolVersion || 'v1', + credentialFormats: { + indy: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + attributes: attributes ? attributes : iterator.attributes.map(({ isRequired, ...rest }) => rest), + credentialDefinitionId + } + }, + autoAcceptCredential: outOfBandCredential.autoAcceptCredential || 'always', + comment, + goalCode: outOfBandCredential.goalCode || undefined, + parentThreadId: outOfBandCredential.parentThreadId || undefined, + willConfirm: outOfBandCredential.willConfirm || undefined, + label: organisation?.name, + imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl, + invitationDid: invitationDid || undefined + }; + } - const payloadAttributes = outOfBandIssuancePayload?.credentialFormats?.jsonld?.credential?.credentialSubject; + if (IssueCredentialType.JSONLD === credentialType) { + outOfBandIssuancePayload = { + protocolVersion: 'v2', + credentialFormats: { + jsonld: { + credential: iterator.credential, + options: iterator.options + } + }, + // For Educreds + autoAcceptCredential: AutoAccept.Always, + comment, + goalCode: outOfBandCredential.goalCode || undefined, + parentThreadId: outOfBandCredential.parentThreadId || undefined, + willConfirm: outOfBandCredential.willConfirm || undefined, + label: organisation?.name, + imageUrl: organisation?.logoUrl || outOfBandCredential?.imageUrl, + invitationDid: invitationDid || undefined + }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { id, ...filteredIssuanceAttributes } = payloadAttributes; + const payloadAttributes = outOfBandIssuancePayload?.credentialFormats?.jsonld?.credential?.credentialSubject; - const schemaServerUrl = outOfBandIssuancePayload?.credentialFormats?.jsonld?.credential?.['@context']?.[1]; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, ...filteredIssuanceAttributes } = payloadAttributes; - const schemaUrlAttributes = await this.getW3CSchemaAttributes(schemaServerUrl); + const schemaServerUrl = outOfBandIssuancePayload?.credentialFormats?.jsonld?.credential?.['@context']?.[1]; - if (isValidateSchema) { - validateW3CSchemaAttributes(filteredIssuanceAttributes, schemaUrlAttributes); + const schemaUrlAttributes = await this.getW3CSchemaAttributes(schemaServerUrl); + + if (isValidateSchema) { + validateW3CSchemaAttributes(filteredIssuanceAttributes, schemaUrlAttributes); + } } - } - const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, orgId); + const credentialCreateOfferDetails = await this._outOfBandCredentialOffer(outOfBandIssuancePayload, url, orgId); - if (!credentialCreateOfferDetails) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound)); - return false; - } + if (!credentialCreateOfferDetails) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.credentialOfferNotFound)); + return false; + } - const invitationUrl: string = credentialCreateOfferDetails.response?.invitationUrl; - const shortenUrl: string = await this.storeIssuanceObjectReturnUrl(invitationUrl); + const invitationUrl: string = credentialCreateOfferDetails.response?.invitationUrl; + const shortenUrl: string = await this.storeIssuanceObjectReturnUrl(invitationUrl); - const deepLinkURL = convertUrlToDeepLinkUrl(shortenUrl); + const deepLinkURL = convertUrlToDeepLinkUrl(shortenUrl); - if (!invitationUrl) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.invitationNotFound)); - return false; - } - const qrCodeOptions = { type: 'image/png' }; - const outOfBandIssuanceQrCode = await QRCode.toDataURL(shortenUrl, qrCodeOptions); - const platformConfigData = await this.issuanceRepository.getPlatformConfigDetails(); - if (!platformConfigData) { - errors.push(new NotFoundException(ResponseMessages.issuance.error.platformConfigNotFound)); - return false; + if (!invitationUrl) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.invitationNotFound)); + return false; + } + const qrCodeOptions = { type: 'image/png' }; + const outOfBandIssuanceQrCode = await QRCode.toDataURL(shortenUrl, qrCodeOptions); + const platformConfigData = await this.issuanceRepository.getPlatformConfigDetails(); + if (!platformConfigData) { + errors.push(new NotFoundException(ResponseMessages.issuance.error.platformConfigNotFound)); + return false; + } + this.emailData.emailFrom = platformConfigData?.emailFrom; + this.emailData.emailTo = iterator?.emailId ?? emailId; + const platform = platformName || process.env.PLATFORM_NAME; + this.emailData.emailSubject = `${platform} Platform: Issuance of Your Credential`; + this.emailData.emailHtml = this.outOfBandIssuance.outOfBandIssuance( + emailId, + organizationDetails.name, + deepLinkURL, + platformName, + organizationLogoUrl + ); + this.emailData.emailAttachments = [ + { + filename: 'qrcode.png', + content: outOfBandIssuanceQrCode.split(';base64,')[1], + contentType: 'image/png', + disposition: 'attachment' } - this.emailData.emailFrom = platformConfigData?.emailFrom; - this.emailData.emailTo = iterator?.emailId ?? emailId; - const platform = platformName || process.env.PLATFORM_NAME; - this.emailData.emailSubject = `${platform} Platform: Issuance of Your Credential`; - this.emailData.emailHtml = this.outOfBandIssuance.outOfBandIssuance(emailId, organizationDetails.name, deepLinkURL, platformName, organizationLogoUrl); - this.emailData.emailAttachments = [ - { - filename: 'qrcode.png', - content: outOfBandIssuanceQrCode.split(';base64,')[1], - contentType: 'image/png', - disposition: 'attachment' - } - ]; + ]; + const isEmailSent = await sendEmail(this.emailData); - const isEmailSent = await sendEmail(this.emailData); - - this.logger.log(`isEmailSent ::: ${JSON.stringify(isEmailSent)}-${this.counter}`); - this.counter++; - if (!isEmailSent) { - errors.push(new InternalServerErrorException(ResponseMessages.issuance.error.emailSend)); - return false; - } + this.logger.log(`isEmailSent ::: ${JSON.stringify(isEmailSent)}-${this.counter}`); + this.counter++; + if (!isEmailSent) { + errors.push(new InternalServerErrorException(ResponseMessages.issuance.error.emailSend)); + return false; + } - return isEmailSent; - - } catch (error) { - const iterationNoMessage = ` at position ${iterationNo}`; - this.logger.error('[OUT-OF-BAND CREATE OFFER - SEND EMAIL]::', JSON.stringify(error)); - const errorStack = error?.status?.message; - if (errorStack) { - errors.push( - new RpcException({ - statusCode: errorStack?.statusCode, - message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}`, - error: `${errorStack?.error?.message} at position ${iterationNo}` - }) - ); + return isEmailSent; + } catch (error) { + const iterationNoMessage = ` at position ${iterationNo}`; + this.logger.error('[OUT-OF-BAND CREATE OFFER - SEND EMAIL]::', JSON.stringify(error)); + const errorStack = error?.status?.message; + if (errorStack) { + errors.push( + new RpcException({ + statusCode: errorStack?.statusCode, + message: `${ResponseMessages.issuance.error.walletError} at position ${iterationNo}`, + error: `${errorStack?.error?.message} at position ${iterationNo}` + }) + ); - error.status.message = `${error.status.message}${iterationNoMessage}`; + error.status.message = `${error.status.message}${iterationNoMessage}`; throw error; - } else { - errors.push( - new RpcException({ - statusCode: error?.response?.statusCode, - message: `${error?.response?.message} at position ${iterationNo}`, - error: error?.response?.error - }) - ); - error.response.message = `${error.response.message}${iterationNoMessage}`; - throw error; // Check With other issuance flow + } else { + errors.push( + new RpcException({ + statusCode: error?.response?.statusCode, + message: `${error?.response?.message} at position ${iterationNo}`, + error: error?.response?.error + }) + ); + error.response.message = `${error.response.message}${iterationNoMessage}`; + throw error; // Check With other issuance flow + } } } -} - async _outOfBandCredentialOffer(outOfBandIssuancePayload: object, url: string, orgId: string): Promise<{ + async _outOfBandCredentialOffer( + outOfBandIssuancePayload: object, + url: string, + orgId: string + ): Promise<{ response; }> { try { @@ -998,43 +1091,47 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO credentialRecordId?: string ): Promise { try { - let url; switch (issuanceMethodLabel) { case 'create-offer': { - url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_ISSUE_CREATE_CRED_OFFER_AFJ}` - : orgAgentType === OrgAgentType.SHARED - ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER}`.replace('#', tenantId) - : null; + url = + orgAgentType === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_CREATE_CRED_OFFER_AFJ}` + : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER}`.replace('#', tenantId) + : null; break; } case 'create-offer-oob': { - url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_OUT_OF_BAND_CREDENTIAL_OFFER}` - : orgAgentType === OrgAgentType.SHARED - ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER_OUT_OF_BAND}`.replace('#', tenantId) - : null; + url = + orgAgentType === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_OUT_OF_BAND_CREDENTIAL_OFFER}` + : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_CREATE_OFFER_OUT_OF_BAND}`.replace('#', tenantId) + : null; break; } case 'get-issue-credentials': { - url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ}` - : orgAgentType === OrgAgentType.SHARED - ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS}`.replace('#', tenantId) - : null; + url = + orgAgentType === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ}` + : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS}`.replace('#', tenantId) + : null; break; } case 'get-issue-credential-by-credential-id': { - - url = orgAgentType === OrgAgentType.DEDICATED - ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ_BY_CRED_REC_ID}/${credentialRecordId}` - : orgAgentType === OrgAgentType.SHARED - ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS_BY_CREDENTIAL_ID}`.replace('#', credentialRecordId).replace('@', tenantId) - : null; + url = + orgAgentType === OrgAgentType.DEDICATED + ? `${agentEndPoint}${CommonConstants.URL_ISSUE_GET_CREDS_AFJ_BY_CRED_REC_ID}/${credentialRecordId}` + : orgAgentType === OrgAgentType.SHARED + ? `${agentEndPoint}${CommonConstants.URL_SHAGENT_GET_CREDENTIALS_BY_CREDENTIAL_ID}` + .replace('#', credentialRecordId) + .replace('@', tenantId) + : null; break; } @@ -1059,44 +1156,43 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO let schemaResponse: SchemaDetails; let fileName: string; - const {schemaType, templateId} = templateDetails; + const { schemaType, templateId } = templateDetails; if (!templateId) { throw new BadRequestException(ResponseMessages.bulkIssuance.error.invalidtemplateId); } const timestamp = Math.floor(Date.now() / 1000); - + if (schemaType === SchemaType.INDY) { - schemaResponse = await this.issuanceRepository.getCredentialDefinitionDetails(templateId); - if (!schemaResponse) { + schemaResponse = await this.issuanceRepository.getCredentialDefinitionDetails(templateId); + if (!schemaResponse) { throw new NotFoundException(ResponseMessages.bulkIssuance.error.invalidIdentifier); } - fileName = `${schemaResponse.tag}-${timestamp}.csv`; - + fileName = `${schemaResponse.tag}-${timestamp}.csv`; } else if (schemaType === SchemaType.W3C_Schema) { const schemaDetails = await this.issuanceRepository.getSchemaDetailsBySchemaIdentifier(templateId); - const {attributes, schemaLedgerId, name} = schemaDetails; + const { attributes, schemaLedgerId, name } = schemaDetails; schemaResponse = { attributes, schemaLedgerId, name }; if (!schemaResponse) { throw new NotFoundException(ResponseMessages.bulkIssuance.error.invalidIdentifier); } - fileName = `${schemaResponse.name}-${timestamp}.csv`; - } - + fileName = `${schemaResponse.name}-${timestamp}.csv`; + } + const attributesArray = JSON.parse(schemaResponse.attributes); - + const csvFields: string[] = [TemplateIdentifier.EMAIL_COLUMN]; - + const flattendData = extractAttributeNames(attributesArray); csvFields.push(...flattendData); const jsonData = []; - + if (!csvFields.length) { // eslint-disable-next-line prefer-promise-reject-errors return Promise.reject('Unable to transform schema data for CSV.'); } - + const csv = parse(jsonData, { fields: csvFields }); const filePath = join(process.cwd(), `uploadedFiles/exports`); await createFile(filePath, fileName, csv); @@ -1116,7 +1212,6 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO } } - async uploadCSVTemplate(importFileDetails: ImportFileDetails, requestId?: string): Promise { try { let credentialDetails; @@ -1153,8 +1248,8 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO transformheader: (header) => header.toLowerCase().replace('#', '').trim(), complete: (results) => results.data }); - - const nestedObject = parsedData.data.map(row => unflattenCsvRow(row)); + + const nestedObject = parsedData.data.map((row) => unflattenCsvRow(row)); if (0 >= parsedData.data.length) { throw new BadRequestException(ResponseMessages.bulkIssuance.error.emptyFile); @@ -1168,7 +1263,7 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO if (0 < invalidEmails.length) { throw new BadRequestException(ResponseMessages.bulkIssuance.error.invalidEmails); } - + const fileData: string[][] = nestedObject.map(Object.values); const fileHeader: string[] = parsedData.meta.fields; const attributesArray = JSON.parse(credentialDetails.attributes); @@ -1185,7 +1280,7 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO validatedData = parsedData.data.map((row) => { const { email_identifier, ...rest } = row; const newRow = { ...rest }; - + attributesArray.forEach((attr) => { if (!(attr?.attributeName in newRow)) { throw new BadRequestException(`Missing attribute ${attr?.attributeName} in CSV data`); @@ -1199,14 +1294,14 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO newRow[attr?.attributeName] = String(newRow[attr?.attributeName]); } }); - + return { email_identifier, ...newRow }; }); } else if (type === SchemaType.W3C_Schema && !isValidateSchema) { validatedData = nestedObject.map((row) => { const { email_identifier, ...rest } = row; const newRow = { ...rest }; - + return { email_identifier, ...newRow }; }); } @@ -1225,22 +1320,17 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO credentialPayload.fileData = type === SchemaType.W3C_Schema ? finalFileData : parsedData; credentialPayload.fileName = fileName; const newCacheKey = uuidv4(); - const cacheTTL = Number(process.env.FILEUPLOAD_CACHE_TTL) || CommonConstants.DEFAULT_CACHE_TTL; + const cacheTTL = Number(process.env.FILEUPLOAD_CACHE_TTL) || CommonConstants.DEFAULT_CACHE_TTL; await this.cacheManager.set(requestId || newCacheKey, JSON.stringify(credentialPayload), cacheTTL); - - return newCacheKey; -} catch (error) { + return newCacheKey; + } catch (error) { this.logger.error(`error in validating credentials : ${error.response}`); throw new RpcException(error.response ? error.response : error); } } - - async previewFileDataForIssuance( - requestId: string, - previewRequest: PreviewRequest - ): Promise { + async previewFileDataForIssuance(requestId: string, previewRequest: PreviewRequest): Promise { try { if ('' !== requestId.trim()) { const cachedData = await this.cacheManager.get(requestId); @@ -1251,19 +1341,20 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO throw new BadRequestException(ResponseMessages.issuance.error.previewCachedData); } const parsedData = JSON.parse(cachedData as string).fileData.data; - + // Apply search to the entire dataset if searchByText is provided let filteredData = parsedData; if (previewRequest.searchByText) { const searchTerm = previewRequest.searchByText.toLowerCase(); - filteredData = parsedData.filter(item => item.email_identifier.toLowerCase().includes(searchTerm) || - item.name.toLowerCase().includes(searchTerm) + filteredData = parsedData.filter( + (item) => + item.email_identifier.toLowerCase().includes(searchTerm) || item.name.toLowerCase().includes(searchTerm) ); } - + // Apply pagination to the filtered data const finalData = paginator(filteredData, previewRequest.pageNumber, previewRequest.pageSize); - + return finalData; } else { throw new BadRequestException(ResponseMessages.issuance.error.previewFile); @@ -1273,14 +1364,9 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO throw new RpcException(error.response); } } - - async getFileDetailsByFileId( - fileId: string, - getAllfileDetails: PreviewRequest - ): Promise { + async getFileDetailsByFileId(fileId: string, getAllfileDetails: PreviewRequest): Promise { try { - const fileData = await this.issuanceRepository.getFileDetailsByFileId(fileId, getAllfileDetails); const fileResponse = { @@ -1298,30 +1384,27 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO } else { throw new NotFoundException(ResponseMessages.issuance.error.fileNotFound); } - } catch (error) { this.logger.error(`error in issuedFileDetails : ${error}`); throw new RpcException(error.response); } } - async issuedFileDetails( - orgId: string, - getAllfileDetails: PreviewRequest - ): Promise { + async issuedFileDetails(orgId: string, getAllfileDetails: PreviewRequest): Promise { try { - const fileDetails = await this.issuanceRepository.getAllFileDetails(orgId, getAllfileDetails); - const templateIds = fileDetails?.fileList.map(file => file.templateId); + const templateIds = fileDetails?.fileList.map((file) => file.templateId); const getSchemaDetails = await this._getSchemaDetails(templateIds); - const fileListWithSchema = fileDetails?.fileList.map(file => { - const schemaDetail = getSchemaDetails?.find(schema => schema.schemaLedgerId === file.templateId); + const fileListWithSchema = fileDetails?.fileList.map((file) => { + const schemaDetail = getSchemaDetails?.find((schema) => schema.schemaLedgerId === file.templateId); return { ...file, - schema: schemaDetail ? { name: schemaDetail.name, version: schemaDetail.version, schemaType: schemaDetail.type } : null + schema: schemaDetail + ? { name: schemaDetail.name, version: schemaDetail.version, schemaType: schemaDetail.type } + : null }; }); @@ -1340,7 +1423,6 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO } else { throw new NotFoundException(ResponseMessages.issuance.error.notFound); } - } catch (error) { this.logger.error(`error in issuedFileDetails : ${error}`); throw new RpcException(error.response); @@ -1355,7 +1437,7 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO }; const schemaDetails = await this.natsClient .send(this.issuanceServiceProxy, pattern, payload) - + .catch((error) => { this.logger.error(`catch: ${JSON.stringify(error)}`); throw new HttpException( @@ -1369,21 +1451,20 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO return schemaDetails; } - async delay(ms): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Processes bulk payload in batches and adds jobs to the queue. * @param bulkPayload - * @param clientDetails + * @param clientDetails * @param orgId * @param requestId */ - + private async processInBatches(bulkPayload, bulkPayloadDetails: BulkPayloadDetails): Promise { - const {clientId, isRetry, orgId, requestId, isValidateSchema } = bulkPayloadDetails; + const { clientId, isRetry, orgId, requestId, isValidateSchema } = bulkPayloadDetails; const delay = (ms: number): Promise => new Promise((resolve) => setTimeout(resolve, ms)); const batchSize = CommonConstants.ISSUANCE_BATCH_SIZE; // initial 1000 const uniqueJobId = uuidv4(); @@ -1430,7 +1511,7 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO this.logger.log(`Processing batch ${batchIndex + 1} with ${batch.length} items.`); // Execute the batched jobs with limited concurrency - await Promise.all(queueJobsArray.map(job => limit(() => job))); + await Promise.all(queueJobsArray.map((job) => limit(() => job))); return queueJobsArray; }; @@ -1439,13 +1520,13 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO for (const batch of createBatches(bulkPayload, batchSize)) { const resolvedBatchJobs = await processBatch(batch, batchIndex); - this.logger.log("Adding resolved jobs to the queue:", resolvedBatchJobs); + this.logger.log('Adding resolved jobs to the queue:', resolvedBatchJobs); await this.bulkIssuanceQueue.addBulk(resolvedBatchJobs); batchIndex++; // Wait for 60 seconds before processing the next batch, if more batches are remaining - if ((batchIndex * batchSize) < bulkPayload.length) { + if (batchIndex * batchSize < bulkPayload.length) { await delay(CommonConstants.ISSUANCE_BATCH_DELAY); } } @@ -1532,7 +1613,6 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO } try { - const bulkPayloadDetails: BulkPayloadDetails = { clientId: clientDetails.clientId, orgId, @@ -1548,7 +1628,7 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO width: clientDetails?.width }; - this.processInBatches(bulkPayload, bulkPayloadDetails); + this.processInBatches(bulkPayload, bulkPayloadDetails); } catch (error) { this.logger.error(`Error processing issuance data: ${error}`); } @@ -1566,7 +1646,12 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO } } - async retryBulkCredential(fileId: string, orgId: string, clientDetails: IClientDetails, isValidateSchema?: boolean): Promise { + async retryBulkCredential( + fileId: string, + orgId: string, + clientDetails: IClientDetails, + isValidateSchema?: boolean + ): Promise { let bulkpayloadRetry; try { const fileDetails = await this.issuanceRepository.getFileDetailsById(fileId); @@ -1578,10 +1663,10 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO const errorMessage = ResponseMessages.bulkIssuance.error.fileDetailsNotFound; throw new BadRequestException(`${errorMessage}`); } - + try { const bulkPayloadDetails: BulkPayloadDetails = { - clientId : clientDetails.clientId, + clientId: clientDetails.clientId, orgId, isRetry: true, isValidateSchema, @@ -1594,19 +1679,18 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO width: clientDetails?.width }; this.processInBatches(bulkpayloadRetry, bulkPayloadDetails); - } catch (error) { - this.logger.error(`Error processing issuance data: ${error}`); - } - + } catch (error) { + this.logger.error(`Error processing issuance data: ${error}`); + } + return ResponseMessages.bulkIssuance.success.reinitiated; } catch (error) { throw new RpcException(error.response ? error.response : error); } } - async processIssuanceData(jobDetails: IQueuePayload): Promise { - const {jobId, totalJobs} = jobDetails; + const { jobId, totalJobs } = jobDetails; if (!this.processedJobsCounters[jobId]) { this.processedJobsCounters[jobId] = 0; } @@ -1652,44 +1736,48 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO isReuseConnection: true }; for (const key in jobDetails?.credential_data) { - if (jobDetails.credential_data.hasOwnProperty(key) && TemplateIdentifier.EMAIL_COLUMN !== key) { const value = jobDetails?.credential_data[key]; oobIssuancepayload.attributes.push({ name: key, value }); } } } else if (jobDetails.credentialType === SchemaType.W3C_Schema) { - const schemaDetails = await this.issuanceRepository.getSchemaDetailsBySchemaIdentifier(jobDetails.schemaLedgerId); - const {name, schemaLedgerId} = schemaDetails; - const JsonldCredentialDetails: IJsonldCredential = { - schemaName : name, - schemaLedgerId, - credentialData: jobDetails.credential_data, - orgDid, - orgId, - isReuseConnection: true - }; + const schemaDetails = await this.issuanceRepository.getSchemaDetailsBySchemaIdentifier( + jobDetails.schemaLedgerId + ); + const { name, schemaLedgerId } = schemaDetails; + const JsonldCredentialDetails: IJsonldCredential = { + schemaName: name, + schemaLedgerId, + credentialData: jobDetails.credential_data, + orgDid, + orgId, + isReuseConnection: true + }; - prettyVc = { - certificate: jobDetails?.certificate, - size: jobDetails?.size, - orientation: jobDetails?.orientation, - height: jobDetails?.height, - width: jobDetails?.width - }; + prettyVc = { + certificate: jobDetails?.certificate, + size: jobDetails?.size, + orientation: jobDetails?.orientation, + height: jobDetails?.height, + width: jobDetails?.width + }; - oobIssuancepayload = await createOobJsonldIssuancePayload(JsonldCredentialDetails, prettyVc); + oobIssuancepayload = await createOobJsonldIssuancePayload(JsonldCredentialDetails, prettyVc); } const oobCredentials = await this.outOfBandCredentialOffer( - oobIssuancepayload, jobDetails?.platformName, jobDetails?.organizationLogoUrl, prettyVc, jobDetails?.isValidateSchema); + oobIssuancepayload, + jobDetails?.platformName, + jobDetails?.organizationLogoUrl, + prettyVc, + jobDetails?.isValidateSchema + ); if (oobCredentials) { await this.issuanceRepository.deleteFileDataByJobId(jobDetails.id); } } catch (error) { - this.logger.error( - `error in issuanceBulkCredential for data : ${JSON.stringify(error)}` - ); + this.logger.error(`error in issuanceBulkCredential for data : ${JSON.stringify(error)}`); fileUploadData.isError = true; fileUploadData.error = JSON.stringify(error.error) ? JSON.stringify(error.error) : JSON.stringify(error); fileUploadData.detailError = `${JSON.stringify(error)}`; @@ -1709,14 +1797,19 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO transports: ['websocket'] }); const errorCount = await this.issuanceRepository.countErrorsForFile(jobDetails.fileUploadId); - const status = - 0 === errorCount ? FileUploadStatus.completed : FileUploadStatus.partially_completed; + const status = 0 === errorCount ? FileUploadStatus.completed : FileUploadStatus.partially_completed; if (!jobDetails.isRetry) { - socket.emit('bulk-issuance-process-completed', { clientId: jobDetails.clientId, fileUploadId: jobDetails.fileUploadId }); + socket.emit('bulk-issuance-process-completed', { + clientId: jobDetails.clientId, + fileUploadId: jobDetails.fileUploadId + }); this.cacheManager.del(jobDetails.cacheId); } else { - socket.emit('bulk-issuance-process-retry-completed', { clientId: jobDetails.clientId, fileUploadId: jobDetails.fileUploadId }); + socket.emit('bulk-issuance-process-retry-completed', { + clientId: jobDetails.clientId, + fileUploadId: jobDetails.fileUploadId + }); } await this.issuanceRepository.updateFileUploadDetails(jobDetails.fileUploadId, { @@ -1735,12 +1828,15 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO }); if (!isErrorOccurred) { isErrorOccurred = true; - socket.emit('error-in-bulk-issuance-process', { clientId: jobDetails.clientId, error, fileUploadId: jobDetails.fileUploadId}); + socket.emit('error-in-bulk-issuance-process', { + clientId: jobDetails.clientId, + error, + fileUploadId: jobDetails.fileUploadId + }); } throw error; - } - return true; + return true; } async splitIntoBatches(array: T[], batchSize: number): Promise { @@ -1751,36 +1847,34 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO return batches; } - async validateFileHeaders( - fileHeader: string[], - schemaAttributes: string[] - ): Promise { + async validateFileHeaders(fileHeader: string[], schemaAttributes: string[]): Promise { try { const fileSchemaHeader: string[] = fileHeader.slice(); - if (TemplateIdentifier.EMAIL_COLUMN === fileHeader[0]) { - fileSchemaHeader.splice(0, 1); + if (TemplateIdentifier.EMAIL_COLUMN === fileHeader[0]) { + fileSchemaHeader.splice(0, 1); } else { - throw new BadRequestException(ResponseMessages.bulkIssuance.error.emailColumn - ); + throw new BadRequestException(ResponseMessages.bulkIssuance.error.emailColumn); } if (schemaAttributes.length !== fileSchemaHeader.length) { - throw new ConflictException(ResponseMessages.bulkIssuance.error.attributeNumber - ); + throw new ConflictException(ResponseMessages.bulkIssuance.error.attributeNumber); } - const mismatchedAttributes = fileSchemaHeader.filter(value => !schemaAttributes.includes(value)); + const mismatchedAttributes = fileSchemaHeader.filter((value) => !schemaAttributes.includes(value)); if (0 < mismatchedAttributes.length) { throw new ConflictException(ResponseMessages.bulkIssuance.error.mismatchedAttributes); } } catch (error) { throw error; - } } - async validateFileData(fileData: string[][], attributesArray: { attributeName: string, schemaDataType: string, displayName: string, isRequired: boolean }[], fileHeader: string[]): Promise { + async validateFileData( + fileData: string[][], + attributesArray: { attributeName: string; schemaDataType: string; displayName: string; isRequired: boolean }[], + fileHeader: string[] + ): Promise { try { const filedata = fileData.map((item: string[]) => { const fileHeaderData = item?.map((element, j) => ({ @@ -1794,7 +1888,6 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO filedata.forEach((attr, i) => { attr.forEach((eachElement) => { - attributesArray.forEach((eachItem) => { if (eachItem.attributeName === eachElement.header) { if (eachItem.isRequired && !eachElement.value) { @@ -1825,28 +1918,31 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ - status: error.status, - error: error.message - }, error.status); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); } } async _storeBulkPayloadInBatch(bulkPayloadObject: IBulkPayloadObject): Promise { try { - const {parsedFileDetails, parsedData, fileUploadId, userId} = bulkPayloadObject; - + const { parsedFileDetails, parsedData, fileUploadId, userId } = bulkPayloadObject; + const limit = pLimit(CommonConstants.MAX_CONCURRENT_OPERATIONS); const startTime = Date.now(); const batches = await this.splitIntoBatches(parsedData, CommonConstants.BATCH_SIZE); - this.logger.log("Total number of batches:", batches.length); - + this.logger.log('Total number of batches:', batches.length); + for (const [index, batch] of batches.entries()) { - - const batchStartTime = Date.now(); - + const batchStartTime = Date.now(); + // Create an array of limited promises for the current batch - const saveFileDetailsPromises = batch.map(element => limit(() => { + const saveFileDetailsPromises = batch.map((element) => + limit(() => { const credentialPayload = { credential_data: element, schemaId: parsedFileDetails.schemaLedgerId, @@ -1860,44 +1956,46 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO return this.issuanceRepository.saveFileDetails(credentialPayload, userId); }) ); - + this.logger.log(`Processing batch ${index + 1} with ${batch.length} elements...`); - + // Wait for all operations in the current batch to complete before moving to the next batch await Promise.all(saveFileDetailsPromises); - + const batchEndTime = Date.now(); // End timing the current batch - this.logger.log(`Batch ${index + 1} processed in ${(batchEndTime - batchStartTime)} milliseconds.`); + this.logger.log(`Batch ${index + 1} processed in ${batchEndTime - batchStartTime} milliseconds.`); } - + const endTime = Date.now(); - this.logger.log(`Total processing time: ${(endTime - startTime)} milliseconds.`); + this.logger.log(`Total processing time: ${endTime - startTime} milliseconds.`); return true; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ - status: error.status, - error: error.message - }, error.status); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); } } async deleteIssuanceRecords(orgId: string, userDetails: user): Promise { try { - const getFileUploadData = await this.issuanceRepository.getFileUploadDataByOrgId(orgId); - const getFileUploadIds = getFileUploadData.map(fileData => fileData.id); - + const getFileUploadIds = getFileUploadData.map((fileData) => fileData.id); + await this.issuanceRepository.deleteFileUploadData(getFileUploadIds, orgId); const deletedCredentialsRecords = await this.issuanceRepository.deleteIssuanceRecordsByOrgId(orgId); - + if (0 === deletedCredentialsRecords?.deleteResult?.count) { throw new NotFoundException(ResponseMessages.issuance.error.issuanceRecordsNotFound); } - const statusCounts = { + const statusCounts = { [IssuanceProcessState.REQUEST_SENT]: 0, [IssuanceProcessState.REQUEST_RECEIVED]: 0, [IssuanceProcessState.PROPOSAL_SENT]: 0, @@ -1909,23 +2007,28 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO [IssuanceProcessState.CREDENTIAL_RECEIVED]: 0, [IssuanceProcessState.CREDENTIAL_ISSUED]: 0, [IssuanceProcessState.ABANDONED]: 0 - }; + }; - await Promise.all(deletedCredentialsRecords?.recordsToDelete?.map(async (record) => { - statusCounts[record.state]++; - })); + await Promise.all( + deletedCredentialsRecords?.recordsToDelete?.map(async (record) => { + statusCounts[record.state]++; + }) + ); - const filteredStatusCounts = Object.fromEntries( - Object.entries(statusCounts).filter(entry => 0 < entry[1]) - ); + const filteredStatusCounts = Object.fromEntries(Object.entries(statusCounts).filter((entry) => 0 < entry[1])); const deletedIssuanceData = { - deletedCredentialsRecordsCount : deletedCredentialsRecords?.deleteResult?.count, + deletedCredentialsRecordsCount: deletedCredentialsRecords?.deleteResult?.count, deletedRecordsStatusCount: filteredStatusCounts - }; + }; + + await this.userActivityRepository._orgDeletedActivity( + orgId, + userDetails, + deletedIssuanceData, + RecordType.ISSUANCE_RECORD + ); - await this.userActivityRepository._orgDeletedActivity(orgId, userDetails, deletedIssuanceData, RecordType.ISSUANCE_RECORD); - return deletedCredentialsRecords; } catch (error) { this.logger.error(`[deleteIssuanceRecords] - error in deleting issuance records: ${JSON.stringify(error)}`); @@ -1945,5 +2048,4 @@ async sendEmailForCredentialOffer(sendEmailCredentialOffer: SendEmailCredentialO throw new RpcException(error.response); } } - -} \ No newline at end of file +} From 80d328939eadb423ea023bf249954b7b84a2349e Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 4 Apr 2025 17:12:12 +0530 Subject: [PATCH 04/14] fix: formatting changes Signed-off-by: bhavanakarwade --- apps/issuance/libs/helpers/attributes.extractor.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/issuance/libs/helpers/attributes.extractor.ts b/apps/issuance/libs/helpers/attributes.extractor.ts index cbe125e35..6fe6615bf 100644 --- a/apps/issuance/libs/helpers/attributes.extractor.ts +++ b/apps/issuance/libs/helpers/attributes.extractor.ts @@ -146,10 +146,10 @@ export function unflattenCsvRow(row: object): object { } } - for (const groupKey in groupedKeys) { - if (Object.prototype.hasOwnProperty.call(groupedKeys, groupKey)) { - const [arrayName, arrayIndex] = groupKey.split('~'); - const keys = groupedKeys[groupKey]; + for (const grpKey in groupedKeys) { + if (Object.prototype.hasOwnProperty.call(groupedKeys, grpKey)) { + const [arrayName, arrayIndex] = grpKey.split('~'); + const keys = groupedKeys[grpKey]; if (!result[arrayName]) { result[arrayName] = []; } From a5783bb62f7b4b3c9f7c4a212ebd2dd16d2433bf Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 4 Apr 2025 17:25:46 +0530 Subject: [PATCH 05/14] fix: resolved sonar cloud issue Signed-off-by: bhavanakarwade --- .../interfaces/issuance.interfaces.ts | 149 ++-- .../src/schema/interfaces/schema.interface.ts | 56 +- apps/ledger/src/schema/schema.service.ts | 668 +++++++++--------- libs/common/src/interfaces/interface.ts | 30 + 4 files changed, 446 insertions(+), 457 deletions(-) diff --git a/apps/issuance/interfaces/issuance.interfaces.ts b/apps/issuance/interfaces/issuance.interfaces.ts index db85a1e68..66c070c3e 100644 --- a/apps/issuance/interfaces/issuance.interfaces.ts +++ b/apps/issuance/interfaces/issuance.interfaces.ts @@ -16,8 +16,8 @@ export interface IAttributes { interface ICredentialsAttributes { connectionId: string; attributes: IAttributes[]; - credential?:ICredential; - options?:IOptions + credential?: ICredential; + options?: IOptions; } export interface IIssuance { user?: IUserRequest; @@ -25,27 +25,27 @@ export interface IIssuance { comment?: string; credentialData: ICredentialsAttributes[]; orgId: string; - autoAcceptCredential?: AutoAccept, + autoAcceptCredential?: AutoAccept; protocolVersion?: string; - goalCode?: string, - parentThreadId?: string, - willConfirm?: boolean, - label?: string, - credentialType: string, + goalCode?: string; + parentThreadId?: string; + willConfirm?: boolean; + label?: string; + credentialType: string; } interface IIndy { - attributes: IAttributes[], - credentialDefinitionId: string + attributes: IAttributes[]; + credentialDefinitionId: string; } export interface IIssueData { protocolVersion?: string; connectionId: string; credentialFormats: { - indy: IIndy - }, - autoAcceptCredential: string, + indy: IIndy; + }; + autoAcceptCredential: string; comment?: string; } @@ -90,8 +90,8 @@ export interface IPattern { } export interface ISendOfferNatsPayload { - issueData: IIssueData, - url: string, + issueData: IIssueData; + url: string; apiKey?: string; orgId?: string; } @@ -137,8 +137,8 @@ export interface ICredentialAttributesInterface { value: string; } -export interface ICredential{ - '@context':[]; +export interface ICredential { + '@context': []; type: string[]; prettyVc?: IPrettyVc; issuer?: { @@ -152,15 +152,15 @@ interface ICredentialSubject { [key: string]: string; } -export interface IOptions{ - proofType:string; - proofPurpose:string; +export interface IOptions { + proofType: string; + proofPurpose: string; } export interface CredentialOffer { emailId: string; attributes: IAttributes[]; - credential?:ICredential; - options?:IOptions + credential?: ICredential; + options?: IOptions; } export interface OutOfBandCredentialOfferPayload { credentialDefinitionId?: string; @@ -171,13 +171,13 @@ export interface OutOfBandCredentialOfferPayload { attributes?: IAttributes[]; protocolVersion?: string; isReuseConnection?: boolean; - goalCode?: string, - parentThreadId?: string, - willConfirm?: boolean, - label?: string, - imageUrl?: string, + goalCode?: string; + parentThreadId?: string; + willConfirm?: boolean; + label?: string; + imageUrl?: string; autoAcceptCredential?: string; - credentialType?:IssueCredentialType; + credentialType?: IssueCredentialType; } export interface OutOfBandCredentialOffer { @@ -199,30 +199,30 @@ export interface ImportFileDetails { isValidateSchema?: boolean; } export interface ICredentialPayload { -schemaLedgerId: string, -credentialDefinitionId: string, -fileData: object, -fileName: string, -credentialType: string, -schemaName?: string + schemaLedgerId: string; + credentialDefinitionId: string; + fileData: object; + fileName: string; + credentialType: string; + schemaName?: string; } export interface PreviewRequest { - pageNumber: number, - pageSize: number, - searchByText: string, - sortField?: string, - sortBy?: string + pageNumber: number; + pageSize: number; + searchByText: string; + sortField?: string; + sortBy?: string; } export interface FileUpload { - name?: string, - upload_type?: string, - status?: string, - orgId?: string, - createDateTime?: Date | null, - lastChangedDateTime?: Date | null, - credentialType?: string, - templateId?: string + name?: string; + upload_type?: string; + status?: string; + orgId?: string; + createDateTime?: Date | null; + lastChangedDateTime?: Date | null; + credentialType?: string; + templateId?: string; } export interface FileUploadData { @@ -284,19 +284,19 @@ export interface SendEmailCredentialOffer { iterator: CredentialOffer; emailId: string; index: number; - credentialType: IssueCredentialType; + credentialType: IssueCredentialType; protocolVersion: string; isReuseConnection?: boolean; - attributes: IAttributes[]; - credentialDefinitionId: string; + attributes: IAttributes[]; + credentialDefinitionId: string; outOfBandCredential: OutOfBandCredentialOfferPayload; comment: string; - organisation: organisation; + organisation: organisation; errors; url: string; - orgId: string; + orgId: string; organizationDetails: organisation; - platformName?: string, + platformName?: string; organizationLogoUrl?: string; prettyVc?: IPrettyVc; isValidateSchema?: boolean; @@ -321,12 +321,12 @@ export interface IJobDetails { schemaLedgerId: string; credentialDefinitionId?: string; status?: boolean; - credential_data: CredentialData + credential_data: CredentialData; orgId: string; credentialType: string; } -export interface IQueuePayload{ +export interface IQueuePayload { id: string; jobId: string; cacheId?: string; @@ -356,17 +356,17 @@ export interface IQueuePayload{ interface FileDetails { schemaLedgerId: string; credentialDefinitionId: string; - fileData:object + fileData: object; fileName: string; credentialType: string; schemaName: string; } export interface IBulkPayloadObject { - parsedData?: unknown[], - parsedFileDetails?: FileDetails, - userId: string, - fileUploadId: string - }; + parsedData?: unknown[]; + parsedFileDetails?: FileDetails; + userId: string; + fileUploadId: string; +} export interface ISchemaAttributes { attributeName: string; schemaDataType: string; @@ -374,31 +374,6 @@ export interface ISchemaAttributes { isRequired: boolean; } -export interface IW3CAttributeValue extends ISchemaAttributes{ - minLength?: number; - maxLength?: number; - pattern?: string; - enum?: string[]; - contentEncoding?: string; - contentMediaType?: string; - minimum?: number; - maximum?: number; - exclusiveMinimum?: number; - exclusiveMaximum?: number; - multipleOf?: number; - minItems?: number; - maxItems?: number; - uniqueItems?: boolean; - items?: IW3CAttributeValue[]; - minProperties?: number; - maxProperties?: number; - additionalProperties?: boolean; - required?: string[]; - dependentRequired?: Record; - properties?: Record; -} - - export interface IIssuanceAttributes { [key: string]: string; } @@ -424,4 +399,4 @@ export interface BulkPayloadDetails { export interface ISchemaId { schemaLedgerId: string; -} \ No newline at end of file +} diff --git a/apps/ledger/src/schema/interfaces/schema.interface.ts b/apps/ledger/src/schema/interfaces/schema.interface.ts index 5604f1f4e..7791d5e2d 100644 --- a/apps/ledger/src/schema/interfaces/schema.interface.ts +++ b/apps/ledger/src/schema/interfaces/schema.interface.ts @@ -1,5 +1,6 @@ -import { JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum/enum'; +import { JSONSchemaType, SchemaTypeEnum } from '@credebl/enum/enum'; import { UserRoleOrgPermsDto } from '../dtos/user-role-org-perms.dto'; +import { IW3CAttributeValue } from '@credebl/common/interfaces/interface'; export interface IUserRequestInterface { id: string; @@ -28,8 +29,7 @@ export interface ISelectedOrgInterface { export interface IOrganizationInterface { name: string; description: string; - org_agents: IOrgAgentInterface[] - + org_agents: IOrgAgentInterface[]; } export interface IOrgAgentInterface { @@ -43,9 +43,9 @@ export interface IOrgAgentInterface { } export interface AgentDetails { - orgDid: string; - agentEndPoint: string; - tenantId: string + orgDid: string; + agentEndPoint: string; + tenantId: string; } export interface ISchemaData { @@ -66,33 +66,6 @@ export interface ISchemasWithCount { schemasCount: number; schemasResult: ISchemaData[]; } -export interface IW3CAttributeValue { - attributeName: string; - schemaDataType: W3CSchemaDataType; - displayName: string; - isRequired: boolean; - minLength?: number; - maxLength?: number; - pattern?: string; - enum?: string[]; - contentEncoding?: string; - contentMediaType?: string; - minimum?: number; - maximum?: number; - exclusiveMinimum?: number; - exclusiveMaximum?: number; - multipleOf?: number; - minItems?: number; - maxItems?: number; - uniqueItems?: boolean; - items?: IW3CAttributeValue[]; - minProperties?: number; - maxProperties?: number; - additionalProperties?: boolean; - required?: string[]; - dependentRequired?: Record; - properties?: Record; -} export interface IProductSchema { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -111,7 +84,7 @@ export interface ICreateSchema { schemaVersion?: string; schemaName: string; attributes: IAttributeValue[]; - orgId?: string; + orgId?: string; orgDid?: string; } export interface ICreateW3CSchema { @@ -121,15 +94,15 @@ export interface ICreateW3CSchema { schemaType: JSONSchemaType; } export interface IGenericSchema { - alias:string; + alias: string; type: SchemaTypeEnum; schemaPayload: ICreateSchema | ICreateW3CSchema; } export interface IschemaPayload { - schemaDetails: IGenericSchema, - user: IUserRequestInterface, - orgId: string + schemaDetails: IGenericSchema; + user: IUserRequestInterface; + orgId: string; } export interface ISchemasResult { id: string; @@ -153,13 +126,12 @@ export interface ISchemasList { schemasResult: ISchemasResult[]; } - export interface IUpdateSchema { alias: string; schemaLedgerId: string; - orgId?: string; + orgId?: string; } export interface UpdateSchemaResponse { - count: number; -} \ No newline at end of file + count: number; +} diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index 3f62de312..d6c436029 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -5,18 +5,48 @@ import { Inject, ConflictException, Injectable, - NotAcceptableException, NotFoundException} from '@nestjs/common'; + NotAcceptableException, + NotFoundException +} from '@nestjs/common'; import { ClientProxy, RpcException } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; import { SchemaRepository } from './repositories/schema.repository'; import { Prisma, schema } from '@prisma/client'; -import { ISaveSchema, ISchema, ISchemaAttributesFormat, ISchemaCredDeffSearchInterface, ISchemaExist, ISchemaSearchCriteria, W3CCreateSchema } from './interfaces/schema-payload.interface'; +import { + ISaveSchema, + ISchema, + ISchemaAttributesFormat, + ISchemaCredDeffSearchInterface, + ISchemaExist, + ISchemaSearchCriteria, + W3CCreateSchema +} from './interfaces/schema-payload.interface'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { ICreateSchema, ICreateW3CSchema, IGenericSchema, IProductSchema, IUpdateSchema, IUserRequestInterface, IW3CAttributeValue, UpdateSchemaResponse } from './interfaces/schema.interface'; +import { + ICreateSchema, + ICreateW3CSchema, + IGenericSchema, + IProductSchema, + IUpdateSchema, + IUserRequestInterface, + UpdateSchemaResponse +} from './interfaces/schema.interface'; import { CreateSchemaAgentRedirection, GetSchemaAgentRedirection, ISchemaId } from './schema.interface'; import { map } from 'rxjs/operators'; -import { JSONSchemaType, LedgerLessConstant, LedgerLessMethods, OrgAgentType, SchemaType, SchemaTypeEnum } from '@credebl/enum/enum'; -import { ICredDefWithPagination, ISchemaData, ISchemaDetails, ISchemasWithPagination } from '@credebl/common/interfaces/schema.interface'; +import { + JSONSchemaType, + LedgerLessConstant, + LedgerLessMethods, + OrgAgentType, + SchemaType, + SchemaTypeEnum +} from '@credebl/enum/enum'; +import { + ICredDefWithPagination, + ISchemaData, + ISchemaDetails, + ISchemasWithPagination +} from '@credebl/common/interfaces/schema.interface'; import { Cache } from 'cache-manager'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { CommonConstants } from '@credebl/common/common.constant'; @@ -36,6 +66,7 @@ import Minimum from 'libs/validations/minimum'; import Pattern from 'libs/validations/pattern'; import MaxLength from 'libs/validations/maxLength'; import MinLength from 'libs/validations/minLength'; +import { IW3CAttributeValue } from '@credebl/common/interfaces/interface'; @Injectable() export class SchemaService extends BaseService { @@ -44,116 +75,108 @@ export class SchemaService extends BaseService { private readonly commonService: CommonService, @Inject('NATS_CLIENT') private readonly schemaServiceProxy: ClientProxy, @Inject(CACHE_MANAGER) private readonly cacheService: Cache, - private readonly natsClient : NATSClient + private readonly natsClient: NATSClient ) { super('SchemaService'); } - async createSchema( - schemaDetails: IGenericSchema, - user: IUserRequestInterface, - orgId: string - ): Promise { - + async createSchema(schemaDetails: IGenericSchema, user: IUserRequestInterface, orgId: string): Promise { const userId = user.id; try { - const {schemaPayload, type, alias} = schemaDetails; - - if (type === SchemaTypeEnum.INDY) { - - const schema = schemaPayload as ICreateSchema; - const schemaExists = await this.schemaRepository.schemaExists( - schema.schemaName, - schema.schemaVersion - ); - - if (0 !== schemaExists.length) { - this.logger.error(ResponseMessages.schema.error.exists); - throw new ConflictException( - ResponseMessages.schema.error.exists, - { cause: new Error(), description: ResponseMessages.errorMessages.conflict } - ); - } - if (null !== schema || schema !== undefined) { - const schemaVersionIndexOf = -1; - if ( + const { schemaPayload, type, alias } = schemaDetails; + + if (type === SchemaTypeEnum.INDY) { + const schema = schemaPayload as ICreateSchema; + const schemaExists = await this.schemaRepository.schemaExists(schema.schemaName, schema.schemaVersion); + + if (0 !== schemaExists.length) { + this.logger.error(ResponseMessages.schema.error.exists); + throw new ConflictException(ResponseMessages.schema.error.exists, { + cause: new Error(), + description: ResponseMessages.errorMessages.conflict + }); + } + if (null !== schema || schema !== undefined) { + const schemaVersionIndexOf = -1; + if ( isNaN(parseFloat(schema.schemaVersion)) || - schema.schemaVersion.toString().indexOf('.') === - schemaVersionIndexOf + schema.schemaVersion.toString().indexOf('.') === schemaVersionIndexOf ) { - throw new NotAcceptableException( - ResponseMessages.schema.error.invalidVersion, - { cause: new Error(), description: ResponseMessages.errorMessages.notAcceptable } - ); + throw new NotAcceptableException(ResponseMessages.schema.error.invalidVersion, { + cause: new Error(), + description: ResponseMessages.errorMessages.notAcceptable + }); } - + const schemaAttributeLength = 0; if (schema.attributes.length === schemaAttributeLength) { - throw new NotAcceptableException( - ResponseMessages.schema.error.insufficientAttributes, - { cause: new Error(), description: ResponseMessages.errorMessages.notAcceptable } - ); - } else if (schema.attributes.length > schemaAttributeLength) { - - const trimmedAttributes = schema.attributes.map(attribute => ({ - attributeName: attribute.attributeName.trim(), - schemaDataType: attribute.schemaDataType, - displayName: attribute.displayName.trim(), - isRequired: attribute.isRequired - })); - - - const attributeNamesLowerCase = trimmedAttributes.map(attribute => attribute.attributeName.toLowerCase()); - const duplicateAttributeNames = attributeNamesLowerCase - .filter((value, index, element) => element.indexOf(value) !== index); - - if (0 < duplicateAttributeNames.length) { - throw new ConflictException( - ResponseMessages.schema.error.uniqueAttributesnames, - { cause: new Error(), description: ResponseMessages.errorMessages.conflict } - ); - } - - const attributeDisplayNamesLowerCase = trimmedAttributes.map(attribute => attribute.displayName.toLocaleLowerCase()); - const duplicateAttributeDisplayNames = attributeDisplayNamesLowerCase - .filter((value, index, element) => element.indexOf(value) !== index); - - if (0 < duplicateAttributeDisplayNames.length) { - throw new ConflictException( - ResponseMessages.schema.error.uniqueAttributesDisplaynames, - { cause: new Error(), description: ResponseMessages.errorMessages.conflict } - ); - } - + throw new NotAcceptableException(ResponseMessages.schema.error.insufficientAttributes, { + cause: new Error(), + description: ResponseMessages.errorMessages.notAcceptable + }); + } else if (schema.attributes.length > schemaAttributeLength) { + const trimmedAttributes = schema.attributes.map((attribute) => ({ + attributeName: attribute.attributeName.trim(), + schemaDataType: attribute.schemaDataType, + displayName: attribute.displayName.trim(), + isRequired: attribute.isRequired + })); + + const attributeNamesLowerCase = trimmedAttributes.map((attribute) => attribute.attributeName.toLowerCase()); + const duplicateAttributeNames = attributeNamesLowerCase.filter( + (value, index, element) => element.indexOf(value) !== index + ); + + if (0 < duplicateAttributeNames.length) { + throw new ConflictException(ResponseMessages.schema.error.uniqueAttributesnames, { + cause: new Error(), + description: ResponseMessages.errorMessages.conflict + }); + } + + const attributeDisplayNamesLowerCase = trimmedAttributes.map((attribute) => + attribute.displayName.toLocaleLowerCase() + ); + const duplicateAttributeDisplayNames = attributeDisplayNamesLowerCase.filter( + (value, index, element) => element.indexOf(value) !== index + ); + + if (0 < duplicateAttributeDisplayNames.length) { + throw new ConflictException(ResponseMessages.schema.error.uniqueAttributesDisplaynames, { + cause: new Error(), + description: ResponseMessages.errorMessages.conflict + }); + } + schema.schemaName = schema.schemaName.trim(); const agentDetails = await this.schemaRepository.getAgentDetailsByOrgId(orgId); if (!agentDetails) { - throw new NotFoundException( - ResponseMessages.schema.error.agentDetailsNotFound, - { cause: new Error(), description: ResponseMessages.errorMessages.notFound } - ); + throw new NotFoundException(ResponseMessages.schema.error.agentDetailsNotFound, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); } const { agentEndPoint, orgDid } = agentDetails; const getAgentDetails = await this.schemaRepository.getAgentType(orgId); // eslint-disable-next-line yoda const did = schema.orgDid?.split(':').length >= 4 ? schema.orgDid : orgDid; - - const orgAgentType = await this.schemaRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); - - const attributeArray = trimmedAttributes.map(item => item.attributeName); - - const isRequiredAttributeExists = trimmedAttributes.some(attribute => attribute.isRequired); - - if (!isRequiredAttributeExists) { - throw new BadRequestException( - ResponseMessages.schema.error.atLeastOneRequired - ); - } - + + const orgAgentType = await this.schemaRepository.getOrgAgentType( + getAgentDetails.org_agents[0].orgAgentTypeId + ); + + const attributeArray = trimmedAttributes.map((item) => item.attributeName); + + const isRequiredAttributeExists = trimmedAttributes.some((attribute) => attribute.isRequired); + + if (!isRequiredAttributeExists) { + throw new BadRequestException(ResponseMessages.schema.error.atLeastOneRequired); + } + let schemaResponseFromAgentService; if (OrgAgentType.DEDICATED === orgAgentType) { const issuerId = did; - + const schemaPayload = { attributes: attributeArray, version: schema.schemaVersion, @@ -164,10 +187,9 @@ export class SchemaService extends BaseService { agentType: OrgAgentType.DEDICATED }; schemaResponseFromAgentService = await this._createSchema(schemaPayload); - } else if (OrgAgentType.SHARED === orgAgentType) { const { tenantId } = await this.schemaRepository.getAgentDetailsByOrgId(orgId); - + const schemaPayload = { tenantId, method: 'registerSchema', @@ -183,9 +205,9 @@ export class SchemaService extends BaseService { }; schemaResponseFromAgentService = await this._createSchema(schemaPayload); } - + const responseObj = JSON.parse(JSON.stringify(schemaResponseFromAgentService.response)); - + const indyNamespace = `${did.split(':')[2]}:${did.split(':')[3]}`; const getLedgerId = await this.schemaRepository.getLedgerByNamespace(indyNamespace); const schemaDetails: ISchema = { @@ -197,7 +219,7 @@ export class SchemaService extends BaseService { ledgerId: getLedgerId.id, type: SchemaType.INDY }; - + if ('finished' === responseObj.schema.state) { schemaDetails.schema.schemaName = responseObj.schema.schema.name; schemaDetails.schema.attributes = trimmedAttributes; @@ -207,16 +229,13 @@ export class SchemaService extends BaseService { schemaDetails.changedBy = userId; schemaDetails.orgId = orgId; schemaDetails.issuerId = responseObj.schema.schema.issuerId; - const saveResponse = this.schemaRepository.saveSchema( - schemaDetails - ); - + const saveResponse = this.schemaRepository.saveSchema(schemaDetails); + const attributesArray = JSON.parse((await saveResponse).attributes); (await saveResponse).attributes = attributesArray; delete (await saveResponse).lastChangedBy; delete (await saveResponse).lastChangedDateTime; return saveResponse; - } else if ('finished' === responseObj.state) { schemaDetails.schema.schemaName = responseObj.schema.name; schemaDetails.schema.attributes = trimmedAttributes; @@ -226,53 +245,53 @@ export class SchemaService extends BaseService { schemaDetails.changedBy = userId; schemaDetails.orgId = orgId; schemaDetails.issuerId = responseObj.schema.issuerId; - const saveResponse = this.schemaRepository.saveSchema( - schemaDetails - ); - + const saveResponse = this.schemaRepository.saveSchema(schemaDetails); + const attributesArray = JSON.parse((await saveResponse).attributes); (await saveResponse).attributes = attributesArray; delete (await saveResponse).lastChangedBy; delete (await saveResponse).lastChangedDateTime; return saveResponse; - } else { - throw new NotFoundException( - ResponseMessages.schema.error.notCreated, - { cause: new Error(), description: ResponseMessages.errorMessages.notFound } - ); + throw new NotFoundException(ResponseMessages.schema.error.notCreated, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); } } else { - throw new BadRequestException( - ResponseMessages.schema.error.emptyData, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + throw new BadRequestException(ResponseMessages.schema.error.emptyData, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } - } else { - throw new BadRequestException( - ResponseMessages.schema.error.emptyData, - { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } - ); + } else { + throw new BadRequestException(ResponseMessages.schema.error.emptyData, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); } - } else if (type === SchemaTypeEnum.JSON) { - const josnSchemaDetails = schemaPayload as unknown as ICreateW3CSchema; + } else if (type === SchemaTypeEnum.JSON) { + const josnSchemaDetails = schemaPayload as unknown as ICreateW3CSchema; const createW3CSchema = await this.createW3CSchema(orgId, josnSchemaDetails, user.id, alias); return createW3CSchema; - } + } } catch (error) { - this.logger.error( - `[createSchema] - outer Error: ${JSON.stringify(error)}` - ); + this.logger.error(`[createSchema] - outer Error: ${JSON.stringify(error)}`); throw new RpcException(error.response ? error.response : error); } } - async createW3CSchema(orgId:string, schemaPayload: ICreateW3CSchema, user: string, alias: string): Promise { + async createW3CSchema( + orgId: string, + schemaPayload: ICreateW3CSchema, + user: string, + alias: string + ): Promise { try { let createSchema; - - const { description, attributes, schemaName} = schemaPayload; + + const { description, attributes, schemaName } = schemaPayload; const agentDetails = await this.schemaRepository.getAgentDetailsByOrgId(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.schema.error.agentDetailsNotFound, { @@ -289,7 +308,7 @@ export class SchemaService extends BaseService { description: ResponseMessages.errorMessages.badRequest }); } - + const getAgentDetails = await this.schemaRepository.getAgentType(orgId); const orgAgentType = await this.schemaRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); let url; @@ -300,7 +319,7 @@ export class SchemaService extends BaseService { url = `${agentEndPoint}${CommonConstants.SHARED_CREATE_POLYGON_W3C_SCHEMA}${tenantId}`; } - const schemaObject = await this.w3cSchemaBuilder(attributes, schemaName, description); + const schemaObject = await this.w3cSchemaBuilder(attributes, schemaName, description); if (!schemaObject) { throw new BadRequestException(ResponseMessages.schema.error.schemaBuilder, { cause: new Error(), @@ -308,7 +327,7 @@ export class SchemaService extends BaseService { }); } const agentSchemaPayload = { - schema:schemaObject, + schema: schemaObject, did: agentDetails.orgDid, schemaName }; @@ -334,16 +353,16 @@ export class SchemaService extends BaseService { createSchema.type = JSONSchemaType.LEDGER_LESS; createSchema.schemaUrl = `${process.env.SCHEMA_FILE_SERVER_URL}${createSchemaPayload.data.schemaId}`; } - - const storeW3CSchema = await this.storeW3CSchemas(createSchema, user, orgId, attributes, alias); - if (!storeW3CSchema) { - throw new BadRequestException(ResponseMessages.schema.error.storeW3CSchema, { - cause: new Error(), - description: ResponseMessages.errorMessages.notFound - }); - } - + const storeW3CSchema = await this.storeW3CSchemas(createSchema, user, orgId, attributes, alias); + + if (!storeW3CSchema) { + throw new BadRequestException(ResponseMessages.schema.error.storeW3CSchema, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } + return storeW3CSchema; } catch (error) { this.logger.error(`[createSchema] - outer Error: ${JSON.stringify(error)}`); @@ -351,87 +370,90 @@ export class SchemaService extends BaseService { } } - private async w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: string, description: string): Promise { - + private async w3cSchemaBuilder( + attributes: IW3CAttributeValue[], + schemaName: string, + description: string + ): Promise { // Function to apply validations based on attribute properties const applyValidations = (attribute, propertyObj): ISchemaAttributesFormat => { const context = { ...propertyObj }; - + // Apply string validations if ('string' === attribute.schemaDataType.toLowerCase()) { if (attribute.minLength !== undefined) { const validation = new MinLength(attribute.minLength); validation.json(context); } - + if (attribute.maxLength !== undefined) { const validation = new MaxLength(attribute.maxLength); validation.json(context); } - + if (attribute.pattern !== undefined) { const validation = new Pattern(attribute.pattern); validation.json(context); } } - + // Apply number validations if (['number', 'integer'].includes(attribute.schemaDataType.toLowerCase())) { if (attribute.minimum !== undefined) { const validation = new Minimum(attribute.minimum); validation.json(context); } - + if (attribute.exclusiveMinimum !== undefined) { const validation = new ExclusiveMinimum(attribute.exclusiveMinimum); validation.json(context); } - + if (attribute.multipleOf !== undefined) { const validation = new MultipleOf(attribute.multipleOf); validation.json(context); } } - + // Apply array validations if ('array' === attribute.schemaDataType.toLowerCase()) { if (attribute.minItems !== undefined) { const validation = new MinItems(attribute.minItems); validation.json(context); } - + if (attribute.maxItems !== undefined) { const validation = new MaxItems(attribute.maxItems); validation.json(context); } - + if (attribute.uniqueItems !== undefined) { const validation = new UniqueItems(attribute.uniqueItems); validation.json(context); } } - + return context; }; - + // Function to recursively process attributes // eslint-disable-next-line @typescript-eslint/no-explicit-any const processAttributes = (attrs: IW3CAttributeValue[]): IProductSchema => { - if (!Array.isArray(attrs)) { - return { properties: {}, required: [] }; + if (!Array.isArray(attrs)) { + return { properties: {}, required: [] }; } const properties = {}; const required = []; - + attrs.forEach((attribute, index) => { const { attributeName, schemaDataType, isRequired, displayName } = attribute; - + // Add to required array if isRequired is true if (isRequired) { required.push(attributeName); } - + // Create base property object with common fields const baseProperty = { type: schemaDataType.toLowerCase(), @@ -444,17 +466,15 @@ export class SchemaService extends BaseService { if (['string', 'number', 'boolean', 'integer'].includes(schemaDataType.toLowerCase())) { // Apply validations to the base property properties[attributeName] = applyValidations(attribute, baseProperty); - } else if ('datetime-local' === schemaDataType.toLowerCase()) { properties[attributeName] = { ...baseProperty, type: 'string', format: 'date-time' }; - } else if ('array' === schemaDataType.toLowerCase() && attribute.items) { const result = processAttributes(attribute.items); - + properties[attributeName] = { ...baseProperty, type: 'array', @@ -466,36 +486,35 @@ export class SchemaService extends BaseService { // Apply array-specific validations properties[attributeName] = applyValidations(attribute, properties[attributeName]); - + // Add required properties to the items schema if any if (0 < result.required.length) { properties[attributeName].items.required = result.required; } - } else if ('object' === schemaDataType.toLowerCase() && attribute.properties) { const nestedProperties = {}; const nestedRequired = []; - + // Process each property in the object - Object.keys(attribute.properties).forEach(propKey => { + Object.keys(attribute.properties).forEach((propKey) => { const prop = attribute.properties[propKey]; // Add to nested required array if isRequired is true if (prop.isRequired) { nestedRequired.push(propKey); } - + // Create base property for nested object const nestedBaseProperty = { type: prop.schemaDataType.toLowerCase(), title: prop.displayName || prop.attributeName, description: `${prop.attributeName} field` }; - + if ('array' === prop.schemaDataType.toLowerCase() && prop.items) { // Handle nested arrays const result = processAttributes(prop.items); - + nestedProperties[propKey] = { ...nestedBaseProperty, type: 'array', @@ -504,10 +523,10 @@ export class SchemaService extends BaseService { properties: result.properties } }; - + // Apply array-specific validations nestedProperties[propKey] = applyValidations(prop, nestedProperties[propKey]); - + // Add required properties to the items schema if any if (0 < result.required.length) { nestedProperties[propKey].items.required = result.required; @@ -517,39 +536,39 @@ export class SchemaService extends BaseService { nestedProperties[propKey] = applyValidations(prop, nestedBaseProperty); } }); - + properties[attributeName] = { ...baseProperty, type: 'object', properties: nestedProperties }; - + // Add required properties to the object schema if any if (0 < nestedRequired.length) { properties[attributeName].required = nestedRequired; } } }); - + return { properties, required }; }; // Process all attributes const result = processAttributes(attributes); - const {properties} = result; + const { properties } = result; // eslint-disable-next-line @typescript-eslint/no-unused-vars const required = ['id', ...result.required]; - + // Add id property properties['id'] = { type: 'string', format: 'uri' }; - const date = new Date().toISOString(); + const date = new Date().toISOString(); const schemaNameObject = {}; schemaNameObject[schemaName] = { - 'const': schemaName + const: schemaName }; const W3CSchema = { @@ -563,7 +582,7 @@ export class SchemaService extends BaseService { 'https://json-schema.org/draft/2020-12/vocab/meta-data': true, 'https://json-schema.org/draft/2020-12/vocab/format-annotation': true, 'https://json-schema.org/draft/2020-12/vocab/content': true - }, + }, type: 'object', required: ['@context', 'issuer', 'issuanceDate', 'type', 'credentialSubject'], properties: { @@ -713,40 +732,37 @@ export class SchemaService extends BaseService { }; return W3CSchema; } - - private async storeW3CSchemas(schemaDetails, user, orgId, attributes, alias): Promise { + + private async storeW3CSchemas(schemaDetails, user, orgId, attributes, alias): Promise { let ledgerDetails; - const schemaServerUrl = `${process.env.SCHEMA_FILE_SERVER_URL}${schemaDetails.schemaId}`; - const schemaRequest = await this.commonService - .httpGet(schemaServerUrl) - .then(async (response) => response); + const schemaServerUrl = `${process.env.SCHEMA_FILE_SERVER_URL}${schemaDetails.schemaId}`; + const schemaRequest = await this.commonService.httpGet(schemaServerUrl).then(async (response) => response); if (!schemaRequest) { throw new NotFoundException(ResponseMessages.schema.error.W3CSchemaNotFOund, { cause: new Error(), description: ResponseMessages.errorMessages.notFound }); } - const indyNamespace = await networkNamespace(schemaDetails?.did); - if (indyNamespace === LedgerLessMethods.WEB || indyNamespace === LedgerLessMethods.KEY) { - ledgerDetails = await this.schemaRepository.getLedgerByNamespace(LedgerLessConstant.NO_LEDGER); - } else { - ledgerDetails = await this.schemaRepository.getLedgerByNamespace(indyNamespace); - } + const indyNamespace = await networkNamespace(schemaDetails?.did); + if (indyNamespace === LedgerLessMethods.WEB || indyNamespace === LedgerLessMethods.KEY) { + ledgerDetails = await this.schemaRepository.getLedgerByNamespace(LedgerLessConstant.NO_LEDGER); + } else { + ledgerDetails = await this.schemaRepository.getLedgerByNamespace(indyNamespace); + } - if (!ledgerDetails) { - throw new NotFoundException(ResponseMessages.schema.error.networkNotFound, { - cause: new Error(), - description: ResponseMessages.errorMessages.notFound - }); - } + if (!ledgerDetails) { + throw new NotFoundException(ResponseMessages.schema.error.networkNotFound, { + cause: new Error(), + description: ResponseMessages.errorMessages.notFound + }); + } const storeSchemaDetails = { - schema: { - schemaName: schemaRequest.title, - schemaVersion: W3CSchemaVersion.W3C_SCHEMA_VERSION, - attributes, - id: schemaDetails.schemaUrl - - }, + schema: { + schemaName: schemaRequest.title, + schemaVersion: W3CSchemaVersion.W3C_SCHEMA_VERSION, + attributes, + id: schemaDetails.schemaUrl + }, issuerId: schemaDetails.did, createdBy: user, changedBy: user, @@ -756,62 +772,62 @@ export class SchemaService extends BaseService { type: SchemaType.W3C_Schema, alias }; - const saveResponse = await this.schemaRepository.saveSchema( - storeSchemaDetails - ); + const saveResponse = await this.schemaRepository.saveSchema(storeSchemaDetails); return saveResponse; - } - + } + async _createSchema(payload: CreateSchemaAgentRedirection): Promise<{ response: string; }> { - const pattern = { - cmd: 'agent-create-schema' - }; - const schemaResponse = await from(this.natsClient - .send(this.schemaServiceProxy, pattern, payload)) - .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { - this.logger.error(`Error in creating schema : ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.statusCode, - error: error.error, - message: error.message - }, error.error); - }); - return schemaResponse; + const pattern = { + cmd: 'agent-create-schema' + }; + const schemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, pattern, payload)) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`Error in creating schema : ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.error, + message: error.message + }, + error.error + ); + }); + return schemaResponse; } async _createW3CSchema(payload: W3CCreateSchema): Promise<{ response: string; }> { - const natsPattern = { - cmd: 'agent-create-w3c-schema' - }; - const W3CSchemaResponse = await from(this.natsClient - .send(this.schemaServiceProxy, natsPattern, payload)) - .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { - this.logger.error(`Error in creating W3C schema : ${JSON.stringify(error)}`); - throw new HttpException( - { - status: error.error.code, - error: error.message, - message: error.error.message.error.message - }, error.error); - }); - return W3CSchemaResponse; + const natsPattern = { + cmd: 'agent-create-w3c-schema' + }; + const W3CSchemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, natsPattern, payload)) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`Error in creating W3C schema : ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.error.code, + error: error.message, + message: error.error.message.error.message + }, + error.error + ); + }); + return W3CSchemaResponse; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/explicit-function-return-type @@ -834,7 +850,7 @@ export class SchemaService extends BaseService { } } ); - + return jsonldSchemaResponse; } catch (error) { this.logger.error('Error creating W3C ledger agnostic schema:', error); @@ -843,18 +859,17 @@ export class SchemaService extends BaseService { } async getSchemaById(schemaId: string, orgId: string): Promise { - - try { - const [{agentEndPoint}, getAgentDetails, getSchemaDetails] = await Promise.all([ + try { + const [{ agentEndPoint }, getAgentDetails, getSchemaDetails] = await Promise.all([ this.schemaRepository.getAgentDetailsByOrgId(orgId), this.schemaRepository.getAgentType(orgId), this.schemaRepository.getSchemaBySchemaId(schemaId) ]); if (!getSchemaDetails) { - throw new NotFoundException(ResponseMessages.schema.error.notFound); + throw new NotFoundException(ResponseMessages.schema.error.notFound); } - + const orgAgentType = await this.schemaRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); let schemaResponse; @@ -881,36 +896,36 @@ export class SchemaService extends BaseService { } return schemaResponse.response; } else if (getSchemaDetails?.type === SchemaType.W3C_Schema) { - return getSchemaDetails; + return getSchemaDetails; } - } catch (error) { this.logger.error(`Error in getting schema by id: ${error}`); if (error && error?.status && error?.status?.message && error?.status?.message?.error) { throw new RpcException({ - message: error?.status?.message?.error?.reason ? error?.status?.message?.error?.reason : error?.status?.message?.error, + message: error?.status?.message?.error?.reason + ? error?.status?.message?.error?.reason + : error?.status?.message?.error, statusCode: error?.status?.code }); - } else { throw new RpcException(error.response ? error.response : error); } } } - async getSchemaDetails(templateIds: string[]): Promise { - try { - const getSchemaData = await this.schemaRepository.getSchemasDetailsBySchemaIds(templateIds); - return getSchemaData; + async getSchemaDetails(templateIds: string[]): Promise { + try { + const getSchemaData = await this.schemaRepository.getSchemasDetailsBySchemaIds(templateIds); + return getSchemaData; } catch (error) { - throw new RpcException(error.response ? error.response : error); + throw new RpcException(error.response ? error.response : error); } } - async getSchemaDetailsBySchemaName(schemaName: string, orgId:string): Promise { - try { - const getSchemaDetails = await this.schemaRepository.getSchemasDetailsBySchemaName(schemaName, orgId); - return getSchemaDetails; + async getSchemaDetailsBySchemaName(schemaName: string, orgId: string): Promise { + try { + const getSchemaDetails = await this.schemaRepository.getSchemasDetailsBySchemaName(schemaName, orgId); + return getSchemaDetails; } catch (error) { throw new RpcException(error.response ? error.response : error); } @@ -921,21 +936,22 @@ export class SchemaService extends BaseService { const pattern = { cmd: 'agent-get-schema' }; - const schemaResponse = await from(this.natsClient - .send(this.schemaServiceProxy, pattern, payload)) + const schemaResponse = await from(this.natsClient.send(this.schemaServiceProxy, pattern, payload)) .pipe( - map((response) => ( - { - response - })) - ).toPromise() - .catch(error => { + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { this.logger.error(`Catch : ${JSON.stringify(error)}`); throw new HttpException( { status: error.statusCode, error: error.message - }, error.error); + }, + error.error + ); }); return schemaResponse; } catch (error) { @@ -950,23 +966,23 @@ export class SchemaService extends BaseService { if (0 === response.schemasCount) { throw new NotFoundException(ResponseMessages.schema.error.notFound); - } - - const schemasDetails = response?.schemasResult.map(schemaAttributeItem => { - const attributes = JSON.parse(schemaAttributeItem.attributes); + } + + const schemasDetails = response?.schemasResult.map((schemaAttributeItem) => { + const attributes = JSON.parse(schemaAttributeItem.attributes); const firstName = schemaAttributeItem?.['organisation']?.userOrgRoles[0]?.user?.firstName; const orgName = schemaAttributeItem?.['organisation'].name; delete schemaAttributeItem?.['organisation']; - return { - ...schemaAttributeItem, - attributes, - organizationName: orgName, - userName: firstName - }; - }); + return { + ...schemaAttributeItem, + attributes, + organizationName: orgName, + userName: firstName + }; + }); - const nextPage:number = Number(schemaSearchCriteria.pageNumber) + 1; + const nextPage: number = Number(schemaSearchCriteria.pageNumber) + 1; const schemasResponse: ISchemasWithPagination = { totalItems: response.schemasCount, @@ -979,24 +995,21 @@ export class SchemaService extends BaseService { }; return schemasResponse; - } catch (error) { this.logger.error(`Error in retrieving schemas by org id: ${error}`); throw new RpcException(error.response ? error.response : error); } } - async getcredDefListBySchemaId( - payload: ISchemaCredDeffSearchInterface - ): Promise { + async getcredDefListBySchemaId(payload: ISchemaCredDeffSearchInterface): Promise { const { schemaSearchCriteria } = payload; - + try { const response = await this.schemaRepository.getSchemasCredDeffList(schemaSearchCriteria); - + if (0 === response.credDefCount) { throw new NotFoundException(ResponseMessages.schema.error.credentialDefinitionNotFound); - } + } const schemasResponse = { totalItems: response.credDefCount, @@ -1009,7 +1022,6 @@ export class SchemaService extends BaseService { }; return schemasResponse; - } catch (error) { this.logger.error(`Error in retrieving credential definition: ${error}`); throw new RpcException(error.response ? error.response : error); @@ -1020,7 +1032,7 @@ export class SchemaService extends BaseService { try { const response = await this.schemaRepository.getAllSchemaDetails(schemaSearchCriteria); - const schemasDetails = response?.schemasResult.map(schemaAttributeItem => { + const schemasDetails = response?.schemasResult.map((schemaAttributeItem) => { const attributes = JSON.parse(schemaAttributeItem.attributes); return { ...schemaAttributeItem, attributes }; }); @@ -1040,8 +1052,6 @@ export class SchemaService extends BaseService { } else { throw new NotFoundException(ResponseMessages.schema.error.notFound); } - - } catch (error) { this.logger.error(`Error in retrieving all schemas: ${error}`); throw new RpcException(error.response ? error.response : error); @@ -1051,39 +1061,43 @@ export class SchemaService extends BaseService { async _getOrgAgentApiKey(orgId: string): Promise { const pattern = { cmd: 'get-org-agent-api-key' }; const payload = { orgId }; - + try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const message = await this.natsClient.send(this.schemaServiceProxy, pattern, payload); return message; } catch (error) { this.logger.error(`catch: ${JSON.stringify(error)}`); - throw new HttpException({ - status: error.status, - error: error.message - }, error.status); + throw new HttpException( + { + status: error.status, + error: error.message + }, + error.status + ); } } - async schemaExist(payload: ISchemaExist): Promise<{ - id: string; - createDateTime: Date; - createdBy: string; - lastChangedDateTime: Date; - lastChangedBy: string; - name: string; - version: string; - attributes: string; - schemaLedgerId: string; - publisherDid: string; - issuerId: string; - orgId: string; - ledgerId: string; - }[]> { + async schemaExist(payload: ISchemaExist): Promise< + { + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + name: string; + version: string; + attributes: string; + schemaLedgerId: string; + publisherDid: string; + issuerId: string; + orgId: string; + ledgerId: string; + }[] + > { try { const schemaExist = await this.schemaRepository.schemaExist(payload); return schemaExist; - } catch (error) { this.logger.error(`Error in schema exist: ${error}`); throw new RpcException(error.response ? error.response : error); @@ -1120,8 +1134,7 @@ export class SchemaService extends BaseService { } } - - async updateSchema(schemaDetails:IUpdateSchema): Promise { + async updateSchema(schemaDetails: IUpdateSchema): Promise { try { const schemaSearchResult = await this.schemaRepository.updateSchema(schemaDetails); @@ -1135,5 +1148,4 @@ export class SchemaService extends BaseService { throw new RpcException(error.response ? error.response : error); } } - -} \ No newline at end of file +} diff --git a/libs/common/src/interfaces/interface.ts b/libs/common/src/interfaces/interface.ts index 75165b3d3..350a2f7b7 100644 --- a/libs/common/src/interfaces/interface.ts +++ b/libs/common/src/interfaces/interface.ts @@ -1,3 +1,5 @@ +import { W3CSchemaDataType } from '@credebl/enum/enum'; + export interface ResponseType { statusCode: number; message: string; @@ -30,3 +32,31 @@ export interface IFormattedResponse { success: boolean; code: number; } + +export interface IW3CAttributeValue { + attributeName: string; + schemaDataType: W3CSchemaDataType; + displayName: string; + isRequired: boolean; + minLength?: number; + maxLength?: number; + pattern?: string; + enum?: string[]; + contentEncoding?: string; + contentMediaType?: string; + minimum?: number; + maximum?: number; + exclusiveMinimum?: number; + exclusiveMaximum?: number; + multipleOf?: number; + minItems?: number; + maxItems?: number; + uniqueItems?: boolean; + items?: IW3CAttributeValue[]; + minProperties?: number; + maxProperties?: number; + additionalProperties?: boolean; + required?: string[]; + dependentRequired?: Record; + properties?: Record; +} From 5865fcfec39ac4d42190460a8decff4a10a38c57 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 4 Apr 2025 18:03:35 +0530 Subject: [PATCH 06/14] fix: security hotspot issue Signed-off-by: bhavanakarwade --- apps/issuance/libs/helpers/attributes.extractor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/issuance/libs/helpers/attributes.extractor.ts b/apps/issuance/libs/helpers/attributes.extractor.ts index 6fe6615bf..6f4867e8f 100644 --- a/apps/issuance/libs/helpers/attributes.extractor.ts +++ b/apps/issuance/libs/helpers/attributes.extractor.ts @@ -61,7 +61,7 @@ function mergeArrayObjects(obj): void { if (Array.isArray(obj[key])) { // Check if the array contains objects with numbered keys const hasNumericKeys = obj[key].some( - (item) => item && 'object' === typeof item && Object.keys(item).some((k) => k.match(/.*?\d+$/)) + (item) => item && 'object' === typeof item && Object.keys(item).some((k) => /\d+$/.test(k)) ); // Only apply the merging logic if we have numeric keys that need merging @@ -70,7 +70,7 @@ function mergeArrayObjects(obj): void { obj[key].forEach((item) => { if ('object' === typeof item && null !== item) { Object.keys(item).forEach((k) => { - const match = k.match(/(.*?)(\d+)$/); + const match = k.match(/^(.+?)(\d+)$/); if (match) { const baseKey = match[1].trim(); const index = parseInt(match[2]); From 80eb1a6d0cdd4ee2da88164b42ed420c6cb65e52 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 4 Apr 2025 19:29:28 +0530 Subject: [PATCH 07/14] feat: added schema builder function Signed-off-by: bhavanakarwade --- .../ledger/libs/helpers/w3c.schema.builder.ts | 434 ++++++++++++++++++ apps/ledger/src/schema/schema.service.ts | 378 +-------------- 2 files changed, 436 insertions(+), 376 deletions(-) create mode 100644 apps/ledger/libs/helpers/w3c.schema.builder.ts diff --git a/apps/ledger/libs/helpers/w3c.schema.builder.ts b/apps/ledger/libs/helpers/w3c.schema.builder.ts new file mode 100644 index 000000000..7a5f7f737 --- /dev/null +++ b/apps/ledger/libs/helpers/w3c.schema.builder.ts @@ -0,0 +1,434 @@ +import { IW3CAttributeValue } from '@credebl/common/interfaces/interface'; +import { ISchemaAttributesFormat } from 'apps/ledger/src/schema/interfaces/schema-payload.interface'; +import { IProductSchema } from 'apps/ledger/src/schema/interfaces/schema.interface'; +import ExclusiveMinimum from 'libs/validations/exclusiveMinimum'; +import MaxItems from 'libs/validations/maxItems'; +import MaxLength from 'libs/validations/maxLength'; +import Minimum from 'libs/validations/minimum'; +import MinItems from 'libs/validations/minItems'; +import MinLength from 'libs/validations/minLength'; +import MultipleOf from 'libs/validations/multipleOf'; +import Pattern from 'libs/validations/pattern'; +import UniqueItems from 'libs/validations/uniqueItems'; + +export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: string, description: string): object { + // Function to apply validations based on attribute properties + const applyValidations = (attribute, propertyObj): ISchemaAttributesFormat => { + const context = { ...propertyObj }; + + // Apply string validations + if ('string' === attribute.schemaDataType.toLowerCase()) { + if (attribute.minLength !== undefined) { + const validation = new MinLength(attribute.minLength); + validation.json(context); + } + + if (attribute.maxLength !== undefined) { + const validation = new MaxLength(attribute.maxLength); + validation.json(context); + } + + if (attribute.pattern !== undefined) { + const validation = new Pattern(attribute.pattern); + validation.json(context); + } + } + + // Apply number validations + if (['number', 'integer'].includes(attribute.schemaDataType.toLowerCase())) { + if (attribute.minimum !== undefined) { + const validation = new Minimum(attribute.minimum); + validation.json(context); + } + + if (attribute.exclusiveMinimum !== undefined) { + const validation = new ExclusiveMinimum(attribute.exclusiveMinimum); + validation.json(context); + } + + if (attribute.multipleOf !== undefined) { + const validation = new MultipleOf(attribute.multipleOf); + validation.json(context); + } + } + + // Apply array validations + if ('array' === attribute.schemaDataType.toLowerCase()) { + if (attribute.minItems !== undefined) { + const validation = new MinItems(attribute.minItems); + validation.json(context); + } + + if (attribute.maxItems !== undefined) { + const validation = new MaxItems(attribute.maxItems); + validation.json(context); + } + + if (attribute.uniqueItems !== undefined) { + const validation = new UniqueItems(attribute.uniqueItems); + validation.json(context); + } + } + + return context; + }; + + // Function to recursively process attributes + const processAttributes = (attrs: IW3CAttributeValue[]): IProductSchema => { + if (!Array.isArray(attrs)) { + return { properties: {}, required: [] }; + } + + const properties = {}; + const required = []; + + attrs.forEach((attribute) => { + const { attributeName, schemaDataType, isRequired, displayName } = attribute; + + // Add to required array if isRequired is true + if (isRequired) { + required.push(attributeName); + } + + // Create base property object with common fields + const baseProperty = { + type: schemaDataType.toLowerCase(), + title: displayName || attributeName, + description: `${attributeName} field` + }; + + // Handle different attribute types + if (['string', 'number', 'boolean', 'integer'].includes(schemaDataType.toLowerCase())) { + // Apply validations to the base property + properties[attributeName] = applyValidations(attribute, baseProperty); + } else if ('datetime-local' === schemaDataType.toLowerCase()) { + properties[attributeName] = { + ...baseProperty, + type: 'string', + format: 'date-time' + }; + } else if ('array' === schemaDataType.toLowerCase() && attribute.items) { + // Process array items + const arrayItemProperties = {}; + const arrayItemRequired = []; + + if (Array.isArray(attribute.items)) { + // If items is an array, process each item + attribute.items.forEach((item) => { + if ('object' === item.schemaDataType.toLowerCase() && item.properties) { + // Process object properties + const nestedObjProperties = {}; + const nestedObjRequired = []; + + // Process properties object + Object.keys(item.properties).forEach((propKey) => { + const prop = item.properties[propKey]; + + if (prop.isRequired) { + nestedObjRequired.push(prop.attributeName); + } + + if ('array' === prop.schemaDataType.toLowerCase() && prop.items) { + // Handle nested array + const nestedArrayResult = processAttributes(prop.items); + + nestedObjProperties[prop.attributeName] = { + type: prop.schemaDataType.toLowerCase(), + title: prop.displayName || prop.attributeName, + description: `${prop.attributeName} field`, + items: { + type: 'object', + properties: nestedArrayResult.properties + } + }; + + if (0 < nestedArrayResult.required.length) { + nestedObjProperties[prop.attributeName].items.required = nestedArrayResult.required; + } + } else { + // Handle basic property + nestedObjProperties[prop.attributeName] = { + type: prop.schemaDataType.toLowerCase(), + title: prop.displayName || prop.attributeName, + description: `${prop.attributeName} field` + }; + + // Apply validations + nestedObjProperties[prop.attributeName] = applyValidations( + prop, + nestedObjProperties[prop.attributeName] + ); + } + }); + + // Add object to array item properties + arrayItemProperties[item.attributeName] = { + type: 'object', + title: item.displayName || item.attributeName, + description: `${item.attributeName} field`, + properties: nestedObjProperties + }; + + if (0 < nestedObjRequired.length) { + arrayItemProperties[item.attributeName].required = nestedObjRequired; + } + + if (item.isRequired) { + arrayItemRequired.push(item.attributeName); + } + } else { + // Handle basic array item + arrayItemProperties[item.attributeName] = { + type: item.schemaDataType.toLowerCase(), + title: item.displayName || item.attributeName, + description: `${item.attributeName} field` + }; + + // Apply validations + arrayItemProperties[item.attributeName] = applyValidations(item, arrayItemProperties[item.attributeName]); + + if (item.isRequired) { + arrayItemRequired.push(item.attributeName); + } + } + }); + } + + properties[attributeName] = { + ...baseProperty, + items: { + type: 'object', + properties: arrayItemProperties + } + }; + + // Apply array-specific validations + properties[attributeName] = applyValidations(attribute, properties[attributeName]); + + // Add required properties to the items schema if any + if (0 < arrayItemRequired.length) { + properties[attributeName].items.required = arrayItemRequired; + } + } else if ('object' === schemaDataType.toLowerCase() && attribute.properties) { + const nestedProperties = {}; + const nestedRequired = []; + + // Process each property in the object + Object.keys(attribute.properties).forEach((propKey) => { + const prop = attribute.properties[propKey]; + + // Add to nested required array if isRequired is true + if (prop.isRequired) { + nestedRequired.push(propKey); + } + + // Create base property for nested object + const nestedBaseProperty = { + type: prop.schemaDataType.toLowerCase(), + title: prop.displayName || prop.attributeName, + description: `${prop.attributeName} field` + }; + + if ('array' === prop.schemaDataType.toLowerCase() && prop.items) { + // Handle nested arrays + const result = processAttributes(prop.items); + + nestedProperties[prop.attributeName] = { + ...nestedBaseProperty, + type: 'array', + items: { + type: 'object', + properties: result.properties + } + }; + + // Apply array-specific validations + nestedProperties[prop.attributeName] = applyValidations(prop, nestedProperties[prop.attributeName]); + + // Add required properties to the items schema if any + if (0 < result.required.length) { + nestedProperties[prop.attributeName].items.required = result.required; + } + } else { + // Handle basic properties with validations + nestedProperties[prop.attributeName] = applyValidations(prop, nestedBaseProperty); + } + }); + + properties[attributeName] = { + ...baseProperty, + type: 'object', + properties: nestedProperties + }; + + // Add required properties to the object schema if any + if (0 < nestedRequired.length) { + properties[attributeName].required = nestedRequired; + } + } + }); + + return { properties, required }; + }; + + // Process all attributes + const result = processAttributes(attributes); + const { properties } = result; + // Add id property to required fields along with other required fields + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const required = ['id', ...result.required]; + + // Add id property + properties['id'] = { + type: 'string', + format: 'uri' + }; + + // Create the final W3C Schema + const W3CSchema = { + $schema: 'https://json-schema.org/draft/2020-12/schema', + $id: `https://example.com/schemas/${schemaName.toLowerCase().replace(/\s+/g, '-')}`, + title: schemaName, + description, + type: 'object', + required: ['@context', 'issuer', 'issuanceDate', 'type', 'credentialSubject'], + properties: { + '@context': { + $ref: '#/$defs/context' + }, + type: { + type: 'array', + items: { + anyOf: [ + { + const: 'VerifiableCredential' + }, + { + const: schemaName + } + ] + } + }, + credentialSubject: { + $ref: '#/$defs/credentialSubject' + }, + id: { + type: 'string', + format: 'uri' + }, + issuer: { + $ref: '#/$defs/uriOrId' + }, + issuanceDate: { + type: 'string', + format: 'date-time' + }, + expirationDate: { + type: 'string', + format: 'date-time' + }, + credentialStatus: { + $ref: '#/$defs/credentialStatus' + }, + credentialSchema: { + $ref: '#/$defs/credentialSchema' + } + }, + $defs: { + context: { + type: 'array', + prefixItems: [ + { + const: 'https://www.w3.org/2018/credentials/v1' + } + ], + items: { + oneOf: [ + { + type: 'string', + format: 'uri' + }, + { + type: 'object' + }, + { + type: 'array', + items: false + } + ] + }, + minItems: 1, + uniqueItems: true + }, + credentialSubject: { + type: 'object', + required: ['id', ...result.required], + additionalProperties: false, + properties + }, + credentialSchema: { + oneOf: [ + { + $ref: '#/$defs/idAndType' + }, + { + type: 'array', + items: { + $ref: '#/$defs/idAndType' + }, + minItems: 1, + uniqueItems: true + } + ] + }, + credentialStatus: { + oneOf: [ + { + $ref: '#/$defs/idAndType' + }, + { + type: 'array', + items: { + $ref: '#/$defs/idAndType' + }, + minItems: 1, + uniqueItems: true + } + ] + }, + idAndType: { + type: 'object', + required: ['id', 'type'], + properties: { + id: { + type: 'string', + format: 'uri' + }, + type: { + type: 'string' + } + } + }, + uriOrId: { + oneOf: [ + { + type: 'string', + format: 'uri' + }, + { + type: 'object', + required: ['id'], + properties: { + id: { + type: 'string', + format: 'uri' + } + } + } + ] + } + } + }; + + return W3CSchema; +} diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index d6c436029..198678295 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -15,7 +15,6 @@ import { Prisma, schema } from '@prisma/client'; import { ISaveSchema, ISchema, - ISchemaAttributesFormat, ISchemaCredDeffSearchInterface, ISchemaExist, ISchemaSearchCriteria, @@ -26,7 +25,6 @@ import { ICreateSchema, ICreateW3CSchema, IGenericSchema, - IProductSchema, IUpdateSchema, IUserRequestInterface, UpdateSchemaResponse @@ -57,16 +55,7 @@ import { networkNamespace } from '@credebl/common/common.utils'; import { checkDidLedgerAndNetwork } from '@credebl/common/cast.helper'; import { NATSClient } from '@credebl/common/NATSClient'; import { from } from 'rxjs'; -import UniqueItems from 'libs/validations/uniqueItems'; -import MaxItems from 'libs/validations/maxItems'; -import MinItems from 'libs/validations/minItems'; -import MultipleOf from 'libs/validations/multipleOf'; -import ExclusiveMinimum from 'libs/validations/exclusiveMinimum'; -import Minimum from 'libs/validations/minimum'; -import Pattern from 'libs/validations/pattern'; -import MaxLength from 'libs/validations/maxLength'; -import MinLength from 'libs/validations/minLength'; -import { IW3CAttributeValue } from '@credebl/common/interfaces/interface'; +import { w3cSchemaBuilder } from 'apps/ledger/libs/helpers/w3c.schema.builder'; @Injectable() export class SchemaService extends BaseService { @@ -319,7 +308,7 @@ export class SchemaService extends BaseService { url = `${agentEndPoint}${CommonConstants.SHARED_CREATE_POLYGON_W3C_SCHEMA}${tenantId}`; } - const schemaObject = await this.w3cSchemaBuilder(attributes, schemaName, description); + const schemaObject = await w3cSchemaBuilder(attributes, schemaName, description); if (!schemaObject) { throw new BadRequestException(ResponseMessages.schema.error.schemaBuilder, { cause: new Error(), @@ -370,369 +359,6 @@ export class SchemaService extends BaseService { } } - private async w3cSchemaBuilder( - attributes: IW3CAttributeValue[], - schemaName: string, - description: string - ): Promise { - // Function to apply validations based on attribute properties - const applyValidations = (attribute, propertyObj): ISchemaAttributesFormat => { - const context = { ...propertyObj }; - - // Apply string validations - if ('string' === attribute.schemaDataType.toLowerCase()) { - if (attribute.minLength !== undefined) { - const validation = new MinLength(attribute.minLength); - validation.json(context); - } - - if (attribute.maxLength !== undefined) { - const validation = new MaxLength(attribute.maxLength); - validation.json(context); - } - - if (attribute.pattern !== undefined) { - const validation = new Pattern(attribute.pattern); - validation.json(context); - } - } - - // Apply number validations - if (['number', 'integer'].includes(attribute.schemaDataType.toLowerCase())) { - if (attribute.minimum !== undefined) { - const validation = new Minimum(attribute.minimum); - validation.json(context); - } - - if (attribute.exclusiveMinimum !== undefined) { - const validation = new ExclusiveMinimum(attribute.exclusiveMinimum); - validation.json(context); - } - - if (attribute.multipleOf !== undefined) { - const validation = new MultipleOf(attribute.multipleOf); - validation.json(context); - } - } - - // Apply array validations - if ('array' === attribute.schemaDataType.toLowerCase()) { - if (attribute.minItems !== undefined) { - const validation = new MinItems(attribute.minItems); - validation.json(context); - } - - if (attribute.maxItems !== undefined) { - const validation = new MaxItems(attribute.maxItems); - validation.json(context); - } - - if (attribute.uniqueItems !== undefined) { - const validation = new UniqueItems(attribute.uniqueItems); - validation.json(context); - } - } - - return context; - }; - - // Function to recursively process attributes - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const processAttributes = (attrs: IW3CAttributeValue[]): IProductSchema => { - if (!Array.isArray(attrs)) { - return { properties: {}, required: [] }; - } - - const properties = {}; - const required = []; - - attrs.forEach((attribute, index) => { - const { attributeName, schemaDataType, isRequired, displayName } = attribute; - - // Add to required array if isRequired is true - if (isRequired) { - required.push(attributeName); - } - - // Create base property object with common fields - const baseProperty = { - type: schemaDataType.toLowerCase(), - order: index, - title: displayName || attributeName, - description: `${attributeName} field` - }; - - // Handle different attribute types - if (['string', 'number', 'boolean', 'integer'].includes(schemaDataType.toLowerCase())) { - // Apply validations to the base property - properties[attributeName] = applyValidations(attribute, baseProperty); - } else if ('datetime-local' === schemaDataType.toLowerCase()) { - properties[attributeName] = { - ...baseProperty, - type: 'string', - format: 'date-time' - }; - } else if ('array' === schemaDataType.toLowerCase() && attribute.items) { - const result = processAttributes(attribute.items); - - properties[attributeName] = { - ...baseProperty, - type: 'array', - items: { - type: 'object', - properties: result.properties - } - }; - - // Apply array-specific validations - properties[attributeName] = applyValidations(attribute, properties[attributeName]); - - // Add required properties to the items schema if any - if (0 < result.required.length) { - properties[attributeName].items.required = result.required; - } - } else if ('object' === schemaDataType.toLowerCase() && attribute.properties) { - const nestedProperties = {}; - const nestedRequired = []; - - // Process each property in the object - Object.keys(attribute.properties).forEach((propKey) => { - const prop = attribute.properties[propKey]; - - // Add to nested required array if isRequired is true - if (prop.isRequired) { - nestedRequired.push(propKey); - } - - // Create base property for nested object - const nestedBaseProperty = { - type: prop.schemaDataType.toLowerCase(), - title: prop.displayName || prop.attributeName, - description: `${prop.attributeName} field` - }; - - if ('array' === prop.schemaDataType.toLowerCase() && prop.items) { - // Handle nested arrays - const result = processAttributes(prop.items); - - nestedProperties[propKey] = { - ...nestedBaseProperty, - type: 'array', - items: { - type: 'object', - properties: result.properties - } - }; - - // Apply array-specific validations - nestedProperties[propKey] = applyValidations(prop, nestedProperties[propKey]); - - // Add required properties to the items schema if any - if (0 < result.required.length) { - nestedProperties[propKey].items.required = result.required; - } - } else { - // Handle basic properties with validations - nestedProperties[propKey] = applyValidations(prop, nestedBaseProperty); - } - }); - - properties[attributeName] = { - ...baseProperty, - type: 'object', - properties: nestedProperties - }; - - // Add required properties to the object schema if any - if (0 < nestedRequired.length) { - properties[attributeName].required = nestedRequired; - } - } - }); - - return { properties, required }; - }; - - // Process all attributes - const result = processAttributes(attributes); - const { properties } = result; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const required = ['id', ...result.required]; - - // Add id property - properties['id'] = { - type: 'string', - format: 'uri' - }; - - const date = new Date().toISOString(); - const schemaNameObject = {}; - schemaNameObject[schemaName] = { - const: schemaName - }; - - const W3CSchema = { - $schema: 'https://json-schema.org/draft/2020-12/schema', - $id: `${date}-${schemaName}`, - $vocabulary: { - 'https://json-schema.org/draft/2020-12/vocab/core': true, - 'https://json-schema.org/draft/2020-12/vocab/applicator': true, - 'https://json-schema.org/draft/2020-12/vocab/unevaluated': true, - 'https://json-schema.org/draft/2020-12/vocab/validation': true, - 'https://json-schema.org/draft/2020-12/vocab/meta-data': true, - 'https://json-schema.org/draft/2020-12/vocab/format-annotation': true, - 'https://json-schema.org/draft/2020-12/vocab/content': true - }, - type: 'object', - required: ['@context', 'issuer', 'issuanceDate', 'type', 'credentialSubject'], - properties: { - '@context': { - $ref: '#/$defs/context' - }, - type: { - type: 'array', - items: { - anyOf: [ - { - $ref: '#/$defs/VerifiableCredential' - }, - { - const: `#/$defs/$${schemaName}` - } - ] - } - }, - credentialSubject: { - $ref: '#/$defs/credentialSubject' - }, - id: { - type: 'string', - format: 'uri' - }, - issuer: { - $ref: '#/$defs/uriOrId' - }, - issuanceDate: { - type: 'string', - format: 'date-time' - }, - expirationDate: { - type: 'string', - format: 'date-time' - }, - credentialStatus: { - $ref: '#/$defs/credentialStatus' - }, - credentialSchema: { - $ref: '#/$defs/credentialSchema' - } - }, - $defs: { - context: { - type: 'array', - items: [ - { - const: 'https://www.w3.org/2018/credentials/v1' - } - ], - additionalItems: { - oneOf: [ - { - type: 'string', - format: 'uri' - }, - { - type: 'object' - }, - { - type: 'array', - items: { - $ref: '#/$defs/context' - } - } - ] - }, - minItems: 1, - uniqueItems: true - }, - credentialSubject: { - type: 'object', - required: ['id'], - additionalProperties: false, - properties - }, - VerifiableCredential: { - const: 'VerifiableCredential' - }, - credentialSchema: { - oneOf: [ - { - $ref: '#/definitions/idAndType' - }, - { - type: 'array', - items: { - $ref: '#/definitions/idAndType' - }, - minItems: 1, - uniqueItems: true - } - ] - }, - credentialStatus: { - oneOf: [ - { - $ref: '#/definitions/idAndType' - }, - { - type: 'array', - items: { - $ref: '#/definitions/idAndType' - }, - minItems: 1, - uniqueItems: true - } - ] - }, - idAndType: { - type: 'object', - required: ['id', 'type'], - properties: { - id: { - type: 'string', - format: 'uri' - }, - type: { - type: 'string' - } - } - }, - uriOrId: { - oneOf: [ - { - type: 'string', - format: 'uri' - }, - { - type: 'object', - required: ['id'], - properties: { - id: { - type: 'string', - format: 'uri' - } - } - } - ] - }, - ...schemaNameObject - }, - title: schemaName, - description: `${description}` - }; - return W3CSchema; - } - private async storeW3CSchemas(schemaDetails, user, orgId, attributes, alias): Promise { let ledgerDetails; const schemaServerUrl = `${process.env.SCHEMA_FILE_SERVER_URL}${schemaDetails.schemaId}`; From 6c3a2f82b33ef5ce1cce6a3c75a624e9ae8c1cfb Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Fri, 4 Apr 2025 19:45:04 +0530 Subject: [PATCH 08/14] fix: resolved issue Signed-off-by: bhavanakarwade --- apps/issuance/libs/helpers/attributes.extractor.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/apps/issuance/libs/helpers/attributes.extractor.ts b/apps/issuance/libs/helpers/attributes.extractor.ts index 6f4867e8f..9f7f4f5d0 100644 --- a/apps/issuance/libs/helpers/attributes.extractor.ts +++ b/apps/issuance/libs/helpers/attributes.extractor.ts @@ -61,7 +61,7 @@ function mergeArrayObjects(obj): void { if (Array.isArray(obj[key])) { // Check if the array contains objects with numbered keys const hasNumericKeys = obj[key].some( - (item) => item && 'object' === typeof item && Object.keys(item).some((k) => /\d+$/.test(k)) + (item) => item && 'object' === typeof item && Object.keys(item).some((k) => /\d{1,10}$/.test(k)) ); // Only apply the merging logic if we have numeric keys that need merging @@ -70,7 +70,8 @@ function mergeArrayObjects(obj): void { obj[key].forEach((item) => { if ('object' === typeof item && null !== item) { Object.keys(item).forEach((k) => { - const match = k.match(/^(.+?)(\d+)$/); + const match = k.match(/^([^0-9]+)(\d{1,10})$/); + if (match) { const baseKey = match[1].trim(); const index = parseInt(match[2]); From 65cbb820c5e9df3a7fdad9807707bab89cc6502c Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Wed, 9 Apr 2025 11:45:53 +0530 Subject: [PATCH 09/14] refactor: modify extract attributes function Signed-off-by: bhavanakarwade --- .../libs/helpers/attributes.extractor.ts | 314 ++++++++++++------ 1 file changed, 218 insertions(+), 96 deletions(-) diff --git a/apps/issuance/libs/helpers/attributes.extractor.ts b/apps/issuance/libs/helpers/attributes.extractor.ts index 9f7f4f5d0..5c62aaa93 100644 --- a/apps/issuance/libs/helpers/attributes.extractor.ts +++ b/apps/issuance/libs/helpers/attributes.extractor.ts @@ -6,11 +6,10 @@ export function extractAttributeNames( attributeObj, parentKey: string = '', result: Set = new Set(), - inNestedArray: boolean = false // Track if we're inside a nested array + inNestedArray: boolean = false ): string[] { if (Array.isArray(attributeObj)) { attributeObj.forEach((item) => { - // For array items, pass through the nested array flag extractAttributeNames(item, parentKey, result, inNestedArray); }); } else if ('object' === typeof attributeObj && null !== attributeObj) { @@ -23,17 +22,9 @@ export function extractAttributeNames( } if (attributeObj.hasOwnProperty('items') && Array.isArray(attributeObj.items)) { - const isNestedArray = parentKey.includes(CommonConstants.NESTED_ATTRIBUTE_SEPARATOR); - - attributeObj.items.forEach((item, index) => { - // For items in nested arrays, always use index 0 - const useIndex = isNestedArray ? 0 : index; - extractAttributeNames( - item, - `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${useIndex}`, - result, - true // Mark that we're now in a nested array context - ); + // Always use index 0 for items in an array + attributeObj.items.forEach((item) => { + extractAttributeNames(item, `${newParentKey}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}0`, result, true); }); } else if (attributeObj.hasOwnProperty('properties')) { Object.entries(attributeObj.properties).forEach(([key, value]) => { @@ -54,140 +45,271 @@ export function extractAttributeNames( return Array.from(result); } -//For merging nested objects with numbered keys nto an array of objects +// Handles both explicitly indexed arrays and implicit arrays function mergeArrayObjects(obj): void { + if (!obj || 'object' !== typeof obj) { + return; + } + + // First pass: Convert objects with numeric keys to arrays for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { - if (Array.isArray(obj[key])) { - // Check if the array contains objects with numbered keys - const hasNumericKeys = obj[key].some( - (item) => item && 'object' === typeof item && Object.keys(item).some((k) => /\d{1,10}$/.test(k)) - ); - - // Only apply the merging logic if we have numeric keys that need merging - if (hasNumericKeys) { - const mergedArray = []; - obj[key].forEach((item) => { - if ('object' === typeof item && null !== item) { - Object.keys(item).forEach((k) => { - const match = k.match(/^([^0-9]+)(\d{1,10})$/); - - if (match) { - const baseKey = match[1].trim(); - const index = parseInt(match[2]); - if (!mergedArray[index]) { - mergedArray[index] = {}; - } - mergedArray[index][baseKey] = item[k]; - } else { - if (!mergedArray[0]) { - mergedArray[0] = {}; - } - mergedArray[0][k] = item[k]; - } + const value = obj[key]; + + // Skip non-objects + if (!value || 'object' !== typeof value) { + continue; + } + + // Process arrays + if (Array.isArray(value)) { + value.forEach((item) => { + if (item && 'object' === typeof item) { + mergeArrayObjects(item); + } + }); + } else { + // Process objects + // Check if this object has numeric keys + const keys = Object.keys(value); + const numericKeys = keys.filter((k) => /^\d+$/.test(k)); + + if (0 < numericKeys.length) { + // Has numeric keys - convert to array + const tempArray = []; + + // First, add all numeric keys to the array + numericKeys + .sort((a, b) => parseInt(a) - parseInt(b)) + .forEach((k) => { + const index = parseInt(k); + tempArray[index] = value[k]; + + // Process recursively + if (value[k] && 'object' === typeof value[k]) { + mergeArrayObjects(value[k]); + } + }); + + // Then add all non-numeric keys to every array element + const nonNumericKeys = keys.filter((k) => !/^\d+$/.test(k)); + if (0 < nonNumericKeys.length) { + tempArray.forEach((item, index) => { + if (!item || 'object' !== typeof item) { + tempArray[index] = {}; + } + + nonNumericKeys.forEach((k) => { + tempArray[index][k] = value[k]; }); - } - }); - obj[key] = mergedArray; + }); + } + + // Replace the object with our array + obj[key] = tempArray; + } else { + // No numeric keys - process recursively + mergeArrayObjects(value); } + } + } + } - // Recursively process array items that are objects + // Second pass: Look for arrays with objects that have common prefixes with numeric suffixes + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + if (Array.isArray(obj[key])) { + // Look for patterns like "field1", "field2" in each array element obj[key].forEach((item) => { - if ('object' === typeof item && null !== item) { + if (item && 'object' === typeof item) { + const keys = Object.keys(item); + const prefixMap = new Map(); + + // Group keys by prefix + keys.forEach((k) => { + const match = k.match(/^([^0-9]+)(\d{1,10})$/); + if (match) { + // eslint-disable-next-line prefer-destructuring + const prefix = match[1]; + const index = parseInt(match[2]); + if (!prefixMap.has(prefix)) { + prefixMap.set(prefix, []); + } + prefixMap.get(prefix).push({ key: k, index }); + } + }); + + // Convert grouped prefixes to arrays + for (const [prefix, matches] of prefixMap.entries()) { + if (0 < matches.length) { + const tempArray = []; + + // Sort by index and populate array + matches + .sort((a, b) => a.index - b.index) + .forEach((match) => { + tempArray[match.index] = item[match.key]; + delete item[match.key]; + }); + + // Set the array on the item + item[prefix] = tempArray; + } + } + + // Process recursively mergeArrayObjects(item); } }); - } else if ('object' === typeof obj[key] && null !== obj[key]) { - mergeArrayObjects(obj[key]); } } } } -// function to converts a flattened CSV row into a nested object. +// Helper function to process remaining parts of a key path +function processRemainingParts(obj, parts: string[], value): void { + let current = obj; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + + // If this is the last part, set the value + if (i === parts.length - 1) { + current[part] = value; + return; + } + + // Check if next part is a number (array index) + const isNextPartNumeric = i < parts.length - 1 && !isNaN(Number(parts[i + 1])); + + if (isNextPartNumeric) { + // This is an array index, create array if needed + if (!current[part]) { + current[part] = []; + } + + const index = parseInt(parts[i + 1]); + while (current[part].length <= index) { + current[part].push({}); + } + + // Update the local variable instead of the parameter + current = current[part][index]; + i++; // Skip the index part + } else { + // This is a regular object property + if (!current[part]) { + current[part] = {}; + } + + current = current[part]; + } + } +} + +// Function to convert a flattened CSV row into a nested object export function unflattenCsvRow(row: object): object { const result: object = {}; const groupedKeys: Record = {}; + // First pass: handle simple keys and identify complex keys for (const key in row) { if (Object.prototype.hasOwnProperty.call(row, key)) { + // Skip empty values + if ('' === row[key]) { + continue; + } + + // Handle email identifier specially if (TemplateIdentifier.EMAIL_COLUMN === key) { result[key] = row[key]; continue; } - const keyParts = key.split(`${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}`); + const keyParts = key.split(CommonConstants.NESTED_ATTRIBUTE_SEPARATOR); + // Handle array notation: key~index~otherParts if (1 < keyParts.length && !isNaN(Number(keyParts[1]))) { // eslint-disable-next-line prefer-destructuring const arrayName = keyParts[0]; // eslint-disable-next-line prefer-destructuring const arrayIndex = keyParts[1]; - const groupKey = `${arrayName}~${arrayIndex}`; + const groupKey = `${arrayName}${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}${arrayIndex}`; + if (!groupedKeys[groupKey]) { groupedKeys[groupKey] = []; } groupedKeys[groupKey].push(key); } else { - let currentLevel = result; - for (let i = 0; i < keyParts.length; i++) { - const part = keyParts[i]; - if (i === keyParts.length - 1) { - if ('' !== row[key]) { + // Handle implicit array notation or simple nested keys + + // Check if this key has any numeric part that might indicate an array + const hasArrayPart = keyParts.some((part, index) => 0 < index && !isNaN(Number(part)) && '' !== part); + + if (hasArrayPart) { + // Handle as potential array, but we'll process it in the second pass + if (!groupedKeys[keyParts[0]]) { + groupedKeys[keyParts[0]] = []; + } + groupedKeys[keyParts[0]].push(key); + } else { + // Handle as simple nested key (no arrays) + let currentLevel = result; + for (let i = 0; i < keyParts.length; i++) { + const part = keyParts[i]; + if (i === keyParts.length - 1) { currentLevel[part] = row[key]; + } else { + if (!currentLevel[part]) { + currentLevel[part] = {}; + } + currentLevel = currentLevel[part]; } - } else { - if (!currentLevel[part]) { - currentLevel[part] = {}; - } - currentLevel = currentLevel[part]; } } } } } + // Second pass: process explicitly indexed arrays for (const grpKey in groupedKeys) { if (Object.prototype.hasOwnProperty.call(groupedKeys, grpKey)) { - const [arrayName, arrayIndex] = grpKey.split('~'); const keys = groupedKeys[grpKey]; - if (!result[arrayName]) { - result[arrayName] = []; - } - const index = parseInt(arrayIndex); - while (result[arrayName].length <= index) { - result[arrayName].push({}); - } - for (const key of keys) { - if ('' !== row[key]) { - const keyParts = key.split(`${CommonConstants.NESTED_ATTRIBUTE_SEPARATOR}`); + // For explicit indexed arrays (format: arrayName~index~...) + if (grpKey.includes(CommonConstants.NESTED_ATTRIBUTE_SEPARATOR)) { + const [arrayName, arrayIndex] = grpKey.split(CommonConstants.NESTED_ATTRIBUTE_SEPARATOR); + + if (!result[arrayName]) { + result[arrayName] = []; + } + + const index = parseInt(arrayIndex); + while (result[arrayName].length <= index) { + result[arrayName].push({}); + } + + for (const key of keys) { + const keyParts = key.split(CommonConstants.NESTED_ATTRIBUTE_SEPARATOR); const remainingParts = keyParts.slice(2); - let currentLevel = result[arrayName][index]; + const currentLevel = result[arrayName][index]; - for (let i = 0; i < remainingParts.length; i++) { - const part = remainingParts[i]; - if (i === remainingParts.length - 1) { - currentLevel[part] = row[key]; - } else { - if (!isNaN(Number(remainingParts[i + 1]))) { - if (!currentLevel[part]) { - currentLevel[part] = []; - } - const nestedIndex = parseInt(remainingParts[i + 1]); - while (currentLevel[part].length <= nestedIndex) { - currentLevel[part].push({}); - } - currentLevel = currentLevel[part][nestedIndex]; - i++; - } else { - if (!currentLevel[part]) { - currentLevel[part] = {}; - } - currentLevel = currentLevel[part]; - } - } - } + processRemainingParts(currentLevel, remainingParts, row[key]); + } + } else { + // For implicit arrays (format: arrayName~field~0~...) + const arrayName = grpKey; + + if (!result[arrayName]) { + result[arrayName] = {}; + } + + for (const key of keys) { + const keyParts = key.split(CommonConstants.NESTED_ATTRIBUTE_SEPARATOR); + const remainingParts = keyParts.slice(1); + const currentLevel = result[arrayName]; + + processRemainingParts(currentLevel, remainingParts, row[key]); } } } From 129c5631c1b54d2a609c279837f95280a2e6d76e Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Wed, 9 Apr 2025 18:35:31 +0530 Subject: [PATCH 10/14] fix: destructured objects Signed-off-by: bhavanakarwade --- .../src/issuance/issuance.service.ts | 374 +++++++++++------- 1 file changed, 235 insertions(+), 139 deletions(-) diff --git a/apps/api-gateway/src/issuance/issuance.service.ts b/apps/api-gateway/src/issuance/issuance.service.ts index 543268f6f..e9077adf3 100644 --- a/apps/api-gateway/src/issuance/issuance.service.ts +++ b/apps/api-gateway/src/issuance/issuance.service.ts @@ -3,174 +3,270 @@ 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, OOBCredentialDtoWithEmail, OOBIssueCredentialDto, PreviewFileDetails, TemplateDetails } from './dtos/issuance.dto'; -import { FileExportResponse, IIssuedCredentialSearchParams, IReqPayload, ITemplateFormat, IssueCredentialType, UploadedFileDetails } from './interfaces'; -import { ICredentialOfferResponse, IDeletedIssuanceRecords, IIssuedCredential } from '@credebl/common/interfaces/issuance.interface'; +import { + ClientDetails, + FileParameter, + IssuanceDto, + OOBCredentialDtoWithEmail, + OOBIssueCredentialDto, + PreviewFileDetails, + TemplateDetails +} from './dtos/issuance.dto'; +import { + FileExportResponse, + IIssuedCredentialSearchParams, + IReqPayload, + ITemplateFormat, + IssueCredentialType, + UploadedFileDetails +} from './interfaces'; +import { + ICredentialOfferResponse, + IDeletedIssuanceRecords, + IIssuedCredential +} from '@credebl/common/interfaces/issuance.interface'; import { IssueCredentialDto } from './dtos/multi-connection.dto'; import { user } from '@prisma/client'; import { NATSClient } from '@credebl/common/NATSClient'; @Injectable() export class IssuanceService extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly issuanceProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('IssuanceService'); + } + sendCredentialCreateOffer( + issueCredentialDto: IssueCredentialDto, + user: IUserRequest + ): Promise { + const payload = { + comment: issueCredentialDto.comment, + credentialDefinitionId: issueCredentialDto.credentialDefinitionId, + credentialData: issueCredentialDto.credentialData, + orgId: issueCredentialDto.orgId, + protocolVersion: issueCredentialDto.protocolVersion, + autoAcceptCredential: issueCredentialDto.autoAcceptCredential, + credentialType: issueCredentialDto.credentialType, + user + }; - constructor( - @Inject('NATS_CLIENT') private readonly issuanceProxy: ClientProxy, - private readonly natsClient : NATSClient - ) { - super('IssuanceService'); - } - - sendCredentialCreateOffer(issueCredentialDto: IssueCredentialDto, user: IUserRequest): Promise { + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'send-credential-create-offer', payload); + } - const payload = { comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, credentialData: issueCredentialDto.credentialData, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType, user }; + sendCredentialOutOfBand(issueCredentialDto: OOBIssueCredentialDto): Promise<{ + response: object; + }> { + const { + attributes, + comment, + options, + credentialDefinitionId, + orgId, + protocolVersion, + goalCode, + parentThreadId, + willConfirm, + label, + autoAcceptCredential, + credentialType, + isShortenUrl, + reuseConnection, + credential, + isValidateSchema + } = issueCredentialDto; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'send-credential-create-offer', payload); + let payload; + if (IssueCredentialType.INDY === issueCredentialDto.credentialType) { + payload = { + attributes, + comment, + credentialDefinitionId, + orgId, + protocolVersion, + goalCode, + parentThreadId, + willConfirm, + label, + autoAcceptCredential, + credentialType, + isShortenUrl, + reuseConnection + }; } - - sendCredentialOutOfBand(issueCredentialDto: OOBIssueCredentialDto): Promise<{ - response: object; - }> { - let payload; - if (IssueCredentialType.INDY === issueCredentialDto.credentialType) { - payload = { attributes: issueCredentialDto.attributes, comment: issueCredentialDto.comment, credentialDefinitionId: issueCredentialDto.credentialDefinitionId, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType, isShortenUrl: issueCredentialDto.isShortenUrl, reuseConnection : issueCredentialDto.reuseConnection }; - } - if (IssueCredentialType.JSONLD === issueCredentialDto.credentialType) { - payload = { credential: issueCredentialDto.credential, options: issueCredentialDto.options, comment: issueCredentialDto.comment, orgId: issueCredentialDto.orgId, protocolVersion: issueCredentialDto.protocolVersion, goalCode: issueCredentialDto.goalCode, parentThreadId: issueCredentialDto.parentThreadId, willConfirm: issueCredentialDto.willConfirm, label: issueCredentialDto.label, autoAcceptCredential: issueCredentialDto.autoAcceptCredential, credentialType: issueCredentialDto.credentialType, isShortenUrl: issueCredentialDto.isShortenUrl, reuseConnection : issueCredentialDto.reuseConnection, isValidateSchema: issueCredentialDto.isValidateSchema}; - } - - return this.natsClient.sendNats(this.issuanceProxy, 'send-credential-create-offer-oob', payload); + if (IssueCredentialType.JSONLD === issueCredentialDto.credentialType) { + payload = { + credential, + options, + comment, + orgId, + protocolVersion, + goalCode, + parentThreadId, + willConfirm, + label, + autoAcceptCredential, + credentialType, + isShortenUrl, + reuseConnection, + isValidateSchema + }; } - getIssueCredentials(issuedCredentialsSearchCriteria: IIssuedCredentialSearchParams, user: IUserRequest, orgId: string): Promise { - const payload = { issuedCredentialsSearchCriteria, user, orgId }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'get-all-issued-credentials', payload); - } + return this.natsClient.sendNats(this.issuanceProxy, 'send-credential-create-offer-oob', payload); + } + getIssueCredentials( + issuedCredentialsSearchCriteria: IIssuedCredentialSearchParams, + user: IUserRequest, + orgId: string + ): Promise { + const payload = { issuedCredentialsSearchCriteria, user, orgId }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'get-all-issued-credentials', payload); + } - getIssueCredentialsbyCredentialRecordId(user: IUserRequest, credentialRecordId: string, orgId: string): Promise<{ - response: object; - }> { - const payload = { user, credentialRecordId, orgId }; - return this.natsClient.sendNats(this.issuanceProxy, 'get-issued-credentials-by-credentialDefinitionId', payload); - } + getIssueCredentialsbyCredentialRecordId( + user: IUserRequest, + credentialRecordId: string, + orgId: string + ): Promise<{ + response: object; + }> { + const payload = { user, credentialRecordId, orgId }; + return this.natsClient.sendNats(this.issuanceProxy, 'get-issued-credentials-by-credentialDefinitionId', payload); + } - getIssueCredentialWebhook(issueCredentialDto: IssuanceDto, id: string): Promise<{ - response: object; - }> { - const payload = { issueCredentialDto, id }; - return this.natsClient.sendNats(this.issuanceProxy, 'webhook-get-issue-credential', payload); - } + getIssueCredentialWebhook( + issueCredentialDto: IssuanceDto, + id: string + ): Promise<{ + response: object; + }> { + const payload = { issueCredentialDto, id }; + return this.natsClient.sendNats(this.issuanceProxy, 'webhook-get-issue-credential', payload); + } - outOfBandCredentialOffer(user: IUserRequest, outOfBandCredentialDto: OOBCredentialDtoWithEmail): Promise<{ - response: object; - }> { - const payload = { user, outOfBandCredentialDto }; - return this.natsClient.sendNats(this.issuanceProxy, 'out-of-band-credential-offer', payload); - } + outOfBandCredentialOffer( + user: IUserRequest, + outOfBandCredentialDto: OOBCredentialDtoWithEmail + ): Promise<{ + response: object; + }> { + const payload = { user, outOfBandCredentialDto }; + return this.natsClient.sendNats(this.issuanceProxy, 'out-of-band-credential-offer', payload); + } - getAllCredentialTemplates(orgId:string, schemaType:string): Promise { - const payload = { orgId, schemaType}; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'get-all-credential-template-for-bulk-operation', payload); - } + getAllCredentialTemplates(orgId: string, schemaType: string): Promise { + const payload = { orgId, schemaType }; + return this.natsClient.sendNatsMessage( + this.issuanceProxy, + 'get-all-credential-template-for-bulk-operation', + payload + ); + } - async downloadBulkIssuanceCSVTemplate(orgId: string, templateDetails: TemplateDetails - ): Promise { - const payload = { orgId, templateDetails }; - return (await this.natsClient.sendNats(this.issuanceProxy, 'download-csv-template-for-bulk-operation', payload)).response; - } + async downloadBulkIssuanceCSVTemplate(orgId: string, templateDetails: TemplateDetails): Promise { + const payload = { orgId, templateDetails }; + return (await this.natsClient.sendNats(this.issuanceProxy, 'download-csv-template-for-bulk-operation', payload)) + .response; + } - async uploadCSVTemplate(importFileDetails: UploadedFileDetails - ): Promise<{ response: object }> { - const payload = { importFileDetails }; - return this.natsClient.sendNats(this.issuanceProxy, 'upload-csv-template', payload); - } + async uploadCSVTemplate(importFileDetails: UploadedFileDetails): Promise<{ response: object }> { + const payload = { importFileDetails }; + return this.natsClient.sendNats(this.issuanceProxy, 'upload-csv-template', payload); + } - async previewCSVDetails(requestId: string, - orgId: string, - previewFileDetails: PreviewFileDetails - ): Promise { - const payload = { - requestId, - orgId, - previewFileDetails - }; - return this.natsClient.sendNats(this.issuanceProxy, 'preview-csv-details', payload); - } + async previewCSVDetails(requestId: string, orgId: string, previewFileDetails: PreviewFileDetails): Promise { + const payload = { + requestId, + orgId, + previewFileDetails + }; + return this.natsClient.sendNats(this.issuanceProxy, 'preview-csv-details', payload); + } - async issuedFileDetails( - orgId: string, - fileParameter: FileParameter - ): Promise<{ response: object }> { - const payload = { - orgId, - fileParameter - }; - return this.natsClient.sendNats(this.issuanceProxy, 'issued-file-details', payload); - } + async issuedFileDetails(orgId: string, fileParameter: FileParameter): Promise<{ response: object }> { + const payload = { + orgId, + fileParameter + }; + return this.natsClient.sendNats(this.issuanceProxy, 'issued-file-details', payload); + } - async getFileDetailsByFileId( - orgId: string, - fileId: string, - fileParameter: FileParameter - ): Promise<{ response: object }> { - const payload = { - orgId, - fileId, - fileParameter - }; - return this.natsClient.sendNats(this.issuanceProxy, 'issued-file-data', payload); - } + async getFileDetailsByFileId( + orgId: string, + fileId: string, + fileParameter: FileParameter + ): Promise<{ response: object }> { + const payload = { + orgId, + fileId, + fileParameter + }; + return this.natsClient.sendNats(this.issuanceProxy, 'issued-file-data', payload); + } - async issueBulkCredential(requestId: string, orgId: string, clientDetails: ClientDetails, reqPayload: IReqPayload, isValidateSchema: boolean): Promise { - const payload = { requestId, orgId, clientDetails, reqPayload, isValidateSchema }; + async issueBulkCredential( + requestId: string, + orgId: string, + clientDetails: ClientDetails, + reqPayload: IReqPayload, + isValidateSchema: boolean + ): Promise { + const payload = { requestId, orgId, clientDetails, reqPayload, isValidateSchema }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'issue-bulk-credentials', payload); - } + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'issue-bulk-credentials', payload); + } - async retryBulkCredential(fileId: string, orgId: string, clientDetails: ClientDetails, isValidateSchema?: boolean): Promise { - const payload = { fileId, orgId, clientDetails, isValidateSchema }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'retry-bulk-credentials', payload); - } + async retryBulkCredential( + fileId: string, + orgId: string, + clientDetails: ClientDetails, + isValidateSchema?: boolean + ): Promise { + const payload = { fileId, orgId, clientDetails, isValidateSchema }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'retry-bulk-credentials', payload); + } - async _getWebhookUrl(tenantId?: string, orgId?: string): Promise { - const pattern = { cmd: 'get-webhookurl' }; - const payload = { tenantId, orgId }; - - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const message = await this.issuanceProxy.send(pattern, payload).toPromise(); - return message; - } catch (error) { - this.logger.error(`catch: ${JSON.stringify(error)}`); - throw error; - } + async _getWebhookUrl(tenantId?: string, orgId?: string): Promise { + const pattern = { cmd: 'get-webhookurl' }; + const payload = { tenantId, orgId }; + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.issuanceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw error; } + } - async _postWebhookResponse(webhookUrl: string, data: object): Promise { - const pattern = { cmd: 'post-webhook-response-to-webhook-url' }; - const payload = { webhookUrl, data }; + async _postWebhookResponse(webhookUrl: string, data: object): Promise { + const pattern = { cmd: 'post-webhook-response-to-webhook-url' }; + const payload = { webhookUrl, data }; - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const message = await this.issuanceProxy.send(pattern, payload).toPromise(); - return message; - } catch (error) { - this.logger.error(`catch: ${JSON.stringify(error)}`); + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const message = await this.issuanceProxy.send(pattern, payload).toPromise(); + return message; + } catch (error) { + this.logger.error(`catch: ${JSON.stringify(error)}`); - throw error; - } + throw error; } + } - async deleteIssuanceRecords(orgId: string, userDetails: user): Promise { - const payload = { orgId, userDetails }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'delete-issuance-records', payload); - } - async getFileDetailsAndFileDataByFileId(orgId: string, fileId: string): Promise { - const payload = { - orgId, - fileId - }; - return this.natsClient.sendNatsMessage(this.issuanceProxy, 'issued-file-data-and-file-details', payload); - } - -} \ No newline at end of file + async deleteIssuanceRecords(orgId: string, userDetails: user): Promise { + const payload = { orgId, userDetails }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'delete-issuance-records', payload); + } + async getFileDetailsAndFileDataByFileId(orgId: string, fileId: string): Promise { + const payload = { + orgId, + fileId + }; + return this.natsClient.sendNatsMessage(this.issuanceProxy, 'issued-file-data-and-file-details', payload); + } +} From 64597a750eda61182857fce369ae77be47fbc311 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Wed, 9 Apr 2025 19:19:38 +0530 Subject: [PATCH 11/14] feat: added description property Signed-off-by: bhavanakarwade --- .../api-gateway/src/dtos/create-schema.dto.ts | 629 +++++++++--------- .../ledger/libs/helpers/w3c.schema.builder.ts | 4 +- libs/common/src/interfaces/interface.ts | 1 + 3 files changed, 324 insertions(+), 310 deletions(-) diff --git a/apps/api-gateway/src/dtos/create-schema.dto.ts b/apps/api-gateway/src/dtos/create-schema.dto.ts index 7d3e58b47..8721a5714 100644 --- a/apps/api-gateway/src/dtos/create-schema.dto.ts +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -1,326 +1,339 @@ -import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsInt, IsNotEmpty, IsNumber, IsOptional, IsPositive, IsString, Min, ValidateIf, ValidateNested } from 'class-validator'; +import { + ArrayMinSize, + IsArray, + IsBoolean, + IsEnum, + IsInt, + IsNotEmpty, + IsNumber, + IsOptional, + IsPositive, + IsString, + Min, + ValidateIf, + ValidateNested +} from 'class-validator'; import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { plainToClass, Transform, Type } from 'class-transformer'; import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; import { JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum/enum'; - export class W3CAttributeValue { - - @ApiProperty() - @IsString() - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'attributeName is required' }) - attributeName: string; - - @ApiProperty() - @IsString() - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'displayName is required' }) - displayName: string; - - @ApiProperty({ - description: 'The type of the schema', - enum: W3CSchemaDataType, - example: W3CSchemaDataType.STRING - }) - @IsEnum(W3CSchemaDataType, { message: 'Schema data type must be a valid type' }) - schemaDataType: W3CSchemaDataType; - - @ApiProperty() - @IsBoolean() - @IsNotEmpty({ message: 'isRequired property is required' }) - isRequired: boolean; - - @ApiPropertyOptional({ description: 'Minimum length for string values' }) - @IsOptional() - @IsInt() - @Min(0) - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) - minLength?: number; - - @ApiPropertyOptional({ description: 'Maximum length for string values' }) - @IsOptional() - @IsInt() - @Min(1) - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) - maxLength?: number; - - @ApiPropertyOptional({ description: 'Regular expression pattern for string values' }) - @IsOptional() - @IsString() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) - pattern?: string; - - @ApiPropertyOptional({ description: 'Enumerated values for string type' }) - @IsOptional() - @IsArray() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) - enum?: string[]; - - @ApiPropertyOptional({ description: 'Content encoding (e.g., base64)' }) - @IsOptional() - @IsString() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) - contentEncoding?: string; - - @ApiPropertyOptional({ description: 'Content media type (e.g., image/png)' }) - @IsOptional() - @IsString() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.STRING) - contentMediaType?: string; - - // Number type specific validations - @ApiPropertyOptional({ description: 'Minimum value (inclusive) for number values' }) - @IsOptional() - @IsNumber() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) - minimum?: number; - - @ApiPropertyOptional({ description: 'Maximum value (inclusive) for number values' }) - @IsOptional() - @IsNumber() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) - maximum?: number; - - @ApiPropertyOptional({ description: 'Minimum value (exclusive) for number values' }) - @IsOptional() - @IsNumber() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) - exclusiveMinimum?: number; - - @ApiPropertyOptional({ description: 'Maximum value (exclusive) for number values' }) - @IsOptional() - @IsNumber() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) - exclusiveMaximum?: number; - - @ApiPropertyOptional({ description: 'Number must be a multiple of this value' }) - @IsOptional() - @IsNumber() - @IsPositive() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.NUMBER) - multipleOf?: number; - - // Array type specific validations - @ApiPropertyOptional({ description: 'Minimum number of items in array' }) - @IsOptional() - @IsInt() - @Min(0) - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) - minItems?: number; - - @ApiPropertyOptional({ description: 'Maximum number of items in array' }) - @IsOptional() - @IsInt() - @Min(1) - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) - maxItems?: number; - - @ApiPropertyOptional({ description: 'Whether array items must be unique' }) - @IsOptional() - @IsBoolean() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) - uniqueItems?: boolean; - - @ApiPropertyOptional({ description: 'Array of items', type: [W3CAttributeValue] }) - @IsArray() - @IsOptional() - @ValidateNested({ each: true }) - @Type(() => W3CAttributeValue) - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.ARRAY) - items?: W3CAttributeValue[]; - - // Object type specific validations - @ApiPropertyOptional({ description: 'Minimum number of properties in object' }) - @IsOptional() - @IsInt() - @Min(0) - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) - minProperties?: number; - - @ApiPropertyOptional({ description: 'Maximum number of properties in object' }) - @IsOptional() - @IsInt() - @Min(1) - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) - maxProperties?: number; - - @ApiPropertyOptional({ description: 'additional properties must be boolean' }) - @IsOptional() - @IsBoolean() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) - additionalProperties?: boolean; - - @ApiPropertyOptional({ description: 'Required properties for object type' }) - @IsOptional() - @IsArray() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) - required?: string[]; - - @ApiPropertyOptional({ description: 'Dependent required properties' }) - @IsOptional() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) - dependentRequired?: Record; - - @ApiPropertyOptional({ description: 'Object with dynamic properties' }) - @IsOptional() - @ValidateIf(o => o.schemaDataType === W3CSchemaDataType.OBJECT) - @Transform(({ value }) => { - if (value && 'object' === typeof value) { - const result = {}; - Object.entries(value).forEach(([key, propValue]) => { - result[key] = plainToClass(W3CAttributeValue, propValue, { - enableImplicitConversion: false - }); +export class W3CAttributeValue { + @ApiProperty() + @IsString() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'attributeName is required' }) + attributeName: string; + + @ApiProperty() + @IsString() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'displayName is required' }) + displayName: string; + + @ApiProperty({ + description: 'The type of the schema', + enum: W3CSchemaDataType, + example: W3CSchemaDataType.STRING + }) + @IsEnum(W3CSchemaDataType, { message: 'Schema data type must be a valid type' }) + schemaDataType: W3CSchemaDataType; + + @ApiProperty() + @IsBoolean() + @IsNotEmpty({ message: 'isRequired property is required' }) + isRequired: boolean; + + @ApiPropertyOptional() + @IsOptional() + description?: string; + + @ApiPropertyOptional({ description: 'Minimum length for string values' }) + @IsOptional() + @IsInt() + @Min(0) + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.STRING) + minLength?: number; + + @ApiPropertyOptional({ description: 'Maximum length for string values' }) + @IsOptional() + @IsInt() + @Min(1) + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.STRING) + maxLength?: number; + + @ApiPropertyOptional({ description: 'Regular expression pattern for string values' }) + @IsOptional() + @IsString() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.STRING) + pattern?: string; + + @ApiPropertyOptional({ description: 'Enumerated values for string type' }) + @IsOptional() + @IsArray() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.STRING) + enum?: string[]; + + @ApiPropertyOptional({ description: 'Content encoding (e.g., base64)' }) + @IsOptional() + @IsString() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.STRING) + contentEncoding?: string; + + @ApiPropertyOptional({ description: 'Content media type (e.g., image/png)' }) + @IsOptional() + @IsString() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.STRING) + contentMediaType?: string; + + // Number type specific validations + @ApiPropertyOptional({ description: 'Minimum value (inclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.NUMBER) + minimum?: number; + + @ApiPropertyOptional({ description: 'Maximum value (inclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.NUMBER) + maximum?: number; + + @ApiPropertyOptional({ description: 'Minimum value (exclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.NUMBER) + exclusiveMinimum?: number; + + @ApiPropertyOptional({ description: 'Maximum value (exclusive) for number values' }) + @IsOptional() + @IsNumber() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.NUMBER) + exclusiveMaximum?: number; + + @ApiPropertyOptional({ description: 'Number must be a multiple of this value' }) + @IsOptional() + @IsNumber() + @IsPositive() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.NUMBER) + multipleOf?: number; + + // Array type specific validations + @ApiPropertyOptional({ description: 'Minimum number of items in array' }) + @IsOptional() + @IsInt() + @Min(0) + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.ARRAY) + minItems?: number; + + @ApiPropertyOptional({ description: 'Maximum number of items in array' }) + @IsOptional() + @IsInt() + @Min(1) + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.ARRAY) + maxItems?: number; + + @ApiPropertyOptional({ description: 'Whether array items must be unique' }) + @IsOptional() + @IsBoolean() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.ARRAY) + uniqueItems?: boolean; + + @ApiPropertyOptional({ description: 'Array of items', type: [W3CAttributeValue] }) + @IsArray() + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => W3CAttributeValue) + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.ARRAY) + items?: W3CAttributeValue[]; + + // Object type specific validations + @ApiPropertyOptional({ description: 'Minimum number of properties in object' }) + @IsOptional() + @IsInt() + @Min(0) + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.OBJECT) + minProperties?: number; + + @ApiPropertyOptional({ description: 'Maximum number of properties in object' }) + @IsOptional() + @IsInt() + @Min(1) + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.OBJECT) + maxProperties?: number; + + @ApiPropertyOptional({ description: 'additional properties must be boolean' }) + @IsOptional() + @IsBoolean() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.OBJECT) + additionalProperties?: boolean; + + @ApiPropertyOptional({ description: 'Required properties for object type' }) + @IsOptional() + @IsArray() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.OBJECT) + required?: string[]; + + @ApiPropertyOptional({ description: 'Dependent required properties' }) + @IsOptional() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.OBJECT) + dependentRequired?: Record; + + @ApiPropertyOptional({ description: 'Object with dynamic properties' }) + @IsOptional() + @ValidateIf((o) => o.schemaDataType === W3CSchemaDataType.OBJECT) + @Transform(({ value }) => { + if (value && 'object' === typeof value) { + const result = {}; + Object.entries(value).forEach(([key, propValue]) => { + result[key] = plainToClass(W3CAttributeValue, propValue, { + enableImplicitConversion: false }); - return result; - } - return value; - }) - properties?: Record; - } - -class AttributeValue { + }); + return result; + } + return value; + }) + properties?: Record; +} - @ApiProperty() - @IsString() - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'attributeName is required' }) - attributeName: string; - - @ApiProperty() - @IsString() - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'schemaDataType is required' }) - schemaDataType: string; - - @ApiProperty() - @IsString() - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'displayName is required' }) - displayName: string; - - @ApiProperty() - @IsBoolean() - @IsNotEmpty({ message: 'isRequired property is required' }) - isRequired: boolean; +class AttributeValue { + @ApiProperty() + @IsString() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'attributeName is required' }) + attributeName: string; + + @ApiProperty() + @IsString() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'schemaDataType is required' }) + schemaDataType: string; + + @ApiProperty() + @IsString() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'displayName is required' }) + displayName: string; + + @ApiProperty() + @IsBoolean() + @IsNotEmpty({ message: 'isRequired property is required' }) + isRequired: boolean; } export class CreateSchemaDto { - @ApiProperty() - @IsString({ message: 'schemaVersion must be a string' }) - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'schemaVersion is required' }) - schemaVersion: string; - - @ApiProperty() - @IsString({ message: 'schemaName must be a string' }) - @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'schemaName is required' }) - @IsNotSQLInjection({ message: 'SchemaName is required.' }) - schemaName: string; - - @ApiProperty({ - type: [AttributeValue], - 'example': [ - { - attributeName: 'name', - schemaDataType: 'string', - displayName: 'Name', - isRequired: true - } - ] - }) - @IsArray({ message: 'attributes must be an array' }) - @IsNotEmpty({ message: 'attributes are required' }) - @ArrayMinSize(1) - @ValidateNested({ each: true }) - @Type(() => AttributeValue) - attributes: AttributeValue[]; - - orgId: string; - - @ApiPropertyOptional() - @Transform(({ value }) => trim(value)) - @IsOptional() - @IsNotEmpty({ message: 'orgDid should not be empty' }) - @IsString({ message: 'orgDid must be a string' }) - orgDid: string; + @ApiProperty() + @IsString({ message: 'schemaVersion must be a string' }) + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'schemaVersion is required' }) + schemaVersion: string; + + @ApiProperty() + @IsString({ message: 'schemaName must be a string' }) + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'schemaName is required' }) + @IsNotSQLInjection({ message: 'SchemaName is required.' }) + schemaName: string; + + @ApiProperty({ + type: [AttributeValue], + example: [ + { + attributeName: 'name', + schemaDataType: 'string', + displayName: 'Name', + isRequired: true + } + ] + }) + @IsArray({ message: 'attributes must be an array' }) + @IsNotEmpty({ message: 'attributes are required' }) + @ArrayMinSize(1) + @ValidateNested({ each: true }) + @Type(() => AttributeValue) + attributes: AttributeValue[]; + + orgId: string; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsNotEmpty({ message: 'orgDid should not be empty' }) + @IsString({ message: 'orgDid must be a string' }) + orgDid: string; } export class CreateW3CSchemaDto { - @ApiProperty({ - type: [W3CAttributeValue], - 'example': [ - { - attributeName: 'name', - schemaDataType: 'string', - displayName: 'Name', - isRequired: true - } - ] - }) - @ValidateNested({each: true}) - @Type(() => W3CAttributeValue) - @IsArray({ message: 'attributes must be an array' }) - @ArrayMinSize(1) - @IsNotEmpty() - attributes: W3CAttributeValue []; - - @ApiProperty() - @IsString({ message: 'schemaName must be a string' }) - @Transform(({ value }) => value.trim()) - @IsNotEmpty({ message: 'schemaName is required' }) - schemaName: string; - - @ApiProperty() - @IsString({ message: 'description must be a string' }) - @IsNotEmpty({ message: 'description is required' }) - description: string; - - @ApiProperty({ - description: 'The type of the schema', - enum: JSONSchemaType, - example: JSONSchemaType.POLYGON_W3C - }) - @IsEnum(JSONSchemaType, { message: 'Schema type must be a valid schema type' }) - @IsNotEmpty({ message: 'Type is required' }) - schemaType: JSONSchemaType; + @ApiProperty({ + type: [W3CAttributeValue], + example: [ + { + attributeName: 'name', + schemaDataType: 'string', + displayName: 'Name', + isRequired: true + } + ] + }) + @ValidateNested({ each: true }) + @Type(() => W3CAttributeValue) + @IsArray({ message: 'attributes must be an array' }) + @ArrayMinSize(1) + @IsNotEmpty() + attributes: W3CAttributeValue[]; + + @ApiProperty() + @IsString({ message: 'schemaName must be a string' }) + @Transform(({ value }) => value.trim()) + @IsNotEmpty({ message: 'schemaName is required' }) + schemaName: string; + + @ApiProperty() + @IsString({ message: 'description must be a string' }) + @IsNotEmpty({ message: 'description is required' }) + description: string; + + @ApiProperty({ + description: 'The type of the schema', + enum: JSONSchemaType, + example: JSONSchemaType.POLYGON_W3C + }) + @IsEnum(JSONSchemaType, { message: 'Schema type must be a valid schema type' }) + @IsNotEmpty({ message: 'Type is required' }) + schemaType: JSONSchemaType; } @ApiExtraModels(CreateSchemaDto, CreateW3CSchemaDto) export class GenericSchemaDTO { - @ApiProperty({ - description: 'The type of the schema', - enum: SchemaTypeEnum, - example: SchemaTypeEnum.INDY - }) - @IsEnum(SchemaTypeEnum, { message: 'Type must be a valid schema type' }) - @IsNotEmpty({ message: 'Type is required' }) - type: SchemaTypeEnum; - - @ApiPropertyOptional() - @Transform(({ value }) => trim(value)) - @IsOptional() - @IsString({ message: 'alias must be a string' }) - @IsNotEmpty({ message: 'alias is required' }) - alias: string; - - @ApiProperty({ - type: Object, - oneOf: [ - { $ref: getSchemaPath(CreateSchemaDto) }, - { $ref: getSchemaPath(CreateW3CSchemaDto) } - ] - }) - @ValidateNested() - @Type(({ object }) => { - if (object.type === SchemaTypeEnum.INDY) { - return CreateSchemaDto; - } else if (object.type === SchemaTypeEnum.JSON) { - return CreateW3CSchemaDto; - } - }) - schemaPayload:CreateSchemaDto | CreateW3CSchemaDto; -} \ No newline at end of file + @ApiProperty({ + description: 'The type of the schema', + enum: SchemaTypeEnum, + example: SchemaTypeEnum.INDY + }) + @IsEnum(SchemaTypeEnum, { message: 'Type must be a valid schema type' }) + @IsNotEmpty({ message: 'Type is required' }) + type: SchemaTypeEnum; + + @ApiPropertyOptional() + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsString({ message: 'alias must be a string' }) + @IsNotEmpty({ message: 'alias is required' }) + alias: string; + + @ApiProperty({ + type: Object, + oneOf: [{ $ref: getSchemaPath(CreateSchemaDto) }, { $ref: getSchemaPath(CreateW3CSchemaDto) }] + }) + @ValidateNested() + @Type(({ object }) => { + if (object.type === SchemaTypeEnum.INDY) { + return CreateSchemaDto; + } else if (object.type === SchemaTypeEnum.JSON) { + return CreateW3CSchemaDto; + } + }) + schemaPayload: CreateSchemaDto | CreateW3CSchemaDto; +} diff --git a/apps/ledger/libs/helpers/w3c.schema.builder.ts b/apps/ledger/libs/helpers/w3c.schema.builder.ts index 7a5f7f737..da967ff39 100644 --- a/apps/ledger/libs/helpers/w3c.schema.builder.ts +++ b/apps/ledger/libs/helpers/w3c.schema.builder.ts @@ -83,7 +83,7 @@ export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: s const required = []; attrs.forEach((attribute) => { - const { attributeName, schemaDataType, isRequired, displayName } = attribute; + const { attributeName, schemaDataType, isRequired, displayName, description } = attribute; // Add to required array if isRequired is true if (isRequired) { @@ -94,7 +94,7 @@ export function w3cSchemaBuilder(attributes: IW3CAttributeValue[], schemaName: s const baseProperty = { type: schemaDataType.toLowerCase(), title: displayName || attributeName, - description: `${attributeName} field` + description: description ? description : `${attributeName} field` }; // Handle different attribute types diff --git a/libs/common/src/interfaces/interface.ts b/libs/common/src/interfaces/interface.ts index 350a2f7b7..4116d21f8 100644 --- a/libs/common/src/interfaces/interface.ts +++ b/libs/common/src/interfaces/interface.ts @@ -38,6 +38,7 @@ export interface IW3CAttributeValue { schemaDataType: W3CSchemaDataType; displayName: string; isRequired: boolean; + description?: string; minLength?: number; maxLength?: number; pattern?: string; From 802bb8f00dc1d6d5960f688522f2e684428e5ab5 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Thu, 10 Apr 2025 13:22:27 +0530 Subject: [PATCH 12/14] fix: added validations for schema type Signed-off-by: bhavanakarwade --- .../api-gateway/src/dtos/create-schema.dto.ts | 18 +- libs/enum/src/enum.ts | 294 +++++++++--------- 2 files changed, 164 insertions(+), 148 deletions(-) diff --git a/apps/api-gateway/src/dtos/create-schema.dto.ts b/apps/api-gateway/src/dtos/create-schema.dto.ts index 8721a5714..9f9b978f9 100644 --- a/apps/api-gateway/src/dtos/create-schema.dto.ts +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -17,7 +17,7 @@ import { import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { plainToClass, Transform, Type } from 'class-transformer'; import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; -import { JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum/enum'; +import { IndySchemaDataType, JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum/enum'; export class W3CAttributeValue { @ApiProperty() @@ -37,7 +37,9 @@ export class W3CAttributeValue { enum: W3CSchemaDataType, example: W3CSchemaDataType.STRING }) - @IsEnum(W3CSchemaDataType, { message: 'Schema data type must be a valid type' }) + @IsEnum(W3CSchemaDataType, { + message: `Schema data type must be one of [${Object.values(W3CSchemaDataType).join(', ')}]` + }) schemaDataType: W3CSchemaDataType; @ApiProperty() @@ -205,11 +207,17 @@ class AttributeValue { @IsNotEmpty({ message: 'attributeName is required' }) attributeName: string; - @ApiProperty() + @ApiProperty({ + description: 'The type of the schema', + enum: IndySchemaDataType, + example: IndySchemaDataType.STRING + }) @IsString() @Transform(({ value }) => trim(value)) - @IsNotEmpty({ message: 'schemaDataType is required' }) - schemaDataType: string; + @IsEnum(IndySchemaDataType, { + message: `Schema data type must be one of [${Object.values(IndySchemaDataType).join(', ')}]` + }) + schemaDataType: IndySchemaDataType; @ApiProperty() @IsString() diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 29dabc308..e0811acbb 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -1,251 +1,259 @@ export enum NATSReconnects { - maxReconnectAttempts = (10 * 60) / 5, // 10 minutes with a reconnection attempt every 5 seconds - reconnectTimeWait = 5000 // 5 second delay between reconnection attempts + maxReconnectAttempts = (10 * 60) / 5, // 10 minutes with a reconnection attempt every 5 seconds + reconnectTimeWait = 5000 // 5 second delay between reconnection attempts } export enum SortValue { - ASC = 'asc', - DESC = 'desc' + ASC = 'asc', + DESC = 'desc' } export enum SortFields { - ID = 'id', - CREATED_DATE_TIME = 'createDateTime', - NAME = 'name', - VERSION = 'version', - LEDGER_ID = 'schemaLedgerId', - PUBLISHER_DID = 'publisherDid', - ISSUER_ID = 'issuerId' + ID = 'id', + CREATED_DATE_TIME = 'createDateTime', + NAME = 'name', + VERSION = 'version', + LEDGER_ID = 'schemaLedgerId', + PUBLISHER_DID = 'publisherDid', + ISSUER_ID = 'issuerId' } export enum CredDefSortFields { - CREATED_DATE_TIME = 'createDateTime', - TAG = 'tag', - LEDGER_ID = 'schemaLedgerId', - CRED_DEF_ID= 'credentialDefinitionId' + CREATED_DATE_TIME = 'createDateTime', + TAG = 'tag', + LEDGER_ID = 'schemaLedgerId', + CRED_DEF_ID = 'credentialDefinitionId' } export enum AgentType { - // TODO: Change to Credo - AFJ = 'AFJ', - ACAPY = 'ACAPY' + // TODO: Change to Credo + AFJ = 'AFJ', + ACAPY = 'ACAPY' } export enum DevelopmentEnvironment { - PRODUCTION = 'production', - DEVELOPMENT = 'development', - TEST = 'test' + PRODUCTION = 'production', + DEVELOPMENT = 'development', + TEST = 'test' } export declare enum KeyType { - Ed25519 = 'ed25519', - Bls12381g1g2 = 'bls12381g1g2', - Bls12381g1 = 'bls12381g1', - Bls12381g2 = 'bls12381g2', - X25519 = 'x25519', - P256 = 'p256', - P384 = 'p384', - P521 = 'p521', - K256 = 'k256' + Ed25519 = 'ed25519', + Bls12381g1g2 = 'bls12381g1g2', + Bls12381g1 = 'bls12381g1', + Bls12381g2 = 'bls12381g2', + X25519 = 'x25519', + P256 = 'p256', + P384 = 'p384', + P521 = 'p521', + K256 = 'k256' } export enum DidMethod { - INDY = 'indy', - KEY = 'key', - WEB = 'web', - POLYGON = 'polygon' + INDY = 'indy', + KEY = 'key', + WEB = 'web', + POLYGON = 'polygon' } export enum Ledgers { - Bcovrin_Testnet = 'Bcovrin Testnet', - Indicio_Testnet = 'Indicio Testnet', - Indicio_Demonet = 'Indicio Demonet', - Indicio_Mainnet = 'Indicio Mainnet', - Not_Applicable = 'NA' + Bcovrin_Testnet = 'Bcovrin Testnet', + Indicio_Testnet = 'Indicio Testnet', + Indicio_Demonet = 'Indicio Demonet', + Indicio_Mainnet = 'Indicio Mainnet', + Not_Applicable = 'NA' } export enum Invitation { - ACCEPTED = 'accepted', - REJECTED = 'rejected', - PENDING = 'pending' + ACCEPTED = 'accepted', + REJECTED = 'rejected', + PENDING = 'pending' } export enum EndorserTransactionType { - SCHEMA = 'schema', - CREDENTIAL_DEFINITION = 'credential-definition', + SCHEMA = 'schema', + CREDENTIAL_DEFINITION = 'credential-definition' } export enum schemaRequestType { - W3C = 'w3c', - INDY = 'indy' + W3C = 'w3c', + INDY = 'indy' } export enum OrgAgentType { - DEDICATED = 'DEDICATED', - SHARED = 'SHARED' + DEDICATED = 'DEDICATED', + SHARED = 'SHARED' } export enum AgentSpinUpStatus { - PENDING = 0, - PROCESSED = 1, - COMPLETED = 2 + PENDING = 0, + PROCESSED = 1, + COMPLETED = 2 } export enum UserCertificateId { - WINNER = 'Winner', - PARTICIPANT = 'Participant', - ARBITER = 'Arbiter', - WORLD_RECORD = 'WorldRecord' + WINNER = 'Winner', + PARTICIPANT = 'Participant', + ARBITER = 'Arbiter', + WORLD_RECORD = 'WorldRecord' } export enum NodeEnvironment { - DEVELOPMENT='DEV', - PRODUCTION='PROD' + DEVELOPMENT = 'DEV', + PRODUCTION = 'PROD' } export enum AutoAccept { - Always = "always", - ContentApproved = "contentApproved", - Never = "never" + Always = 'always', + ContentApproved = 'contentApproved', + Never = 'never' } export enum SortMembers { - CREATED_DATE_TIME = 'createDateTime', - STATUS = 'status', - ID = 'id', - ORGANIZATION = 'organization' + CREATED_DATE_TIME = 'createDateTime', + STATUS = 'status', + ID = 'id', + ORGANIZATION = 'organization' } const transitionMap: { [key in Invitation]: Invitation[] } = { - [Invitation.PENDING]: [Invitation.ACCEPTED, Invitation.REJECTED], - [Invitation.ACCEPTED]: [], - [Invitation.REJECTED]: [] + [Invitation.PENDING]: [Invitation.ACCEPTED, Invitation.REJECTED], + [Invitation.ACCEPTED]: [], + [Invitation.REJECTED]: [] }; -export const transition = (currentStatus: Invitation, nextStatus: Invitation): boolean => (transitionMap[currentStatus].includes(nextStatus)); +export const transition = (currentStatus: Invitation, nextStatus: Invitation): boolean => + transitionMap[currentStatus].includes(nextStatus); export enum SchemaType { - INDY = 'indy', - W3C_Schema = 'w3c' + INDY = 'indy', + W3C_Schema = 'w3c' } export enum IssueCredentialType { - JSONLD = 'jsonld', - INDY = 'indy' + JSONLD = 'jsonld', + INDY = 'indy' } export enum TemplateIdentifier { - EMAIL_COLUMN = 'email_identifier' + EMAIL_COLUMN = 'email_identifier' } export enum PromiseResult { - REJECTED = 'rejected', - FULFILLED = 'fulfilled' + REJECTED = 'rejected', + FULFILLED = 'fulfilled' } export enum PrismaTables { - PRESENTATIONS = 'presentations', - CREDENTIALS = 'credentials', - ORG_AGENTS = 'org_agents', - ORG_DIDS = 'org_dids', - AGENT_INVITATIONS = 'agent_invitations', - CONNECTIONS = 'connections', - FILE_UPLOAD = 'file_upload', - NOTIFICATION = 'notification', - USER_ACTIVITY = 'user_activity', - USER_ORG_ROLES = 'user_org_roles', - ORG_INVITATIONS = 'org_invitations', - ORGANIZATION = 'organization' + PRESENTATIONS = 'presentations', + CREDENTIALS = 'credentials', + ORG_AGENTS = 'org_agents', + ORG_DIDS = 'org_dids', + AGENT_INVITATIONS = 'agent_invitations', + CONNECTIONS = 'connections', + FILE_UPLOAD = 'file_upload', + NOTIFICATION = 'notification', + USER_ACTIVITY = 'user_activity', + USER_ORG_ROLES = 'user_org_roles', + ORG_INVITATIONS = 'org_invitations', + ORGANIZATION = 'organization' } export enum IssuanceProcessState { - PROPOSAL_SENT = 'proposal-sent', - PROPOSAL_RECEIVED = 'proposal-received', - OFFER_SENT = 'offer-sent', - OFFER_RECEIVED = 'offer-received', - DECLIEND = 'decliend', - REQUEST_SENT = 'request-sent', - REQUEST_RECEIVED = 'request-received', - CREDENTIAL_ISSUED = 'credential-issued', - CREDENTIAL_RECEIVED = 'credential-received', - DONE = 'done', - ABANDONED = 'abandoned' + PROPOSAL_SENT = 'proposal-sent', + PROPOSAL_RECEIVED = 'proposal-received', + OFFER_SENT = 'offer-sent', + OFFER_RECEIVED = 'offer-received', + DECLIEND = 'decliend', + REQUEST_SENT = 'request-sent', + REQUEST_RECEIVED = 'request-received', + CREDENTIAL_ISSUED = 'credential-issued', + CREDENTIAL_RECEIVED = 'credential-received', + DONE = 'done', + ABANDONED = 'abandoned' } export enum VerificationProcessState { - PROPOSAL_SENT = 'proposal-sent', - PROPOSAL_RECEIVED = 'proposal-received', - REQUEST_SENT = 'request-sent', - REQUEST_RECEIVED = 'request-received', - PRESENTATION_SENT = 'presentation-sent', - PRESENTATION_RECEIVED = 'presentation-received', - DECLIEND = 'declined', - ABANDONED = 'abandoned', - DONE = 'done' + PROPOSAL_SENT = 'proposal-sent', + PROPOSAL_RECEIVED = 'proposal-received', + REQUEST_SENT = 'request-sent', + REQUEST_RECEIVED = 'request-received', + PRESENTATION_SENT = 'presentation-sent', + PRESENTATION_RECEIVED = 'presentation-received', + DECLIEND = 'declined', + ABANDONED = 'abandoned', + DONE = 'done' } export enum ConnectionProcessState { - START = 'start', - INVITATION_SENT = 'invitation-sent', - INVITATION_RECEIVED = 'invitation-received', - REQUEST_SENT = 'request-sent', - DECLIEND = 'decliend', - REQUEST_RECEIVED = 'request-received', - RESPONSE_SENT = 'response-sent', - RESPONSE_RECEIVED = 'response-received', - COMPLETE = 'completed', - ABANDONED = 'abandoned' + START = 'start', + INVITATION_SENT = 'invitation-sent', + INVITATION_RECEIVED = 'invitation-received', + REQUEST_SENT = 'request-sent', + DECLIEND = 'decliend', + REQUEST_RECEIVED = 'request-received', + RESPONSE_SENT = 'response-sent', + RESPONSE_RECEIVED = 'response-received', + COMPLETE = 'completed', + ABANDONED = 'abandoned' } export enum SchemaTypeEnum { - JSON = 'json', - INDY = 'indy' - } + JSON = 'json', + INDY = 'indy' +} export enum W3CSchemaDataType { - NUMBER = 'number', - INTEGER = 'integer', - STRING = 'string', - DATE_TIME = 'datetime-local', - ARRAY= 'array', - OBJECT = 'object', - BOOLEAN = 'boolean' - } + NUMBER = 'number', + INTEGER = 'integer', + STRING = 'string', + DATE_TIME = 'datetime-local', + ARRAY = 'array', + OBJECT = 'object', + BOOLEAN = 'boolean' +} + +export enum IndySchemaDataType { + NUMBER = 'number', + STRING = 'string', + DATE_TIME = 'datetime-local', + BOOLEAN = 'boolean' +} export enum JSONSchemaType { - POLYGON_W3C = 'polygon', - LEDGER_LESS = 'no_ledger' + POLYGON_W3C = 'polygon', + LEDGER_LESS = 'no_ledger' } export enum NetworkNamespace { - POLYGON_TESTNET = 'polygon:testnet' + POLYGON_TESTNET = 'polygon:testnet' } export enum LedgerLessMethods { - WEB = 'web', - KEY = 'key' + WEB = 'web', + KEY = 'key' } export enum LedgerLessConstant { - NO_LEDGER = 'no_ledger', + NO_LEDGER = 'no_ledger' } export enum ledgerLessDIDType { - DID_KEY = 'did:key', - DID_WEB = 'did:web' + DID_KEY = 'did:key', + DID_WEB = 'did:web' } export enum CloudWalletType { - BASE_WALLET = 'CLOUD_BASE_WALLET', - SUB_WALLET = 'CLOUD_SUB_WALLET' + BASE_WALLET = 'CLOUD_BASE_WALLET', + SUB_WALLET = 'CLOUD_SUB_WALLET' } export enum UserRole { - DEFAULT_USER = 'DEFAULT_USER', - HOLDER = 'HOLDER' + DEFAULT_USER = 'DEFAULT_USER', + HOLDER = 'HOLDER' } export enum ProofType { - POLYGON_PROOFTYPE = 'EcdsaSecp256k1Signature2019', - NO_LEDGER_PROOFTYPE = 'Ed25519Signature2018' -} \ No newline at end of file + POLYGON_PROOFTYPE = 'EcdsaSecp256k1Signature2019', + NO_LEDGER_PROOFTYPE = 'Ed25519Signature2018' +} From 390e63ba6a14fe0027a6fd098835af19767c7cca Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Thu, 10 Apr 2025 13:31:14 +0530 Subject: [PATCH 13/14] formatted enum file Signed-off-by: bhavanakarwade --- libs/enum/src/enum.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index e0811acbb..f2e9242d4 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -213,13 +213,6 @@ export enum W3CSchemaDataType { BOOLEAN = 'boolean' } -export enum IndySchemaDataType { - NUMBER = 'number', - STRING = 'string', - DATE_TIME = 'datetime-local', - BOOLEAN = 'boolean' -} - export enum JSONSchemaType { POLYGON_W3C = 'polygon', LEDGER_LESS = 'no_ledger' From ae5269eca31d4460ffc455a07234c43130f68687 Mon Sep 17 00:00:00 2001 From: bhavanakarwade Date: Thu, 10 Apr 2025 13:35:04 +0530 Subject: [PATCH 14/14] chore: added enum for indy schema data type Signed-off-by: bhavanakarwade --- libs/enum/src/enum.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index f2e9242d4..e0811acbb 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -213,6 +213,13 @@ export enum W3CSchemaDataType { BOOLEAN = 'boolean' } +export enum IndySchemaDataType { + NUMBER = 'number', + STRING = 'string', + DATE_TIME = 'datetime-local', + BOOLEAN = 'boolean' +} + export enum JSONSchemaType { POLYGON_W3C = 'polygon', LEDGER_LESS = 'no_ledger'