Skip to content
Snippets Groups Projects
Verified Commit ee9e5819 authored by Konstantin Tsabolov's avatar Konstantin Tsabolov
Browse files

Merge branch 'main' into offer-to-self-event

parents 7f3dbbe2 955494fd
No related branches found
No related tags found
No related merge requests found
Pipeline #39751 canceled
Showing
with 1589 additions and 0 deletions
include:
- project: 'eclipse/xfsc/dev-ops/ci-templates'
file: 'helm-build-ci.yaml'
ref: main
variables:
DOCKERFILE: Dockerfile
TAG: ${HARBOR_HOST}/${HARBOR_PROJECT}/$SERVICE
......
This diff is collapsed.
import { readFileSync } from 'node:fs';
const swcConfig = JSON.parse(readFileSync('../../.swcrc', 'utf8'));
/** @type {import('jest').Config} */
export default {
moduleFileExtensions: ['js', 'ts'],
testEnvironment: 'node',
transform: {
'^.+\\.(js|ts)$': [
'@swc/jest',
{
...swcConfig,
sourceMaps: false,
exclude: [],
swcrc: false,
},
],
},
extensionsToTreatAsEsm: ['.ts'],
moduleNameMapper: {
// ESM modules require `.js` extension to be specified, but Jest doesn't work with them
// Removing `.js` extension from module imports
'^uuid$': 'uuid',
'^(.*)/(.*)\\.js$': '$1/$2',
},
collectCoverageFrom: ['src/**/*.(t|j)s'],
coverageReporters:
process.env.CI === 'true'
? ['text-summary', 'json-summary']
: ['text-summary', 'html'],
coveragePathIgnorePatterns: [
'<rootDir>/node_modules/',
'<rootDir>/coverage/',
'<rootDir>/dist/',
'__tests__',
'@types',
'.dto.(t|j)s',
'.enum.ts',
'.interface.ts',
'.type.ts',
'.spec.ts',
],
coverageDirectory: './coverage',
// With v8 coverage provider it's much faster, but
// with this enabled it's not possible to ignore whole files' coverage
coverageProvider: 'v8',
};
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"typeCheck": true,
"builder": {
"type": "swc",
"options": {
"swcrcPath": "../../.swcrc"
}
}
}
}
{
"name": "@ocm/did-manager",
"version": "1.0.0",
"description": "",
"author": "Konstantin Tsabolov <konstantin.tsabolov@spherity.com>",
"contributors": [
"Konstantin Tsabolov <konstantin.tsabolov@spherity.com>"
],
"private": true,
"license": "Apache-2.0",
"type": "module",
"scripts": {
"clean": "rimraf dist coverage *.tsbuildinfo",
"prebuild": "pnpm clean",
"build": "nest build -p tsconfig.production.json",
"start": "nest start --watch --preserveWatchOutput",
"test": "jest"
},
"dependencies": {
"@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.3.0",
"@nestjs/microservices": "^10.3.0",
"@nestjs/platform-express": "^10.3.0",
"@nestjs/swagger": "^7.2.0",
"@ocm/shared": "workspace:*",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"express": "^4.17.3",
"joi": "^17.11.0",
"nats": "^2.18.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
"@nestjs/schematics": "^10.1.0",
"@nestjs/testing": "^10.3.0",
"@swc/cli": "^0.1.62",
"@swc/core": "^1.3.96",
"@swc/jest": "^0.2.29",
"@types/express": "^4.17.21",
"@types/jest": "^29.5.8",
"@types/node": "^20.9.0",
"jest": "^29.7.0",
"rimraf": "^5.0.5",
"typescript": "^5.3.2"
}
}
import type { ConfigType } from '@nestjs/config';
import type { ClientProvider } from '@nestjs/microservices';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { RouterModule } from '@nestjs/core';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { HealthModule } from '@ocm/shared';
import { NATS_CLIENT } from './common/constants.js';
import { httpConfig } from './config/http.config.js';
import { natsConfig } from './config/nats.config.js';
import { validationSchema } from './config/validation.js';
import { DIDsModule } from './dids/dids.module.js';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
load: [httpConfig, natsConfig],
cache: true,
expandVariables: true,
validationSchema,
validationOptions: {
allowUnknown: true,
abortEarly: true,
},
}),
ClientsModule.registerAsync({
isGlobal: true,
clients: [
{
name: NATS_CLIENT,
inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => {
const provider: Required<ClientProvider> = {
transport: Transport.NATS,
options: {
servers: config.url as string,
},
};
if ('user' in config && 'password' in config) {
provider.options.user = config.user as string;
provider.options.pass = config.password as string;
}
return provider;
},
},
],
}),
HealthModule.registerAsync({
inject: [natsConfig.KEY],
useFactory: (config: ConfigType<typeof natsConfig>) => {
const options: Parameters<typeof HealthModule.register>[0] = {};
if (config.monitoringUrl) {
options.nats = {
monitoringUrl: config.monitoringUrl as string,
};
}
return options;
},
}),
DIDsModule,
RouterModule.register([
{ module: HealthModule, path: '/health' },
{ module: DIDsModule, path: '/dids' },
]),
],
})
export class Application {}
export const SERVICE_NAME = 'DID_MANAGER_SERVICE';
export const NATS_CLIENT = Symbol('NATS_CLIENT');
import { registerAs } from '@nestjs/config';
export const httpConfig = registerAs('http', () => ({
host: process.env.HTTP_HOST || '0.0.0.0',
port: Number(process.env.HTTP_PORT) || 3000,
}));
import { registerAs } from '@nestjs/config';
export const natsConfig = registerAs('nats', () => ({
url: process.env.NATS_URL || 'nats://localhost:4222',
user: process.env.NATS_USER,
password: process.env.NATS_PASSWORD,
monitoringUrl: process.env.NATS_MONITORING_URL || 'http://localhost:8222',
}));
import Joi from 'joi';
export const validationSchema = Joi.object({
HTTP_HOST: Joi.string(),
HTTP_PORT: Joi.number(),
NATS_URL: Joi.string().uri(),
NATS_USER: Joi.string().optional(),
NATS_PASSWORD: Joi.string().optional(),
NATS_MONITORING_URL: Joi.string().uri(),
});
import type { TestingModule } from '@nestjs/testing';
import type {
EventDidsDidConfiguration,
EventDidsRegisterIndyFromSeed,
EventDidsRegisterIndyFromSeedInput,
EventDidsResolve,
} from '@ocm/shared';
import { Test } from '@nestjs/testing';
import { Subject, of, takeUntil } from 'rxjs';
import { NATS_CLIENT } from '../../common/constants.js';
import { DIDsController } from '../dids.controller.js';
import { DIDsService } from '../dids.service.js';
describe('DIDsController', () => {
const natsClientMock = {};
let controller: DIDsController;
let service: DIDsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [DIDsController],
providers: [
{ provide: NATS_CLIENT, useValue: natsClientMock },
DIDsService,
],
}).compile();
controller = module.get<DIDsController>(DIDsController);
service = module.get<DIDsService>(DIDsService);
});
describe('resolve', () => {
it('should call service.resolve with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const did = 'did';
const expectedResult = {} as EventDidsResolve['data'];
jest.spyOn(service, 'resolve').mockReturnValueOnce(of(expectedResult));
controller
.resolve({ tenantId }, { did })
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(service.resolve).toHaveBeenCalledWith(tenantId, did);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('registerFromSeed', () => {
it('should call service.registerFromSeed with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const seed = 'seed';
const services: EventDidsRegisterIndyFromSeedInput['services'] = [
{
identifier: 'serviceId',
type: 'serviceType',
url: 'serviceUrl',
},
];
const expectedResult = {} as EventDidsRegisterIndyFromSeed['data'];
jest
.spyOn(service, 'registerFromSeed')
.mockReturnValueOnce(of(expectedResult));
controller
.registerFromSeed({ tenantId }, { seed, services })
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(service.registerFromSeed).toHaveBeenCalledWith(
tenantId,
seed,
services,
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('getConfiguration', () => {
it('should call service.getConfiguration with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const domain = 'domain';
const expiryTime = 123;
const expectedResult = {} as EventDidsDidConfiguration['data'];
jest
.spyOn(service, 'getConfiguration')
.mockReturnValueOnce(of(expectedResult));
controller
.getConfiguration({ tenantId }, { domain, expiryTime })
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(service.getConfiguration).toHaveBeenCalledWith(
tenantId,
domain,
expiryTime,
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
});
import { ClientsModule } from '@nestjs/microservices';
import { Test } from '@nestjs/testing';
import { NATS_CLIENT } from '../../common/constants.js';
import { DIDsController } from '../dids.controller.js';
import { DIDsModule } from '../dids.module.js';
import { DIDsService } from '../dids.service.js';
describe('DIDsModule', () => {
let didsController: DIDsController;
let didsService: DIDsService;
beforeEach(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ClientsModule.registerAsync({
isGlobal: true,
clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }],
}),
DIDsModule,
],
}).compile();
didsController = moduleRef.get<DIDsController>(DIDsController);
didsService = moduleRef.get<DIDsService>(DIDsService);
});
it('should be defined', () => {
expect(didsController).toBeDefined();
expect(didsController).toBeInstanceOf(DIDsController);
expect(didsService).toBeDefined();
expect(didsService).toBeInstanceOf(DIDsService);
});
});
import type { EventDidsRegisterIndyFromSeedInput } from '@ocm/shared';
import { Test } from '@nestjs/testing';
import {
EventDidsDidConfiguration,
EventDidsRegisterIndyFromSeed,
EventDidsResolve,
} from '@ocm/shared';
import { Subject, of, takeUntil } from 'rxjs';
import { NATS_CLIENT } from '../../common/constants.js';
import { DIDsService } from '../dids.service.js';
describe('DIDsService', () => {
let service: DIDsService;
const natsClientMock = { send: jest.fn() };
beforeEach(async () => {
jest.resetAllMocks();
const module = await Test.createTestingModule({
providers: [
{ provide: NATS_CLIENT, useValue: natsClientMock },
DIDsService,
],
}).compile();
service = module.get<DIDsService>(DIDsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('resolve', () => {
it('should call natsClient.send with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const did = 'did';
const expectedResult = {} as EventDidsResolve['data'];
natsClientMock.send.mockReturnValueOnce(
of(new EventDidsResolve(expectedResult, tenantId)),
);
service
.resolve(tenantId, did)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventDidsResolve.token,
{ tenantId, did },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('registerFromSeed', () => {
it('should call natsClient.send with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const seed = 'seed';
const services: EventDidsRegisterIndyFromSeedInput['services'] = [
{
type: 'indy',
url: 'url',
identifier: 'identifier',
},
];
const expectedResult = {} as EventDidsRegisterIndyFromSeed['data'];
natsClientMock.send.mockReturnValueOnce(
of(new EventDidsRegisterIndyFromSeed(expectedResult, tenantId)),
);
service
.registerFromSeed(tenantId, seed, services)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventDidsRegisterIndyFromSeed.token,
{ tenantId, seed, services },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
describe('getConfiguration', () => {
it('should call natsClient.send with the correct arguments', (done) => {
const unsubscribe$ = new Subject<void>();
const tenantId = 'tenantId';
const domain = 'domain';
const expiryTime = 1;
const expectedResult: EventDidsDidConfiguration['data'] = {
entries: [],
};
natsClientMock.send.mockReturnValueOnce(
of(new EventDidsDidConfiguration(expectedResult, tenantId)),
);
service
.getConfiguration(tenantId, domain, expiryTime)
.pipe(takeUntil(unsubscribe$))
.subscribe((result) => {
expect(natsClientMock.send).toHaveBeenCalledWith(
EventDidsDidConfiguration.token,
{ tenantId, domain, expiryTime },
);
expect(result).toStrictEqual(expectedResult);
unsubscribe$.next();
unsubscribe$.complete();
done();
});
});
});
});
import {
Body,
Controller,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Query,
UseInterceptors,
UsePipes,
ValidationPipe,
} from '@nestjs/common';
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { MultitenancyParams, ResponseFormatInterceptor } from '@ocm/shared';
import { DIDsService } from './dids.service.js';
import { GetConfigurationPayload } from './dto/get-configuration.dto.js';
import { RegisterFromSeedPayload } from './dto/register-from-seed.dto.js';
import { ResolveParams } from './dto/resolve.dto.js';
@Controller()
@ApiTags('DIDs')
@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
@UseInterceptors(new ResponseFormatInterceptor())
export class DIDsController {
public constructor(private readonly service: DIDsService) {}
@Get(':did')
@ApiOperation({
summary: 'Resolve DID',
description: 'Resolve DID',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'DID resolved successfully',
content: {
'application/json': {
schema: {},
examples: {
'DID resolved successfully': {
value: {
statusCode: 200,
message: 'DID resolved successfully',
data: {},
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'DID not found',
content: {
'application/json': {
schema: {},
examples: {
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
},
},
'DID not found': {
value: {
statusCode: 404,
message: 'DID not found',
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid DID',
content: {
'application/json': {
schema: {},
examples: {
'Invalid DID': {
value: {
statusCode: 400,
message: 'Invalid DID',
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'Something went wrong',
content: {
'application/json': {
schema: {},
examples: {
'Something went wrong': {
value: {
statusCode: 500,
message: 'Something went wrong',
error: 'Internal Server Error',
},
},
},
},
},
})
public resolve(
@Query() { tenantId }: MultitenancyParams,
@Param() { did }: ResolveParams,
) {
return this.service.resolve(tenantId, did);
}
@Post()
@ApiOperation({
summary: 'Register DID from seed',
description: 'Register DID from seed',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'DID registered successfully',
content: {
'application/json': {
schema: {},
examples: {
'DID registered successfully': {
value: {
statusCode: 200,
message: 'DID registered successfully',
data: {},
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.BAD_REQUEST,
description: 'Invalid seed',
content: {
'application/json': {
schema: {},
examples: {
'Invalid seed': {
value: {
statusCode: 400,
message: 'Invalid seed',
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Tenant not found',
content: {
'application/json': {
schema: {},
examples: {
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'Something went wrong',
content: {
'application/json': {
schema: {},
examples: {
'Something went wrong': {
value: {
statusCode: 500,
message: 'Something went wrong',
error: 'Internal Server Error',
},
},
},
},
},
})
public registerFromSeed(
@Query() { tenantId }: MultitenancyParams,
@Body() { seed, services }: RegisterFromSeedPayload,
) {
return this.service.registerFromSeed(tenantId, seed, services);
}
@Post('configuration')
@HttpCode(HttpStatus.OK)
@ApiOperation({
summary: 'Get DID configuration',
description: 'Get DID configuration',
})
@ApiResponse({
status: HttpStatus.OK,
description: 'DID configuration fetched successfully',
content: {
'application/json': {
schema: {},
examples: {
'DID configuration fetched successfully': {
value: {
statusCode: 200,
message: 'DID configuration fetched successfully',
data: {},
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.NOT_FOUND,
description: 'Tenant not found',
content: {
'application/json': {
schema: {},
examples: {
'Tenant not found': {
value: {
statusCode: 404,
message: 'Tenant not found',
},
},
},
},
},
})
@ApiResponse({
status: HttpStatus.INTERNAL_SERVER_ERROR,
description: 'Something went wrong',
content: {
'application/json': {
schema: {},
examples: {
'Something went wrong': {
value: {
statusCode: 500,
message: 'Something went wrong',
error: 'Internal Server Error',
},
},
},
},
},
})
public getConfiguration(
@Query() { tenantId }: MultitenancyParams,
@Body() { domain, expiryTime }: GetConfigurationPayload,
) {
return this.service.getConfiguration(tenantId, domain, expiryTime);
}
}
import { Module } from '@nestjs/common';
import { DIDsController } from './dids.controller.js';
import { DIDsService } from './dids.service.js';
@Module({
providers: [DIDsService],
controllers: [DIDsController],
})
export class DIDsModule {}
import type {
EventDidsDidConfigurationInput,
EventDidsRegisterIndyFromSeedInput,
EventDidsResolveInput,
} from '@ocm/shared';
import { Inject, Injectable } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import {
EventDidsDidConfiguration,
EventDidsRegisterIndyFromSeed,
EventDidsResolve,
} from '@ocm/shared';
import { map } from 'rxjs';
import { NATS_CLIENT } from '../common/constants.js';
@Injectable()
export class DIDsService {
public constructor(
@Inject(NATS_CLIENT) private readonly natsClient: ClientProxy,
) {}
public resolve(tenantId: string, did: EventDidsResolveInput['did']) {
return this.natsClient
.send<
EventDidsResolve,
EventDidsResolveInput
>(EventDidsResolve.token, { tenantId, did })
.pipe(map(({ data }) => data));
}
public registerFromSeed(
tenantId: string,
seed: EventDidsRegisterIndyFromSeedInput['seed'],
services?: EventDidsRegisterIndyFromSeedInput['services'],
) {
return this.natsClient
.send<
EventDidsRegisterIndyFromSeed,
EventDidsRegisterIndyFromSeedInput
>(EventDidsRegisterIndyFromSeed.token, { tenantId, seed, services })
.pipe(map(({ data }) => data));
}
public getConfiguration(
tenantId: string,
domain: EventDidsDidConfigurationInput['domain'],
expiryTime: EventDidsDidConfigurationInput['expiryTime'],
) {
return this.natsClient
.send<
EventDidsDidConfiguration,
EventDidsDidConfigurationInput
>(EventDidsDidConfiguration.token, { tenantId, domain, expiryTime })
.pipe(map(({ data }) => data));
}
}
import { IsNumber, IsString } from 'class-validator';
export class GetConfigurationPayload {
@IsString()
public domain: string;
@IsNumber()
public expiryTime: number;
}
import { ApiProperty } from '@nestjs/swagger';
import { IsString, IsArray } from 'class-validator';
export class RegisterFromSeedPayload {
@IsString()
@ApiProperty({
description: 'Seed to use for DID generation',
example: '000000000000000000000000Steward1',
})
public seed: string;
@IsArray()
@ApiProperty({
description: 'Services to associate with DID',
example: [
{
identifier: 'example',
url: 'https://example.com',
type: 'example',
},
],
})
public services?: Array<{
identifier: string;
url: string;
type: string;
}>;
}
import { ApiProperty } from '@nestjs/swagger';
import { IsString, Matches } from 'class-validator';
export class ResolveParams {
@IsString()
@Matches(/^did:[a-z0-9]+:.+$/)
@ApiProperty({
description: 'DID to resolve',
example: 'did:example:123',
})
public did: string;
}
/* c8 ignore start */
import type { ConfigType } from '@nestjs/config';
import type { MicroserviceOptions, NatsOptions } from '@nestjs/microservices';
import { Logger, VersioningType } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { Transport } from '@nestjs/microservices';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import { Application } from './application.js';
import { httpConfig } from './config/http.config.js';
import { natsConfig } from './config/nats.config.js';
const app = await NestFactory.create(Application);
app.enableCors();
const { url, user, password } = app.get(natsConfig.KEY) as ConfigType<
typeof natsConfig
>;
const microserviceOptions: Required<NatsOptions> = {
transport: Transport.NATS,
options: {
servers: [url],
},
};
if (user && password) {
microserviceOptions.options.user = user;
microserviceOptions.options.pass = password;
}
app.connectMicroservice<MicroserviceOptions>(microserviceOptions);
app.enableVersioning({
defaultVersion: ['1'],
type: VersioningType.URI,
});
const swaggerConfig = new DocumentBuilder()
.setTitle('Gaia-X OCM DID Manager API')
.setDescription('API documentation for Gaia-X OCM DID Manager')
.setVersion('1.0')
.build();
const document = SwaggerModule.createDocument(app, swaggerConfig);
SwaggerModule.setup('/', app, document);
await app.startAllMicroservices();
const { host, port } = app.get(httpConfig.KEY) as ConfigType<typeof httpConfig>;
await app.listen(port as number, host as string);
Logger.log(`Application is running on: ${await app.getUrl()}`);
/* c8 ignore stop */
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment