Browse Source

Merge branch 'query_node' into development

Bedeho Mender 4 years ago
parent
commit
089d1c3164
91 changed files with 6102 additions and 0 deletions
  1. 8 0
      .gitignore
  2. 12 0
      query-node/joystream-query-node/.env
  3. 145 0
      query-node/joystream-query-node/README.md
  4. 1 0
      query-node/joystream-query-node/bootstrap/index.ts
  5. 41 0
      query-node/joystream-query-node/bootstrap/members.ts
  6. 1827 0
      query-node/joystream-query-node/bootstrap/package-lock.json
  7. 19 0
      query-node/joystream-query-node/bootstrap/package.json
  8. 18 0
      query-node/joystream-query-node/bootstrap/tsconfig.json
  9. 1 0
      query-node/joystream-query-node/mappings/index.ts
  10. 82 0
      query-node/joystream-query-node/mappings/members.ts
  11. 21 0
      query-node/joystream-query-node/schema.graphql
  12. 14 0
      query-node/joystream-query-node/scripts/reset-dev.sh
  13. 7 0
      query-node/joystream-query-node/scripts/run-dev.sh
  14. 15 0
      query-node/substrate-query-framework/README.md
  15. 1 0
      query-node/substrate-query-framework/cli/.eslintignore
  16. 15 0
      query-node/substrate-query-framework/cli/.eslintrc.js
  17. 9 0
      query-node/substrate-query-framework/cli/.gitignore
  18. 80 0
      query-node/substrate-query-framework/cli/README.md
  19. 5 0
      query-node/substrate-query-framework/cli/bin/run
  20. 3 0
      query-node/substrate-query-framework/cli/bin/run.cmd
  21. 95 0
      query-node/substrate-query-framework/cli/package.json
  22. 139 0
      query-node/substrate-query-framework/cli/src/commands/codegen.ts
  23. 21 0
      query-node/substrate-query-framework/cli/src/commands/db.ts
  24. 153 0
      query-node/substrate-query-framework/cli/src/generate/FTSQueryRenderer.ts
  25. 182 0
      query-node/substrate-query-framework/cli/src/generate/ModelRenderer.ts
  26. 134 0
      query-node/substrate-query-framework/cli/src/generate/SourcesGenerator.ts
  27. 50 0
      query-node/substrate-query-framework/cli/src/generate/utils.ts
  28. 100 0
      query-node/substrate-query-framework/cli/src/helpers/SchemaDirective.ts
  29. 156 0
      query-node/substrate-query-framework/cli/src/helpers/SchemaParser.ts
  30. 159 0
      query-node/substrate-query-framework/cli/src/helpers/WarthogModelBuilder.ts
  31. 130 0
      query-node/substrate-query-framework/cli/src/helpers/WarthogWrapper.ts
  32. 15 0
      query-node/substrate-query-framework/cli/src/helpers/constant.ts
  33. 86 0
      query-node/substrate-query-framework/cli/src/helpers/db.ts
  34. 17 0
      query-node/substrate-query-framework/cli/src/helpers/formatter.ts
  35. 17 0
      query-node/substrate-query-framework/cli/src/helpers/tsTypes.ts
  36. 10 0
      query-node/substrate-query-framework/cli/src/index.ts
  37. 23 0
      query-node/substrate-query-framework/cli/src/model/FTSQuery.ts
  38. 16 0
      query-node/substrate-query-framework/cli/src/model/ScalarTypes.ts
  39. 197 0
      query-node/substrate-query-framework/cli/src/model/WarthogModel.ts
  40. 4 0
      query-node/substrate-query-framework/cli/src/model/index.ts
  41. 14 0
      query-node/substrate-query-framework/cli/src/templates/db-helper.mst
  42. 9 0
      query-node/substrate-query-framework/cli/src/templates/dotenv-ormconfig.mst
  43. 1 0
      query-node/substrate-query-framework/cli/src/templates/dotenv.mst
  44. 59 0
      query-node/substrate-query-framework/cli/src/templates/entities/model.ts.mst
  45. 65 0
      query-node/substrate-query-framework/cli/src/templates/entities/resolver.ts.mst
  46. 15 0
      query-node/substrate-query-framework/cli/src/templates/entities/service.ts.mst
  47. 5 0
      query-node/substrate-query-framework/cli/src/templates/event-class-defination.mst
  48. 45 0
      query-node/substrate-query-framework/cli/src/templates/graphql-server.index.mst
  49. 149 0
      query-node/substrate-query-framework/cli/src/templates/index-builder-entry.mst
  50. 37 0
      query-node/substrate-query-framework/cli/src/templates/indexer.package.json
  51. 19 0
      query-node/substrate-query-framework/cli/src/templates/indexer.tsconfig.json
  52. 7 0
      query-node/substrate-query-framework/cli/src/templates/mappings.mst
  53. 8 0
      query-node/substrate-query-framework/cli/src/templates/processing-pack.mst
  54. 57 0
      query-node/substrate-query-framework/cli/src/templates/textsearch/migration.ts.mst
  55. 46 0
      query-node/substrate-query-framework/cli/src/templates/textsearch/resolver.ts.mst
  56. 80 0
      query-node/substrate-query-framework/cli/src/templates/textsearch/service.ts.mst
  57. 40 0
      query-node/substrate-query-framework/cli/src/templates/warthog.env.yml
  58. 35 0
      query-node/substrate-query-framework/cli/src/utils/utils.ts
  59. 19 0
      query-node/substrate-query-framework/cli/test/fixtures/multiple-entities.graphql
  60. 17 0
      query-node/substrate-query-framework/cli/test/fixtures/multiple-queries.graphql
  61. 4 0
      query-node/substrate-query-framework/cli/test/fixtures/non-string-query.graphql
  62. 17 0
      query-node/substrate-query-framework/cli/test/fixtures/single-type.graphql
  63. 39 0
      query-node/substrate-query-framework/cli/test/helpers/FTSQueryRenderer.test.ts
  64. 136 0
      query-node/substrate-query-framework/cli/test/helpers/ModelRenderer.test.ts
  65. 124 0
      query-node/substrate-query-framework/cli/test/helpers/SchemaParser.test.ts
  66. 33 0
      query-node/substrate-query-framework/cli/test/helpers/WarthogModel.test.ts
  67. 28 0
      query-node/substrate-query-framework/cli/test/helpers/WarthogModelBuilder.test.ts
  68. 0 0
      query-node/substrate-query-framework/cli/test/helpers/__snapshots__/FTSQueryRenderer.test.ts.snap.js
  69. 41 0
      query-node/substrate-query-framework/cli/test/helpers/model.ts
  70. 5 0
      query-node/substrate-query-framework/cli/test/mocha.opts
  71. 9 0
      query-node/substrate-query-framework/cli/test/tsconfig.json
  72. 19 0
      query-node/substrate-query-framework/cli/tsconfig.json
  73. 11 0
      query-node/substrate-query-framework/docker-compose.yml
  74. 19 0
      query-node/substrate-query-framework/index-builder/package.json
  75. 32 0
      query-node/substrate-query-framework/index-builder/src/ISubstrateQueryService.ts
  76. 103 0
      query-node/substrate-query-framework/index-builder/src/IndexBuilder.ts
  77. 171 0
      query-node/substrate-query-framework/index-builder/src/QueryBlockProducer.ts
  78. 80 0
      query-node/substrate-query-framework/index-builder/src/QueryEvent.ts
  79. 14 0
      query-node/substrate-query-framework/index-builder/src/QueryEventBlock.ts
  80. 9 0
      query-node/substrate-query-framework/index-builder/src/QueryEventProcessingPack.ts
  81. 90 0
      query-node/substrate-query-framework/index-builder/src/QueryNode.ts
  82. 48 0
      query-node/substrate-query-framework/index-builder/src/QueryNodeManager.ts
  83. 10 0
      query-node/substrate-query-framework/index-builder/src/bootstrap/BootstrapPack.ts
  84. 101 0
      query-node/substrate-query-framework/index-builder/src/bootstrap/Bootstrapper.ts
  85. 4 0
      query-node/substrate-query-framework/index-builder/src/bootstrap/index.ts
  86. 56 0
      query-node/substrate-query-framework/index-builder/src/db/DatabaseManager.ts
  87. 71 0
      query-node/substrate-query-framework/index-builder/src/db/entities.ts
  88. 22 0
      query-node/substrate-query-framework/index-builder/src/db/helper.ts
  89. 4 0
      query-node/substrate-query-framework/index-builder/src/db/index.ts
  90. 29 0
      query-node/substrate-query-framework/index-builder/src/index.ts
  91. 18 0
      query-node/substrate-query-framework/index-builder/tsconfig.json

+ 8 - 0
.gitignore

@@ -32,3 +32,11 @@ yarn*
 
 # Istanbul report output
 **.nyc_output/
+
+# Warthog codegen
+**/generated/
+# Auto generated api preview
+apipreview.graphql
+
+# eslint cache
+**/.eslintcache

+ 12 - 0
query-node/joystream-query-node/.env

@@ -0,0 +1,12 @@
+WS_PROVIDER_ENDPOINT_URI=ws://localhost:9944
+QUERY_NODE_BOOTSTRAP_DB=true
+BOOTSTRAP_PACK_LOCATION=../../bootstrap
+TYPE_REGISTER_PACKAGE_NAME=@joystream/types
+TYPE_REGISTER_FUNCTION=registerJoystreamTypes
+DB_NAME=query_node
+DB_USER=postgres
+DB_PASS=postgres
+DB_HOST=localhost
+DB_PORT=5432
+GRAPHQL_SERVER_PORT=4000
+DEBUG=index-builder:*

+ 145 - 0
query-node/joystream-query-node/README.md

@@ -0,0 +1,145 @@
+# Joystream Query Node
+
+Joystream query node can be generated by using `substrate-query-node/cli`.
+
+## Getting Started
+
+Cli create a folder named `generated` and put everthing inside it.
+
+```
+$ cli codegen
+```
+
+Start graphql server:
+
+```
+$ cd generated/graphql-server
+$ yarn start:dev
+```
+
+Start block indexer:
+
+```
+$ cd generated/indexer
+$ yarn start
+```
+
+## Add a new mapping for Joystream MemberRegistered event
+
+1. Every mapping function get a parameter of `DB` type
+
+```ts
+import { DB } from '../generated/indexer';
+```
+
+2. `db` object is for database operations save/get/remove and access to event itself
+
+3. Define the event handler function with the following signature and import the entity class
+
+```ts
+import { MemberRegistereds } from '../generated/indexer/entities/MemberRegistereds';
+export async function handleMemberRegistered(db: DB) {}
+```
+
+4. Inside the handler function create a new instance of the entity and fill properties with event data.
+
+```ts
+// Get event data
+const { AccountId, MemberId } = db.event.event_params;
+const member = new MemberRegistereds({ accountId: AccountId.toString(), memberId: +MemberId });
+```
+
+5. Call `db.save()` method to save data on database
+
+```ts
+// Save to database.
+db.save<MemberRegistereds>(member);
+```
+
+6. Query database
+
+```ts
+// Query from database
+const findOptions = { where: { memberId: 123 } }; // match the record
+const m = await db.get(MemberRegistereds, findOptions);
+```
+
+Below you can find the complete code
+
+**Complete code**
+
+```ts
+import { MemberRegistereds } from "../generated/indexer/entities/MemberRegistereds";
+import { DB } from "../generated/indexer";
+
+export async function handleMemberRegistered(db: DB) {
+  // Get event data
+  const { AccountId, MemberId } = db.event.event_params;
+
+  const member = new MemberRegistereds({ accountId: AccountId.toString(), memberId: +MemberId };
+
+  // Save to database.
+  db.save<MemberRegistereds>(member);
+
+  // Query from database
+  const m = await db.get(MemberRegistereds, { where: { memberId: 123 } });
+}
+```
+
+## Query Node Constructs Explained
+
+1. `schema.graphql` is where you define types for graphql server. Graphql server use these types to generate db models, db tables, graphql resolvers.
+
+Below you can find a type defination example:
+
+```graphql
+type Membership {
+  # Member's root account id
+  accountId: String!
+
+  # Member's id
+  memberId: Int!
+
+  # The unique handle chosen by member
+  handle: String
+
+  # A Url to member's Avatar image
+  avatarUri: String
+
+  # Short text chosen by member to share information about themselves
+  about: String
+}
+```
+
+**Important** Relationship between types not supported yet!
+
+2. Block indexer is block consumer and every block can have events that we want to store their data. So indexing data from events we need to send the event to a function or a class that can handle the event and stores the event data on the database. `mappings` are the functions that we use to update our database with events data. Functions that we define in our mappings will be called only when the event name match our function name (function name pattern is `'handle' + eventName`). We call mapping functions as event handlers. Each event handler have only one parameter which is the `db: DB`. Every database operation is made with `db` object and the event can be accessed with `db` object. Below you can find an example for the event a handler:
+
+```ts
+// mappings/index.ts
+
+import { DB } from '../generated/indexer';
+
+export function handleMemberRegistered(db: DB) {
+  console.log(`Event parameters: ${db.event.event_params}`);
+}
+```
+
+3. Block indexer connects to a blockchain node via WebSocket so we need to tell block indexer where to find the address of the node. Also, on the initialization of the indexer, we must pass the type register function as a parameter. So we put these variables inside the `.env` file that indexer can find and use them. For Joystream we will be running a local development node and add the name of the function, the package for the type registration:
+
+```
+WS_PROVIDER_ENDPOINT_URI=ws://localhost:9944
+TYPE_REGISTER_PACKAGE_NAME=@joystream/types
+TYPE_REGISTER_FUNCTION=registerJoystreamTypes
+```
+
+4. Database connections options are defined in `.env`:
+
+```
+DB_NAME=test
+DB_USER=postgres
+DB_PASS=postgres
+DB_HOST=localhost
+DB_PORT=5432
+GRAPHQL_SERVER_PORT=4000
+```

+ 1 - 0
query-node/joystream-query-node/bootstrap/index.ts

@@ -0,0 +1 @@
+export { bootMembers } from './members';

+ 41 - 0
query-node/joystream-query-node/bootstrap/members.ts

@@ -0,0 +1,41 @@
+import { Member } from '../generated/graphql-server/src/modules/member/member.model';
+import { DB, getLogger } from '../generated/indexer';
+
+import { ApiPromise } from '@polkadot/api';
+import { Hash } from '@polkadot/types/interfaces';
+import { Option } from '@polkadot/types/codec';
+import type { Profile } from '@joystream/types/lib/members';
+import { Codec } from '@polkadot/types/types';
+
+const logger = getLogger();
+
+export async function bootMembers(api: ApiPromise, db: DB) {
+  let blkHeight: number = process.env.BLOCK_HEIGHT ? parseInt(process.env.BLOCK_HEIGHT) : 0;
+  let blkHash: Hash = await api.rpc.chain.getBlockHash(blkHeight);
+  let ids = await api.query.members.membersCreated.at(blkHash);
+  let num: number = parseInt(ids.toString());
+
+  for (let i = 0; i < num; i++) {
+    let profileOpt = (await api.query.members.memberProfile.at(blkHash, i)) as Option<Profile & Codec>;
+    let profile: Profile | null = profileOpt.unwrapOr(null);
+
+    if (!profile) {
+      continue;
+    }
+
+    let member = new Member();
+    member.memberId = i.toString();
+    member.handle = profile.handle.toString();
+    member.avatarUri = profile.avatar_uri.toString();
+    member.about = profile.about.toString();
+
+    member.rootAccount = Buffer.from(profile.root_account);
+    member.controllerAccount = Buffer.from(profile.controller_account);
+    member.registeredAtBlock = profile.registered_at_block.toString();
+
+    logger.trace(`Saving member: ${JSON.stringify(member, null, 2)}`);
+    await db.save<Member>(member);
+    logger.info(`Saved members: ${i}/${num}`);
+  }
+  logger.info(`Done bootstrapping members!`);
+}

+ 1827 - 0
query-node/joystream-query-node/bootstrap/package-lock.json

@@ -0,0 +1,1827 @@
+{
+  "name": "mappings",
+  "version": "1.0.0",
+  "lockfileVersion": 1,
+  "requires": true,
+  "dependencies": {
+    "@babel/runtime": {
+      "version": "7.9.6",
+      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.9.6.tgz",
+      "integrity": "sha512-64AF1xY3OAkFHqOb9s4jpgk1Mm5vDZ4L3acHvAml+53nO1XbXLuDodsVpO4OIUsmemlUHMxNdYMNJmsvOwLrvQ==",
+      "requires": {
+        "regenerator-runtime": "^0.13.4"
+      }
+    },
+    "@joystream/types": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/@joystream/types/-/types-0.8.0.tgz",
+      "integrity": "sha512-Gw9Igc7Q9TnpPal1JUBv9bWy6+wpRxK8waGxnI53lYhC5F6PH7KGoRb+VlxnqNdk1lIgcl+kHc4zAYvXJD5TBg==",
+      "requires": {
+        "@polkadot/types": "^0.96.1",
+        "@types/vfile": "^4.0.0",
+        "ajv": "^6.11.0"
+      },
+      "dependencies": {
+        "@polkadot/types": {
+          "version": "0.96.1",
+          "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-0.96.1.tgz",
+          "integrity": "sha512-b8AZBNmMjB0+34Oxue3AYc0gIjDHYCdVGtDpel0omHkLMcEquSvrCniLm+p7g4cfArICiZPFmS9In/OWWdRUVA==",
+          "requires": {
+            "@babel/runtime": "^7.7.1",
+            "@polkadot/util": "^1.7.0-beta.5",
+            "@polkadot/util-crypto": "^1.7.0-beta.5",
+            "@types/memoizee": "^0.4.3",
+            "memoizee": "^0.4.14"
+          }
+        },
+        "@polkadot/util": {
+          "version": "1.8.1",
+          "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-1.8.1.tgz",
+          "integrity": "sha512-sFpr+JLCG9d+epjboXsmJ1qcKa96r8ZYzXmVo8+aPzI/9jKKyez6Unox/dnfnpKppZB2nJuLcsxQm6nocp2Caw==",
+          "requires": {
+            "@babel/runtime": "^7.7.7",
+            "@types/bn.js": "^4.11.6",
+            "bn.js": "^4.11.8",
+            "camelcase": "^5.3.1",
+            "chalk": "^3.0.0",
+            "ip-regex": "^4.1.0",
+            "moment": "^2.24.0"
+          }
+        },
+        "@polkadot/util-crypto": {
+          "version": "1.8.1",
+          "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-1.8.1.tgz",
+          "integrity": "sha512-ypUs10hV1HPvYc0ZsEu+LTGSEh0rkr0as/FUh7+Z9v3Bxibn3aO+EOxJPQuDbZZ59FSMRmc9SeOSa0wn9ddrnw==",
+          "requires": {
+            "@babel/runtime": "^7.7.7",
+            "@polkadot/util": "^1.8.1",
+            "@polkadot/wasm-crypto": "^0.14.1",
+            "@types/bip39": "^2.4.2",
+            "@types/bs58": "^4.0.0",
+            "@types/pbkdf2": "^3.0.0",
+            "@types/secp256k1": "^3.5.0",
+            "@types/xxhashjs": "^0.2.1",
+            "base-x": "3.0.5",
+            "bip39": "^2.5.0",
+            "blakejs": "^1.1.0",
+            "bs58": "^4.0.1",
+            "js-sha3": "^0.8.0",
+            "secp256k1": "^3.8.0",
+            "tweetnacl": "^1.0.1",
+            "xxhashjs": "^0.2.2"
+          }
+        },
+        "@polkadot/wasm-crypto": {
+          "version": "0.14.1",
+          "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-0.14.1.tgz",
+          "integrity": "sha512-Xng7L2Z8TNZa/5g6pot4O06Jf0ohQRZdvfl8eQL+E/L2mcqJYC1IjkMxJBSBuQEV7hisWzh9mHOy5WCcgPk29Q=="
+        },
+        "base-x": {
+          "version": "3.0.5",
+          "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.5.tgz",
+          "integrity": "sha512-C3picSgzPSLE+jW3tcBzJoGwitOtazb5B+5YmAxZm2ybmTi9LNgAtDO/jjVEBZwHoXmDBZ9m/IELj3elJVRBcA==",
+          "requires": {
+            "safe-buffer": "^5.0.1"
+          }
+        },
+        "bip39": {
+          "version": "2.6.0",
+          "resolved": "https://registry.npmjs.org/bip39/-/bip39-2.6.0.tgz",
+          "integrity": "sha512-RrnQRG2EgEoqO24ea+Q/fftuPUZLmrEM3qNhhGsA3PbaXaCW791LTzPuVyx/VprXQcTbPJ3K3UeTna8ZnVl2sg==",
+          "requires": {
+            "create-hash": "^1.1.0",
+            "pbkdf2": "^3.0.9",
+            "randombytes": "^2.0.1",
+            "safe-buffer": "^5.0.1",
+            "unorm": "^1.3.3"
+          }
+        },
+        "bn.js": {
+          "version": "4.11.8",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+          "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+        },
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        }
+      }
+    },
+    "@polkadot/api": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-1.14.1.tgz",
+      "integrity": "sha512-8HKrDp8khcy41jaEcTbelLircEhT+tCGc0yX9rSij2N4oulpxdY8fWWaF2ipVr2GKpE2t6fjBPiZcVMCDwpu9g==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/api-derive": "1.14.1",
+        "@polkadot/keyring": "^2.10.1",
+        "@polkadot/metadata": "1.14.1",
+        "@polkadot/rpc-core": "1.14.1",
+        "@polkadot/rpc-provider": "1.14.1",
+        "@polkadot/types": "1.14.1",
+        "@polkadot/types-known": "1.14.1",
+        "@polkadot/util": "^2.10.1",
+        "@polkadot/util-crypto": "^2.10.1",
+        "bn.js": "^5.1.1",
+        "eventemitter3": "^4.0.1",
+        "rxjs": "^6.5.5"
+      }
+    },
+    "@polkadot/api-derive": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-1.14.1.tgz",
+      "integrity": "sha512-Qt+ijb+9WhDvBQM1ZJPSJKMchILY4/9pgGAMUcWZf9iC2nR9tOE8OG+OeFdzno43jspuZ8qUrkg5vapfZ/5/gQ==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/api": "1.14.1",
+        "@polkadot/rpc-core": "1.14.1",
+        "@polkadot/rpc-provider": "1.14.1",
+        "@polkadot/types": "1.14.1",
+        "@polkadot/util": "^2.10.1",
+        "@polkadot/util-crypto": "^2.10.1",
+        "bn.js": "^5.1.1",
+        "memoizee": "^0.4.14",
+        "rxjs": "^6.5.5"
+      }
+    },
+    "@polkadot/keyring": {
+      "version": "2.10.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-2.10.1.tgz",
+      "integrity": "sha512-6Wbft7MtxbnWaHZpvg3yT8l4oQNp5xTwbqVkdaRfXmPsmhJ1YJcprFWLuKsWZE4x59cYyK7eKhnKcAvFny4HTQ==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/util": "2.10.1",
+        "@polkadot/util-crypto": "2.10.1"
+      }
+    },
+    "@polkadot/metadata": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/metadata/-/metadata-1.14.1.tgz",
+      "integrity": "sha512-nehxg81vcjSay5ScIwASNzM6Li59M0BR3g57hVkkj4uAbvu4bOEZ92dnQuGKbfzMmEQtvLfDdw/iDPqzCgxyGg==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/types": "1.14.1",
+        "@polkadot/types-known": "1.14.1",
+        "@polkadot/util": "^2.10.1",
+        "@polkadot/util-crypto": "^2.10.1",
+        "bn.js": "^5.1.1"
+      }
+    },
+    "@polkadot/rpc-core": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-1.14.1.tgz",
+      "integrity": "sha512-GxZlnxO4ocwaMKdfHgAc7/fvH5nLqZ1AdR9xKkqyR2t3MVjQozB2NeJi99mmZ093AhYHKvGmaBtu38CVTO8Sxg==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/metadata": "1.14.1",
+        "@polkadot/rpc-provider": "1.14.1",
+        "@polkadot/types": "1.14.1",
+        "@polkadot/util": "^2.10.1",
+        "memoizee": "^0.4.14",
+        "rxjs": "^6.5.5"
+      }
+    },
+    "@polkadot/rpc-provider": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-1.14.1.tgz",
+      "integrity": "sha512-bp1EVLYVculRwcxKGlNxi1CWdsCEal9rtwMryOu5jw637Lpm23N1xq5vrX/pVQB2DzytvmuBJeUsf7DdgHEVAg==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/metadata": "1.14.1",
+        "@polkadot/types": "1.14.1",
+        "@polkadot/util": "^2.10.1",
+        "@polkadot/util-crypto": "^2.10.1",
+        "bn.js": "^5.1.1",
+        "eventemitter3": "^4.0.1",
+        "isomorphic-fetch": "^2.2.1",
+        "websocket": "^1.0.31"
+      }
+    },
+    "@polkadot/types": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-1.14.1.tgz",
+      "integrity": "sha512-Q3JIlAXMVWNGjsdWG0xnhone/1uj7R3vWFjft+cweNs40/tUalY6AbyBZ29XTU8WPEmmUspprQ5YmujhHtZg8Q==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/metadata": "1.14.1",
+        "@polkadot/util": "^2.10.1",
+        "@polkadot/util-crypto": "^2.10.1",
+        "@types/bn.js": "^4.11.6",
+        "bn.js": "^5.1.1",
+        "memoizee": "^0.4.14",
+        "rxjs": "^6.5.5"
+      }
+    },
+    "@polkadot/types-known": {
+      "version": "1.14.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-1.14.1.tgz",
+      "integrity": "sha512-fqde3QavX1z+xIra1D6cObf36ATbK5rCcwG2vQU3YXV3NIHYIqQ6CO79TaybKsxx+Sv7ygy4j13G2rSdLqSuXQ==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/types": "1.14.1",
+        "@polkadot/util": "^2.10.1",
+        "bn.js": "^5.1.1"
+      }
+    },
+    "@polkadot/util": {
+      "version": "2.10.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-2.10.1.tgz",
+      "integrity": "sha512-DaIvvx3zphDlf3ZywLnlrRTngcjGIl7Dn3lbwsgHlMSyENz07TG6YG+ztr0ztUrb9BqFKAeH6XGNtGPBp0LxwA==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@types/bn.js": "^4.11.6",
+        "bn.js": "^5.1.1",
+        "camelcase": "^5.3.1",
+        "chalk": "^4.0.0",
+        "ip-regex": "^4.1.0"
+      }
+    },
+    "@polkadot/util-crypto": {
+      "version": "2.10.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-2.10.1.tgz",
+      "integrity": "sha512-sxJZwi5CWfOrytVGtvMT5gn7+rrdgCECtmiG94AouyzdCIWqr9DC+BbX95q7Rja8+kLwkm08FWAsI5pwN9oizQ==",
+      "requires": {
+        "@babel/runtime": "^7.9.6",
+        "@polkadot/util": "2.10.1",
+        "@polkadot/wasm-crypto": "^1.2.1",
+        "base-x": "^3.0.8",
+        "bip39": "^3.0.2",
+        "blakejs": "^1.1.0",
+        "bn.js": "^5.1.1",
+        "bs58": "^4.0.1",
+        "elliptic": "^6.5.2",
+        "js-sha3": "^0.8.0",
+        "pbkdf2": "^3.0.17",
+        "tweetnacl": "^1.0.3",
+        "xxhashjs": "^0.2.2"
+      }
+    },
+    "@polkadot/wasm-crypto": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/@polkadot/wasm-crypto/-/wasm-crypto-1.2.1.tgz",
+      "integrity": "sha512-nckIoZBV4nBZdeKwFwH5t7skS7L7GO5EFUl5B1F6uCjUfdNpDz3DtqbYQHcLdCZNmG4TDLg6w/1J+rkl2SiUZw=="
+    },
+    "@types/bip39": {
+      "version": "2.4.2",
+      "resolved": "https://registry.npmjs.org/@types/bip39/-/bip39-2.4.2.tgz",
+      "integrity": "sha512-Vo9lqOIRq8uoIzEVrV87ZvcIM0PN9t0K3oYZ/CS61fIYKCBdOIM7mlWzXuRvSXrDtVa1uUO2w1cdfufxTC0bzg==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/bn.js": {
+      "version": "4.11.6",
+      "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-4.11.6.tgz",
+      "integrity": "sha512-pqr857jrp2kPuO9uRjZ3PwnJTjoQy+fcdxvBTvHm6dkmEL9q+hDD/2j/0ELOBPtPnS8LjCX0gI9nbl8lVkadpg==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/bs58": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-4.0.1.tgz",
+      "integrity": "sha512-yfAgiWgVLjFCmRv8zAcOIHywYATEwiTVccTLnRp6UxTNavT55M9d/uhK3T03St/+8/z/wW+CRjGKUNmEqoHHCA==",
+      "requires": {
+        "base-x": "^3.0.6"
+      }
+    },
+    "@types/color-name": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
+      "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
+    },
+    "@types/memoizee": {
+      "version": "0.4.4",
+      "resolved": "https://registry.npmjs.org/@types/memoizee/-/memoizee-0.4.4.tgz",
+      "integrity": "sha512-c9+1g6+6vEqcw5UuM0RbfQV0mssmZcoG9+hNC5ptDCsv4G+XJW1Z4pE13wV5zbc9e0+YrDydALBTiD3nWG1a3g=="
+    },
+    "@types/node": {
+      "version": "14.0.1",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz",
+      "integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA=="
+    },
+    "@types/pbkdf2": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.0.0.tgz",
+      "integrity": "sha512-6J6MHaAlBJC/eVMy9jOwj9oHaprfutukfW/Dyt0NEnpQ/6HN6YQrpvLwzWdWDeWZIdenjGHlbYDzyEODO5Z+2Q==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/secp256k1": {
+      "version": "3.5.3",
+      "resolved": "https://registry.npmjs.org/@types/secp256k1/-/secp256k1-3.5.3.tgz",
+      "integrity": "sha512-NGcsPDR0P+Q71O63e2ayshmiZGAwCOa/cLJzOIuhOiDvmbvrCIiVtEpqdCJGogG92Bnr6tw/6lqVBsRMEl15OQ==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "@types/unist": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz",
+      "integrity": "sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ=="
+    },
+    "@types/vfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/@types/vfile/-/vfile-4.0.0.tgz",
+      "integrity": "sha512-eleP0/Cz8uVWxARDLi3Axq2+fDdN4ibAXoC6Pv8p6s7znXaUL7XvhgeIhjCiNMnvlLNP+tmCLd+RuCryGgmtEg==",
+      "requires": {
+        "vfile": "*"
+      }
+    },
+    "@types/xxhashjs": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/@types/xxhashjs/-/xxhashjs-0.2.1.tgz",
+      "integrity": "sha512-Akm13wkwsQylVnBokl/aiKLtSxndSjfgTjdvmSxXNehYy4NymwdfdJHwGhpV54wcYfmOByOp3ak8AGdUlvp0sA==",
+      "requires": {
+        "@types/node": "*"
+      }
+    },
+    "ajv": {
+      "version": "6.12.2",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz",
+      "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==",
+      "requires": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      }
+    },
+    "ansi-regex": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz",
+      "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg=="
+    },
+    "ansi-styles": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
+      "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
+      "requires": {
+        "@types/color-name": "^1.1.1",
+        "color-convert": "^2.0.1"
+      }
+    },
+    "any-promise": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
+      "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8="
+    },
+    "app-root-path": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.0.0.tgz",
+      "integrity": "sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw=="
+    },
+    "argparse": {
+      "version": "1.0.10",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+      "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+      "requires": {
+        "sprintf-js": "~1.0.2"
+      }
+    },
+    "balanced-match": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+      "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
+    },
+    "base-x": {
+      "version": "3.0.8",
+      "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.8.tgz",
+      "integrity": "sha512-Rl/1AWP4J/zRrk54hhlxH4drNxPJXYUaKffODVI53/dAsV4t9fBxyxYKAVPU1XBHxYwOWP9h9H0hM2MVw4YfJA==",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "base64-js": {
+      "version": "1.3.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz",
+      "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g=="
+    },
+    "bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "requires": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
+    "bip39": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.0.2.tgz",
+      "integrity": "sha512-J4E1r2N0tUylTKt07ibXvhpT2c5pyAFgvuA5q1H9uDy6dEGpjV8jmymh3MTYJDLCNbIVClSB9FbND49I6N24MQ==",
+      "requires": {
+        "@types/node": "11.11.6",
+        "create-hash": "^1.1.0",
+        "pbkdf2": "^3.0.9",
+        "randombytes": "^2.0.1"
+      },
+      "dependencies": {
+        "@types/node": {
+          "version": "11.11.6",
+          "resolved": "https://registry.npmjs.org/@types/node/-/node-11.11.6.tgz",
+          "integrity": "sha512-Exw4yUWMBXM3X+8oqzJNRqZSwUAaS4+7NdvHqQuFi/d+synz++xmX3QIf+BFqneW8N31R8Ky+sikfZUXq07ggQ=="
+        }
+      }
+    },
+    "bip66": {
+      "version": "1.1.5",
+      "resolved": "https://registry.npmjs.org/bip66/-/bip66-1.1.5.tgz",
+      "integrity": "sha1-AfqHSHhcpwlV1QESF9GzE5lpyiI=",
+      "requires": {
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "blakejs": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/blakejs/-/blakejs-1.1.0.tgz",
+      "integrity": "sha1-ad+S75U6qIylGjLfarHFShVfx6U="
+    },
+    "bn.js": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.1.tgz",
+      "integrity": "sha512-IUTD/REb78Z2eodka1QZyyEk66pciRcP6Sroka0aI3tG/iwIdYLrBD62RsubR7vqdt3WyX8p4jxeatzmRSphtA=="
+    },
+    "brace-expansion": {
+      "version": "1.1.11",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+      "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+      "requires": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "brorand": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+      "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
+    },
+    "browserify-aes": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+      "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+      "requires": {
+        "buffer-xor": "^1.0.3",
+        "cipher-base": "^1.0.0",
+        "create-hash": "^1.1.0",
+        "evp_bytestokey": "^1.0.3",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "bs58": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz",
+      "integrity": "sha1-vhYedsNU9veIrkBx9j806MTwpCo=",
+      "requires": {
+        "base-x": "^3.0.2"
+      }
+    },
+    "buffer": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz",
+      "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==",
+      "requires": {
+        "base64-js": "^1.0.2",
+        "ieee754": "^1.1.4"
+      }
+    },
+    "buffer-xor": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+      "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
+    },
+    "camelcase": {
+      "version": "5.3.1",
+      "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz",
+      "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="
+    },
+    "chalk": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz",
+      "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==",
+      "requires": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      }
+    },
+    "cipher-base": {
+      "version": "1.0.4",
+      "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+      "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "cli-highlight": {
+      "version": "2.1.4",
+      "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.4.tgz",
+      "integrity": "sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==",
+      "requires": {
+        "chalk": "^3.0.0",
+        "highlight.js": "^9.6.0",
+        "mz": "^2.4.0",
+        "parse5": "^5.1.1",
+        "parse5-htmlparser2-tree-adapter": "^5.1.1",
+        "yargs": "^15.0.0"
+      },
+      "dependencies": {
+        "chalk": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+          "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+          "requires": {
+            "ansi-styles": "^4.1.0",
+            "supports-color": "^7.1.0"
+          }
+        },
+        "yargs": {
+          "version": "15.3.1",
+          "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz",
+          "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==",
+          "requires": {
+            "cliui": "^6.0.0",
+            "decamelize": "^1.2.0",
+            "find-up": "^4.1.0",
+            "get-caller-file": "^2.0.1",
+            "require-directory": "^2.1.1",
+            "require-main-filename": "^2.0.0",
+            "set-blocking": "^2.0.0",
+            "string-width": "^4.2.0",
+            "which-module": "^2.0.0",
+            "y18n": "^4.0.0",
+            "yargs-parser": "^18.1.1"
+          }
+        }
+      }
+    },
+    "cliui": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz",
+      "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==",
+      "requires": {
+        "string-width": "^4.2.0",
+        "strip-ansi": "^6.0.0",
+        "wrap-ansi": "^6.2.0"
+      }
+    },
+    "color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "requires": {
+        "color-name": "~1.1.4"
+      }
+    },
+    "color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
+    },
+    "concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
+    },
+    "create-hash": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+      "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+      "requires": {
+        "cipher-base": "^1.0.1",
+        "inherits": "^2.0.1",
+        "md5.js": "^1.3.4",
+        "ripemd160": "^2.0.1",
+        "sha.js": "^2.4.0"
+      }
+    },
+    "create-hmac": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+      "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+      "requires": {
+        "cipher-base": "^1.0.3",
+        "create-hash": "^1.1.0",
+        "inherits": "^2.0.1",
+        "ripemd160": "^2.0.0",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "cuint": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/cuint/-/cuint-0.2.2.tgz",
+      "integrity": "sha1-QICG1AlVDCYxFVYZ6fp7ytw7mRs="
+    },
+    "d": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz",
+      "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==",
+      "requires": {
+        "es5-ext": "^0.10.50",
+        "type": "^1.0.1"
+      }
+    },
+    "date-format": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz",
+      "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w=="
+    },
+    "debug": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
+      "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
+      "requires": {
+        "ms": "^2.1.1"
+      }
+    },
+    "decamelize": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+      "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+    },
+    "dotenv": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
+      "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w=="
+    },
+    "drbg.js": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz",
+      "integrity": "sha1-Pja2xCs3BDgjzbwzLVjzHiRFSAs=",
+      "requires": {
+        "browserify-aes": "^1.0.6",
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4"
+      }
+    },
+    "elliptic": {
+      "version": "6.5.2",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz",
+      "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==",
+      "requires": {
+        "bn.js": "^4.4.0",
+        "brorand": "^1.0.1",
+        "hash.js": "^1.0.0",
+        "hmac-drbg": "^1.0.0",
+        "inherits": "^2.0.1",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.0"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.8",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+          "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+        }
+      }
+    },
+    "emoji-regex": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+      "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
+    },
+    "encoding": {
+      "version": "0.1.12",
+      "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
+      "integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
+      "requires": {
+        "iconv-lite": "~0.4.13"
+      }
+    },
+    "es5-ext": {
+      "version": "0.10.53",
+      "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz",
+      "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==",
+      "requires": {
+        "es6-iterator": "~2.0.3",
+        "es6-symbol": "~3.1.3",
+        "next-tick": "~1.0.0"
+      },
+      "dependencies": {
+        "next-tick": {
+          "version": "1.0.0",
+          "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz",
+          "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw="
+        }
+      }
+    },
+    "es6-iterator": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz",
+      "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.35",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "es6-symbol": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz",
+      "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==",
+      "requires": {
+        "d": "^1.0.1",
+        "ext": "^1.1.2"
+      }
+    },
+    "es6-weak-map": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz",
+      "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.46",
+        "es6-iterator": "^2.0.3",
+        "es6-symbol": "^3.1.1"
+      }
+    },
+    "escape-string-regexp": {
+      "version": "1.0.5",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+      "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+    },
+    "esprima": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+    },
+    "event-emitter": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz",
+      "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=",
+      "requires": {
+        "d": "1",
+        "es5-ext": "~0.10.14"
+      }
+    },
+    "eventemitter3": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
+      "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ=="
+    },
+    "evp_bytestokey": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+      "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+      "requires": {
+        "md5.js": "^1.3.4",
+        "safe-buffer": "^5.1.1"
+      }
+    },
+    "ext": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz",
+      "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==",
+      "requires": {
+        "type": "^2.0.0"
+      },
+      "dependencies": {
+        "type": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz",
+          "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow=="
+        }
+      }
+    },
+    "fast-deep-equal": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz",
+      "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA=="
+    },
+    "fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
+    },
+    "figlet": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/figlet/-/figlet-1.4.0.tgz",
+      "integrity": "sha512-CxxIjEKHlqGosgXaIA+sikGDdV6KZOOlzPJnYuPgQlOSHZP5h9WIghYI30fyXnwEVeSH7Hedy72gC6zJrFC+SQ=="
+    },
+    "file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
+    },
+    "find-up": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
+      "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
+      "requires": {
+        "locate-path": "^5.0.0",
+        "path-exists": "^4.0.0"
+      }
+    },
+    "flatted": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
+      "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA=="
+    },
+    "fs-extra": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz",
+      "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==",
+      "requires": {
+        "graceful-fs": "^4.2.0",
+        "jsonfile": "^4.0.0",
+        "universalify": "^0.1.0"
+      }
+    },
+    "fs.realpath": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+      "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+    },
+    "get-caller-file": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+      "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="
+    },
+    "glob": {
+      "version": "7.1.6",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
+      "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
+      "requires": {
+        "fs.realpath": "^1.0.0",
+        "inflight": "^1.0.4",
+        "inherits": "2",
+        "minimatch": "^3.0.4",
+        "once": "^1.3.0",
+        "path-is-absolute": "^1.0.0"
+      }
+    },
+    "graceful-fs": {
+      "version": "4.2.4",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz",
+      "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw=="
+    },
+    "has-ansi": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+      "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+      "requires": {
+        "ansi-regex": "^2.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+        }
+      }
+    },
+    "has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
+    },
+    "hash-base": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz",
+      "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==",
+      "requires": {
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.6.0",
+        "safe-buffer": "^5.2.0"
+      }
+    },
+    "hash.js": {
+      "version": "1.1.7",
+      "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+      "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+      "requires": {
+        "inherits": "^2.0.3",
+        "minimalistic-assert": "^1.0.1"
+      }
+    },
+    "highlight.js": {
+      "version": "9.18.1",
+      "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.1.tgz",
+      "integrity": "sha512-OrVKYz70LHsnCgmbXctv/bfuvntIKDz177h0Co37DQ5jamGZLVmoCVMtjMtNZY3X9DrCcKfklHPNeA0uPZhSJg=="
+    },
+    "hmac-drbg": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+      "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+      "requires": {
+        "hash.js": "^1.0.3",
+        "minimalistic-assert": "^1.0.0",
+        "minimalistic-crypto-utils": "^1.0.1"
+      }
+    },
+    "iconv-lite": {
+      "version": "0.4.24",
+      "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+      "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+      "requires": {
+        "safer-buffer": ">= 2.1.2 < 3"
+      }
+    },
+    "ieee754": {
+      "version": "1.1.13",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz",
+      "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
+    },
+    "inflight": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+      "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+      "requires": {
+        "once": "^1.3.0",
+        "wrappy": "1"
+      }
+    },
+    "inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+    },
+    "ip-regex": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.1.0.tgz",
+      "integrity": "sha512-pKnZpbgCTfH/1NLIlOduP/V+WRXzC2MOz3Qo8xmxk8C5GudJLgK5QyLVXOSWy3ParAH7Eemurl3xjv/WXYFvMA=="
+    },
+    "is-buffer": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.4.tgz",
+      "integrity": "sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A=="
+    },
+    "is-fullwidth-code-point": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+      "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="
+    },
+    "is-promise": {
+      "version": "2.2.2",
+      "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+      "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ=="
+    },
+    "is-stream": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+      "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+    },
+    "is-typedarray": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+      "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+    },
+    "isomorphic-fetch": {
+      "version": "2.2.1",
+      "resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
+      "integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
+      "requires": {
+        "node-fetch": "^1.0.1",
+        "whatwg-fetch": ">=0.10.0"
+      }
+    },
+    "js-sha3": {
+      "version": "0.8.0",
+      "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz",
+      "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q=="
+    },
+    "js-yaml": {
+      "version": "3.13.1",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+      "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+      "requires": {
+        "argparse": "^1.0.7",
+        "esprima": "^4.0.0"
+      }
+    },
+    "json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
+    },
+    "jsonfile": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+      "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+      "requires": {
+        "graceful-fs": "^4.1.6"
+      }
+    },
+    "locate-path": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
+      "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
+      "requires": {
+        "p-locate": "^4.1.0"
+      }
+    },
+    "log4js": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.2.1.tgz",
+      "integrity": "sha512-7n+Oqxxz7VcQJhIlqhcYZBTpbcQ7XsR0MUIfJkx/n3VUjkAS4iUr+4UJlhxf28RvP9PMGQXbgTUhLApnu0XXgA==",
+      "requires": {
+        "date-format": "^3.0.0",
+        "debug": "^4.1.1",
+        "flatted": "^2.0.1",
+        "rfdc": "^1.1.4",
+        "streamroller": "^2.2.4"
+      }
+    },
+    "lru-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz",
+      "integrity": "sha1-Jzi9nw089PhEkMVzbEhpmsYyzaM=",
+      "requires": {
+        "es5-ext": "~0.10.2"
+      }
+    },
+    "md5.js": {
+      "version": "1.3.5",
+      "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+      "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.1.2"
+      }
+    },
+    "memoizee": {
+      "version": "0.4.14",
+      "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.14.tgz",
+      "integrity": "sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==",
+      "requires": {
+        "d": "1",
+        "es5-ext": "^0.10.45",
+        "es6-weak-map": "^2.0.2",
+        "event-emitter": "^0.3.5",
+        "is-promise": "^2.1",
+        "lru-queue": "0.1",
+        "next-tick": "1",
+        "timers-ext": "^0.1.5"
+      }
+    },
+    "minimalistic-assert": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+      "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+    },
+    "minimalistic-crypto-utils": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+      "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+    },
+    "minimatch": {
+      "version": "3.0.4",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+      "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+      "requires": {
+        "brace-expansion": "^1.1.7"
+      }
+    },
+    "minimist": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
+      "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
+    },
+    "mkdirp": {
+      "version": "0.5.5",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
+      "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==",
+      "requires": {
+        "minimist": "^1.2.5"
+      }
+    },
+    "moment": {
+      "version": "2.25.3",
+      "resolved": "https://registry.npmjs.org/moment/-/moment-2.25.3.tgz",
+      "integrity": "sha512-PuYv0PHxZvzc15Sp8ybUCoQ+xpyPWvjOuK72a5ovzp2LI32rJXOiIfyoFoYvG3s6EwwrdkMyWuRiEHSZRLJNdg=="
+    },
+    "ms": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+      "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
+    },
+    "mz": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
+      "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==",
+      "requires": {
+        "any-promise": "^1.0.0",
+        "object-assign": "^4.0.1",
+        "thenify-all": "^1.0.0"
+      }
+    },
+    "nan": {
+      "version": "2.14.1",
+      "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.1.tgz",
+      "integrity": "sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw=="
+    },
+    "next-tick": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz",
+      "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
+    },
+    "node-fetch": {
+      "version": "1.7.3",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
+      "integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
+      "requires": {
+        "encoding": "^0.1.11",
+        "is-stream": "^1.0.1"
+      }
+    },
+    "object-assign": {
+      "version": "4.1.1",
+      "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+      "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+    },
+    "once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+      "requires": {
+        "wrappy": "1"
+      }
+    },
+    "p-limit": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+      "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
+      "requires": {
+        "p-try": "^2.0.0"
+      }
+    },
+    "p-locate": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
+      "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
+      "requires": {
+        "p-limit": "^2.2.0"
+      }
+    },
+    "p-try": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
+      "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
+    },
+    "parent-require": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/parent-require/-/parent-require-1.0.0.tgz",
+      "integrity": "sha1-dGoWdjgIOoYLDu9nMssn7UbDKXc="
+    },
+    "parse5": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz",
+      "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="
+    },
+    "parse5-htmlparser2-tree-adapter": {
+      "version": "5.1.1",
+      "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz",
+      "integrity": "sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw==",
+      "requires": {
+        "parse5": "^5.1.1"
+      }
+    },
+    "path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="
+    },
+    "path-is-absolute": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+      "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+    },
+    "pbkdf2": {
+      "version": "3.0.17",
+      "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
+      "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+      "requires": {
+        "create-hash": "^1.1.2",
+        "create-hmac": "^1.1.4",
+        "ripemd160": "^2.0.1",
+        "safe-buffer": "^5.0.1",
+        "sha.js": "^2.4.8"
+      }
+    },
+    "punycode": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
+    },
+    "randombytes": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+      "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+      "requires": {
+        "safe-buffer": "^5.1.0"
+      }
+    },
+    "readable-stream": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
+      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
+      "requires": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      }
+    },
+    "reflect-metadata": {
+      "version": "0.1.13",
+      "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz",
+      "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg=="
+    },
+    "regenerator-runtime": {
+      "version": "0.13.5",
+      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz",
+      "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA=="
+    },
+    "replace-ext": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz",
+      "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs="
+    },
+    "require-directory": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+      "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I="
+    },
+    "require-main-filename": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
+      "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="
+    },
+    "rfdc": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.1.4.tgz",
+      "integrity": "sha512-5C9HXdzK8EAqN7JDif30jqsBzavB7wLpaubisuQIGHWf2gUXSpzy6ArX/+Da8RjFpagWsCn+pIgxTMAmKw9Zug=="
+    },
+    "ripemd160": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+      "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+      "requires": {
+        "hash-base": "^3.0.0",
+        "inherits": "^2.0.1"
+      }
+    },
+    "rxjs": {
+      "version": "6.5.5",
+      "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.5.tgz",
+      "integrity": "sha512-WfQI+1gohdf0Dai/Bbmk5L5ItH5tYqm3ki2c5GdWhKjalzjg93N3avFjVStyZZz+A2Em+ZxKH5bNghw9UeylGQ==",
+      "requires": {
+        "tslib": "^1.9.0"
+      }
+    },
+    "safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="
+    },
+    "safer-buffer": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+      "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+    },
+    "sax": {
+      "version": "1.2.4",
+      "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz",
+      "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
+    },
+    "secp256k1": {
+      "version": "3.8.0",
+      "resolved": "https://registry.npmjs.org/secp256k1/-/secp256k1-3.8.0.tgz",
+      "integrity": "sha512-k5ke5avRZbtl9Tqx/SA7CbY3NF6Ro+Sj9cZxezFzuBlLDmyqPiL8hJJ+EmzD8Ig4LUDByHJ3/iPOVoRixs/hmw==",
+      "requires": {
+        "bindings": "^1.5.0",
+        "bip66": "^1.1.5",
+        "bn.js": "^4.11.8",
+        "create-hash": "^1.2.0",
+        "drbg.js": "^1.0.1",
+        "elliptic": "^6.5.2",
+        "nan": "^2.14.0",
+        "safe-buffer": "^5.1.2"
+      },
+      "dependencies": {
+        "bn.js": {
+          "version": "4.11.8",
+          "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+          "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
+        }
+      }
+    },
+    "set-blocking": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
+      "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc="
+    },
+    "sha.js": {
+      "version": "2.4.11",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "requires": {
+        "inherits": "^2.0.1",
+        "safe-buffer": "^5.0.1"
+      }
+    },
+    "sprintf-js": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+    },
+    "streamroller": {
+      "version": "2.2.4",
+      "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz",
+      "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==",
+      "requires": {
+        "date-format": "^2.1.0",
+        "debug": "^4.1.1",
+        "fs-extra": "^8.1.0"
+      },
+      "dependencies": {
+        "date-format": {
+          "version": "2.1.0",
+          "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz",
+          "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA=="
+        }
+      }
+    },
+    "string-width": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz",
+      "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==",
+      "requires": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.0"
+      }
+    },
+    "string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "requires": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "strip-ansi": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz",
+      "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==",
+      "requires": {
+        "ansi-regex": "^5.0.0"
+      }
+    },
+    "supports-color": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
+      "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
+      "requires": {
+        "has-flag": "^4.0.0"
+      }
+    },
+    "thenify": {
+      "version": "3.3.0",
+      "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz",
+      "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=",
+      "requires": {
+        "any-promise": "^1.0.0"
+      }
+    },
+    "thenify-all": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz",
+      "integrity": "sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY=",
+      "requires": {
+        "thenify": ">= 3.1.0 < 4"
+      }
+    },
+    "timers-ext": {
+      "version": "0.1.7",
+      "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.7.tgz",
+      "integrity": "sha512-b85NUNzTSdodShTIbky6ZF02e8STtVVfD+fu4aXXShEELpozH+bCpJLYMPZbsABN2wDH7fJpqIoXxJpzbf0NqQ==",
+      "requires": {
+        "es5-ext": "~0.10.46",
+        "next-tick": "1"
+      }
+    },
+    "tslib": {
+      "version": "1.13.0",
+      "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz",
+      "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q=="
+    },
+    "tweetnacl": {
+      "version": "1.0.3",
+      "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
+      "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw=="
+    },
+    "type": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz",
+      "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg=="
+    },
+    "typedarray-to-buffer": {
+      "version": "3.1.5",
+      "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+      "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+      "requires": {
+        "is-typedarray": "^1.0.0"
+      }
+    },
+    "typeorm": {
+      "version": "0.2.24",
+      "resolved": "https://registry.npmjs.org/typeorm/-/typeorm-0.2.24.tgz",
+      "integrity": "sha512-L9tQv6nNLRyh+gex/qc8/CyLs8u0kXKqk1OjYGF13k/KOg6N2oibwkuGgv0FuoTGYx2ta2NmqvuMUAMrHIY5ew==",
+      "requires": {
+        "app-root-path": "^3.0.0",
+        "buffer": "^5.1.0",
+        "chalk": "^2.4.2",
+        "cli-highlight": "^2.0.0",
+        "debug": "^4.1.1",
+        "dotenv": "^6.2.0",
+        "glob": "^7.1.2",
+        "js-yaml": "^3.13.1",
+        "mkdirp": "^0.5.1",
+        "reflect-metadata": "^0.1.13",
+        "sha.js": "^2.4.11",
+        "tslib": "^1.9.0",
+        "xml2js": "^0.4.17",
+        "yargonaut": "^1.1.2",
+        "yargs": "^13.2.1"
+      },
+      "dependencies": {
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "chalk": {
+          "version": "2.4.2",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+          "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+          "requires": {
+            "ansi-styles": "^3.2.1",
+            "escape-string-regexp": "^1.0.5",
+            "supports-color": "^5.3.0"
+          }
+        },
+        "color-convert": {
+          "version": "1.9.3",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+          "requires": {
+            "color-name": "1.1.3"
+          }
+        },
+        "color-name": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+          "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+        },
+        "has-flag": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+          "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+        },
+        "supports-color": {
+          "version": "5.5.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+          "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+          "requires": {
+            "has-flag": "^3.0.0"
+          }
+        }
+      }
+    },
+    "unist-util-stringify-position": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz",
+      "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==",
+      "requires": {
+        "@types/unist": "^2.0.2"
+      }
+    },
+    "universalify": {
+      "version": "0.1.2",
+      "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+      "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+    },
+    "unorm": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz",
+      "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA=="
+    },
+    "uri-js": {
+      "version": "4.2.2",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz",
+      "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==",
+      "requires": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+    },
+    "vfile": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.1.0.tgz",
+      "integrity": "sha512-BaTPalregj++64xbGK6uIlsurN3BCRNM/P2Pg8HezlGzKd1O9PrwIac6bd9Pdx2uTb0QHoioZ+rXKolbVXEgJg==",
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "is-buffer": "^2.0.0",
+        "replace-ext": "1.0.0",
+        "unist-util-stringify-position": "^2.0.0",
+        "vfile-message": "^2.0.0"
+      }
+    },
+    "vfile-message": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz",
+      "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==",
+      "requires": {
+        "@types/unist": "^2.0.0",
+        "unist-util-stringify-position": "^2.0.0"
+      }
+    },
+    "websocket": {
+      "version": "1.0.31",
+      "resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.31.tgz",
+      "integrity": "sha512-VAouplvGKPiKFDTeCCO65vYHsyay8DqoBSlzIO3fayrfOgU94lQN5a1uWVnFrMLceTJw/+fQXR5PGbUVRaHshQ==",
+      "requires": {
+        "debug": "^2.2.0",
+        "es5-ext": "^0.10.50",
+        "nan": "^2.14.0",
+        "typedarray-to-buffer": "^3.1.5",
+        "yaeti": "^0.0.6"
+      },
+      "dependencies": {
+        "debug": {
+          "version": "2.6.9",
+          "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+          "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+          "requires": {
+            "ms": "2.0.0"
+          }
+        },
+        "ms": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+          "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+        }
+      }
+    },
+    "whatwg-fetch": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
+      "integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
+    },
+    "which-module": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz",
+      "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho="
+    },
+    "wrap-ansi": {
+      "version": "6.2.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+      "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+      "requires": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      }
+    },
+    "wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+    },
+    "xml2js": {
+      "version": "0.4.23",
+      "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
+      "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
+      "requires": {
+        "sax": ">=0.6.0",
+        "xmlbuilder": "~11.0.0"
+      }
+    },
+    "xmlbuilder": {
+      "version": "11.0.1",
+      "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
+      "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
+    },
+    "xxhashjs": {
+      "version": "0.2.2",
+      "resolved": "https://registry.npmjs.org/xxhashjs/-/xxhashjs-0.2.2.tgz",
+      "integrity": "sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw==",
+      "requires": {
+        "cuint": "^0.2.2"
+      }
+    },
+    "y18n": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz",
+      "integrity": "sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w=="
+    },
+    "yaeti": {
+      "version": "0.0.6",
+      "resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
+      "integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc="
+    },
+    "yargonaut": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/yargonaut/-/yargonaut-1.1.4.tgz",
+      "integrity": "sha512-rHgFmbgXAAzl+1nngqOcwEljqHGG9uUZoPjsdZEs1w5JW9RXYzrSvH/u70C1JE5qFi0qjsdhnUX/dJRpWqitSA==",
+      "requires": {
+        "chalk": "^1.1.1",
+        "figlet": "^1.1.1",
+        "parent-require": "^1.0.0"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "2.1.1",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+          "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+        },
+        "ansi-styles": {
+          "version": "2.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+          "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+        },
+        "chalk": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+          "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+          "requires": {
+            "ansi-styles": "^2.2.1",
+            "escape-string-regexp": "^1.0.2",
+            "has-ansi": "^2.0.0",
+            "strip-ansi": "^3.0.0",
+            "supports-color": "^2.0.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "3.0.1",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+          "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+          "requires": {
+            "ansi-regex": "^2.0.0"
+          }
+        },
+        "supports-color": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+          "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+        }
+      }
+    },
+    "yargs": {
+      "version": "13.3.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz",
+      "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==",
+      "requires": {
+        "cliui": "^5.0.0",
+        "find-up": "^3.0.0",
+        "get-caller-file": "^2.0.1",
+        "require-directory": "^2.1.1",
+        "require-main-filename": "^2.0.0",
+        "set-blocking": "^2.0.0",
+        "string-width": "^3.0.0",
+        "which-module": "^2.0.0",
+        "y18n": "^4.0.0",
+        "yargs-parser": "^13.1.2"
+      },
+      "dependencies": {
+        "ansi-regex": {
+          "version": "4.1.0",
+          "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
+          "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg=="
+        },
+        "ansi-styles": {
+          "version": "3.2.1",
+          "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+          "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+          "requires": {
+            "color-convert": "^1.9.0"
+          }
+        },
+        "cliui": {
+          "version": "5.0.0",
+          "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
+          "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
+          "requires": {
+            "string-width": "^3.1.0",
+            "strip-ansi": "^5.2.0",
+            "wrap-ansi": "^5.1.0"
+          }
+        },
+        "color-convert": {
+          "version": "1.9.3",
+          "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+          "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+          "requires": {
+            "color-name": "1.1.3"
+          }
+        },
+        "color-name": {
+          "version": "1.1.3",
+          "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+          "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+        },
+        "emoji-regex": {
+          "version": "7.0.3",
+          "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz",
+          "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA=="
+        },
+        "find-up": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz",
+          "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==",
+          "requires": {
+            "locate-path": "^3.0.0"
+          }
+        },
+        "is-fullwidth-code-point": {
+          "version": "2.0.0",
+          "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+          "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
+        },
+        "locate-path": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz",
+          "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==",
+          "requires": {
+            "p-locate": "^3.0.0",
+            "path-exists": "^3.0.0"
+          }
+        },
+        "p-locate": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz",
+          "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==",
+          "requires": {
+            "p-limit": "^2.0.0"
+          }
+        },
+        "path-exists": {
+          "version": "3.0.0",
+          "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+          "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+        },
+        "string-width": {
+          "version": "3.1.0",
+          "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
+          "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
+          "requires": {
+            "emoji-regex": "^7.0.1",
+            "is-fullwidth-code-point": "^2.0.0",
+            "strip-ansi": "^5.1.0"
+          }
+        },
+        "strip-ansi": {
+          "version": "5.2.0",
+          "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
+          "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
+          "requires": {
+            "ansi-regex": "^4.1.0"
+          }
+        },
+        "wrap-ansi": {
+          "version": "5.1.0",
+          "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
+          "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
+          "requires": {
+            "ansi-styles": "^3.2.0",
+            "string-width": "^3.0.0",
+            "strip-ansi": "^5.0.0"
+          }
+        },
+        "yargs-parser": {
+          "version": "13.1.2",
+          "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
+          "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
+          "requires": {
+            "camelcase": "^5.0.0",
+            "decamelize": "^1.2.0"
+          }
+        }
+      }
+    },
+    "yargs-parser": {
+      "version": "18.1.3",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz",
+      "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==",
+      "requires": {
+        "camelcase": "^5.0.0",
+        "decamelize": "^1.2.0"
+      }
+    }
+  }
+}

+ 19 - 0
query-node/joystream-query-node/bootstrap/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "mappings",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.ts",
+  "scripts": {
+    "build": "tsc --build tsconfig.json",
+    "postinstall": "tsc --build tsconfig.json"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "@joystream/types": "^0.8.0",
+    "@polkadot/api": "^1.14.1",
+    "@polkadot/types": "^1.14.1",
+    "log4js": "^6.2.1",
+    "typeorm": "^0.2.24"
+  }
+}

+ 18 - 0
query-node/joystream-query-node/bootstrap/tsconfig.json

@@ -0,0 +1,18 @@
+{
+	"compilerOptions": {
+		"declaration": true,
+		"importHelpers": true,
+		"module": "commonjs",
+		"outDir": "lib",
+		"rootDir": "./..",
+		"strict": true,
+		"target": "es2017",
+		"experimentalDecorators": true,
+		"emitDecoratorMetadata": true,
+		"skipLibCheck": true,
+		"sourceMap": true,
+		"inlineSources": false
+	},
+	"include": ["./*.ts"],
+	"exclude": ["node_modules"]
+}

+ 1 - 0
query-node/joystream-query-node/mappings/index.ts

@@ -0,0 +1 @@
+export * from './members';

+ 82 - 0
query-node/joystream-query-node/mappings/members.ts

@@ -0,0 +1,82 @@
+import * as assert from 'assert';
+import { CheckedUserInfo } from '@joystream/types/lib/members';
+
+import { Member } from '../generated/graphql-server/src/modules/member/member.model';
+import { DB, SubstrateEvent } from '../generated/indexer';
+
+export async function handleMemberRegistered(db: DB, event: SubstrateEvent) {
+  const { AccountId, MemberId } = event.event_params;
+
+  // Not safe type casting!
+  const userInfo = (event.extrinsic?.args[1].toJSON() as unknown) as CheckedUserInfo;
+
+  let member = new Member();
+  member.registeredAtBlock = event.block_number.toString();
+  member.memberId = MemberId.toString();
+  member.rootAccount = Buffer.from(AccountId);
+  member.controllerAccount = Buffer.from(AccountId);
+  member.handle = userInfo.handle.toString();
+  member.avatarUri = userInfo.avatar_uri.toString();
+  member.about = userInfo.about.toString();
+
+  db.save<Member>(member);
+}
+
+export async function handleMemberUpdatedAboutText(db: DB, event: SubstrateEvent) {
+  const { MemberId } = event.event_params;
+  const member = await db.get(Member, { where: { memberId: MemberId.toString() } });
+
+  assert(member);
+
+  // Not safe type casting!
+  const userInfo = (event.extrinsic?.args[1].toJSON() as unknown) as CheckedUserInfo;
+  member.about = userInfo.about.toString();
+
+  db.save<Member>(member);
+}
+
+export async function handleMemberUpdatedAvatar(db: DB, event: SubstrateEvent) {
+  const { MemberId } = event.event_params;
+  const member = await db.get(Member, { where: { memberId: MemberId.toString() } });
+
+  assert(member);
+
+  // Not safe type casting!
+  const userInfo = (event.extrinsic?.args[1].toJSON() as unknown) as CheckedUserInfo;
+  member.avatarUri = userInfo.avatar_uri.toString();
+
+  db.save<Member>(member);
+}
+
+export async function handleMemberUpdatedHandle(db: DB, event: SubstrateEvent) {
+  const { MemberId } = event.event_params;
+  const member = await db.get(Member, { where: { memberId: MemberId.toString() } });
+
+  assert(member);
+
+  // Not safe type casting!
+  const userInfo = (event.extrinsic?.args[1].toJSON() as unknown) as CheckedUserInfo;
+  member.handle = userInfo.handle.toString();
+
+  db.save<Member>(member);
+}
+
+export async function handleMemberSetRootAccount(db: DB, event: SubstrateEvent) {
+  const { MemberId, AccountId } = event.event_params;
+  const member = await db.get(Member, { where: { memberId: MemberId.toString() } });
+
+  assert(member);
+
+  member.rootAccount = Buffer.from(AccountId);
+  db.save<Member>(member);
+}
+
+export async function handleMemberSetControllerAccount(db: DB, event: SubstrateEvent) {
+  const { MemberId, AccountId } = event.event_params;
+  const member = await db.get(Member, { where: { memberId: MemberId.toString() } });
+
+  assert(member);
+
+  member.controllerAccount = Buffer.from(AccountId);
+  db.save<Member>(member);
+}

+ 21 - 0
query-node/joystream-query-node/schema.graphql

@@ -0,0 +1,21 @@
+type Member @entity {
+  memberId: BigInt!
+
+  # The unique handle chosen by member
+  handle: String @fulltext(query: "handles")
+
+  # A Url to member's Avatar image
+  avatarUri: String
+
+  # Short text chosen by member to share information about themselves
+  about: String
+
+  # Blocknumber when member was registered
+  registeredAtBlock: BigInt!
+
+  # Member's controller account id.
+  controllerAccount: Bytes!
+
+  # Member's root account id
+  rootAccount: Bytes!
+}

+ 14 - 0
query-node/joystream-query-node/scripts/reset-dev.sh

@@ -0,0 +1,14 @@
+#!/bin/sh
+set -x
+## restart postgres
+docker container ps | grep postgres | awk {'print $1'} | xargs -I {} docker restart {}
+## drop and create db
+cd ./../generated/graphql-server && yarn config:dev && yarn db:drop
+## wipe out indexer and graphql-server
+cd ./../../ && rm -rf ./generated/indexer && rm -rf ./generated/graphql-server
+## create both
+cli codegen
+## fire up and bootstrap the indexer
+cd ./generated/indexer && yarn start:dev &
+## start the graphql server
+cd ./../graphql-server && yarn start:dev &

+ 7 - 0
query-node/joystream-query-node/scripts/run-dev.sh

@@ -0,0 +1,7 @@
+#!/bin/sh
+## create both
+cd ./../ && cli codegen
+## fire up and bootstrap the indexer
+cd ./generated/indexer && yarn start:dev &
+## start the graphql server
+cd ./generated/graphql-server && yarn start:dev &

+ 15 - 0
query-node/substrate-query-framework/README.md

@@ -0,0 +1,15 @@
+# Substrate Query Node
+
+This repository is a package that you can use to build a query node. It contains source code Block Indexer and CLI tool.
+
+## Data Store
+
+Block indexer store event data into a postgresql database. You can run the database with docker.
+
+```
+$ docker-compose up -d
+```
+
+## CLI Tool
+
+CLI generates a GraphQL Server and a Block Indexer for Substrate based chains. How to use CLI [CLI](./cli/README.md)

+ 1 - 0
query-node/substrate-query-framework/cli/.eslintignore

@@ -0,0 +1 @@
+/lib

+ 15 - 0
query-node/substrate-query-framework/cli/.eslintrc.js

@@ -0,0 +1,15 @@
+module.exports = {
+    extends: [
+		"eslint:recommended",
+		"plugin:@typescript-eslint/eslint-recommended",
+		"plugin:@typescript-eslint/recommended",
+		"plugin:@typescript-eslint/recommended-requiring-type-checking"
+	],
+	parser: "@typescript-eslint/parser",
+	parserOptions: {
+        project: './tsconfig.json',
+        tsconfigRootDir: __dirname,
+        //debugLevel: true
+	},
+	plugins: ["@typescript-eslint"]
+}

+ 9 - 0
query-node/substrate-query-framework/cli/.gitignore

@@ -0,0 +1,9 @@
+*-debug.log
+*-error.log
+/.nyc_output
+/dist
+/lib
+/package-lock.json
+/tmp
+node_modules
+.editorconfig

+ 80 - 0
query-node/substrate-query-framework/cli/README.md

@@ -0,0 +1,80 @@
+# cli
+
+Create query node for substrate based chains.
+
+USAGE
+
+```
+$ cli [COMMAND]
+```
+
+COMMANDS
+
+```
+codegen   Generate graphql server and block indexer ready to run
+```
+
+## Using CLI
+
+Start use CLI by navigate `query-node/substrate-query-node/cli` directory then:
+
+```
+$ ./bin/run [COMMAND]
+```
+
+or
+
+```
+$ yarn link
+$ cli [COMMAND]
+```
+
+## Prerequisites
+
+CLI generates a grapqh server and a substrate block indexer.
+
+- Graphql server uses warthog underneath and generate `model/resolver/service` from the schema you will define in the current working directory (cwd). Before starting code generation make sure you have a file named `schema.json` inside the cwd and at least one type defination inside it. Later we will see an example how to add a new type defination to our schema.
+- Block indexer consumes produced blocks from a substrate based chain. Before running code generation make sure you have `.env` file inside the cwd, and `mappings` directory with `index.ts`. Which must have variables below with appropriate values:
+
+```
+WS_PROVIDER_ENDPOINT_URI=<provider>    # e.g ws://localhost:9944
+TYPE_REGISTER_PACKAGE_NAME=<packname>  # name of the package to import TYPE_REGISTER_FUNCTION
+TYPE_REGISTER_FUNCTION=<register_type> # name of the function that will called for type registration
+```
+
+Typical your cwd should look like this:
+
+```
+├── .env
+├── mappings
+│   ├── index.ts
+└── schema.json
+```
+
+## Getting Started
+
+Run cli inside the directory where you put `mappings`, `.env`, `schema.json`.
+
+### Generate Graphql Server and Block Indexer
+
+Cli create a folder named `generated` and put everthing inside it.
+
+```
+$ cli codegen
+```
+
+Start graphql server:
+
+```
+$ cd generated/graphql-server
+$ yarn start:dev
+```
+
+Start block indexer:
+
+```
+$ cd generated/indexer
+$ yarn start
+```
+
+Looking for an existing example check out: [joystream-query-node](../../joystream-query-node)

+ 5 - 0
query-node/substrate-query-framework/cli/bin/run

@@ -0,0 +1,5 @@
+#!/usr/bin/env node
+
+require('@oclif/command').run()
+.then(require('@oclif/command/flush'))
+.catch(require('@oclif/errors/handle'))

+ 3 - 0
query-node/substrate-query-framework/cli/bin/run.cmd

@@ -0,0 +1,3 @@
+@echo off
+
+node "%~dp0\run" %*

+ 95 - 0
query-node/substrate-query-framework/cli/package.json

@@ -0,0 +1,95 @@
+{
+  "name": "cli",
+  "description": "Query node cli",
+  "version": "0.0.0",
+  "author": "metmirr @metmirr",
+  "bin": {
+    "cli": "./bin/run"
+  },
+  "bugs": "https://github.com/joystream/joystream/joystream-query-node/cli/issues",
+  "dependencies": {
+    "@oclif/command": "^1.5.20",
+    "@oclif/config": "^1",
+    "@oclif/plugin-help": "^2",
+    "@types/fs-extra": "^8.1.0",
+    "@types/graphql": "^14.5.0",
+    "@types/mustache": "^4.0.1",
+    "@types/node": "^12.12.30",
+    "fs-extra": "^9.0.0",
+    "gluegun": "^4.3.1",
+    "graphql": "^15.0.0",
+    "lodash": "^4.17.15",
+    "mustache": "^4.0.1",
+    "tslib": "1.11.2",
+    "typeorm-model-generator": "^0.4.2",
+    "warthog": "^2.9"
+  },
+  "devDependencies": {
+    "@oclif/dev-cli": "^1",
+    "@oclif/test": "^1",
+    "@types/chai": "^4",
+    "@types/mocha": "^7.0.2",
+    "@types/temp": "^0.8.34",
+    "@types/tmp": "^0.2.0",
+    "@typescript-eslint/eslint-plugin": "^3.0.2",
+    "@typescript-eslint/parser": "^3.0.2",
+    "chai": "^4",
+    "eslint": "^7.1.0",
+    "globby": "^10",
+    "husky": "^4.2.5",
+    "jest": "^26.0.1",
+    "lint-staged": "^10.2.6",
+    "mocha": "^5",
+    "mocha-chai-snapshot": "^1.0.0",
+    "nyc": "^14",
+    "prettier": "^2.0.5",
+    "spawn-command": "^0.0.2-1",
+    "temp": "^0.9.1",
+    "ts-node": "^8",
+    "typescript": "^3.9.3"
+  },
+  "engines": {
+    "node": ">=8.0.0"
+  },
+  "files": [
+    "/bin",
+    "/lib",
+    "/npm-shrinkwrap.json",
+    "/oclif.manifest.json"
+  ],
+  "homepage": "https://github.com/joystream/joystream/joystream-query-node/cli",
+  "keywords": [
+    "oclif"
+  ],
+  "license": "MIT",
+  "main": "lib/index.js",
+  "oclif": {
+    "commands": "./lib/src/commands",
+    "bin": "cli",
+    "plugins": [
+      "@oclif/plugin-help"
+    ]
+  },
+  "repository": "joystream/joystream/joystream-query-node/cli",
+  "scripts": {
+    "build": "rm -rf lib && tsc --build tsconfig.json",
+    "postpack": "rm -f oclif.manifest.json",
+    "lint": "eslint . --cache --ext .ts --config .eslintrc.js",
+    "prepack": "rm -rf lib && tsc -b && oclif-dev manifest && oclif-dev readme",
+    "test": "nyc --extension .ts mocha --forbid-only \"test/**/*.test.ts\"",
+    "version": "oclif-dev readme && git add README.md"
+  },
+  "types": "lib/index.d.ts",
+  "resolutions": {
+    "tslib": "1.11.2"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "lint-staged",
+      "pre-push": "yarn test"
+    }
+  },
+  "lint-staged": {
+    "*.ts": "yarn lint"
+  }
+}

+ 139 - 0
query-node/substrate-query-framework/cli/src/commands/codegen.ts

@@ -0,0 +1,139 @@
+import * as path from 'path';
+import * as fs from 'fs-extra';
+import * as dotenv from 'dotenv';
+import * as Mustache from 'mustache';
+import { readFileSync, copyFileSync } from 'fs-extra';
+import { Command, flags } from '@oclif/command';
+import { execSync } from 'child_process';
+
+import { createDir, getTemplatePath, createFile } from '../utils/utils';
+import { formatWithPrettier } from '../helpers/formatter';
+import WarthogWrapper from '../helpers/WarthogWrapper';
+import { getTypeormConfig, getTypeormModelGeneratorConnectionConfig, createSavedEntityEventTable } from '../helpers/db';
+import Debug from "debug";
+
+const debug = Debug('qnode-cli:codegen');
+
+export default class Codegen extends Command {
+  static description = 'Code generator';
+  static generatedFolderName = 'generated';
+
+  static flags = {
+    schema: flags.string({ char: 's', description: 'Schema path', default: '../../schema.graphql' }),
+    // pass --no-indexer to skip indexer generation
+    indexer: flags.boolean({ char: 'i', allowNo: true, description: 'Generate indexer', default: true }),
+    // pass --no-graphql to skip graphql generation
+    graphql: flags.boolean({ char: 'g', allowNo: true, description: 'Generate GraphQL server', default: true }),
+    preview: flags.boolean({ char: 'p', allowNo: true, description: 'Generate GraphQL API preview', default: false }),
+  };
+
+  async run(): Promise<void> {
+    dotenv.config();
+
+    const { flags } = this.parse(Codegen);
+
+    const generatedFolderPath = path.resolve(process.cwd(), Codegen.generatedFolderName);
+    const isGeneratedFolderPathExists = fs.existsSync(generatedFolderPath);
+
+    createDir(generatedFolderPath);
+
+    // Change directory to generated
+    process.chdir(generatedFolderPath);
+
+    if (flags.preview) {
+      debug('Generating GraphQL API preview');
+      await this.generateAPIPreview(flags.schema, generatedFolderPath, isGeneratedFolderPathExists);
+      return;
+    }
+
+    // Create warthog graphql server
+    if (flags.graphql) {
+        debug("Generating GraphQL server");
+        await this.createGraphQLServer(flags.schema);
+    }
+    
+    // Create block indexer
+    if (flags.indexer) {
+        debug("Generating indexer");
+        await this.createBlockIndexer();
+    }
+    
+  }
+
+  async createGraphQLServer(schemaPath: string): Promise<void> {
+    const goBackDir = process.cwd();
+
+    const warthogProjectName = 'graphql-server';
+    const warthogProjectPath = path.resolve(goBackDir, warthogProjectName);
+
+    createDir(warthogProjectPath);
+
+    process.chdir(warthogProjectPath);
+    
+    const warthogWrapper = new WarthogWrapper(this, schemaPath);
+    await warthogWrapper.run();
+
+    process.chdir(goBackDir);
+  }
+
+  async createBlockIndexer(): Promise<void> {
+    // Take process where back at the end of the function execution
+    const goBackDir = process.cwd();
+
+    // Block indexer folder path
+    const indexerPath = path.resolve(goBackDir, 'indexer');
+
+    createDir(indexerPath);
+    process.chdir(indexerPath);
+
+    // Create index.ts file
+    let indexFileContent = readFileSync(getTemplatePath('index-builder-entry.mst'), 'utf8');
+    indexFileContent = Mustache.render(indexFileContent, {
+      packageName: process.env.TYPE_REGISTER_PACKAGE_NAME,
+      typeRegistrator: process.env.TYPE_REGISTER_FUNCTION
+    });
+    createFile(path.resolve('index.ts'), formatWithPrettier(indexFileContent));
+
+    // Create package.json
+    copyFileSync(getTemplatePath('indexer.package.json'), path.resolve(process.cwd(), 'package.json'));
+
+    // Create .env file for typeorm database connection
+    fs.writeFileSync('.env', getTypeormConfig());
+
+    // Create
+    copyFileSync(getTemplatePath('indexer.tsconfig.json'), path.resolve(process.cwd(), 'tsconfig.json'));
+
+    this.log('Installing dependendies for indexer...');
+    execSync('yarn install');
+    if (process.env.TYPE_REGISTER_PACKAGE_NAME) 
+        execSync(`yarn add ${process.env.TYPE_REGISTER_PACKAGE_NAME}`);
+    this.log('done...');
+
+    this.log('Generating typeorm db entities...');
+    await createSavedEntityEventTable();
+    execSync(getTypeormModelGeneratorConnectionConfig());
+    this.log('done...');
+
+    process.chdir(goBackDir);
+  }
+
+  async generateAPIPreview(schemaPath: string, generatedFolderPath: string, isExists: boolean): Promise<void> {
+    const warthogProjectPath = path.resolve(process.cwd(), 'api-preview');
+
+    createDir(warthogProjectPath);
+    process.chdir(warthogProjectPath);
+
+    await new WarthogWrapper(this, schemaPath).generateAPIPreview();
+
+    fs.copyFileSync(
+      path.resolve(warthogProjectPath, Codegen.generatedFolderName, 'schema.graphql'),
+      path.resolve('../../apipreview.graphql')
+    );
+    // if 'generated' folder was already there dont delete it otherwise delete
+    if (!isExists) {
+      debug('Removing unused files...');
+      fs.removeSync(generatedFolderPath);
+      this.log('Generated API Preview file -> apipreview.graphql');
+    }
+  }
+}

+ 21 - 0
query-node/substrate-query-framework/cli/src/commands/db.ts

@@ -0,0 +1,21 @@
+import { Command, flags } from '@oclif/command';
+
+import { resetLastProcessedEvent } from '../helpers/db';
+
+export default class DB extends Command {
+  static description = 'Typeorm commands';
+
+  static flags = {
+    reset: flags.boolean({ char: 'r', default: true, description: 'Reset last processed event to genesis' }),
+  };
+
+  async run(): Promise<void> {
+    const { flags } = this.parse(DB);
+
+    if (flags.reset) {
+      this.log('Resetting the last processed event...');
+      await resetLastProcessedEvent();
+      this.log('Done...');
+    }
+  }
+}

+ 153 - 0
query-node/substrate-query-framework/cli/src/generate/FTSQueryRenderer.ts

@@ -0,0 +1,153 @@
+import Mustache from 'mustache';
+import Debug from 'debug';
+import { FTSQuery, ObjectType } from '../model';
+import { upperFirst, lowerFirst, kebabCase } from 'lodash';
+import { snakeCase } from 'typeorm/util/StringUtils';
+import { GeneratorContext } from '../generate/SourcesGenerator';
+
+const debug = Debug('qnode-cli:model-generator');
+
+interface MustacheQuery {
+    entities: MustacheOrmEnitity[],
+    query: {
+        name: string,
+        typePrefix: string, // used to define types for inputs and outputs
+        viewName: string, // view name holding the union of the documents
+        language: string, 
+        documents: MustacheQueryDocument[], // all text fields in a table are grouped into documents
+        ts: number // migration timestamp
+    }
+}
+
+interface MustacheOrmEnitity {
+    type: string,
+    fieldName: string,
+    arrayName: string,
+    table: string, // SQL table the enitity is mapped to
+    model: string,  // warthog model name
+    last: boolean
+}
+
+interface MustacheQueryDocument {
+    tsvColumn: string, // generated column to be used for the ts_vector index
+    docColumn: string, // generated column where the concatenated fields as a doc are stored
+    index_name: string, // name of the ts_vector index
+    table: string, // SQL table the text fields belong to 
+    fields: MustacheQueryField[] // text fields to be grouped into a document
+    last: boolean // if its last in the list of documents
+}
+
+interface MustacheQueryField {
+    weight: string, // reserved; can be 'A', 'B', 'C' or 'D'. Always set to 'A' for now.
+    column: string,  // SQL column this field is mapped to
+    last: boolean // this field is need for joining, e.g. '<field> || <field> || <field>'
+}
+
+
+
+export class FTSQueryRenderer {
+    private _context: GeneratorContext = {};
+
+    constructor(context: GeneratorContext = {}) {
+        this._context = context;
+    }
+    
+    generate(mustacheTeplate: string, query: FTSQuery):string {
+        debug(`Generating query with ${JSON.stringify(query, null, 2)}`);
+        const mustacheQuery = this.transform(query);
+        return Mustache.render(mustacheTeplate, mustacheQuery);
+    }
+
+    private transform(query: FTSQuery): MustacheQuery {
+        if (query.clauses.length == 0) {
+            throw new Error("A query should contain at least one clause");
+        }
+
+        const prefix = this.queryName2prefix(query.name);
+
+        //const entityObjType = this.lookupType(query.fields[0]);
+        const entities: MustacheOrmEnitity[] = [];
+        const documents: MustacheQueryDocument[] = [];
+        
+        const name2doc: { [entity:string]: MustacheQueryDocument } = {};
+        const name2entity: { [entity:string]: MustacheOrmEnitity } = {};
+
+        query.clauses.map((v) => {
+            if (!name2doc[v.entity.name]) {
+                const table = this.name2table(v.entity.name);
+                name2doc[v.entity.name] = {
+                    tsvColumn: `${prefix}_tsv`,
+                    docColumn: `${prefix}_doc`,
+                    index_name: `${prefix}_${table}_idx`,
+                    table,
+                    fields: [],
+                    last: false
+                };
+                name2entity[v.entity.name] = this.objectTypeToMustache(v.entity);
+            }
+            name2doc[v.entity.name].fields.push({
+                column: this.name2column(v.field.name),
+                weight: 'A',
+                last: false
+            });
+            
+        })
+
+        Object.entries(name2doc).forEach(([entityName, doc]) => {
+            entities.push(name2entity[entityName]);
+            doc.fields[doc.fields.length - 1].last = true;
+            documents.push(doc);
+        })
+        
+        documents[documents.length - 1].last = true;
+        entities[entities.length - 1].last = true;
+        
+        return {
+            entities,
+            query: {
+                viewName: `${prefix}_view`,
+                typePrefix: upperFirst(query.name),
+                name: query.name,
+                language: 'english',// only English is supported for now
+                documents,
+                ts: (this._context["ts"]) ? this._context["ts"] as number : Date.now()
+            }
+        }
+    }
+
+    private objectTypeToMustache(objType: ObjectType): MustacheOrmEnitity {
+        return {
+            type: upperFirst(objType.name),
+            table: this.name2table(objType.name), 
+            model: this.name2modelName(objType.name),
+            fieldName: this.fieldName(objType.name),
+            arrayName: this.arrayName(objType.name),
+            last: false
+        }
+    } 
+
+    // TODO: hmm this really depends on typeorm naming strategy
+    private name2column(name: string):string {
+        return `"${name}"`;
+    }
+
+    private name2table(name: string): string {
+        return snakeCase(name);
+    }
+
+    private queryName2prefix(qName: string): string {
+        return snakeCase(qName);
+    }
+
+    private name2modelName(name: string): string {
+        return kebabCase(name);
+    }
+
+    private fieldName(name: string): string {
+        return lowerFirst(name);
+    }
+
+    private arrayName(name: string): string {
+        return `${this.fieldName(name)}s`;
+    }
+}

+ 182 - 0
query-node/substrate-query-framework/cli/src/generate/ModelRenderer.ts

@@ -0,0 +1,182 @@
+import Mustache from 'mustache';
+import { Field, ObjectType } from '../model';
+import * as path from 'path';
+import { kebabCase, camelCase } from 'lodash';
+import { getTypesForArray, names, pascalCase, camelPlural } from './utils';
+import Debug from "debug";
+import { GeneratorContext } from './SourcesGenerator';
+
+const debug = Debug('qnode-cli:model-renderer');
+
+const TYPE_FIELDS: { [key: string]: { [key: string]: string } } = {
+  bool: {
+    decorator: 'BooleanField',
+    tsType: 'boolean'
+  },
+  date: {
+    decorator: 'DateField',
+    tsType: 'Date'
+  },
+  int: {
+    decorator: 'IntField',
+    tsType: 'number'
+  },
+  float: {
+    decorator: 'FloatField',
+    tsType: 'number'
+  },
+  json: {
+    decorator: 'JSONField',
+    tsType: 'JsonObject'
+  },
+  otm: {
+    decorator: 'OneToMany',
+    tsType: '---'
+  },
+  string: {
+    decorator: 'StringField',
+    tsType: 'string'
+  },
+  numeric: {
+    decorator: 'NumericField',
+    tsType: 'string'
+  },
+  decimal: {
+    decorator: 'NumericField',
+    tsType: 'string'
+  },
+  oto: {
+    decorator: 'OneToOne',
+    tsType: '---'
+  },
+  array: {
+    decorator: 'ArrayField',
+    tsType: '' // will be updated with the correct type
+  },
+  bytes: {
+    decorator: 'BytesField',
+    tsType: 'Buffer'
+  }
+};
+
+type FunctionProp = () => string;
+type MustacheProp = string | FunctionProp;
+
+
+export interface MustacheObjectType {
+  generatedFolderRelPath: string,
+  className: string
+  fields: MustacheField[],
+  has: Props // hasBooleanField, hasIntField, ...
+}
+
+export interface MustacheField {
+  camelName: MustacheProp,
+  tsType: MustacheProp,
+  decorator: MustacheProp,
+  relClassName?: string,
+  relCamelName?: string,
+  relPathForModel?: string,
+  apiType?: string,
+  dbType?: string,
+  required: boolean,
+  is: Props // isOtm, isMto, isScalar ...
+} 
+
+interface Props {
+  [key: string]: boolean | string
+}
+
+export class ModelRenderer {
+
+  private context: GeneratorContext = {};
+
+  constructor(context: GeneratorContext = {}) {
+    this.context = context;
+  }
+
+  transformField(f: Field, entity: ObjectType): MustacheField {
+    let ret = {};
+    const isProps: Props = {};
+    isProps['array'] = f.isArray(); 
+    isProps['scalar'] = f.isScalar();
+    ['mto', 'oto', 'otm'].map((s) => isProps[s] = (f.type === s));
+
+    isProps['refType'] = isProps['mto'] || isProps['oto'] || isProps['otm'];
+
+    const fieldType = f.columnType();
+   
+    ret =  {
+      is: isProps,
+      required: !f.nullable,
+      ...TYPE_FIELDS[fieldType]
+    };
+
+    if (isProps['array']) {
+      ret = {
+        ...ret,
+        ...getTypesForArray(fieldType),
+        decorator: 'ArrayField',
+      }
+    }
+
+    const names = this.interpolateNames(f, entity);
+
+    ret = {
+      ...ret,
+      ...this.interpolateNames(f, entity),
+      relPathForModel: this.relativePathForModel(names['relClassName'])
+    }
+
+    debug(`Mustache Field: ${JSON.stringify(ret, null, 2)}`);
+
+    return ret as MustacheField; 
+  }
+
+  interpolateNames(f: Field, entity: ObjectType): { [key: string]: string } {
+    const single = (f.type === 'otm') ? f.name.slice(0, -1) : f.name; // strip s at the end if otm
+    return {
+        ...names(single),
+        relClassName: pascalCase(single),
+        relCamelName: camelCase(single),
+        relFieldName: camelCase(entity.name),
+        relFieldNamePlural: camelPlural(entity.name)
+    }
+    
+  }
+
+
+  transform(objType: ObjectType): MustacheObjectType {
+    const fields: MustacheField[] = [];
+    
+    objType.fields.map((f) => fields.push(this.transformField(f, objType)));
+    
+    const has: Props = {};
+    for (const key in TYPE_FIELDS) {
+      const _key: string = (key === 'numeric') ? 'numeric' || 'decimal' : key;
+      has[key] = objType.fields.some((f) => f.columnType() === _key);
+    }
+    has['array'] = objType.fields.some((f) => f.isArray());
+
+    debug(`ObjectType has: ${JSON.stringify(has, null, 2)}`);
+
+    return { fields, 
+            generatedFolderRelPath: this.context["generatedFolderRelPath"] as string, //this.getGeneratedFolderRelativePath(objType.name),
+            has,
+            ...names(objType.name) } as MustacheObjectType;
+  }
+
+  generate(mustacheTeplate: string, objType: ObjectType):string {
+    const mustacheQuery = this.transform(objType);
+    return Mustache.render(mustacheTeplate, mustacheQuery);
+  }
+
+  
+  relativePathForModel(referenced: string): string {
+    return path.join(
+      '..',
+      kebabCase(referenced),
+      `${kebabCase(referenced)}.model`
+    );
+  }
+}

+ 134 - 0
query-node/substrate-query-framework/cli/src/generate/SourcesGenerator.ts

@@ -0,0 +1,134 @@
+import { Config } from 'warthog';
+import * as fs from 'fs-extra';
+import * as path from 'path';
+import { getTemplatePath, createFile, createDir } from '../utils/utils';
+import * as prettier from 'prettier';
+
+import Debug from "debug";
+import { WarthogModel } from '../model';
+import { FTSQueryRenderer } from './FTSQueryRenderer';
+import { ModelRenderer } from './ModelRenderer';
+import { kebabCase, camelCase } from 'lodash';
+import { supplant, pascalCase, camelPlural } from './utils';
+
+const debug = Debug('qnode-cli:sources-generator');
+
+const FULL_TEXT_QUERIES_FOLDER = 'fulltextqueries';
+/**
+ * additional context to be passed to the generator, 
+ * e.g. to have predictable timestamps
+ */
+export interface GeneratorContext {
+  [key:string]: unknown
+}
+
+export class SourcesGenerator {
+  readonly config: Config;
+  readonly cliGeneratePath: string;
+  readonly model: WarthogModel;
+
+  constructor(model: WarthogModel) {
+    this.config = new Config();
+    this.config.loadSync();
+    this.model = model;
+
+    this.cliGeneratePath =
+      path.join(this.config.get('ROOT_FOLDER'), '/', this.config.get('CLI_GENERATE_PATH'), '/');
+    
+  }
+
+  generate(): void {
+    this.generateModels();
+    this.generateQueries();
+  }
+
+  generateModels():void {
+    
+    //TODO: Refactor this and read all the paths from Warthog's config
+    createDir(path.resolve(process.cwd(), 'src/modules'), false, true);
+
+    this.model.types.map((objType) => {
+      const destFolder = this.getDestFolder(objType.name);
+      const generatedFolderRelPath = path.relative(destFolder, this.config.get('GENERATED_FOLDER'));
+
+      const modelRenderer = new ModelRenderer({ "generatedFolderRelPath": generatedFolderRelPath });
+      const render = (template:string) => modelRenderer.generate(template, objType);
+      
+      createDir(path.resolve(process.cwd(), destFolder), false, true);
+      
+      ['model', 'resolver', 'service'].map((template) => {
+        this.renderAndWrite(`entities/${template}.ts.mst`, 
+          path.join(destFolder, `${kebabCase(objType.name)}.${template}.ts`),
+          render);
+      })
+    });
+  } 
+
+  generateQueries():void {
+    if (!this.model) {
+        throw new Error("Warthog model is undefined");
+    }
+
+    // create migrations dir if not exists
+    const migrationsDir = this.config.get('DB_MIGRATIONS_DIR') as string;
+    createDir(path.resolve(process.cwd(), migrationsDir), false, true);
+    
+    // create dir if the textsearch module
+    const ftsDir = this.getDestFolder(FULL_TEXT_QUERIES_FOLDER);
+    createDir(path.resolve(process.cwd(), ftsDir), false, true);
+
+    const queryRenderer = new FTSQueryRenderer();
+    
+    this.model.ftsQueries.map((query) => {
+      const render = (template:string) => queryRenderer.generate(template, query);
+      const filePrefix = kebabCase(query.name);
+
+       // migration
+      this.renderAndWrite('textsearch/migration.ts.mst', 
+          path.join(migrationsDir, `${filePrefix}.migration.ts`), render);
+        
+       // resolver   
+      this.renderAndWrite('textsearch/resolver.ts.mst', 
+          path.join(ftsDir, `${filePrefix}.resolver.ts`), render);   
+
+       // service
+      this.renderAndWrite('textsearch/service.ts.mst', 
+          path.join(ftsDir, `${filePrefix}.service.ts`), render);   
+    })
+    
+  }
+
+
+  /**
+   * 
+   * @param template relative path to a template from the templates folder, e.g. 'db-helper.mst'
+   * @param destPath relative path to the `generated/graphql-server' folder, e.g. 'src/index.ts'
+   * @param render function which transforms the template contents
+   */
+  private renderAndWrite(template: string, destPath: string, render: (data: string) => string) {
+    const templateData: string = fs.readFileSync(getTemplatePath(template), 'utf-8');
+    debug(`Source: ${getTemplatePath(template)}`);
+    let rendered: string = render(templateData);
+    
+    rendered = prettier.format(rendered, {
+      parser: 'typescript'
+    });
+
+    debug(`Transformed: ${rendered}`);
+    const destFullPath = path.resolve(process.cwd(), destPath);
+    
+    debug(`Writing to: ${destFullPath}`);
+    createFile(destFullPath, rendered, true);
+  }
+
+  getDestFolder(name: string): string {
+    const names = {
+      className: pascalCase(name),
+      camelName: camelCase(name),
+      kebabName: kebabCase(name),
+      camelNamePlural: camelPlural(name)
+    }
+    return supplant(this.cliGeneratePath, names);
+  }
+  
+}

+ 50 - 0
query-node/substrate-query-framework/cli/src/generate/utils.ts

@@ -0,0 +1,50 @@
+import { upperFirst, kebabCase, camelCase } from 'lodash';
+
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+export function supplant(str: string, obj: Record<string, unknown>): string {
+  return str.replace(/\${([^${}]*)}/g, (a, b) => {
+    const r = obj[b];
+    return typeof r === 'string' ? r : a;
+  });
+}
+
+export function pascalCase(str: string): string {
+  return upperFirst(camelCase(str));
+}
+
+export function camelPlural(str: string): string {
+  return `${camelCase(str)}s`;
+}
+
+export function getTypesForArray(typeName: string): { [key: string]: string } {
+  const graphQLFieldTypes: { [key: string]: string } = {
+    bool: 'boolean',
+    int: 'integer',
+    string: 'string',
+    float: 'float',
+    date: 'date',
+    numeric: 'numeric',
+    decimal: 'numeric'
+  };
+  const apiType = graphQLFieldTypes[typeName];
+
+  let dbType = apiType;
+  if (dbType === 'string') {
+    dbType = 'text'; // postgres doesnt have 'string'
+  } else if (dbType === 'float') {
+    dbType = 'decimal'; // postgres doesnt have 'float'
+  }
+
+  return { dbType, apiType };
+}
+
+export function names(name: string): { [key: string]: string } {
+  return {
+    className: pascalCase(name),
+    camelName: camelCase(name),
+    kebabName: kebabCase(name),
+    // Not proper pluralization, but good enough and easy to fix in generated code
+    camelNamePlural: camelPlural(name)
+  }
+}

+ 100 - 0
query-node/substrate-query-framework/cli/src/helpers/SchemaDirective.ts

@@ -0,0 +1,100 @@
+import { SchemaNode } from './SchemaParser';
+import { WarthogModel } from '../model';
+import { DirectiveNode, TypeNode, ArgumentNode, StringValueNode, FieldDefinitionNode, ObjectTypeDefinitionNode } from 'graphql';
+import { cloneDeep } from 'lodash';
+
+export const FULL_TEXT_SEARCHABLE_DIRECTIVE = 'fulltext';
+
+export interface DirectiveVisitor {
+    // directive name to watch
+    directiveName: string,
+    /**
+     * Generic visit function for the AST schema traversal. 
+     * Only ObjectTypeDefinition and FieldDefinition nodes are included in the path during the 
+     * traversal
+     * 
+     * May throw validation errors
+     * 
+     * @param path: BFS path in the schema tree ending at the directive node of interest
+     */
+    visit: (path: SchemaNode[]) => void;
+}
+
+export interface SchemaDirective {  
+    // directive definition to be added to the
+    // schema preamble
+    preamble: string,
+    name: string,
+    validate: (path: SchemaNode[]) => void; 
+    generate: (path: SchemaNode[], model: WarthogModel) => WarthogModel,
+}
+
+export class FTSDirective implements SchemaDirective {
+    preamble = `directive @${FULL_TEXT_SEARCHABLE_DIRECTIVE}(query: String!) on FIELD_DEFINITION`
+    name = FULL_TEXT_SEARCHABLE_DIRECTIVE
+    
+    validate(_path: SchemaNode[]):void {  
+        const path = cloneDeep(_path);
+
+        if (path.length < 3) {
+            throw new Error("The path should contain at least a type and field definition nodes");
+        }
+        const dirNode = path.pop();
+        if (dirNode?.kind !== 'Directive') {
+            throw new Error("The path should end at a directive node");
+        }
+        const fieldNode = path.pop();
+        if (fieldNode?.kind !== 'FieldDefinition') {
+            throw new Error("The directive should be applied to a field node");
+        }
+        let type: TypeNode = fieldNode.type;
+        if (fieldNode.type.kind === 'NonNullType') {
+            type = fieldNode.type.type;
+        } 
+        if (type.kind == 'ListType') {
+            throw new Error("Only single named types are supported");
+        }
+        if (type.kind !== 'NamedType') {
+            throw new Error("Only single named types are supported");
+        }
+        if (type.name.value !== 'String') {
+            throw new Error(`Only string types can be annotaed ${FULL_TEXT_SEARCHABLE_DIRECTIVE}`);
+        } 
+    }
+
+    generate(path: SchemaNode[], model: WarthogModel): WarthogModel { 
+        this.validate(path);
+
+        const dirNode = path.pop() as DirectiveNode;
+        const fieldNode = path.pop() as FieldDefinitionNode;
+        const objTypeNode = path.pop() as ObjectTypeDefinitionNode;
+
+        const qName: string  = this._checkFullTextSearchDirective(dirNode);
+        model.addQueryClause(qName, fieldNode.name.value, objTypeNode.name.value);
+
+        return model 
+    }
+
+
+   /**
+    * 
+    * Does the checks and returns full text query names to be used;
+    * 
+    * @param d Directive Node
+    * @returns Fulltext query names 
+    */
+    private _checkFullTextSearchDirective(d: DirectiveNode): string {
+        if (!d.arguments) {
+            throw new Error(`@${FULL_TEXT_SEARCHABLE_DIRECTIVE} should have a query argument`)
+        }
+
+        const qarg: ArgumentNode[] = d.arguments.filter((arg) => (arg.name.value === `query`) && (arg.value.kind === `StringValue`))
+        
+        if (qarg.length !== 1) {
+            throw new Error(`@${FULL_TEXT_SEARCHABLE_DIRECTIVE} should have a single query argument with a sting value`);
+        }
+        return (qarg[0].value as StringValueNode).value;
+    }
+}
+
+export const DIRECTIVES: SchemaDirective[] = [new FTSDirective()];

+ 156 - 0
query-node/substrate-query-framework/cli/src/helpers/SchemaParser.ts

@@ -0,0 +1,156 @@
+import {
+  parse,
+  visit,
+  buildASTSchema,
+  GraphQLSchema,
+  validateSchema,
+  ObjectTypeDefinitionNode,
+  FieldDefinitionNode,
+  DirectiveNode
+} from 'graphql';
+import * as fs from 'fs-extra';
+import Debug from "debug";
+import { cloneDeep } from 'lodash';
+import { DIRECTIVES } from './SchemaDirective'
+import { SCHEMA_DEFINITIONS_PREAMBLE } from './constant';
+
+const debug = Debug('qnode-cli:schema-parser');
+
+export type SchemaNode = ObjectTypeDefinitionNode | FieldDefinitionNode | DirectiveNode;
+
+export interface Visitor {
+    /**
+     * Generic visit function for the AST schema traversal. 
+     * Only ObjectTypeDefinition and FieldDefinition nodes are included in the path during the 
+     * traversal
+     * 
+     * May throw validation errors
+     * 
+     * @param path: DFS path in the schema tree ending at the directive node of interest
+     */
+    visit: (path: SchemaNode[]) => void
+}
+
+export interface Visitors {
+    /**
+     * A map from the node name to the Visitor
+     * 
+     * During a DFS traversal of the AST tree if a directive node
+     * name matches the key in the directives map, the corresponding visitor is called
+     */
+    directives: { [name: string]: Visitor };
+}
+
+/**
+ * Parse GraphQL schema
+ * @constructor(schemaPath: string)
+ */
+export class GraphQLSchemaParser {
+  // GraphQL shchema
+  schema: GraphQLSchema;
+  // List of the object types defined in schema
+  private _objectTypeDefinations: ObjectTypeDefinitionNode[];
+
+  constructor(schemaPath: string) {
+    if (!fs.existsSync(schemaPath)) {
+        throw new Error('Schema not found');
+    }
+    const contents = fs.readFileSync(schemaPath, 'utf8');
+    this.schema = GraphQLSchemaParser.buildSchema(contents);
+    this._objectTypeDefinations = GraphQLSchemaParser.createObjectTypeDefinations(this.schema);
+  }
+
+  private static buildPreamble(): string {
+      let preamble = SCHEMA_DEFINITIONS_PREAMBLE;
+      DIRECTIVES.map((d) => preamble += (d.preamble + '\n'));
+      return preamble;
+  }
+  /**
+   * Read GrapqhQL schema and build a schema from it
+   */
+  static buildSchema(contents: string): GraphQLSchema {
+    const schema = GraphQLSchemaParser.buildPreamble().concat(contents);
+    const ast = parse(schema);
+    // in order to build AST with undeclared directive, we need to 
+    // switch off SDL validation
+    const schemaAST = buildASTSchema(ast);
+
+    const errors = validateSchema(schemaAST);
+
+    if (errors.length > 0) {
+      // There are errors
+      let errorMsg = `Schema is not valid. Please fix the following errors: \n`;
+      errors.forEach(e => errorMsg += `\t ${e.name}: ${e.message}\n`);
+      debug(errorMsg);
+      throw new Error(errorMsg);
+    }
+
+    return schemaAST;
+  }
+
+  /**
+   * Get object type definations from the schema. Build-in and scalar types are excluded.
+   */
+  static createObjectTypeDefinations(schema: GraphQLSchema): ObjectTypeDefinitionNode[] {
+    return [
+      ...Object.values(schema.getTypeMap())
+        // eslint-disable-next-line @typescript-eslint/prefer-regexp-exec
+        .filter(t => !t.name.match(/^__/) && !t.name.match(/Query/)) // skip the top-level Query type
+        .sort((a, b) => (a.name > b.name ? 1 : -1))
+        .map(t => t.astNode)
+    ]
+      .filter(Boolean) // Remove undefineds and nulls
+      .filter(typeDefinationNode => typeDefinationNode?.kind === 'ObjectTypeDefinition') as ObjectTypeDefinitionNode[];
+  }
+
+  /**
+   * Returns fields for a given GraphQL object
+   * @param objDefinationNode ObjectTypeDefinitionNode
+   */
+  getFields(objDefinationNode: ObjectTypeDefinitionNode): FieldDefinitionNode[] {
+    if (objDefinationNode.fields) return [...objDefinationNode.fields];
+    return [];
+  }
+
+  /**
+   * Returns GraphQL object names
+   */
+  getTypeNames(): string[] {
+    return this._objectTypeDefinations.map(o => o.name.value);
+  }
+
+  /**
+   * Returns GraphQL object type definations
+   */
+  getObjectDefinations(): ObjectTypeDefinitionNode[] {
+    return this._objectTypeDefinations;
+  }
+
+  /**
+   * DFS traversal of the AST
+   */
+  dfsTraversal(visitors: Visitors): void {
+    // we traverse starting from each definition 
+    this._objectTypeDefinations.map((objType) => {
+        const path: SchemaNode[] = [];
+        visit(objType, {
+            enter: (node) => {
+                if (node.kind !== 'Directive' && 
+                    node.kind !== 'ObjectTypeDefinition' && 
+                    node.kind !== 'FieldDefinition') {
+                        // skip non-definition fields;
+                        return false;
+                }
+                path.push(node);
+                if (node.kind === 'Directive') {
+                    if (node.name.value in visitors.directives) {
+                        (visitors.directives[node.name.value]).visit(cloneDeep(path))
+                    }
+                } 
+               
+            },
+            leave: () => path.pop()
+        })
+    })
+  }
+}

+ 159 - 0
query-node/substrate-query-framework/cli/src/helpers/WarthogModelBuilder.ts

@@ -0,0 +1,159 @@
+import { ObjectTypeDefinitionNode, FieldDefinitionNode, ListTypeNode, NamedTypeNode } from 'graphql';
+import { GraphQLSchemaParser, Visitors, SchemaNode } from './SchemaParser';
+import { WarthogModel, Field, ObjectType } from '../model';
+import Debug from 'debug';
+import { DIRECTIVES } from './SchemaDirective';
+import { ENTITY_DIRECTIVE } from './constant';
+
+const debug = Debug('qnode-cli:model-generator');
+
+/**
+ * Parse a graphql schema and generate model defination strings for Warthog. It use GraphQLSchemaParser for parsing
+ * @constructor(schemaPath: string)
+ */
+export class WarthogModelBuilder {
+  private _schemaParser: GraphQLSchemaParser;
+  private _model: WarthogModel;
+
+  constructor(schemaPath: string) {
+    this._schemaParser = new GraphQLSchemaParser(schemaPath);
+    this._model = new WarthogModel();
+  }
+
+  /**
+   * Returns true if type is Scalar, String, Int, Boolean, Float otherwise false
+   * Scalar types are also built-in
+   */
+  private _isBuildinType(type: string): boolean {
+    return !this._schemaParser.getTypeNames().includes(type);
+  }
+
+  private _listType(typeNode: ListTypeNode, fieldName: string): Field {
+    let field: Field;
+
+    if (typeNode.type.kind === 'ListType') {
+      throw new Error('Only one level lists are allowed');
+    } else if (typeNode.type.kind === 'NamedType') {
+      field = this._namedType(fieldName, typeNode.type);
+      field.isList = true;
+    } else {
+      if (typeNode.type.type.kind === 'ListType') {
+        throw new Error('Only one level lists are allowed');
+      }
+      field = this._namedType(fieldName, typeNode.type.type);
+      field.nullable = false;
+    }
+
+    field.isList = true;
+    field.isBuildinType = this._isBuildinType(field.type);
+    return field;
+  }
+
+  /**
+   * Create a new Field type from NamedTypeNode
+   * @param name string
+   * @param namedTypeNode NamedTypeNode
+   * @param directives: additional directives of FieldDefinitionNode
+   */
+  private _namedType(name: string, namedTypeNode: NamedTypeNode): Field {
+    const field = new Field(name, namedTypeNode.name.value);
+    field.isBuildinType = this._isBuildinType(field.type);
+
+    return field;
+  }
+
+  /**
+   * Mark the object type as entity if '@entity' directive is used
+   * @param o ObjectTypeDefinitionNode
+   */
+  private isEntity(o: ObjectTypeDefinitionNode): boolean {
+    const entityDirective = o.directives?.find((d) => d.name.value === ENTITY_DIRECTIVE);
+    return entityDirective ? true : false;
+  }
+
+  /**
+   * Generate a new ObjectType from ObjectTypeDefinitionNode
+   * @param o ObjectTypeDefinitionNode
+   */
+  private generateTypeDefination(o: ObjectTypeDefinitionNode): ObjectType {
+    const fields = this._schemaParser.getFields(o).map((fieldNode: FieldDefinitionNode) => {
+      const typeNode = fieldNode.type;
+      const fieldName = fieldNode.name.value;
+
+      if (typeNode.kind === 'NamedType') {
+        return this._namedType(fieldName, typeNode);
+      } else if (typeNode.kind === 'NonNullType') {
+        const field =
+          typeNode.type.kind === 'NamedType'
+            ? this._namedType(fieldName, typeNode.type)
+            : this._listType(typeNode.type, fieldName);
+
+        field.nullable = false;
+        return field;
+      } else if (typeNode.kind === 'ListType') {
+        return this._listType(typeNode, fieldName);
+      } else {
+        throw new Error(`Unrecognized type. ${JSON.stringify(typeNode, null, 2)}`);
+      }
+    });
+
+    debug(`Read and parsed fields: ${JSON.stringify(fields, null, 2)}`);
+
+    return { name: o.name.value, fields: fields, isEntity: this.isEntity(o) } as ObjectType;
+  }
+
+  /**
+   * Add SQL OneToOne and ManyToOne relationship to object types if there is
+   */
+  generateSQLRelationships(): void {
+    const additionalFields: { [key: string]: string | Field }[] = [];
+
+    this._model.types.forEach(({ name, fields }) => {
+      for (const field of fields) {
+        if (!field.isBuildinType && field.isList) {
+          const typeName = field.type;
+          field.name = field.type.toLowerCase().concat('s');
+          field.type = 'otm'; // OneToMany
+
+          const newField = new Field(field.type, field.type);
+          newField.isBuildinType = false;
+          newField.nullable = false;
+          newField.type = 'mto'; // ManyToOne
+          newField.name = name.toLowerCase();
+          additionalFields.push({ field: newField, name: typeName });
+        }
+      }
+    });
+
+    for (const objType of this._model.types) {
+      for (const field of additionalFields) {
+        if (objType.name === field.name) {
+          objType.fields.push(field.field as Field);
+        }
+      }
+    }
+  }
+
+  buildWarthogModel(): WarthogModel {
+    this._model = new WarthogModel();
+
+    this._schemaParser.getObjectDefinations().map((o) => {
+      const objType = this.generateTypeDefination(o);
+      this._model.addObjectType(objType);
+    });
+
+    this.generateSQLRelationships();
+
+    const visitors: Visitors = {
+      directives: {},
+    };
+    DIRECTIVES.map((d) => {
+      visitors.directives[d.name] = {
+        visit: (path: SchemaNode[]) => d.generate(path, this._model),
+      };
+    });
+    this._schemaParser.dfsTraversal(visitors);
+
+    return this._model;
+  }
+}

+ 130 - 0
query-node/substrate-query-framework/cli/src/helpers/WarthogWrapper.ts

@@ -0,0 +1,130 @@
+import * as fs from 'fs-extra';
+import { execSync } from 'child_process';
+import * as path from 'path';
+import * as dotenv from 'dotenv';
+
+import Command from '@oclif/command';
+import { copyFileSync } from 'fs-extra';
+import { cli as warthogCli } from '../index';
+
+import { WarthogModelBuilder } from './WarthogModelBuilder';
+import { getTemplatePath } from '../utils/utils';
+import Debug from "debug";
+import { SourcesGenerator } from '../generate/SourcesGenerator';
+
+const debug = Debug('qnode-cli:warthog-wrapper');
+
+export default class WarthogWrapper {
+  private readonly command: Command;
+  private readonly schemaPath: string;
+  
+  constructor(command: Command, schemaPath: string) {
+    this.command = command;
+    this.schemaPath = schemaPath;
+  }
+
+  async run():Promise<void> {
+    // Order of calling functions is important!!!
+    await this.newProject();
+
+    this.installDependencies();
+
+    await this.createDB();
+
+    this.generateWarthogSources();
+
+    this.codegen();
+
+    this.createMigrations();
+
+    this.runMigrations();
+  }
+
+  async generateAPIPreview(): Promise<void> {
+    // Order of calling functions is important!!!
+    await this.newProject();
+    this.installDependencies();
+    this.generateWarthogSources();
+    this.codegen();
+  
+  }
+
+  async newProject(projectName = 'query_node'):Promise<void> {
+    await warthogCli.run(`new ${projectName}`);
+
+    // Override warthog's index.ts file for custom naming strategy
+    fs.copyFileSync(getTemplatePath('graphql-server.index.mst'), path.resolve(process.cwd(), 'src/index.ts'));
+
+    this.updateDotenv();
+  }
+
+  installDependencies():void {
+    if (!fs.existsSync('package.json')) {
+      this.command.error('Could not found package.json file in the current working directory');
+    }
+
+    // Temporary tslib fix
+    const pkgFile = JSON.parse(fs.readFileSync('package.json', 'utf8')) as Record<string, Record<string, unknown>>; 
+    pkgFile.resolutions['tslib'] = '1.11.2';
+    pkgFile.scripts['sync'] = 'SYNC=true WARTHOG_DB_SYNCHRONIZE=true ts-node-dev --type-check src/index.ts';
+    fs.writeFileSync('package.json', JSON.stringify(pkgFile, null, 2));
+
+    this.command.log('Installing graphql-server dependencies...');
+
+    execSync('yarn install');
+
+    this.command.log('done...');
+  }
+
+  async createDB():Promise<void> {
+    await warthogCli.run('db:create');
+  }
+
+  /**
+   * Generate the warthog source files: 
+   *   - model/resolver/service for entities
+   *   - Fulltext search queries (migration/resolver/service)
+   */
+  generateWarthogSources():void {
+    const schemaPath = path.resolve(process.cwd(), this.schemaPath);
+
+    const modelBuilder = new WarthogModelBuilder(schemaPath);
+    const model = modelBuilder.buildWarthogModel();
+    
+    const sourcesGenerator = new SourcesGenerator(model);
+    sourcesGenerator.generate();
+  }
+
+  codegen():void {
+    execSync('yarn warthog codegen && yarn dotenv:generate');
+  }
+
+  createMigrations():void {
+    execSync('yarn sync');
+  }
+
+  runMigrations():void {
+      debug('performing migrations');
+      execSync('yarn db:migrate');
+  }
+
+  updateDotenv():void {
+    // copy dotnenvi env.yml file 
+    debug("Creating graphql-server/env.yml")
+    copyFileSync(getTemplatePath('warthog.env.yml'), path.resolve(process.cwd(), 'env.yml'));
+    const envConfig = dotenv.parse(fs.readFileSync('.env'));
+
+    // Override DB_NAME, PORT, ...
+    envConfig['WARTHOG_DB_DATABASE'] = process.env.DB_NAME || envConfig['WARTHOG_DB_DATABASE'];
+    envConfig['WARTHOG_DB_USERNAME'] = process.env.DB_USER || envConfig['WARTHOG_DB_USERNAME'];
+    envConfig['WARTHOG_DB_PASSWORD'] = process.env.DB_PASS || envConfig['WARTHOG_DB_PASSWORD'];
+    envConfig['WARTHOG_DB_HOST'] = process.env.DB_HOST || envConfig['WARTHOG_DB_HOST'];
+    envConfig['WARTHOG_DB_PORT'] = process.env.DB_PORT || envConfig['WARTHOG_DB_PORT'];
+    envConfig['WARTHOG_APP_PORT'] = process.env.GRAPHQL_SERVER_PORT || envConfig['WARTHOG_APP_PORT'];
+
+    const newEnvConfig = Object.keys(envConfig)
+      .map(key => `${key}=${envConfig[key]}`)
+      .join('\n');
+    fs.writeFileSync('.env', newEnvConfig);
+  }
+}

+ 15 - 0
query-node/substrate-query-framework/cli/src/helpers/constant.ts

@@ -0,0 +1,15 @@
+/**
+ * This preamble is added to the schema in order to pass the SDL validation
+ * Add additional scalar types and directives to the schema
+ */
+export const SCHEMA_DEFINITIONS_PREAMBLE = `
+directive @entity on OBJECT  # Make type defination entity
+scalar BigInt                # Arbitrarily large integers
+scalar BigDecimal            # is used to represent arbitrary precision decimals
+scalar Bytes                 # Byte array, represented as a hexadecimal string
+type Query {
+    _dummy: String           # empty queries are not allowed
+}
+`
+
+export const ENTITY_DIRECTIVE = 'entity'

+ 86 - 0
query-node/substrate-query-framework/cli/src/helpers/db.ts

@@ -0,0 +1,86 @@
+import * as dotenv from 'dotenv';
+import * as fs from 'fs-extra';
+import { createConnection, getConnection } from 'typeorm';
+
+import { getTemplatePath } from '../utils/utils';
+
+/**
+ * Update typeorms' .env config file with top level .env file
+ */
+export function getTypeormConfig(): string {
+  const envConfig = dotenv.parse(fs.readFileSync(getTemplatePath('dotenv-ormconfig.mst')));
+
+  envConfig['TYPEORM_DATABASE'] = process.env.DB_NAME || envConfig['TYPEORM_DATABASE'];
+  envConfig['TYPEORM_USERNAME'] = process.env.DB_USER || envConfig['TYPEORM_USERNAME'];
+  envConfig['TYPEORM_PASSWORD'] = process.env.DB_PASS || envConfig['TYPEORM_PASSWORD'];
+  envConfig['TYPEORM_HOST'] = process.env.DB_HOST || envConfig['TYPEORM_HOST'];
+  envConfig['TYPEORM_PORT'] = process.env.DB_PORT || envConfig['TYPEORM_PORT'];
+
+  const newEnvConfig = Object.keys(envConfig)
+    .map(key => `${key}=${envConfig[key]}`)
+    .join('\n');
+  return newEnvConfig;
+}
+
+export function getTypeormModelGeneratorConnectionConfig():string {
+  const envConfig = dotenv.parse(fs.readFileSync('.env'));
+
+  const command = [
+    './node_modules/.bin/typeorm-model-generator ',
+    `--host ${envConfig['TYPEORM_HOST']}`,
+    `--port ${envConfig['TYPEORM_PORT']}`,
+    `--database ${envConfig['TYPEORM_DATABASE']}`,
+    `--engine postgres`,
+    `--output entities`,
+    `--user ${envConfig['TYPEORM_USERNAME']}`,
+    `--pass ${envConfig['TYPEORM_PASSWORD']}`,
+    '--noConfig',
+    '--generateConstructor'
+  ].join(' ');
+  return command;
+}
+
+export async function resetLastProcessedEvent(): Promise<void> {
+  await createConnection();
+  // get a connection and create a new query runner
+  const queryRunner = getConnection().createQueryRunner();
+
+  // establish real database connection using our new query runner
+  await queryRunner.connect();
+  const lastProcessedEvent = {
+    blockNumber: 0,
+    eventName: 'ExtrinsicSuccess',
+    index: 0
+  };
+
+  // now we can execute any queries on a query runner
+  await queryRunner.query(
+    `UPDATE saved_entity_event SET 
+    "blockNumber" = ${lastProcessedEvent.blockNumber}, 
+    index = ${lastProcessedEvent.index},
+    "eventName" = '${lastProcessedEvent.eventName}';`
+  );
+
+  await queryRunner.release();
+}
+
+export async function createSavedEntityEventTable(): Promise<void> {
+  const query = `CREATE TABLE "saved_entity_event" (
+      "id" integer PRIMARY KEY DEFAULT 1,
+      "index" numeric,
+      "eventName" character varying NOT NULL,
+      "blockNumber" numeric NOT NULL,
+      "updatedAt" TIMESTAMP NOT NULL DEFAULT now())`;
+
+  await createConnection();
+  // get a connection and create a new query runner
+  const queryRunner = getConnection().createQueryRunner();
+
+  // establish real database connection using our new query runner
+  await queryRunner.connect();
+
+  // now we can execute any queries on a query runner
+  await queryRunner.query(query);
+
+  await queryRunner.release();
+}

+ 17 - 0
query-node/substrate-query-framework/cli/src/helpers/formatter.ts

@@ -0,0 +1,17 @@
+import * as Prettier from 'prettier';
+
+const prettierOptions: Prettier.Options = {
+  parser: 'typescript',
+  endOfLine: 'auto',
+};
+
+export function formatWithPrettier(text: string, options: Prettier.Options = prettierOptions):string {
+  let formatted = '';
+  try {
+    formatted = Prettier.format(text, options);
+  } catch (error) {
+    console.error('There were some errors while formatting with Prettier', error);
+    formatted = text;
+  }
+  return formatted;
+}

+ 17 - 0
query-node/substrate-query-framework/cli/src/helpers/tsTypes.ts

@@ -0,0 +1,17 @@
+export const fieldTypes: { [key: string]: { [key: string]: string } } = {
+  bool: {
+    tsType: 'boolean',
+  },
+  date: {
+    tsType: 'Date',
+  },
+  int: {
+    tsType: 'number',
+  },
+  float: {
+    tsType: 'number',
+  },
+  string: {
+    tsType: 'string',
+  },
+};

+ 10 - 0
query-node/substrate-query-framework/cli/src/index.ts

@@ -0,0 +1,10 @@
+import { build } from 'gluegun';
+
+export const cli = build()
+    .brand('warthog')
+    .src(`${__dirname}/../node_modules/warthog/dist/cli`)
+    .help() // provides default for help, h, --help, -h
+    .version() // provides default for version, v, --version, -v
+    .create();
+
+export { run } from '@oclif/command'

+ 23 - 0
query-node/substrate-query-framework/cli/src/model/FTSQuery.ts

@@ -0,0 +1,23 @@
+import { Field, ObjectType } from './WarthogModel';
+
+
+/**
+ * FTSQueryClause represents a single entity/field which 
+ * corresponds to a text-based table column in the corresponding database schema.
+ * 
+ * The clauses are concatenated and stored in a separated db view;
+ */
+export interface FTSQueryClause {
+    entity: ObjectType,
+    field: Field 
+}
+
+/**
+ * Represnts Fulltext search query as defined by
+ *  fields in GraphGL  decorated FTSDirective directive
+ */
+export interface FTSQuery {
+    name: string;
+    clauses: FTSQueryClause[];
+}
+

+ 16 - 0
query-node/substrate-query-framework/cli/src/model/ScalarTypes.ts

@@ -0,0 +1,16 @@
+export interface ScalarType {
+  [name: string]: string;
+}
+
+// Supported built-in scalar types and corressponding warthog type
+export const availableTypes: ScalarType = {
+  ID: 'string',
+  String: 'string',
+  Int: 'int',
+  Boolean: 'bool',
+  Date: 'date',
+  Float: 'float',
+  BigInt: 'numeric',
+  BigDecimal: 'decimal',
+  Bytes: 'bytes',
+};

+ 197 - 0
query-node/substrate-query-framework/cli/src/model/WarthogModel.ts

@@ -0,0 +1,197 @@
+import { FTSQuery } from './FTSQuery';
+import { availableTypes } from './ScalarTypes'
+
+export class WarthogModel {
+    private  _types: ObjectType[];
+    private _ftsQueries: FTSQuery[];
+    private _name2query: { [key: string]: FTSQuery } = {};
+    private _name2type: { [key: string]: ObjectType } = {};
+
+    constructor() {
+        this._types = [];
+        this._ftsQueries = [];
+    }
+
+    addObjectType(type: ObjectType): void {
+        if (!type.isEntity) return;
+
+        this._types.push(type);
+        this._name2type[type.name] = type; 
+    }
+
+    addFTSQuery(query: FTSQuery):void {
+        if (!this._name2query[query.name]) {
+            this._name2query[query.name] = query;
+        }
+        this._ftsQueries.push(query);
+    }
+
+    /**
+     * Add emply full text search query with the given name
+     * 
+     * @param name query name to be added
+     */
+    addEmptyFTSQuery(name: string): FTSQuery {
+        const query = {
+            name,
+            clauses: []
+        };
+        this.addFTSQuery(query);
+        return query;
+    }
+
+    private _addQueryClause(name:string, f: Field, t: ObjectType):void {
+        let q: FTSQuery = this._name2query[name];
+        if (!q) {
+            q = this.addEmptyFTSQuery(name);
+        }
+        q.clauses.push({
+            entity: t,
+            field: f
+        });
+    }
+
+    /**
+     * Add text search field to the named FTS query
+     * 
+     * @param queryName fulltext query name
+     * @param fieldName name of the field to be added to the query
+     * @param typeName  objectType which defined that field
+     */
+    addQueryClause(queryName: string, fieldName: string, typeName: string):void {
+        const field = this.lookupField(typeName, fieldName);
+        const objType = this.lookupType(typeName);
+        this._addQueryClause(queryName, field, objType);
+    }
+
+    get types(): ObjectType[] {
+        return this._types;
+    }
+
+    get ftsQueries(): FTSQuery[] {
+        return this._ftsQueries;
+    }
+
+    lookupQuery(queryName: string): FTSQuery {
+        if (!this._name2query) {
+            throw new Error(`No query with name ${queryName} found`);
+        }
+        return this._name2query[queryName];
+    }
+
+    /**
+     * Lookup Warthog's Field model object by it's ObjectType and name
+     * 
+     * @param objTypeName Type name with the given field defined
+     * @param name the name of the field 
+     */
+    lookupField(objTypeName: string, name: string): Field {
+        const objType = this.lookupType(objTypeName);
+        const field = objType.fields.find((f) => f.name === name);
+        if (!field) {
+            throw new Error(`No field ${name} is found for object type ${objTypeName}`);
+        }
+        return field;
+    }
+
+    addField(entity: string, field: Field): void {
+        const objType = this.lookupType(entity);
+        objType.fields.push(field);
+    }
+
+    /**
+     * Lookup ObjectType by it's name (as defined in the schema file)
+     * 
+     * @param name ObjectTypeName as defined in the schema
+     */
+    lookupType(name: string): ObjectType {
+        if (!this._name2type[name]) {
+            throw new Error(`No ObjectType ${name} found`);
+        }
+        return this._name2type[name];
+    }
+
+    /**
+     * Generate model defination as one-line string literal
+     * Example: User username! age:int! isActive:bool!
+     */
+    toWarthogStringDefinitions(): string[] {
+        const models = this._types.map(input => {
+            const fields = input.fields.map(field => field.format()).join(' ');
+            return [input.name, fields].join(' ');
+        });
+        return models;
+    }
+
+}
+
+
+/**
+* Reperesent GraphQL object type
+*/
+export interface ObjectType {
+    name: string;
+    fields: Field[];
+    isEntity: boolean;
+}
+
+
+  
+/**
+ * Reperenst GraphQL object type field
+ * @constructor(name: string, type: string, nullable: boolean = true, isBuildinType: boolean = true, isList = false)
+ */
+export class Field {
+    // GraphQL field name
+    name: string;
+    // GraphQL field type
+    type: string;
+    // Is field type built-in or not
+    isBuildinType: boolean;
+    // Is field nullable or not
+    nullable: boolean;
+    // Is field a list. eg: post: [Post]
+    isList: boolean;
+
+
+    constructor(name: string, 
+        type: string, 
+        nullable = true, 
+        isBuildinType = true, 
+        isList = false) {
+        this.name = name;
+        this.type = type;
+        this.nullable = nullable;
+        this.isBuildinType = isBuildinType;
+        this.isList = isList;
+    }
+
+    /**
+     * Create a string from name, type properties in the 'name:type' format. If field is not nullable
+     * it adds exclamation mark (!) at then end of string
+     */
+    format(): string {
+      const colon = ':';
+      const columnType = this.columnType();
+      let column: string = columnType === 'string' ? this.name : this.name.concat(colon, columnType);
+
+      if (!this.isBuildinType && !this.isList && this.type !== 'otm' && this.type !== 'mto') {
+        column = this.name.concat(colon, 'oto');
+      } else if (this.isBuildinType && this.isList) {
+        column = this.name + colon + 'array' + columnType;
+      }
+      return this.nullable ? column : column + '!';
+    }
+
+    columnType(): string {
+      return this.isBuildinType ? availableTypes[this.type] : this.type;
+    }
+
+    isArray(): boolean {
+        return this.isBuildinType && this.isList;
+    }
+
+    isScalar(): boolean {
+        return this.isBuildinType && !this.isList;
+    }
+}

+ 4 - 0
query-node/substrate-query-framework/cli/src/model/index.ts

@@ -0,0 +1,4 @@
+import { FTSQuery } from './FTSQuery';
+import { WarthogModel, ObjectType, Field } from './WarthogModel';
+
+export { FTSQuery, WarthogModel, ObjectType, Field } 

+ 14 - 0
query-node/substrate-query-framework/cli/src/templates/db-helper.mst

@@ -0,0 +1,14 @@
+import { getConnection } from "typeorm";
+import * as shortid from "shortid";
+
+/**
+ * Fixes compatibility between typeorm and warthog by filling id, createById and version
+ * @param entity Instance of the entity
+ */
+export function insertToDatabase<T>(entity: T) {
+  entity["id"] = shortid.generate();
+  entity["createdById"] = shortid.generate();
+  entity["version"] = 1;
+
+  getConnection().getRepository(entity.constructor.name).insert(entity);
+}

+ 9 - 0
query-node/substrate-query-framework/cli/src/templates/dotenv-ormconfig.mst

@@ -0,0 +1,9 @@
+TYPEORM_CONNECTION=postgres
+TYPEORM_HOST=localhost
+TYPEORM_USERNAME=postgres
+TYPEORM_PASSWORD=postgres
+TYPEORM_DATABASE=postgres
+TYPEORM_PORT=5432
+TYPEORM_SYNCHRONIZE=false
+TYPEORM_LOGGING=true
+TYPEORM_ENTITIES=entities/*.ts,node_modules/index-builder/lib/db/entities.js,../graphql-server/src/modules/**/*.model.ts

+ 1 - 0
query-node/substrate-query-framework/cli/src/templates/dotenv.mst

@@ -0,0 +1 @@
+WS_PROVIDER_ENDPOINT_URI=ws://localhost:9944

+ 59 - 0
query-node/substrate-query-framework/cli/src/templates/entities/model.ts.mst

@@ -0,0 +1,59 @@
+import {
+  BaseModel,
+  {{#has.bool}}BooleanField,{{/has.bool}}
+  {{#has.date}}DateField,{{/has.date}}
+  {{#has.float}}FloatField,{{/has.float}}
+  {{#has.int}}IntField,{{/has.int}}
+  {{#has.numeric}}NumericField,{{/has.numeric}}
+  {{#has.json}}JSONField,{{/has.json}}
+  {{#has.bytes}}BytesField,{{/has.bytes}}
+  Model,
+  {{#has.mto}}ManyToOne,{{/has.mto}}
+  {{#has.otm}}OneToMany,{{/has.otm}}
+  {{#has.oto}}OneToOne, JoinColumn,{{/has.oto}}
+  {{#has.array}}CustomField,{{/has.array}}
+  StringField
+} from 'warthog';  {{! we don't need extra twists here }}
+
+{{#fields}}
+  {{#is.refType}}
+  import { {{relClassName}} } from '{{{relPathForModel}}}'
+  {{/is.refType}}
+{{/fields}}
+
+@Model()
+export class {{className}} extends BaseModel {
+{{#fields}}
+  {{#is.otm}}
+    @OneToMany(() => {{relClassName}}, ({{relCamelName}}: {{relClassName}}) => {{relCamelName}}.{{relFieldName}})
+    {{camelNamePlural}}?: {{relClassName}}[];  
+  {{/is.otm}}
+
+  {{#is.mto}}
+    @ManyToOne(() => {{relClassName}}, ({{relCamelName}}: {{relClassName}}) => {{relCamelName}}.{{relFieldNamePlural}}, { 
+      skipGraphQLField: true{{^required}},
+      nullable: true{{/required}} 
+    })
+    {{camelName}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{relClassName}};
+  {{/is.mto}}
+
+  {{#is.oto}}
+    @OneToOne(() => {{relClassName}})
+    @JoinColumn()
+    {{camelName}}?: {{relClassName}};
+  {{/is.oto}}
+
+  {{#is.array}}
+    @CustomField({ db: { type: '{{dbType}}', array: true{{^required}}, nullable: true{{/required}} }, 
+        api: { type: '{{apiType}}'{{^required}}, nullable: true{{/required}} }})
+    {{camelName}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{tsType}}[];
+  {{/is.array}}
+
+  {{! TODO: add enums here }}
+  {{#is.scalar}}
+    @{{decorator}}({{^required}}{ nullable: true }{{/required}})
+    {{camelName}}{{^required}}?{{/required}}{{#required}}!{{/required}}: {{tsType}};
+  {{/is.scalar}}
+
+{{/fields}}
+}

+ 65 - 0
query-node/substrate-query-framework/cli/src/templates/entities/resolver.ts.mst

@@ -0,0 +1,65 @@
+import { Arg, Args, Mutation, Query, Resolver } from 'type-graphql';
+import { Inject } from 'typedi';
+import { Fields, StandardDeleteResponse, UserId } from 'warthog';
+
+import {
+  {{className}}CreateInput,
+  {{className}}CreateManyArgs,
+  {{className}}UpdateArgs,
+  {{className}}WhereArgs,
+  {{className}}WhereInput,
+  {{className}}WhereUniqueInput
+} from '{{{generatedFolderRelPath}}}';
+
+import { {{className}} } from './{{kebabName}}.model';
+import { {{className}}Service } from './{{kebabName}}.service';
+
+@Resolver({{className}})
+export class {{className}}Resolver {
+  constructor(@Inject('{{className}}Service') public readonly service: {{className}}Service) {}
+
+  @Query(() => [{{className}}])
+  async {{camelNamePlural}}(
+    @Args() { where, orderBy, limit, offset }: {{className}}WhereArgs,
+    @Fields() fields: string[]
+  ): Promise<{{className}}[]> {
+    return this.service.find<{{className}}WhereInput>(where, orderBy, limit, offset, fields);
+  }
+
+  @Query(() => {{className}})
+  async {{camelName}}(@Arg('where') where: {{className}}WhereUniqueInput): Promise<{{className}}> {
+    return this.service.findOne<{{className}}WhereUniqueInput>(where);
+  }
+
+  @Mutation(() => {{className}})
+  async create{{className}}(
+    @Arg('data') data: {{className}}CreateInput,
+    @UserId() userId: string
+  ): Promise<{{className}}> {
+    return this.service.create(data, userId);
+  }
+
+  @Mutation(() => [{{className}}])
+  async createMany{{className}}s(
+    @Args() { data }: {{className}}CreateManyArgs,
+    @UserId() userId: string
+  ): Promise<{{className}}[]> {
+    return this.service.createMany(data, userId);
+  }
+
+  @Mutation(() => {{className}})
+  async update{{className}}(
+    @Args() { data, where }: {{className}}UpdateArgs,
+    @UserId() userId: string
+  ): Promise<{{className}}> {
+    return this.service.update(data, where, userId);
+  }
+
+  @Mutation(() => StandardDeleteResponse)
+  async delete{{className}}(
+    @Arg('where') where: {{className}}WhereUniqueInput,
+    @UserId() userId: string
+  ): Promise<StandardDeleteResponse> {
+    return this.service.delete(where, userId);
+  }
+}

+ 15 - 0
query-node/substrate-query-framework/cli/src/templates/entities/service.ts.mst

@@ -0,0 +1,15 @@
+import { Service } from 'typedi';
+import { Repository } from 'typeorm';
+import { InjectRepository } from 'typeorm-typedi-extensions';
+import { BaseService } from 'warthog';
+
+import { {{className}} } from './{{kebabName}}.model';
+
+@Service('{{className}}Service')
+export class {{className}}Service extends BaseService<{{className}}> {
+  constructor(
+    @InjectRepository({{className}}) protected readonly repository: Repository<{{className}}>
+  ) {
+    super({{className}}, repository);
+  }
+}

+ 5 - 0
query-node/substrate-query-framework/cli/src/templates/event-class-defination.mst

@@ -0,0 +1,5 @@
+export default class {{input.name}} {
+  {{#input.fields}}
+    {{name}}: {{type}}
+  {{/input.fields}}
+}

+ 45 - 0
query-node/substrate-query-framework/cli/src/templates/graphql-server.index.mst

@@ -0,0 +1,45 @@
+import 'reflect-metadata';
+
+import { SnakeNamingStrategy } from 'warthog';
+import { snakeCase } from 'typeorm/util/StringUtils';
+
+import { loadConfig } from '../src/config';
+import { Logger } from '../src/logger';
+
+import { getServer } from './server';
+
+class CustomNamingStrategy extends SnakeNamingStrategy {
+  constructor() {
+    super();
+  }
+  tableName(className: string, customName?: string): string {
+    return customName ? customName : `${snakeCase(className)}`;
+  }
+  columnName(propertyName: string, customName?: string, embeddedPrefixes?: string[]): string {
+    return propertyName;
+  }
+}
+
+async function bootstrap() {
+  await loadConfig();
+
+  const server = getServer({}, { namingStrategy: new CustomNamingStrategy() });
+
+  // Create database tables. Warthog migrate command does not support CustomNamingStrategy thats why
+  // we have this code
+  const syncDatabase: string | undefined = process.env.SYNC;
+  if (syncDatabase) {
+    await server.establishDBConnection();
+    process.exit(0);
+  }
+
+  await server.start();
+}
+
+bootstrap().catch((error: Error) => {
+  Logger.error(error);
+  if (error.stack) {
+    Logger.error(error.stack.split('\n'));
+  }
+  process.exit(1);
+});

+ 149 - 0
query-node/substrate-query-framework/cli/src/templates/index-builder-entry.mst

@@ -0,0 +1,149 @@
+import "reflect-metadata";
+import * as dotenv from "dotenv";
+import * as chalk from 'chalk';
+import * as figlet from 'figlet';
+import { Command } from 'commander';
+import { configure, getLogger } from 'log4js';
+
+import { createConnection } from "typeorm";
+import { {{typeRegistrator}} } from '{{{packageName}}}';
+import {
+  QueryNodeManager,
+  BootstrapPack,
+  BootstrapFunc,
+  DatabaseManager,
+  SubstrateEvent
+} from "index-builder/lib";
+
+// Mappings use!
+export { DatabaseManager as DB, getLogger, SubstrateEvent };
+
+const logger = getLogger();
+
+function getProcessingPack() {
+  const handlers = require("../../mappings");
+
+  let processingPack: { [key: string]: any } = {};
+
+  Object.keys(handlers)
+    .filter(handler => handler.startsWith("handle"))
+    .map((handler: string) => {
+      let eventName = handler.replace("handle", "");
+      processingPack[eventName] = handlers[handler];
+    });
+  return processingPack;
+}
+
+function getBootstrapPack() {
+  const bootstrapPack: BootstrapPack = { pack: [] };
+  if (!process.env.BOOTSTRAP_PACK_LOCATION) {
+      // TODO: very basic, we should read form the config file
+      throw new Error(`No boostrap location found. 
+Please set BOOTSTRAP_FILE_LOCATION environment variable`)
+  }
+  let handlers: { [key: string]: BootstrapFunc } = {};
+  try {
+    handlers = require(process.env.BOOTSTRAP_PACK_LOCATION);
+  } catch (e) {
+    throw new Error(`Cannot load bootstrap mappings from ${process.env.BOOTSTRAP_FILE_LOCATION}: ${e}`);
+  }
+  
+
+  Object.keys(handlers)
+    .filter(handler => handler.startsWith("boot"))
+    .map((handler: string) => bootstrapPack.pack.push(handlers[handler]));
+
+  return bootstrapPack;
+}
+
+function banner():Command {
+    console.log(
+        chalk.green(
+          figlet.textSync('Joystream-Indexer')
+        )
+    );
+    const program = new Command();
+    const version = require('./package.json').version;
+
+    program
+        .version(version)
+        .description("Joystream Indexer")
+        .option('-b, --bootstrap', 'Load initial data using the boot* mappings')
+        .option('-l, --logging <file>', 'Path to log4js config', null)
+        .option('-h, --height <height>', 'Block height to start from', 0)
+        .option('-e, --env <file>', '.env file location', '../../.env')
+        .option('-n, --no-start', "Do not run the indexer");
+
+    
+    program.parse(process.argv);
+    
+    return program;
+}
+
+function setUp(opts: any) {
+      
+    const height:string = opts.height || '0';
+    process.env.BLOCK_HEIGHT = height;
+
+    if (opts.bootstrap) {
+        process.env.QUERY_NODE_BOOTSTRAP_DB = 'true'
+    }
+
+    // dotenv config
+    dotenv.config({ path: opts.env });
+
+    //log4js config
+    if (opts.logging) {
+        configure(opts.logging)
+    } else {
+        // log4js default: DEBUG to console output;
+        getLogger().level = 'debug';
+    }
+    
+}
+
+async function doBootstrap(node: QueryNodeManager) {
+    await node.bootstrap(
+        process.env.WS_PROVIDER_ENDPOINT_URI as string,
+        getBootstrapPack(),
+        {{typeRegistrator}}
+    );
+    logger.info("Bootstrap done");
+}
+
+async function main() {
+  const command = banner();  
+  setUp(command);
+  
+  const providerUri = process.env.WS_PROVIDER_ENDPOINT_URI;
+
+  if (!providerUri) {
+    throw new Error(
+      "WS_PROVIDER_ENDPOINT_URI environment variable is not set! Make sure you set it in your .env file or system wide."
+    );
+  }
+  
+  // Required for warthog models.
+  dotenv.config({ path: '../graphql-server/.env' });
+  await createConnection();
+
+  const node = new QueryNodeManager(process);
+
+  const bootstrap = process.env.QUERY_NODE_BOOTSTRAP_DB;
+  if (bootstrap) {
+    await doBootstrap(node) 
+  }
+
+  if (!command.start) {
+      logger.info("The --no-start flag was set to true, the indexer will not start");
+      process.exit(0);
+  }
+
+  node.start(providerUri, getProcessingPack(), {{typeRegistrator}});
+}
+
+main().catch((e) => {
+    console.error(e);
+    process.exit(1)
+});
+

+ 37 - 0
query-node/substrate-query-framework/cli/src/templates/indexer.package.json

@@ -0,0 +1,37 @@
+{
+    "name": "indexer",
+    "version": "1.0.0",
+    "main": "index.js",
+    "license": "MIT",
+    "scripts": {
+      "start": "ts-node index.ts",
+        "start:dev": "DEBUG=index-builder:* ts-node index.ts --bootstrap",
+        "bootstrap": "ts-node index.ts --no-start --bootstrap",
+        "bootstrap:dev": "DEBUG=index-builder:* ts-node index.ts --no-start --bootstrap",
+        "postinstall": "rm -rf node_modules/index-builder/node_modules"
+    },
+    "dependencies": {
+      "@joystream/types": "^0.8.0",
+      "@types/bn.js": "^4.11.6",
+      "@types/shortid": "^0.0.29",
+      "bn.js": "^5.1.1",
+      "chalk": "^4.0.0",
+      "dotenv": "^8.0.0",
+      "figlet": "^1.4.0",
+      "index-builder": "file:../../../substrate-query-framework/index-builder",
+      "lodash": "^4.17.15",
+      "log4js": "^6.2.1",
+      "pg": "^8.0.3",
+      "reflect-metadata": "^0.1.13",
+      "shortid": "^2.2.15",
+      "typeorm": "^0.2.24",
+      "typeorm-model-generator": "^0.4.2"
+    },
+    "resolutions": {
+      "tslib": "1.11.2"
+    },
+    "devDependencies": {
+      "@types/figlet": "^1.2.0"
+    }
+}
+  

+ 19 - 0
query-node/substrate-query-framework/cli/src/templates/indexer.tsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "declaration": true,
+    "importHelpers": true,
+    "module": "commonjs",
+    "outDir": "lib",
+    "rootDir": "src",
+    "strict": true,
+    "target": "es2017",
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "skipLibCheck": true,
+    "sourceMap": true,
+    "inlineSources": false,
+    "strictPropertyInitialization": false
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules"]
+}

+ 7 - 0
query-node/substrate-query-framework/cli/src/templates/mappings.mst

@@ -0,0 +1,7 @@
+import { QueryEvent } from './index-builder';
+import { insertToDatabase } from './helper';
+
+// This is an example you can delete it :)
+export function handleExtrinsicSuccess(event: QueryEvent) {
+  console.log(event.event_name);
+}

+ 8 - 0
query-node/substrate-query-framework/cli/src/templates/processing-pack.mst

@@ -0,0 +1,8 @@
+import { QueryEventProcessingPack } from './index-builder';
+import { handleExtrinsicSuccess } from './mappings';
+
+const processingPack: QueryEventProcessingPack = {
+  ExtrinsicSuccess: handleExtrinsicSuccess,
+};
+
+export default processingPack;

+ 57 - 0
query-node/substrate-query-framework/cli/src/templates/textsearch/migration.ts.mst

@@ -0,0 +1,57 @@
+import { MigrationInterface, QueryRunner } from "typeorm";
+
+export class {{query.typePrefix}}Migration{{query.ts}} implements MigrationInterface {
+    name = '{{query.name}}Migration{{query.ts}}'
+
+    public async up(queryRunner: QueryRunner): Promise<void> {
+        // TODO: escape 
+    {{#query.documents}}    
+        await queryRunner.query(`
+            ALTER TABLE {{table}} 
+            ADD COLUMN {{tsvColumn}} tsvector 
+            GENERATED ALWAYS AS (  
+                {{#fields}}
+                    setweight(to_tsvector('{{query.language}}', coalesce({{{column}}}, '')), '{{weight}}') {{^last}} || {{/last}}
+                {{/fields}}
+                ) 
+            STORED;
+        `);
+        await queryRunner.query(`
+            ALTER TABLE {{table}} 
+            ADD COLUMN {{docColumn}} text 
+            GENERATED ALWAYS AS (  
+                {{#fields}}
+                    coalesce({{{column}}}, '') {{^last}} || {{/last}}
+                {{/fields}}
+                ) 
+            STORED;
+        `);
+        await queryRunner.query(`CREATE INDEX {{index_name}} ON {{table}} USING GIN ({{tsvColumn}})`);
+    {{/query.documents}}
+
+        await queryRunner.query(`
+            CREATE VIEW {{query.viewName}} AS
+        {{#query.documents}}    
+            SELECT 
+                text '{{table}}' AS origin_table, id, {{tsvColumn}} AS tsv, {{docColumn}} AS document 
+            FROM
+                {{table}}
+            {{^last}}    
+            UNION ALL
+            {{/last}}
+        {{/query.documents}}    
+        `);
+
+    }
+
+    public async down(queryRunner: QueryRunner): Promise<void> {
+        await queryRunner.query(`DROP VIEW {{query.viewName}}`);
+    {{#query.documents}}    
+        await queryRunner.query(`DROP INDEX {{index_name}}`);
+        await queryRunner.query(`ALTER TABLE {{table}} DROP COLUMN {{tsvColumn}}`);
+        await queryRunner.query(`ALTER TABLE {{table}} DROP COLUMN {{docColumn}}`);
+    {{/query.documents}}    
+    }
+
+
+}

+ 46 - 0
query-node/substrate-query-framework/cli/src/templates/textsearch/resolver.ts.mst

@@ -0,0 +1,46 @@
+import { ObjectType, Field, Float, Int, Arg, Query, Resolver, createUnionType } from 'type-graphql';
+import { Inject } from 'typedi';
+{{#entities}}  
+import { {{type}} } from '../{{model}}/{{model}}.model';
+{{/entities}}  
+import { {{query.typePrefix}}FTSService } from './{{query.name}}.service';
+
+@ObjectType()
+export class {{query.typePrefix}}FTSOutput {
+    @Field(type => {{query.typePrefix}}SearchItem)
+    item!: typeof {{query.typePrefix}}SearchItem
+
+    @Field(type => Float)
+    rank!: number
+
+    @Field(type => String)
+    isTypeOf!: string
+
+    @Field(type => String)
+    highlight!: string
+}
+
+export const {{query.typePrefix}}SearchItem = createUnionType({
+    name: "{{query.typePrefix}}SearchResult",
+    types: () => [
+    {{#entities}}  
+        {{type}},
+    {{/entities}}     
+    ],
+});
+
+
+@Resolver()
+export default class {{query.typePrefix}}FTSResolver {
+
+    constructor(@Inject('{{query.typePrefix}}FTSService') readonly service: {{query.typePrefix}}FTSService) {}
+
+    @Query(() => [{{query.typePrefix}}FTSOutput])
+    async {{query.name}}(
+        @Arg('text') query: string, 
+        @Arg('limit', () => Int, { defaultValue: 5 }) limit: number):Promise<Array<{{query.typePrefix}}FTSOutput>>{
+        
+        return this.service.search(query, limit);
+    }
+
+}

+ 80 - 0
query-node/substrate-query-framework/cli/src/templates/textsearch/service.ts.mst

@@ -0,0 +1,80 @@
+import { Service } from 'typedi';
+{{#entities}}  
+import { {{type}} } from '../{{model}}/{{model}}.model';
+{{/entities}}  
+
+import { InjectRepository } from 'typeorm-typedi-extensions';
+import { Repository, getConnection } from 'typeorm';
+
+import { {{query.typePrefix}}FTSOutput } from './{{query.typePrefix}}.resolver';
+
+interface RawSQLResult {
+    origin_table: string,
+    id: string,
+    rank: number,
+    highlight: string
+}
+
+@Service('{{query.typePrefix}}FTSService')
+export class {{query.typePrefix}}FTSService {
+    {{#entities}}  
+    readonly {{fieldName}}Repository: Repository<{{type}}>;
+    {{/entities}} 
+
+    constructor({{#entities}}@InjectRepository({{type}}) {{fieldName}}Repository: Repository<{{type}}>{{^last}},{{/last}}
+                 {{/entities}}) {
+        {{#entities}}  
+        this.{{fieldName}}Repository = {{fieldName}}Repository;
+        {{/entities}} 
+    }
+
+    async search(text: string, limit:number = 5): Promise<{{query.typePrefix}}FTSOutput[]> {
+        const connection = getConnection();
+		const queryRunner = connection.createQueryRunner();
+        // establish real database connection using our new query runner
+		await queryRunner.connect();
+        await queryRunner.startTransaction('REPEATABLE READ');
+
+        try {    
+            const query = `
+            SELECT origin_table, id, 
+                ts_rank(tsv, phraseto_tsquery('{{query.language}}', $1)) as rank,
+                ts_headline(document, phraseto_tsquery('{{query.language}}', $1)) as highlight
+            FROM {{query.viewName}}
+            WHERE phraseto_tsquery('{{query.language}}', $1) @@ tsv
+            ORDER BY rank DESC
+            LIMIT $2`;
+
+            const results = await queryRunner.query(query, [text, limit]) as RawSQLResult[];
+
+            if (results.length == 0) {
+                return [];
+            }
+
+            const idMap:{ [id:string]: RawSQLResult } = {};
+            results.forEach(item => idMap[item.id] = item);
+            const ids: string[] = results.map(item => item.id);
+            
+            {{#entities}}
+            const {{arrayName}}: {{type}}[] = await this.{{fieldName}}Repository.createQueryBuilder()
+                        .where("id IN (:...ids)", { ids }).getMany();
+            {{/entities}}
+
+            const enhancedEntities = [{{#entities}}...{{arrayName}} {{^last}},{{/last}}{{/entities}}].map((e) => {
+                return { item: e, 
+                    rank: idMap[e.id].rank, 
+                    highlight: idMap[e.id].highlight,
+                    isTypeOf: idMap[e.id].origin_table } as {{query.typePrefix}}FTSOutput;
+            });
+            
+            return enhancedEntities.reduce((accum: {{query.typePrefix}}FTSOutput[], entity) => {
+                if (entity.rank > 0) {
+                    accum.push(entity);
+                }
+                return accum;
+            }, []).sort((a,b) => b.rank - a.rank);
+        } finally {
+            await queryRunner.commitTransaction();
+        }
+    }
+}

+ 40 - 0
query-node/substrate-query-framework/cli/src/templates/warthog.env.yml

@@ -0,0 +1,40 @@
+default_env: &default_env
+    WARTHOG_DB_SYNCHRONIZE: false  
+    ## if the DB is synced, the new columns 
+    ## created by migrations are dropped, no good
+    ## one should run `WARTHOG_DB_SYNCHRONIZE=true yarn sync` manually instead
+    WARTHOG_DB_OVERRIDE: false
+    WARTHOG_DB_DATABASE: {env:DB_NAME}
+    WARTHOG_DB_USERNAME: {env:DB_USER}
+    WARTHOG_DB_PASSWORD: {env:DB_PASS}
+    WARTHOG_DB_HOST: {env:DB_HOST}
+    WARTHOG_DB_PORT: {env:DB_PORT}
+    WARTHOG_APP_PORT: {env:GRAPHQL_SERVER_PORT}
+
+
+development:
+    <<: *default_env
+    DEBUG: '*'
+    NODE_ENV: development
+    WARTHOG_APP_HOST: localhost
+    WARTHOG_APP_PORT: 4000
+    WARTHOG_DB_HOST: localhost
+    WARTHOG_DB_LOGGING: all
+    WARTHOG_DB_DATABASE: query_node
+    WARTHOG_DB_PASSWORD: postgres
+    WARTHOG_DB_USERNAME: postgres
+    WARTHOG_DB_PORT: 5432
+    ## These envs should be set for typeorm??
+    PGDATABASE: query_node
+    PGUSER: postgres
+    PGPASSWORD: postgres
+    PGHOST: localhost
+    PGPORT: 5432
+    GRAPHQL_SERVER_PORT: 4000
+
+staging:
+    <<: *default_env
+
+production:
+    <<: *default_env
+  

+ 35 - 0
query-node/substrate-query-framework/cli/src/utils/utils.ts

@@ -0,0 +1,35 @@
+import * as fs from 'fs-extra';
+import * as path from 'path';
+
+export function createDir(path: string, del = false, recursive = false):void {
+  if (!fs.existsSync(path)) {
+    fs.mkdirSync(path, { recursive });
+  }
+  if (del) {
+    fs.removeSync(path);
+    fs.mkdirSync(path);
+  }
+}
+
+export function createFile(path: string, content = '', replace =false):void {
+  if (!fs.existsSync(path) || replace) {
+    fs.writeFileSync(path, content);
+  }
+}
+
+export async function copyFiles(from: string, to: string): Promise<void> {
+  try {
+    await fs.copy(from, to);
+  } catch (err) {
+    console.error(err);
+  }
+}
+
+export function getTemplatePath(template: string): string {
+  const templatePath = path.resolve(__dirname, '..', 'templates', template);
+  if (!fs.existsSync(templatePath)) {
+    console.error(`Tempate ${template} does not exists!`);
+    process.exit(1);
+  }
+  return templatePath;
+}

+ 19 - 0
query-node/substrate-query-framework/cli/test/fixtures/multiple-entities.graphql

@@ -0,0 +1,19 @@
+type Category @entity {
+  id: ID!,
+  name: String! @fulltext(query: "search"),
+  parent_category: Category,
+}
+
+type Thread @entity {
+  id: ID!,
+  category: Category!,
+  title: String @fulltext(query: "search"),
+  initial_body_text: String @fulltext(query: "search")
+}
+
+type Post @entity {
+  id: ID!,
+  thread: Thread!,
+  index: Int!,
+  initial_body_text: String @fulltext(query: "search")
+}

+ 17 - 0
query-node/substrate-query-framework/cli/test/fixtures/multiple-queries.graphql

@@ -0,0 +1,17 @@
+type Membership @entity {
+  # Member's root account id
+  accountId: String!
+
+  # Member's id
+  memberId: Int!
+
+  # The unique handle chosen by member
+  handle: String @fulltext(query: "handles1")
+
+  # A Url to member's Avatar image
+  avatarUri: String
+
+  # Short text chosen by member to share information about themselves
+  about: String  @fulltext(query: "handles2")
+}
+

+ 4 - 0
query-node/substrate-query-framework/cli/test/fixtures/non-string-query.graphql

@@ -0,0 +1,4 @@
+type Cat {
+  # should fail
+  memberId: Int! @fulltext(query:"meow") 
+}

+ 17 - 0
query-node/substrate-query-framework/cli/test/fixtures/single-type.graphql

@@ -0,0 +1,17 @@
+type Membership {
+  # Member's root account id
+  accountId: String!
+
+  # Member's id
+  memberId: Int!
+
+  # The unique handle chosen by member
+  handle: String @fulltext(query: "handles")
+
+  # A Url to member's Avatar image
+  avatarUri: String
+
+  # Short text chosen by member to share information about themselves
+  about: String
+}
+

+ 39 - 0
query-node/substrate-query-framework/cli/test/helpers/FTSQueryRenderer.test.ts

@@ -0,0 +1,39 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+/* eslint-disable @typescript-eslint/no-var-requires */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+import { createModel } from './model';
+import * as fs from 'fs-extra';
+import { FTSQueryRenderer } from '../../src/generate/FTSQueryRenderer';
+import * as chai from 'chai';
+
+const chaiSnapshot = require('mocha-chai-snapshot');
+const { expect } = chai;
+chai.use(chaiSnapshot);
+
+
+describe('FTSQueryRenderer', () => {
+    let generator: FTSQueryRenderer;
+
+    before(() => {
+        // set timestamp in the context to make the output predictable
+        generator = new FTSQueryRenderer({"ts": 111111111});
+    })
+
+    it('Should generate migration', function() {
+        const warthogModel = createModel();
+
+        warthogModel.addQueryClause("test1", "initial_body_text", "Post");
+        warthogModel.addQueryClause("test1", "title", "Post");
+        warthogModel.addQueryClause("test1", "initial_body_text", "Thread");
+        warthogModel.addQueryClause("test1", "title", "Thread");
+        
+        const templateData = fs.readFileSync('./src/templates/textsearch/migration.ts.mst', 'utf-8');
+        
+        const transformed = generator.generate(templateData, warthogModel.lookupQuery("test1"));
+
+        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+        (expect(transformed).to as any).matchSnapshot(this);
+        
+    })
+})

+ 136 - 0
query-node/substrate-query-framework/cli/test/helpers/ModelRenderer.test.ts

@@ -0,0 +1,136 @@
+import { ModelRenderer } from "../../src/generate/ModelRenderer";
+import { WarthogModel, Field, ObjectType } from '../../src/model';
+import { createModel, fromStringSchema } from './model';
+import * as fs from 'fs-extra';
+import { expect } from 'chai';
+import Debug from "debug";
+
+const debug = Debug('cli-test:model-renderer');
+
+describe('ModelRenderer', () => {
+  let generator: ModelRenderer;
+  let warthogModel: WarthogModel;
+  let modelTemplate: string;
+
+  before(() => {
+    // set timestamp in the context to make the output predictable
+    generator = new ModelRenderer();
+    modelTemplate = fs.readFileSync('./src/templates/entities/model.ts.mst', 'utf-8');
+
+  })
+
+  beforeEach(() => {
+    warthogModel = createModel();
+  })
+
+  it('should transform fields to camelCase', function () {
+
+    warthogModel.addField("Post", new Field("CamelCase", "String"));
+    warthogModel.addField("Post", new Field("snake_case", "String"));
+    warthogModel.addField("Post", new Field("kebab-case", "String"));
+
+    const rendered = generator.generate(modelTemplate, warthogModel.lookupType("Post"));
+
+    debug(`rendered: ${JSON.stringify(rendered, null, 2)}`);
+
+    expect(rendered).to.include('camelCase?: string', 'Should camelCase correctly');
+    expect(rendered).to.include('snakeCase?: string', 'Should camelCase correctly');
+    expect(rendered).to.include('kebabCase?: string', 'Should camelCase correctly');
+
+  })
+
+  it('should render ClassName', function () {
+    warthogModel.addObjectType({
+      name: `some_randomEntity`,
+      isEntity: true,
+      fields: [new Field("a", "String")]
+    } as ObjectType);
+
+    const rendered = generator.generate(modelTemplate, warthogModel.lookupType("some_randomEntity"));
+    debug(`rendered: ${JSON.stringify(rendered, null, 2)}`);
+
+    expect(rendered).to.include('export class SomeRandomEntity extends BaseModel', 'Should render ClassName corretly');
+
+  })
+
+  it('should include imports', function () {
+
+    warthogModel.addField("Post", new Field("a", 'ID'));
+    warthogModel.addField("Post", new Field("b", 'String'));
+    warthogModel.addField("Post", new Field("c", 'Int'));
+    warthogModel.addField("Post", new Field("d", "Date"));
+    warthogModel.addField("Post", new Field("e", "Float"));
+    warthogModel.addField("Post", new Field("f", "BigInt"));
+    warthogModel.addField("Post", new Field("g", "BigDecimal"));
+    warthogModel.addField("Post", new Field("h", "Bytes"));
+    warthogModel.addField("Post", new Field("j", "Boolean"));
+
+    const rendered = generator.generate(modelTemplate, warthogModel.lookupType("Post"));
+
+    expect(rendered).to.include('BooleanField,', 'Should import BooleanField');
+    expect(rendered).to.include('DateField,', 'Should import DateField');
+    expect(rendered).to.include('FloatField,', 'Should import FloatField');
+    expect(rendered).to.include('IntField,', 'Should import IntField');
+    expect(rendered).to.include('NumericField,', 'Should import NumericField');
+    expect(rendered).to.include('BytesField,', 'Should import BytesField');
+
+
+  })
+
+  it('should render otm types', function() {
+    const model = fromStringSchema(`
+    type Author @entity {
+      posts: [Post!]
+    }
+    
+    type Post @entity {
+      title: String
+      author: Author!
+    }`)
+    const rendered = generator.generate(modelTemplate, model.lookupType("Author"));
+    debug(`rendered: ${JSON.stringify(rendered, null, 2)}`);
+
+    expect(rendered).to.include(`import { Post } from '../post/post.model`, `Should render imports`);
+    expect(rendered).to.include(`@OneToMany(() => Post, (post: Post) => post.author)`, 'Should render OTM decorator');
+    expect(rendered).to.include(`posts?: Post[];`, 'Should render plural references');
+  
+  })
+
+  it('should render mto types', function() {
+    const model = fromStringSchema(`
+    type Author @entity {
+      posts: [Post!]
+    }
+    
+    type Post @entity {
+      title: String
+      author: Author!
+    }`)
+    const rendered = generator.generate(modelTemplate, model.lookupType("Post"));
+    debug(`rendered: ${JSON.stringify(rendered, null, 2)}`);
+
+    expect(rendered).to.include(`import { Author } from '../author/author.model`, `Should render imports`);
+    expect(rendered).to.include(`@ManyToOne(() => Author, (author: Author) => author.posts, { 
+      skipGraphQLField: true 
+    })`, 'Should render MTO decorator'); // nullable: true is not includered?
+    expect(rendered).to.include(`author!: Author;`, 'Should render required referenced field');
+  
+  })
+
+  it('should renderer array types', function() {
+    const model = fromStringSchema(`
+    type Author @entity {
+      posts: [String]
+    }`)
+
+    const rendered = generator.generate(modelTemplate, model.lookupType("Author"));
+    debug(`rendered: ${JSON.stringify(rendered, null, 2)}`);
+    expect(rendered).to.include('CustomField,', 'Should import CustomField');
+    expect(rendered).to.include(`@CustomField`, 'Should decorate arrays with @CustomField');
+    expect(rendered).to.include(`db: { type: 'text', array: true, nullable: true }`, 'Should add db option')
+    expect(rendered).to.include(`api: { type: 'string', nullable: true }`, 'Should inclued api option')
+    expect(rendered).to.include('posts?: string[]', `should add an array field`);
+    
+  })
+
+})

+ 124 - 0
query-node/substrate-query-framework/cli/test/helpers/SchemaParser.test.ts

@@ -0,0 +1,124 @@
+import { GraphQLSchemaParser, SchemaNode, Visitor } from './../../src/helpers/SchemaParser';
+import { expect } from 'chai';
+import { FULL_TEXT_SEARCHABLE_DIRECTIVE } from './../../src/helpers/SchemaDirective';
+
+describe('SchemaParser', () => {
+    it('should fail on non-existent file', () => {
+            expect(() => new GraphQLSchemaParser('./non-existent')).to.throw('not found');
+    });
+    
+    it('should find a top-level entity', () => {
+       const schema = GraphQLSchemaParser.buildSchema(`
+            type Cat {
+                meow: String!
+            }
+       `); 
+       expect(GraphQLSchemaParser.createObjectTypeDefinations(schema)).length(1, 
+            "Should detect exactly one type");
+    });
+
+    it('should throw an error on invalid schema', () => {
+        const schema = `
+            _type Cat {
+                meow: String!
+            }`; 
+       expect(() => GraphQLSchemaParser.buildSchema(schema)).to.throw('Syntax Error');
+    });
+
+    it('should throw on unknown directive', () => {
+        const schema = `
+            type Cat {
+                meow: String! @undeclared
+            }`; 
+       expect(() => GraphQLSchemaParser.buildSchema(schema)).to.throw('Unknown directive');
+    });
+
+    it('should throw on wrong location', () => {
+        const schema = `
+            type Cat @${FULL_TEXT_SEARCHABLE_DIRECTIVE} {
+                meow: String! 
+            }`; 
+       expect(() => GraphQLSchemaParser.buildSchema(schema)).to.throw('may not be used on OBJECT');
+    });
+
+    it('should throw on wrong argument', () => {
+        const schema = `
+            type Cat {
+                meow: String! @${FULL_TEXT_SEARCHABLE_DIRECTIVE}(qquery: "dfd")
+            }`; 
+       expect(() => GraphQLSchemaParser.buildSchema(schema)).to.throw('"String!" is required');
+    });
+
+    it('should detect fields types and directives', () => {
+        const schema = `
+            type Cat {
+                meow: String! @${FULL_TEXT_SEARCHABLE_DIRECTIVE}(query: "dfd")
+            }`; 
+       const gSchema = GraphQLSchemaParser.buildSchema(schema);
+       const typeNodes = GraphQLSchemaParser.createObjectTypeDefinations(gSchema)
+       expect(typeNodes).length(1);
+       const node = typeNodes[0];
+       expect(node?.fields?.[0]?.directives?.[0]?.name?.value).eq(`${FULL_TEXT_SEARCHABLE_DIRECTIVE}`, 'Should find a directive');
+    });
+
+    // TODO: this test now failes because apparently __ prefixed types do not pass validation
+    //
+    // it('should detect fields types and directives', () => {
+    //     const schema = `
+    //         type __Skip {
+    //             a: String! 
+    //         }`; 
+    //    const gSchema = GraphQLSchemaParser.buildSchema(schema);
+    //    const typeNodes = GraphQLSchemaParser.createObjectTypeDefinations(gSchema)
+    //    expect(typeNodes).length(0,'Should ignore __ prefixed types');    
+    // })
+
+    it('should load file', () => {
+        const parser = new GraphQLSchemaParser('test/fixtures/single-type.graphql');
+        expect(parser.getTypeNames()).length(1, "Should detect one type");
+        expect(parser.getFields(parser.getObjectDefinations()[0])).length(5, "Should detect fields");
+    });
+
+    it('should visit directives', () => {
+        const parser = new GraphQLSchemaParser('test/fixtures/single-type.graphql');
+        const names: string[] = [];
+        const visitor: Visitor = {
+            visit: (path) => {
+                path.map((n: SchemaNode) => names.push(n.name.value));
+            }
+        }    
+        const directives: { [key:string]: Visitor } = {};
+        directives[`${FULL_TEXT_SEARCHABLE_DIRECTIVE}`] = visitor;
+        parser.dfsTraversal({
+            directives
+        });
+
+        expect(names).members(["Membership", "handle", `${FULL_TEXT_SEARCHABLE_DIRECTIVE}`], "Should detect full path");
+    });
+
+    // TODO: in order to allow multiple directives we need to switch off SDL validation
+    // in the parser. So this test is comment for the future use
+    //
+    // it('should support multiple directives', () => {
+    //     const parser = new GraphQLSchemaParser('test/fixtures/multiple-queries.graphql');
+    //     const queries: string[] = [];
+    //     const visitor: Visitor = {
+    //         visit: (path) => {
+    //             path.map((n: SchemaNode) => {
+    //                 if (n.kind === 'Directive') {
+    //                     queries.push((n.arguments?.[0].value as StringValueNode).value)    
+    //                 }
+                    
+    //             });
+    //         }
+    //     }    
+    //     parser.dfsTraversal({
+    //         directives: {
+    //             "fullTextSearchable": visitor
+    //         }
+    //     });
+
+    //     expect(queries).members(["handles1", "handles2"], "Should detect multiple queries on the same field");
+    // })
+
+});

+ 33 - 0
query-node/substrate-query-framework/cli/test/helpers/WarthogModel.test.ts

@@ -0,0 +1,33 @@
+import { createModel, threadObjType, postObjType } from "./model";
+import { expect } from 'chai';
+import { WarthogModel } from '../../src/model';
+
+describe('WarthogModel', () => {
+    let warthogModel: WarthogModel;
+
+    beforeEach(() => {
+        warthogModel = createModel();
+    })
+
+    it('Should lookup types', () => {
+        expect(warthogModel.lookupType("Thread")).eq(threadObjType, "Should find Thread type");
+        expect(warthogModel.lookupType("Post")).eq(postObjType, "Should find Post type");
+        
+        expect(() => warthogModel.lookupType("NoSuchType")).throw("No ObjectType");
+
+    })
+
+    it('Should lookup fields', () => {
+        const field = warthogModel.lookupField("Thread", "initial_body_text")
+        expect(field.type).equals("String", "Should lookup the field")
+
+        expect(() => warthogModel.lookupField("Thread", "noexistent")).throw("No field");
+    })
+
+    it('Should add queries', () => {
+        warthogModel.addQueryClause("test", "initial_body_text", "Thread");
+        warthogModel.addQueryClause("test", "initial_body_text", "Post");
+        expect(warthogModel.ftsQueries).length(1, "Should add a query");
+        expect(warthogModel.ftsQueries[0].clauses).length(2, "Should add two clauses");
+    })
+})

+ 28 - 0
query-node/substrate-query-framework/cli/test/helpers/WarthogModelBuilder.test.ts

@@ -0,0 +1,28 @@
+import { WarthogModelBuilder } from './../../src/helpers/WarthogModelBuilder';
+import { expect } from 'chai';
+
+describe('WarthogModelBuild', () => {
+    it('should add multi-field multi-entity FTSQuery to the model', () => {
+        const generator = new WarthogModelBuilder('test/fixtures/multiple-entities.graphql');
+        const model = generator.buildWarthogModel()
+
+        expect(model.ftsQueries).length(1, "Should detect a query");
+        const query = model.ftsQueries[0];
+        expect(query.clauses).length(4, "The query should contain four clauses");
+        const entities: string[] = [];
+        const fields: string[] = [];
+        query.clauses.map((c) =>  {
+            entities.push(c.entity.name);
+            fields.push(c.field.name);
+        })
+        expect(entities).to.include.members(["Category", "Thread", "Post"], "Should detect three entities");
+        expect(fields).to.include.members(["initial_body_text", "name"], "Should detect fields");
+        
+    });
+
+    it('should detect multiple queries', () => {
+        const generator = new WarthogModelBuilder('test/fixtures/multiple-queries.graphql');
+        const model = generator.buildWarthogModel();
+        expect(model.ftsQueries).length(2, "Should detect two queries");
+    })
+});

File diff suppressed because it is too large
+ 0 - 0
query-node/substrate-query-framework/cli/test/helpers/__snapshots__/FTSQueryRenderer.test.ts.snap.js


+ 41 - 0
query-node/substrate-query-framework/cli/test/helpers/model.ts

@@ -0,0 +1,41 @@
+/* eslint-disable @typescript-eslint/no-unsafe-member-access */
+/* eslint-disable @typescript-eslint/no-unsafe-assignment */
+/* eslint-disable @typescript-eslint/no-unsafe-call */
+import { WarthogModel, Field } from "../../src/model";
+
+import * as tmp from 'tmp';
+import * as fs from 'fs-extra';
+import { WarthogModelBuilder } from '../../src/helpers/WarthogModelBuilder';
+
+const threadObjType = {
+    name: "Thread",
+    isEntity: true,
+    fields: [new Field("initial_body_text", "String"),  
+        new Field("title", "String"), 
+        new Field("id", "ID")]
+}
+
+const postObjType = {
+    name: "Post",
+    isEntity: true,
+    fields: [new Field("initial_body_text", "String"), 
+        new Field("title", "String"), 
+        new Field("id", "ID")]
+}
+
+const createModel = ():WarthogModel => {
+    const warthogModel = new WarthogModel();
+    warthogModel.addObjectType(threadObjType);
+    warthogModel.addObjectType(postObjType);
+    return warthogModel;
+}
+
+export function fromStringSchema (schema: string): WarthogModel {
+    const tmpobj = tmp.fileSync();
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
+    fs.writeSync(tmpobj.fd, schema);
+    const modelBuilder = new WarthogModelBuilder(tmpobj.name);
+    return modelBuilder.buildWarthogModel();
+}
+
+export { threadObjType, postObjType, createModel }

+ 5 - 0
query-node/substrate-query-framework/cli/test/mocha.opts

@@ -0,0 +1,5 @@
+--require ts-node/register
+--watch-extensions ts
+--recursive
+--reporter spec
+--timeout 5000

+ 9 - 0
query-node/substrate-query-framework/cli/test/tsconfig.json

@@ -0,0 +1,9 @@
+{
+  "extends": "../tsconfig",
+  "compilerOptions": {
+    "noEmit": true
+  },
+  "references": [
+    {"path": ".."}
+  ]
+}

+ 19 - 0
query-node/substrate-query-framework/cli/tsconfig.json

@@ -0,0 +1,19 @@
+{
+  "compilerOptions": {
+    "declaration": true,
+    "importHelpers": true,
+    "module": "commonjs",
+    "outDir": "lib",
+    "rootDir": ".",
+    "strict": true,
+    "target": "es2017",
+    "esModuleInterop": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "types": [ "node", "mocha" ],
+    "typeRoots": [
+        "./src/node_modules/@types"
+    ] 
+  },
+  "include": ["./src/**/*.ts", "./test/**/*.ts"]
+}

+ 11 - 0
query-node/substrate-query-framework/docker-compose.yml

@@ -0,0 +1,11 @@
+version: "3"
+
+services:
+  db:
+    image: postgres:12
+    restart: always
+    ports:
+      - "5432:5432"
+    environment:
+      POSTGRES_PASSWORD: postgres
+      POSTGRES_DB: query_node

+ 19 - 0
query-node/substrate-query-framework/index-builder/package.json

@@ -0,0 +1,19 @@
+{
+  "name": "index-builder",
+  "version": "1.0.0",
+  "main": "index.js",
+  "license": "MIT",
+  "scripts": {
+    "build": "tsc --build tsconfig.json",
+    "postinstall": "tsc --build tsconfig.json"
+  },
+  "dependencies": {
+    "@polkadot/api": "^0.96.1"
+  },
+  "devDependencies": {
+    "@polkadot/ts": "^0.3.14",
+    "@types/node": "^10",
+    "ts-node": "^8",
+    "typescript": "^3.8"
+  }
+}

+ 32 - 0
query-node/substrate-query-framework/index-builder/src/ISubstrateQueryService.ts

@@ -0,0 +1,32 @@
+import { Hash, Header, BlockNumber, EventRecord, SignedBlock } from '@polkadot/types/interfaces';
+import { Callback, Codec } from '@polkadot/types/types';
+import { UnsubscribePromise } from '@polkadot/api/types';
+import { ApiPromise} from '@polkadot/api';
+
+/**
+ * @description ...
+ */
+export default interface ISubstrateQueryService {
+
+    getFinalizedHead(): Promise<Hash>;
+    getHeader(hash?: Hash | Uint8Array | string): Promise<Header>;
+    subscribeNewHeads(v: Callback<Header> ): UnsubscribePromise;
+    getBlockHash(blockNumber?: BlockNumber | Uint8Array | number | string): Promise<Hash>;
+    getBlock(hash?: Hash | Uint8Array | string): Promise<SignedBlock>;
+    // Cut down from at: (hash: Hash | Uint8Array | string, ...args: Parameters<F>) => PromiseOrObs<ApiType, ObsInnerType<ReturnType<F>>>;
+    eventsAt(hash: Hash | Uint8Array | string): Promise<EventRecord[] & Codec>;
+    //eventsRange()
+    //events()   
+}
+
+export function makeQueryService(api: ApiPromise) : ISubstrateQueryService {
+
+    return  { 
+        getHeader: (hash?: Hash | Uint8Array | string) => { return api.rpc.chain.getHeader(hash)},
+        getFinalizedHead: () => { return api.rpc.chain.getFinalizedHead();}, 
+        subscribeNewHeads: (v: Callback<Header> ) => { return api.rpc.chain.subscribeNewHeads(v); },
+        getBlockHash: (blockNumber?: BlockNumber | Uint8Array | number | string) => { return api.rpc.chain.getBlockHash(blockNumber); },
+        getBlock: (hash?: Hash | Uint8Array | string) => { return api.rpc.chain.getBlock(hash); },
+        eventsAt: (hash: Hash | Uint8Array | string) => { return api.query.system.events.at(hash); }
+     } as ISubstrateQueryService;
+}

+ 103 - 0
query-node/substrate-query-framework/index-builder/src/IndexBuilder.ts

@@ -0,0 +1,103 @@
+// @ts-check
+
+import { getRepository, getConnection } from 'typeorm';
+
+import {
+  QueryBlockProducer,
+  QueryEventProcessingPack,
+  QueryEventBlock,
+  ISubstrateQueryService,
+  SavedEntityEvent,
+  makeDatabaseManager,
+  QueryEvent,
+} from '.';
+
+const debug = require('debug')('index-builder:indexer');
+
+export default class IndexBuilder {
+  private _producer: QueryBlockProducer;
+
+  private _processing_pack!: QueryEventProcessingPack;
+
+  private lastProcessedEvent!: SavedEntityEvent;
+
+  private constructor(producer: QueryBlockProducer, processing_pack: QueryEventProcessingPack) {
+    this._producer = producer;
+    this._processing_pack = processing_pack;
+  }
+
+  static create(service: ISubstrateQueryService, processing_pack: QueryEventProcessingPack): IndexBuilder {
+    const producer = new QueryBlockProducer(service);
+
+    return new IndexBuilder(producer, processing_pack);
+  }
+
+  async start() {
+    // check state
+
+    // STORE THIS SOMEWHERE
+    this._producer.on('QueryEventBlock', (query_event_block: QueryEventBlock): void => {
+      this._onQueryEventBlock(query_event_block);
+    });
+
+    debug('Spawned worker.');
+
+    const lastProcessedEvent = await getRepository(SavedEntityEvent).findOne({ where: { id: 1 } });
+
+    if (lastProcessedEvent !== undefined) {
+      this.lastProcessedEvent = lastProcessedEvent;
+      await this._producer.start(this.lastProcessedEvent.blockNumber, this.lastProcessedEvent.index);
+    } else {
+      // Setup worker
+      await this._producer.start();
+    }
+
+    debug('Started worker.');
+  }
+
+  async stop() {}
+
+  _onQueryEventBlock(query_event_block: QueryEventBlock): void {
+    debug(`Yay, block producer at height: #${query_event_block.block_number}`);
+
+    asyncForEach(query_event_block.query_events, async (query_event: QueryEvent) => {
+      if (!this._processing_pack[query_event.event_method]) {
+        debug(`Unrecognized: ` + query_event.event_name);
+
+        query_event.log(0, debug);
+      } else {
+        debug(`Recognized: ` + query_event.event_name);
+
+        const queryRunner = getConnection().createQueryRunner();
+        try {
+          // establish real database connection
+          await queryRunner.connect();
+          await queryRunner.startTransaction();
+
+          // Call event handler
+          await this._processing_pack[query_event.event_method](makeDatabaseManager(queryRunner.manager), query_event);
+
+          // Update last processed event
+          await SavedEntityEvent.update(query_event, queryRunner.manager);
+
+          await queryRunner.commitTransaction();
+        } catch (error) {
+          debug(`There are errors. Rolling back the transaction. Reason: ${error.message}`);
+
+          // Since we have errors lets rollback changes we made
+          await queryRunner.rollbackTransaction();
+          throw new Error(error);
+        } finally {
+          // Query runner needs to be released manually.
+          await queryRunner.release();
+        }
+      }
+    });
+  }
+}
+
+async function asyncForEach(array: Array<any>, callback: Function) {
+  for (let index = 0; index < array.length; index++) {
+    await callback(array[index], index, array);
+  }
+}

+ 171 - 0
query-node/substrate-query-framework/index-builder/src/QueryBlockProducer.ts

@@ -0,0 +1,171 @@
+import ISubstrateQueryService from './ISubstrateQueryService';
+import QueryEvent from './QueryEvent';
+import QueryEventBlock from './QueryEventBlock';
+import { Header, Extrinsic, EventRecord } from '@polkadot/types/interfaces';
+import { EventEmitter } from 'events';
+import * as assert from 'assert';
+import * as BN from 'bn.js';
+
+const DEBUG_TOPIC = 'index-builder:producer';
+
+var debug = require('debug')(DEBUG_TOPIC);
+
+export default class QueryBlockProducer extends EventEmitter {
+  private _started: boolean;
+
+  private _producing_blocks_blocks: boolean;
+
+  private readonly _query_service: ISubstrateQueryService;
+
+  private _new_heads_unsubscriber: () => void;
+
+  private _block_to_be_produced_next: BN;
+
+  // Index of the last processed event
+  private _last_processed_event_index: BN;
+
+  private _at_block: BN;
+
+  private _height_of_chain: BN;
+
+  constructor(query_service: ISubstrateQueryService) {
+    super();
+
+    this._started = false;
+    this._producing_blocks_blocks = false;
+    this._query_service = query_service;
+
+    // TODO
+    // need to set this up, when state is better, it
+    // will be refactored
+    this._new_heads_unsubscriber = () => {};
+
+    this._block_to_be_produced_next = new BN(0);
+    this._height_of_chain = new BN(0);
+    this._last_processed_event_index = new BN(0);
+    this._at_block = new BN(0)
+  }
+
+  // TODO: We cannot assume first block has events... we need more robust logic.
+  async start(at_block?: BN, at_event?: BN) {
+    if (this._started) throw Error(`Cannot start when already started.`);
+
+    // mark as started
+    this._started = true;
+
+    // Try to get initial header right away
+    this._height_of_chain = await this._query_service
+      .getFinalizedHead()
+      .then((hash) => {
+        return this._query_service.getHeader(hash);
+      })
+      .then((header) => {
+        return header.number.toBn();
+      });
+
+    if (at_block) {
+      this._block_to_be_produced_next = at_block;
+      this._at_block = at_block;
+
+      if (at_block.gt(this._height_of_chain)) throw Error(`Provided block is ahead of chain.`);
+    }
+
+    if (at_event) {
+      this._last_processed_event_index = at_event
+    }
+
+    //
+    this._new_heads_unsubscriber = await this._query_service.subscribeNewHeads((header) => {
+      this._OnNewHeads(header);
+    });
+
+    // Start producing blocks right away
+    if (!this._producing_blocks_blocks) this._produce_blocks();
+  }
+
+  async stop() {
+    if (!this._started) throw new Error(`Cannot stop when not already started.`);
+
+    // THIS IS VERY CRUDE, NEED TO MANAGE LOTS OF STUFF HERE!
+
+    (await this._new_heads_unsubscriber)();
+
+    this._started = false;
+  }
+
+  private async _OnNewHeads(header: Header) {
+    assert(this._started, 'Has to be started to process new heads.');
+
+    this._height_of_chain = header.number.toBn();
+
+    debug(`New block found at height #${this._height_of_chain}`);
+
+    if (!this._producing_blocks_blocks) await this._produce_blocks();
+  }
+
+  private async _produce_blocks() {
+    assert(!this._producing_blocks_blocks, 'Cannot already be producing blocks.');
+
+    this._producing_blocks_blocks = true;
+
+    // Continue as long as we know there are outstanding blocks.
+    while (this._block_to_be_produced_next.lte(this._height_of_chain)) {
+      debug(`Fetching block #${this._block_to_be_produced_next}`);
+
+      let block_hash_of_target = await this._query_service.getBlockHash(this._block_to_be_produced_next.toString());
+      // TODO: CATCH HERE
+
+      debug(`\tHash ${block_hash_of_target.toString()}.`);
+
+      let records = await this._query_service.eventsAt(block_hash_of_target);
+      // TODO: CATCH HERE
+
+      debug(`\tRead ${records.length} events.`);
+
+      // Since there is at least 1 event, we will fetch block.
+      let signed_block = await this._query_service.getBlock(block_hash_of_target);
+      // TODO: CATCH HERE
+
+      debug(`\tFetched full block.`);
+
+      let extrinsics_array = signed_block.block.extrinsics.toArray();
+
+      let query_events: QueryEvent[] = records.map(
+        (record, index): QueryEvent => {
+          // Extract the phase, event
+          const { phase } = record;
+
+          // Try to recover extrinsic: only possible if its right phase, and extrinsics arra is non-empty, the last constraint
+          // is needed to avoid events from build config code in genesis, and possibly other cases.
+          let extrinsic =
+            phase.isApplyExtrinsic && extrinsics_array.length
+              ? extrinsics_array[phase.asApplyExtrinsic.toBn()]
+              : undefined;
+
+          let query_event = new QueryEvent(record, this._block_to_be_produced_next, extrinsic);
+
+          // Logging
+          query_event.log(0, debug);
+
+          return query_event;
+        }
+      );
+      
+      // Remove processed events from the list.
+      if (this._block_to_be_produced_next.eq(this._at_block)) {
+        query_events = query_events.filter((event) => event.index.gt(this._last_processed_event_index));
+      }
+      
+
+      let query_block = new QueryEventBlock(this._block_to_be_produced_next, query_events);
+
+      this.emit('QueryEventBlock', query_block);
+
+      debug(`\tEmitted query event block.`);
+
+      this._block_to_be_produced_next = this._block_to_be_produced_next.addn(1);
+    }
+
+    this._producing_blocks_blocks = false;
+  }
+}

+ 80 - 0
query-node/substrate-query-framework/index-builder/src/QueryEvent.ts

@@ -0,0 +1,80 @@
+import { EventRecord, Extrinsic } from '@polkadot/types/interfaces';
+import { Codec } from '@polkadot/types/types';
+import * as BN from 'bn.js';
+
+interface EventParameters {
+  [key: string]: Codec;
+}
+
+export interface SubstrateEvent {
+  event_name: string;
+  event_method: string;
+  event_params: EventParameters;
+  index: BN;
+  block_number: BN;
+  extrinsic?: Extrinsic;
+}
+
+export default class QueryEvent implements SubstrateEvent {
+  readonly event_record: EventRecord;
+
+  readonly block_number: BN;
+
+  readonly extrinsic?: Extrinsic;
+
+  constructor(event_record: EventRecord, block_number: BN, extrinsic?: Extrinsic) {
+    this.event_record = event_record;
+    this.extrinsic = extrinsic;
+    this.block_number = block_number;
+  }
+
+  get event_name(): string {
+    let event = this.event_record.event;
+
+    return event.section + '.' + event.method;
+  }
+
+  get event_method(): string {
+    return this.event_record.event.method;
+  }
+
+  get event_params(): EventParameters {
+    const { event } = this.event_record;
+
+    let params: EventParameters = {};
+
+    event.data.forEach((data, index) => {
+      params[event.typeDef[index].type] = data;
+    });
+    return params;
+  }
+
+  // Get event index as number
+  get index(): BN {
+    return new BN(this.event_record.event.index);
+  }
+
+  log(indent: number, logger: (str: string) => void): void {
+    // Extract the phase, event
+    const { event, phase } = this.event_record;
+
+    logger(`\t\t\tParameters:`);
+    event.data.forEach((data, index) => {
+      logger(`\t\t\t\t${event.typeDef[index]}: ${data.toString()}`);
+    });
+
+    logger(
+      `\t\t\tExtrinsic: ${
+        this.extrinsic ? this.extrinsic.method.sectionName + '.' + this.extrinsic.method.methodName : 'NONE'
+      }`
+    );
+    logger(`\t\t\t\tPhase: ${phase.toString()}`);
+
+    if (this.extrinsic) {
+      logger(`\t\t\t\tParameters:`);
+      this.extrinsic.args.forEach((arg, index) => {
+        logger(`\t\t\t\t\t${arg.toRawType()}: ${arg.toString()}`);
+      });
+    }
+  }
+}

+ 14 - 0
query-node/substrate-query-framework/index-builder/src/QueryEventBlock.ts

@@ -0,0 +1,14 @@
+//import { BlockNumber } from '@polkadot/types/interfaces';
+import QueryEvent from './QueryEvent';
+import * as BN from 'bn.js';
+
+export default class QueryEventBlock {
+  readonly block_number: BN;
+
+  readonly query_events: QueryEvent[];
+
+  constructor(block_number: BN, query_events: QueryEvent[]) {
+    this.block_number = block_number;
+    this.query_events = query_events;
+  }
+}

+ 9 - 0
query-node/substrate-query-framework/index-builder/src/QueryEventProcessingPack.ts

@@ -0,0 +1,9 @@
+// @ts-check
+
+import { DatabaseManager, SubstrateEvent } from '.';
+
+export type QueryEventProcessorResult = void | Promise<void>;
+
+export default interface QueryEventProcessingPack {
+  [index: string]: (db: DatabaseManager, event: SubstrateEvent) => QueryEventProcessorResult;
+}

+ 90 - 0
query-node/substrate-query-framework/index-builder/src/QueryNode.ts

@@ -0,0 +1,90 @@
+// @ts-check
+
+import { ApiPromise, WsProvider /*RuntimeVersion*/ } from '@polkadot/api';
+
+import { makeQueryService, IndexBuilder, QueryEventProcessingPack } from '.';
+
+export enum QueryNodeState {
+  NOT_STARTED,
+  BOOTSTRAPPING,
+  STARTING,
+  STARTED,
+  STOPPING,
+  STOPPED,
+}
+
+const debug = require('debug')('index-builder:query-node');
+
+export default class QueryNode {
+  // State of the node,
+  private _state: QueryNodeState;
+
+  // ..
+  private _websocketProvider: WsProvider;
+
+  // API instance for talking to Substrate full node.
+  private _api: ApiPromise;
+
+  // Query index building node.
+  private _indexBuilder: IndexBuilder;
+  
+
+  private constructor(websocketProvider: WsProvider, api: ApiPromise, 
+    indexBuilder: IndexBuilder) {
+    this._state = QueryNodeState.NOT_STARTED;
+    this._websocketProvider = websocketProvider;
+    this._api = api;
+    this._indexBuilder = indexBuilder;
+  }
+
+  static async create(
+    ws_provider_endpoint_uri: string,
+    processing_pack: QueryEventProcessingPack,
+    type_registrator: () => void,
+  ) {
+    // TODO: Do we really need to do it like this?
+    // Its pretty ugly, but the registrtion appears to be
+    // accessing some sort of global state, and has to be done after
+    // the provider is created.
+
+    // Initialise the provider to connect to the local node
+    const provider = new WsProvider(ws_provider_endpoint_uri);
+
+    // Register types before creating the api
+    type_registrator();
+
+    // Create the API and wait until ready
+    const api = await ApiPromise.create({ provider });
+
+    const service = makeQueryService(api);
+
+    const index_buider = IndexBuilder.create(service, processing_pack);
+
+    return new QueryNode(provider, api, index_buider);
+  }
+
+  async start() {
+    if (this._state != QueryNodeState.NOT_STARTED) throw new Error('Starting requires ');
+
+    this._state = QueryNodeState.STARTING;
+
+    // Start the
+    await this._indexBuilder.start();
+
+    this._state = QueryNodeState.STARTED;
+  }
+
+  async stop() {
+    if (this._state != QueryNodeState.STARTED) throw new Error('Can only stop once fully started');
+
+    this._state = QueryNodeState.STOPPING;
+
+    await this._indexBuilder.stop();
+
+    this._state = QueryNodeState.STOPPED;
+  }
+
+  get state() {
+    return this._state;
+  }
+}

+ 48 - 0
query-node/substrate-query-framework/index-builder/src/QueryNodeManager.ts

@@ -0,0 +1,48 @@
+import QueryNode, { QueryNodeState } from './QueryNode';
+import { QueryEventProcessingPack } from '.';
+import { EventEmitter } from 'events';
+import { Bootstrapper, BootstrapPack } from './bootstrap';
+
+// Respondible for creating, starting up and shutting down the query node.
+// Currently this class is a bit thin, but it will almost certainly grow
+// as the integration logic between the library types and the application
+// evolves, and that will pay abstraction overhead off in terms of testability of otherwise
+// anonymous code in root file scope.
+export default class QueryNodeManager {
+  private _query_node!: QueryNode;
+
+  constructor(exitEmitter: EventEmitter) {
+    // Hook into application
+    process.on('exit', this._onProcessExit);
+  }
+
+  async start(
+    ws_provider_endpoint_uri: string,
+    processing_pack: QueryEventProcessingPack,
+    type_registrator: () => void
+  ) {
+    
+    if (this._query_node) throw Error('Cannot start the same manager multiple times.');
+    
+    this._query_node = await QueryNode.create(ws_provider_endpoint_uri, processing_pack, type_registrator);
+    await this._query_node.start();
+    
+  }
+
+  async bootstrap(
+    ws_provider_endpoint_uri: string,
+    bootstrap_pack: BootstrapPack,
+    type_registrator: () => void,
+  ) {
+    let bootstrapper = await Bootstrapper.create(ws_provider_endpoint_uri, bootstrap_pack, type_registrator);
+    await bootstrapper.bootstrap();
+  }
+
+  async _onProcessExit(code: number) {
+    // Stop if query node has been constructed and started.
+    if (this._query_node && this._query_node.state == QueryNodeState.STARTED) {
+      await this._query_node.stop();
+    }
+    code;
+  }
+}

+ 10 - 0
query-node/substrate-query-framework/index-builder/src/bootstrap/BootstrapPack.ts

@@ -0,0 +1,10 @@
+import { ApiPromise } from '@polkadot/api';
+import { DatabaseManager } from '..';
+
+export type BootstrapResult = void | Promise<void>;
+
+export type BootstrapFunc = (api: ApiPromise, db: DatabaseManager) => BootstrapResult;
+
+export default interface BootstrapPack {
+  pack: BootstrapFunc[];
+}

+ 101 - 0
query-node/substrate-query-framework/index-builder/src/bootstrap/Bootstrapper.ts

@@ -0,0 +1,101 @@
+import * as BN from 'bn.js'
+import { BootstrapPack, BootstrapFunc, SubstrateEvent, DatabaseManager, SavedEntityEvent } from '..';
+import { WsProvider, ApiPromise } from '@polkadot/api';
+import {  getConnection, EntityManager } from 'typeorm';
+import { makeDatabaseManager } from '..';
+
+const debug = require('debug')('index-builder:bootstrapper');
+
+export default class Bootstrapper {
+
+    private _api: ApiPromise;
+    private _bootstrapPack: BootstrapPack;
+
+    private constructor(api: ApiPromise, 
+        bootstrapPack: BootstrapPack) {
+        this._api = api;
+        this._bootstrapPack = bootstrapPack;
+    }
+
+    static async create(
+        ws_provider_endpoint_uri: string,
+        bootstrapPack: BootstrapPack,
+        type_registrator: () => void): Promise<Bootstrapper> {
+        // Initialise the provider to connect to the local node
+        const provider = new WsProvider(ws_provider_endpoint_uri);
+
+        // Register types before creating the api
+        type_registrator();
+
+        // Create the API and wait until ready
+        const api = await ApiPromise.create({ provider });
+        return new Bootstrapper(api, bootstrapPack);
+    }
+
+    async bootstrap() {
+        debug("Bootstraping the database");
+        const queryRunner = getConnection().createQueryRunner();
+        const api = this._api;
+        await queryRunner.connect();
+        
+        try {
+            await queryRunner.startTransaction();
+            // establish real database connection
+            // perform all the bootstrap logic in one large
+            // atomic transaction 
+            for (const boot of this._bootstrapPack.pack) {
+                let shouldBootstrap = await this.shouldBootstrap(queryRunner.manager, boot);
+                if (!shouldBootstrap) {
+                    debug(`${boot.name} already bootstrapped, skipping`);
+                    continue;
+                }
+
+                let bootEvent = this.createBootEvent(boot);
+                await boot(api, makeDatabaseManager(queryRunner.manager));
+
+                // Save the bootstrap events so 
+                await SavedEntityEvent.update(bootEvent, queryRunner.manager);
+            }
+
+            debug("Database bootstrap successfull");
+            await queryRunner.commitTransaction();
+
+        } catch (error) {
+            console.error(error);
+            await queryRunner.rollbackTransaction();
+            throw new Error(`Bootstrapping failed: ${error}`);
+        } finally {
+            await queryRunner.release();
+        }
+    }
+    
+    /**
+     * This creates a generic bootstrap event for compatibility with DB
+     * and bookeeping of successfull bootstrap events
+     */
+    private createBootEvent(boot: BootstrapFunc): SubstrateEvent {
+        return {
+            event_name: 'Bootstrap',
+            event_method: `Bootstrap.${boot.name}`,
+            event_params: {},
+            index: new BN(Date.now() / 1000 | 0), // simply put the timestamp here
+            block_number: process.env.BLOCK_HEIGHT ? new BN(process.env.BLOCK_HEIGHT) : new BN(0), 
+        };
+    }
+
+    /**
+     * Looksup the saved boot events to find out if the given handler has already
+     * bootstrapped the data
+     * 
+     * @param em  `EntityManager` by `typeorm`
+     * @param boot boothandler
+     */
+    private async shouldBootstrap(em: EntityManager, boot: BootstrapFunc):Promise<boolean> {
+        const event = await em.findOne(SavedEntityEvent, { 
+            where: { 
+                eventName: `Bootstrap.${boot.name}`,
+            }
+        })
+        return event ? false : true;
+    }
+}

+ 4 - 0
query-node/substrate-query-framework/index-builder/src/bootstrap/index.ts

@@ -0,0 +1,4 @@
+import Bootstrapper from './Bootstrapper';
+import BootstrapPack from './BootstrapPack'
+
+export { Bootstrapper, BootstrapPack }

+ 56 - 0
query-node/substrate-query-framework/index-builder/src/db/DatabaseManager.ts

@@ -0,0 +1,56 @@
+import { FindOneOptions, DeepPartial, EntityManager } from 'typeorm';
+
+import * as helper from './helper';
+
+/**
+ * Database access interface. Use typeorm transactional entity manager to perform get/save/remove operations.
+ */
+export default interface DatabaseManager {
+  /**
+   * Save given entity instance, if entity is exists then just update
+   * @param entity
+   */
+  save<T>(entity: DeepPartial<T>): Promise<void>;
+
+  /**
+   * Removes a given entity from the database.
+   * @param entity: DeepPartial<T>
+   */
+  remove<T>(entity: DeepPartial<T>): Promise<void>;
+
+  /**
+   * Finds first entity that matches given options.
+   * @param entity: T
+   * @param options: FindOneOptions<T>
+   */
+  get<T>(entity: { new (...args: any[]): T }, options: FindOneOptions<T>): Promise<T | undefined>;
+
+  /**
+   * Finds entities that match given options.
+   * @param entity: T
+   * @param options: FindOneOptions<T>
+   */
+  getMany<T>(entity: { new (...args: any[]): T }, options: FindOneOptions<T>): Promise<T[]>;
+}
+
+/**
+ * Create database manager.
+ * @param entityManager EntityManager
+ */
+export function makeDatabaseManager(entityManager: EntityManager): DatabaseManager {
+  return {
+    save: async <T>(entity: DeepPartial<T>): Promise<void> => {
+      entity = helper.fillRequiredWarthogFields(entity);
+      await entityManager.save(entity);
+    },
+    remove: async <T>(entity: DeepPartial<T>): Promise<void> => {
+      await entityManager.remove(entity);
+    },
+    get: async <T>(entity: { new (...args: any[]): T }, options: FindOneOptions<T>): Promise<T | undefined> => {
+      return await entityManager.findOne(entity, options);
+    },
+    getMany: async <T>(entity: { new (...args: any[]): T }, options: FindOneOptions<T>): Promise<T[]> => {
+      return await entityManager.find(entity, options);
+    },
+  } as DatabaseManager;
+}

+ 71 - 0
query-node/substrate-query-framework/index-builder/src/db/entities.ts

@@ -0,0 +1,71 @@
+import { Entity, Column, EntityManager, PrimaryGeneratedColumn, ValueTransformer } from 'typeorm';
+import { SubstrateEvent } from '..';
+import * as BN from 'bn.js';
+
+class NumericTransformer implements ValueTransformer {
+      /**
+     * Used to marshal data when writing to the database.
+     */
+    to(value: BN): string {
+      return value.toString()
+    }
+    /**
+     * Used to unmarshal data when reading from the database.
+     */
+    from(value: string): BN {
+      return new BN(value)
+    }
+}
+
+
+/**
+ * Represents the last processed event. Corresponding database table will hold only one record
+ *  and the single record will be updated
+ */
+@Entity()
+export class SavedEntityEvent {
+  @PrimaryGeneratedColumn()
+  id!: number;
+
+  // Index of the event. @polkadot/types/interfaces/EventId
+  @Column({ type: 'numeric', transformer: new NumericTransformer()})
+  index!: BN;
+
+  // The actually event name without event section. Event.method
+  @Column()
+  eventName!: string;
+
+  // Block number. Event emitted from this block.
+  @Column({ type: 'numeric', transformer: new NumericTransformer() })
+  blockNumber!: BN;
+
+  // When the event is added to the database
+  @Column('timestamp without time zone', {
+    default: () => 'now()',
+  })
+  updatedAt!: Date;
+
+  constructor(init?: Partial<SavedEntityEvent>) {
+    Object.assign(this, init);
+  }
+
+  /**
+   * Get the single database record or create a new instance and then update entity properties
+   * with the event parameter
+   * @param event
+   */
+  static async update(event: SubstrateEvent, manager: EntityManager): Promise<void> {
+    let lastProcessedEvent = await manager.findOne(SavedEntityEvent, { where: { id: 1 } });
+
+    if (!lastProcessedEvent) {
+      lastProcessedEvent = new SavedEntityEvent();
+    }
+
+    lastProcessedEvent.index = event.index;
+    lastProcessedEvent.eventName = event.event_method;
+    lastProcessedEvent.blockNumber = event.block_number;
+    lastProcessedEvent.updatedAt = new Date();
+
+    await manager.save(lastProcessedEvent);
+  }
+}

+ 22 - 0
query-node/substrate-query-framework/index-builder/src/db/helper.ts

@@ -0,0 +1,22 @@
+import * as shortid from 'shortid';
+import { DeepPartial } from 'typeorm';
+/**
+ * Fixes compatibility between typeorm and warthog models.
+ *
+ * @tutorial Warthog add extra properties to its BaseModel and some of these properties are
+ * required. This function mutate the entity to make it compatible with warthog models.
+ * Warthog throw error if required properties contains null values.
+ *
+ * @param entity: DeepPartial<T>
+ */
+export function fillRequiredWarthogFields<T>(entity: DeepPartial<T>): DeepPartial<T> {
+  // Modifying an existing entity so do not add warthog fields
+  if (entity.hasOwnProperty('id')) return entity;
+
+  const requiredFields = {
+    id: shortid.generate(),
+    createdById: shortid.generate(),
+    version: 1,
+  };
+  return Object.assign(entity, requiredFields);
+}

+ 4 - 0
query-node/substrate-query-framework/index-builder/src/db/index.ts

@@ -0,0 +1,4 @@
+import { SavedEntityEvent } from './entities';
+import DatabaseManager, { makeDatabaseManager } from './DatabaseManager';
+
+export { DatabaseManager, makeDatabaseManager, SavedEntityEvent };

+ 29 - 0
query-node/substrate-query-framework/index-builder/src/index.ts

@@ -0,0 +1,29 @@
+import ISubstrateQueryService, { makeQueryService } from './ISubstrateQueryService';
+import QueryBlockProducer from './QueryBlockProducer';
+import QueryEventProcessingPack from './QueryEventProcessingPack';
+import QueryEvent, { SubstrateEvent } from './QueryEvent';
+import QueryEventBlock from './QueryEventBlock';
+import IndexBuilder from './IndexBuilder';
+import QueryNode, { QueryNodeState } from './QueryNode';
+import QueryNodeManager from './QueryNodeManager';
+import { DatabaseManager, SavedEntityEvent, makeDatabaseManager } from './db';
+import BootstrapPack, { BootstrapFunc } from './bootstrap/BootstrapPack';
+
+export {
+  ISubstrateQueryService,
+  makeQueryService,
+  QueryBlockProducer,
+  QueryEventProcessingPack,
+  QueryEvent,
+  SubstrateEvent,
+  QueryEventBlock,
+  IndexBuilder,
+  QueryNode,
+  QueryNodeState,
+  QueryNodeManager,
+  makeDatabaseManager,
+  DatabaseManager,
+  SavedEntityEvent,
+  BootstrapPack,
+  BootstrapFunc,
+};

+ 18 - 0
query-node/substrate-query-framework/index-builder/tsconfig.json

@@ -0,0 +1,18 @@
+{
+	"compilerOptions": {
+		"declaration": true,
+		"importHelpers": true,
+		"module": "commonjs",
+		"outDir": "lib",
+		"rootDir": "src",
+		"strict": true,
+		"target": "es2017",
+		"experimentalDecorators": true,
+		"emitDecoratorMetadata": true,
+		"skipLibCheck": true,
+		"sourceMap": true,
+		"inlineSources": false
+	},
+	"include": ["src/**/*"],
+	"exclude": ["node_modules"]
+}

Some files were not shown because too many files changed in this diff