Hyperledge Fabric: make Reactive and CQRS-ES
A powerful and light-weight library, to empower middle-tier application of Hyperledger,
to break through the barriers of Deeply Decentralized application.
Why this project is created? And what problem does it solve? And how?
See [Motivation] under development
- Simplicity. This is intentionally a library, instead of framework
- Model-free
- Deterministic
- Strong Decoupling
- Highly reactive
- Reactive Pattern (Reactivity)
- Command Query Responsibility Segregation (CQRS)
- Event Sourcing (ES)
- Redux-like architecture (Redux)
- End-to-end Type Coverage (Type-system)
Command-side is Fabric; query-side is in-memory database.
See [How we may achieve deep decentralization] under development
- Hyperledger Fabric V1.4; its prerequisite remains
- Install Fabric-samples
Env Variables | Values |
---|---|
CERT_PATH | e.g. ~/fabric-samples/basic-network/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/signcerts/User1@org1.example.com-cert.pem |
CHANNEL_HUB | peer name to be listened, e.g. peer0.org1.example.com |
CHANNEL_NAME | e.g. mychannel |
CONNECTION_PROFILE_PATH | path to connection profile e.g. ~/connection.yaml |
IDENTITY | registered user name in Fabric, e.g. User1@example.com |
KEY_PATH | e.g. fabric-samples/basic-network/crypto-config/peerOrganizations/org1.example.com/users/User1@org1.example.com/msp/keystore/ |
MSPID | e.g. Org1MSP |
PRIVATE_KEY | filename of private key under KEY_PATH |
WALLET | Directory name of local wallet |
WALLET_ROOT | absolute path of wallet |
Note that all paths are absolute paths. And, all configurations are required.
Step 1: Install Fabric, and fabric-samples
Step 2: Bootstrap the basic-network
Remember to run fabcar example, to validate the installation.
Step 3: get boilerplated application & set env variables
More examples will added, in Examples.
See Hyperledger documentation from step 1 to 2.
see Examples, from step 3.
Step 4: make default reducer for each peer application
Based on counter example:
// counter-reducer.ts
import { BaseEvent, Reducer } from 'fabric-rx-cqrs';
export interface CounterEvent extends BaseEvent {
type: 'ADD' | 'MINUS';
}
export interface Counter {
id: string;
value: number;
}
export const counterReducer: Reducer<Counter> = (
history: CounterEvent[],
initial = { value: 0 }
): Counter => history.reduce(reducer, initial);
const reducer = ({ value }, e: CounterEvent) => {
switch (e.type) {
case 'ADD':
value++;
return { value };
case 'MINUS':
value--;
return { value };
default:
return { value };
}
};
Each peer application requires at least one reducer function, to compute the current state, from history of events.
type Reducer<T> = (history: { type: string; payload?: any }[], initial?: T) => T;
Similarly, one, or more events are required. Payload will be used to compute the currents state.
type BaseEvent = { type: string, payload: any}
Each entity must be given an unique identitier id
, as entity Id.
Step 5: set the defaultReducer
// counter.spec.ts
import { Entity, setDefaultReducer, getRepository, channelEvent, container } from 'fabric-rx-cqrs';
import { Counter, CounterEvent, counterReducer } from './counter-reducer';
setDefaultReducer(container, 'counter', counterReducer);
Each peer application have only a single default reducer.
The getProjection
function depends on default reducer.
setDefaultReducer(container: Container, entityName:string, reducer: Reducer)
Step 6: listen to channel event hub
// counter.spec.ts
await channelEvent.invoke();
This invokes the channel event hub. Whenever new commits, it publishes to in-memory query-side database.
Step 7: get repository
// counter.spec.ts
const entityName = 'counter';
const id = 'counter_test' + Math.floor(Math.random() * 1000);
const counterRepository = getRepository<Counter, CounterEvent>(
'counter',
counterReducer
);
getRepository is the most commonly used function, which returns typed Repository; K being the type argument for typed BaseEvent.
getRepository<T, K>(entityName: string, reducer: () => void): Repository
Repository is function factory, which returns:
type Repository<T, K> = {
create: Function; // create entity
getById: Function; // return current state and save(), by entity id
getByEntityName: Function; // return array of (currentState) entities by, entity name
getCommitById: Function; // return commits by entity id
getProjection: Function; // return array of entities, by projection criteria
}
Note that argument reducer
of getRepository
can be the same or different reducer from defaultReducer
;
referred as non-default reducer. getById
, getByEntityName
are computed
based on non-default reducer. getProjection
is based on default reducer.
Step 8: write event to Fabric repository
// counter.spec.ts
await counterRepository.create(id).save([{ type: 'ADD' }])
This writes to Fabric, returning type Entity; is a commit object. If write failure,
it return null
commit, and throw error.
type Repository<T, K> = {
create: (id: string) => { save: (events: K[]) => Promise<Entity> }
}
Every successful write will return commit object.
type Entity = {
id: string;
commitId: string;
version: number;
events;
entityId: string;
committeAt: string;
}
Step 9: get current state by projection
// counter.spec.ts
counterRepository
.getProjection({ where: { id } })
.then(({ projections }) => {
expect(projections).to.deep.equal([{ id, value: 2 }]);
});
getProjection
is similar to a search function, but implemented with selector.
The computation is based on defaultReducer. Currently,
there are three operator: where
, all
, contain
.
type Repository<T, K> = {
getProjection: ({ where, all, contain }:
{ where?: Partial<T>; all?: boolean; contain?: string }
) => Promise<{ projections: T[] }>;
}
Reconcile
Lastly, reconcile()
performs reconcilation from write-side Fabric, to in-memory database;
is the key bootstraping procedure for each peer application.
const reconcile = async (entityName: string, reducer: () => void) => Promise<any>
In each peer application, it can run multiple reconcile, for different entityName
. e.g.
reconcile('counterA', counterReducer);
reconcile('counterB', counterReducer);
It provides getQueryResolver
, getSubscriptionResolver
, which return high-order resolvers for GraphQL server.
This is very convenient feature to generate CRUD resolvers, via decorated domain model.
Thanks to Type-GraphQL.
It will generate executable resolver, for below example query.
getCounter(id: "counterId") {
id
value
}
getAllCounter {
id
value
}
getCommits(id: "counterId") {
id
commitId
version
events
entityId
committeAt
}
It leverages pubSub()
, as default publish-subscription engine, for GraphQL subscription.
GraphQL is the preferred API implementation; beyond the scope of this library.
GraphQL test-net is under development.
- GraphQL
- Hyperledger Fabric
- Inversify
- Redux
- Rxjs
- Typescript
See Basic Examples