Prechádzať zdrojové kódy

introduce content directory mappings

metmirr 4 rokov pred
rodič
commit
ffe52b6228

+ 59 - 0
query-node/.env

@@ -0,0 +1,59 @@
+# Project name
+PROJECT_NAME=query_node
+
+###########################
+#     Common settings     #
+###########################
+
+# The env variables below are by default used by all services and should be 
+# overriden in local env files (e.g. ./generated/indexer) if needed
+# DB config
+DB_NAME=query_node
+DB_USER=postgres
+DB_PASS=postgres
+DB_HOST=localhost
+DB_PORT=5432
+DEBUG=index-builder:*
+TYPEORM_LOGGING=error
+
+###########################
+#    Indexer options      #
+###########################
+
+# Substrate endpoint to source events from
+WS_PROVIDER_ENDPOINT_URI=ws://localhost:9944/
+# Block height to start indexing from.
+# Note, that if there are already some indexed events, this setting is ignored
+BLOCK_HEIGHT=0
+
+# Custom types to register for Substrate API
+# TYPE_REGISTER_PACKAGE_NAME=
+# TYPE_REGISTER_PACKAGE_VERSION=
+# TYPE_REGISTER_FUNCTION=
+
+# Redis cache server
+REDIS_URI=redis://localhost:6379/0
+
+###########################
+#    Processor options    #
+###########################
+
+# Where the mapping scripts are located, relative to ./generated/indexer
+MAPPINGS_LOCATION=../../src
+TYPES_JSON=../../typedefs.json
+
+# Indexer GraphQL API endpoint to fetch indexed events
+INDEXER_ENDPOINT_URL=http://localhost:4100/graphql
+
+# Block height from which the processor starts. Note that if 
+# there are already processed events in the database, this setting is ignored
+BLOCK_HEIGHT=0
+
+###############################
+#    Processor GraphQL API    #
+###############################
+
+GRAPHQL_SERVER_PORT=4100
+GRAPHQL_SERVER_HOST=localhost
+WARTHOG_APP_PORT=4100
+WARTHOG_APP_HOST=localhost

+ 36 - 0
query-node/package.json

@@ -0,0 +1,36 @@
+{
+	"name": "joystream_query_node",
+	"version": "1.0.0",
+	"description": "GraphQL server and Substrate indexer. Generated with ♥ by Hydra-CLI",
+	"scripts": {
+		"build": "tsc --build tsconfig.json",
+		"test": "echo \"Error: no test specified\" && exit 1",
+		"clean": "rm -rf ./generated",
+		"processor:start": "(cd ./generated/indexer && yarn && DEBUG=${DEBUG} yarn start:processor)",
+		"indexer:start": "(cd ./generated/indexer && yarn && DEBUG=${DEBUG} yarn start:indexer)",
+		"server:start:dev": "(cd ./generated/graphql-server && yarn start:dev)",
+		"server:start:prod": "(cd ./generated/graphql-server && yarn start:prod)",
+		"configure": "(cd ./generated/graphql-server && yarn config:dev)",
+		"db:up": "docker-compose up -d db",
+		"db:drop": "(cd ./generated/graphql-server && yarn db:drop)",
+		"db:schema:migrate": "(cd ./generated/graphql-server && yarn db:create && yarn db:sync && yarn db:migrate)",
+		"db:indexer:migrate": "(cd ./generated/indexer && yarn db:migrate)",
+		"db:migrate": "yarn db:schema:migrate && yarn db:indexer:migrate",
+		"codegen:all": "hydra-cli codegen",
+		"codegen:indexer": "hydra-cli codegen --no-graphql",
+		"codegen:server": "hydra-cli codegen --no-indexer",
+		"docker:indexer:build": "docker build -t hydra-indexer -f docker/Dockerfile.indexer .",
+		"docker:server:build": "docker build -t hydra-graphql-server -f docker/Dockerfile.server .",
+		"docker:up": "docker-compose up -d"
+	},
+	"author": "",
+	"license": "ISC",
+	"dependencies": {
+		"@joystream/types": "^0.14.0",
+		"@types/bn.js": "^4.11.6",
+		"@types/debug": "^4.1.5",
+		"bn.js": "^5.1.2",
+		"debug": "^4.2.0",
+		"tslib": "^2.0.0"
+	}
+}

+ 102 - 0
query-node/src/content-directory/content-dir-consts.ts

@@ -0,0 +1,102 @@
+import { IPropertyIdWithName } from '../types'
+
+// Content directory predefined class names
+export enum ContentDirectoryKnownClasses {
+  CHANNEL = 'Channel',
+  CATEGORY = 'Category',
+  KNOWNLICENSE = 'KnownLicense',
+  USERDEFINEDLICENSE = 'UserDefinedLicense',
+  JOYSTREAMMEDIALOCATION = 'JoystreamMediaLocation',
+  HTTPMEDIALOCATION = 'HttpMediaLocation',
+  VIDEOMEDIA = 'VideoMedia',
+  VIDEO = 'Video',
+  LANGUAGE = 'Language',
+  VIDEOMEDIAENCODING = 'VideoMediaEncoding',
+}
+
+// Predefined content-directory classes, classId may change after the runtime seeding
+export const contentDirectoryClassNamesWithId: { classId: number; name: string }[] = [
+  { name: ContentDirectoryKnownClasses.CHANNEL, classId: 1 },
+  { name: ContentDirectoryKnownClasses.CATEGORY, classId: 2 },
+  { name: ContentDirectoryKnownClasses.KNOWNLICENSE, classId: 6 },
+  { name: ContentDirectoryKnownClasses.USERDEFINEDLICENSE, classId: 0 },
+  { name: ContentDirectoryKnownClasses.LANGUAGE, classId: 7 },
+  { name: ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION, classId: 5 },
+  { name: ContentDirectoryKnownClasses.HTTPMEDIALOCATION, classId: 4 },
+  { name: ContentDirectoryKnownClasses.VIDEOMEDIA, classId: 12 },
+  { name: ContentDirectoryKnownClasses.VIDEO, classId: 11 },
+  { name: ContentDirectoryKnownClasses.VIDEOMEDIAENCODING, classId: 13 },
+]
+
+export const CategoryPropertyNamesWithId: IPropertyIdWithName = {
+  0: 'name',
+  1: 'description',
+}
+
+export const channelPropertyNamesWithId: IPropertyIdWithName = {
+  0: 'title',
+  1: 'description',
+  2: 'coverPhotoURL',
+  3: 'avatarPhotoURL',
+  4: 'isPublic',
+  5: 'isCurated',
+  6: 'language',
+}
+
+export const knownLicensePropertyNamesWIthId: IPropertyIdWithName = {
+  0: 'code',
+  1: 'name',
+  2: 'description',
+  3: 'url',
+}
+
+export const languagePropertyNamesWIthId: IPropertyIdWithName = {
+  0: 'name',
+  1: 'code',
+}
+
+export const userDefinedLicensePropertyNamesWithId: IPropertyIdWithName = {
+  0: 'content',
+}
+
+export const joystreamMediaLocationPropertyNamesWithId: IPropertyIdWithName = {
+  0: 'dataObjectId',
+}
+
+export const httpMediaLocationPropertyNamesWithId: IPropertyIdWithName = {
+  0: 'url',
+  1: 'port',
+}
+
+export const videoMediaEncodingPropertyNamesWithId: IPropertyIdWithName = {
+  0: 'name',
+}
+
+export const videoMediaPropertyNamesWithId: IPropertyIdWithName = {
+  0: 'encoding',
+  1: 'pixelWidth',
+  2: 'pixelHeight',
+  3: 'size',
+  4: 'location',
+}
+
+export const videoPropertyNamesWithId: IPropertyIdWithName = {
+  // referenced entity's id
+  0: 'channel',
+  // referenced entity's id
+  1: 'category',
+  2: 'title',
+  3: 'description',
+  4: 'duration',
+  5: 'skippableIntroDuration',
+  6: 'thumbnailURL',
+  7: 'language',
+  // referenced entity's id
+  8: 'media',
+  9: 'hasMarketing',
+  10: 'publishedBeforeJoystream',
+  11: 'isPublic',
+  12: 'isExplicit',
+  13: 'license',
+  14: 'isCurated',
+}

+ 136 - 0
query-node/src/content-directory/decode.ts

@@ -0,0 +1,136 @@
+import { SubstrateEvent } from '../../generated/indexer'
+import {
+  IPropertyIdWithName,
+  IClassEntity,
+  IProperty,
+  IBatchOperation,
+  ICreateEntityOperation,
+  IEntity,
+} from '../types'
+
+import { ParametrizedClassPropertyValue, UpdatePropertyValuesOperation } from '@joystream/types/content-directory'
+import { createType } from '@joystream/types'
+
+function stringIfyEntityId(event: SubstrateEvent): string {
+  const { 1: entityId } = event.params
+  return entityId.value as string
+}
+
+function setProperties<T>({ extrinsic, blockNumber }: SubstrateEvent, propNamesWithId: IPropertyIdWithName): T {
+  if (extrinsic === undefined) throw Error('Undefined extrinsic')
+
+  const { 3: newPropertyValues } = extrinsic?.args
+  const properties: { [key: string]: any } = {}
+
+  for (const [k, v] of Object.entries(newPropertyValues.value)) {
+    const propertyName = propNamesWithId[k]
+    properties[propertyName] = v
+  }
+  properties.version = blockNumber
+  return properties as T
+}
+
+function getClassEntity(event: SubstrateEvent): IClassEntity {
+  const { 0: classId } = event.extrinsic?.args
+  const { 1: entityId } = event.params
+  return {
+    entityId: (entityId.value as unknown) as number,
+    classId: (classId.value as unknown) as number,
+  }
+}
+
+function setEntityPropertyValues<T>(properties: IProperty[], propertyNamesWithId: IPropertyIdWithName): T {
+  const entityProperties: { [key: string]: any } = {}
+
+  for (const [propId, propName] of Object.entries(propertyNamesWithId)) {
+    // get the property value by id
+    const p = properties.find((p) => p.propertyId === propId)
+    const propertyValue = p ? p.value : undefined
+    entityProperties[propName] = propertyValue
+  }
+  // console.log(entityProperties);
+  return entityProperties as T
+}
+
+// Decode entity property values
+function getEntityProperties(propertyValues: ParametrizedClassPropertyValue[]): IProperty[] {
+  const properties: IProperty[] = []
+  const entityPropertyValues = createType('Vec<ParametrizedClassPropertyValue>', propertyValues)
+
+  entityPropertyValues.map((pv) => {
+    const v = createType('ParametrizedPropertyValue', pv.value)
+    const propertyId = pv.in_class_index.toJSON()
+
+    let value
+    if (v.isOfType('InputPropertyValue')) {
+      const inputPropVal = v.asType('InputPropertyValue')
+      value = inputPropVal.isOfType('Single')
+        ? inputPropVal.asType('Single').value.toJSON()
+        : inputPropVal.asType('Vector').value.toJSON()
+    } else if (v.isOfType('InternalEntityJustAdded')) {
+      // const inputPropVal = v.asType('InternalEntityJustAdded');
+      value = v.asType('InternalEntityJustAdded').toJSON()
+    } else {
+      // TODO: Add support for v.asType('InternalEntityVec')
+      throw Error('InternalEntityVec property type is not supported yet!')
+    }
+    properties.push({ propertyId: `${propertyId}`, value })
+  })
+  return properties
+}
+
+function getOperations({ extrinsic }: SubstrateEvent): IBatchOperation {
+  const operations = createType('Vec<OperationType>', extrinsic?.args[1].value)
+
+  const updatePropertyValuesOperations: IEntity[] = []
+  const addSchemaSupportToEntityOperations: IEntity[] = []
+  const createEntityOperations: ICreateEntityOperation[] = []
+
+  for (const operation of operations) {
+    if (operation.isOfType('CreateEntity')) {
+      const cep = operation.asType('CreateEntity')
+      createEntityOperations.push({ classId: cep.class_id.toJSON() })
+    } else if (operation.isOfType('AddSchemaSupportToEntity')) {
+      const op = operation.asType('AddSchemaSupportToEntity')
+      const pe = createType('ParameterizedEntity', op.entity_id)
+      const entity: IEntity = {
+        properties: decode.getEntityProperties(op.parametrized_property_values),
+      }
+      if (pe.isOfType('InternalEntityJustAdded')) {
+        entity.indexOf = pe.asType('InternalEntityJustAdded').toJSON()
+      } else {
+        entity.entityId = pe.asType('ExistingEntity').toJSON()
+      }
+      addSchemaSupportToEntityOperations.push(entity)
+    } else {
+      updatePropertyValuesOperations.push(makeEntity(operation.asType('UpdatePropertyValues')))
+    }
+  }
+  return {
+    updatePropertyValuesOperations,
+    addSchemaSupportToEntityOperations,
+    createEntityOperations,
+  }
+}
+
+function makeEntity(upv: UpdatePropertyValuesOperation): IEntity {
+  const entity: IEntity = {
+    properties: decode.getEntityProperties(upv.new_parametrized_property_values),
+  }
+  const pe = createType('ParameterizedEntity', upv.entity_id)
+  if (pe.isOfType('InternalEntityJustAdded')) {
+    entity.indexOf = pe.asType('InternalEntityJustAdded').toJSON()
+  } else {
+    entity.entityId = pe.asType('ExistingEntity').toJSON()
+  }
+  return entity
+}
+
+export const decode = {
+  stringIfyEntityId,
+  getClassEntity,
+  setEntityPropertyValues,
+  getEntityProperties,
+  getOperations,
+  setProperties,
+}

+ 474 - 0
query-node/src/content-directory/entity-helper.ts

@@ -0,0 +1,474 @@
+import { DB, SubstrateEvent } from '../../generated/indexer'
+import { Channel } from '../../generated/graphql-server/src/modules/channel/channel.model'
+import { Category } from '../../generated/graphql-server/src/modules/category/category.model'
+import { KnownLicense } from '../../generated/graphql-server/src/modules/known-license/known-license.model'
+import { UserDefinedLicense } from '../../generated/graphql-server/src/modules/user-defined-license/user-defined-license.model'
+import { JoystreamMediaLocation } from '../../generated/graphql-server/src/modules/joystream-media-location/joystream-media-location.model'
+import { HttpMediaLocation } from '../../generated/graphql-server/src/modules/http-media-location/http-media-location.model'
+import { VideoMedia } from '../../generated/graphql-server/src/modules/video-media/video-media.model'
+import { Video } from '../../generated/graphql-server/src/modules/video/video.model'
+import { Block, Network } from '../../generated/graphql-server/src/modules/block/block.model'
+import { Language } from '../../generated/graphql-server/src/modules/language/language.model'
+import { VideoMediaEncoding } from '../../generated/graphql-server/src/modules/video-media-encoding/video-media-encoding.model'
+import { ClassEntity } from '../../generated/graphql-server/src/modules/class-entity/class-entity.model'
+import { decode } from './decode'
+import {
+  CategoryPropertyNamesWithId,
+  channelPropertyNamesWithId,
+  httpMediaLocationPropertyNamesWithId,
+  joystreamMediaLocationPropertyNamesWithId,
+  knownLicensePropertyNamesWIthId,
+  languagePropertyNamesWIthId,
+  userDefinedLicensePropertyNamesWithId,
+  videoMediaEncodingPropertyNamesWithId,
+  videoPropertyNamesWithId,
+  contentDirectoryClassNamesWithId,
+  ContentDirectoryKnownClasses,
+} from './content-dir-consts'
+import {
+  ICategory,
+  IChannel,
+  ICreateEntityOperation,
+  IDBBlockId,
+  IEntity,
+  IHttpMediaLocation,
+  IJoystreamMediaLocation,
+  IKnownLicense,
+  ILanguage,
+  IUserDefinedLicense,
+  IVideo,
+  IVideoMedia,
+  IVideoMediaEncoding,
+  IWhereCond,
+} from '../types'
+
+async function createBlockOrGetFromDatabase(db: DB, blockNumber: number): Promise<Block> {
+  let b = await db.get(Block, { where: { block: blockNumber } })
+  if (b === undefined) {
+    // TODO: get timestamp from the event or extrinsic
+    b = new Block({ block: blockNumber, nework: Network.BABYLON, timestamp: 123 })
+    await db.save<Block>(b)
+  }
+  return b
+}
+
+async function createChannel({ db, block, id }: IDBBlockId, p: IChannel): Promise<void> {
+  // const { properties: p } = decode.channelEntity(event);
+  const channel = new Channel()
+
+  channel.version = block
+  channel.id = id
+  channel.title = p.title
+  channel.description = p.description
+  channel.isCurated = p.isCurated || false
+  channel.isPublic = p.isPublic
+  channel.coverPhotoUrl = p.coverPhotoURL
+  channel.avatarPhotoUrl = p.avatarPhotoURL
+  channel.languageId = p.language
+  channel.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  await db.save(channel)
+}
+
+async function createCategory({ db, block, id }: IDBBlockId, p: ICategory): Promise<void> {
+  // const p = decode.categoryEntity(event);
+  const category = new Category()
+
+  category.id = id
+  category.name = p.name
+  category.description = p.description
+  category.version = block
+  category.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  await db.save(category)
+}
+
+async function createKnownLicense({ db, block, id }: IDBBlockId, p: IKnownLicense): Promise<void> {
+  const knownLicence = new KnownLicense()
+
+  knownLicence.id = id
+  knownLicence.code = p.code
+  knownLicence.name = p.name
+  knownLicence.description = p.description
+  knownLicence.url = p.url
+  knownLicence.version = block
+  knownLicence.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  await db.save(knownLicence)
+}
+
+async function createUserDefinedLicense({ db, block, id }: IDBBlockId, p: IUserDefinedLicense): Promise<void> {
+  const userDefinedLicense = new UserDefinedLicense()
+
+  userDefinedLicense.id = id
+  userDefinedLicense.content = p.content
+  userDefinedLicense.version = block
+  userDefinedLicense.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  await db.save(userDefinedLicense)
+}
+
+async function createJoystreamMediaLocation({ db, block, id }: IDBBlockId, p: IJoystreamMediaLocation): Promise<void> {
+  const joyMediaLoc = new JoystreamMediaLocation()
+
+  joyMediaLoc.id = id
+  joyMediaLoc.dataObjectId = p.dataObjectId
+  joyMediaLoc.version = block
+  joyMediaLoc.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  await db.save(joyMediaLoc)
+}
+
+async function createHttpMediaLocation({ db, block, id }: IDBBlockId, p: IHttpMediaLocation): Promise<void> {
+  const httpMediaLoc = new HttpMediaLocation()
+
+  httpMediaLoc.id = id
+  httpMediaLoc.url = p.url
+  httpMediaLoc.port = p.port
+  httpMediaLoc.version = block
+  httpMediaLoc.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  await db.save(httpMediaLoc)
+}
+
+async function createVideoMedia({ db, block, id }: IDBBlockId, p: IVideoMedia): Promise<void> {
+  const videoMedia = new VideoMedia()
+
+  videoMedia.id = id
+  videoMedia.encodingId = p.encoding
+  videoMedia.locationId = p.location
+  videoMedia.pixelHeight = p.pixelHeight
+  videoMedia.pixelWidth = p.pixelWidth
+  videoMedia.size = p.size
+  videoMedia.version = block
+  videoMedia.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  await db.save(videoMedia)
+}
+
+async function createVideo({ db, block, id }: IDBBlockId, p: IVideo): Promise<void> {
+  const video = new Video()
+
+  video.id = id
+  video.title = p.title
+  video.description = p.description
+  video.categoryId = p.category
+  video.channelId = p.channel
+  video.duration = p.duration
+  video.hasMarketing = p.hasMarketing
+  // TODO: needs to be handled correctly, from runtime CurationStatus is coming
+  video.isCurated = p.isCurated || true
+  video.isExplicit = p.isExplicit
+  video.isPublic = p.isPublic
+  video.languageId = p.language
+  video.licenseId = p.license
+  video.videoMediaId = p.media
+  video.publishedBeforeJoystream = p.publishedBeforeJoystream
+  video.skippableIntroDuration = p.skippableIntroDuration
+  video.thumbnailUrl = p.thumbnailURL
+  video.version = block
+  video.happenedIn = await createBlockOrGetFromDatabase(db, block)
+  console.log(video)
+  await db.save<Video>(video)
+}
+
+async function createLanguage({ db, block, id }: IDBBlockId, p: ILanguage): Promise<void> {
+  const language = new Language()
+  language.id = id
+  language.name = p.name
+  language.code = p.code
+  language.version = block
+  await db.save<Language>(language)
+}
+
+async function createVideoMediaEncoding({ db, block, id }: IDBBlockId, p: IVideoMediaEncoding): Promise<void> {
+  const encoding = new VideoMediaEncoding()
+
+  encoding.id = id
+  encoding.name = p.name
+  encoding.version = block
+
+  await db.save<VideoMediaEncoding>(encoding)
+}
+
+async function batchCreateClassEntities(db: DB, block: number, operations: ICreateEntityOperation[]): Promise<void> {
+  // Create entities before adding schema support
+  operations.map(async ({ classId }, index) => {
+    const c = new ClassEntity()
+    c.id = index.toString()
+    c.classId = classId
+    c.version = block
+    c.happenedIn = await createBlockOrGetFromDatabase(db, block)
+    await db.save<ClassEntity>(c)
+  })
+}
+
+async function getClassName(
+  db: DB,
+  entity: IEntity,
+  createEntityOperations: ICreateEntityOperation[]
+): Promise<string | undefined> {
+  const { entityId, indexOf } = entity
+  if (entityId === undefined && indexOf === undefined) {
+    throw Error(`Can not determine class of the entity`)
+  }
+
+  let classId: number
+  // Is newly created entity in the same transaction
+  if (indexOf !== undefined) {
+    classId = createEntityOperations[indexOf].classId
+  } else {
+    const ce = await db.get(ClassEntity, { where: { id: entityId } })
+    if (ce === undefined) throw Error(`Class not found for the entity: ${entityId}`)
+    classId = ce.classId
+  }
+
+  const c = contentDirectoryClassNamesWithId.find((c) => c.classId === classId)
+  // TODO: stop execution, class should be created before entity creation
+  if (c === undefined) console.log(`Not recognized class id: ${classId}`)
+  return c ? c.name : undefined
+}
+
+async function removeChannel(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(Channel, where)
+  if (record === undefined) throw Error(`Channel not found`)
+  await db.remove<Channel>(record)
+}
+async function removeCategory(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(Category, where)
+  if (record === undefined) throw Error(`Category not found`)
+  await db.remove<Category>(record)
+}
+async function removeVideoMedia(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(VideoMedia, where)
+  if (record === undefined) throw Error(`VideoMedia not found`)
+  await db.remove<VideoMedia>(record)
+}
+async function removeVideo(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(Video, where)
+  if (record === undefined) throw Error(`Video not found`)
+  await db.remove<Video>(record)
+}
+async function removeUserDefinedLicense(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(UserDefinedLicense, where)
+  if (record === undefined) throw Error(`UserDefinedLicense not found`)
+  await db.remove<UserDefinedLicense>(record)
+}
+async function removeKnownLicense(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(KnownLicense, where)
+  if (record === undefined) throw Error(`KnownLicense not found`)
+  await db.remove<KnownLicense>(record)
+}
+async function removeHttpMediaLocation(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(HttpMediaLocation, where)
+  if (record === undefined) throw Error(`HttpMediaLocation not found`)
+  await db.remove<HttpMediaLocation>(record)
+}
+async function removeJoystreamMediaLocation(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(JoystreamMediaLocation, where)
+  if (record === undefined) throw Error(`JoystreamMediaLocation not found`)
+  await db.remove<JoystreamMediaLocation>(record)
+}
+async function removeLanguage(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(Language, where)
+  if (record === undefined) throw Error(`Language not found`)
+  await db.remove<Language>(record)
+}
+async function removeVideoMediaEncoding(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(VideoMediaEncoding, where)
+  if (record === undefined) throw Error(`Language not found`)
+  await db.remove<VideoMediaEncoding>(record)
+}
+
+// ========Entity property value updates========
+
+async function updateCategoryEntityPropertyValues(db: DB, where: IWhereCond, props: ICategory): Promise<void> {
+  const record = await db.get(Category, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<Category>(record)
+}
+async function updateChannelEntityPropertyValues(db: DB, where: IWhereCond, props: IChannel): Promise<void> {
+  const record = await db.get(Channel, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<Channel>(record)
+}
+async function updateVideoMediaEntityPropertyValues(db: DB, where: IWhereCond, props: IVideoMedia): Promise<void> {
+  const record = await db.get(VideoMedia, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<VideoMedia>(record)
+}
+async function updateVideoEntityPropertyValues(db: DB, where: IWhereCond, props: IVideo): Promise<void> {
+  const record = await db.get(Video, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<Video>(record)
+}
+async function updateUserDefinedLicenseEntityPropertyValues(
+  db: DB,
+  where: IWhereCond,
+  props: IUserDefinedLicense
+): Promise<void> {
+  const record = await db.get(UserDefinedLicense, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<UserDefinedLicense>(record)
+}
+async function updateKnownLicenseEntityPropertyValues(db: DB, where: IWhereCond, props: IKnownLicense): Promise<void> {
+  const record = await db.get(KnownLicense, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<KnownLicense>(record)
+}
+async function updateHttpMediaLocationEntityPropertyValues(
+  db: DB,
+  where: IWhereCond,
+  props: IHttpMediaLocation
+): Promise<void> {
+  const record = await db.get(HttpMediaLocation, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<HttpMediaLocation>(record)
+}
+async function updateJoystreamMediaLocationEntityPropertyValues(
+  db: DB,
+  where: IWhereCond,
+  props: IJoystreamMediaLocation
+): Promise<void> {
+  const record = await db.get(JoystreamMediaLocation, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<JoystreamMediaLocation>(record)
+}
+async function updateLanguageEntityPropertyValues(db: DB, where: IWhereCond, props: ILanguage): Promise<void> {
+  const record = await db.get(Language, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<Language>(record)
+}
+async function updateVideoMediaEncodingEntityPropertyValues(
+  db: DB,
+  where: IWhereCond,
+  props: IVideoMediaEncoding
+): Promise<void> {
+  const record = await db.get(VideoMediaEncoding, where)
+  if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
+  Object.assign(record, props)
+  await db.save<VideoMediaEncoding>(record)
+}
+
+async function updateEntityPropertyValues(
+  db: DB,
+  event: SubstrateEvent,
+  where: IWhereCond,
+  className: string
+): Promise<void> {
+  switch (className) {
+    case ContentDirectoryKnownClasses.CHANNEL:
+      updateChannelEntityPropertyValues(db, where, decode.setProperties<IChannel>(event, channelPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.CATEGORY:
+      await updateCategoryEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<ICategory>(event, CategoryPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.KNOWNLICENSE:
+      await updateKnownLicenseEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IKnownLicense>(event, knownLicensePropertyNamesWIthId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.USERDEFINEDLICENSE:
+      await updateUserDefinedLicenseEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IUserDefinedLicense>(event, userDefinedLicensePropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION:
+      await updateJoystreamMediaLocationEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IJoystreamMediaLocation>(event, joystreamMediaLocationPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.HTTPMEDIALOCATION:
+      await updateHttpMediaLocationEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IHttpMediaLocation>(event, httpMediaLocationPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIA:
+      await updateVideoMediaEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IVideoMedia>(event, videoPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.VIDEO:
+      await updateVideoEntityPropertyValues(db, where, decode.setProperties<IVideo>(event, videoPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.LANGUAGE:
+      await updateLanguageEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<ILanguage>(event, languagePropertyNamesWIthId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING:
+      await updateVideoMediaEncodingEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IVideoMediaEncoding>(event, videoMediaEncodingPropertyNamesWithId)
+      )
+      break
+
+    default:
+      throw new Error(`Unknown class name: ${className}`)
+  }
+}
+
+export {
+  createCategory,
+  createChannel,
+  createVideoMedia,
+  createVideo,
+  createUserDefinedLicense,
+  createKnownLicense,
+  createHttpMediaLocation,
+  createJoystreamMediaLocation,
+  createLanguage,
+  createVideoMediaEncoding,
+  removeCategory,
+  removeChannel,
+  removeVideoMedia,
+  removeVideo,
+  removeUserDefinedLicense,
+  removeKnownLicense,
+  removeHttpMediaLocation,
+  removeJoystreamMediaLocation,
+  removeLanguage,
+  removeVideoMediaEncoding,
+  createBlockOrGetFromDatabase,
+  batchCreateClassEntities,
+  getClassName,
+  updateCategoryEntityPropertyValues,
+  updateChannelEntityPropertyValues,
+  updateVideoMediaEntityPropertyValues,
+  updateVideoEntityPropertyValues,
+  updateUserDefinedLicenseEntityPropertyValues,
+  updateHttpMediaLocationEntityPropertyValues,
+  updateJoystreamMediaLocationEntityPropertyValues,
+  updateKnownLicenseEntityPropertyValues,
+  updateLanguageEntityPropertyValues,
+  updateVideoMediaEncodingEntityPropertyValues,
+  updateEntityPropertyValues,
+}

+ 323 - 0
query-node/src/content-directory/entity.ts

@@ -0,0 +1,323 @@
+import Debug from 'debug'
+import { DB, SubstrateEvent } from '../../generated/indexer'
+import { ClassEntity } from '../../generated/graphql-server/src/modules/class-entity/class-entity.model'
+
+import { decode } from './decode'
+import {
+  createCategory,
+  createChannel,
+  createVideoMedia,
+  createVideo,
+  createUserDefinedLicense,
+  createKnownLicense,
+  createHttpMediaLocation,
+  createJoystreamMediaLocation,
+  removeCategory,
+  removeChannel,
+  removeVideoMedia,
+  removeVideo,
+  removeUserDefinedLicense,
+  removeKnownLicense,
+  removeHttpMediaLocation,
+  removeJoystreamMediaLocation,
+  removeLanguage,
+  removeVideoMediaEncoding,
+  createLanguage,
+  createVideoMediaEncoding,
+  updateCategoryEntityPropertyValues,
+  updateChannelEntityPropertyValues,
+  updateVideoMediaEntityPropertyValues,
+  updateVideoEntityPropertyValues,
+  updateUserDefinedLicenseEntityPropertyValues,
+  updateHttpMediaLocationEntityPropertyValues,
+  updateJoystreamMediaLocationEntityPropertyValues,
+  updateKnownLicenseEntityPropertyValues,
+  updateLanguageEntityPropertyValues,
+  updateVideoMediaEncodingEntityPropertyValues,
+} from './entity-helper'
+import {
+  CategoryPropertyNamesWithId,
+  channelPropertyNamesWithId,
+  httpMediaLocationPropertyNamesWithId,
+  joystreamMediaLocationPropertyNamesWithId,
+  knownLicensePropertyNamesWIthId,
+  languagePropertyNamesWIthId,
+  userDefinedLicensePropertyNamesWithId,
+  videoMediaEncodingPropertyNamesWithId,
+  videoPropertyNamesWithId,
+  contentDirectoryClassNamesWithId,
+  ContentDirectoryKnownClasses,
+} from './content-dir-consts'
+
+import {
+  IChannel,
+  ICategory,
+  IKnownLicense,
+  IUserDefinedLicense,
+  IJoystreamMediaLocation,
+  IHttpMediaLocation,
+  IVideoMedia,
+  IVideo,
+  ILanguage,
+  IVideoMediaEncoding,
+  IDBBlockId,
+  IWhereCond,
+} from '../types'
+
+const debug = Debug('mappings:content-directory')
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+async function contentDirectory_EntitySchemaSupportAdded(db: DB, event: SubstrateEvent): Promise<void> {
+  if (event.extrinsic && event.extrinsic.method === 'transaction') return
+  debug(`EntitySchemaSupportAdded event: ${JSON.stringify(event)}`)
+
+  const { blockNumber: block } = event
+  const entityId = decode.stringIfyEntityId(event)
+  const classEntity = await db.get(ClassEntity, { where: { id: entityId } })
+
+  if (classEntity === undefined) throw Error(`Class not found for the EntityId: ${entityId}`)
+
+  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === classEntity.classId)
+  if (cls === undefined) throw Error('Not recognized class')
+
+  const arg: IDBBlockId = { db, block, id: entityId }
+
+  switch (cls.name) {
+    case ContentDirectoryKnownClasses.CHANNEL:
+      await createChannel(arg, decode.setProperties<IChannel>(event, channelPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.CATEGORY:
+      await createCategory(arg, decode.setProperties<ICategory>(event, CategoryPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.KNOWNLICENSE:
+      await createKnownLicense(arg, decode.setProperties<IKnownLicense>(event, knownLicensePropertyNamesWIthId))
+      break
+
+    case ContentDirectoryKnownClasses.USERDEFINEDLICENSE:
+      await createUserDefinedLicense(
+        arg,
+        decode.setProperties<IUserDefinedLicense>(event, userDefinedLicensePropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION:
+      await createJoystreamMediaLocation(
+        arg,
+        decode.setProperties<IJoystreamMediaLocation>(event, joystreamMediaLocationPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.HTTPMEDIALOCATION:
+      await createHttpMediaLocation(
+        arg,
+        decode.setProperties<IHttpMediaLocation>(event, httpMediaLocationPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIA:
+      await createVideoMedia(arg, decode.setProperties<IVideoMedia>(event, videoPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.VIDEO:
+      await createVideo(arg, decode.setProperties<IVideo>(event, videoPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.LANGUAGE:
+      await createLanguage(arg, decode.setProperties<ILanguage>(event, languagePropertyNamesWIthId))
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING:
+      await createVideoMediaEncoding(
+        arg,
+        decode.setProperties<IVideoMediaEncoding>(event, videoMediaEncodingPropertyNamesWithId)
+      )
+      break
+
+    default:
+      throw new Error(`Unknown class name: ${cls.name}`)
+  }
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+async function contentDirectory_EntityRemoved(db: DB, event: SubstrateEvent): Promise<void> {
+  debug(`EntityRemoved event: ${JSON.stringify(event)}`)
+
+  const entityId = decode.stringIfyEntityId(event)
+  const where: IWhereCond = { where: { id: entityId } }
+
+  const classEntity = await db.get(ClassEntity, where)
+  if (classEntity === undefined) throw Error(`Class not found for the EntityId: ${entityId}`)
+
+  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === classEntity.classId)
+  if (cls === undefined) throw Error('Undefined class')
+
+  switch (cls.name) {
+    case ContentDirectoryKnownClasses.CHANNEL:
+      await removeChannel(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.CATEGORY:
+      await removeCategory(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.KNOWNLICENSE:
+      await removeKnownLicense(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.USERDEFINEDLICENSE:
+      await removeUserDefinedLicense(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION:
+      await removeJoystreamMediaLocation(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.HTTPMEDIALOCATION:
+      await removeHttpMediaLocation(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIA:
+      await removeVideoMedia(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.VIDEO:
+      await removeVideo(db, where)
+      break
+    case ContentDirectoryKnownClasses.LANGUAGE:
+      await removeLanguage(db, where)
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING:
+      await removeVideoMediaEncoding(db, where)
+      break
+
+    default:
+      throw new Error(`Unknown class name: ${cls.name}`)
+  }
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+async function contentDirectory_EntityCreated(db: DB, event: SubstrateEvent): Promise<void> {
+  if (event.extrinsic && event.extrinsic.method === 'transaction') return
+  debug(`EntityCreated event: ${JSON.stringify(event)}`)
+
+  const c = decode.getClassEntity(event)
+  const classEntity = new ClassEntity()
+
+  classEntity.classId = c.classId
+  classEntity.id = c.entityId.toString()
+  classEntity.version = event.blockNumber
+  await db.save<ClassEntity>(classEntity)
+}
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+async function contentDirectory_EntityPropertyValuesUpdated(db: DB, event: SubstrateEvent): Promise<void> {
+  debug(`EntityPropertyValuesUpdated event: ${JSON.stringify(event)}`)
+
+  const { extrinsic } = event
+
+  if (extrinsic && extrinsic.method === 'transaction') return
+  if (extrinsic === undefined) throw Error(`Extrinsic data not found for event: ${event.id}`)
+
+  const { 2: newPropertyValues } = extrinsic.args
+  const entityId = decode.stringIfyEntityId(event)
+
+  const ce = await db.get(ClassEntity, { where: { id: entityId } })
+  if (ce === undefined) throw Error(`Class not found for the entity id: ${entityId}`)
+
+  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === ce.classId)
+  if (cls === undefined) throw Error(`Not known class id: ${ce.classId}`)
+
+  const where: IWhereCond = { where: { id: entityId } }
+
+  // TODO: change setProperties method signature to accecpt SubstrateExtrinsic, then remove the following
+  // line. The reason we push the same arg is beacuse of the setProperties method check the 3rd indices
+  // to get properties values
+  extrinsic.args.push(newPropertyValues)
+
+  switch (cls.name) {
+    case ContentDirectoryKnownClasses.CHANNEL:
+      updateChannelEntityPropertyValues(db, where, decode.setProperties<IChannel>(event, channelPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.CATEGORY:
+      await updateCategoryEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<ICategory>(event, CategoryPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.KNOWNLICENSE:
+      await updateKnownLicenseEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IKnownLicense>(event, knownLicensePropertyNamesWIthId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.USERDEFINEDLICENSE:
+      await updateUserDefinedLicenseEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IUserDefinedLicense>(event, userDefinedLicensePropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION:
+      await updateJoystreamMediaLocationEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IJoystreamMediaLocation>(event, joystreamMediaLocationPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.HTTPMEDIALOCATION:
+      await updateHttpMediaLocationEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IHttpMediaLocation>(event, httpMediaLocationPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIA:
+      await updateVideoMediaEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IVideoMedia>(event, videoPropertyNamesWithId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.VIDEO:
+      await updateVideoEntityPropertyValues(db, where, decode.setProperties<IVideo>(event, videoPropertyNamesWithId))
+      break
+
+    case ContentDirectoryKnownClasses.LANGUAGE:
+      await updateLanguageEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<ILanguage>(event, languagePropertyNamesWIthId)
+      )
+      break
+
+    case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING:
+      await updateVideoMediaEncodingEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IVideoMediaEncoding>(event, videoMediaEncodingPropertyNamesWithId)
+      )
+      break
+
+    default:
+      throw new Error(`Unknown class name: ${cls.name}`)
+  }
+}
+
+export {
+  contentDirectory_EntityCreated,
+  contentDirectory_EntityRemoved,
+  contentDirectory_EntitySchemaSupportAdded,
+  contentDirectory_EntityPropertyValuesUpdated,
+}

+ 6 - 0
query-node/src/content-directory/mapping.ts

@@ -0,0 +1,6 @@
+export {
+  contentDirectory_EntitySchemaSupportAdded,
+  contentDirectory_EntityRemoved,
+  contentDirectory_EntityCreated,
+} from './entity'
+export { contentDirectory_TransactionCompleted } from './transaction'

+ 285 - 0
query-node/src/content-directory/transaction.ts

@@ -0,0 +1,285 @@
+import Debug from 'debug'
+
+import { DB, SubstrateEvent } from '../../generated/indexer'
+import { decode } from './decode'
+import {
+  ICategory,
+  IChannel,
+  ICreateEntityOperation,
+  IDBBlockId,
+  IEntity,
+  IHttpMediaLocation,
+  IJoystreamMediaLocation,
+  IKnownLicense,
+  ILanguage,
+  IUserDefinedLicense,
+  IVideo,
+  IVideoMedia,
+  IVideoMediaEncoding,
+  IWhereCond,
+} from '../types'
+import {
+  CategoryPropertyNamesWithId,
+  channelPropertyNamesWithId,
+  knownLicensePropertyNamesWIthId,
+  userDefinedLicensePropertyNamesWithId,
+  joystreamMediaLocationPropertyNamesWithId,
+  httpMediaLocationPropertyNamesWithId,
+  videoMediaPropertyNamesWithId,
+  videoMediaEncodingPropertyNamesWithId,
+  videoPropertyNamesWithId,
+  languagePropertyNamesWIthId,
+  ContentDirectoryKnownClasses,
+} from './content-dir-consts'
+import {
+  createCategory,
+  createChannel,
+  createVideoMedia,
+  createVideo,
+  createUserDefinedLicense,
+  createKnownLicense,
+  createHttpMediaLocation,
+  createJoystreamMediaLocation,
+  createLanguage,
+  createVideoMediaEncoding,
+  getClassName,
+  updateCategoryEntityPropertyValues,
+  updateChannelEntityPropertyValues,
+  updateVideoMediaEntityPropertyValues,
+  updateVideoEntityPropertyValues,
+  updateUserDefinedLicenseEntityPropertyValues,
+  updateHttpMediaLocationEntityPropertyValues,
+  updateJoystreamMediaLocationEntityPropertyValues,
+  updateKnownLicenseEntityPropertyValues,
+  updateLanguageEntityPropertyValues,
+  updateVideoMediaEncodingEntityPropertyValues,
+  batchCreateClassEntities,
+} from './entity-helper'
+
+const debug = Debug('mappings:content-directory:TransactionCompleted')
+
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export async function contentDirectory_TransactionCompleted(db: DB, event: SubstrateEvent): Promise<void> {
+  debug(`Substrate event: ${JSON.stringify(event)}`)
+
+  const { extrinsic, blockNumber: block } = event
+  if (!extrinsic) {
+    throw Error(`No extrinsic found for the event: ${event.id}`)
+  }
+
+  const { 1: operations } = extrinsic.args
+  if (operations.name.toString() !== 'operations') {
+    throw Error(`Could not found 'operations' in the extrinsic.args[1]`)
+  }
+
+  const {
+    addSchemaSupportToEntityOperations,
+    createEntityOperations,
+    updatePropertyValuesOperations,
+  } = decode.getOperations(event)
+
+  // Create entities before adding schema support
+  // We need this to know which entity belongs to which class(we will need to know to update/create
+  // Channel, Video etc.). For example if there is
+  // a property update operation there is no class id
+  await batchCreateClassEntities(db, block, createEntityOperations)
+  await batchUpdatePropertyValue(db, createEntityOperations, updatePropertyValuesOperations)
+  await batchAddSchemaSupportToEntity(db, createEntityOperations, addSchemaSupportToEntityOperations, block)
+}
+
+/**
+ *
+ * @param db database connection
+ * @param createEntityOperations: Entity creations with in the same transaction
+ * @param entities List of entities that schema support is added for
+ * @param block block number
+ */
+async function batchAddSchemaSupportToEntity(
+  db: DB,
+  createEntityOperations: ICreateEntityOperation[],
+  entities: IEntity[],
+  block: number
+) {
+  // find the related entity ie. Channel, Video etc
+  for (const entity of entities) {
+    debug(`Entity: ${JSON.stringify(entity)}`)
+
+    const { entityId, indexOf, properties } = entity
+
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const id = entityId ? entityId.toString() : indexOf!.toString()
+
+    const className = await getClassName(db, entity, createEntityOperations)
+    if (className === undefined) continue
+
+    const arg: IDBBlockId = { db, block, id }
+
+    switch (className) {
+      case ContentDirectoryKnownClasses.CATEGORY:
+        await createCategory(arg, decode.setEntityPropertyValues<ICategory>(properties, CategoryPropertyNamesWithId))
+        break
+
+      case ContentDirectoryKnownClasses.CHANNEL:
+        await createChannel(arg, decode.setEntityPropertyValues<IChannel>(properties, channelPropertyNamesWithId))
+        break
+
+      case ContentDirectoryKnownClasses.KNOWNLICENSE:
+        await createKnownLicense(
+          arg,
+          decode.setEntityPropertyValues<IKnownLicense>(properties, knownLicensePropertyNamesWIthId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.USERDEFINEDLICENSE:
+        await createUserDefinedLicense(
+          arg,
+          decode.setEntityPropertyValues<IUserDefinedLicense>(properties, userDefinedLicensePropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION:
+        await createJoystreamMediaLocation(
+          arg,
+          decode.setEntityPropertyValues<IJoystreamMediaLocation>(properties, joystreamMediaLocationPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.HTTPMEDIALOCATION:
+        await createHttpMediaLocation(
+          arg,
+          decode.setEntityPropertyValues<IHttpMediaLocation>(properties, httpMediaLocationPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.VIDEOMEDIA:
+        await createVideoMedia(
+          arg,
+          decode.setEntityPropertyValues<IVideoMedia>(properties, videoMediaPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.VIDEO:
+        await createVideo(arg, decode.setEntityPropertyValues<IVideo>(properties, videoPropertyNamesWithId))
+        break
+
+      case ContentDirectoryKnownClasses.LANGUAGE:
+        await createLanguage(arg, decode.setEntityPropertyValues<ILanguage>(properties, languagePropertyNamesWIthId))
+        break
+
+      case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING:
+        await createVideoMediaEncoding(
+          arg,
+          decode.setEntityPropertyValues<IVideoMediaEncoding>(properties, videoMediaEncodingPropertyNamesWithId)
+        )
+        break
+
+      default:
+        throw new Error(`Unknown class name: ${className}`)
+    }
+  }
+}
+
+/**
+ * Batch update operations for entity properties values update
+ * @param db database connection
+ * @param createEntityOperations Entity creations with in the same transaction
+ * @param entities list of entities those properties values updated
+ */
+async function batchUpdatePropertyValue(db: DB, createEntityOperations: ICreateEntityOperation[], entities: IEntity[]) {
+  for (const entity of entities) {
+    debug(`Update entity properties values: ${JSON.stringify(entity)}`)
+
+    const { entityId, indexOf, properties } = entity
+    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+    const id = entityId ? entityId.toString() : indexOf!.toString()
+
+    const where: IWhereCond = { where: { id } }
+    const className = await getClassName(db, entity, createEntityOperations)
+    if (className === undefined) throw Error(`Can not update entity properties values. Unknown class name`)
+
+    switch (className) {
+      case ContentDirectoryKnownClasses.CHANNEL:
+        updateChannelEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IChannel>(properties, CategoryPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.CATEGORY:
+        await updateCategoryEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<ICategory>(properties, CategoryPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.KNOWNLICENSE:
+        await updateKnownLicenseEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IKnownLicense>(properties, knownLicensePropertyNamesWIthId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.USERDEFINEDLICENSE:
+        await updateUserDefinedLicenseEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IUserDefinedLicense>(properties, userDefinedLicensePropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION:
+        await updateJoystreamMediaLocationEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IJoystreamMediaLocation>(properties, joystreamMediaLocationPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.HTTPMEDIALOCATION:
+        await updateHttpMediaLocationEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IHttpMediaLocation>(properties, httpMediaLocationPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.VIDEOMEDIA:
+        await updateVideoMediaEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IVideoMedia>(properties, videoPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.VIDEO:
+        await updateVideoEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IVideo>(properties, videoPropertyNamesWithId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.LANGUAGE:
+        await updateLanguageEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<ILanguage>(properties, languagePropertyNamesWIthId)
+        )
+        break
+
+      case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING:
+        await updateVideoMediaEncodingEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IVideoMediaEncoding>(properties, videoMediaEncodingPropertyNamesWithId)
+        )
+        break
+
+      default:
+        throw new Error(`Unknown class name: ${className}`)
+    }
+  }
+}

+ 2 - 0
query-node/src/index.ts

@@ -0,0 +1,2 @@
+export * from './content-directory/mapping'
+// export * from "./membership/mapping"

+ 170 - 0
query-node/src/types.ts

@@ -0,0 +1,170 @@
+import * as BN from 'bn.js'
+import { EntityId, SchemaId, ParametrizedClassPropertyValue, ClassId } from '@joystream/types/content-directory'
+import { DB } from '../generated/indexer'
+
+export interface BaseJoystreamMember {
+  memberId: BN
+}
+
+export interface JoystreamMember extends BaseJoystreamMember {
+  handle: string
+  avatarUri: string
+  about: string
+  registeredAtBlock: number
+  rootAccount: Buffer
+  controllerAccount: Buffer
+}
+
+export interface MemberAboutText extends BaseJoystreamMember {
+  about: string
+}
+
+export interface MemberAvatarURI extends BaseJoystreamMember {
+  avatarUri: string
+}
+
+export interface MemberHandle extends BaseJoystreamMember {
+  handle: string
+}
+
+export interface MemberRootAccount extends BaseJoystreamMember {
+  rootAccount: Buffer
+}
+export interface MemberControllerAccount extends BaseJoystreamMember {
+  controllerAccount: Buffer
+}
+
+export interface IChannel {
+  title: string
+  description: string
+  coverPhotoURL: string
+  avatarPhotoURL: string
+  isPublic: boolean
+  isCurated: boolean
+  language: number
+}
+
+export interface ICategory {
+  name: string
+  description: string
+}
+
+export interface IKnownLicense {
+  code: string
+  name?: string
+  description?: string
+  url?: string
+}
+
+export interface IUserDefinedLicense {
+  content: string
+}
+
+export interface IJoystreamMediaLocation {
+  dataObjectId: string
+}
+
+export interface IHttpMediaLocation {
+  url: string
+  port?: number
+}
+
+export interface ILanguage {
+  name: string
+  code: string
+}
+
+export interface IVideoMediaEncoding {
+  name: string
+}
+
+export interface IVideoMedia {
+  encoding: number
+  pixelWidth: number
+  pixelHeight: number
+  size: number
+  location: number
+}
+
+export interface IVideo {
+  // referenced entity's id
+  channel: number
+  // referenced entity's id
+  category: number
+  title: string
+  description: string
+  duration: number
+  skippableIntroDuration?: number
+  thumbnailURL: string
+  language: number
+  // referenced entity's id
+  media: number
+  hasMarketing?: boolean
+  publishedBeforeJoystream?: number
+  isPublic: boolean
+  isCurated: boolean
+  isExplicit: boolean
+  license: number
+}
+
+export enum OperationType {
+  CreateEntity = 'CreateEntity',
+  AddSchemaSupportToEntity = 'AddSchemaSupportToEntity',
+  UpdatePropertyValues = 'UpdatePropertyValues',
+}
+
+export interface IAddSchemaSupportToEntity {
+  entity_id: EntityId
+  schema_id: SchemaId
+  parametrized_property_values: ParametrizedClassPropertyValue[]
+}
+
+export interface ICreateEntity {
+  class_id: ClassId
+}
+
+export interface IClassEntity {
+  entityId: number
+  classId: number
+}
+
+export interface IBatchOperation {
+  createEntityOperations: ICreateEntityOperation[]
+  addSchemaSupportToEntityOperations: IEntity[]
+  updatePropertyValuesOperations: IEntity[]
+}
+
+export interface IProperty {
+  [propertyId: string]: any
+  // propertyId: string;
+  // value: any;
+}
+
+export interface IEntity {
+  classId?: number
+  entityId?: number
+  // if entity is created in the same transaction, this is the entity id which is the index of the create
+  // entity operation
+  indexOf?: number
+  properties: IProperty[]
+}
+
+export interface IPropertyIdWithName {
+  // propertyId - property name
+  [propertyId: string]: string
+}
+
+export interface IWhereCond {
+  where: { id: string }
+}
+
+export interface ICreateEntityOperation {
+  classId: number
+}
+
+// An interface to use in function signature to simplify function parameters
+export interface IDBBlockId {
+  db: DB
+  block: number
+  id: string
+}

+ 22 - 0
query-node/tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "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,
+    "baseUrl": ".",
+    "paths": {
+      "@polkadot/types/augment": ["./node_modules/@joystream/types/augment-codec/augment-types.ts"]
+    }
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules"]
+}

+ 920 - 0
query-node/typedefs.json

@@ -0,0 +1,920 @@
+{
+  "Credential": "u64",
+  "CredentialSet": "BTreeSet<Credential>",
+  "BlockAndTime": {
+    "block": "u32",
+    "time": "u64"
+  },
+  "ThreadId": "u64",
+  "PostId": "u64",
+  "InputValidationLengthConstraint": {
+    "min": "u16",
+    "max_min_diff": "u16"
+  },
+  "WorkingGroup": {
+    "_enum": ["Storage", "Content"]
+  },
+  "SlashingTerms": {
+    "_enum": {
+      "Unslashable": "Null",
+      "Slashable": "SlashableTerms"
+    }
+  },
+  "SlashableTerms": {
+    "max_count": "u16",
+    "max_percent_pts_per_time": "u16"
+  },
+  "MemoText": "Text",
+  "Address": "AccountId",
+  "LookupSource": "AccountId",
+  "EntryMethod": {
+    "_enum": {
+      "Paid": "u64",
+      "Screening": "AccountId",
+      "Genesis": "Null"
+    }
+  },
+  "MemberId": "u64",
+  "PaidTermId": "u64",
+  "SubscriptionId": "u64",
+  "Membership": {
+    "handle": "Text",
+    "avatar_uri": "Text",
+    "about": "Text",
+    "registered_at_block": "u32",
+    "registered_at_time": "u64",
+    "entry": "EntryMethod",
+    "suspended": "bool",
+    "subscription": "Option<SubscriptionId>",
+    "root_account": "GenericAccountId",
+    "controller_account": "GenericAccountId"
+  },
+  "PaidMembershipTerms": {
+    "fee": "u128",
+    "text": "Text"
+  },
+  "ActorId": "u64",
+  "ElectionStage": {
+    "_enum": {
+      "Announcing": "u32",
+      "Voting": "u32",
+      "Revealing": "u32"
+    }
+  },
+  "ElectionStake": {
+    "new": "u128",
+    "transferred": "u128"
+  },
+  "SealedVote": {
+    "voter": "GenericAccountId",
+    "commitment": "Hash",
+    "stake": "ElectionStake",
+    "vote": "Option<GenericAccountId>"
+  },
+  "TransferableStake": {
+    "seat": "u128",
+    "backing": "u128"
+  },
+  "ElectionParameters": {
+    "announcing_period": "u32",
+    "voting_period": "u32",
+    "revealing_period": "u32",
+    "council_size": "u32",
+    "candidacy_limit": "u32",
+    "new_term_duration": "u32",
+    "min_council_stake": "u128",
+    "min_voting_stake": "u128"
+  },
+  "Seat": {
+    "member": "GenericAccountId",
+    "stake": "u128",
+    "backers": "Backers"
+  },
+  "Seats": "Vec<Seat>",
+  "Backer": {
+    "member": "GenericAccountId",
+    "stake": "u128"
+  },
+  "Backers": "Vec<Backer>",
+  "RoleParameters": {
+    "min_stake": "u128",
+    "min_actors": "u32",
+    "max_actors": "u32",
+    "reward": "u128",
+    "reward_period": "u32",
+    "bonding_period": "u32",
+    "unbonding_period": "u32",
+    "min_service_period": "u32",
+    "startup_grace_period": "u32",
+    "entry_request_fee": "u128"
+  },
+  "PostTextChange": {
+    "expired_at": "BlockAndTime",
+    "text": "Text"
+  },
+  "ModerationAction": {
+    "moderated_at": "BlockAndTime",
+    "moderator_id": "GenericAccountId",
+    "rationale": "Text"
+  },
+  "ChildPositionInParentCategory": {
+    "parent_id": "CategoryId",
+    "child_nr_in_parent_category": "u32"
+  },
+  "CategoryId": "u64",
+  "Category": {
+    "id": "CategoryId",
+    "title": "Text",
+    "description": "Text",
+    "created_at": "BlockAndTime",
+    "deleted": "bool",
+    "archived": "bool",
+    "num_direct_subcategories": "u32",
+    "num_direct_unmoderated_threads": "u32",
+    "num_direct_moderated_threads": "u32",
+    "position_in_parent_category": "Option<ChildPositionInParentCategory>",
+    "moderator_id": "GenericAccountId"
+  },
+  "Thread": {
+    "id": "ThreadId",
+    "title": "Text",
+    "category_id": "CategoryId",
+    "nr_in_category": "u32",
+    "moderation": "Option<ModerationAction>",
+    "num_unmoderated_posts": "u32",
+    "num_moderated_posts": "u32",
+    "created_at": "BlockAndTime",
+    "author_id": "GenericAccountId"
+  },
+  "Post": {
+    "id": "PostId",
+    "thread_id": "ThreadId",
+    "nr_in_thread": "u32",
+    "current_text": "Text",
+    "moderation": "Option<ModerationAction>",
+    "text_change_history": "Vec<PostTextChange>",
+    "created_at": "BlockAndTime",
+    "author_id": "GenericAccountId"
+  },
+  "ReplyId": "u64",
+  "Reply": {
+    "owner": "GenericAccountId",
+    "thread_id": "ThreadId",
+    "text": "Text",
+    "moderation": "Option<ModerationAction>"
+  },
+  "StakeId": "u64",
+  "Stake": {
+    "created": "u32",
+    "staking_status": "StakingStatus"
+  },
+  "StakingStatus": {
+    "_enum": {
+      "NotStaked": "Null",
+      "Staked": "Staked"
+    }
+  },
+  "Staked": {
+    "staked_amount": "u128",
+    "staked_status": "StakedStatus",
+    "next_slash_id": "u64",
+    "ongoing_slashes": "BTreeMap<u64,Slash>"
+  },
+  "StakedStatus": {
+    "_enum": {
+      "Normal": "Null",
+      "Unstaking": "Unstaking"
+    }
+  },
+  "Unstaking": {
+    "started_at_block": "u32",
+    "is_active": "bool",
+    "blocks_remaining_in_active_period_for_unstaking": "u32"
+  },
+  "Slash": {
+    "started_at_block": "u32",
+    "is_active": "bool",
+    "blocks_remaining_in_active_period_for_slashing": "u32",
+    "slash_amount": "u128"
+  },
+  "MintId": "u64",
+  "Mint": {
+    "capacity": "u128",
+    "next_adjustment": "Option<NextAdjustment>",
+    "created_at": "u32",
+    "total_minted": "u128"
+  },
+  "MintBalanceOf": "u128",
+  "BalanceOfMint": "u128",
+  "NextAdjustment": {
+    "adjustment": "AdjustOnInterval",
+    "at_block": "u32"
+  },
+  "AdjustOnInterval": {
+    "block_interval": "u32",
+    "adjustment_type": "AdjustCapacityBy"
+  },
+  "AdjustCapacityBy": {
+    "_enum": {
+      "Setting": "u128",
+      "Adding": "u128",
+      "Reducing": "u128"
+    }
+  },
+  "RecipientId": "u64",
+  "RewardRelationshipId": "u64",
+  "Recipient": {
+    "total_reward_received": "u128",
+    "total_reward_missed": "u128"
+  },
+  "RewardRelationship": {
+    "recipient": "RecipientId",
+    "mint_id": "MintId",
+    "account": "GenericAccountId",
+    "amount_per_payout": "u128",
+    "next_payment_at_block": "Option<u32>",
+    "payout_interval": "Option<u32>",
+    "total_reward_received": "u128",
+    "total_reward_missed": "u128"
+  },
+  "ApplicationId": "u64",
+  "OpeningId": "u64",
+  "Application": {
+    "opening_id": "OpeningId",
+    "application_index_in_opening": "u32",
+    "add_to_opening_in_block": "u32",
+    "active_role_staking_id": "Option<StakeId>",
+    "active_application_staking_id": "Option<StakeId>",
+    "stage": "ApplicationStage",
+    "human_readable_text": "Text"
+  },
+  "ApplicationStage": {
+    "_enum": {
+      "Active": "Null",
+      "Unstaking": "UnstakingApplicationStage",
+      "Inactive": "InactiveApplicationStage"
+    }
+  },
+  "ActivateOpeningAt": {
+    "_enum": {
+      "CurrentBlock": "Null",
+      "ExactBlock": "u32"
+    }
+  },
+  "ApplicationRationingPolicy": {
+    "max_active_applicants": "u32"
+  },
+  "OpeningStage": {
+    "_enum": {
+      "WaitingToBegin": "WaitingToBeingOpeningStageVariant",
+      "Active": "ActiveOpeningStageVariant"
+    }
+  },
+  "StakingPolicy": {
+    "amount": "u128",
+    "amount_mode": "StakingAmountLimitMode",
+    "crowded_out_unstaking_period_length": "Option<u32>",
+    "review_period_expired_unstaking_period_length": "Option<u32>"
+  },
+  "Opening": {
+    "created": "u32",
+    "stage": "OpeningStage",
+    "max_review_period_length": "u32",
+    "application_rationing_policy": "Option<ApplicationRationingPolicy>",
+    "application_staking_policy": "Option<StakingPolicy>",
+    "role_staking_policy": "Option<StakingPolicy>",
+    "human_readable_text": "Text"
+  },
+  "WaitingToBeingOpeningStageVariant": {
+    "begins_at_block": "u32"
+  },
+  "ActiveOpeningStageVariant": {
+    "stage": "ActiveOpeningStage",
+    "applications_added": "Vec<ApplicationId>",
+    "active_application_count": "u32",
+    "unstaking_application_count": "u32",
+    "deactivated_application_count": "u32"
+  },
+  "ActiveOpeningStage": {
+    "_enum": {
+      "AcceptingApplications": "AcceptingApplications",
+      "ReviewPeriod": "ReviewPeriod",
+      "Deactivated": "Deactivated"
+    }
+  },
+  "AcceptingApplications": {
+    "started_accepting_applicants_at_block": "u32"
+  },
+  "ReviewPeriod": {
+    "started_accepting_applicants_at_block": "u32",
+    "started_review_period_at_block": "u32"
+  },
+  "Deactivated": {
+    "cause": "OpeningDeactivationCause",
+    "deactivated_at_block": "u32",
+    "started_accepting_applicants_at_block": "u32",
+    "started_review_period_at_block": "Option<u32>"
+  },
+  "OpeningDeactivationCause": {
+    "_enum": [
+      "CancelledBeforeActivation",
+      "CancelledAcceptingApplications",
+      "CancelledInReviewPeriod",
+      "ReviewPeriodExpired",
+      "Filled"
+    ]
+  },
+  "InactiveApplicationStage": {
+    "deactivation_initiated": "u32",
+    "deactivated": "u32",
+    "cause": "ApplicationDeactivationCause"
+  },
+  "UnstakingApplicationStage": {
+    "deactivation_initiated": "u32",
+    "cause": "ApplicationDeactivationCause"
+  },
+  "ApplicationDeactivationCause": {
+    "_enum": ["External", "Hired", "NotHired", "CrowdedOut", "OpeningCancelled", "ReviewPeriodExpired", "OpeningFilled"]
+  },
+  "StakingAmountLimitMode": {
+    "_enum": ["AtLeast", "Exact"]
+  },
+  "ChannelId": "u64",
+  "CuratorId": "u64",
+  "CuratorOpeningId": "u64",
+  "CuratorApplicationId": "u64",
+  "LeadId": "u64",
+  "PrincipalId": "u64",
+  "OptionalText": "Option<Text>",
+  "Channel": {
+    "verified": "bool",
+    "handle": "Text",
+    "title": "OptionalText",
+    "description": "OptionalText",
+    "avatar": "OptionalText",
+    "banner": "OptionalText",
+    "content": "ChannelContentType",
+    "owner": "MemberId",
+    "role_account": "GenericAccountId",
+    "publication_status": "ChannelPublicationStatus",
+    "curation_status": "ChannelCurationStatus",
+    "created": "u32",
+    "principal_id": "PrincipalId"
+  },
+  "ChannelContentType": {
+    "_enum": ["Video", "Music", "Ebook"]
+  },
+  "ChannelCurationStatus": {
+    "_enum": ["Normal", "Censored"]
+  },
+  "ChannelPublicationStatus": {
+    "_enum": ["Public", "Unlisted"]
+  },
+  "CurationActor": {
+    "_enum": {
+      "Lead": "Null",
+      "Curator": "CuratorId"
+    }
+  },
+  "Curator": {
+    "role_account": "GenericAccountId",
+    "reward_relationship": "Option<RewardRelationshipId>",
+    "role_stake_profile": "Option<CuratorRoleStakeProfile>",
+    "stage": "CuratorRoleStage",
+    "induction": "CuratorInduction",
+    "principal_id": "PrincipalId"
+  },
+  "CuratorApplication": {
+    "role_account": "GenericAccountId",
+    "curator_opening_id": "CuratorOpeningId",
+    "member_id": "MemberId",
+    "application_id": "ApplicationId"
+  },
+  "CuratorOpening": {
+    "opening_id": "OpeningId",
+    "curator_applications": "Vec<CuratorApplicationId>",
+    "policy_commitment": "OpeningPolicyCommitment"
+  },
+  "Lead": {
+    "member_id": "MemberId",
+    "role_account": "GenericAccountId",
+    "reward_relationship": "Option<RewardRelationshipId>",
+    "inducted": "u32",
+    "stage": "LeadRoleState"
+  },
+  "OpeningPolicyCommitment": {
+    "application_rationing_policy": "Option<ApplicationRationingPolicy>",
+    "max_review_period_length": "u32",
+    "application_staking_policy": "Option<StakingPolicy>",
+    "role_staking_policy": "Option<StakingPolicy>",
+    "role_slashing_terms": "SlashingTerms",
+    "fill_opening_successful_applicant_application_stake_unstaking_period": "Option<u32>",
+    "fill_opening_failed_applicant_application_stake_unstaking_period": "Option<u32>",
+    "fill_opening_failed_applicant_role_stake_unstaking_period": "Option<u32>",
+    "terminate_curator_application_stake_unstaking_period": "Option<u32>",
+    "terminate_curator_role_stake_unstaking_period": "Option<u32>",
+    "exit_curator_role_application_stake_unstaking_period": "Option<u32>",
+    "exit_curator_role_stake_unstaking_period": "Option<u32>"
+  },
+  "Principal": {
+    "_enum": {
+      "Lead": "Null",
+      "Curator": "CuratorId",
+      "ChannelOwner": "ChannelId"
+    }
+  },
+  "WorkingGroupUnstaker": {
+    "_enum": {
+      "Lead": "LeadId",
+      "Curator": "CuratorId"
+    }
+  },
+  "CuratorApplicationIdToCuratorIdMap": "BTreeMap<ApplicationId,CuratorId>",
+  "CuratorApplicationIdSet": "BTreeSet<CuratorApplicationId>",
+  "CuratorRoleStakeProfile": {
+    "stake_id": "StakeId",
+    "termination_unstaking_period": "Option<u32>",
+    "exit_unstaking_period": "Option<u32>"
+  },
+  "CuratorRoleStage": {
+    "_enum": {
+      "Active": "Null",
+      "Unstaking": "CuratorExitSummary",
+      "Exited": "CuratorExitSummary"
+    }
+  },
+  "CuratorExitSummary": {
+    "origin": "CuratorExitInitiationOrigin",
+    "initiated_at_block_number": "u32",
+    "rationale_text": "Text"
+  },
+  "CuratorExitInitiationOrigin": {
+    "_enum": ["Lead", "Curator"]
+  },
+  "LeadRoleState": {
+    "_enum": {
+      "Active": "Null",
+      "Exited": "ExitedLeadRole"
+    }
+  },
+  "ExitedLeadRole": {
+    "initiated_at_block_number": "u32"
+  },
+  "CuratorInduction": {
+    "lead": "LeadId",
+    "curator_application_id": "CuratorApplicationId",
+    "at_block": "u32"
+  },
+  "RationaleText": "Bytes",
+  "ApplicationOf": {
+    "role_account_id": "GenericAccountId",
+    "opening_id": "OpeningId",
+    "member_id": "MemberId",
+    "application_id": "ApplicationId"
+  },
+  "ApplicationIdSet": "BTreeSet<ApplicationId>",
+  "ApplicationIdToWorkerIdMap": "BTreeMap<ApplicationId,WorkerId>",
+  "WorkerId": "u64",
+  "WorkerOf": {
+    "member_id": "MemberId",
+    "role_account_id": "GenericAccountId",
+    "reward_relationship": "Option<RewardRelationshipId>",
+    "role_stake_profile": "Option<RoleStakeProfile>"
+  },
+  "OpeningOf": {
+    "hiring_opening_id": "OpeningId",
+    "applications": "Vec<ApplicationId>",
+    "policy_commitment": "WorkingGroupOpeningPolicyCommitment",
+    "opening_type": "OpeningType"
+  },
+  "StorageProviderId": "u64",
+  "OpeningType": {
+    "_enum": {
+      "Leader": "Null",
+      "Worker": "Null"
+    }
+  },
+  "HiringApplicationId": "u64",
+  "RewardPolicy": {
+    "amount_per_payout": "u128",
+    "next_payment_at_block": "u32",
+    "payout_interval": "Option<u32>"
+  },
+  "WorkingGroupOpeningPolicyCommitment": {
+    "application_rationing_policy": "Option<ApplicationRationingPolicy>",
+    "max_review_period_length": "u32",
+    "application_staking_policy": "Option<StakingPolicy>",
+    "role_staking_policy": "Option<StakingPolicy>",
+    "role_slashing_terms": "SlashingTerms",
+    "fill_opening_successful_applicant_application_stake_unstaking_period": "Option<u32>",
+    "fill_opening_failed_applicant_application_stake_unstaking_period": "Option<u32>",
+    "fill_opening_failed_applicant_role_stake_unstaking_period": "Option<u32>",
+    "terminate_application_stake_unstaking_period": "Option<u32>",
+    "terminate_role_stake_unstaking_period": "Option<u32>",
+    "exit_role_application_stake_unstaking_period": "Option<u32>",
+    "exit_role_stake_unstaking_period": "Option<u32>"
+  },
+  "RoleStakeProfile": {
+    "stake_id": "StakeId",
+    "termination_unstaking_period": "Option<u32>",
+    "exit_unstaking_period": "Option<u32>"
+  },
+  "Url": "Text",
+  "IPNSIdentity": "Text",
+  "ServiceProviderRecord": {
+    "identity": "IPNSIdentity",
+    "expires_at": "u32"
+  },
+  "ContentId": "[u8;32]",
+  "LiaisonJudgement": {
+    "_enum": ["Pending", "Accepted", "Rejected"]
+  },
+  "DataObject": {
+    "owner": "MemberId",
+    "added_at": "BlockAndTime",
+    "type_id": "DataObjectTypeId",
+    "size": "u64",
+    "liaison": "StorageProviderId",
+    "liaison_judgement": "LiaisonJudgement",
+    "ipfs_content_id": "Text"
+  },
+  "DataObjectStorageRelationshipId": "u64",
+  "DataObjectStorageRelationship": {
+    "content_id": "ContentId",
+    "storage_provider": "StorageProviderId",
+    "ready": "bool"
+  },
+  "DataObjectTypeId": "u64",
+  "DataObjectType": {
+    "description": "Text",
+    "active": "bool"
+  },
+  "DataObjectsMap": "BTreeMap<ContentId,DataObject>",
+  "ProposalId": "u32",
+  "ProposalStatus": {
+    "_enum": {
+      "Active": "Option<ActiveStake>",
+      "Finalized": "Finalized"
+    }
+  },
+  "ProposalOf": {
+    "parameters": "ProposalParameters",
+    "proposerId": "MemberId",
+    "title": "Text",
+    "description": "Text",
+    "createdAt": "u32",
+    "status": "ProposalStatus",
+    "votingResults": "VotingResults"
+  },
+  "ProposalDetails": {
+    "_enum": {
+      "Text": "Text",
+      "RuntimeUpgrade": "Bytes",
+      "SetElectionParameters": "ElectionParameters",
+      "Spending": "(Balance,AccountId)",
+      "SetLead": "Option<SetLeadParams>",
+      "SetContentWorkingGroupMintCapacity": "u128",
+      "EvictStorageProvider": "GenericAccountId",
+      "SetValidatorCount": "u32",
+      "SetStorageRoleParameters": "RoleParameters",
+      "AddWorkingGroupLeaderOpening": "AddOpeningParameters",
+      "BeginReviewWorkingGroupLeaderApplication": "(OpeningId,WorkingGroup)",
+      "FillWorkingGroupLeaderOpening": "FillOpeningParameters",
+      "SetWorkingGroupMintCapacity": "(Balance,WorkingGroup)",
+      "DecreaseWorkingGroupLeaderStake": "(WorkerId,Balance,WorkingGroup)",
+      "SlashWorkingGroupLeaderStake": "(WorkerId,Balance,WorkingGroup)",
+      "SetWorkingGroupLeaderReward": "(WorkerId,Balance,WorkingGroup)",
+      "TerminateWorkingGroupLeaderRole": "TerminateRoleParameters"
+    }
+  },
+  "ProposalDetailsOf": {
+    "_enum": {
+      "Text": "Text",
+      "RuntimeUpgrade": "Bytes",
+      "SetElectionParameters": "ElectionParameters",
+      "Spending": "(Balance,AccountId)",
+      "SetLead": "Option<SetLeadParams>",
+      "SetContentWorkingGroupMintCapacity": "u128",
+      "EvictStorageProvider": "GenericAccountId",
+      "SetValidatorCount": "u32",
+      "SetStorageRoleParameters": "RoleParameters",
+      "AddWorkingGroupLeaderOpening": "AddOpeningParameters",
+      "BeginReviewWorkingGroupLeaderApplication": "(OpeningId,WorkingGroup)",
+      "FillWorkingGroupLeaderOpening": "FillOpeningParameters",
+      "SetWorkingGroupMintCapacity": "(Balance,WorkingGroup)",
+      "DecreaseWorkingGroupLeaderStake": "(WorkerId,Balance,WorkingGroup)",
+      "SlashWorkingGroupLeaderStake": "(WorkerId,Balance,WorkingGroup)",
+      "SetWorkingGroupLeaderReward": "(WorkerId,Balance,WorkingGroup)",
+      "TerminateWorkingGroupLeaderRole": "TerminateRoleParameters"
+    }
+  },
+  "VotingResults": {
+    "abstensions": "u32",
+    "approvals": "u32",
+    "rejections": "u32",
+    "slashes": "u32"
+  },
+  "ProposalParameters": {
+    "votingPeriod": "u32",
+    "gracePeriod": "u32",
+    "approvalQuorumPercentage": "u32",
+    "approvalThresholdPercentage": "u32",
+    "slashingQuorumPercentage": "u32",
+    "slashingThresholdPercentage": "u32",
+    "requiredStake": "Option<u128>"
+  },
+  "VoteKind": {
+    "_enum": ["Approve", "Reject", "Slash", "Abstain"]
+  },
+  "ThreadCounter": {
+    "author_id": "MemberId",
+    "counter": "u32"
+  },
+  "DiscussionThread": {
+    "title": "Bytes",
+    "created_at": "u32",
+    "author_id": "MemberId"
+  },
+  "DiscussionPost": {
+    "text": "Bytes",
+    "created_at": "u32",
+    "updated_at": "u32",
+    "author_id": "MemberId",
+    "thread_id": "ThreadId",
+    "edition_number": "u32"
+  },
+  "AddOpeningParameters": {
+    "activate_at": "ActivateOpeningAt",
+    "commitment": "WorkingGroupOpeningPolicyCommitment",
+    "human_readable_text": "Bytes",
+    "working_group": "WorkingGroup"
+  },
+  "FillOpeningParameters": {
+    "opening_id": "OpeningId",
+    "successful_application_id": "ApplicationId",
+    "reward_policy": "Option<RewardPolicy>",
+    "working_group": "WorkingGroup"
+  },
+  "TerminateRoleParameters": {
+    "worker_id": "WorkerId",
+    "rationale": "Bytes",
+    "slash": "bool",
+    "working_group": "WorkingGroup"
+  },
+  "ActiveStake": {
+    "stake_id": "StakeId",
+    "source_account_id": "GenericAccountId"
+  },
+  "Finalized": {
+    "proposalStatus": "ProposalDecisionStatus",
+    "finalizedAt": "u32",
+    "encodedUnstakingErrorDueToBrokenRuntime": "Option<Vec<u8>>",
+    "stakeDataAfterUnstakingError": "Option<ActiveStake>"
+  },
+  "ProposalDecisionStatus": {
+    "_enum": {
+      "Canceled": "Null",
+      "Vetoed": "Null",
+      "Rejected": "Null",
+      "Slashed": "Null",
+      "Expired": "Null",
+      "Approved": "Approved"
+    }
+  },
+  "ExecutionFailed": {
+    "error": "Text"
+  },
+  "Approved": {
+    "_enum": {
+      "PendingExecution": "Null",
+      "Executed": "Null",
+      "ExecutionFailed": "ExecutionFailed"
+    }
+  },
+  "SetLeadParams": "(MemberId,GenericAccountId)",
+  "Nonce": "u64",
+  "EntityId": "u64",
+  "ClassId": "u64",
+  "CuratorGroupId": "u64",
+  "VecMaxLength": "u16",
+  "TextMaxLength": "u16",
+  "HashedTextMaxLength": "Option<u16>",
+  "PropertyId": "u16",
+  "SchemaId": "u16",
+  "SameController": "bool",
+  "ClassPermissions": {
+    "any_member": "bool",
+    "entity_creation_blocked": "bool",
+    "all_entity_property_values_locked": "bool",
+    "maintainers": "Vec<CuratorGroupId>"
+  },
+  "PropertyTypeSingle": {
+    "_enum": {
+      "Bool": "Null",
+      "Uint16": "Null",
+      "Uint32": "Null",
+      "Uint64": "Null",
+      "Int16": "Null",
+      "Int32": "Null",
+      "Int64": "Null",
+      "Text": "TextMaxLength",
+      "Hash": "HashedTextMaxLength",
+      "Reference": "(ClassId,SameController)"
+    }
+  },
+  "PropertyTypeVector": {
+    "vec_type": "PropertyTypeSingle",
+    "max_length": "VecMaxLength"
+  },
+  "PropertyType": {
+    "_enum": {
+      "Single": "PropertyTypeSingle",
+      "Vector": "PropertyTypeVector"
+    }
+  },
+  "PropertyLockingPolicy": {
+    "is_locked_from_maintainer": "bool",
+    "is_locked_from_controller": "bool"
+  },
+  "Property": {
+    "property_type": "PropertyType",
+    "required": "bool",
+    "unique": "bool",
+    "name": "Text",
+    "description": "Text",
+    "locking_policy": "PropertyLockingPolicy"
+  },
+  "Schema": {
+    "properties": "Vec<PropertyId>",
+    "is_active": "bool"
+  },
+  "Class": {
+    "class_permissions": "ClassPermissions",
+    "properties": "Vec<Property>",
+    "schemas": "Vec<Schema>",
+    "name": "Text",
+    "description": "Text",
+    "maximum_entities_count": "EntityId",
+    "current_number_of_entities": "EntityId",
+    "default_entity_creation_voucher_upper_bound": "EntityId"
+  },
+  "EntityController": {
+    "_enum": {
+      "Maintainers": "Null",
+      "Member": "MemberId",
+      "Lead": "Null"
+    }
+  },
+  "EntityPermissions": {
+    "controller": "EntityController",
+    "frozen": "bool",
+    "referenceable": "bool"
+  },
+  "StoredValue": {
+    "_enum": {
+      "Bool": "bool",
+      "Uint16": "u16",
+      "Uint32": "u32",
+      "Uint64": "u64",
+      "Int16": "i16",
+      "Int32": "i32",
+      "Int64": "i64",
+      "Text": "Text",
+      "Hash": "Hash",
+      "Reference": "EntityId"
+    }
+  },
+  "VecStoredValue": {
+    "_enum": {
+      "Bool": "Vec<bool>",
+      "Uint16": "Vec<u16>",
+      "Uint32": "Vec<u32>",
+      "Uint64": "Vec<u64>",
+      "Int16": "Vec<i16>",
+      "Int32": "Vec<i32>",
+      "Int64": "Vec<i64>",
+      "Hash": "Vec<Hash>",
+      "Text": "Vec<Text>",
+      "Reference": "Vec<EntityId>"
+    }
+  },
+  "VecStoredPropertyValue": {
+    "vec_value": "VecStoredValue",
+    "nonce": "Nonce"
+  },
+  "StoredPropertyValue": {
+    "_enum": {
+      "Single": "StoredValue",
+      "Vector": "VecStoredPropertyValue"
+    }
+  },
+  "InboundReferenceCounter": {
+    "total": "u32",
+    "same_owner": "u32"
+  },
+  "Entity": {
+    "entity_permissions": "EntityPermissions",
+    "class_id": "ClassId",
+    "supported_schemas": "Vec<SchemaId>",
+    "values": "BTreeMap<PropertyId,StoredPropertyValue>",
+    "reference_counter": "InboundReferenceCounter"
+  },
+  "CuratorGroup": {
+    "curators": "Vec<u64>",
+    "active": "bool",
+    "number_of_classes_maintained": "u32"
+  },
+  "EntityCreationVoucher": {
+    "maximum_entities_count": "EntityId",
+    "entities_created": "EntityId"
+  },
+  "Actor": {
+    "_enum": {
+      "Curator": "(CuratorGroupId,u64)",
+      "Member": "MemberId",
+      "Lead": "Null"
+    }
+  },
+  "EntityReferenceCounterSideEffect": {
+    "total": "i32",
+    "same_owner": "i32"
+  },
+  "ReferenceCounterSideEffects": "BTreeMap<EntityId,EntityReferenceCounterSideEffect>",
+  "SideEffects": "Option<ReferenceCounterSideEffects>",
+  "SideEffect": "Option<(EntityId,EntityReferenceCounterSideEffect)>",
+  "Status": "bool",
+  "InputValue": {
+    "_enum": {
+      "Bool": "bool",
+      "Uint16": "u16",
+      "Uint32": "u32",
+      "Uint64": "u64",
+      "Int16": "i16",
+      "Int32": "i32",
+      "Int64": "i64",
+      "Text": "Text",
+      "TextToHash": "Text",
+      "Reference": "EntityId"
+    }
+  },
+  "VecInputValue": {
+    "_enum": {
+      "Bool": "Vec<bool>",
+      "Uint16": "Vec<u16>",
+      "Uint32": "Vec<u32>",
+      "Uint64": "Vec<u64>",
+      "Int16": "Vec<i16>",
+      "Int32": "Vec<i32>",
+      "Int64": "Vec<i64>",
+      "TextToHash": "Vec<Text>",
+      "Text": "Vec<Text>",
+      "Reference": "Vec<EntityId>"
+    }
+  },
+  "InputPropertyValue": {
+    "_enum": {
+      "Single": "InputValue",
+      "Vector": "VecInputValue"
+    }
+  },
+  "ParameterizedEntity": {
+    "_enum": {
+      "InternalEntityJustAdded": "u32",
+      "ExistingEntity": "EntityId"
+    }
+  },
+  "ParametrizedPropertyValue": {
+    "_enum": {
+      "InputPropertyValue": "InputPropertyValue",
+      "InternalEntityJustAdded": "u32",
+      "InternalEntityVec": "Vec<ParameterizedEntity>"
+    }
+  },
+  "ParametrizedClassPropertyValue": {
+    "in_class_index": "PropertyId",
+    "value": "ParametrizedPropertyValue"
+  },
+  "CreateEntityOperation": {
+    "class_id": "ClassId"
+  },
+  "UpdatePropertyValuesOperation": {
+    "entity_id": "ParameterizedEntity",
+    "new_parametrized_property_values": "Vec<ParametrizedClassPropertyValue>"
+  },
+  "AddSchemaSupportToEntityOperation": {
+    "entity_id": "ParameterizedEntity",
+    "schema_id": "SchemaId",
+    "parametrized_property_values": "Vec<ParametrizedClassPropertyValue>"
+  },
+  "OperationType": {
+    "_enum": {
+      "CreateEntity": "CreateEntityOperation",
+      "UpdatePropertyValues": "UpdatePropertyValuesOperation",
+      "AddSchemaSupportToEntity": "AddSchemaSupportToEntityOperation"
+    }
+  },
+  "ClassPermissionsType": "Null",
+  "ClassPropertyValue": "Null",
+  "Operation": "Null",
+  "ReferenceConstraint": "Null"
+}