TypeScript library for developing Hyperledger Fabric chaincode with ORM-like entity management, relations, pagination, and automatic validation
npm install @hlf-core/chaincode> TypeScript библиотека для разработки chaincode в Hyperledger Fabric


@hlf-core/chaincode — это высокоуровневая TypeScript библиотека для разработки chaincode (смарт-контрактов) в Hyperledger Fabric. Она предоставляет абстракции и инструменты для управления данными, сущностями и их отношениями в блокчейн-реестре.
- 🗄️ Управление базой данных — работа с key-value хранилищем Hyperledger Fabric
- 📦 Управление сущностями — ORM-подобный API для работы с бизнес-объектами
- 🔗 Отношения между сущностями — управление связями parent-child между объектами
- 📄 Пагинация — эффективная работа с большими наборами данных
- ✅ Валидация и трансформация — автоматическая сериализация/десериализация с валидацией
- 📘 TypeScript — полная типизация и поддержка современных возможностей языка
- 📦 Dual Package — поддержка как CommonJS, так и ES Modules
``bash`
npm install @hlf-core/chaincode
Библиотека требует следующие пакеты:
`bash`
npm install @ts-core/common fabric-shim reflect-metadata
`typescript
import { IUIDable } from '@ts-core/common';
import { IsString, IsNumber } from 'class-validator';
export class User implements IUIDable {
@IsString()
uid: string;
@IsString()
name: string;
@IsNumber()
age: number;
@IsString()
email: string;
}
`
`typescript
import { EntityManagerImpl } from '@hlf-core/chaincode';
import { ILogger, TransformUtil } from '@ts-core/common';
import { IStub } from '@hlf-core/chaincode';
export class UserManager extends EntityManagerImpl
constructor(logger: ILogger, stub: IStub) {
super(logger, stub);
}
// Обязательный метод: преобразование из plain object в класс
public toEntity(item: any): User {
return TransformUtil.toClass(User, item);
}
// Префикс для хранения в блокчейне
public get prefix(): string {
return 'user:';
}
// Опционально: загрузка связанных данных
public async loadDetails(item: User, details?: Array
// Загрузка дополнительных данных, если нужно
}
}
`
`typescript
import { Context } from 'fabric-contract-api';
export class MyContract {
async createUser(ctx: Context, userId: string, name: string, age: number, email: string): Promise
const logger = // ваш logger
const stub = // адаптер для ctx.stub
const userManager = new UserManager(logger, stub);
const user = new User();
user.uid = userId;
user.name = name;
user.age = age;
user.email = email;
// Сохранение с автоматической валидацией и сериализацией
await userManager.save(user);
}
async getUser(ctx: Context, userId: string): Promise
const logger = // ваш logger
const stub = // адаптер для ctx.stub
const userManager = new UserManager(logger, stub);
// Получение с автоматической десериализацией и валидацией
return await userManager.get(userId);
}
async getAllUsers(ctx: Context, pageSize: number, bookmark?: string): Promise
const logger = // ваш logger
const stub = // адаптер для ctx.stub
const userManager = new UserManager(logger, stub);
// Получение с пагинацией
const result = await userManager.findPaginated({
pageSize,
pageBookmark: bookmark
});
return {
users: result.items,
bookmark: result.pageBookmark,
hasMore: !result.isAllLoaded
};
}
}
`
Базовый класс для работы с key-value хранилищем.
#### Методы
##### getKV(start: string, finish?: string): Promise
Получает все пары ключ-значение в диапазоне.
`typescript`
const kv = await databaseManager.getKV('user:', 'user:\ufff0');
##### getKeys(start: string, finish?: string): Promise
Получает только ключи в диапазоне.
`typescript`
const keys = await databaseManager.getKeys('user:');
##### getValues(start: string, finish?: string): Promise
Получает только значения в диапазоне.
`typescript`
const values = await databaseManager.getValues('user:');
##### getPaginatedKV(request: IPageBookmark, start: string, finish?: string): Promise
Получает пары ключ-значение с пагинацией.
`typescript`
const result = await databaseManager.getPaginatedKV(
{ pageSize: 10, pageBookmark: '' },
'user:'
);
Абстрактный класс для управления сущностями. Наследуется от DatabaseManager.
#### Методы CRUD
##### get(item: UID, details?: Array
Получает сущность по ID.
`typescript`
const user = await userManager.get('user123');
##### getMany(items: Array
Получает несколько сущностей по ID.
`typescript`
const users = await userManager.getMany(['user1', 'user2', 'user3']);
##### save(item: T): Promise
Сохраняет сущность (создание или обновление).
`typescript`
await userManager.save(user);
##### saveMany(items: Array
Сохраняет несколько сущностей.
`typescript`
await userManager.saveMany([user1, user2, user3]);
##### saveIfNotExists(item: T): Promise
Сохраняет сущность только если она еще не существует.
`typescript`
await userManager.saveIfNotExists(user);
##### has(item: UID): Promise
Проверяет существование сущности.
`typescript`
const exists = await userManager.has('user123');
##### remove(item: UID): Promise
Удаляет сущность.
`typescript`
await userManager.remove('user123');
##### removeMany(items: Array
Удаляет несколько сущностей.
`typescript`
await userManager.removeMany(['user1', 'user2']);
#### Методы поиска
##### find(details?: Array
Находит все сущности с заданным префиксом.
`typescript`
const allUsers = await userManager.find();
##### findPaginated(data: IPaginableBookmark
Находит сущности с пагинацией.
`typescript
const result = await userManager.findPaginated({
pageSize: 20,
pageBookmark: 'bookmark123'
});
console.log(result.items); // Array
console.log(result.pageBookmark); // Следующий bookmark
console.log(result.isAllLoaded); // Все ли загружено
`
#### Абстрактные методы
При наследовании от EntityManager необходимо реализовать:
`typescript
// Префикс для ключей в блокчейне
abstract get prefix(): string;
// Сериализация сущности для хранения
protected abstract serialize
// Десериализация сущности из хранилища
protected abstract deserialize(item: any, details?: Array
// Загрузка дополнительных данных
public abstract loadDetails(item: T, details?: Array
`
Реализация EntityManager с автоматической сериализацией и валидацией через class-validator и class-transformer.
При наследовании нужно реализовать только:
`typescript
// Преобразование plain object в класс
public abstract toEntity(item: any): T;
// Префикс для ключей
public abstract get prefix(): string;
// Опционально: загрузка связанных данных
public async loadDetails(item: T, details?: Array
// Реализация опциональна
}
`
Управление отношениями между сущностями.
`typescript
import { EntityRelation } from '@hlf-core/chaincode';
// Создание связи между постом и комментариями
const relation = new EntityRelation(
stub,
'post', // родительский префикс
'comment' // дочерний префикс
);
// Добавление связи
await relation.childLinkAdd('comment123', 'post456');
// Проверка связи
const hasLink = await relation.childLinkHas('comment123', 'post456');
// Удаление связи
await relation.childLinkRemove('comment123', 'post456');
// Удаление всех связей для дочернего элемента
await relation.childRemove('comment123');
// Удаление родителя и получение всех дочерних ID
const childIds = await relation.parentRemove('post456');
`
Расширение EntityRelation с автоматическим управлением дочерними сущностями.
`typescript
import { EntityRelationChild } from '@hlf-core/chaincode';
const commentRelation = new EntityRelationChild(
'post', // родительский префикс
'comment', // дочерний префикс
commentManager // менеджер для Comment
);
// Получение всех комментариев поста с пагинацией
const comments = await commentRelation.childList('post456', {
pageSize: 10
});
// Удаление поста и всех его комментариев
const deletedCommentIds = await commentRelation.parentRemove('post456');
`
``
┌─────────────────────────────────────────┐
│ IStub Interface │
│ (Абстракция над fabric-shim) │
└───────────────┬─────────────────────────┘
│
│ использует
▼
┌─────────────────────────────────────────┐
│ DatabaseManager │
│ • getKV / putKV │
│ • getKeys / getValues │
│ • getPaginatedKV │
└───────────────┬─────────────────────────┘
│
│ наследует
▼
┌─────────────────────────────────────────┐
│ EntityManager
│ • get / save / remove │
│ • find / findPaginated │
│ • serialize / deserialize (abstract) │
└───────────────┬─────────────────────────┘
│
│ наследует
▼
┌─────────────────────────────────────────┐
│ EntityManagerImpl
│ • Автоматическая сериализация │
│ • Автоматическая валидация │
│ • toEntity(item) (abstract) │
└─────────────────────────────────────────┘
│
│ используется в
▼
┌───────────────┐
│ Ваш Chaincode │
└───────────────┘
``
┌──────────────────────┐
│ EntityRelation │
│ • Управление связями│
│ • childAdd/Remove │
│ • parentRemove │
└──────────┬───────────┘
│
│ наследует
▼
┌──────────────────────────────┐
│ EntityRelationChild
│ • childList (с пагинацией) │
│ • Каскадное удаление │
└──────────────────────────────┘
`typescript
import { EntityManagerImpl } from '@hlf-core/chaincode';
import { IsString, IsNumber } from 'class-validator';
import { IUIDable, TransformUtil } from '@ts-core/common';
// Модель
class Product implements IUIDable {
@IsString()
uid: string;
@IsString()
name: string;
@IsNumber()
price: number;
@IsNumber()
quantity: number;
}
// Менеджер
class ProductManager extends EntityManagerImpl
public toEntity(item: any): Product {
return TransformUtil.toClass(Product, item);
}
public get prefix(): string {
return 'product:';
}
}
// Использование в chaincode
async function createProduct(ctx, id, name, price, quantity) {
const manager = new ProductManager(logger, stub);
const product = new Product();
product.uid = id;
product.name = name;
product.price = price;
product.quantity = quantity;
await manager.save(product);
}
async function updateProductQuantity(ctx, id, newQuantity) {
const manager = new ProductManager(logger, stub);
const product = await manager.get(id);
product.quantity = newQuantity;
await manager.save(product);
}
async function deleteProduct(ctx, id) {
const manager = new ProductManager(logger, stub);
await manager.remove(id);
}
`
`typescript
// Модели
class Post implements IUIDable {
@IsString()
uid: string;
@IsString()
title: string;
@IsString()
content: string;
}
class Comment implements IUIDable {
@IsString()
uid: string;
@IsString()
postId: string;
@IsString()
text: string;
@IsString()
authorId: string;
}
// Менеджеры
class PostManager extends EntityManagerImpl
public toEntity(item: any): Post {
return TransformUtil.toClass(Post, item);
}
public get prefix(): string { return 'post:'; }
}
class CommentManager extends EntityManagerImpl
public toEntity(item: any): Comment {
return TransformUtil.toClass(Comment, item);
}
public get prefix(): string { return 'comment:'; }
}
// Создание связи
const commentManager = new CommentManager(logger, stub);
const commentRelation = new EntityRelationChild(
'post',
'comment',
commentManager
);
// Добавление комментария к посту
async function addComment(ctx, commentId, postId, text, authorId) {
const comment = new Comment();
comment.uid = commentId;
comment.postId = postId;
comment.text = text;
comment.authorId = authorId;
await commentManager.save(comment);
await commentRelation.childLinkAdd(commentId, postId);
}
// Получение всех комментариев поста
async function getPostComments(ctx, postId, pageSize = 10, bookmark = '') {
const result = await commentRelation.childList(postId, {
pageSize,
pageBookmark: bookmark
});
return {
comments: result.items,
nextBookmark: result.pageBookmark,
hasMore: !result.isAllLoaded
};
}
// Удаление поста со всеми комментариями
async function deletePost(ctx, postId) {
const postManager = new PostManager(logger, stub);
// Удаляет пост и все связанные комментарии
await commentRelation.parentRemove(postId);
await postManager.remove(postId);
}
`
`typescript
class CustomEntityManager extends EntityManager
protected async serialize(item: MyEntity): Promise
// Кастомная логика сериализации
return {
id: item.uid,
data: encrypt(JSON.stringify(item)),
timestamp: Date.now()
};
}
protected async deserialize(item: any, details?: Array
if (!item) return null;
// Кастомная логика десериализации
const decrypted = decrypt(item.data);
const entity = JSON.parse(decrypted);
await this.loadDetails(entity, details);
return entity;
}
public async loadDetails(item: MyEntity, details?: Array
// Загрузка связанных данных
if (details?.includes('author')) {
item.author = await this.loadAuthor(item.authorId);
}
}
public get prefix(): string {
return 'custom:';
}
}
`
Интерфейс для работы с Hyperledger Fabric stub. Необходимо создать адаптер, реализующий этот интерфейс.
`typescript
interface IStub {
readonly user: IStubUser;
readonly requestId: string;
readonly transaction: IStubTransaction;
getState(key: string, type?: ClassType): Promise;
getStateRaw(key: string): Promise
getStateByRange(startKey: string, endKey: string): Promise
getStateByRangeWithPagination(startKey: string, endKey: string, pageSize: number, bookmark?: string): Promise
putState(key: string, value: U, options: IPutStateOptions): Promise;
putStateRaw(key: string, value: string): Promise
hasState(key: string): Promise
removeState(key: string): Promise
loadKV(iterator: StateQueryIterator): Promise
getPaginatedKV(request: IPageBookmark, start: string, finish: string): Promise
}
`
`typescript`
interface IKeyValue {
key: string;
value?: string;
}
- Node.js >= 14
- TypeScript >= 4.0
- Hyperledger Fabric >= 2.0
`bashУстановка зависимостей
npm install
$3
`
hlf-core-chaincode/
├── src/
│ ├── database/
│ │ ├── entity/
│ │ │ ├── EntityManager.ts # Абстрактный менеджер сущностей
│ │ │ ├── EntityManagerImpl.ts # Реализация с валидацией
│ │ │ ├── EntityRelation.ts # Управление связями
│ │ │ └── EntityRelationChild.ts # Связи с каскадным удалением
│ │ └── DatabaseManager.ts # Базовый менеджер БД
│ ├── stub/
│ │ ├── IStub.ts # Интерфейс stub
│ │ └── IStubHolder.ts # Holder для stub
│ └── public-api.ts # Публичный API
├── dist/ # Скомпилированный код
│ ├── cjs/ # CommonJS
│ └── esm/ # ES Modules
├── package.json
├── tsconfig.json
└── README.md
``ISC
Renat Gubaev
- Email: renat.gubaev@gmail.com
- GitHub: @ManhattanDoctor
- GitHub Repository
- NPM Package
- Issue Tracker
- Hyperledger Fabric Documentation
Если вы нашли баг или у вас есть предложение по улучшению, пожалуйста, создайте issue в GitHub Issues.
---
Примечание: Эта библиотека находится в активной разработке. API может меняться в минорных версиях.