catapult-restでは一部のAPIがプラグインとして用意されていて、こちらを見た感じ新たなプラグインの開発も想定されているように思うので試しに作ってみます。
restのバージョンはv2.1.0です。
最新バージョンはv2.2.1のようですが出たのがつい最近なので試せてません。
今回はお試しなので名前など適当ですが、mypluginというプラグイン名で
namespaceのIDをパラメータとして受け取り、今までにエイリアスのリンクとアンリンクしたすべてのmosaicのID、ブロック高、リンクのactionを返す
というAPIを作りたいと思います。
イメージとしてはhttp://node/myplugin/namespace1とすると下のようなデータが得られるというものです。
{
    namespaceId: namespace1,
    mosaicAlias: [
        {
            height: 1000,
            mosaicId: mosaic1,
            action: 0
        },
        {
            height: 2000,
            mosaicId: mosaic2,
            action: 0
        }
    ]
}
git cloneでcatpult-restをダウンロードしてそこにファイルを追加していく形で進めたいと思います。
レスポンスのスキーマを作り登録する
レスポンスとして返すjsonのスキーマはあらかじめオブジェクトで定義しておく必要があり、catapult-rest/catapult-sdk/src/model/ModelSchemaBuilder.jsのModelSchemaBuilderというclassのconstructor内にあるthis.schemaに用意されています。
この中からスキーマを指定して、レスポンスの構造や型を指定したものと合わせる必要があり、違う場合はエラーになります。
元から存在するもの以外にプラグイン用としてrest起動時にスキーマを追加することができるので、まずは自作プラグインのレスポンススキーマの追加処理を書いていきます。
catapult-rest/catpult-sdk/src/pluginsにmyplugin.jsというファイルを作成し下のコードを追加。
const ModelType = require('../model/ModelType');
const myplugin = {
    registerSchema: builder => {
        builder.addSchema('myplugin.mosaicAlias', {
            'height':   ModelType.uint64,
            'mosaicId': ModelType.uint64HexIdentifier,
            'action':   ModelType.int,
        });
        builder.addSchema('myplugin', {
            'namespaceId': ModelType.string,
            'mosaicAlias': { type: ModelType.array, schemaName: 'myplugin.mosaicAlias' }
        });
    },
    registerCodecs: () => {},
};
module.exports = myplugin;
registerSchemaとregisterCodecsという2つの関数を持つオブジェクトを作りexportします。
registerSchemaの引数builderがさきほどのModelSchemaBuilderのことで、addSchemaメソッドの第一引数にスキーマ名、第二引数に構造や型を指定することでスキーマを追加することができます。
配列やオブジェクトになる場合は別でスキーマをaddし、使いたい側のスキーマで指定する必要があるみたいです。
型はcatapult-rest/catapult-sdk/src/model/ModelType.jsに用意されていて、catapult-rest/catapult-sdk/src/utils/SchemaType.jsで定義されている配列やオブジェクトなどの構造を表すタイプもObject.assignでまとめられています。
registerCodecs関数は使いませんが別のところで呼び出されるので中身が空のものを用意しておきます。
次にcatapult-rest/catapult-sdk/src/plugins/catapultModelSystem.jsにmyplugin.jsをimportし、constで定義されているpluginsの配列にmypluginを追加します。
// 上で作ったmyplugin.jsをimport
const myplugin = require('./myplugin');
const plugins = {
	accountLink,
	aggregate,
	lockHash,
	lockSecret,
	metadata,
	mosaic,
	multisig,
	namespace,
	receipts,
	restrictions,
	transfer,
	myplugin, //ここに追加
};
これで先ほど作ったregisterSchema関数がrest起動時の一連の処理の中で呼び出されスキーマの登録ができます。
plugin追加に必要な関数の作成
catapult-rest/rest/src/pluginsにmypluginというディレクトリを作りmyplugin.js、MypluginDb.js、mypluginRoutes.jsというファイルを作成します。
それぞれのファイルに書いていく内容は下のようになります。
myplugin.js
→plugin追加時に必要な関数
MypluginDb.js
→mongodbとやりとりして必要なデータを取得するclass
mypluginRoutes.js
→APIを叩いたときに行われる処理
上の3つのファイルの中からまずはmyplugin.jsにコードを書いていきます。
const MypluginDb = require('./MypluginDb');
const mypluginRoutes = require('./mypluginRoutes');
module.exports = {
	createDb: db => new MypluginDb(db),
	registerTransactionStates: () => {},
	registerMessageChannels: () => {},
	registerRoutes: (...args) => {
		mypluginRoutes.register(...args);
	}
};
4つの関数を持つオブジェクトを作りexportするだけです。
createDb関数は、引数のdbをそのままMypluginDbに渡してリターンしています。
registerRoutes関数は引数で受け取ったものをそのままmypluginRoutesのregister関数(あとで解説)に渡すだけです。
残りの2つは使わないので空の関数にします。
そのあとはcatapult-rest/rest/src/plugins/routeSystem.jsにmyplugin.jsをimportし、constで定義されているpluginsにmypluginを追加します。
const aggregate = require('./aggregate/aggregate');
const empty = require('./empty');
const lockHash = require('./lockHash/lockHash');
const lockSecret = require('./lockSecret/lockSecret');
const metadata = require('./metadata/metadata');
const mosaic = require('./mosaic/mosaic');
const multisig = require('./multisig/multisig');
const namespace = require('./namespace/namespace');
const receipts = require('./receipts/receipts');
const restrictions = require('./restrictions/restrictions');
const MessageChannelBuilder = require('../connection/MessageChannelBuilder');
// importする
const myplugin = require('./myplugin/myplugin');
const plugins = {
	accountLink: empty,
	aggregate,
	lockHash,
	lockSecret,
	metadata,
	mosaic,
	multisig,
	namespace,
	receipts,
	restrictions,
	transfer: empty,
	myplugin // ここに追加
};
pluginsに追加されたプラグインはrest起動時の処理でrouteSystem.jsのconfigure関数内で先ほど作った4つの関数が呼び出され使用できるようになります。
mongodbとやりとりするclass作成
MypluginDb.jsにMypluginDbというclassを作りexportします。
このclassはconstructorにCatapaltDbが渡されるのでthis.catpultDbにセットします。
CatapaltDbはmongodbとやりとりするメソッドを複数持っており、今回のプラグインに必要なデータはqueryDocumentsメソッドに条件を渡すことで得られます。
取得したいデータはtransactionsコレクションにあるので、その中からトランザクションのタイプが17230(MosaicAliasTransaction)で、パラメータとして受け取ったidと一致するnamespaceのものを探します。
このデータ取得処理をmosaicAliasというメソッド名でMypluginDbに追加します。
class MypluginDb {
    constructor(db) {
        this.catapultDb = db;
    }
    mosaicAlias(id) {
        const condition = {
            'transaction.type': 17230,
            'transaction.namespaceId': id
        };
        return this.catapultDb.queryDocuments('transactions', condition);
    }
}
module.exports = MypluginDb;
routeの登録
plugin追加に必要な関数のregisterRoutesで呼び出されていたregister関数を作ります。
このregister関数にはserver、db、servicesという3つの引数があります。
dbにはMypluginDb、serverにはrouteの登録ができるオブジェクト、servicesには各種設定などのオブジェクトが渡されます。
serverやservicesが具体的にどのようなものなのかここでは説明しませんが、index.jsのメインの関数やcreateServer、registerRoutesなどの関数を追っていけばわかると思います。
routeの登録はserver.get(route, handler)やserver.post(route, handler)というように何個も登録できます。
今回は一つだけなので下のようになります。
const routeUtils = require('../../routes/routeUtils');
const { convertToLong } = require('../../db/dbUtils');
const catapult = require('catapult-sdk');
const { uint64 } = catapult.utils;
module.exports = {
	register: (server, db, services) => {
		server.get('/myplugin/:namespaceId', (req, res, next) => {
			const id = convertToLong(routeUtils.parseArgument(req.params, 'namespaceId', uint64.fromHex));
			db.mosaicAlias(id).then(r => {
				const alias = r.map(x => {
					return {
						height: x.meta.height,
						mosaicId: x.transaction.mosaicId,
						action: x.transaction.aliasAction,
					}
				});
				routeUtils.createSender('myplugin').sendOne('myplugin', res, next)(
					{
						'namespaceId': req.params.namespaceId,
						'mosaicAlias': alias,
					}
				);
			})
		})
	}
};
server.getの第二引数にAPIを叩いたときに行われる処理を書いていきます。
まず初めにparseArgment関数を使ってパラメータとして受け取るnamespaceIdを
parseし、さらにmongodbのlong型に変換します。
次にMypluginDbのmosaicAliasメソッドの引数にnamespaceIdを渡すことによって必要なデータが得られます。
最後にcreateSender関数をレスポンスのスキーマにmypluginを指定して呼び出します。
この関数は返り値として3つの関数をもったobjectを返すのでその中から必要な関数を使います。
今回はsendOneを使い、返り値の関数にレスポンスとして返したいデータを渡します。
最初にnamespaceIdをlong型に変換したように、今度はmongodbの型から元に戻す必要がありますが、これはレスポンススキーマで指定した型に自動で変換してくれるようです。
変換に使われてるのはcatapult-rest/rest/src/db/dbFormattingRules.jsに定義されてる関数だと思われます。
sendOneにデータを渡したとき、指定したスキーマと構造が違ったり、型が違ったりしたらエラーになるようです。
以上でプラグインの作成は終わりです。
pluginの動作を試す
symbol-bootstrapを使ってプラグインの動作を試してみます。
symbol-bootstrapの使い方に関してはこちらで詳しく解説してくれています。
パラメータとして渡すnamespaceのidは9281B90983AAA34Dを使用します。
これは僕がテストネットで作成したnamespaceで何度かエイリアスのリンクをしてあります。
まずはsymbol-bootstrapでコンテナを起動してhealth checkします。
symbol-bootstrap run -d
symbol-bootstrap healthCheck
次にmongodbコンテナのidをdocker psで調べ、そのIDを使ってmongodbのipアドレスを探します。
docker inspect "コンテナID" | grep "IPAddress"
下のように表示されるので"172.20.0.3"の部分をメモしておきます。
"SecondaryIPAddresses": null,
"IPAddress": "",
        "IPAddress": "172.20.0.3",
次にcatapult-rest/rest/resources/rest.jsonを編集します。
まずmongodbのipアドレスがlocalhostになっているのでさきほどメモしたものに変更します。
次にrestが使うport番号が3000になっているので適当な数字に変更します。これはsymbol-bootstrapで起動するrestですでに3000が使われているからです。
最後にextensionsのなかにmypluginを追加します。
rest起動時にcatapult-rest/rest/src/plugins/routeSystem.jsのconfigure関数が呼び出され、同じくrouteSystem.jsにconstで定義されたpluginsがrest.jsonのextentionsにあるかチェックされてなければエラーになるようです。
  "network": {
    "name": "mijinTest",
    "description": "catapult development network"
  },
  "port": 3030, //port番号変更
  "crossDomain": {
    "allowedHosts": ["*"],
    "allowedMethods": ["GET", "POST", "PUT", "OPTIONS"]
  },
  "extensions": [
    "accountLink",
    "aggregate",
    "lockHash",
    "lockSecret",
    "mosaic",
    "metadata",
    "multisig",
    "namespace",
    "receipts",
    "restrictions",
    "transfer",
    "myplugin"  // plugin追加
  ],
  "db": {
    "url": "mongodb://172.20.0.3:27017/", //ipアドレス変更
    "name": "catapult",
    "pageSizeMin": 10,
    "pageSizeMax": 100,
    "pageSizeDefault": 20,
    "maxConnectionAttempts": 5,
    "baseRetryDelay": 500,
    "connectionPoolSize": 10
  },
プラグインに関する設定は終わりですが、これだけではrest起動時にエラーがでてしまうのでcatapult-rest/rest/src/index.jsを2ヶ所編集します。
まずloadConfig関数の../resources/rest.jsonの部分がなぜかエラーになってしまうので絶対パスに変更。
const loadConfig = () => {
	let configFiles = process.argv.slice(2);
	if (0 === configFiles.length)
		configFiles = ['../resources/rest.json']; // ここを絶対パスにする
	let config;
	configFiles.forEach(configFile => {
		winston.info(`loading config from ${configFile}`);
		const partialConfig = JSON.parse(fs.readFileSync(configFile, 'utf8'));
		if (config) {
			// override config
			catapult.utils.objects.checkSchemaAgainstTemplate(config, partialConfig);
			catapult.utils.objects.deepAssign(config, partialConfig);
		} else {
			// primary config
			config = partialConfig;
		}
	});
	validateConfig(config);
	return config;
};
次にメインとなる関数内でconst定義されたconnectionConfigの一部をコメントアウト
(() => {
	const config = loadConfig();
	configureLogging(config.logging);
	winston.verbose('finished loading rest server config', config);
	const network = catapult.model.networkInfo.networks[config.network.name];
	if (!network) {
		winston.error(`no network found with name: '${config.network.name}'`);
		return;
	}
	const serviceManager = createServiceManager();
	const db = new CatapultDb({
		networkId: network.id,
		// to be removed when old pagination is not used anymore
		// json settings should also be moved from config.db to config.api or similar
		pageSizeMin: config.db.pageSizeMin,
		pageSizeMax: config.db.pageSizeMax
	});
	serviceManager.pushService(db, 'close');
	winston.info(`connecting to ${config.db.url} (database:${config.db.name})`);
	connectToDbWithRetry(db, config.db)
		.then(() => {
			winston.info('registering routes');
			const serverAndCodec = createServer(config);
			const { server } = serverAndCodec;
			serviceManager.pushService(server, 'close');
			const connectionConfig = {
				apiNode: config.apiNode,
                // ---ここをコメントアウト---
				// certificate: fs.readFileSync(config.apiNode.tlsClientCertificatePath),
				// key: fs.readFileSync(config.apiNode.tlsClientKeyPath),
				// caCertificate: fs.readFileSync(config.apiNode.tlsCaCertificatePath)
			};
			const connectionService = createConnectionService(connectionConfig, winston.verbose);
			registerRoutes(server, db, { codec: serverAndCodec.codec, config, connectionService });
			winston.info(`listening on port ${config.port}`);
			server.listen(config.port);
		})
		.catch(err => {
			winston.error('rest server is exiting due to error', err);
			serviceManager.stopAll();
		});
	process.on('SIGINT', () => {
		winston.info('SIGINT detected, shutting down rest server');
		serviceManager.stopAll();
	});
})();
これで準備はokなのでcatpult-rest/yarn_setup.shを使ってパッケージのインストールやcatpult-sdkのビルドをします。
あとはブロックの同期がある程度進むのを待ってからrestを起動してブラウザを開きurlを打ち込みます。
http://localhost:3030/myplugin/9281B90983AAA34D
するとこのようなデータが返ってきます。
最初にイメージした通りのデータが返ってきました!
以上です!

