From 090604935ffac2ce6606677831189b0f8c3edcfe Mon Sep 17 00:00:00 2001
From: Konstantin Tsabolov <konstantin.tsabolov@spherity.com>
Date: Tue, 5 Dec 2023 15:56:03 +0100
Subject: [PATCH] feat: implement schema manager base functionality

---
 apps/schema-manager/jest.config.js            |   3 +-
 apps/schema-manager/package.json              |   1 +
 .../src/__tests__/application.spec.ts         |  26 +++
 .../src/{app.module.ts => application.ts}     |  25 ++-
 apps/schema-manager/src/common/constants.ts   |   1 +
 apps/schema-manager/src/main.ts               |   6 +-
 .../__tests__/schemas.controller.spec.ts      | 140 +++++++++++++
 .../schemas/__tests__/schemas.module.spec.ts  |  35 ++++
 .../schemas/__tests__/schemas.service.spec.ts | 139 +++++++++++++
 .../src/schemas/dto/get-by-id.dto.ts          |   9 +
 .../src/schemas/dto/register-schema.dto.ts    |  26 +++
 .../src/schemas/dto/tenant-id.dto.ts          |  12 ++
 .../src/schemas/schemas.controller.ts         | 186 ++++++++++++++++++
 .../src/schemas/schemas.module.ts             |  10 +
 .../src/schemas/schemas.service.ts            |  62 ++++++
 pnpm-lock.yaml                                |   3 +
 16 files changed, 679 insertions(+), 5 deletions(-)
 create mode 100644 apps/schema-manager/src/__tests__/application.spec.ts
 rename apps/schema-manager/src/{app.module.ts => application.ts} (51%)
 create mode 100644 apps/schema-manager/src/schemas/__tests__/schemas.controller.spec.ts
 create mode 100644 apps/schema-manager/src/schemas/__tests__/schemas.module.spec.ts
 create mode 100644 apps/schema-manager/src/schemas/__tests__/schemas.service.spec.ts
 create mode 100644 apps/schema-manager/src/schemas/dto/get-by-id.dto.ts
 create mode 100644 apps/schema-manager/src/schemas/dto/register-schema.dto.ts
 create mode 100644 apps/schema-manager/src/schemas/dto/tenant-id.dto.ts
 create mode 100644 apps/schema-manager/src/schemas/schemas.controller.ts
 create mode 100644 apps/schema-manager/src/schemas/schemas.module.ts
 create mode 100644 apps/schema-manager/src/schemas/schemas.service.ts

diff --git a/apps/schema-manager/jest.config.js b/apps/schema-manager/jest.config.js
index 5ece9fc..ccdd468 100644
--- a/apps/schema-manager/jest.config.js
+++ b/apps/schema-manager/jest.config.js
@@ -31,10 +31,9 @@ export default {
       : ['text-summary', 'html'],
   coveragePathIgnorePatterns: [
     '<rootDir>/node_modules/',
-    '<rootDir>/test/',
     '<rootDir>/coverage/',
     '<rootDir>/dist/',
-    '<rootDir>/**/test',
+    '__tests__',
     '@types',
     '.dto.(t|j)s',
     '.enum.ts',
diff --git a/apps/schema-manager/package.json b/apps/schema-manager/package.json
index 7f32d7f..7c87d2a 100644
--- a/apps/schema-manager/package.json
+++ b/apps/schema-manager/package.json
@@ -32,6 +32,7 @@
     "@nestjs/platform-express": "^10.2.8",
     "@nestjs/swagger": "^7.1.16",
     "@nestjs/terminus": "^10.1.1",
+    "@ocm/shared": "workspace:*",
     "axios": "^1.6.2",
     "class-transformer": "^0.5.1",
     "class-validator": "^0.14.0",
diff --git a/apps/schema-manager/src/__tests__/application.spec.ts b/apps/schema-manager/src/__tests__/application.spec.ts
new file mode 100644
index 0000000..3973b1e
--- /dev/null
+++ b/apps/schema-manager/src/__tests__/application.spec.ts
@@ -0,0 +1,26 @@
+import type { INestApplication } from '@nestjs/common';
+
+import { Test } from '@nestjs/testing';
+
+import { Application } from '../application.js';
+
+describe('Application', () => {
+  let app: INestApplication;
+
+  beforeAll(async () => {
+    const moduleFixture = await Test.createTestingModule({
+      imports: [Application],
+    }).compile();
+
+    app = moduleFixture.createNestApplication();
+    await app.init();
+  });
+
+  afterAll(async () => {
+    await app.close();
+  });
+
+  it('should be defined', () => {
+    expect(app).toBeDefined();
+  });
+});
diff --git a/apps/schema-manager/src/app.module.ts b/apps/schema-manager/src/application.ts
similarity index 51%
rename from apps/schema-manager/src/app.module.ts
rename to apps/schema-manager/src/application.ts
index 2f9297a..f255cc8 100644
--- a/apps/schema-manager/src/app.module.ts
+++ b/apps/schema-manager/src/application.ts
@@ -1,11 +1,16 @@
+import type { ConfigType } from '@nestjs/config';
+
 import { Module } from '@nestjs/common';
 import { ConfigModule } from '@nestjs/config';
+import { ClientsModule, Transport } from '@nestjs/microservices';
 
+import { NATS_CLIENT } from './common/constants.js';
 import { httpConfig } from './config/http.config.js';
 import { natsConfig } from './config/nats.config.js';
 import { ssiConfig } from './config/ssi.config.js';
 import { validationSchema } from './config/validation.js';
 import { HealthModule } from './health/health.module.js';
+import { SchemasModule } from './schemas/schemas.module.js';
 
 @Module({
   imports: [
@@ -20,7 +25,25 @@ import { HealthModule } from './health/health.module.js';
         abortEarly: true,
       },
     }),
+
+    ClientsModule.registerAsync({
+      isGlobal: true,
+      clients: [
+        {
+          name: NATS_CLIENT,
+          inject: [natsConfig.KEY],
+          useFactory: (config: ConfigType<typeof natsConfig>) => ({
+            transport: Transport.NATS,
+            options: {
+              url: config.url as string,
+            },
+          }),
+        },
+      ],
+    }),
+
     HealthModule,
+    SchemasModule,
   ],
 })
-export default class AppModule {}
+export class Application {}
diff --git a/apps/schema-manager/src/common/constants.ts b/apps/schema-manager/src/common/constants.ts
index 159f0fd..5122ca1 100644
--- a/apps/schema-manager/src/common/constants.ts
+++ b/apps/schema-manager/src/common/constants.ts
@@ -1 +1,2 @@
 export const SERVICE_NAME = 'SCHEMA_MANAGER_SERVICE';
+export const NATS_CLIENT = Symbol('NATS_CLIENT');
diff --git a/apps/schema-manager/src/main.ts b/apps/schema-manager/src/main.ts
index 4936407..3ee27f1 100644
--- a/apps/schema-manager/src/main.ts
+++ b/apps/schema-manager/src/main.ts
@@ -1,3 +1,4 @@
+/* c8 ignore start */
 import type { MicroserviceOptions } from '@nestjs/microservices';
 
 import { VersioningType } from '@nestjs/common';
@@ -6,9 +7,9 @@ import { NestFactory } from '@nestjs/core';
 import { Transport } from '@nestjs/microservices';
 import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
 
-import AppModule from './app.module.js';
+import { Application } from './application.js';
 
-const app = await NestFactory.create(AppModule);
+const app = await NestFactory.create(Application);
 const configService = app.get(ConfigService);
 app.enableCors();
 
@@ -36,3 +37,4 @@ SwaggerModule.setup('/swagger', app, document);
 await app.startAllMicroservices();
 
 await app.listen(configService.get('PORT') || 3000);
+/* c8 ignore stop */
diff --git a/apps/schema-manager/src/schemas/__tests__/schemas.controller.spec.ts b/apps/schema-manager/src/schemas/__tests__/schemas.controller.spec.ts
new file mode 100644
index 0000000..055708c
--- /dev/null
+++ b/apps/schema-manager/src/schemas/__tests__/schemas.controller.spec.ts
@@ -0,0 +1,140 @@
+import type { RegisterSchemaPayload } from '../dto/register-schema.dto.js';
+import type { TestingModule } from '@nestjs/testing';
+import type {
+  EventAnonCredsSchemasGetAll,
+  EventAnonCredsSchemasGetById,
+  EventAnonCredsSchemasRegister,
+} from '@ocm/shared';
+
+import { Test } from '@nestjs/testing';
+import { Subject, of, takeUntil } from 'rxjs';
+
+import { NATS_CLIENT } from '../../common/constants.js';
+import { SchemasController } from '../schemas.controller.js';
+import { SchemasService } from '../schemas.service.js';
+
+describe('SchemasController', () => {
+  const natsClientMock = {};
+
+  let controller: SchemasController;
+  let service: SchemasService;
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      controllers: [SchemasController],
+      providers: [
+        { provide: NATS_CLIENT, useValue: natsClientMock },
+        SchemasService,
+      ],
+    }).compile();
+
+    controller = module.get<SchemasController>(SchemasController);
+    service = module.get<SchemasService>(SchemasService);
+  });
+
+  describe('getAll', () => {
+    it('should return a list of schemas', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const tenantId = 'exampleTenantId';
+      const expectedResult: EventAnonCredsSchemasGetAll['data'] = [];
+
+      jest.spyOn(service, 'getAll').mockReturnValue(of(expectedResult));
+
+      controller
+        .getAll({ tenantId })
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+
+  describe('getById', () => {
+    it('should return a schema by id', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const schemaId = 'exampleSchemaId';
+      const tenantId = 'exampleTenantId';
+      const expectedResult: EventAnonCredsSchemasGetById['data'] = {
+        attrNames: ['exampleAttributeName'],
+        issuerId: 'exampleIssuerDid',
+        name: 'exampleName',
+        version: '1.0.0',
+      };
+
+      jest.spyOn(service, 'getById').mockReturnValue(of(expectedResult));
+
+      controller
+        .getById({ schemaId }, { tenantId })
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+
+    it('should throw a NotFoundException if the service returned null', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const schemaId = 'exampleSchemaId';
+      const tenantId = 'exampleTenantId';
+
+      jest.spyOn(service, 'getById').mockReturnValue(of(null));
+
+      controller
+        .getById({ schemaId }, { tenantId })
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe({
+          error: (error) => {
+            expect(error.status).toBe(404);
+            expect(error.message).toBe(`Schema with id ${schemaId} not found`);
+
+            unsubscribe$.next();
+            unsubscribe$.complete();
+
+            done();
+          },
+        });
+    });
+  });
+
+  describe('register', () => {
+    it('should register a new schema', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const tenantId = 'exampleTenantId';
+      const payload: RegisterSchemaPayload = {
+        attributeNames: ['exampleAttributeName'],
+        issuerDid: 'exampleIssuerDid',
+        name: 'exampleName',
+        version: '1.0.0',
+      };
+      const expectedResult: EventAnonCredsSchemasRegister['data'] = {
+        attrNames: payload.attributeNames,
+        issuerId: payload.issuerDid,
+        name: payload.name,
+        version: payload.version,
+      };
+
+      jest.spyOn(service, 'register').mockReturnValue(of(expectedResult));
+
+      controller
+        .register({ tenantId }, payload)
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+});
diff --git a/apps/schema-manager/src/schemas/__tests__/schemas.module.spec.ts b/apps/schema-manager/src/schemas/__tests__/schemas.module.spec.ts
new file mode 100644
index 0000000..861fa9a
--- /dev/null
+++ b/apps/schema-manager/src/schemas/__tests__/schemas.module.spec.ts
@@ -0,0 +1,35 @@
+import { ClientsModule } from '@nestjs/microservices';
+import { Test } from '@nestjs/testing';
+
+import { NATS_CLIENT } from '../../common/constants.js';
+import { SchemasController } from '../schemas.controller.js';
+import { SchemasModule } from '../schemas.module.js';
+import { SchemasService } from '../schemas.service.js';
+
+describe('Schemas Module', () => {
+  let schemasController: SchemasController;
+  let schemasService: SchemasService;
+
+  beforeEach(async () => {
+    const moduleRef = await Test.createTestingModule({
+      imports: [
+        ClientsModule.registerAsync({
+          isGlobal: true,
+          clients: [{ name: NATS_CLIENT, useFactory: () => ({}) }],
+        }),
+        SchemasModule,
+      ],
+    }).compile();
+
+    schemasController = moduleRef.get<SchemasController>(SchemasController);
+    schemasService = moduleRef.get<SchemasService>(SchemasService);
+  });
+
+  it('should be defined', () => {
+    expect(schemasController).toBeDefined();
+    expect(schemasController).toBeInstanceOf(SchemasController);
+
+    expect(schemasService).toBeDefined();
+    expect(schemasService).toBeInstanceOf(SchemasService);
+  });
+});
diff --git a/apps/schema-manager/src/schemas/__tests__/schemas.service.spec.ts b/apps/schema-manager/src/schemas/__tests__/schemas.service.spec.ts
new file mode 100644
index 0000000..8d91e99
--- /dev/null
+++ b/apps/schema-manager/src/schemas/__tests__/schemas.service.spec.ts
@@ -0,0 +1,139 @@
+import type { TestingModule } from '@nestjs/testing';
+
+import { Test } from '@nestjs/testing';
+import {
+  EventAnonCredsSchemasGetAll,
+  EventAnonCredsSchemasGetById,
+  EventAnonCredsSchemasRegister,
+} from '@ocm/shared';
+import { Subject, of, takeUntil } from 'rxjs';
+
+import { NATS_CLIENT } from '../../common/constants.js';
+import { SchemasService } from '../schemas.service.js';
+
+describe('SchemasService', () => {
+  let service: SchemasService;
+  const natsClientMock = { send: jest.fn() };
+
+  beforeEach(async () => {
+    const module: TestingModule = await Test.createTestingModule({
+      providers: [
+        { provide: NATS_CLIENT, useValue: natsClientMock },
+        SchemasService,
+      ],
+    }).compile();
+
+    service = module.get<SchemasService>(SchemasService);
+
+    jest.resetAllMocks();
+  });
+
+  describe('getAll', () => {
+    it('should return the data from NATS client', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const payload = {
+        tenantId: 'mocked tenantId',
+        endpoint: EventAnonCredsSchemasGetAll.token,
+      };
+      const expectedResult: EventAnonCredsSchemasGetAll['data'] = [];
+
+      natsClientMock.send.mockReturnValueOnce(
+        of(new EventAnonCredsSchemasGetAll([], payload.tenantId)),
+      );
+
+      service
+        .getAll(payload)
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(natsClientMock.send).toHaveBeenCalledWith(
+            { endpoint: EventAnonCredsSchemasGetAll.token },
+            payload,
+          );
+
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+
+  describe('getById', () => {
+    it('should return the data from NATS client', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const payload = {
+        tenantId: 'mocked tenantId',
+        schemaId: 'mocked id',
+      };
+      const expectedResult: EventAnonCredsSchemasGetById['data'] = {
+        issuerId: 'mocked issuerDid',
+        name: 'mocked name',
+        version: '1.0.0',
+        attrNames: ['mocked attribute1', 'mocked attribute2'],
+      };
+
+      natsClientMock.send.mockReturnValueOnce(
+        of(new EventAnonCredsSchemasGetById(expectedResult, payload.tenantId)),
+      );
+
+      service
+        .getById(payload)
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(natsClientMock.send).toHaveBeenCalledWith(
+            { endpoint: EventAnonCredsSchemasGetById.token },
+            payload,
+          );
+
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+
+  describe('register', () => {
+    it('should return the data from NATS client', (done) => {
+      const unsubscribe$ = new Subject<void>();
+      const payload = {
+        tenantId: 'mocked tenantId',
+        issuerDid: 'mocked issuerDid',
+        name: 'mocked name',
+        version: '1.0.0',
+        attributeNames: ['mocked attribute1', 'mocked attribute2'],
+      };
+      const expectedResult: EventAnonCredsSchemasRegister['data'] = {
+        issuerId: 'mocked issuerDid',
+        name: 'mocked name',
+        version: '1.0.0',
+        attrNames: ['mocked attribute1', 'mocked attribute2'],
+      };
+
+      natsClientMock.send.mockReturnValueOnce(
+        of(new EventAnonCredsSchemasRegister(expectedResult, payload.tenantId)),
+      );
+
+      service
+        .register(payload)
+        .pipe(takeUntil(unsubscribe$))
+        .subscribe((result) => {
+          expect(natsClientMock.send).toHaveBeenCalledWith(
+            { endpoint: EventAnonCredsSchemasRegister.token },
+            payload,
+          );
+
+          expect(result).toStrictEqual(expectedResult);
+
+          unsubscribe$.next();
+          unsubscribe$.complete();
+
+          done();
+        });
+    });
+  });
+});
diff --git a/apps/schema-manager/src/schemas/dto/get-by-id.dto.ts b/apps/schema-manager/src/schemas/dto/get-by-id.dto.ts
new file mode 100644
index 0000000..d01de1f
--- /dev/null
+++ b/apps/schema-manager/src/schemas/dto/get-by-id.dto.ts
@@ -0,0 +1,9 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, IsString } from 'class-validator';
+
+export class GetByIdParams {
+  @IsString()
+  @IsNotEmpty()
+  @ApiProperty({ description: 'The schema ID to retrieve', format: 'string' })
+  public schemaId: string;
+}
diff --git a/apps/schema-manager/src/schemas/dto/register-schema.dto.ts b/apps/schema-manager/src/schemas/dto/register-schema.dto.ts
new file mode 100644
index 0000000..f8d2536
--- /dev/null
+++ b/apps/schema-manager/src/schemas/dto/register-schema.dto.ts
@@ -0,0 +1,26 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsArray, IsNotEmpty, IsSemVer, IsString } from 'class-validator';
+
+export class RegisterSchemaPayload {
+  @IsString()
+  @IsNotEmpty()
+  @ApiProperty()
+  public issuerDid: string;
+
+  @IsString()
+  @IsNotEmpty()
+  @ApiProperty()
+  public name: string;
+
+  @IsString()
+  @IsNotEmpty()
+  @IsSemVer()
+  @ApiProperty()
+  public version: string;
+
+  @IsArray()
+  @IsString({ each: true })
+  @IsNotEmpty({ each: true })
+  @ApiProperty()
+  public attributeNames: Array<string>;
+}
diff --git a/apps/schema-manager/src/schemas/dto/tenant-id.dto.ts b/apps/schema-manager/src/schemas/dto/tenant-id.dto.ts
new file mode 100644
index 0000000..5498941
--- /dev/null
+++ b/apps/schema-manager/src/schemas/dto/tenant-id.dto.ts
@@ -0,0 +1,12 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, IsString } from 'class-validator';
+
+export class TenantIdParam {
+  @IsString()
+  @IsNotEmpty()
+  @ApiProperty({
+    description: 'The tenant ID to use for the request',
+    format: 'string',
+  })
+  public tenantId: string;
+}
diff --git a/apps/schema-manager/src/schemas/schemas.controller.ts b/apps/schema-manager/src/schemas/schemas.controller.ts
new file mode 100644
index 0000000..8c52ef1
--- /dev/null
+++ b/apps/schema-manager/src/schemas/schemas.controller.ts
@@ -0,0 +1,186 @@
+import type {
+  EventAnonCredsSchemasGetAll,
+  EventAnonCredsSchemasGetById,
+  EventAnonCredsSchemasRegister,
+} from '@ocm/shared';
+
+import {
+  Body,
+  Controller,
+  Get,
+  HttpStatus,
+  NotFoundException,
+  Param,
+  Post,
+  Query,
+  UsePipes,
+  ValidationPipe,
+  Version,
+} from '@nestjs/common';
+import {
+  ApiBody,
+  ApiOperation,
+  ApiParam,
+  ApiQuery,
+  ApiResponse,
+  ApiTags,
+} from '@nestjs/swagger';
+import { Observable, of, switchMap } from 'rxjs';
+
+import { GetByIdParams } from './dto/get-by-id.dto.js';
+import { RegisterSchemaPayload } from './dto/register-schema.dto.js';
+import { TenantIdParam } from './dto/tenant-id.dto.js';
+import { SchemasService } from './schemas.service.js';
+
+@Controller('schemas')
+@UsePipes(new ValidationPipe({ transform: true, whitelist: true }))
+@ApiTags('Schemas')
+export class SchemasController {
+  public constructor(private readonly schemasService: SchemasService) {}
+
+  @Version('1')
+  @Get()
+  @ApiOperation({
+    summary: 'Fetch a list of schemas',
+    description: 'This call provides a list of schemas for a given tenant',
+  })
+  @ApiQuery({ name: 'tenantId', required: true })
+  @ApiResponse({
+    status: HttpStatus.OK,
+    description: 'Schemas fetched successfully',
+    content: {
+      // TBD
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.NOT_FOUND,
+    description: 'Tenant not found',
+    content: {
+      // TBD
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.INTERNAL_SERVER_ERROR,
+    description: 'Internal server error',
+    content: {
+      // TBD
+    },
+  })
+  public getAll(
+    @Query() { tenantId }: TenantIdParam,
+  ): Observable<EventAnonCredsSchemasGetAll['data']> {
+    return this.schemasService.getAll({
+      tenantId,
+    });
+  }
+
+  @Version('1')
+  @Get(':schemaId')
+  @ApiOperation({
+    summary: 'Fetch a schema by id',
+    description:
+      'This call allows you to retrieve schema data for a given tenant by specifying the `schemaId`.',
+  })
+  @ApiParam({ name: 'schemaId', required: true })
+  @ApiQuery({ name: 'tenantId', required: true })
+  @ApiResponse({
+    status: HttpStatus.OK,
+    description: 'Schema fetched successfully',
+    content: {
+      // TBD
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.NOT_FOUND,
+    description: 'Tenant not found',
+    content: {
+      // TBD
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.NOT_FOUND,
+    description: 'Schema not found',
+    content: {
+      // TBD
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.INTERNAL_SERVER_ERROR,
+    description: 'Internal server error',
+    content: {
+      // TBD
+    },
+  })
+  public getById(
+    @Param() { schemaId }: GetByIdParams,
+    @Query() { tenantId }: TenantIdParam,
+  ): Observable<EventAnonCredsSchemasGetById['data']> {
+    return this.schemasService
+      .getById({
+        tenantId,
+        schemaId,
+      })
+      .pipe(
+        switchMap((schema) => {
+          if (schema === null) {
+            throw new NotFoundException(`Schema with id ${schemaId} not found`);
+          }
+          return of(schema);
+        }),
+      );
+  }
+
+  @Version('1')
+  @Post()
+  @ApiOperation({
+    summary: 'Register a new schema',
+    description:
+      'This call provides the capability to create new schema on ledger by name, author, version, schema attributes and type. Later this schema can be used to issue new credential definition. This call returns an information about created schema.',
+  })
+  @ApiQuery({ name: 'tenantId', required: true })
+  @ApiBody({ type: RegisterSchemaPayload })
+  @ApiResponse({
+    status: HttpStatus.CREATED,
+    description: 'Schema registered successfully',
+    content: {
+      'application/json': {},
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.NOT_FOUND,
+    description: 'Tenant not found',
+    content: {
+      'application/json': {},
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.BAD_REQUEST,
+    description: 'All fields are required for schema registration',
+    content: {
+      'application/json': {},
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.CONFLICT,
+    description: 'Schema already exists',
+    content: {
+      'application/json': {},
+    },
+  })
+  @ApiResponse({
+    status: HttpStatus.INTERNAL_SERVER_ERROR,
+    description: 'Internal server error',
+    content: {
+      'application/json': {},
+    },
+  })
+  public register(
+    @Query() { tenantId }: TenantIdParam,
+    @Body() payload: RegisterSchemaPayload,
+  ): Observable<EventAnonCredsSchemasRegister['data']> {
+    return this.schemasService.register({
+      ...payload,
+      tenantId,
+    });
+  }
+}
diff --git a/apps/schema-manager/src/schemas/schemas.module.ts b/apps/schema-manager/src/schemas/schemas.module.ts
new file mode 100644
index 0000000..be2705b
--- /dev/null
+++ b/apps/schema-manager/src/schemas/schemas.module.ts
@@ -0,0 +1,10 @@
+import { Module } from '@nestjs/common';
+
+import { SchemasController } from './schemas.controller.js';
+import { SchemasService } from './schemas.service.js';
+
+@Module({
+  controllers: [SchemasController],
+  providers: [SchemasService],
+})
+export class SchemasModule {}
diff --git a/apps/schema-manager/src/schemas/schemas.service.ts b/apps/schema-manager/src/schemas/schemas.service.ts
new file mode 100644
index 0000000..6a630e9
--- /dev/null
+++ b/apps/schema-manager/src/schemas/schemas.service.ts
@@ -0,0 +1,62 @@
+import type {
+  EventAnonCredsSchemasGetAllInput,
+  EventAnonCredsSchemasGetByIdInput,
+  EventAnonCredsSchemasRegisterInput,
+} from '@ocm/shared';
+
+import { Inject, Injectable } from '@nestjs/common';
+import { ClientProxy } from '@nestjs/microservices';
+import {
+  EventAnonCredsSchemasGetAll,
+  EventAnonCredsSchemasGetById,
+  EventAnonCredsSchemasRegister,
+} from '@ocm/shared';
+import { map, type Observable } from 'rxjs';
+
+import { NATS_CLIENT } from '../common/constants.js';
+
+@Injectable()
+export class SchemasService {
+  public constructor(
+    @Inject(NATS_CLIENT) private readonly natsClient: ClientProxy,
+  ) {}
+
+  public getAll(
+    payload: EventAnonCredsSchemasGetAllInput,
+  ): Observable<EventAnonCredsSchemasGetAll['data']> {
+    const pattern = { endpoint: EventAnonCredsSchemasGetAll.token };
+
+    return this.natsClient
+      .send<EventAnonCredsSchemasGetAll, EventAnonCredsSchemasGetAllInput>(
+        pattern,
+        payload,
+      )
+      .pipe(map((result) => result.data));
+  }
+
+  public getById(
+    payload: EventAnonCredsSchemasGetByIdInput,
+  ): Observable<EventAnonCredsSchemasGetById['data']> {
+    const pattern = { endpoint: EventAnonCredsSchemasGetById.token };
+
+    return this.natsClient
+      .send<EventAnonCredsSchemasGetById, EventAnonCredsSchemasGetByIdInput>(
+        pattern,
+        payload,
+      )
+      .pipe(map((result) => result.data));
+  }
+
+  public register(
+    payload: EventAnonCredsSchemasRegisterInput,
+  ): Observable<EventAnonCredsSchemasRegister['data']> {
+    const pattern = { endpoint: EventAnonCredsSchemasRegister.token };
+
+    return this.natsClient
+      .send<EventAnonCredsSchemasRegister, EventAnonCredsSchemasRegisterInput>(
+        pattern,
+        payload,
+      )
+      .pipe(map((result) => result.data));
+  }
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 02d4884..c6f02fb 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -603,6 +603,9 @@ importers:
       '@nestjs/terminus':
         specifier: ^10.1.1
         version: 10.1.1(@nestjs/axios@3.0.1)(@nestjs/common@10.2.10)(@nestjs/core@10.2.10)(@nestjs/microservices@10.2.10)(reflect-metadata@0.1.13)(rxjs@7.8.1)
+      '@ocm/shared':
+        specifier: workspace:*
+        version: link:../shared
       axios:
         specifier: ^1.6.2
         version: 1.6.2
-- 
GitLab