How to generate automatically a NodeJS API backend for Hyperledger Fabric applications built with Convector

Luca Tamburrano
Mar 1 · 21 min read
git clone https://github.com/xcottos/convector-example-supplychain-master

Dependencies

The first thing we need to do is to change the lerna.json of the project in order to exclude from the hoisting the @types/bytebuffer. This will prevent exceptions in the future compilation of the API application. The lerna.json should look like:

{
"packages": [
"packages/*"
],
"version": "0.1.0",
"command": {
"bootstrap": {
"hoist": true,
"nohoist":[
"@types/bytebuffer"
]
}
}
}
{
"name": "supplychainchaincode-cc",
"version": "0.1.0",
"description": "Chaincodes package for testnewchaincode",
"main": "./dist/src/index.js",
"typings": "./dist/src/index.d.ts",
"files": [
"dist/*"
],
"scripts": {
"clean": "rimraf dist client",
"build": "npm run clean && tsc",
"prepare": "npm run build",
"test": "npm run build && mocha -r ts-node/register tests/*.spec.ts --reporter spec"
},
"dependencies": {
"yup": "^0.26.6",
"reflect-metadata": "^0.1.12",
"@worldsibu/convector-core": "~1.3.0",
"@worldsibu/convector-platform-fabric": "~1.3.0",
"@worldsibu/convector-rest-api-decorators": "1.0.5"
},
"devDependencies": {
"@types/node": "^10.12.5",
"@worldsibu/convector-storage-couchdb": "~1.3.0",
"rimraf": "^2.6.2",
"ts-node": "^8.0.2",
"mocha": "^5.0.3",
"chai": "^4.1.2",
"@types/mocha": "^5.2.5",
"@types/chai": "^4.1.4"
}
}
npm i
  • API configurations

Infrastructure configuration

For the infrastructure configuration you need to create a file in the root of the project (in our scenario is convector-example-supplychain-master) called api.json that contains the infrastructure parameters.

{
"selected":"dev",
"environments": [
{
"name":"dev",
"PORT":"3000",
"LOG_LEVEL":"debug",
"REQUEST_LIMIT":"100kb",
"SESSION_SECRET":"mySecret",
"SWAGGER_API_SPEC":"/spec",
"KEYSTORE":"../../../fabric-hurl/.hfc-org1",
"USERCERT":"admin",
"ORGCERT":"org1",
"NETWORKPROFILE":"../../../fabric-hurl/network-profiles/org1.network-profile.yaml",
"CHANNEL":"ch1",
"CHAINCODE":"supplychainchaincode",
"COUCHDBVIEW":"ch1_supplychainchaincode",
"COUCHDB_PORT":"5984",
"COUCHDB_HOST":"localhost",
"COUCHDB_PROTOCOL":"http"
},
{
"name":"prod",
"PORT":"3000",
"LOG_LEVEL":"error",
"REQUEST_LIMIT":"100kb",
"SESSION_SECRET":"mySecret",
"SWAGGER_API_SPEC":"/spec",
"KEYSTORE":"../../../fabric-hurl/.hfc-org1",
"USERCERT":"admin",
"ORGCERT":"org1",
"NETWORKPROFILE":"../../../fabric-hurl/network-profiles/org1.network-profile.yaml",
"CHANNEL":"ch1",
"CHAINCODE":"supplychainchaincode",
"COUCHDBVIEW":"ch1_supplychainchaincode",
"COUCHDB_PORT":"5984",
"COUCHDB_HOST":"localhost",
"COUCHDB_PROTOCOL":"http"
}
]
}
  • LOG_LEVEL: the log level of the app. The app uses pino for the logging
  • REQUEST_LIMIT: the limit in kb of the request that can reach the API when invoked.
  • SESSION_SECRET: used to parse and match session cookies
  • SWAGGER_API_SPEC: apiPath property of the Swagger. Location of the swagger docs
  • KEYSTORE: The folder that contains the hurl (Hyperledger Fabric) keystore.
  • USERCERT: the name of the hurl (Hyperledger Fabric) identity that will perform the API calls
  • ORGCERT: The organization of the USERCERT identity
  • NETWORKPROFILE: Location of the yaml file that contains the hurl (Hyperledger Fabric) network definition
  • CHANNEL: the channel of the peer the chaincode invoked is installed in
  • CHAINCODE: chaincode name
  • COUCHDBVIEW: name of the couchdb view
  • COUCHDB_PORT: the port where couhdb is in listening
  • COUCHDB_HOST: the host where couchdb is installed
  • COUCHDB_PROTOCOL: the protocol used by couchdb

API configuration

On the other side, the API configuration is achieved by annotating the methods in the chaincode controller (usually located in packages/<chaincode-name>-cc/src folder) with the following possibilities:

@Create('Supplier')
@Invokable()
public async createSupplier(
@Param(Supplier)
supplier: Supplier
) {
await supplier.save();
}
@GetById('Supplier')
@Invokable()
public async getSupplierById(
@Param(yup.string())
supplierId: string
)
{
const supplier = await Supplier.getOne(supplierId);
return supplier;
}
@GetAll('Supplier')
@Invokable()
public async getAllSuppliers()
{
const storedSuppliers = await Supplier.getAll('io.worldsibu.Supplier');
return storedSuppliers;
}
@Service()
@Invokable()
public async fetchRawMaterial(
@Param(yup.string())
supplierId: string,
@Param(yup.number())
rawMaterialSupply: number
) {
const supplier = await Supplier.getOne(supplierId);
supplier.rawMaterialAvailable = supplier.rawMaterialAvailable + rawMaterialSupply;
await supplier.save();
}
import * as yup from 'yup';
import {
Controller,
ConvectorController,
Invokable,
Param
} from '@worldsibu/convector-core-controller';
import { Supplier } from './Supplier.model';
import { Manufacturer } from './Manufacturer.model';
import { Distributor } from './Distributor.model';
import { Retailer } from './Retailer.model';
import { Customer } from './Customer.model';
import { GetById, GetAll, Create, Service } from '@worldsibu/convector-rest-api-decorators';@Controller('supplychainchaincode')
export class SupplychainchaincodeController extends ConvectorController {
@Create('Supplier')
@Invokable()
public async createSupplier(
@Param(Supplier)
supplier: Supplier
) {
await supplier.save();
}
@Create('Manufacturer')
@Invokable()
public async createManufacturer(
@Param(Manufacturer)
manufacturer: Manufacturer
) {
await manufacturer.save();
}
@Create('Distributor')
@Invokable()
public async createDistributor(
@Param(Distributor)
distributor: Distributor
) {
await distributor.save();
}
@Create('Retailer')
@Invokable()
public async createRetailer(
@Param(Retailer)
retailer: Retailer
) {
await retailer.save();
}
@Create('Customer')
@Invokable()
public async createCustomer(
@Param(Customer)
customer: Customer
) {
await customer.save();
}
@GetAll('Supplier')
@Invokable()
public async getAllSuppliers()
{
const storedSuppliers = await Supplier.getAll<Supplier>();
return storedSuppliers;
}
@GetById('Supplier')
@Invokable()
public async getSupplierById(
@Param(yup.string())
supplierId: string
)
{
const supplier = await Supplier.getOne(supplierId);
return supplier;
}
@GetAll('Manufacturer')
@Invokable()
public async getAllManufacturers()
{
const storedManufacturers = await Manufacturer.getAll<Manufacturer>();
return storedManufacturers;
}
@GetById('Manufacturer')
@Invokable()
public async getManufacturerById(
@Param(yup.string())
manufacturerId: string
)
{
const manufacturer = await Manufacturer.getOne(manufacturerId);
return manufacturer;
}
@GetAll('Distributor')
@Invokable()
public async getAllDistributors()
{
const storedDistributors = await Distributor.getAll<Distributor>();
return storedDistributors
}
@GetById('Distributor')
@Invokable()
public async getDistributorById(
@Param(yup.string())
distributorId: string
)
{
const distributor = await Distributor.getOne(distributorId);
return distributor;
}
@GetAll('Retailer')
@Invokable()
public async getAllRetailers()
{
const storedRetailers = await Retailer.getAll<Retailer>();
return storedRetailers;
}
@GetById('Retailer')
@Invokable()
public async getRetailerById(
@Param(yup.string())
retailerId: string
)
{
const retailer = await Retailer.getOne(retailerId);
return retailer;
}
@GetAll('Customer')
@Invokable()
public async getAllCustomers()
{
const storedCustomers = await Customer.getAll<Customer>();
return storedCustomers;
}
@GetById('Customer')
@Invokable()
public async getCustomerById(
@Param(yup.string())
customerId: string
)
{
const customer = await Customer.getOne(customerId);
return customer;
}
@Invokable()
public async getAllModels()
{
const storedCustomers = await Customer.getAll<Customer>();
console.log(storedCustomers);
const storedRetailers = await Retailer.getAll<Retailer>();
console.log(storedRetailers);
const storedDistributors = await Distributor.getAll<Distributor>();
console.log(storedDistributors);
const storedManufacturers = await Manufacturer.getAll<Manufacturer>();
console.log(storedManufacturers);
const storedSuppliers = await Supplier.getAll<Supplier>();
console.log(storedSuppliers);
}
@Service()
@Invokable()
public async fetchRawMaterial(
@Param(yup.string())
supplierId: string,
@Param(yup.number())
rawMaterialSupply: number
) {
const supplier = await Supplier.getOne(supplierId);
supplier.rawMaterialAvailable = supplier.rawMaterialAvailable + rawMaterialSupply;
await supplier.save();
}
@Service()
@Invokable()
public async getRawMaterialFromSupplier(
@Param(yup.string())
manufacturerId: string,
@Param(yup.string())
supplierId: string,
@Param(yup.number())
rawMaterialSupply: number
) {
const supplier = await Supplier.getOne(supplierId);
supplier.rawMaterialAvailable = supplier.rawMaterialAvailable - rawMaterialSupply;
const manufacturer = await Manufacturer.getOne(manufacturerId);
manufacturer.rawMaterialAvailable = rawMaterialSupply + manufacturer.rawMaterialAvailable;
await supplier.save();
await manufacturer.save();
}
@Service()
@Invokable()
public async createProducts(
@Param(yup.string())
manufacturerId: string,
@Param(yup.number())
rawMaterialConsumed: number,
@Param(yup.number())
productsCreated: number
) {
const manufacturer = await Manufacturer.getOne(manufacturerId);
manufacturer.rawMaterialAvailable = manufacturer.rawMaterialAvailable - rawMaterialConsumed;
manufacturer.productsAvailable = manufacturer.productsAvailable + productsCreated;
await manufacturer.save();
}
@Service()
@Invokable()
public async sendProductsToDistribution(
@Param(yup.string())
manufacturerId: string,
@Param(yup.string())
distributorId: string,
@Param(yup.number())
sentProducts: number
) {
const distributor = await Distributor.getOne(distributorId);
distributor.productsToBeShipped = distributor.productsToBeShipped + sentProducts;
const manufacturer = await Manufacturer.getOne(manufacturerId);
manufacturer.productsAvailable = manufacturer.productsAvailable - sentProducts;
await distributor.save();
await manufacturer.save();
}
@Service()
@Invokable()
public async orderProductsFromDistributor(
@Param(yup.string())
retailerId: string,
@Param(yup.string())
distributorId: string,
@Param(yup.number())
orderedProducts: number
) {
const retailer = await Retailer.getOne(retailerId);
retailer.productsOrdered = retailer.productsOrdered + orderedProducts;
const distributor = await Distributor.getOne(distributorId);
distributor.productsToBeShipped = distributor.productsToBeShipped - orderedProducts;
distributor.productsShipped = distributor.productsShipped + orderedProducts;
await retailer.save();
await distributor.save();
}
@Service()
@Invokable()
public async receiveProductsFromDistributor(
@Param(yup.string())
retailerId: string,
@Param(yup.string())
distributorId: string,
@Param(yup.number())
receivedProducts: number
) {
const retailer = await Retailer.getOne(retailerId);
retailer.productsAvailable = retailer.productsAvailable + receivedProducts;
const distributor = await Distributor.getOne(distributorId);
distributor.productsReceived = distributor.productsReceived + receivedProducts;
await retailer.save();
await distributor.save();
}
@Service()
@Invokable()
public async buyProductsFromRetailer(
@Param(yup.string())
retailerId: string,
@Param(yup.string())
customerId: string,
@Param(yup.number())
boughtProducts: number
) {
const retailer = await Retailer.getOne(retailerId);
retailer.productsAvailable = retailer.productsAvailable - boughtProducts;
retailer.productsSold = retailer.productsSold + boughtProducts;
const customer = await Customer.getOne(customerId);
customer.productsBought = customer.productsBought + boughtProducts;
await retailer.save();
await customer.save();
}
}

API generation

Once defined the infrastructure and annotated the controller methods, we need to install the yeoman (https://yeoman.io) generator that will be used for creating the skeleton of our backend:

npm install -g generator-express-no-stress-typescript
npm install -g @worldsibu/convector-rest-api
conv-rest-api generate api -c <chaincode name> -p <project name> -f <chaincode config file>
conv-rest-api generate api -c supplychainchaincode -p supplychain -f ./org1.supplychainchaincode.config.json
  • It invokes the yeoman generator for generating in the packages folder the stub of the API application in a folder called <chaoncode name>-app. In our supply chain scenario, the folder will be called supplychainchaincode-app
  • It installs some external dependencies
  • It installs the chaincode as a dependency of the project
  • It copies a tsconfig.ts file bundled in the convector-rest-api package in the root folder of the API project (packages/supplychainchaincode-app). This file is identical to the file that the generator-express-no-stress-typescript yeoman generator generates, with the difference that we added the experimental decorators:
{
"compileOnSave": false,
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"esModuleInterop": true,
"sourceMap": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"outDir": "dist",
"typeRoots": ["node_modules/@types"]
},
"include": ["typings.d.ts", "server/**/*.ts"],
"exclude": ["node_modules"]
}
KEYSTORE=../../../fabric-hurl/.hfc-org1
USERCERT=admin
ORGCERT=org1
/** Referenced from: https://github.com/ksachdeva/hyperledger-fabric-example/blob/c41fcaa352e78cbf3c7cfb210338ac0f20b8357e/src/client.ts */
import * as fs from 'fs';
import { join } from 'path';
import Client from 'fabric-client';

import { IEnrollmentRequest, IRegisterRequest } from 'fabric-ca-client';

export type UserParams = IRegisterRequest;
export type AdminParams = IEnrollmentRequest;

export namespace SelfGenContext {

interface IdentityFiles {
privateKey: string;
signedCert: string;
}

export async function getClient() {
// Check if needed
let contextPath = '';
if (process.env.KEYSTORE[0] == '/') {
contextPath = join(process.env.KEYSTORE + '/' + process.env.USERCERT);
}
else {
contextPath = join(__dirname, process.env.KEYSTORE + '/' + process.env.USERCERT);
}

fs.readFile(contextPath, 'utf8', async function (err, data) {
if (err) {
// doesnt exist! Create it.
const client = new Client();

console.log('Setting up the cryptoSuite ..');

// ## Setup the cryptosuite (we are using the built in default s/w based implementation)
const cryptoSuite = Client.newCryptoSuite();
cryptoSuite.setCryptoKeyStore(Client.newCryptoKeyStore({
path: process.env.KEYSTORE
}));

client.setCryptoSuite(cryptoSuite);

console.log('Setting up the keyvalue store ..');

// ## Setup the default keyvalue store where the state will be stored
const store = await Client.newDefaultKeyValueStore({
path: process.env.KEYSTORE
});

client.setStateStore(store);

console.log('Creating the admin user context ..');

const privateKeyFile = fs.readdirSync(process.env.KEYSTORE + '/keystore')[0];

// ### GET THE NECESSRY KEY MATERIAL FOR THE ADMIN OF THE SPECIFIED ORG ##
const cryptoContentOrgAdmin: IdentityFiles = {
privateKey: process.env.KEYSTORE + '/keystore/' + privateKeyFile,
signedCert: process.env.KEYSTORE + '/signcerts/cert.pem'
};

await client.createUser({
username: process.env.USERCERT,
mspid: `${process.env.ORGCERT}MSP`,
cryptoContent: cryptoContentOrgAdmin,
skipPersistence: false
});

return client;
} else {
console.log('Context exists');
}
});

}

}
import { resolve } from "path";
import { ClientFactory } from "@worldsibu/convector-core-adapter";
import { SelfGenContext } from "./selfgenfabriccontext";
import { SupplychainchaincodeController } from "supplychainchaincode-cc/dist/src";
import { FabricControllerAdapter } from '@worldsibu/convector-adapter-fabric';

export namespace SupplychainchaincodeControllerClient {
export async function init(): Promise<SupplychainchaincodeController> {
const user = process.env.USERCERT || 'user1';
await SelfGenContext.getClient();
// Inject a Adapter of type *Fabric Controller*
// Setup accordingly to the
const adapter = new FabricControllerAdapter({
txTimeout: 300000,
user: user,
channel: process.env.CHANNEL,
chaincode: process.env.CHAINCODE,
keyStore: resolve(__dirname, process.env.KEYSTORE),
networkProfile: resolve(__dirname, process.env.NETWORKPROFILE),
userMspPath: resolve(__dirname, process.env.KEYSTORE),
});
await adapter.init();
// Return your own implementation of the controller

return ClientFactory(SupplychainchaincodeController, adapter);
}
}
import { BaseStorage } from '@worldsibu/convector-core-storage';
import { CouchDBStorage } from '@worldsibu/convector-storage-couchdb';
import { Customer as CustomerModel } from 'supplychainchaincode-cc/dist/src';
import { Distributor as DistributorModel } from 'supplychainchaincode-cc/dist/src';
import { Manufacturer as ManufacturerModel } from 'supplychainchaincode-cc/dist/src';
import { Retailer as RetailerModel } from 'supplychainchaincode-cc/dist/src';
import { Supplier as SupplierModel } from 'supplychainchaincode-cc/dist/src';
export namespace Models {
export const Customer = CustomerModel;
export const Distributor = DistributorModel;
export const Manufacturer = ManufacturerModel;
export const Retailer = RetailerModel;
export const Supplier = SupplierModel;
}
import { Request, Response } from 'express';
import { SupplychainchaincodeControllerClient } from '../../../smartContractControllers';
import { Models } from '../../../smartContractModels';
export class Controller {async supplychainchaincode_getAllSuppliers(req: Request, res: Response): Promise<void> {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getAllSuppliers();
res.json(result);
}
async supplychainchaincode_getAllManufacturers(req: Request, res: Response): Promise<void> {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getAllManufacturers();
res.json(result);
}
async supplychainchaincode_getAllDistributors(req: Request, res: Response): Promise<void> {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getAllDistributors();
res.json(result);
}
async supplychainchaincode_getAllRetailers(req: Request, res: Response): Promise<void> {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getAllRetailers();
res.json(result);
}
async supplychainchaincode_getAllCustomers(req: Request, res: Response): Promise<void> {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getAllCustomers();
res.json(result);
}
async supplychainchaincode_getSupplierById(req: Request, res: Response) {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getSupplierById(req.params.id);
if (!result) {
return res.status(404);
}
res.json(result);
}
async supplychainchaincode_getManufacturerById(req: Request, res: Response) {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getManufacturerById(req.params.id);
if (!result) {
return res.status(404);
}
res.json(result);
}
async supplychainchaincode_getDistributorById(req: Request, res: Response) {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getDistributorById(req.params.id);
if (!result) {
return res.status(404);
}
res.json(result);
}
async supplychainchaincode_getRetailerById(req: Request, res: Response) {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getRetailerById(req.params.id);
if (!result) {
return res.status(404);
}
res.json(result);
}
async supplychainchaincode_getCustomerById(req: Request, res: Response) {
let cntrl = await SupplychainchaincodeControllerClient.init();
let result = await cntrl.getCustomerById(req.params.id);
if (!result) {
return res.status(404);
}
res.json(result);
}
async supplychainchaincode_createSupplier(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let modelRaw = req.body;
let model = new Models.Supplier(modelRaw);
await cntrl.createSupplier(model);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_createManufacturer(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let modelRaw = req.body;
let model = new Models.Manufacturer(modelRaw);
await cntrl.createManufacturer(model);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_createDistributor(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let modelRaw = req.body;
let model = new Models.Distributor(modelRaw);
await cntrl.createDistributor(model);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_createRetailer(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let modelRaw = req.body;
let model = new Models.Retailer(modelRaw);
await cntrl.createRetailer(model);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_createCustomer(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let modelRaw = req.body;
let model = new Models.Customer(modelRaw);
await cntrl.createCustomer(model);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_fetchRawMaterial(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let params = req.body;

await cntrl.fetchRawMaterial(params.supplierId,params.rawMaterialSupply);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_getRawMaterialFromSupplier(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let params = req.body;

await cntrl.getRawMaterialFromSupplier(params.manufacturerId,params.supplierId,params.rawMaterialSupply);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_createProducts(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let params = req.body;

await cntrl.createProducts(params.manufacturerId,params.rawMaterialConsumed,params.productsCreated);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_sendProductsToDistribution(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let params = req.body;

await cntrl.sendProductsToDistribution(params.manufacturerId,params.distributorId,params.sentProducts);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_orderProductsFromDistributor(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let params = req.body;

await cntrl.orderProductsFromDistributor(params.retailerId,params.distributorId,params.orderedProducts);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_receiveProductsFromDistributor(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let params = req.body;

await cntrl.receiveProductsFromDistributor(params.retailerId,params.distributorId,params.receivedProducts);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
async supplychainchaincode_buyProductsFromRetailer(req: Request, res: Response) {
try {
let cntrl = await SupplychainchaincodeControllerClient.init();
let params = req.body;

await cntrl.buyProductsFromRetailer(params.retailerId,params.customerId,params.boughtProducts);
res.sendStatus(201);
} catch (ex) {
console.log(ex.message, ex.stack);
res.status(500).send(ex);
}
}
}
export default new Controller();
import { Application } from 'express';
import supplychainRouter from './api/controllers/examples/router'
export default function routes(app: Application): void {
app.use('/api/v1/supplychain', supplychainRouter);
};
  • @GetAll methods: will be mapped to GET methods where, as the previous one, the endpoint will be the name of the model class of all the instances that will be retrieved with the first letter lowercase and with an ‘s’ at the end; for example .get('/suppliers/', controller.getAllSuppliers)
  • @GetById methods: will be mapped to GET methods where, as the previous ones, the endpoint will be the name of the model class with the first letter lowercase and with an ‘s’ at the end and the parameter will be the id of the model to be retrieved; for example .get('/suppliers/:id', controller.getSupplierById)
  • @Service methods: will be mapped to POST methods whereas a convention the endpoint will be the name of the methods and all the parameters will be passed inside an object; For example: .post('/fetchRawMaterial', controller.fetchRawMaterial) And a sample object to be passed will be:
    { "supplierId": "SPL_1", "rawMaterialSupply": 12345555 }
import express from 'express';
import controller from './controller'
export default express.Router()

.post('/suppliers/', controller.supplychainchaincode_createSupplier)
.post('/manufacturers/', controller.supplychainchaincode_createManufacturer)
.post('/distributors/', controller.supplychainchaincode_createDistributor)
.post('/retailers/', controller.supplychainchaincode_createRetailer)
.post('/customers/', controller.supplychainchaincode_createCustomer)
.get('/suppliers/', controller.supplychainchaincode_getAllSuppliers)
.get('/manufacturers/', controller.supplychainchaincode_getAllManufacturers)
.get('/distributors/', controller.supplychainchaincode_getAllDistributors)
.get('/retailers/', controller.supplychainchaincode_getAllRetailers)
.get('/customers/', controller.supplychainchaincode_getAllCustomers)
.get('/suppliers/:id', controller.supplychainchaincode_getSupplierById)
.get('/manufacturers/:id', controller.supplychainchaincode_getManufacturerById)
.get('/distributors/:id', controller.supplychainchaincode_getDistributorById)
.get('/retailers/:id', controller.supplychainchaincode_getRetailerById)
.get('/customers/:id', controller.supplychainchaincode_getCustomerById)
.post('/fetchRawMaterial', controller.supplychainchaincode_fetchRawMaterial)
.post('/getRawMaterialFromSupplier', controller.supplychainchaincode_getRawMaterialFromSupplier)
.post('/createProducts', controller.supplychainchaincode_createProducts)
.post('/sendProductsToDistribution', controller.supplychainchaincode_sendProductsToDistribution)
.post('/orderProductsFromDistributor', controller.supplychainchaincode_orderProductsFromDistributor)
.post('/receiveProductsFromDistributor', controller.supplychainchaincode_receiveProductsFromDistributor)
.post('/buyProductsFromRetailer', controller.supplychainchaincode_buyProductsFromRetailer)

;
swagger: "2.0"
info:
version: 1.0.0
title: supplychain
description: supplychain REST API Application
basePath: /api/v1/supplychain

tags:

- name: Customers
description: Simple customer endpoints

- name: Distributors
description: Simple distributor endpoints

- name: Manufacturers
description: Simple manufacturer endpoints

- name: Retailers
description: Simple retailer endpoints

- name: Suppliers
description: Simple supplier endpoints


consumes:
- application/json
produces:
- application/json

definitions:

CustomerBody:
type: object
title: Customer
required:
- id
- name
- productsBought
properties:
id:
type: string
example: a_text
name:
type: string
example: a_text
productsBought:
type: number
example: 123
DistributorBody:
type: object
title: Distributor
required:
- id
- name
- productsToBeShipped
- productsShipped
- productsReceived
properties:
id:
type: string
example: a_text
name:
type: string
example: a_text
productsToBeShipped:
type: number
example: 123
productsShipped:
type: number
example: 123
productsReceived:
type: number
example: 123
ManufacturerBody:
type: object
title: Manufacturer
required:
- id
- name
- productsAvailable
- rawMaterialAvailable
properties:
id:
type: string
example: a_text
name:
type: string
example: a_text
productsAvailable:
type: number
example: 123
rawMaterialAvailable:
type: number
example: 123
RetailerBody:
type: object
title: Retailer
required:
- id
- name
- productsOrdered
- productsAvailable
- productsSold
properties:
id:
type: string
example: a_text
name:
type: string
example: a_text
productsOrdered:
type: number
example: 123
productsAvailable:
type: number
example: 123
productsSold:
type: number
example: 123
SupplierBody:
type: object
title: Supplier
required:
- id
- name
- rawMaterialAvailable
properties:
id:
type: string
example: a_text
name:
type: string
example: a_text
rawMaterialAvailable:
type: number
example: 123
fetchRawMaterialBody:
type: object
title: fetchRawMaterialParams
required:
- supplierId
- rawMaterialSupply
properties:
supplierId:
type: string
example: a_text
rawMaterialSupply:
type: number
example: 123
getRawMaterialFromSupplierBody:
type: object
title: getRawMaterialFromSupplierParams
required:
- manufacturerId
- supplierId
- rawMaterialSupply
properties:
manufacturerId:
type: string
example: a_text
supplierId:
type: string
example: a_text
rawMaterialSupply:
type: number
example: 123
createProductsBody:
type: object
title: createProductsParams
required:
- manufacturerId
- rawMaterialConsumed
- productsCreated
properties:
manufacturerId:
type: string
example: a_text
rawMaterialConsumed:
type: number
example: 123
productsCreated:
type: number
example: 123
sendProductsToDistributionBody:
type: object
title: sendProductsToDistributionParams
required:
- manufacturerId
- distributorId
- sentProducts
properties:
manufacturerId:
type: string
example: a_text
distributorId:
type: string
example: a_text
sentProducts:
type: number
example: 123
orderProductsFromDistributorBody:
type: object
title: orderProductsFromDistributorParams
required:
- retailerId
- distributorId
- orderedProducts
properties:
retailerId:
type: string
example: a_text
distributorId:
type: string
example: a_text
orderedProducts:
type: number
example: 123
receiveProductsFromDistributorBody:
type: object
title: receiveProductsFromDistributorParams
required:
- retailerId
- distributorId
- receivedProducts
properties:
retailerId:
type: string
example: a_text
distributorId:
type: string
example: a_text
receivedProducts:
type: number
example: 123
buyProductsFromRetailerBody:
type: object
title: buyProductsFromRetailerParams
required:
- retailerId
- customerId
- boughtProducts
properties:
retailerId:
type: string
example: a_text
customerId:
type: string
example: a_text
boughtProducts:
type: number
example: 123

paths:

/customers:
get:
tags:
- Customers
description: Fetch all customers
responses:
200:
description: Returns all customers
post:
tags:
- Customers
description: Create a new customer
parameters:
- name: customer
in: body
description: a customer
required: true
schema:
$ref: "#/definitions/CustomerBody"
responses:
201:
description: Successful insertion of customers

/customers/{id}:
get:
tags:
- Customers
parameters:
- name: id
in: path
required: true
description: The id of the customer to retrieve
type: string
responses:
200:
description: Return the customer with the specified id
404:
description: Customer not found
/distributors:
get:
tags:
- Distributors
description: Fetch all distributors
responses:
200:
description: Returns all distributors
post:
tags:
- Distributors
description: Create a new distributor
parameters:
- name: distributor
in: body
description: a distributor
required: true
schema:
$ref: "#/definitions/DistributorBody"
responses:
201:
description: Successful insertion of distributors

/distributors/{id}:
get:
tags:
- Distributors
parameters:
- name: id
in: path
required: true
description: The id of the distributor to retrieve
type: string
responses:
200:
description: Return the distributor with the specified id
404:
description: Distributor not found
/manufacturers:
get:
tags:
- Manufacturers
description: Fetch all manufacturers
responses:
200:
description: Returns all manufacturers
post:
tags:
- Manufacturers
description: Create a new manufacturer
parameters:
- name: manufacturer
in: body
description: a manufacturer
required: true
schema:
$ref: "#/definitions/ManufacturerBody"
responses:
201:
description: Successful insertion of manufacturers

/manufacturers/{id}:
get:
tags:
- Manufacturers
parameters:
- name: id
in: path
required: true
description: The id of the manufacturer to retrieve
type: string
responses:
200:
description: Return the manufacturer with the specified id
404:
description: Manufacturer not found
/retailers:
get:
tags:
- Retailers
description: Fetch all retailers
responses:
200:
description: Returns all retailers
post:
tags:
- Retailers
description: Create a new retailer
parameters:
- name: retailer
in: body
description: a retailer
required: true
schema:
$ref: "#/definitions/RetailerBody"
responses:
201:
description: Successful insertion of retailers

/retailers/{id}:
get:
tags:
- Retailers
parameters:
- name: id
in: path
required: true
description: The id of the retailer to retrieve
type: string
responses:
200:
description: Return the retailer with the specified id
404:
description: Retailer not found
/suppliers:
get:
tags:
- Suppliers
description: Fetch all suppliers
responses:
200:
description: Returns all suppliers
post:
tags:
- Suppliers
description: Create a new supplier
parameters:
- name: supplier
in: body
description: a supplier
required: true
schema:
$ref: "#/definitions/SupplierBody"
responses:
201:
description: Successful insertion of suppliers

/suppliers/{id}:
get:
tags:
- Suppliers
parameters:
- name: id
in: path
required: true
description: The id of the supplier to retrieve
type: string
responses:
200:
description: Return the supplier with the specified id
404:
description: Supplier not found

/fetchRawMaterial:
post:
tags:
- fetchRawMaterial
description: fetchRawMaterial
parameters:
- name: fetchRawMaterialParams
in: body
required: true
schema:
$ref: "#/definitions/fetchRawMaterialBody"
responses:
201:
description: fetchRawMaterial executed correctly
500:
description: fetchRawMaterial raised an exception

/getRawMaterialFromSupplier:
post:
tags:
- getRawMaterialFromSupplier
description: getRawMaterialFromSupplier
parameters:
- name: getRawMaterialFromSupplierParams
in: body
required: true
schema:
$ref: "#/definitions/getRawMaterialFromSupplierBody"
responses:
201:
description: getRawMaterialFromSupplier executed correctly
500:
description: getRawMaterialFromSupplier raised an exception

/createProducts:
post:
tags:
- createProducts
description: createProducts
parameters:
- name: createProductsParams
in: body
required: true
schema:
$ref: "#/definitions/createProductsBody"
responses:
201:
description: createProducts executed correctly
500:
description: createProducts raised an exception

/sendProductsToDistribution:
post:
tags:
- sendProductsToDistribution
description: sendProductsToDistribution
parameters:
- name: sendProductsToDistributionParams
in: body
required: true
schema:
$ref: "#/definitions/sendProductsToDistributionBody"
responses:
201:
description: sendProductsToDistribution executed correctly
500:
description: sendProductsToDistribution raised an exception

/orderProductsFromDistributor:
post:
tags:
- orderProductsFromDistributor
description: orderProductsFromDistributor
parameters:
- name: orderProductsFromDistributorParams
in: body
required: true
schema:
$ref: "#/definitions/orderProductsFromDistributorBody"
responses:
201:
description: orderProductsFromDistributor executed correctly
500:
description: orderProductsFromDistributor raised an exception

/receiveProductsFromDistributor:
post:
tags:
- receiveProductsFromDistributor
description: receiveProductsFromDistributor
parameters:
- name: receiveProductsFromDistributorParams
in: body
required: true
schema:
$ref: "#/definitions/receiveProductsFromDistributorBody"
responses:
201:
description: receiveProductsFromDistributor executed correctly
500:
description: receiveProductsFromDistributor raised an exception

/buyProductsFromRetailer:
post:
tags:
- buyProductsFromRetailer
description: buyProductsFromRetailer
parameters:
- name: buyProductsFromRetailerParams
in: body
required: true
schema:
$ref: "#/definitions/buyProductsFromRetailerBody"
responses:
201:
description: buyProductsFromRetailer executed correctly
500:
description: buyProductsFromRetailer raised an exception
npx lerna run compile --scope < chaincode name >-app
npx lerna run compile --scope supplychainchaincode-app
in compileApiApplication in command.ts chaincode=supplychainchaincode
lerna notice cli v3.13.0
lerna info filter [ 'supplychainchaincode-app' ]
lerna info Executing command in 1 package: "npm run compile"
lerna info run Ran npm script 'compile' in 'supplychainchaincode-app' in 3.8s:
> supplychainchaincode-app@1.0.0 compile /Users/luca/Projects/GitHubProjects/convector-example-supplychain-master/packages/supplychainchaincode-app
> ts-node build.ts && tsc
lerna success run Ran npm script 'compile' in 1 package in 3.8s:
lerna success - supplychainchaincode-app
npx lerna run start --scope < chaincode name >-app --stream
npx lerna run start --scope supplychainchaincode-app --stream
in startApiApplication in command.ts chaincode=supplychainchaincode
lerna notice cli v3.13.0
lerna info filter [ 'supplychainchaincode-app' ]
lerna info Executing command in 1 package: "npm run dev"
supplychainchaincode-app: > supplychainchaincode-app@1.0.0 dev /Users/luca/Projects/GitHubProjects/convector-example-supplychain-master/packages/supplychainchaincode-app
supplychainchaincode-app: > nodemon server/index.ts | pino-pretty
supplychainchaincode-app: [nodemon] 1.18.10
supplychainchaincode-app: [nodemon] to restart at any time, enter `rs`
supplychainchaincode-app: [nodemon] watching: /Users/luca/Projects/GitHubProjects/convector-example-supplychain-master/packages/supplychainchaincode-app/server/**/*
supplychainchaincode-app: [nodemon] starting `ts-node server/index.ts`
supplychainchaincode-app: (node:1541) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
supplychainchaincode-app: (node:1541) DeprecationWarning: grpc.load: Use the @grpc/proto-loader module with grpc.loadPackageDefinition instead
supplychainchaincode-app: (node:1541) MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 uncaughtException listeners added. Use emitter.setMaxListeners() to increase limit
supplychainchaincode-app: [1550919167863] INFO (supplychainchaincode-app/1541 on lucas-MacBook-Pro.local): up and running in development @: lucas-MacBook-Pro.local on port: 3000}
curl -X POST "http://localhost:3000/api/v1/supplychain/distributors" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"id\": \"DST3\", \"name\": \"Distributor3\", \"productsToBeShipped\": 123, \"productsShipped\": 123, \"productsReceived\": 123}"
curl -X GET "http://localhost:3000/api/v1/supplychain/manufacturers" -H "accept: application/json"
curl -X GET "http://localhost:3000/api/v1/supplychain/retailers/RTL_2" -H "accept: application/json"
curl -X POST "http://localhost:3000/api/v1/supplychain/fetchRawMaterial" -H "accept: application/json" -H "Content-Type: application/json" -d "{ \"supplierId\": \"SPL_1\", \"rawMaterialSupply\": 12345555}"

Multicontroller Chaincodes

Multicontroller chaincodes are now supported. You can take as example the https://github.com/worldsibu/convector-identity-patterns applying the decorators to the 2 controllers (in participant-cc and in product-cc) and adding the api.json file.

conv-rest-api generate api -c identities -p identitiesproject
curl -X POST “http://localhost:3000/api/v1/identitiesproject/participant/register" -H “accept: application/json” -H “Content-Type: application/json” -d “{ \”id\”: \”luca\”}”
curl -X POST “http://localhost:3000/api/v1/identitiesproject/product/create" -H “accept: application/json” -H “Content-Type: application/json” -d “{ \”id\”: \”pro_1\”, \”name\”: \”apple\”, \”ownerID\”: \”luca\”}”

Actual Known Limitations:

  • Generating code for infinite Hierarchies of Models (now only supports one ancestor)
  • Handling complex return types from Controller functions (like arrays of custom objects)

WorldSibu

A unified development platform to create and deploy enterprise smart contract systems.

Thanks to WorldSibu Media and Walter Montes.

Luca Tamburrano

Written by

WorldSibu

WorldSibu

A unified development platform to create and deploy enterprise smart contract systems.