Browse Source

Added substrate, joystream query node

metmirr 4 years ago
parent
commit
b39f9ab5e4
35 changed files with 1883 additions and 0 deletions
  1. 13 0
      joystream-query-node/.env
  2. 25 0
      joystream-query-node/README.md
  3. 296 0
      joystream-query-node/generated/binding.ts
  4. 351 0
      joystream-query-node/generated/classes.ts
  5. 1 0
      joystream-query-node/generated/index.ts
  6. 3 0
      joystream-query-node/generated/ormconfig.ts
  7. 241 0
      joystream-query-node/generated/schema.graphql
  8. 105 0
      joystream-query-node/package.json
  9. 7 0
      joystream-query-node/src/config.ts
  10. 21 0
      joystream-query-node/src/index.ts
  11. 35 0
      joystream-query-node/src/logger.ts
  12. 10 0
      joystream-query-node/src/modules/member-registered/member-registered.model.ts
  13. 69 0
      joystream-query-node/src/modules/member-registered/member-registered.resolver.ts
  14. 15 0
      joystream-query-node/src/modules/member-registered/member-registered.service.ts
  15. 34 0
      joystream-query-node/src/server.ts
  16. 24 0
      joystream-query-node/tsconfig.json
  17. 3 0
      joystream-query-node/warthog.config.js
  18. 40 0
      substrate-query-node/README.md
  19. BIN
      substrate-query-node/images/Flow.png
  20. 26 0
      substrate-query-node/ormconfig.json
  21. 35 0
      substrate-query-node/package.json
  22. 8 0
      substrate-query-node/src/config.ts
  23. 20 0
      substrate-query-node/src/dispatch.ts
  24. 1 0
      substrate-query-node/src/eventNames.ts
  25. 19 0
      substrate-query-node/src/helper.ts
  26. 40 0
      substrate-query-node/src/index.ts
  27. 17 0
      substrate-query-node/src/mapping.ts
  28. 50 0
      substrate-query-node/src/member.ts
  29. 42 0
      substrate-query-node/src/models/members/MemberRegistereds.ts
  30. 101 0
      substrate-query-node/tests/membershipCreationTest.ts
  31. 123 0
      substrate-query-node/tests/utils/apiWrapper.ts
  32. 9 0
      substrate-query-node/tests/utils/db.ts
  33. 68 0
      substrate-query-node/tests/utils/sender.ts
  34. 21 0
      substrate-query-node/tests/utils/utils.ts
  35. 10 0
      substrate-query-node/tsconfig.json

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

@@ -0,0 +1,13 @@
+DEBUG=*
+NODE_ENV=development
+WARTHOG_AUTO_OPEN_PLAYGROUND=false
+WARTHOG_AUTO_GENERATE_FILES=false
+WARTHOG_APP_HOST=localhost
+WARTHOG_APP_PORT=4100
+WARTHOG_DB_DATABASE=joystream_node_test
+WARTHOG_DB_HOST=localhost
+WARTHOG_DB_LOGGING=all
+WARTHOG_DB_USERNAME=postgres
+WARTHOG_DB_PASSWORD=postgres
+WARTHOG_DB_PORT=5432
+WARTHOG_DB_SYNCHRONIZE=true

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

@@ -0,0 +1,25 @@
+# Joystream Query Node
+
+This is an experimental work. Query Joystream blockchain data with through GraphQL API.
+
+## Run Grapql server
+
+Install requirements
+
+```bash
+yarn install
+```
+
+Before running server make sure you add your postgresql username, password to .env file. In .env file there is database name so make sure you create the database.
+
+```bash
+yarn start:dev
+```
+
+Now Go to http://127.0.0.1:4100/graphql
+
+## Updating GraphQL schema
+
+```bash
+yarn warthog codegen
+```

+ 296 - 0
joystream-query-node/generated/binding.ts

@@ -0,0 +1,296 @@
+import 'graphql-import-node'; // Needed so you can import *.graphql files 
+
+import { makeBindingClass, Options } from 'graphql-binding'
+import { GraphQLResolveInfo, GraphQLSchema } from 'graphql'
+import { IResolvers } from 'graphql-tools/dist/Interfaces'
+import * as schema from  './schema.graphql'
+
+export interface Query {
+    memberRegistereds: <T = Array<MemberRegistered>>(args: { offset?: Int | null, limit?: Int | null, where?: MemberRegisteredWhereInput | null, orderBy?: MemberRegisteredOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    memberRegistered: <T = MemberRegistered>(args: { where: MemberRegisteredWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    memberUpdatedAboutTexts: <T = Array<MemberUpdatedAboutText>>(args: { offset?: Int | null, limit?: Int | null, where?: MemberUpdatedAboutTextWhereInput | null, orderBy?: MemberUpdatedAboutTextOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    memberUpdatedAboutText: <T = MemberUpdatedAboutText>(args: { where: MemberUpdatedAboutTextWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> 
+  }
+
+export interface Mutation {
+    createMemberRegistered: <T = MemberRegistered>(args: { data: MemberRegisteredCreateInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    createManyMemberRegistereds: <T = Array<MemberRegistered>>(args: { data: Array<MemberRegisteredCreateInput> }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    updateMemberRegistered: <T = MemberRegistered>(args: { data: MemberRegisteredUpdateInput, where: MemberRegisteredWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    deleteMemberRegistered: <T = StandardDeleteResponse>(args: { where: MemberRegisteredWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    createMemberUpdatedAboutText: <T = MemberUpdatedAboutText>(args: { data: MemberUpdatedAboutTextCreateInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    createManyMemberUpdatedAboutTexts: <T = Array<MemberUpdatedAboutText>>(args: { data: Array<MemberUpdatedAboutTextCreateInput> }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    updateMemberUpdatedAboutText: <T = MemberUpdatedAboutText>(args: { data: MemberUpdatedAboutTextUpdateInput, where: MemberUpdatedAboutTextWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    deleteMemberUpdatedAboutText: <T = StandardDeleteResponse>(args: { where: MemberUpdatedAboutTextWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> 
+  }
+
+export interface Subscription {}
+
+export interface Binding {
+  query: Query
+  mutation: Mutation
+  subscription: Subscription
+  request: <T = any>(query: string, variables?: {[key: string]: any}) => Promise<T>
+  delegate(operation: 'query' | 'mutation', fieldName: string, args: {
+      [key: string]: any;
+  }, infoOrQuery?: GraphQLResolveInfo | string, options?: Options): Promise<any>;
+  delegateSubscription(fieldName: string, args?: {
+      [key: string]: any;
+  }, infoOrQuery?: GraphQLResolveInfo | string, options?: Options): Promise<AsyncIterator<any>>;
+  getAbstractResolvers(filterSchema?: GraphQLSchema | string): IResolvers;
+}
+
+export interface BindingConstructor<T> {
+  new(...args: any[]): T
+}
+
+export const Binding = makeBindingClass<BindingConstructor<Binding>>({ schema: schema as any })
+
+/**
+ * Types
+*/
+
+export type MemberRegisteredOrderByInput =   'createdAt_ASC' |
+  'createdAt_DESC' |
+  'updatedAt_ASC' |
+  'updatedAt_DESC' |
+  'deletedAt_ASC' |
+  'deletedAt_DESC' |
+  'memberId_ASC' |
+  'memberId_DESC' |
+  'accountId_ASC' |
+  'accountId_DESC'
+
+export type MemberUpdatedAboutTextOrderByInput =   'createdAt_ASC' |
+  'createdAt_DESC' |
+  'updatedAt_ASC' |
+  'updatedAt_DESC' |
+  'deletedAt_ASC' |
+  'deletedAt_DESC' |
+  'memberId_ASC' |
+  'memberId_DESC'
+
+export interface BaseWhereInput {
+  id_eq?: String | null
+  id_in?: String[] | String | null
+  createdAt_eq?: String | null
+  createdAt_lt?: String | null
+  createdAt_lte?: String | null
+  createdAt_gt?: String | null
+  createdAt_gte?: String | null
+  createdById_eq?: String | null
+  updatedAt_eq?: String | null
+  updatedAt_lt?: String | null
+  updatedAt_lte?: String | null
+  updatedAt_gt?: String | null
+  updatedAt_gte?: String | null
+  updatedById_eq?: String | null
+  deletedAt_all?: Boolean | null
+  deletedAt_eq?: String | null
+  deletedAt_lt?: String | null
+  deletedAt_lte?: String | null
+  deletedAt_gt?: String | null
+  deletedAt_gte?: String | null
+  deletedById_eq?: String | null
+}
+
+export interface MemberRegisteredCreateInput {
+  memberId: Float
+  accountId: String
+}
+
+export interface MemberRegisteredUpdateInput {
+  memberId?: Float | null
+  accountId?: String | null
+}
+
+export interface MemberRegisteredWhereInput {
+  id_eq?: ID_Input | null
+  id_in?: ID_Output[] | ID_Output | null
+  createdAt_eq?: DateTime | null
+  createdAt_lt?: DateTime | null
+  createdAt_lte?: DateTime | null
+  createdAt_gt?: DateTime | null
+  createdAt_gte?: DateTime | null
+  createdById_eq?: ID_Input | null
+  createdById_in?: ID_Output[] | ID_Output | null
+  updatedAt_eq?: DateTime | null
+  updatedAt_lt?: DateTime | null
+  updatedAt_lte?: DateTime | null
+  updatedAt_gt?: DateTime | null
+  updatedAt_gte?: DateTime | null
+  updatedById_eq?: ID_Input | null
+  updatedById_in?: ID_Output[] | ID_Output | null
+  deletedAt_all?: Boolean | null
+  deletedAt_eq?: DateTime | null
+  deletedAt_lt?: DateTime | null
+  deletedAt_lte?: DateTime | null
+  deletedAt_gt?: DateTime | null
+  deletedAt_gte?: DateTime | null
+  deletedById_eq?: ID_Input | null
+  deletedById_in?: ID_Output[] | ID_Output | null
+  memberId_eq?: Int | null
+  memberId_gt?: Int | null
+  memberId_gte?: Int | null
+  memberId_lt?: Int | null
+  memberId_lte?: Int | null
+  memberId_in?: Int[] | Int | null
+  accountId_eq?: String | null
+  accountId_contains?: String | null
+  accountId_startsWith?: String | null
+  accountId_endsWith?: String | null
+  accountId_in?: String[] | String | null
+}
+
+export interface MemberRegisteredWhereUniqueInput {
+  id: ID_Output
+}
+
+export interface MemberUpdatedAboutTextCreateInput {
+  memberId: Float
+}
+
+export interface MemberUpdatedAboutTextUpdateInput {
+  memberId?: Float | null
+}
+
+export interface MemberUpdatedAboutTextWhereInput {
+  id_eq?: ID_Input | null
+  id_in?: ID_Output[] | ID_Output | null
+  createdAt_eq?: DateTime | null
+  createdAt_lt?: DateTime | null
+  createdAt_lte?: DateTime | null
+  createdAt_gt?: DateTime | null
+  createdAt_gte?: DateTime | null
+  createdById_eq?: ID_Input | null
+  createdById_in?: ID_Output[] | ID_Output | null
+  updatedAt_eq?: DateTime | null
+  updatedAt_lt?: DateTime | null
+  updatedAt_lte?: DateTime | null
+  updatedAt_gt?: DateTime | null
+  updatedAt_gte?: DateTime | null
+  updatedById_eq?: ID_Input | null
+  updatedById_in?: ID_Output[] | ID_Output | null
+  deletedAt_all?: Boolean | null
+  deletedAt_eq?: DateTime | null
+  deletedAt_lt?: DateTime | null
+  deletedAt_lte?: DateTime | null
+  deletedAt_gt?: DateTime | null
+  deletedAt_gte?: DateTime | null
+  deletedById_eq?: ID_Input | null
+  deletedById_in?: ID_Output[] | ID_Output | null
+  memberId_eq?: Int | null
+  memberId_gt?: Int | null
+  memberId_gte?: Int | null
+  memberId_lt?: Int | null
+  memberId_lte?: Int | null
+  memberId_in?: Int[] | Int | null
+}
+
+export interface MemberUpdatedAboutTextWhereUniqueInput {
+  id: ID_Output
+}
+
+export interface BaseGraphQLObject {
+  id: ID_Output
+  createdAt: DateTime
+  createdById: String
+  updatedAt?: DateTime | null
+  updatedById?: String | null
+  deletedAt?: DateTime | null
+  deletedById?: String | null
+  version: Int
+}
+
+export interface DeleteResponse {
+  id: ID_Output
+}
+
+export interface BaseModel extends BaseGraphQLObject {
+  id: ID_Output
+  createdAt: DateTime
+  createdById: String
+  updatedAt?: DateTime | null
+  updatedById?: String | null
+  deletedAt?: DateTime | null
+  deletedById?: String | null
+  version: Int
+}
+
+export interface BaseModelUUID extends BaseGraphQLObject {
+  id: ID_Output
+  createdAt: DateTime
+  createdById: String
+  updatedAt?: DateTime | null
+  updatedById?: String | null
+  deletedAt?: DateTime | null
+  deletedById?: String | null
+  version: Int
+}
+
+export interface MemberRegistered extends BaseGraphQLObject {
+  id: ID_Output
+  createdAt: DateTime
+  createdById: String
+  updatedAt?: DateTime | null
+  updatedById?: String | null
+  deletedAt?: DateTime | null
+  deletedById?: String | null
+  version: Int
+  memberId: Int
+  accountId: String
+}
+
+export interface MemberUpdatedAboutText extends BaseGraphQLObject {
+  id: ID_Output
+  createdAt: DateTime
+  createdById: String
+  updatedAt?: DateTime | null
+  updatedById?: String | null
+  deletedAt?: DateTime | null
+  deletedById?: String | null
+  version: Int
+  memberId: Int
+}
+
+export interface PageInfo {
+  limit: Float
+  offset: Float
+  totalCount: Float
+  hasNextPage: Boolean
+  hasPreviousPage: Boolean
+}
+
+export interface StandardDeleteResponse {
+  id: ID_Output
+}
+
+/*
+The `Boolean` scalar type represents `true` or `false`.
+*/
+export type Boolean = boolean
+
+/*
+The javascript `Date` as string. Type represents date and time as the ISO Date string.
+*/
+export type DateTime = Date | string
+
+/*
+The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).
+*/
+export type Float = number
+
+/*
+The `ID` scalar type represents a unique identifier, often used to refetch an object or as key for a cache. The ID type appears in a JSON response as a String; however, it is not intended to be human-readable. When expected as an input type, any string (such as `"4"`) or integer (such as `4`) input value will be accepted as an ID.
+*/
+export type ID_Input = string | number
+export type ID_Output = string
+
+/*
+The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
+*/
+export type Int = number
+
+/*
+The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text.
+*/
+export type String = string

+ 351 - 0
joystream-query-node/generated/classes.ts

@@ -0,0 +1,351 @@
+// This file has been auto-generated by Warthog.  Do not update directly as it
+// will be re-written.  If you need to change this file, update models or add
+// new TypeGraphQL objects
+// @ts-ignore
+import { GraphQLDateTime as DateTime } from "graphql-iso-date";
+// @ts-ignore
+import { GraphQLID as ID } from "graphql";
+// @ts-ignore
+import {
+  ArgsType,
+  Field as TypeGraphQLField,
+  Float,
+  InputType as TypeGraphQLInputType,
+  Int
+} from "type-graphql";
+// @ts-ignore
+import { registerEnumType } from "type-graphql";
+
+// @ts-ignore eslint-disable-next-line @typescript-eslint/no-var-requires
+const { GraphQLJSONObject } = require("graphql-type-json");
+
+// @ts-ignore
+import { BaseWhereInput, JsonObject, PaginationArgs } from "warthog";
+// @ts-ignore
+import { MemberRegistered } from "../src/modules/member-registered/member-registered.model";
+// @ts-ignore
+import { MemberUpdatedAboutText } from "../src/modules/member-updated-about-text/member-updated-about-text.model";
+
+export enum MemberRegisteredOrderByEnum {
+  createdAt_ASC = "createdAt_ASC",
+  createdAt_DESC = "createdAt_DESC",
+
+  updatedAt_ASC = "updatedAt_ASC",
+  updatedAt_DESC = "updatedAt_DESC",
+
+  deletedAt_ASC = "deletedAt_ASC",
+  deletedAt_DESC = "deletedAt_DESC",
+
+  memberId_ASC = "memberId_ASC",
+  memberId_DESC = "memberId_DESC",
+
+  accountId_ASC = "accountId_ASC",
+  accountId_DESC = "accountId_DESC"
+}
+
+registerEnumType(MemberRegisteredOrderByEnum, {
+  name: "MemberRegisteredOrderByInput"
+});
+
+@TypeGraphQLInputType()
+export class MemberRegisteredWhereInput {
+  @TypeGraphQLField(() => ID, { nullable: true })
+  id_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  id_in?: string[];
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_eq?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_lt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_lte?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_gt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  createdById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  createdById_in?: string[];
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_eq?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_lt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_lte?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_gt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  updatedById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  updatedById_in?: string[];
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_all?: Boolean;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_eq?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_lt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_lte?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_gt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  deletedById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  deletedById_in?: string[];
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_eq?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_gt?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_gte?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_lt?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_lte?: number;
+
+  @TypeGraphQLField(() => [Int], { nullable: true })
+  memberId_in?: number[];
+
+  @TypeGraphQLField({ nullable: true })
+  accountId_eq?: string;
+
+  @TypeGraphQLField({ nullable: true })
+  accountId_contains?: string;
+
+  @TypeGraphQLField({ nullable: true })
+  accountId_startsWith?: string;
+
+  @TypeGraphQLField({ nullable: true })
+  accountId_endsWith?: string;
+
+  @TypeGraphQLField(() => [String], { nullable: true })
+  accountId_in?: string[];
+}
+
+@TypeGraphQLInputType()
+export class MemberRegisteredWhereUniqueInput {
+  @TypeGraphQLField(() => ID)
+  id?: string;
+}
+
+@TypeGraphQLInputType()
+export class MemberRegisteredCreateInput {
+  @TypeGraphQLField()
+  memberId!: number;
+
+  @TypeGraphQLField()
+  accountId!: string;
+}
+
+@TypeGraphQLInputType()
+export class MemberRegisteredUpdateInput {
+  @TypeGraphQLField({ nullable: true })
+  memberId?: number;
+
+  @TypeGraphQLField({ nullable: true })
+  accountId?: string;
+}
+
+@ArgsType()
+export class MemberRegisteredWhereArgs extends PaginationArgs {
+  @TypeGraphQLField(() => MemberRegisteredWhereInput, { nullable: true })
+  where?: MemberRegisteredWhereInput;
+
+  @TypeGraphQLField(() => MemberRegisteredOrderByEnum, { nullable: true })
+  orderBy?: MemberRegisteredOrderByEnum;
+}
+
+@ArgsType()
+export class MemberRegisteredCreateManyArgs {
+  @TypeGraphQLField(() => [MemberRegisteredCreateInput])
+  data!: MemberRegisteredCreateInput[];
+}
+
+@ArgsType()
+export class MemberRegisteredUpdateArgs {
+  @TypeGraphQLField() data!: MemberRegisteredUpdateInput;
+  @TypeGraphQLField() where!: MemberRegisteredWhereUniqueInput;
+}
+
+export enum MemberUpdatedAboutTextOrderByEnum {
+  createdAt_ASC = "createdAt_ASC",
+  createdAt_DESC = "createdAt_DESC",
+
+  updatedAt_ASC = "updatedAt_ASC",
+  updatedAt_DESC = "updatedAt_DESC",
+
+  deletedAt_ASC = "deletedAt_ASC",
+  deletedAt_DESC = "deletedAt_DESC",
+
+  memberId_ASC = "memberId_ASC",
+  memberId_DESC = "memberId_DESC"
+}
+
+registerEnumType(MemberUpdatedAboutTextOrderByEnum, {
+  name: "MemberUpdatedAboutTextOrderByInput"
+});
+
+@TypeGraphQLInputType()
+export class MemberUpdatedAboutTextWhereInput {
+  @TypeGraphQLField(() => ID, { nullable: true })
+  id_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  id_in?: string[];
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_eq?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_lt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_lte?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_gt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  createdAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  createdById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  createdById_in?: string[];
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_eq?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_lt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_lte?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_gt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  updatedAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  updatedById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  updatedById_in?: string[];
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_all?: Boolean;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_eq?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_lt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_lte?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_gt?: Date;
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  deletedById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  deletedById_in?: string[];
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_eq?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_gt?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_gte?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_lt?: number;
+
+  @TypeGraphQLField(() => Int, { nullable: true })
+  memberId_lte?: number;
+
+  @TypeGraphQLField(() => [Int], { nullable: true })
+  memberId_in?: number[];
+}
+
+@TypeGraphQLInputType()
+export class MemberUpdatedAboutTextWhereUniqueInput {
+  @TypeGraphQLField(() => ID)
+  id?: string;
+}
+
+@TypeGraphQLInputType()
+export class MemberUpdatedAboutTextCreateInput {
+  @TypeGraphQLField()
+  memberId!: number;
+}
+
+@TypeGraphQLInputType()
+export class MemberUpdatedAboutTextUpdateInput {
+  @TypeGraphQLField({ nullable: true })
+  memberId?: number;
+}
+
+@ArgsType()
+export class MemberUpdatedAboutTextWhereArgs extends PaginationArgs {
+  @TypeGraphQLField(() => MemberUpdatedAboutTextWhereInput, { nullable: true })
+  where?: MemberUpdatedAboutTextWhereInput;
+
+  @TypeGraphQLField(() => MemberUpdatedAboutTextOrderByEnum, { nullable: true })
+  orderBy?: MemberUpdatedAboutTextOrderByEnum;
+}
+
+@ArgsType()
+export class MemberUpdatedAboutTextCreateManyArgs {
+  @TypeGraphQLField(() => [MemberUpdatedAboutTextCreateInput])
+  data!: MemberUpdatedAboutTextCreateInput[];
+}
+
+@ArgsType()
+export class MemberUpdatedAboutTextUpdateArgs {
+  @TypeGraphQLField() data!: MemberUpdatedAboutTextUpdateInput;
+  @TypeGraphQLField() where!: MemberUpdatedAboutTextWhereUniqueInput;
+}

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

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

+ 3 - 0
joystream-query-node/generated/ormconfig.ts

@@ -0,0 +1,3 @@
+import { getBaseConfig } from 'warthog';
+
+module.exports = getBaseConfig();

+ 241 - 0
joystream-query-node/generated/schema.graphql

@@ -0,0 +1,241 @@
+interface BaseGraphQLObject {
+  id: ID!
+  createdAt: DateTime!
+  createdById: String!
+  updatedAt: DateTime
+  updatedById: String
+  deletedAt: DateTime
+  deletedById: String
+  version: Int!
+}
+
+type BaseModel implements BaseGraphQLObject {
+  id: ID!
+  createdAt: DateTime!
+  createdById: String!
+  updatedAt: DateTime
+  updatedById: String
+  deletedAt: DateTime
+  deletedById: String
+  version: Int!
+}
+
+type BaseModelUUID implements BaseGraphQLObject {
+  id: ID!
+  createdAt: DateTime!
+  createdById: String!
+  updatedAt: DateTime
+  updatedById: String
+  deletedAt: DateTime
+  deletedById: String
+  version: Int!
+}
+
+input BaseWhereInput {
+  id_eq: String
+  id_in: [String!]
+  createdAt_eq: String
+  createdAt_lt: String
+  createdAt_lte: String
+  createdAt_gt: String
+  createdAt_gte: String
+  createdById_eq: String
+  updatedAt_eq: String
+  updatedAt_lt: String
+  updatedAt_lte: String
+  updatedAt_gt: String
+  updatedAt_gte: String
+  updatedById_eq: String
+  deletedAt_all: Boolean
+  deletedAt_eq: String
+  deletedAt_lt: String
+  deletedAt_lte: String
+  deletedAt_gt: String
+  deletedAt_gte: String
+  deletedById_eq: String
+}
+
+"""
+The javascript `Date` as string. Type represents date and time as the ISO Date string.
+"""
+scalar DateTime
+
+interface DeleteResponse {
+  id: ID!
+}
+
+type MemberRegistered implements BaseGraphQLObject {
+  id: ID!
+  createdAt: DateTime!
+  createdById: String!
+  updatedAt: DateTime
+  updatedById: String
+  deletedAt: DateTime
+  deletedById: String
+  version: Int!
+  memberId: Int!
+  accountId: String!
+}
+
+input MemberRegisteredCreateInput {
+  memberId: Float!
+  accountId: String!
+}
+
+enum MemberRegisteredOrderByInput {
+  createdAt_ASC
+  createdAt_DESC
+  updatedAt_ASC
+  updatedAt_DESC
+  deletedAt_ASC
+  deletedAt_DESC
+  memberId_ASC
+  memberId_DESC
+  accountId_ASC
+  accountId_DESC
+}
+
+input MemberRegisteredUpdateInput {
+  memberId: Float
+  accountId: String
+}
+
+input MemberRegisteredWhereInput {
+  id_eq: ID
+  id_in: [ID!]
+  createdAt_eq: DateTime
+  createdAt_lt: DateTime
+  createdAt_lte: DateTime
+  createdAt_gt: DateTime
+  createdAt_gte: DateTime
+  createdById_eq: ID
+  createdById_in: [ID!]
+  updatedAt_eq: DateTime
+  updatedAt_lt: DateTime
+  updatedAt_lte: DateTime
+  updatedAt_gt: DateTime
+  updatedAt_gte: DateTime
+  updatedById_eq: ID
+  updatedById_in: [ID!]
+  deletedAt_all: Boolean
+  deletedAt_eq: DateTime
+  deletedAt_lt: DateTime
+  deletedAt_lte: DateTime
+  deletedAt_gt: DateTime
+  deletedAt_gte: DateTime
+  deletedById_eq: ID
+  deletedById_in: [ID!]
+  memberId_eq: Int
+  memberId_gt: Int
+  memberId_gte: Int
+  memberId_lt: Int
+  memberId_lte: Int
+  memberId_in: [Int!]
+  accountId_eq: String
+  accountId_contains: String
+  accountId_startsWith: String
+  accountId_endsWith: String
+  accountId_in: [String!]
+}
+
+input MemberRegisteredWhereUniqueInput {
+  id: ID!
+}
+
+type MemberUpdatedAboutText implements BaseGraphQLObject {
+  id: ID!
+  createdAt: DateTime!
+  createdById: String!
+  updatedAt: DateTime
+  updatedById: String
+  deletedAt: DateTime
+  deletedById: String
+  version: Int!
+  memberId: Int!
+}
+
+input MemberUpdatedAboutTextCreateInput {
+  memberId: Float!
+}
+
+enum MemberUpdatedAboutTextOrderByInput {
+  createdAt_ASC
+  createdAt_DESC
+  updatedAt_ASC
+  updatedAt_DESC
+  deletedAt_ASC
+  deletedAt_DESC
+  memberId_ASC
+  memberId_DESC
+}
+
+input MemberUpdatedAboutTextUpdateInput {
+  memberId: Float
+}
+
+input MemberUpdatedAboutTextWhereInput {
+  id_eq: ID
+  id_in: [ID!]
+  createdAt_eq: DateTime
+  createdAt_lt: DateTime
+  createdAt_lte: DateTime
+  createdAt_gt: DateTime
+  createdAt_gte: DateTime
+  createdById_eq: ID
+  createdById_in: [ID!]
+  updatedAt_eq: DateTime
+  updatedAt_lt: DateTime
+  updatedAt_lte: DateTime
+  updatedAt_gt: DateTime
+  updatedAt_gte: DateTime
+  updatedById_eq: ID
+  updatedById_in: [ID!]
+  deletedAt_all: Boolean
+  deletedAt_eq: DateTime
+  deletedAt_lt: DateTime
+  deletedAt_lte: DateTime
+  deletedAt_gt: DateTime
+  deletedAt_gte: DateTime
+  deletedById_eq: ID
+  deletedById_in: [ID!]
+  memberId_eq: Int
+  memberId_gt: Int
+  memberId_gte: Int
+  memberId_lt: Int
+  memberId_lte: Int
+  memberId_in: [Int!]
+}
+
+input MemberUpdatedAboutTextWhereUniqueInput {
+  id: ID!
+}
+
+type Mutation {
+  createMemberRegistered(data: MemberRegisteredCreateInput!): MemberRegistered!
+  createManyMemberRegistereds(data: [MemberRegisteredCreateInput!]!): [MemberRegistered!]!
+  updateMemberRegistered(data: MemberRegisteredUpdateInput!, where: MemberRegisteredWhereUniqueInput!): MemberRegistered!
+  deleteMemberRegistered(where: MemberRegisteredWhereUniqueInput!): StandardDeleteResponse!
+  createMemberUpdatedAboutText(data: MemberUpdatedAboutTextCreateInput!): MemberUpdatedAboutText!
+  createManyMemberUpdatedAboutTexts(data: [MemberUpdatedAboutTextCreateInput!]!): [MemberUpdatedAboutText!]!
+  updateMemberUpdatedAboutText(data: MemberUpdatedAboutTextUpdateInput!, where: MemberUpdatedAboutTextWhereUniqueInput!): MemberUpdatedAboutText!
+  deleteMemberUpdatedAboutText(where: MemberUpdatedAboutTextWhereUniqueInput!): StandardDeleteResponse!
+}
+
+type PageInfo {
+  limit: Float!
+  offset: Float!
+  totalCount: Float!
+  hasNextPage: Boolean!
+  hasPreviousPage: Boolean!
+}
+
+type Query {
+  memberRegistereds(offset: Int, limit: Int = 50, where: MemberRegisteredWhereInput, orderBy: MemberRegisteredOrderByInput): [MemberRegistered!]!
+  memberRegistered(where: MemberRegisteredWhereUniqueInput!): MemberRegistered!
+  memberUpdatedAboutTexts(offset: Int, limit: Int = 50, where: MemberUpdatedAboutTextWhereInput, orderBy: MemberUpdatedAboutTextOrderByInput): [MemberUpdatedAboutText!]!
+  memberUpdatedAboutText(where: MemberUpdatedAboutTextWhereUniqueInput!): MemberUpdatedAboutText!
+}
+
+type StandardDeleteResponse {
+  id: ID!
+}

+ 105 - 0
joystream-query-node/package.json

@@ -0,0 +1,105 @@
+{
+  "name": "query-node-graphql-server",
+  "version": "0.0.0",
+  "description": "Generated Warthog Project",
+  "license": "MIT",
+  "scripts": {
+    "bootstrap": "yarn bootstrap:dev",
+    "bootstrap:dev": "yarn && yarn build:dev && yarn db:drop && yarn db:create && yarn db:migrate && yarn db:seed",
+    "bootstrap:prod": "yarn && yarn build:prod && yarn start:prod",
+    "//": "This is the default command run in CI, so it should point to Prod and also create Prod config",
+    "build": "yarn build:prod",
+    "build:prod": "WARTHOG_ENV=production yarn run config && yarn compile",
+    "build:dev": "yarn run config:dev && yarn codegen && yarn compile",
+    "check:code": "tsc --noEmit && yarn lint && prettier ./{src,test,tools}/**/*.ts --write",
+    "clean": "yarn db:drop && rm -rf ./node_modules ./generated ./dist",
+    "codegen": "warthog codegen",
+    "config": "WARTHOG_ENV=$NODE_ENV yarn dotenv:generate",
+    "config:dev": "WARTHOG_ENV=development yarn dotenv:generate",
+    "compile": "rm -rf ./dist && yarn tsc",
+    "deploy": "heroku git:remote -a warthog-starter && git push heroku master && WARTHOG_ENV=production yarn dotenv:generate && warthog db:migrate",
+    "dotenv:generate": "dotenvi -s ${WARTHOG_ENV:-development}",
+    "db:create": "warthog db:create",
+    "db:drop": "warthog db:drop",
+    "db:migrate:generate": "warthog db:migrate:generate --name",
+    "db:migrate": "warthog db:migrate",
+    "db:seed": "ts-node tools/seed.ts",
+    "lint": "eslint './+(src|test|tools)/**/*.{js,ts}' --fix",
+    "list:users": "ts-node ./tools/list-users.ts",
+    "playground": "warthog playground",
+    "prettier": "prettier ./{src,test,tools}/**/*.ts --write",
+    "start": "yarn start:prod",
+    "start:dev": "ts-node-dev --type-check src/index.ts",
+    "start:dev:watch": "nodemon -e ts,graphql -x ts-node --type-check src/index.ts",
+    "start:prod": "WARTHOG_ENV=production yarn dotenv:generate && node dist/src/index.js",
+    "test": "DEBUG= jest --verbose --coverage",
+    "test:watch": "DEBUG= jest --watch"
+  },
+  "husky": {
+    "hooks": {
+      "pre-commit": "yarn run config:dev && lint-staged && tsc -p ./tsconfig.json && yarn test"
+    }
+  },
+  "lint-staged": {
+    "linters": {
+      "*.ts": [
+        "eslint --fix",
+        "prettier --write",
+        "git add"
+      ],
+      "*.{js,json}": [
+        "prettier --write",
+        "git add"
+      ]
+    },
+    "ignore": [
+      "**/generated/*"
+    ]
+  },
+  "dependencies": {
+    "@types/sqlite3": "^3.1.5",
+    "dotenv": "^8.0.0",
+    "dotenvi": "^0.6.0",
+    "reflect-metadata": "^0.1.13",
+    "warthog": "^2.6.0"
+  },
+  "devDependencies": {
+    "@types/jest": "^24.0.15",
+    "jest": "^24.8.0",
+    "ts-jest": "^24.0.2",
+    "ts-node": "^7.0.1",
+    "ts-node-dev": "^1.0.0-pre.40",
+    "typescript": "3.5.2"
+  },
+  "jest": {
+    "globals": {
+      "ts-jest": {
+        "tsConfig": "tsconfig.test.json"
+      }
+    },
+    "transform": {
+      ".ts": "ts-jest"
+    },
+    "testRegex": "\\.test\\.ts$",
+    "moduleFileExtensions": [
+      "js",
+      "ts"
+    ],
+    "coveragePathIgnorePatterns": [
+      "/node_modules/",
+      "\\.test\\.ts$"
+    ],
+    "globalSetup": "<rootDir>/test/globalSetup.ts",
+    "globalTeardown": "<rootDir>/test/globalTeardown.ts",
+    "setupFilesAfterEnv": [
+      "./test/setupFilesAfterEnv.ts"
+    ]
+  },
+  "prettier": {
+    "printWidth": 100,
+    "singleQuote": true
+  },
+  "resolutions": {
+    "ts-node": "7.0.1"
+  }
+}

+ 7 - 0
joystream-query-node/src/config.ts

@@ -0,0 +1,7 @@
+import * as dotenv from 'dotenv';
+import * as path from 'path';
+
+export function loadConfig() {
+  delete process.env.NODE_ENV;
+  dotenv.config({ path: path.join(__dirname, '../.env') });
+}

+ 21 - 0
joystream-query-node/src/index.ts

@@ -0,0 +1,21 @@
+import 'reflect-metadata';
+
+import { loadConfig } from '../src/config';
+import { Logger } from '../src/logger';
+
+import { getServer } from './server';
+
+async function bootstrap() {
+  await loadConfig();
+
+  const server = getServer();
+  await server.start();
+}
+
+bootstrap().catch((error: Error) => {
+  Logger.error(error);
+  if (error.stack) {
+    Logger.error(error.stack.split('\n'));
+  }
+  process.exit(1);
+});

+ 35 - 0
joystream-query-node/src/logger.ts

@@ -0,0 +1,35 @@
+/* eslint-disable no-console */
+
+import * as util from 'util';
+
+import { getBindingError } from 'warthog';
+
+export class Logger {
+  static info(...args: any[]) {
+    args = args.length === 1 ? args[0] : args;
+    console.log(util.inspect(args, { showHidden: false, depth: null }));
+  }
+
+  static error(...args: any[]) {
+    args = args.length === 1 ? args[0] : args;
+    console.error(util.inspect(args, { showHidden: false, depth: null }));
+  }
+
+  // static debug(...args: any[]) {
+  //   console.debug(args);
+  // }
+
+  static log(...args: any[]) {
+    console.log(args);
+  }
+
+  static warn(...args: any[]) {
+    console.warn(args);
+  }
+
+  // This takes a raw GraphQL error and pulls out the relevant info
+  static logGraphQLError(error: Error) {
+    console.error(util.inspect(getBindingError(error), { showHidden: false, depth: null }));
+  }
+}
+/* eslint-enable no-console */

+ 10 - 0
joystream-query-node/src/modules/member-registered/member-registered.model.ts

@@ -0,0 +1,10 @@
+import { BaseModel, IntField, Model, StringField } from 'warthog';
+
+@Model()
+export class MemberRegistered extends BaseModel {
+  @IntField()
+  memberId!: number;
+
+  @StringField()
+  accountId!: string;
+}

+ 69 - 0
joystream-query-node/src/modules/member-registered/member-registered.resolver.ts

@@ -0,0 +1,69 @@
+import { Arg, Args, Mutation, Query, Resolver } from 'type-graphql';
+import { Inject } from 'typedi';
+import { Fields, StandardDeleteResponse, UserId } from 'warthog';
+
+import {
+  MemberRegisteredCreateInput,
+  MemberRegisteredCreateManyArgs,
+  MemberRegisteredUpdateArgs,
+  MemberRegisteredWhereArgs,
+  MemberRegisteredWhereInput,
+  MemberRegisteredWhereUniqueInput
+} from '../../../generated';
+
+import { MemberRegistered } from './member-registered.model';
+import { MemberRegisteredService } from './member-registered.service';
+
+@Resolver(MemberRegistered)
+export class MemberRegisteredResolver {
+  constructor(
+    @Inject('MemberRegisteredService') public readonly service: MemberRegisteredService
+  ) {}
+
+  @Query(() => [MemberRegistered])
+  async memberRegistereds(
+    @Args() { where, orderBy, limit, offset }: MemberRegisteredWhereArgs,
+    @Fields() fields: string[]
+  ): Promise<MemberRegistered[]> {
+    return this.service.find<MemberRegisteredWhereInput>(where, orderBy, limit, offset, fields);
+  }
+
+  @Query(() => MemberRegistered)
+  async memberRegistered(
+    @Arg('where') where: MemberRegisteredWhereUniqueInput
+  ): Promise<MemberRegistered> {
+    return this.service.findOne<MemberRegisteredWhereUniqueInput>(where);
+  }
+
+  @Mutation(() => MemberRegistered)
+  async createMemberRegistered(
+    @Arg('data') data: MemberRegisteredCreateInput,
+    @UserId() userId: string
+  ): Promise<MemberRegistered> {
+    return this.service.create(data, userId);
+  }
+
+  @Mutation(() => [MemberRegistered])
+  async createManyMemberRegistereds(
+    @Args() { data }: MemberRegisteredCreateManyArgs,
+    @UserId() userId: string
+  ): Promise<MemberRegistered[]> {
+    return this.service.createMany(data, userId);
+  }
+
+  @Mutation(() => MemberRegistered)
+  async updateMemberRegistered(
+    @Args() { data, where }: MemberRegisteredUpdateArgs,
+    @UserId() userId: string
+  ): Promise<MemberRegistered> {
+    return this.service.update(data, where, userId);
+  }
+
+  @Mutation(() => StandardDeleteResponse)
+  async deleteMemberRegistered(
+    @Arg('where') where: MemberRegisteredWhereUniqueInput,
+    @UserId() userId: string
+  ): Promise<StandardDeleteResponse> {
+    return this.service.delete(where, userId);
+  }
+}

+ 15 - 0
joystream-query-node/src/modules/member-registered/member-registered.service.ts

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

+ 34 - 0
joystream-query-node/src/server.ts

@@ -0,0 +1,34 @@
+import 'reflect-metadata';
+
+import { BaseContext, Server } from 'warthog';
+
+import { Logger } from './logger';
+
+interface Context extends BaseContext {
+  user: {
+    email: string;
+    id: string;
+    permissions: string;
+  };
+}
+
+export function getServer(AppOptions = {}, dbOptions = {}) {
+  return new Server<Context>(
+    {
+      // Inject a fake user.  In a real app you'd parse a JWT to add the user
+      context: (request: any) => {
+        const userId = JSON.stringify(request.headers).length.toString();
+
+        return {
+          user: {
+            id: `user:${userId}`
+          }
+        };
+      },
+      introspection: true,
+      logger: Logger,
+      ...AppOptions
+    },
+    dbOptions
+  );
+}

+ 24 - 0
joystream-query-node/tsconfig.json

@@ -0,0 +1,24 @@
+{
+  "compilerOptions": {
+    "allowSyntheticDefaultImports": true,
+    "skipLibCheck": true,
+    "outDir": "./dist",
+    "lib": ["dom", "es5", "es6", "es7", "esnext"],
+    "target": "es5",
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "sourceMap": true,
+    "keyofStringsOnly": true,
+    "noImplicitAny": true,
+    "noImplicitReturns": true,
+    "noImplicitThis": true,
+    "noUnusedLocals": false, // Avoid noise in our resolvers when we use DI
+    "strict": true,
+    "strictNullChecks": true,
+    "types": ["isomorphic-fetch", "node"]
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules/**/*", "generated/**/*", "tools/**/*"]
+}

+ 3 - 0
joystream-query-node/warthog.config.js

@@ -0,0 +1,3 @@
+module.exports = {
+  cliGeneratePath: './src/modules/${kebabName}'
+};

+ 40 - 0
substrate-query-node/README.md

@@ -0,0 +1,40 @@
+# Substrate Query Node (Experimental)
+
+This is an experimental work, Runtime Event Listener.
+
+### Run Joystream node
+
+In order to run Even Listener a Joystrem node must be running.
+
+```bash
+# Purge existing local chain and run local development chain
+yes | cargo run --release -p joystream-node -- purge-chain --dev && cargo run --release -p joystream-node -- --dev
+```
+
+### Run event listener
+
+1. Install requirements with yarn `yarn install`
+2. Setup a PostgreSQL database. Event Listener uses typeorm to connect to database. For database configuration edit `typeorm.json` file. `typeorm.json` contains more than one database connection options and we are going to use option name `test` therefore we need to create a database named `joystream_node_test`:
+
+```bash
+$ sudo -i -u postgres
+$ createdb joystream_node_test
+```
+
+3. To run Event Listener
+
+```bash
+yarn start:test
+```
+
+### Run buy membership test
+
+Before going to run GraphQL server (joystream-query-node) we will register a member so we can have some data to check.
+
+```bash
+yarn test
+```
+
+#### How it works
+
+![Flow diagram](images/Flow.png)

BIN
substrate-query-node/images/Flow.png


+ 26 - 0
substrate-query-node/ormconfig.json

@@ -0,0 +1,26 @@
+[
+  {
+    "name": "default",
+    "type": "postgres",
+    "host": "localhost",
+    "port": 5432,
+    "username": "postgres",
+    "password": "postgres",
+    "database": "joystream_node",
+    "schema": "public",
+    "synchronize": true,
+    "entities": ["src/models/**/*.ts"]
+  },
+  {
+    "name": "test",
+    "type": "postgres",
+    "host": "localhost",
+    "port": 5432,
+    "username": "postgres",
+    "password": "postgres",
+    "database": "joystream_node_test",
+    "schema": "public",
+    "synchronize": true,
+    "entities": ["src/models/**/*.ts"]
+  }
+]

+ 35 - 0
substrate-query-node/package.json

@@ -0,0 +1,35 @@
+{
+  "name": "joystream-query-node",
+  "version": "0.1.0",
+  "license": "GPL-3.0-only",
+  "scripts": {
+    "build": "tsc --build tsconfig.json",
+    "test": "mocha -r ts-node/register tests/*.ts",
+    "lint": "tslint --project tsconfig.json",
+    "prettier": "prettier --write ./src",
+    "start": "ts-node src/index.ts default",
+    "start:test": "ts-node src/index.ts test"
+  },
+  "dependencies": {
+    "@joystream/types": "^0.7.0",
+    "@polkadot/api": "^0.96.1",
+    "@polkadot/keyring": "^1.7.0-beta.5",
+    "@types/bn.js": "^4.11.5",
+    "bn.js": "^4.11.8",
+    "dotenv": "^8.2.0",
+    "pg": "^8.0.2",
+    "shortid": "^2.2.15",
+    "typeorm": "^0.2.24"
+  },
+  "devDependencies": {
+    "@polkadot/ts": "^0.3.14",
+    "@types/chai": "^4.2.11",
+    "@types/mocha": "^7.0.2",
+    "chai": "^4.2.0",
+    "mocha": "^7.1.1",
+    "prettier": "2.0.2",
+    "ts-node": "^8.8.1",
+    "tslint": "^6.1.0",
+    "typescript": "^3.8.3"
+  }
+}

+ 8 - 0
substrate-query-node/src/config.ts

@@ -0,0 +1,8 @@
+import { config } from "dotenv";
+
+export function initConfig() {
+  // load envs
+  config();
+}
+
+export const typeOrmConfigName: string = process.argv[2];

+ 20 - 0
substrate-query-node/src/dispatch.ts

@@ -0,0 +1,20 @@
+import { Event } from "@polkadot/types/interfaces";
+
+import { handleMemberRegistered } from "./mapping";
+import { MEMBERREGISTERED } from "./eventNames";
+import { MemberRegisteredEvent } from "./member";
+import { getEventClassInstance } from "./helper";
+
+export async function dispatch(event: Event) {
+  const { method } = event;
+
+  switch (method) {
+    case MEMBERREGISTERED:
+      const instance = getEventClassInstance<MemberRegisteredEvent>(event, MemberRegisteredEvent);
+      handleMemberRegistered(instance);
+      break;
+
+    default:
+      break;
+  }
+}

+ 1 - 0
substrate-query-node/src/eventNames.ts

@@ -0,0 +1 @@
+export const MEMBERREGISTERED = "MemberRegistered";

+ 19 - 0
substrate-query-node/src/helper.ts

@@ -0,0 +1,19 @@
+import * as _ from "lodash";
+import { Event } from "@polkadot/types/interfaces";
+
+import { BaseMemberEvent } from "./member";
+
+export function getEventClassInstance<T extends BaseMemberEvent>(event: Event, type: { new (): T }): T {
+  const { data, typeDef } = event;
+  const instance = new type();
+
+  let key: string;
+  // loop through each of the parameters, get type and data
+  data.map((data, index) => {
+    key = _.camelCase(typeDef[index].type);
+    if (data) {
+      instance[key] = data;
+    }
+  });
+  return instance;
+}

+ 40 - 0
substrate-query-node/src/index.ts

@@ -0,0 +1,40 @@
+import "reflect-metadata";
+import { WsProvider, ApiPromise } from "@polkadot/api";
+import { registerJoystreamTypes } from "@joystream/types";
+import { EventRecord } from "@polkadot/types/interfaces";
+import { Vec } from "@polkadot/types";
+import { createConnection } from "typeorm";
+
+import { initConfig, typeOrmConfigName } from "./config";
+import { dispatch } from "./dispatch";
+
+async function main() {
+  // Load envs
+  initConfig();
+
+  // type orm db connection
+  await createConnection(typeOrmConfigName);
+
+  registerJoystreamTypes();
+
+  const nodeUrl: string = process.env.NODE_URL!;
+  const provider = new WsProvider(nodeUrl);
+  const api = await ApiPromise.create({ provider });
+
+  console.log("Listening Joystream events...");
+  api.query.system.events((events: Vec<EventRecord>) => {
+    // loop through the Vec<EventRecord>
+    events.forEach((record: EventRecord) => {
+      // extract the phase, event and the event types
+      const { event } = record;
+      console.log(`Event====>>> ${event.section}:${event.method}`);
+      if (event.section === "members") {
+        dispatch(event);
+      }
+    });
+  });
+
+  //api.disconnect();
+}
+
+main();

+ 17 - 0
substrate-query-node/src/mapping.ts

@@ -0,0 +1,17 @@
+import { getConnection } from "typeorm";
+import * as shortid from "shortid";
+
+import { MemberRegisteredEvent } from "./member";
+import { MemberRegistereds } from "./models/members/MemberRegistereds";
+import { typeOrmConfigName } from "./config";
+
+export function handleMemberRegistered(event: MemberRegisteredEvent) {
+	const member = new MemberRegistereds();
+	member.memberId = event.memberId;
+	member.accountId = event.accountId;
+	member.id = shortid.generate();
+
+	console.log(member);
+
+	getConnection(typeOrmConfigName).getRepository(MemberRegistereds).save(member);
+}

+ 50 - 0
substrate-query-node/src/member.ts

@@ -0,0 +1,50 @@
+/**
+ * This module contains corresponding classes for Joystream member module
+ */
+
+export class BaseMemberEvent {
+  private _memberId: number;
+
+  public set memberId(v: any) {
+    this._memberId = +v;
+  }
+
+  public get memberId() {
+    return this._memberId;
+  }
+}
+
+export class BaseMemberAccountEvent extends BaseMemberEvent {
+  private _accountId: string;
+
+  public set accountId(v: any) {
+    this._accountId = v.toString();
+  }
+
+  public get accountId() {
+    return this._accountId;
+  }
+}
+
+export class BaseMemberRoleEvent extends BaseMemberEvent {
+  private _actorId: string;
+
+  public set actorId(v: any) {
+    this._actorId = v.toString();
+  }
+
+  public get actorId() {
+    return this._actorId;
+  }
+}
+
+export class MemberRegisteredEvent extends BaseMemberAccountEvent {}
+export class MemberSetRootAccountEvent extends BaseMemberAccountEvent {}
+export class MemberSetControllerAccountEvent extends BaseMemberAccountEvent {}
+
+export class MemberUpdatedAboutTextEvent extends BaseMemberEvent {}
+export class MemberUpdatedAvatarEvent extends BaseMemberEvent {}
+export class MemberUpdatedHandleEvent extends BaseMemberEvent {}
+
+export class MemberRegisteredRoleEvent extends BaseMemberEvent {}
+export class MemberUnregisteredRoleEvent extends BaseMemberEvent {}

+ 42 - 0
substrate-query-node/src/models/members/MemberRegistereds.ts

@@ -0,0 +1,42 @@
+import { Column, Entity, Index } from "typeorm";
+
+@Index("PK_0b13cb7ccd19cd85d37a280561f", ["id"], { unique: true })
+@Entity("member_registereds", { schema: "public" })
+export class MemberRegistereds {
+  @Column("character varying", { primary: true, name: "id" })
+  id: string;
+
+  @Column("timestamp without time zone", {
+    name: "created_at",
+    default: () => "now()",
+  })
+  createdAt: Date;
+
+  @Column("integer", { name: "version", default: 1 })
+  version: number;
+
+  @Column("integer", { name: "member_id" })
+  memberId: number;
+
+  @Column("character varying", { name: "account_id" })
+  accountId: string;
+
+  @Column("timestamp without time zone", { name: "deleted_at", nullable: true })
+  deletedAt: Date | null;
+
+  @Column("timestamp without time zone", {
+    name: "updated_at",
+    nullable: true,
+    default: () => "now()",
+  })
+  updatedAt: Date | null;
+
+  @Column("character varying", { name: "updated_by_id", nullable: true })
+  updatedById: string | null;
+
+  @Column("character varying", { name: "deleted_by_id", nullable: true })
+  deletedById: string | null;
+
+  @Column("character varying", { name: "created_by_id", default: "1" })
+  createdById: string;
+}

+ 101 - 0
substrate-query-node/tests/membershipCreationTest.ts

@@ -0,0 +1,101 @@
+import BN = require("bn.js");
+import { WsProvider } from "@polkadot/api";
+import { registerJoystreamTypes } from "@joystream/types";
+import { Keyring } from "@polkadot/keyring";
+import { assert } from "chai";
+import { KeyringPair } from "@polkadot/keyring/types";
+import { createConnection, Connection } from "typeorm";
+
+import { ApiWrapper } from "./utils/apiWrapper";
+import { initConfig } from "../src/config";
+import { clearDBData } from "./utils/db";
+
+describe("Create memberships for event listener", () => {
+	initConfig();
+
+	const keyring = new Keyring({ type: "sr25519" });
+	const nKeyPairs: KeyringPair[] = new Array();
+	const N: number = +process.env.MEMBERSHIP_CREATION_N!;
+	const paidTerms: number = +process.env.MEMBERSHIP_PAID_TERMS!;
+	const nodeUrl: string = process.env.NODE_URL!;
+	const sudoUri: string = process.env.SUDO_ACCOUNT_URI!;
+	const defaultTimeout: number = 30000;
+	let apiWrapper: ApiWrapper;
+	let sudo: KeyringPair;
+	let aKeyPair: KeyringPair;
+	let membershipFee: BN;
+	let membershipTransactionFee: BN;
+	let connection: Connection;
+
+	before(async function () {
+		connection = await createConnection("test");
+		clearDBData("MemberRegistereds", connection);
+
+		this.timeout(defaultTimeout);
+		registerJoystreamTypes();
+		const provider = new WsProvider(nodeUrl);
+		apiWrapper = await ApiWrapper.create(provider);
+		sudo = keyring.addFromUri(sudoUri);
+		for (let i = 0; i < N; i++) {
+			nKeyPairs.push(keyring.addFromUri(i.toString()));
+		}
+		aKeyPair = keyring.addFromUri("A");
+		membershipFee = await apiWrapper.getMembershipFee(paidTerms);
+		membershipTransactionFee = apiWrapper.estimateBuyMembershipFee(
+			sudo,
+			paidTerms,
+			"member_name_which_is_longer_than_expected"
+		);
+		await apiWrapper.transferBalanceToAccounts(
+			sudo,
+			nKeyPairs,
+			membershipTransactionFee.add(new BN(membershipFee))
+		);
+		await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipTransactionFee);
+	});
+
+	it("Buy membeship is accepted with sufficient funds", async () => {
+		await Promise.all(
+			nKeyPairs.map(async (keyPair, index) => {
+				await apiWrapper.buyMembership(keyPair, paidTerms, `new_member_${index}`);
+			})
+		);
+		nKeyPairs.forEach((keyPair, index) =>
+			apiWrapper
+				.getMembership(keyPair.address)
+				.then((membership) => assert(!membership.isEmpty, `Account ${index} is not a member`))
+		);
+	}).timeout(defaultTimeout);
+
+	it("Account A can not buy the membership with insufficient funds", async () => {
+		apiWrapper
+			.getBalance(aKeyPair.address)
+			.then((balance) =>
+				assert(
+					balance.toBn() < membershipFee.add(membershipTransactionFee),
+					"Account A already have sufficient balance to purchase membership"
+				)
+			);
+		await apiWrapper.buyMembership(aKeyPair, paidTerms, "late_member", true);
+		apiWrapper
+			.getMembership(aKeyPair.address)
+			.then((membership) => assert(membership.isEmpty, "Account A is a member"));
+	}).timeout(defaultTimeout);
+
+	it("Account A was able to buy the membership with insufficient funds", async () => {
+		await apiWrapper.transferBalance(sudo, aKeyPair.address, membershipFee.add(membershipTransactionFee));
+		apiWrapper
+			.getBalance(aKeyPair.address)
+			.then((balance) =>
+				assert(balance.toBn() >= membershipFee, "The account balance is insufficient to purchase membership")
+			);
+		await apiWrapper.buyMembership(aKeyPair, paidTerms, "late_member");
+		apiWrapper
+			.getMembership(aKeyPair.address)
+			.then((membership) => assert(!membership.isEmpty, "Account A is a not member"));
+	}).timeout(defaultTimeout);
+
+	after(async () => {
+		apiWrapper.close();
+	});
+});

+ 123 - 0
substrate-query-node/tests/utils/apiWrapper.ts

@@ -0,0 +1,123 @@
+import { ApiPromise, WsProvider } from "@polkadot/api";
+import { Option } from "@polkadot/types";
+import { KeyringPair } from "@polkadot/keyring/types";
+import { UserInfo, PaidMembershipTerms } from "@joystream/types/lib/members";
+import { Balance } from "@polkadot/types/interfaces";
+import BN = require("bn.js");
+import { SubmittableExtrinsic } from "@polkadot/api/types";
+import { Sender } from "./sender";
+import { Utils } from "./utils";
+
+export class ApiWrapper {
+  private readonly api: ApiPromise;
+  private readonly sender: Sender;
+
+  public static async create(provider: WsProvider): Promise<ApiWrapper> {
+    const api = await ApiPromise.create({ provider });
+    return new ApiWrapper(api);
+  }
+
+  constructor(api: ApiPromise) {
+    this.api = api;
+    this.sender = new Sender(api);
+  }
+
+  public close() {
+    this.api.disconnect();
+  }
+
+  public async buyMembership(
+    account: KeyringPair,
+    paidTermsId: number,
+    name: string,
+    expectFailure = false
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.members.buyMembership(
+        paidTermsId,
+        new UserInfo({ handle: name, avatar_uri: "", about: "" })
+      ),
+      account,
+      expectFailure
+    );
+  }
+
+  public getMembership(address: string): Promise<any> {
+    return this.api.query.members.memberIdsByControllerAccountId(address);
+  }
+
+  public getBalance(address: string): Promise<Balance> {
+    return this.api.query.balances.freeBalance<Balance>(address);
+  }
+
+  public async transferBalance(
+    from: KeyringPair,
+    to: string,
+    amount: BN
+  ): Promise<void> {
+    return this.sender.signAndSend(
+      this.api.tx.balances.transfer(to, amount),
+      from
+    );
+  }
+
+  public getPaidMembershipTerms(
+    paidTermsId: number
+  ): Promise<Option<PaidMembershipTerms>> {
+    return this.api.query.members.paidMembershipTermsById<
+      Option<PaidMembershipTerms>
+    >(paidTermsId);
+  }
+
+  public getMembershipFee(paidTermsId: number): Promise<BN> {
+    return this.getPaidMembershipTerms(paidTermsId).then((terms) =>
+      terms.unwrap().fee.toBn()
+    );
+  }
+
+  public async transferBalanceToAccounts(
+    from: KeyringPair,
+    to: KeyringPair[],
+    amount: BN
+  ): Promise<void[]> {
+    return Promise.all(
+      to.map(async (keyPair) => {
+        await this.transferBalance(from, keyPair.address, amount);
+      })
+    );
+  }
+
+  private getBaseTxFee(): BN {
+    return this.api
+      .createType(
+        "BalanceOf",
+        this.api.consts.transactionPayment.transactionBaseFee
+      )
+      .toBn();
+  }
+
+  private estimateTxFee(tx: SubmittableExtrinsic<"promise">): BN {
+    const baseFee: BN = this.getBaseTxFee();
+    const byteFee: BN = this.api
+      .createType(
+        "BalanceOf",
+        this.api.consts.transactionPayment.transactionByteFee
+      )
+      .toBn();
+    return Utils.calcTxLength(tx).mul(byteFee).add(baseFee);
+  }
+
+  public estimateBuyMembershipFee(
+    account: KeyringPair,
+    paidTermsId: number,
+    name: string
+  ): BN {
+    const nonce: BN = new BN(0);
+    return this.estimateTxFee(
+      this.api.tx.members.buyMembership(
+        paidTermsId,
+        new UserInfo({ handle: name, avatar_uri: "", about: "" })
+      )
+    );
+  }
+}

+ 9 - 0
substrate-query-node/tests/utils/db.ts

@@ -0,0 +1,9 @@
+import { getRepository, getConnection, Connection } from "typeorm";
+
+export async function clearDBData(model: string, connection: Connection) {
+  const records = await connection.getRepository(model).find();
+  console.log("Records", records);
+  records.forEach(async (record) => {
+    await connection.getRepository(model).delete({ id: record["id"] });
+  });
+}

+ 68 - 0
substrate-query-node/tests/utils/sender.ts

@@ -0,0 +1,68 @@
+import BN = require("bn.js");
+import { ApiPromise } from "@polkadot/api";
+import { Index } from "@polkadot/types/interfaces";
+import { SubmittableExtrinsic } from "@polkadot/api/types";
+import { KeyringPair } from "@polkadot/keyring/types";
+
+export class Sender {
+  private readonly api: ApiPromise;
+  private nonceMap: Map<string, BN> = new Map();
+
+  constructor(api: ApiPromise) {
+    this.api = api;
+  }
+
+  private async getNonce(address: string): Promise<BN> {
+    let oncahinNonce: any;
+    if (!this.nonceMap.get(address)) {
+      oncahinNonce = await this.api.query.system.accountNonce<Index>(address);
+    }
+    let nonce: BN | undefined = this.nonceMap.get(address);
+    if (!nonce) {
+      nonce = oncahinNonce;
+    }
+    const nextNonce: BN = nonce.addn(1);
+    this.nonceMap.set(address, nextNonce);
+    return nonce;
+  }
+
+  private clearNonce(address: string): void {
+    this.nonceMap.delete(address);
+  }
+
+  public async signAndSend(
+    tx: SubmittableExtrinsic<"promise">,
+    account: KeyringPair,
+    expectFailure = false
+  ): Promise<void> {
+    return new Promise(async (resolve, reject) => {
+      const nonce: BN = await this.getNonce(account.address);
+      const signedTx = tx.sign(account, { nonce });
+      await signedTx
+        .send(async (result) => {
+          if (
+            result.status.isFinalized === true &&
+            result.events !== undefined
+          ) {
+            result.events.forEach((event) => {
+              if (event.event.method === "ExtrinsicFailed") {
+                if (expectFailure) {
+                  resolve();
+                } else {
+                  reject(new Error("Extrinsic failed unexpectedly"));
+                }
+              }
+            });
+            resolve();
+          }
+          if (result.status.isFuture) {
+            this.clearNonce(account.address);
+            reject(new Error("Extrinsic nonce is in future"));
+          }
+        })
+        .catch((error) => {
+          reject(error);
+        });
+    });
+  }
+}

+ 21 - 0
substrate-query-node/tests/utils/utils.ts

@@ -0,0 +1,21 @@
+import { IExtrinsic } from '@polkadot/types/types';
+import { compactToU8a } from '@polkadot/util';
+import BN = require('bn.js');
+
+export class Utils {
+  private static LENGTH_ADDRESS = 32 + 1; // publicKey + prefix
+  private static LENGTH_ERA = 2; // assuming mortals
+  private static LENGTH_SIGNATURE = 64; // assuming ed25519 or sr25519
+  private static LENGTH_VERSION = 1; // 0x80 & version
+
+  public static calcTxLength = (extrinsic?: IExtrinsic | null, nonce?: BN): BN => {
+    return new BN(
+      Utils.LENGTH_VERSION +
+        Utils.LENGTH_ADDRESS +
+        Utils.LENGTH_SIGNATURE +
+        Utils.LENGTH_ERA +
+        compactToU8a(nonce || 0).length +
+        (extrinsic ? extrinsic.encodedLength : 0)
+    );
+  };
+}

+ 10 - 0
substrate-query-node/tsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "target": "es2017",
+    "module": "commonjs",
+    "moduleResolution": "node",
+    "emitDecoratorMetadata": true,
+    "experimentalDecorators": true,
+    "sourceMap": true
+  }
+}