diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 8c9690edd..1d8d9e552 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -1834,6 +1834,7 @@ export class AgentServiceService { return schemaRequest; } catch (error) { this.logger.error(`Error in createW3CSchema request in agent service : ${JSON.stringify(error)}`); + throw error; } } diff --git a/apps/api-gateway/src/dtos/create-schema.dto.ts b/apps/api-gateway/src/dtos/create-schema.dto.ts index cc7aca6ba..aa7ecde58 100644 --- a/apps/api-gateway/src/dtos/create-schema.dto.ts +++ b/apps/api-gateway/src/dtos/create-schema.dto.ts @@ -1,9 +1,37 @@ -import { ArrayMinSize, IsArray, IsBoolean, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; +import { ArrayMinSize, IsArray, IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { Transform, Type } from 'class-transformer'; import { IsNotSQLInjection, trim } from '@credebl/common/cast.helper'; +import { JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum/enum'; + class W3CAttributeValue { + + @ApiProperty() + @IsString() + @Transform(({ value }) => trim(value)) + @IsNotEmpty({ message: 'attributeName is required' }) + attributeName: 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() + @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() @@ -74,43 +102,69 @@ export class CreateSchemaDto { export class CreateW3CSchemaDto { @ApiProperty({ - type: [], + type: [W3CAttributeValue], 'example': [ { - title: 'name', - type: 'string' + attributeName: 'name', + schemaDataType: 'string', + displayName: 'Name', + isRequired: true } ] }) - @IsNotEmpty({ message: 'Schema attributes are required' }) - schemaAttributes: SchemaAttributes []; + @ValidateNested({each: true}) + @Type(() => W3CAttributeValue) + @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: 'did must be a string' }) - @Transform(({ value }) => value.trim()) - @IsNotEmpty({ message: 'did is required' }) - did: 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; } -export class SchemaAttributes { - @ApiProperty() - @IsNotEmpty({ message: 'type is required' }) - @IsString({ message: 'type must be a string' }) - type: string; +@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; - @ApiProperty() - @IsNotEmpty({ message: 'title is required' }) - @IsString({ message: 'title must be a string' }) - title: 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 diff --git a/apps/api-gateway/src/schema/schema.controller.ts b/apps/api-gateway/src/schema/schema.controller.ts index 6ec60f3c8..d3ec7187f 100644 --- a/apps/api-gateway/src/schema/schema.controller.ts +++ b/apps/api-gateway/src/schema/schema.controller.ts @@ -1,7 +1,7 @@ import { Controller, Logger, Post, Body, HttpStatus, UseGuards, Get, Query, BadRequestException, Res, UseFilters, Param, ParseUUIDPipe } from '@nestjs/common'; /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable camelcase */ -import { ApiOperation, ApiResponse, ApiTags, ApiBearerAuth, ApiForbiddenResponse, ApiUnauthorizedResponse, ApiQuery } from '@nestjs/swagger'; +import { ApiOperation, ApiResponse, ApiTags, ApiBearerAuth, ApiForbiddenResponse, ApiUnauthorizedResponse, ApiQuery, ApiExtraModels, ApiBody, getSchemaPath } from '@nestjs/swagger'; import { SchemaService } from './schema.service'; import { AuthGuard } from '@nestjs/passport'; import { ApiResponseDto } from '../dtos/apiResponse.dto'; @@ -17,7 +17,7 @@ import { OrgRoles } from 'libs/org-roles/enums'; import { Roles } from '../authz/decorators/roles.decorator'; import { IUserRequestInterface } from './interfaces'; import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; -import { CreateSchemaDto, CreateW3CSchemaDto } from '../dtos/create-schema.dto'; +import { GenericSchemaDTO } from '../dtos/create-schema.dto'; import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; import { CredDefSortFields, SortFields } from '@credebl/enum/enum'; @@ -133,42 +133,22 @@ export class SchemaController { return res.status(HttpStatus.OK).json(finalResponse); } - @Post('/:orgId/polygon-w3c/schemas') - @ApiOperation({ - summary: 'Create and sends a W3C-schema to the ledger.', - description: 'Create and sends a W3C-schema to the ledger.' - }) - @Roles(OrgRoles.OWNER, OrgRoles.ADMIN, OrgRoles.ISSUER, OrgRoles.VERIFIER, OrgRoles.MEMBER) - @UseGuards(AuthGuard('jwt'), OrgRolesGuard) - @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) - async createW3CSchema(@Res() res: Response, @Body() schemaPayload: CreateW3CSchemaDto, @Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string, @User() user: IUserRequestInterface): Promise { - - const schemaDetails = await this.appService.createW3CSchema(schemaPayload, orgId, user.id); - const finalResponse: IResponse = { - statusCode: HttpStatus.CREATED, - message: ResponseMessages.schema.success.create, - data: schemaDetails - }; - return res.status(HttpStatus.CREATED).json(finalResponse); - } - + @Post('/:orgId/schemas') @ApiOperation({ - summary: 'Create and sends a schema to the ledger.', - description: 'Create and sends a schema to the ledger.' - }) + summary: 'Create and register various types of schemas.', + description: 'Enables the creation and registration of schemas across different systems: the Indy ledger, the Polygon blockchain network, and W3C ledger-less standards.' + } + ) @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) @UseGuards(AuthGuard('jwt'), OrgRolesGuard) @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) - async createSchema(@Res() res: Response, @Body() schema: CreateSchemaDto, @Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string, @User() user: IUserRequestInterface): Promise { - - schema.orgId = orgId; - const schemaDetails = await this.appService.createSchema(schema, user, schema.orgId); - + async createSchema(@Res() res: Response, @Body() schemaDetails: GenericSchemaDTO, @Param('orgId', new ParseUUIDPipe({exceptionFactory: (): Error => { throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); }})) orgId: string, @User() user: IUserRequestInterface): Promise { + const schemaResponse = await this.appService.createSchema(schemaDetails, user, orgId); const finalResponse: IResponse = { statusCode: HttpStatus.CREATED, message: ResponseMessages.schema.success.create, - data: schemaDetails + data: schemaResponse }; return res.status(HttpStatus.CREATED).json(finalResponse); } diff --git a/apps/api-gateway/src/schema/schema.service.ts b/apps/api-gateway/src/schema/schema.service.ts index 63b5800a6..d6476cf29 100644 --- a/apps/api-gateway/src/schema/schema.service.ts +++ b/apps/api-gateway/src/schema/schema.service.ts @@ -1,10 +1,10 @@ import { Injectable, Inject } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { BaseService } from '../../../../libs/service/base.service'; -import { CreateSchemaDto } from '../dtos/create-schema.dto'; -import { ISchemaSearchPayload, W3CSchemaPayload } from '../interfaces/ISchemaSearch.interface'; +import { GenericSchemaDTO } from '../dtos/create-schema.dto'; +import { ISchemaSearchPayload } from '../interfaces/ISchemaSearch.interface'; import { ISchemaInfo, IUserRequestInterface } from './interfaces'; -import { ICredDefWithPagination, ISchemaData, ISchemasWithPagination, IW3CSchema } from '@credebl/common/interfaces/schema.interface'; +import { ICredDefWithPagination, ISchemaData, ISchemasWithPagination } from '@credebl/common/interfaces/schema.interface'; import { GetCredentialDefinitionBySchemaIdDto } from './dtos/get-all-schema.dto'; @Injectable() @@ -14,16 +14,12 @@ export class SchemaService extends BaseService { @Inject('NATS_CLIENT') private readonly schemaServiceProxy: ClientProxy ) { super(`Schema Service`); } - createSchema(schema: CreateSchemaDto, user: IUserRequestInterface, orgId: string): Promise { - const payload = { schema, user, orgId }; + createSchema(schemaDetails: GenericSchemaDTO, user: IUserRequestInterface, orgId: string): Promise { + const payload = { schemaDetails, user, orgId }; return this.sendNatsMessage(this.schemaServiceProxy, 'create-schema', payload); } - createW3CSchema(schemaPayload: W3CSchemaPayload, orgId: string, user: string): Promise { - const payload = { schemaPayload, orgId, user }; - return this.sendNatsMessage(this.schemaServiceProxy, 'create-w3c-schema', payload); - } - + getSchemaById(schemaId: string, orgId: string): Promise { const payload = { schemaId, orgId }; return this.sendNatsMessage(this.schemaServiceProxy, 'get-schema-by-id', payload); diff --git a/apps/ledger/src/schema/enum/schema.enum.ts b/apps/ledger/src/schema/enum/schema.enum.ts index b8021361b..247e7050b 100644 --- a/apps/ledger/src/schema/enum/schema.enum.ts +++ b/apps/ledger/src/schema/enum/schema.enum.ts @@ -12,4 +12,4 @@ export enum SortFields { export enum W3CSchemaVersion { W3C_SCHEMA_VERSION = 'draft-07' -} \ No newline at end of file +} diff --git a/apps/ledger/src/schema/interfaces/schema-payload.interface.ts b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts index 5eb341c99..4bc1866de 100644 --- a/apps/ledger/src/schema/interfaces/schema-payload.interface.ts +++ b/apps/ledger/src/schema/interfaces/schema-payload.interface.ts @@ -78,6 +78,7 @@ export interface SchemaPayload { schemaName: string, did: string, description: string + jsonSchemaType?: string } export interface W3CSchemaAttributes { diff --git a/apps/ledger/src/schema/interfaces/schema.interface.ts b/apps/ledger/src/schema/interfaces/schema.interface.ts index a89044961..a31023ff0 100644 --- a/apps/ledger/src/schema/interfaces/schema.interface.ts +++ b/apps/ledger/src/schema/interfaces/schema.interface.ts @@ -1,3 +1,4 @@ +import { JSONSchemaType, SchemaTypeEnum, W3CSchemaDataType } from '@credebl/enum/enum'; import { UserRoleOrgPermsDto } from '../dtos/user-role-org-perms.dto'; export interface IUserRequestInterface { @@ -65,3 +66,40 @@ export interface ISchemasWithCount { schemasCount: number; schemasResult: ISchemaData[]; } +interface IW3CAttributeValue { + attributeName: string; + schemaDataType: W3CSchemaDataType; + displayName: string; + isRequired: boolean; +} + +interface IAttributeValue { + attributeName: string; + schemaDataType: string; + displayName: string; + isRequired: boolean; +} + +export interface ICreateSchema { + schemaVersion?: string; + schemaName: string; + attributes: IAttributeValue[]; + orgId?: string; + orgDid?: string; +} +export interface ICreateW3CSchema { + attributes: IW3CAttributeValue[]; + schemaName: string; + description: string; + schemaType: JSONSchemaType; +} +export interface IGenericSchema { + type: SchemaTypeEnum; + schemaPayload: ICreateSchema | ICreateW3CSchema; +} + +export interface IschemaPayload { + schemaDetails: IGenericSchema, + user: IUserRequestInterface, + orgId: string +} \ No newline at end of file diff --git a/apps/ledger/src/schema/schema.controller.ts b/apps/ledger/src/schema/schema.controller.ts index a8236d9d7..5b367ae58 100644 --- a/apps/ledger/src/schema/schema.controller.ts +++ b/apps/ledger/src/schema/schema.controller.ts @@ -5,8 +5,7 @@ import { ISchema, ISchemaCredDeffSearchInterface, ISchemaExist, - ISchemaSearchPayload, - W3CSchemaPayload + ISchemaSearchPayload } from './interfaces/schema-payload.interface'; import { schema } from '@prisma/client'; import { @@ -15,22 +14,18 @@ import { ISchemaDetails, ISchemasWithPagination } from '@credebl/common/interfaces/schema.interface'; +import { IschemaPayload } from './interfaces/schema.interface'; @Controller('schema') export class SchemaController { constructor(private readonly schemaService: SchemaService) {} @MessagePattern({ cmd: 'create-schema' }) - async createSchema(payload: ISchema): Promise { - const { schema, user, orgId } = payload; - return this.schemaService.createSchema(schema, user, orgId); + async createSchema(payload: IschemaPayload): Promise { + const { schemaDetails, user, orgId } = payload; + return this.schemaService.createSchema(schemaDetails, user, orgId); } - @MessagePattern({ cmd: 'create-w3c-schema' }) - async createW3CSchema(payload: W3CSchemaPayload): Promise { - const {orgId, schemaPayload, user} = payload; - return this.schemaService.createW3CSchema(orgId, schemaPayload, user); - } @MessagePattern({ cmd: 'get-schema-by-id' }) async getSchemaById(payload: ISchema): Promise { diff --git a/apps/ledger/src/schema/schema.service.ts b/apps/ledger/src/schema/schema.service.ts index 7681e1874..3607d1a85 100644 --- a/apps/ledger/src/schema/schema.service.ts +++ b/apps/ledger/src/schema/schema.service.ts @@ -11,18 +11,21 @@ import { ClientProxy, RpcException } from '@nestjs/microservices'; import { BaseService } from 'libs/service/base.service'; import { SchemaRepository } from './repositories/schema.repository'; import { schema } from '@prisma/client'; -import { ISchema, ISchemaCredDeffSearchInterface, ISchemaExist, ISchemaPayload, ISchemaSearchCriteria, SchemaPayload, W3CCreateSchema } from './interfaces/schema-payload.interface'; +import { ISchema, ISchemaCredDeffSearchInterface, ISchemaExist, ISchemaSearchCriteria, W3CCreateSchema } from './interfaces/schema-payload.interface'; import { ResponseMessages } from '@credebl/common/response-messages'; -import { IUserRequestInterface } from './interfaces/schema.interface'; +import { ICreateSchema, ICreateW3CSchema, IGenericSchema, IUserRequestInterface } from './interfaces/schema.interface'; import { CreateSchemaAgentRedirection, GetSchemaAgentRedirection } from './schema.interface'; import { map } from 'rxjs/operators'; -import { OrgAgentType, SchemaType } from '@credebl/enum/enum'; +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'; import { CommonService } from '@credebl/common'; import { W3CSchemaVersion } from './enum/schema.enum'; +import { v4 as uuidv4 } from 'uuid'; +import { networkNamespace } from '@credebl/common/common.utils'; +import { checkDidLedgerAndNetwork } from '@credebl/common/cast.helper'; @Injectable() export class SchemaService extends BaseService { @@ -36,207 +39,214 @@ export class SchemaService extends BaseService { } async createSchema( - schema: ISchemaPayload, + schemaDetails: IGenericSchema, user: IUserRequestInterface, orgId: string ): Promise { const userId = user.id; try { - - 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 - ) { - throw new NotAcceptableException( - ResponseMessages.schema.error.invalidVersion, - { cause: new Error(), description: ResponseMessages.errorMessages.notAcceptable } - ); - } - - const schemaAttributeLength = 0; - if (schema.attributes.length === schemaAttributeLength) { + const {schemaPayload, type} = 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 + ) { throw new NotAcceptableException( - ResponseMessages.schema.error.insufficientAttributes, + ResponseMessages.schema.error.invalidVersion, { 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 } - ); } - 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 - ); - } - - let schemaResponseFromAgentService; - if (OrgAgentType.DEDICATED === orgAgentType) { - const issuerId = did; - - const schemaPayload = { - attributes: attributeArray, - version: schema.schemaVersion, - name: schema.schemaName, - issuerId, - agentEndPoint, - orgId, - 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', - payload: { + + 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 } + ); + } + + 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 } + ); + } + 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 + ); + } + + let schemaResponseFromAgentService; + if (OrgAgentType.DEDICATED === orgAgentType) { + const issuerId = did; + + const schemaPayload = { attributes: attributeArray, version: schema.schemaVersion, name: schema.schemaName, - issuerId: did - }, - agentEndPoint, + issuerId, + agentEndPoint, + orgId, + 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', + payload: { + attributes: attributeArray, + version: schema.schemaVersion, + name: schema.schemaName, + issuerId: did + }, + agentEndPoint, + orgId, + agentType: OrgAgentType.SHARED + }; + 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 = { + schema: { schemaName: '', attributes: [], schemaVersion: '', id: '' }, + createdBy: `0`, + issuerId: '', + onLedgerStatus: 'Submitted on ledger', orgId, - agentType: OrgAgentType.SHARED + ledgerId: getLedgerId.id, + type: SchemaType.INDY }; - 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 = { - schema: { schemaName: '', attributes: [], schemaVersion: '', id: '' }, - createdBy: `0`, - issuerId: '', - onLedgerStatus: 'Submitted on ledger', - orgId, - ledgerId: getLedgerId.id, - type: SchemaType.INDY - }; - - if ('finished' === responseObj.schema.state) { - schemaDetails.schema.schemaName = responseObj.schema.schema.name; - schemaDetails.schema.attributes = trimmedAttributes; - schemaDetails.schema.schemaVersion = responseObj.schema.schema.version; - schemaDetails.createdBy = userId; - schemaDetails.schema.id = responseObj.schema.schemaId; - schemaDetails.changedBy = userId; - schemaDetails.orgId = orgId; - schemaDetails.issuerId = responseObj.schema.schema.issuerId; - 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; - schemaDetails.schema.schemaVersion = responseObj.schema.version; - schemaDetails.createdBy = userId; - schemaDetails.schema.id = responseObj.schemaId; - schemaDetails.changedBy = userId; - schemaDetails.orgId = orgId; - schemaDetails.issuerId = responseObj.schema.issuerId; - 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; - + + if ('finished' === responseObj.schema.state) { + schemaDetails.schema.schemaName = responseObj.schema.schema.name; + schemaDetails.schema.attributes = trimmedAttributes; + schemaDetails.schema.schemaVersion = responseObj.schema.schema.version; + schemaDetails.createdBy = userId; + schemaDetails.schema.id = responseObj.schema.schemaId; + schemaDetails.changedBy = userId; + schemaDetails.orgId = orgId; + schemaDetails.issuerId = responseObj.schema.schema.issuerId; + 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; + schemaDetails.schema.schemaVersion = responseObj.schema.version; + schemaDetails.createdBy = userId; + schemaDetails.schema.id = responseObj.schemaId; + schemaDetails.changedBy = userId; + schemaDetails.orgId = orgId; + schemaDetails.issuerId = responseObj.schema.issuerId; + 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 } + ); + } } else { - throw new NotFoundException( - ResponseMessages.schema.error.notCreated, - { cause: new Error(), description: ResponseMessages.errorMessages.notFound } + throw new BadRequestException( + ResponseMessages.schema.error.emptyData, + { cause: new Error(), description: ResponseMessages.errorMessages.badRequest } ); } - } else { + } 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; + const createW3CSchema = await this.createW3CSchema(orgId, josnSchemaDetails, user.id); + return createW3CSchema; + } } catch (error) { this.logger.error( `[createSchema] - outer Error: ${JSON.stringify(error)}` @@ -246,14 +256,15 @@ export class SchemaService extends BaseService { } } - async createW3CSchema(orgId:string, schemaPayload: SchemaPayload, user: string): Promise { + async createW3CSchema(orgId:string, schemaPayload: ICreateW3CSchema, user: string): Promise { try { + let createSchema; const isSchemaExist = await this.schemaRepository.schemaExists(schemaPayload.schemaName, W3CSchemaVersion.W3C_SCHEMA_VERSION); if (0 !== isSchemaExist.length) { throw new ConflictException(ResponseMessages.schema.error.exists); } - const { description, did, schemaAttributes, schemaName} = schemaPayload; + const { description, attributes, schemaName} = schemaPayload; const agentDetails = await this.schemaRepository.getAgentDetailsByOrgId(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.schema.error.agentDetailsNotFound, { @@ -262,6 +273,15 @@ export class SchemaService extends BaseService { }); } const { agentEndPoint } = agentDetails; + + const ledgerAndNetworkDetails = await checkDidLedgerAndNetwork(schemaPayload.schemaType, agentDetails.orgDid); + if (!ledgerAndNetworkDetails) { + throw new BadRequestException(ResponseMessages.schema.error.orgDidAndSchemaType, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); + } + const getAgentDetails = await this.schemaRepository.getAgentType(orgId); const orgAgentType = await this.schemaRepository.getOrgAgentType(getAgentDetails.org_agents[0].orgAgentTypeId); let url; @@ -271,9 +291,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(schemaAttributes, schemaName, description); - + const schemaObject = await this.w3cSchemaBuilder(attributes, schemaName, description); if (!schemaObject) { throw new BadRequestException(ResponseMessages.schema.error.schemaBuilder, { cause: new Error(), @@ -282,39 +300,54 @@ export class SchemaService extends BaseService { } const agentSchemaPayload = { schema:schemaObject, - did, + did: agentDetails.orgDid, schemaName }; - const W3cSchemaPayload = { url, orgId, schemaRequestPayload: agentSchemaPayload }; - const createSchema = await this._createW3CSchema(W3cSchemaPayload); - const storeW3CSchema = await this.storeW3CSchemas(createSchema.response, user, orgId); + if (schemaPayload.schemaType === JSONSchemaType.POLYGON_W3C) { + const createSchemaPayload = await this._createW3CSchema(W3cSchemaPayload); + createSchema = createSchemaPayload.response; + createSchema.type = JSONSchemaType.POLYGON_W3C; + } else { + const createSchemaPayload = await this._createW3CledgerAgnostic(schemaObject); + if (!createSchemaPayload) { + throw new BadRequestException(ResponseMessages.schema.error.schemaUploading, { + cause: new Error(), + description: ResponseMessages.errorMessages.badRequest + }); + } + createSchema = createSchemaPayload.data; + createSchema.did = agentDetails.orgDid; + createSchema.type = JSONSchemaType.LEDGER_LESS; + createSchema.schemaUrl = `${process.env.SCHEMA_FILE_SERVER_URL}${createSchemaPayload.data.schemaId}`; + } + + const storeW3CSchema = await this.storeW3CSchemas(createSchema, user, orgId); if (!storeW3CSchema) { - throw new ConflictException(ResponseMessages.schema.error.storeW3CSchema, { + throw new BadRequestException(ResponseMessages.schema.error.storeW3CSchema, { cause: new Error(), description: ResponseMessages.errorMessages.notFound }); } - return createSchema.response; + return storeW3CSchema; } catch (error) { this.logger.error(`[createSchema] - outer Error: ${JSON.stringify(error)}`); - throw new RpcException(error.response ? error.response : error); + throw error; } } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - private async w3cSchemaBuilder(schemaAttributes, schemaName: string, description: string) { - const schemaAttributeJson = schemaAttributes.map((attribute, index) => ({ - [attribute.title]: { - type: attribute.type.toLowerCase(), + 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.title + title: attribute.attributeName } })); @@ -335,7 +368,7 @@ export class SchemaService extends BaseService { const schemaNameObject = {}; schemaNameObject[schemaName] = { - "const": schemaName + 'const': schemaName }; const date = new Date().toISOString(); @@ -492,15 +525,12 @@ export class SchemaService extends BaseService { return W3CSchema; } - // eslint-disable-next-line @typescript-eslint/explicit-function-return-type - private async storeW3CSchemas(schemaDetails, user, orgId) { - + private async storeW3CSchemas(schemaDetails, user, orgId): Promise { + let ledgerDetails; 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(), @@ -509,7 +539,6 @@ export class SchemaService extends BaseService { } const schemaAttributeJson = schemaRequest.definitions.credentialSubject.properties; const extractedData = []; - for (const key in schemaAttributeJson) { if (2 < Object.keys(schemaAttributeJson[key]).length) { const { type, title } = schemaAttributeJson[key]; @@ -519,9 +548,20 @@ export class SchemaService extends BaseService { extractedData.push({ 'attributeName': title, schemaDataType, displayName, isRequired }); } } - const indyNamespace = schemaDetails?.did.includes(':testnet:') ? 'polygon:testnet' : 'polygon'; - const getLedgerId = 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 + }); + } const storeSchemaDetails = { schema: { schemaName: schemaRequest.title, @@ -535,7 +575,7 @@ export class SchemaService extends BaseService { changedBy: user, publisherDid: schemaDetails.did, orgId, - ledgerId: getLedgerId.id, + ledgerId: ledgerDetails.id, type: SchemaType.W3C_Schema }; const saveResponse = await this.schemaRepository.saveSchema( @@ -586,11 +626,43 @@ export class SchemaService extends BaseService { ).toPromise() .catch(error => { this.logger.error(`Error in creating W3C schema : ${JSON.stringify(error)}`); - throw new Error(error.error ? error.error.message : error.message); + 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 + async _createW3CledgerAgnostic(payload) { + const schemaResourceId = uuidv4(); + + const schemaPayload = JSON.stringify({ + schemaId: `${schemaResourceId}`, + schema: payload + }); + + try { + const jsonldSchemaResponse = await this.commonService.httpPost( + `${process.env.SCHEMA_FILE_SERVER_URL}`, + schemaPayload, + { + headers: { + authorization: `Bearer ${process.env.SCHEMA_FILE_SERVER_TOKEN}`, + 'Content-Type': 'application/json' + } + } + ); + + return jsonldSchemaResponse; + } catch (error) { + this.logger.error('Error creating W3C ledger agnostic schema:', error); + throw new Error(`Failed to create W3C ledger agnostic schema: ${error.message}`); + } + } async getSchemaById(schemaId: string, orgId: string): Promise { diff --git a/libs/common/src/cast.helper.ts b/libs/common/src/cast.helper.ts index 4dec6be5c..debf2ac26 100644 --- a/libs/common/src/cast.helper.ts +++ b/libs/common/src/cast.helper.ts @@ -1,4 +1,4 @@ -import { schemaRequestType } from '@credebl/enum/enum'; +import { JSONSchemaType, ledgerLessDIDType, schemaRequestType } from '@credebl/enum/enum'; import { ISchemaFields } from './interfaces/schema.interface'; import { BadRequestException, PipeTransform } from '@nestjs/common'; import { plainToClass } from 'class-transformer'; @@ -366,3 +366,19 @@ export function IsHostPortOrDomain(validationOptions?: ValidationOptions) { }); }; } + +export function checkDidLedgerAndNetwork(schemaType: string, did: string): boolean { + + const cleanSchemaType = schemaType.trim().toLowerCase(); + const cleanDid = did.trim().toLowerCase(); + + if (JSONSchemaType.POLYGON_W3C === cleanSchemaType) { + return cleanDid.includes(JSONSchemaType.POLYGON_W3C); + } + + if (JSONSchemaType.LEDGER_LESS === cleanSchemaType) { + return cleanDid.startsWith(ledgerLessDIDType.DID_KEY) || cleanDid.startsWith(ledgerLessDIDType.DID_WEB); + } + + return false; +} diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 62e399015..0f8a4084b 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -50,3 +50,14 @@ export function convertUrlToDeepLinkUrl(url: string): string { const deepLinkUrl = (process.env.DEEPLINK_DOMAIN as string).concat(url); return deepLinkUrl; } + +export const networkNamespace = (did):string => { + // Split the DID into segments using the colon as a delimiter + const segments = did.split(':'); + const containsTestnet = segments.some(segment => segment.includes('polygon')); + if (containsTestnet) { + return `${segments[1]}:${segments[2]}`; + } else { + return segments[1]; + } +}; \ No newline at end of file diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index 2414e08ec..8754415af 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -174,8 +174,11 @@ export const ResponseMessages = { failedFetchSchema: 'Failed to fetch schema data', atLeastOneRequired: 'At least one of the attributes should have isReuired as `true`', schemaBuilder: 'Error while creating schema JSON', + schemaUploading: 'Error while uploading schema JSON', W3CSchemaNotFOund: 'Error while resolving W3C schema', - storeW3CSchema: 'Error while storing W3C schema' + storeW3CSchema: 'Error while storing W3C schema', + networkNotFound: 'Error while fetching network', + orgDidAndSchemaType: 'Organization DID and schema type does not match' } }, credentialDefinition: { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 2e22928e9..55fe1f64e 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -201,4 +201,38 @@ export enum ConnectionProcessState { RESPONSE_RECEIVED = 'response-received', COMPLETE = 'completed', ABANDONED = 'abandoned' +} + +export enum SchemaTypeEnum { + JSON = 'json', + INDY = 'indy' + } + +export enum W3CSchemaDataType { + NUMBER = 'number', + INTEGER = 'integer', + STRING = 'string' + } + +export enum JSONSchemaType { + POLYGON_W3C = 'polygon', + LEDGER_LESS = 'no_ledger' +} + +export enum NetworkNamespace { + POLYGON_TESTNET = 'polygon:testnet' +} + +export enum LedgerLessMethods { + WEB = 'web', + KEY = 'key' +} + +export enum LedgerLessConstant { + NO_LEDGER = 'no_ledger', +} + +export enum ledgerLessDIDType { + DID_KEY = 'did:key', + DID_WEB = 'did:web' } \ No newline at end of file