Browse Source

Merge branch 'babylon' into ops/sudo-init-working-group-leads

Mokhtar Naamani 4 years ago
parent
commit
e5571b2f16
100 changed files with 1344 additions and 1784 deletions
  1. 9 16
      .env
  2. 1 1
      .github/workflows/content-directory-schemas.yml
  3. 1 1
      .github/workflows/joystream-node-docker.yml
  4. 4 4
      .github/workflows/run-network-tests.yml
  5. 2 2
      Cargo.lock
  6. 3 3
      build.sh
  7. 8 1
      cli/README.md
  8. 1 1
      cli/package.json
  9. 0 3
      cli/src/@types/@ffmpeg-installer/ffmpeg/index.d.ts
  10. 3 0
      cli/src/@types/@ffprobe-installer/ffprobe/index.d.ts
  11. 5 2
      cli/src/base/AccountsCommandBase.ts
  12. 29 9
      cli/src/base/ContentDirectoryCommandBase.ts
  13. 10 5
      cli/src/base/MediaCommandBase.ts
  14. 19 4
      cli/src/commands/account/choose.ts
  15. 3 3
      cli/src/commands/content-directory/addClassSchema.ts
  16. 3 3
      cli/src/commands/content-directory/createClass.ts
  17. 2 2
      cli/src/commands/content-directory/entity.ts
  18. 1 1
      cli/src/commands/content-directory/initialize.ts
  19. 2 2
      cli/src/commands/content-directory/updateClassPermissions.ts
  20. 12 7
      cli/src/commands/media/createChannel.ts
  21. 3 3
      cli/src/commands/media/curateContent.ts
  22. 1 1
      cli/src/commands/media/featuredVideos.ts
  23. 2 2
      cli/src/commands/media/myChannels.ts
  24. 1 1
      cli/src/commands/media/myVideos.ts
  25. 3 3
      cli/src/commands/media/removeChannel.ts
  26. 1 1
      cli/src/commands/media/removeVideo.ts
  27. 3 3
      cli/src/commands/media/setFeaturedVideos.ts
  28. 7 6
      cli/src/commands/media/updateChannel.ts
  29. 4 4
      cli/src/commands/media/updateVideo.ts
  30. 2 2
      cli/src/commands/media/updateVideoLicense.ts
  31. 131 73
      cli/src/commands/media/uploadVideo.ts
  32. 11 7
      cli/src/helpers/InputOutput.ts
  33. 40 35
      cli/src/helpers/JsonSchemaPrompt.ts
  34. 20 20
      content-directory-schemas/README.md
  35. 5 5
      content-directory-schemas/examples/createChannel.ts
  36. 6 6
      content-directory-schemas/examples/createChannelWithoutTransaction.ts
  37. 6 6
      content-directory-schemas/examples/createVideo.ts
  38. 5 5
      content-directory-schemas/examples/updateChannelTitle.ts
  39. 6 6
      content-directory-schemas/examples/updateChannelTitleWithoutTransaction.ts
  40. 2 2
      content-directory-schemas/inputs/classes/ChannelClass.json
  41. 2 2
      content-directory-schemas/inputs/classes/ContentCategoryClass.json
  42. 2 2
      content-directory-schemas/inputs/classes/FeaturedVideoClass.json
  43. 2 2
      content-directory-schemas/inputs/classes/HttpMediaLocationClass.json
  44. 2 2
      content-directory-schemas/inputs/classes/JoystreamMediaLocationClass.json
  45. 2 2
      content-directory-schemas/inputs/classes/KnownLicenseClass.json
  46. 2 2
      content-directory-schemas/inputs/classes/LanguageClass.json
  47. 2 2
      content-directory-schemas/inputs/classes/LicenseClass.json
  48. 2 2
      content-directory-schemas/inputs/classes/MediaLocationClass.json
  49. 2 2
      content-directory-schemas/inputs/classes/UserDefinedLicenseClass.json
  50. 2 2
      content-directory-schemas/inputs/classes/VideoClass.json
  51. 2 2
      content-directory-schemas/inputs/classes/VideoMediaClass.json
  52. 0 13
      content-directory-schemas/inputs/entityBatches/ChannelBatch.json
  53. 56 6
      content-directory-schemas/inputs/entityBatches/KnownLicenseBatch.json
  54. 0 63
      content-directory-schemas/inputs/entityBatches/VideoBatch.json
  55. 5 6
      content-directory-schemas/inputs/schemas/ChannelSchema.json
  56. 9 0
      content-directory-schemas/inputs/schemas/KnownLicenseSchema.json
  57. 6 0
      content-directory-schemas/inputs/schemas/LicenseSchema.json
  58. 2 3
      content-directory-schemas/inputs/schemas/VideoSchema.json
  59. 3 3
      content-directory-schemas/package.json
  60. 1 1
      content-directory-schemas/src/helpers/InputParser.ts
  61. 25 15
      docker-compose.yml
  62. 1 1
      node/Cargo.toml
  63. 2 2
      package.json
  64. 0 62
      query-node/.env
  65. 5 1
      query-node/README.md
  66. 19 1
      query-node/build.sh
  67. 13 0
      query-node/db-migrate.sh
  68. 14 7
      query-node/mappings/content-directory/content-dir-consts.ts
  69. 13 4
      query-node/mappings/content-directory/decode.ts
  70. 105 46
      query-node/mappings/content-directory/entity/create.ts
  71. 41 37
      query-node/mappings/content-directory/entity/index.ts
  72. 77 46
      query-node/mappings/content-directory/entity/remove.ts
  73. 105 33
      query-node/mappings/content-directory/entity/update.ts
  74. 82 38
      query-node/mappings/content-directory/get-or-create.ts
  75. 1 1
      query-node/mappings/content-directory/mapping.ts
  76. 42 18
      query-node/mappings/content-directory/transaction.ts
  77. 16 6
      query-node/mappings/types.ts
  78. 6 7
      query-node/package.json
  79. 15 0
      query-node/processor-start.sh
  80. 21 10
      query-node/run-tests.sh
  81. 80 32
      query-node/schema.graphql
  82. 0 974
      query-node/typedefs.json
  83. 110 45
      runtime-modules/content-directory/src/lib.rs
  84. 2 1
      runtime-modules/content-directory/src/mock.rs
  85. 1 1
      runtime-modules/content-directory/src/tests.rs
  86. 1 1
      runtime-modules/content-directory/src/tests/add_class_schema.rs
  87. 1 1
      runtime-modules/content-directory/src/tests/add_curator_group.rs
  88. 1 1
      runtime-modules/content-directory/src/tests/add_curator_to_group.rs
  89. 1 1
      runtime-modules/content-directory/src/tests/add_maintainer_to_class.rs
  90. 1 1
      runtime-modules/content-directory/src/tests/clear_entity_property_vector.rs
  91. 1 1
      runtime-modules/content-directory/src/tests/create_class.rs
  92. 1 1
      runtime-modules/content-directory/src/tests/create_entity.rs
  93. 1 1
      runtime-modules/content-directory/src/tests/insert_at_entity_property_vector.rs
  94. 1 1
      runtime-modules/content-directory/src/tests/remove_at_entity_property_vector.rs
  95. 1 1
      runtime-modules/content-directory/src/tests/remove_curator_from_group.rs
  96. 1 1
      runtime-modules/content-directory/src/tests/remove_curator_group.rs
  97. 1 1
      runtime-modules/content-directory/src/tests/remove_entity.rs
  98. 1 1
      runtime-modules/content-directory/src/tests/remove_maintainer_from_class.rs
  99. 1 1
      runtime-modules/content-directory/src/tests/set_curator_group_status.rs
  100. 53 4
      runtime-modules/content-directory/src/tests/transaction.rs

+ 9 - 16
.env

@@ -1,17 +1,14 @@
 COMPOSE_PROJECT_NAME=joystream
 COMPOSE_PROJECT_NAME=joystream
+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
+# We will use a single postgres service with multiple databases
+INDEXER_DB_NAME=query_node_indexer
+PROCESSOR_DB_NAME=query_node_processor
 DB_USER=postgres
 DB_USER=postgres
 DB_PASS=postgres
 DB_PASS=postgres
 DB_HOST=localhost
 DB_HOST=localhost
 DB_PORT=5432
 DB_PORT=5432
+
 DEBUG=index-builder:*
 DEBUG=index-builder:*
 TYPEORM_LOGGING=error
 TYPEORM_LOGGING=error
 
 
@@ -20,16 +17,12 @@ TYPEORM_LOGGING=error
 ###########################
 ###########################
 
 
 # Substrate endpoint to source events from
 # Substrate endpoint to source events from
-WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944/
+WS_PROVIDER_ENDPOINT_URI=ws://localhost:9944/
+
 # Block height to start indexing from.
 # Block height to start indexing from.
 # Note, that if there are already some indexed events, this setting is ignored
 # Note, that if there are already some indexed events, this setting is ignored
 BLOCK_HEIGHT=0
 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 cache server
 REDIS_URI=redis://localhost:6379/0
 REDIS_URI=redis://localhost:6379/0
 
 
@@ -38,10 +31,10 @@ REDIS_URI=redis://localhost:6379/0
 ###########################
 ###########################
 
 
 # Where the mapping scripts are located, relative to ./generated/indexer
 # Where the mapping scripts are located, relative to ./generated/indexer
-TYPES_JSON=../../typedefs.json
+TYPES_JSON=../../../types/augment/all/defs.json
 
 
 # Indexer GraphQL API endpoint to fetch indexed events
 # Indexer GraphQL API endpoint to fetch indexed events
-INDEXER_ENDPOINT_URL=http://localhost:4100/graphql
+INDEXER_ENDPOINT_URL=http://localhost:4000/graphql
 
 
 # Block height from which the processor starts. Note that if
 # Block height from which the processor starts. Note that if
 # there are already processed events in the database, this setting is ignored
 # there are already processed events in the database, this setting is ignored

+ 1 - 1
.github/workflows/content-directory-schemas.yml

@@ -17,4 +17,4 @@ jobs:
     - name: validate
     - name: validate
       run: |
       run: |
         yarn install --frozen-lockfile
         yarn install --frozen-lockfile
-        yarn workspace cd-schemas checks --quiet
+        yarn workspace @joystream/cd-schemas checks --quiet

+ 1 - 1
.github/workflows/joystream-node-docker.yml

@@ -55,7 +55,7 @@ jobs:
             docker save --output joystream-node-docker-image.tar joystream/node
             docker save --output joystream-node-docker-image.tar joystream/node
             gzip joystream-node-docker-image.tar
             gzip joystream-node-docker-image.tar
             cp joystream-node-docker-image.tar.gz ~/docker-images/
             cp joystream-node-docker-image.tar.gz ~/docker-images/
-            echo "::set-env name=NEW_BUILD::true"
+            echo "NEW_BUILD=true" >> $GITHUB_ENV
           fi
           fi
 
 
       - name: Save joystream/node image to Artifacts
       - name: Save joystream/node image to Artifacts

+ 4 - 4
.github/workflows/run-network-tests.yml

@@ -77,7 +77,7 @@ jobs:
         with:
         with:
           name: ${{ steps.compute_shasum.outputs.shasum }}-joystream-node-docker-image.tar.gz
           name: ${{ steps.compute_shasum.outputs.shasum }}-joystream-node-docker-image.tar.gz
           path: joystream-node-docker-image.tar.gz
           path: joystream-node-docker-image.tar.gz
-  
+
   basic_runtime_with_upgrade:
   basic_runtime_with_upgrade:
     name: Integration Tests (Runtime Upgrade)
     name: Integration Tests (Runtime Upgrade)
     needs: build_images
     needs: build_images
@@ -146,11 +146,11 @@ jobs:
       - name: Install packages and dependencies
       - name: Install packages and dependencies
         run: yarn install --frozen-lockfile
         run: yarn install --frozen-lockfile
       - name: Ensure tests are runnable
       - name: Ensure tests are runnable
-        run: yarn workspace cd-schemas checks --quiet
+        run: yarn workspace @joystream/cd-schemas checks --quiet
       - name: Start chain
       - name: Start chain
         run: docker-compose up -d joystream-node
         run: docker-compose up -d joystream-node
       - name: Initialize the content directory
       - name: Initialize the content directory
-        run: yarn workspace cd-schemas initialize:dev
+        run: yarn workspace @joystream/cd-schemas initialize:dev
 
 
   query_node:
   query_node:
     name: Query Node Integration Tests
     name: Query Node Integration Tests
@@ -179,7 +179,7 @@ jobs:
       # integration tests
       # integration tests
       - name: Execute Tests
       - name: Execute Tests
         run: query-node/run-tests.sh
         run: query-node/run-tests.sh
-  
+
   storage_node:
   storage_node:
     name: Storage Node Tests
     name: Storage Node Tests
     needs: build_images
     needs: build_images

+ 2 - 2
Cargo.lock

@@ -1993,7 +1993,7 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "joystream-node"
 name = "joystream-node"
-version = "3.4.1"
+version = "3.5.0"
 dependencies = [
 dependencies = [
  "frame-benchmarking",
  "frame-benchmarking",
  "frame-benchmarking-cli",
  "frame-benchmarking-cli",
@@ -2053,7 +2053,7 @@ dependencies = [
 
 
 [[package]]
 [[package]]
 name = "joystream-node-runtime"
 name = "joystream-node-runtime"
-version = "7.7.0"
+version = "7.9.0"
 dependencies = [
 dependencies = [
  "frame-benchmarking",
  "frame-benchmarking",
  "frame-executive",
  "frame-executive",

+ 3 - 3
build.sh

@@ -4,8 +4,8 @@ set -e
 
 
 yarn
 yarn
 yarn workspace @joystream/types build
 yarn workspace @joystream/types build
-yarn workspace cd-schemas generate:all
-yarn workspace cd-schemas build
+yarn workspace @joystream/cd-schemas generate:all
+yarn workspace @joystream/cd-schemas build
 yarn workspace @joystream/cli build
 yarn workspace @joystream/cli build
 yarn workspace query-node-root build
 yarn workspace query-node-root build
 yarn workspace storage-node build
 yarn workspace storage-node build
@@ -52,4 +52,4 @@ do
 
 
    * )     break;;
    * )     break;;
   esac
   esac
-done
+done

+ 8 - 1
cli/README.md

@@ -136,7 +136,8 @@ USAGE
   $ joystream-cli account:choose
   $ joystream-cli account:choose
 
 
 OPTIONS
 OPTIONS
-  --showSpecial  Whether to show special (DEV chain) accounts
+  -S, --showSpecial      Whether to show special (DEV chain) accounts
+  -a, --address=address  Select account by address (if available)
 ```
 ```
 
 
 _See code: [src/commands/account/choose.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/choose.ts)_
 _See code: [src/commands/account/choose.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/account/choose.ts)_
@@ -628,6 +629,8 @@ OPTIONS
 
 
   -o, --output=output  Path to the directory where the output JSON file should be placed (the output file can be then
   -o, --output=output  Path to the directory where the output JSON file should be placed (the output file can be then
                        reused as input)
                        reused as input)
+
+  -y, --confirm        Confirm the provided input
 ```
 ```
 
 
 _See code: [src/commands/media/createChannel.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/createChannel.ts)_
 _See code: [src/commands/media/createChannel.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/createChannel.ts)_
@@ -796,6 +799,10 @@ ARGUMENTS
 OPTIONS
 OPTIONS
   -c, --channel=channel  ID of the channel to assign the video to (if omitted - one of the owned channels can be
   -c, --channel=channel  ID of the channel to assign the video to (if omitted - one of the owned channels can be
                          selected from the list)
                          selected from the list)
+
+  -i, --input=input      Path to JSON file to use as input (if not specified - the input can be provided interactively)
+
+  -y, --confirm          Confirm the provided input
 ```
 ```
 
 
 _See code: [src/commands/media/uploadVideo.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/uploadVideo.ts)_
 _See code: [src/commands/media/uploadVideo.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/uploadVideo.ts)_

+ 1 - 1
cli/package.json

@@ -9,7 +9,7 @@
   "bugs": "https://github.com/Joystream/joystream/issues",
   "bugs": "https://github.com/Joystream/joystream/issues",
   "dependencies": {
   "dependencies": {
     "@apidevtools/json-schema-ref-parser": "^9.0.6",
     "@apidevtools/json-schema-ref-parser": "^9.0.6",
-    "@ffmpeg-installer/ffmpeg": "^1.0.20",
+    "@ffprobe-installer/ffprobe": "^1.1.0",
     "@joystream/types": "^0.14.0",
     "@joystream/types": "^0.14.0",
     "@oclif/command": "^1.5.19",
     "@oclif/command": "^1.5.19",
     "@oclif/config": "^1.14.0",
     "@oclif/config": "^1.14.0",

+ 0 - 3
cli/src/@types/@ffmpeg-installer/ffmpeg/index.d.ts

@@ -1,3 +0,0 @@
-declare module '@ffmpeg-installer/ffmpeg' {
-  export const path: string
-}

+ 3 - 0
cli/src/@types/@ffprobe-installer/ffprobe/index.d.ts

@@ -0,0 +1,3 @@
+declare module '@ffprobe-installer/ffprobe' {
+  export const path: string
+}

+ 5 - 2
cli/src/base/AccountsCommandBase.ts

@@ -177,8 +177,11 @@ export default abstract class AccountsCommandBase extends ApiCommandBase {
     return password
     return password
   }
   }
 
 
-  async requireConfirmation(message = 'Are you sure you want to execute this action?'): Promise<void> {
-    const { confirmed } = await inquirer.prompt([{ type: 'confirm', name: 'confirmed', message, default: false }])
+  async requireConfirmation(
+    message = 'Are you sure you want to execute this action?',
+    defaultVal = false
+  ): Promise<void> {
+    const { confirmed } = await inquirer.prompt([{ type: 'confirm', name: 'confirmed', message, default: defaultVal }])
     if (!confirmed) this.exit(ExitCodes.OK)
     if (!confirmed) this.exit(ExitCodes.OK)
   }
   }
 
 

+ 29 - 9
cli/src/base/ContentDirectoryCommandBase.ts

@@ -1,7 +1,7 @@
 import ExitCodes from '../ExitCodes'
 import ExitCodes from '../ExitCodes'
 import { WorkingGroups } from '../Types'
 import { WorkingGroups } from '../Types'
-import { ReferenceProperty } from 'cd-schemas/types/extrinsics/AddClassSchema'
-import { FlattenRelations } from 'cd-schemas/types/utility'
+import { ReferenceProperty } from '@joystream/cd-schemas/types/extrinsics/AddClassSchema'
+import { FlattenRelations } from '@joystream/cd-schemas/types/utility'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
 import {
 import {
   Class,
   Class,
@@ -11,10 +11,13 @@ import {
   Entity,
   Entity,
   EntityId,
   EntityId,
   Actor,
   Actor,
+  PropertyType,
 } from '@joystream/types/content-directory'
 } from '@joystream/types/content-directory'
 import { Worker } from '@joystream/types/working-group'
 import { Worker } from '@joystream/types/working-group'
 import { CLIError } from '@oclif/errors'
 import { CLIError } from '@oclif/errors'
 import { Codec } from '@polkadot/types/types'
 import { Codec } from '@polkadot/types/types'
+import AbstractInt from '@polkadot/types/codec/AbstractInt'
+import { AnyJson } from '@polkadot/types/types/helpers'
 import _ from 'lodash'
 import _ from 'lodash'
 import { RolesCommandBase } from './WorkingGroupsCommandBase'
 import { RolesCommandBase } from './WorkingGroupsCommandBase'
 import { createType } from '@joystream/types'
 import { createType } from '@joystream/types'
@@ -24,6 +27,8 @@ import { flags } from '@oclif/command'
 const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
 const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
 type Context = typeof CONTEXTS[number]
 type Context = typeof CONTEXTS[number]
 
 
+type ParsedPropertyValue = { value: Codec | null; type: PropertyType['type']; subtype: PropertyType['subtype'] }
+
 /**
 /**
  * Abstract base class for commands related to content directory
  * Abstract base class for commands related to content directory
  */
  */
@@ -278,7 +283,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
       choices: entityEntries.map(([id, entity]) => {
       choices: entityEntries.map(([id, entity]) => {
         const parsedEntityPropertyValues = this.parseEntityPropertyValues(entity, entityClass)
         const parsedEntityPropertyValues = this.parseEntityPropertyValues(entity, entityClass)
         return {
         return {
-          name: (propName && parsedEntityPropertyValues[propName]?.value.toString()) || `ID:${id.toString()}`,
+          name: (propName && parsedEntityPropertyValues[propName]?.value?.toString()) || `ID:${id.toString()}`,
           value: id.toString(), // With numbers there are issues with "default"
           value: id.toString(), // With numbers there are issues with "default"
         }
         }
       }),
       }),
@@ -298,31 +303,46 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     return (await this.promptForEntityEntry(message, className, propName, ownerMemberId, defaultId))[0].toNumber()
     return (await this.promptForEntityEntry(message, className, propName, ownerMemberId, defaultId))[0].toNumber()
   }
   }
 
 
+  parseStoredPropertyInnerValue(value: Codec | null): AnyJson {
+    if (value === null) {
+      return null
+    }
+
+    if (value instanceof AbstractInt) {
+      return value.toNumber() // Integers (signed ones) are by default converted to hex when using .toJson()
+    }
+
+    return value.toJSON()
+  }
+
   parseEntityPropertyValues(
   parseEntityPropertyValues(
     entity: Entity,
     entity: Entity,
     entityClass: Class,
     entityClass: Class,
     includedProperties?: string[]
     includedProperties?: string[]
-  ): Record<string, { value: Codec; type: string }> {
+  ): Record<string, ParsedPropertyValue> {
     const { properties } = entityClass
     const { properties } = entityClass
     return Array.from(entity.getField('values').entries()).reduce((columns, [propId, propValue]) => {
     return Array.from(entity.getField('values').entries()).reduce((columns, [propId, propValue]) => {
       const prop = properties[propId.toNumber()]
       const prop = properties[propId.toNumber()]
       const propName = prop.name.toString()
       const propName = prop.name.toString()
       const included = !includedProperties || includedProperties.some((p) => p.toLowerCase() === propName.toLowerCase())
       const included = !includedProperties || includedProperties.some((p) => p.toLowerCase() === propName.toLowerCase())
+      const { type: propType, subtype: propSubtype } = prop.property_type
 
 
       if (included) {
       if (included) {
         columns[propName] = {
         columns[propName] = {
-          value: propValue.getValue(),
-          type: `${prop.property_type.type}<${prop.property_type.subtype}>`,
+          // If type doesn't match (Boolean(false) for optional fields case) - use "null" as value
+          value: propType !== propValue.type || propSubtype !== propValue.subtype ? null : propValue.getValue(),
+          type: propType,
+          subtype: propSubtype,
         }
         }
       }
       }
       return columns
       return columns
-    }, {} as Record<string, { value: Codec; type: string }>)
+    }, {} as Record<string, ParsedPropertyValue>)
   }
   }
 
 
   async parseToKnownEntityJson<T>(entity: Entity): Promise<FlattenRelations<T>> {
   async parseToKnownEntityJson<T>(entity: Entity): Promise<FlattenRelations<T>> {
     const entityClass = (await this.classEntryByNameOrId(entity.class_id.toString()))[1]
     const entityClass = (await this.classEntryByNameOrId(entity.class_id.toString()))[1]
     return (_.mapValues(this.parseEntityPropertyValues(entity, entityClass), (v) =>
     return (_.mapValues(this.parseEntityPropertyValues(entity, entityClass), (v) =>
-      v.type !== 'Single<Bool>' && v.value.toJSON() === false ? null : v.value.toJSON()
+      this.parseStoredPropertyInnerValue(v.value)
     ) as unknown) as FlattenRelations<T>
     ) as unknown) as FlattenRelations<T>
   }
   }
 
 
@@ -349,7 +369,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
         'ID': id.toString(),
         'ID': id.toString(),
         ...defaultValues,
         ...defaultValues,
         ..._.mapValues(this.parseEntityPropertyValues(entity, entityClass, includedProps), (v) =>
         ..._.mapValues(this.parseEntityPropertyValues(entity, entityClass, includedProps), (v) =>
-          v.value.toJSON() === false && v.type !== 'Single<Bool>' ? chalk.grey('[not set]') : v.value.toString()
+          v.value === null ? chalk.grey('[not set]') : v.value.toString()
         ),
         ),
       }))
       }))
     )) as Record<string, string>[]
     )) as Record<string, string>[]

+ 10 - 5
cli/src/base/MediaCommandBase.ts

@@ -1,5 +1,5 @@
 import ContentDirectoryCommandBase from './ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from './ContentDirectoryCommandBase'
-import { VideoEntity } from 'cd-schemas/types/entities'
+import { VideoEntity, KnownLicenseEntity, LicenseEntity } from '@joystream/cd-schemas/types/entities'
 import fs from 'fs'
 import fs from 'fs'
 import { DistinctQuestion } from 'inquirer'
 import { DistinctQuestion } from 'inquirer'
 import path from 'path'
 import path from 'path'
@@ -12,7 +12,7 @@ const MAX_USER_LICENSE_CONTENT_LENGTH = 4096
  */
  */
 export default abstract class MediaCommandBase extends ContentDirectoryCommandBase {
 export default abstract class MediaCommandBase extends ContentDirectoryCommandBase {
   async promptForNewLicense(): Promise<VideoEntity['license']> {
   async promptForNewLicense(): Promise<VideoEntity['license']> {
-    let license: VideoEntity['license']
+    let licenseInput: LicenseEntity
     const licenseType: 'known' | 'custom' = await this.simplePrompt({
     const licenseType: 'known' | 'custom' = await this.simplePrompt({
       type: 'list',
       type: 'list',
       message: 'Choose license type',
       message: 'Choose license type',
@@ -22,7 +22,12 @@ export default abstract class MediaCommandBase extends ContentDirectoryCommandBa
       ],
       ],
     })
     })
     if (licenseType === 'known') {
     if (licenseType === 'known') {
-      license = { new: { knownLicense: await this.promptForEntityId('Choose License', 'KnownLicense', 'code') } }
+      const [id, knownLicenseEntity] = await this.promptForEntityEntry('Choose License', 'KnownLicense', 'code')
+      const knownLicense = await this.parseToKnownEntityJson<KnownLicenseEntity>(knownLicenseEntity)
+      licenseInput = { knownLicense: id.toNumber() }
+      if (knownLicense.attributionRequired) {
+        licenseInput.attribution = await this.simplePrompt({ message: 'Attribution' })
+      }
     } else {
     } else {
       let licenseContent: null | string = null
       let licenseContent: null | string = null
       while (licenseContent === null) {
       while (licenseContent === null) {
@@ -38,10 +43,10 @@ export default abstract class MediaCommandBase extends ContentDirectoryCommandBa
           licenseContent = null
           licenseContent = null
         }
         }
       }
       }
-      license = { new: { userDefinedLicense: { new: { content: licenseContent } } } }
+      licenseInput = { userDefinedLicense: { new: { content: licenseContent } } }
     }
     }
 
 
-    return license
+    return { new: licenseInput }
   }
   }
 
 
   async promptForPublishedBeforeJoystream(current?: number | null): Promise<number | null> {
   async promptForPublishedBeforeJoystream(current?: number | null): Promise<number | null> {

+ 19 - 4
cli/src/commands/account/choose.ts

@@ -9,13 +9,19 @@ export default class AccountChoose extends AccountsCommandBase {
   static flags = {
   static flags = {
     showSpecial: flags.boolean({
     showSpecial: flags.boolean({
       description: 'Whether to show special (DEV chain) accounts',
       description: 'Whether to show special (DEV chain) accounts',
+      char: 'S',
+      required: false,
+    }),
+    address: flags.string({
+      description: 'Select account by address (if available)',
+      char: 'a',
       required: false,
       required: false,
     }),
     }),
   }
   }
 
 
   async run() {
   async run() {
-    const { showSpecial } = this.parse(AccountChoose).flags
-    const accounts: NamedKeyringPair[] = this.fetchAccounts(showSpecial)
+    const { showSpecial, address } = this.parse(AccountChoose).flags
+    const accounts: NamedKeyringPair[] = this.fetchAccounts(!!address || showSpecial)
     const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount()
     const selectedAccount: NamedKeyringPair | null = this.getSelectedAccount()
 
 
     this.log(chalk.white(`Found ${accounts.length} existing accounts...\n`))
     this.log(chalk.white(`Found ${accounts.length} existing accounts...\n`))
@@ -25,9 +31,18 @@ export default class AccountChoose extends AccountsCommandBase {
       this.exit(ExitCodes.NoAccountFound)
       this.exit(ExitCodes.NoAccountFound)
     }
     }
 
 
-    const choosenAccount: NamedKeyringPair = await this.promptForAccount(accounts, selectedAccount)
+    let choosenAccount: NamedKeyringPair
+    if (address) {
+      const matchingAccount = accounts.find((a) => a.address === address)
+      if (!matchingAccount) {
+        this.error(`No matching account found by address: ${address}`, { exit: ExitCodes.InvalidInput })
+      }
+      choosenAccount = matchingAccount
+    } else {
+      choosenAccount = await this.promptForAccount(accounts, selectedAccount)
+    }
 
 
     await this.setSelectedAccount(choosenAccount)
     await this.setSelectedAccount(choosenAccount)
-    this.log(chalk.greenBright('\nAccount switched!'))
+    this.log(chalk.greenBright(`\nAccount switched to ${chalk.white(choosenAccount.address)}!`))
   }
   }
 }
 }

+ 3 - 3
cli/src/commands/content-directory/addClassSchema.ts

@@ -1,7 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import AddClassSchemaSchema from 'cd-schemas/schemas/extrinsics/AddClassSchema.schema.json'
-import { AddClassSchema } from 'cd-schemas/types/extrinsics/AddClassSchema'
-import { InputParser } from 'cd-schemas'
+import AddClassSchemaSchema from '@joystream/cd-schemas/schemas/extrinsics/AddClassSchema.schema.json'
+import { AddClassSchema } from '@joystream/cd-schemas/types/extrinsics/AddClassSchema'
+import { InputParser } from '@joystream/cd-schemas'
 import { JsonSchemaPrompter, JsonSchemaCustomPrompts } from '../../helpers/JsonSchemaPrompt'
 import { JsonSchemaPrompter, JsonSchemaCustomPrompts } from '../../helpers/JsonSchemaPrompt'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'

+ 3 - 3
cli/src/commands/content-directory/createClass.ts

@@ -1,7 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import CreateClassSchema from 'cd-schemas/schemas/extrinsics/CreateClass.schema.json'
-import { CreateClass } from 'cd-schemas/types/extrinsics/CreateClass'
-import { InputParser } from 'cd-schemas'
+import CreateClassSchema from '@joystream/cd-schemas/schemas/extrinsics/CreateClass.schema.json'
+import { CreateClass } from '@joystream/cd-schemas/types/extrinsics/CreateClass'
+import { InputParser } from '@joystream/cd-schemas'
 import { JsonSchemaPrompter, JsonSchemaCustomPrompts } from '../../helpers/JsonSchemaPrompt'
 import { JsonSchemaPrompter, JsonSchemaCustomPrompts } from '../../helpers/JsonSchemaPrompt'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'

+ 2 - 2
cli/src/commands/content-directory/entity.ts

@@ -36,8 +36,8 @@ export default class EntityCommand extends ContentDirectoryCommandBase {
       _.mapValues(
       _.mapValues(
         propertyValues,
         propertyValues,
         (v) =>
         (v) =>
-          (v.value.toJSON() === false && v.type !== 'Single<Bool>' ? chalk.grey('[not set]') : v.value.toString()) +
-          ` ${chalk.green(`${v.type}`)}`
+          (v.value === null ? chalk.grey('[not set]') : v.value.toString()) +
+          ` ${chalk.green(`${v.type}<${v.subtype}>`)}`
       )
       )
     )
     )
   }
   }

+ 1 - 1
cli/src/commands/content-directory/initialize.ts

@@ -1,5 +1,5 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { InputParser, ExtrinsicsHelper, getInitializationInputs } from 'cd-schemas'
+import { InputParser, ExtrinsicsHelper, getInitializationInputs } from '@joystream/cd-schemas'
 import { flags } from '@oclif/command'
 import { flags } from '@oclif/command'
 
 
 export default class InitializeCommand extends ContentDirectoryCommandBase {
 export default class InitializeCommand extends ContentDirectoryCommandBase {

+ 2 - 2
cli/src/commands/content-directory/updateClassPermissions.ts

@@ -1,8 +1,8 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import CreateClassSchema from 'cd-schemas/schemas/extrinsics/CreateClass.schema.json'
+import CreateClassSchema from '@joystream/cd-schemas/schemas/extrinsics/CreateClass.schema.json'
 import chalk from 'chalk'
 import chalk from 'chalk'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
-import { CreateClass } from 'cd-schemas/types/extrinsics/CreateClass'
+import { CreateClass } from '@joystream/cd-schemas/types/extrinsics/CreateClass'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 
 
 export default class UpdateClassPermissionsCommand extends ContentDirectoryCommandBase {
 export default class UpdateClassPermissionsCommand extends ContentDirectoryCommandBase {

+ 12 - 7
cli/src/commands/media/createChannel.ts

@@ -1,15 +1,19 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import ChannelEntitySchema from 'cd-schemas/schemas/entities/ChannelEntity.schema.json'
-import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
-import { InputParser } from 'cd-schemas'
+import ChannelEntitySchema from '@joystream/cd-schemas/schemas/entities/ChannelEntity.schema.json'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
+import { InputParser } from '@joystream/cd-schemas'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 
 
+import { flags } from '@oclif/command'
+import _ from 'lodash'
+
 export default class CreateChannelCommand extends ContentDirectoryCommandBase {
 export default class CreateChannelCommand extends ContentDirectoryCommandBase {
   static description = 'Create a new channel on Joystream (requires a membership).'
   static description = 'Create a new channel on Joystream (requires a membership).'
   static flags = {
   static flags = {
     ...IOFlags,
     ...IOFlags,
+    confirm: flags.boolean({ char: 'y', name: 'confirm', required: false, description: 'Confirm the provided input' }),
   }
   }
 
 
   async run() {
   async run() {
@@ -21,7 +25,7 @@ export default class CreateChannelCommand extends ContentDirectoryCommandBase {
 
 
     const channelJsonSchema = (ChannelEntitySchema as unknown) as JSONSchema
     const channelJsonSchema = (ChannelEntitySchema as unknown) as JSONSchema
 
 
-    const { input, output } = this.parse(CreateChannelCommand).flags
+    const { input, output, confirm } = this.parse(CreateChannelCommand).flags
 
 
     let inputJson = await getInputJson<ChannelEntity>(input, channelJsonSchema)
     let inputJson = await getInputJson<ChannelEntity>(input, channelJsonSchema)
     if (!inputJson) {
     if (!inputJson) {
@@ -32,14 +36,15 @@ export default class CreateChannelCommand extends ContentDirectoryCommandBase {
 
 
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, undefined, customPrompts)
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, undefined, customPrompts)
 
 
-      inputJson = await prompter.promptAll(true)
+      inputJson = await prompter.promptAll()
     }
     }
 
 
     this.jsonPrettyPrint(JSON.stringify(inputJson))
     this.jsonPrettyPrint(JSON.stringify(inputJson))
-    const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
+    const confirmed =
+      confirm || (await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' }))
 
 
     if (confirmed) {
     if (confirmed) {
-      saveOutputJson(output, `${inputJson.title}Channel.json`, inputJson)
+      saveOutputJson(output, `${_.startCase(inputJson.handle)}Channel.json`, inputJson)
       const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
       const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
         {
         {
           className: 'Channel',
           className: 'Channel',

+ 3 - 3
cli/src/commands/media/curateContent.ts

@@ -1,8 +1,8 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { InputParser } from 'cd-schemas'
+import { InputParser } from '@joystream/cd-schemas'
 import { flags } from '@oclif/command'
 import { flags } from '@oclif/command'
-import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
-import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
 
 
 const CLASSES = ['Channel', 'Video'] as const
 const CLASSES = ['Channel', 'Video'] as const
 const STATUSES = ['Accepted', 'Censored'] as const
 const STATUSES = ['Accepted', 'Censored'] as const

+ 1 - 1
cli/src/commands/media/featuredVideos.ts

@@ -1,6 +1,6 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { displayTable } from '../../helpers/display'
 import { displayTable } from '../../helpers/display'
-import { FeaturedVideoEntity, VideoEntity } from 'cd-schemas/types/entities'
+import { FeaturedVideoEntity, VideoEntity } from '@joystream/cd-schemas/types/entities'
 import chalk from 'chalk'
 import chalk from 'chalk'
 
 
 export default class FeaturedVideosCommand extends ContentDirectoryCommandBase {
 export default class FeaturedVideosCommand extends ContentDirectoryCommandBase {

+ 2 - 2
cli/src/commands/media/myChannels.ts

@@ -1,5 +1,5 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
 import { displayTable } from '../../helpers/display'
 import { displayTable } from '../../helpers/display'
 import chalk from 'chalk'
 import chalk from 'chalk'
 
 
@@ -9,7 +9,7 @@ export default class MyChannelsCommand extends ContentDirectoryCommandBase {
   async run() {
   async run() {
     const memberId = await this.getRequiredMemberId()
     const memberId = await this.getRequiredMemberId()
 
 
-    const props: (keyof ChannelEntity)[] = ['title', 'isPublic']
+    const props: (keyof ChannelEntity)[] = ['handle', 'isPublic']
 
 
     const list = await this.createEntityList('Channel', props, [], memberId)
     const list = await this.createEntityList('Channel', props, [], memberId)
 
 

+ 1 - 1
cli/src/commands/media/myVideos.ts

@@ -1,5 +1,5 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
 import { displayTable } from '../../helpers/display'
 import { displayTable } from '../../helpers/display'
 import chalk from 'chalk'
 import chalk from 'chalk'
 import { flags } from '@oclif/command'
 import { flags } from '@oclif/command'

+ 3 - 3
cli/src/commands/media/removeChannel.ts

@@ -1,7 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { Entity } from '@joystream/types/content-directory'
 import { Entity } from '@joystream/types/content-directory'
 import { createType } from '@joystream/types'
 import { createType } from '@joystream/types'
-import { ChannelEntity } from 'cd-schemas/types/entities'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities'
 
 
 export default class RemoveChannelCommand extends ContentDirectoryCommandBase {
 export default class RemoveChannelCommand extends ContentDirectoryCommandBase {
   static description = 'Removes a channel (required controller access).'
   static description = 'Removes a channel (required controller access).'
@@ -29,13 +29,13 @@ export default class RemoveChannelCommand extends ContentDirectoryCommandBase {
       channelId = parseInt(id)
       channelId = parseInt(id)
       channelEntity = await this.getEntity(channelId, 'Channel', memberId)
       channelEntity = await this.getEntity(channelId, 'Channel', memberId)
     } else {
     } else {
-      const [id, channel] = await this.promptForEntityEntry('Select a channel to remove', 'Channel', 'title', memberId)
+      const [id, channel] = await this.promptForEntityEntry('Select a channel to remove', 'Channel', 'handle', memberId)
       channelId = id.toNumber()
       channelId = id.toNumber()
       channelEntity = channel
       channelEntity = channel
     }
     }
     const channel = await this.parseToKnownEntityJson<ChannelEntity>(channelEntity)
     const channel = await this.parseToKnownEntityJson<ChannelEntity>(channelEntity)
 
 
-    await this.requireConfirmation(`Are you sure you want to remove "${channel.title}" channel?`)
+    await this.requireConfirmation(`Are you sure you want to remove "${channel.handle}" channel?`)
 
 
     const api = this.getOriginalApi()
     const api = this.getOriginalApi()
     this.log(`Removing Channel entity (ID: ${channelId})...`)
     this.log(`Removing Channel entity (ID: ${channelId})...`)

+ 1 - 1
cli/src/commands/media/removeVideo.ts

@@ -1,6 +1,6 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { Entity } from '@joystream/types/content-directory'
 import { Entity } from '@joystream/types/content-directory'
-import { VideoEntity } from 'cd-schemas/types/entities'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities'
 import { createType } from '@joystream/types'
 import { createType } from '@joystream/types'
 
 
 export default class RemoveVideoCommand extends ContentDirectoryCommandBase {
 export default class RemoveVideoCommand extends ContentDirectoryCommandBase {

+ 3 - 3
cli/src/commands/media/setFeaturedVideos.ts

@@ -1,7 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { VideoEntity } from 'cd-schemas/types/entities'
-import { InputParser, ExtrinsicsHelper } from 'cd-schemas'
-import { FlattenRelations } from 'cd-schemas/types/utility'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities'
+import { InputParser, ExtrinsicsHelper } from '@joystream/cd-schemas'
+import { FlattenRelations } from '@joystream/cd-schemas/types/utility'
 import { flags } from '@oclif/command'
 import { flags } from '@oclif/command'
 import { createType } from '@joystream/types'
 import { createType } from '@joystream/types'
 
 

+ 7 - 6
cli/src/commands/media/updateChannel.ts

@@ -1,13 +1,14 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import ChannelEntitySchema from 'cd-schemas/schemas/entities/ChannelEntity.schema.json'
-import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
-import { InputParser } from 'cd-schemas'
+import ChannelEntitySchema from '@joystream/cd-schemas/schemas/entities/ChannelEntity.schema.json'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
+import { InputParser } from '@joystream/cd-schemas'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 import { IOFlags, getInputJson, saveOutputJson } from '../../helpers/InputOutput'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { Actor, Entity } from '@joystream/types/content-directory'
 import { Actor, Entity } from '@joystream/types/content-directory'
 import { flags } from '@oclif/command'
 import { flags } from '@oclif/command'
 import { createType } from '@joystream/types'
 import { createType } from '@joystream/types'
+import _ from 'lodash'
 
 
 export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
 export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
   static description = 'Update one of the owned channels on Joystream (requires a membership).'
   static description = 'Update one of the owned channels on Joystream (requires a membership).'
@@ -51,7 +52,7 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
       channelId = parseInt(id)
       channelId = parseInt(id)
       channelEntity = await this.getEntity(channelId, 'Channel', memberId)
       channelEntity = await this.getEntity(channelId, 'Channel', memberId)
     } else {
     } else {
-      const [id, channel] = await this.promptForEntityEntry('Select a channel to update', 'Channel', 'title', memberId)
+      const [id, channel] = await this.promptForEntityEntry('Select a channel to update', 'Channel', 'handle', memberId)
       channelId = id.toNumber()
       channelId = id.toNumber()
       channelEntity = channel
       channelEntity = channel
     }
     }
@@ -80,14 +81,14 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
 
 
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, currentValues, customPrompts)
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, currentValues, customPrompts)
 
 
-      inputJson = await prompter.promptAll(true)
+      inputJson = await prompter.promptAll()
     }
     }
 
 
     this.jsonPrettyPrint(JSON.stringify(inputJson))
     this.jsonPrettyPrint(JSON.stringify(inputJson))
     const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
     const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
 
 
     if (confirmed) {
     if (confirmed) {
-      saveOutputJson(output, `${inputJson.title}Channel.json`, inputJson)
+      saveOutputJson(output, `${_.startCase(inputJson.handle)}Channel.json`, inputJson)
       const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
       const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
       const updateOperations = await inputParser.getEntityUpdateOperations(inputJson, 'Channel', channelId)
       const updateOperations = await inputParser.getEntityUpdateOperations(inputJson, 'Channel', channelId)
       this.log('Sending the extrinsic...')
       this.log('Sending the extrinsic...')

+ 4 - 4
cli/src/commands/media/updateVideo.ts

@@ -1,6 +1,6 @@
-import VideoEntitySchema from 'cd-schemas/schemas/entities/VideoEntity.schema.json'
-import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
-import { InputParser } from 'cd-schemas'
+import VideoEntitySchema from '@joystream/cd-schemas/schemas/entities/VideoEntity.schema.json'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
+import { InputParser } from '@joystream/cd-schemas'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { Actor, Entity } from '@joystream/types/content-directory'
 import { Actor, Entity } from '@joystream/types/content-directory'
@@ -83,7 +83,7 @@ export default class UpdateVideoCommand extends MediaCommandBase {
       'category',
       'category',
       'title',
       'title',
       'description',
       'description',
-      'thumbnailURL',
+      'thumbnailUrl',
       'duration',
       'duration',
       'isPublic',
       'isPublic',
       'isExplicit',
       'isExplicit',

+ 2 - 2
cli/src/commands/media/updateVideoLicense.ts

@@ -1,6 +1,6 @@
 import MediaCommandBase from '../../base/MediaCommandBase'
 import MediaCommandBase from '../../base/MediaCommandBase'
-import { LicenseEntity, VideoEntity } from 'cd-schemas/types/entities'
-import { InputParser } from 'cd-schemas'
+import { LicenseEntity, VideoEntity } from '@joystream/cd-schemas/types/entities'
+import { InputParser } from '@joystream/cd-schemas'
 import { Entity } from '@joystream/types/content-directory'
 import { Entity } from '@joystream/types/content-directory'
 import { createType } from '@joystream/types'
 import { createType } from '@joystream/types'
 
 

+ 131 - 73
cli/src/commands/media/uploadVideo.ts

@@ -1,8 +1,8 @@
-import VideoEntitySchema from 'cd-schemas/schemas/entities/VideoEntity.schema.json'
-import VideoMediaEntitySchema from 'cd-schemas/schemas/entities/VideoMediaEntity.schema.json'
-import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
-import { VideoMediaEntity } from 'cd-schemas/types/entities/VideoMediaEntity'
-import { InputParser } from 'cd-schemas'
+import VideoEntitySchema from '@joystream/cd-schemas/schemas/entities/VideoEntity.schema.json'
+import VideoMediaEntitySchema from '@joystream/cd-schemas/schemas/entities/VideoMediaEntity.schema.json'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
+import { VideoMediaEntity } from '@joystream/cd-schemas/types/entities/VideoMediaEntity'
+import { InputParser } from '@joystream/cd-schemas'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { flags } from '@oclif/command'
 import { flags } from '@oclif/command'
@@ -17,14 +17,15 @@ import ipfsHttpClient from 'ipfs-http-client'
 import first from 'it-first'
 import first from 'it-first'
 import last from 'it-last'
 import last from 'it-last'
 import toBuffer from 'it-to-buffer'
 import toBuffer from 'it-to-buffer'
-import ffmpegInstaller from '@ffmpeg-installer/ffmpeg'
+import ffprobeInstaller from '@ffprobe-installer/ffprobe'
 import ffmpeg from 'fluent-ffmpeg'
 import ffmpeg from 'fluent-ffmpeg'
 import MediaCommandBase from '../../base/MediaCommandBase'
 import MediaCommandBase from '../../base/MediaCommandBase'
+import { getInputJson, validateInput, IOFlags } from '../../helpers/InputOutput'
 
 
-ffmpeg.setFfmpegPath(ffmpegInstaller.path)
+ffmpeg.setFfprobePath(ffprobeInstaller.path)
 
 
 const DATA_OBJECT_TYPE_ID = 1
 const DATA_OBJECT_TYPE_ID = 1
-const MAX_FILE_SIZE = 500 * 1024 * 1024
+const MAX_FILE_SIZE = 2000 * 1024 * 1024
 
 
 type VideoMetadata = {
 type VideoMetadata = {
   width?: number
   width?: number
@@ -37,13 +38,14 @@ type VideoMetadata = {
 export default class UploadVideoCommand extends MediaCommandBase {
 export default class UploadVideoCommand extends MediaCommandBase {
   static description = 'Upload a new Video to a channel (requires a membership).'
   static description = 'Upload a new Video to a channel (requires a membership).'
   static flags = {
   static flags = {
-    // TODO: ...IOFlags, - providing input as json
+    input: IOFlags.input,
     channel: flags.integer({
     channel: flags.integer({
       char: 'c',
       char: 'c',
       required: false,
       required: false,
       description:
       description:
         'ID of the channel to assign the video to (if omitted - one of the owned channels can be selected from the list)',
         'ID of the channel to assign the video to (if omitted - one of the owned channels can be selected from the list)',
     }),
     }),
+    confirm: flags.boolean({ char: 'y', name: 'confirm', required: false, description: 'Confirm the provided input' }),
   }
   }
 
 
   static args = [
   static args = [
@@ -217,6 +219,117 @@ export default class UploadVideoCommand extends MediaCommandBase {
     }
     }
   }
   }
 
 
+  private async promptForVideoInput(
+    channelId: number,
+    fileSize: number,
+    contentId: ContentId,
+    videoMetadata: VideoMetadata | null
+  ) {
+    // Set the defaults
+    const videoMediaDefaults: Partial<VideoMediaEntity> = {
+      pixelWidth: videoMetadata?.width,
+      pixelHeight: videoMetadata?.height,
+    }
+    const videoDefaults: Partial<VideoEntity> = {
+      duration: videoMetadata?.duration,
+      skippableIntroDuration: 0,
+    }
+
+    // Prompt for data
+    const videoJsonSchema = (VideoEntitySchema as unknown) as JSONSchema
+    const videoMediaJsonSchema = (VideoMediaEntitySchema as unknown) as JSONSchema
+
+    const videoMediaPrompter = new JsonSchemaPrompter<VideoMediaEntity>(videoMediaJsonSchema, videoMediaDefaults)
+    const videoPrompter = new JsonSchemaPrompter<VideoEntity>(videoJsonSchema, videoDefaults)
+
+    // Prompt for the data
+    const encodingSuggestion =
+      videoMetadata && videoMetadata.codecFullName ? ` (suggested: ${videoMetadata.codecFullName})` : ''
+    const encoding = await this.promptForEntityId(
+      `Choose Video encoding${encodingSuggestion}`,
+      'VideoMediaEncoding',
+      'name'
+    )
+    const { pixelWidth, pixelHeight } = await videoMediaPrompter.promptMultipleProps(['pixelWidth', 'pixelHeight'])
+    const language = await this.promptForEntityId('Choose Video language', 'Language', 'name')
+    const category = await this.promptForEntityId('Choose Video category', 'ContentCategory', 'name')
+    const videoProps = await videoPrompter.promptMultipleProps([
+      'title',
+      'description',
+      'thumbnailUrl',
+      'duration',
+      'isPublic',
+      'isExplicit',
+      'hasMarketing',
+      'skippableIntroDuration',
+    ])
+
+    const license = await videoPrompter.promptSingleProp('license', () => this.promptForNewLicense())
+    const publishedBeforeJoystream = await videoPrompter.promptSingleProp('publishedBeforeJoystream', () =>
+      this.promptForPublishedBeforeJoystream()
+    )
+
+    // Create final inputs
+    const videoMediaInput: VideoMediaEntity = {
+      encoding,
+      pixelWidth,
+      pixelHeight,
+      size: fileSize,
+      location: { new: { joystreamMediaLocation: { new: { dataObjectId: contentId.encode() } } } },
+    }
+    return {
+      ...videoProps,
+      channel: channelId,
+      language,
+      category,
+      license,
+      media: { new: videoMediaInput },
+      publishedBeforeJoystream,
+    }
+  }
+
+  private async getVideoInputFromFile(
+    filePath: string,
+    channelId: number,
+    fileSize: number,
+    contentId: ContentId,
+    videoMetadata: VideoMetadata | null
+  ) {
+    let videoInput = await getInputJson<any>(filePath)
+    if (typeof videoInput !== 'object' || videoInput === null) {
+      this.error('Invalid input json - expected an object', { exit: ExitCodes.InvalidInput })
+    }
+    const videoMediaDefaults: Partial<VideoMediaEntity> = {
+      pixelWidth: videoMetadata?.width,
+      pixelHeight: videoMetadata?.height,
+      size: fileSize,
+    }
+    const videoDefaults: Partial<VideoEntity> = {
+      channel: channelId,
+      duration: videoMetadata?.duration,
+    }
+    const inputVideoMedia =
+      videoInput.media && typeof videoInput.media === 'object' && (videoInput.media as any).new
+        ? (videoInput.media as any).new
+        : {}
+    videoInput = {
+      ...videoDefaults,
+      ...videoInput,
+      media: {
+        new: {
+          ...videoMediaDefaults,
+          ...inputVideoMedia,
+          location: { new: { joystreamMediaLocation: { new: { dataObjectId: contentId.encode() } } } },
+        },
+      },
+    }
+
+    const videoJsonSchema = (VideoEntitySchema as unknown) as JSONSchema
+    await validateInput(videoInput, videoJsonSchema)
+
+    return videoInput as VideoEntity
+  }
+
   async run() {
   async run() {
     const account = await this.getRequiredSelectedAccount()
     const account = await this.getRequiredSelectedAccount()
     const memberId = await this.getRequiredMemberId()
     const memberId = await this.getRequiredMemberId()
@@ -226,7 +339,7 @@ export default class UploadVideoCommand extends MediaCommandBase {
 
 
     const {
     const {
       args: { filePath },
       args: { filePath },
-      flags: { channel: inputChannelId },
+      flags: { channel: inputChannelId, input, confirm },
     } = this.parse(UploadVideoCommand)
     } = this.parse(UploadVideoCommand)
 
 
     // Basic file validation
     // Basic file validation
@@ -255,7 +368,7 @@ export default class UploadVideoCommand extends MediaCommandBase {
       channelId = await this.promptForEntityId(
       channelId = await this.promptForEntityId(
         'Select a channel to publish the video under',
         'Select a channel to publish the video under',
         'Channel',
         'Channel',
-        'title',
+        'handle',
         memberId
         memberId
       )
       )
     } else {
     } else {
@@ -303,71 +416,16 @@ export default class UploadVideoCommand extends MediaCommandBase {
 
 
     await this.uploadVideo(filePath, fileSize, uploadUrl)
     await this.uploadVideo(filePath, fileSize, uploadUrl)
 
 
-    // Prompting for the data:
-
-    // Set the defaults
-    const videoMediaDefaults: Partial<VideoMediaEntity> = {
-      pixelWidth: videoMetadata?.width,
-      pixelHeight: videoMetadata?.height,
-    }
-    const videoDefaults: Partial<VideoEntity> = {
-      duration: videoMetadata?.duration,
-      skippableIntroDuration: 0,
-    }
-    // Create prompting helpers
-    const videoJsonSchema = (VideoEntitySchema as unknown) as JSONSchema
-    const videoMediaJsonSchema = (VideoMediaEntitySchema as unknown) as JSONSchema
-
-    const videoMediaPrompter = new JsonSchemaPrompter<VideoMediaEntity>(videoMediaJsonSchema, videoMediaDefaults)
-    const videoPrompter = new JsonSchemaPrompter<VideoEntity>(videoJsonSchema, videoDefaults)
-
-    // Prompt for the data
-    const encodingSuggestion =
-      videoMetadata && videoMetadata.codecFullName ? ` (suggested: ${videoMetadata.codecFullName})` : ''
-    const encoding = await this.promptForEntityId(
-      `Choose Video encoding${encodingSuggestion}`,
-      'VideoMediaEncoding',
-      'name'
-    )
-    const { pixelWidth, pixelHeight } = await videoMediaPrompter.promptMultipleProps(['pixelWidth', 'pixelHeight'])
-    const language = await this.promptForEntityId('Choose Video language', 'Language', 'name')
-    const category = await this.promptForEntityId('Choose Video category', 'ContentCategory', 'name')
-    const videoProps = await videoPrompter.promptMultipleProps([
-      'title',
-      'description',
-      'thumbnailURL',
-      'duration',
-      'isPublic',
-      'isExplicit',
-      'hasMarketing',
-      'skippableIntroDuration',
-    ])
+    // No input, create prompting helpers
+    const videoInput = input
+      ? await this.getVideoInputFromFile(input, channelId, fileSize, contentId, videoMetadata)
+      : await this.promptForVideoInput(channelId, fileSize, contentId, videoMetadata)
 
 
-    const license = await videoPrompter.promptSingleProp('license', () => this.promptForNewLicense())
-    const publishedBeforeJoystream = await videoPrompter.promptSingleProp('publishedBeforeJoystream', () =>
-      this.promptForPublishedBeforeJoystream()
-    )
+    this.jsonPrettyPrint(JSON.stringify(videoInput))
 
 
-    // Create final inputs
-    const videoMediaInput: VideoMediaEntity = {
-      encoding,
-      pixelWidth,
-      pixelHeight,
-      size: fileSize,
-      location: { new: { joystreamMediaLocation: { new: { dataObjectId: contentId.encode() } } } },
+    if (!confirm) {
+      await this.requireConfirmation('Do you confirm the provided input?', true)
     }
     }
-    const videoInput: VideoEntity = {
-      ...videoProps,
-      channel: channelId,
-      language,
-      category,
-      license,
-      media: { new: videoMediaInput },
-      publishedBeforeJoystream,
-    }
-
-    this.jsonPrettyPrint(JSON.stringify(videoInput))
-    await this.requireConfirmation('Do you confirm the provided input?')
 
 
     // Parse inputs into operations and send final extrinsic
     // Parse inputs into operations and send final extrinsic
     const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
     const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [

+ 11 - 7
cli/src/helpers/InputOutput.ts

@@ -5,7 +5,7 @@ import fs from 'fs'
 import path from 'path'
 import path from 'path'
 import Ajv from 'ajv'
 import Ajv from 'ajv'
 import $RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import $RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser'
-import { getSchemasLocation } from 'cd-schemas'
+import { getSchemasLocation } from '@joystream/cd-schemas'
 import chalk from 'chalk'
 import chalk from 'chalk'
 
 
 // Default schema path for resolving refs
 // Default schema path for resolving refs
@@ -39,12 +39,7 @@ export async function getInputJson<T>(inputPath?: string, schema?: JSONSchema, s
       throw new CLIError(`JSON parsing failed for file: ${inputPath}`, { exit: ExitCodes.InvalidInput })
       throw new CLIError(`JSON parsing failed for file: ${inputPath}`, { exit: ExitCodes.InvalidInput })
     }
     }
     if (schema) {
     if (schema) {
-      const ajv = new Ajv()
-      schema = await $RefParser.dereference(schemaPath || DEFAULT_SCHEMA_PATH, schema, {})
-      const valid = ajv.validate(schema, jsonObj) as boolean
-      if (!valid) {
-        throw new CLIError(`Input JSON file is not valid: ${ajv.errorsText()}`)
-      }
+      await validateInput(jsonObj, schema, schemaPath)
     }
     }
 
 
     return jsonObj as T
     return jsonObj as T
@@ -53,6 +48,15 @@ export async function getInputJson<T>(inputPath?: string, schema?: JSONSchema, s
   return null
   return null
 }
 }
 
 
+export async function validateInput(input: unknown, schema: JSONSchema, schemaPath?: string): Promise<void> {
+  const ajv = new Ajv({ allErrors: true })
+  schema = await $RefParser.dereference(schemaPath || DEFAULT_SCHEMA_PATH, schema, {})
+  const valid = ajv.validate(schema, input) as boolean
+  if (!valid) {
+    throw new CLIError(`Input JSON file is not valid: ${ajv.errorsText()}`)
+  }
+}
+
 export function saveOutputJson(outputPath: string | undefined, fileName: string, data: any): void {
 export function saveOutputJson(outputPath: string | undefined, fileName: string, data: any): void {
   if (outputPath) {
   if (outputPath) {
     let outputFilePath = path.join(outputPath, fileName)
     let outputFilePath = path.join(outputPath, fileName)

+ 40 - 35
cli/src/helpers/JsonSchemaPrompt.ts

@@ -4,7 +4,7 @@ import _ from 'lodash'
 import RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import chalk from 'chalk'
 import chalk from 'chalk'
 import { BOOL_PROMPT_OPTIONS } from './prompting'
 import { BOOL_PROMPT_OPTIONS } from './prompting'
-import { getSchemasLocation } from 'cd-schemas'
+import { getSchemasLocation } from '@joystream/cd-schemas'
 import path from 'path'
 import path from 'path'
 
 
 type CustomPromptMethod = () => Promise<any>
 type CustomPromptMethod = () => Promise<any>
@@ -106,7 +106,11 @@ export class JsonSchemaPrompter<JsonResult> {
     if (schema.oneOf) {
     if (schema.oneOf) {
       const oneOf = schema.oneOf as JSONSchema[]
       const oneOf = schema.oneOf as JSONSchema[]
       const options = this.oneOfToOptions(oneOf, currentValue)
       const options = this.oneOfToOptions(oneOf, currentValue)
-      const { choosen } = await inquirer.prompt({ name: 'choosen', message: propDisplayName, type: 'list', ...options })
+      const choosen = await this.inquirerSinglePrompt({
+        message: propDisplayName,
+        type: 'list',
+        ...options,
+      })
       if (choosen !== options.default) {
       if (choosen !== options.default) {
         _.set(this.filledObject, propertyPath, undefined) // Clear any previous value if different variant selected
         _.set(this.filledObject, propertyPath, undefined) // Clear any previous value if different variant selected
       }
       }
@@ -128,18 +132,13 @@ export class JsonSchemaPrompter<JsonResult> {
         const required = allPropsRequired || (Array.isArray(schema.required) && schema.required.includes(pName))
         const required = allPropsRequired || (Array.isArray(schema.required) && schema.required.includes(pName))
 
 
         if (!required) {
         if (!required) {
-          confirmed = (
-            await inquirer.prompt([
-              {
-                message: `Do you want to provide optional ${chalk.greenBright(objectPropertyPath)}?`,
-                type: 'confirm',
-                name: 'confirmed',
-                default:
-                  _.get(this.filledObject, objectPropertyPath) !== undefined &&
-                  _.get(this.filledObject, objectPropertyPath) !== null,
-              },
-            ])
-          ).confirmed
+          confirmed = await this.inquirerSinglePrompt({
+            message: `Do you want to provide optional ${chalk.greenBright(objectPropertyPath)}?`,
+            type: 'confirm',
+            default:
+              _.get(this.filledObject, objectPropertyPath) !== undefined &&
+              _.get(this.filledObject, objectPropertyPath) !== null,
+          })
         }
         }
         if (confirmed) {
         if (confirmed) {
           value[pName] = await this.prompt(pSchema, objectPropertyPath)
           value[pName] = await this.prompt(pSchema, objectPropertyPath)
@@ -207,14 +206,11 @@ export class JsonSchemaPrompter<JsonResult> {
     let currItem = 0
     let currItem = 0
     const result = []
     const result = []
     while (currItem < maxItems) {
     while (currItem < maxItems) {
-      const { next } = await inquirer.prompt([
-        {
-          ...BOOL_PROMPT_OPTIONS,
-          name: 'next',
-          message: `Do you want to add another item to ${this.propertyDisplayName(propertyPath)} array?`,
-          default: _.get(this.filledObject, `${propertyPath}[${currItem}]`) !== undefined,
-        },
-      ])
+      const next = await this.inquirerSinglePrompt({
+        ...BOOL_PROMPT_OPTIONS,
+        message: `Do you want to add another item to ${this.propertyDisplayName(propertyPath)} array?`,
+        default: _.get(this.filledObject, `${propertyPath}[${currItem}]`) !== undefined,
+      })
       if (!next) {
       if (!next) {
         break
         break
       }
       }
@@ -228,20 +224,17 @@ export class JsonSchemaPrompter<JsonResult> {
   }
   }
 
 
   private async promptSimple(promptOptions: DistinctQuestion, propertyPath: string, normalize?: (v: any) => any) {
   private async promptSimple(promptOptions: DistinctQuestion, propertyPath: string, normalize?: (v: any) => any) {
-    const { result } = await inquirer.prompt([
-      {
-        ...promptOptions,
-        name: 'result',
-        validate: (v) => {
-          v = normalize ? normalize(v) : v
-          return (
-            this.setValueAndGetError(propertyPath, v) ||
-            (promptOptions.validate ? promptOptions.validate(v) : true) ||
-            true
-          )
-        },
+    const result = await this.inquirerSinglePrompt({
+      ...promptOptions,
+      validate: (v) => {
+        v = normalize ? normalize(v) : v
+        return (
+          this.setValueAndGetError(propertyPath, v) ||
+          (promptOptions.validate ? promptOptions.validate(v) : true) ||
+          true
+        )
       },
       },
-    ])
+    })
 
 
     return result
     return result
   }
   }
@@ -291,4 +284,16 @@ export class JsonSchemaPrompter<JsonResult> {
     await this.prompt(mainSchema.properties![p] as JSONSchema, p, customPrompt)
     await this.prompt(mainSchema.properties![p] as JSONSchema, p, customPrompt)
     return this.filledObject[p] as Exclude<JsonResult[P], undefined>
     return this.filledObject[p] as Exclude<JsonResult[P], undefined>
   }
   }
+
+  async inquirerSinglePrompt(question: DistinctQuestion) {
+    const { result } = await inquirer.prompt([
+      {
+        ...question,
+        name: 'result',
+        prefix: '',
+      },
+    ])
+
+    return result
+  }
 }
 }

+ 20 - 20
content-directory-schemas/README.md

@@ -14,7 +14,7 @@ In order to make this documentation as clear as possible it is important to make
 In order to intialize the content directory on a development chain based on data that is provided in form of json files inside `/inputs` directory (`classes`, `schemas` and example entities - `entityBatches`), we can run:
 In order to intialize the content directory on a development chain based on data that is provided in form of json files inside `/inputs` directory (`classes`, `schemas` and example entities - `entityBatches`), we can run:
 
 
 ```
 ```
-yarn workspace cd-schemas initialize:dev
+yarn workspace @joystream/cd-schemas initialize:dev
 ```
 ```
 
 
 This will handle:
 This will handle:
@@ -51,7 +51,7 @@ For more context, see: https://code.visualstudio.com/docs/languages/json
 
 
 ### Validate inputs and `json-schemas` via a command
 ### Validate inputs and `json-schemas` via a command
 
 
-All inputs inside `inputs` directory and `json-schemas` used to validate those inputs can also be validated using `yarn workspace cd-schemas validate` command. This is mainly to facilitate checking the validity of `.json` and `.schema.json` files inside `content-directory-schemas` through CI.
+All inputs inside `inputs` directory and `json-schemas` used to validate those inputs can also be validated using `yarn workspace @joystream/cd-schemas validate` command. This is mainly to facilitate checking the validity of `.json` and `.schema.json` files inside `content-directory-schemas` through CI.
 
 
 ### Entity batches
 ### Entity batches
 
 
@@ -109,7 +109,7 @@ We can do it by either using `"new"` or `"existing"` keyword.
 There is a script that provides an easy way of converting `runtime-schemas` (based on inputs from `inputs/schemas`) to `json-schemas` (`.schema.json` files) which allow validating the input (ie. json files) describing some specific entities. It can be run with:
 There is a script that provides an easy way of converting `runtime-schemas` (based on inputs from `inputs/schemas`) to `json-schemas` (`.schema.json` files) which allow validating the input (ie. json files) describing some specific entities. It can be run with:
 
 
 ```
 ```
-yarn workspace cd-schemas generate:entity-schemas
+yarn workspace @joystream/cd-schemas generate:entity-schemas
 ```
 ```
 
 
 Those `json-schemas` are currently mainly used for validating the inputs inside `inputs/entityBatches`.
 Those `json-schemas` are currently mainly used for validating the inputs inside `inputs/entityBatches`.
@@ -125,7 +125,7 @@ The generated `json-schemas` include:
 Thanks to the `json-schema-to-typescript` library, we can very simply generate Typescript interfaces based on existing `json-schemas`. This can be done via:
 Thanks to the `json-schema-to-typescript` library, we can very simply generate Typescript interfaces based on existing `json-schemas`. This can be done via:
 
 
 ```
 ```
-yarn workspace cd-schemas generate:types
+yarn workspace @joystream/cd-schemas generate:types
 ```
 ```
 
 
 This command will generate:
 This command will generate:
@@ -153,19 +153,19 @@ The best way to ilustrate this would be by providing some examples:
 
 
 #### Creating a channel
 #### Creating a channel
 ```
 ```
-  import { InputParser } from 'cd-schemas'
-  import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+  import { InputParser } from '@joystream/cd-schemas'
+  import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
   // Other imports...
   // Other imports...
 
 
   async main() {
   async main() {
     // Initialize the api, SENDER_KEYPAIR and SENDER_MEMBER_ID...
     // Initialize the api, SENDER_KEYPAIR and SENDER_MEMBER_ID...
 
 
     const channel: ChannelEntity = {
     const channel: ChannelEntity = {
-      title: 'Example channel',
+      handle: 'Example channel',
       description: 'This is an example channel',
       description: 'This is an example channel',
       language: { existing: { code: 'EN' } },
       language: { existing: { code: 'EN' } },
       coverPhotoUrl: '',
       coverPhotoUrl: '',
-      avatarPhotoURL: '',
+      avatarPhotoUrl: '',
       isPublic: true,
       isPublic: true,
     }
     }
 
 
@@ -182,12 +182,12 @@ The best way to ilustrate this would be by providing some examples:
       .signAndSend(SENDER_KEYPAIR)
       .signAndSend(SENDER_KEYPAIR)
   }
   }
 ```
 ```
-_Full example with comments can be found in `content-directory-schemas/examples/createChannel.ts` and ran with `yarn workspace cd-schemas example:createChannel`_
+_Full example with comments can be found in `content-directory-schemas/examples/createChannel.ts` and ran with `yarn workspace @joystream/cd-schemas example:createChannel`_
 
 
 #### Creating a video
 #### Creating a video
 ```
 ```
-import { InputParser } from 'cd-schemas'
-import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+import { InputParser } from '@joystream/cd-schemas'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
 // ...
 // ...
 
 
 async main() {
 async main() {
@@ -198,7 +198,7 @@ async main() {
     description: 'This is an example video',
     description: 'This is an example video',
     language: { existing: { code: 'EN' } },
     language: { existing: { code: 'EN' } },
     category: { existing: { name: 'Education' } },
     category: { existing: { name: 'Education' } },
-    channel: { existing: { title: 'Example channel' } },
+    channel: { existing: { handle: 'Example channel' } },
     media: {
     media: {
       new: {
       new: {
         encoding: { existing: { name: 'H.263_MP4' } },
         encoding: { existing: { name: 'H.263_MP4' } },
@@ -221,7 +221,7 @@ async main() {
       },
       },
     },
     },
     duration: 3600,
     duration: 3600,
-    thumbnailURL: '',
+    thumbnailUrl: '',
     isExplicit: false,
     isExplicit: false,
     isPublic: true,
     isPublic: true,
   }
   }
@@ -239,25 +239,25 @@ async main() {
     .signAndSend(SENDER_KEYPAIR)
     .signAndSend(SENDER_KEYPAIR)
 }
 }
 ```
 ```
-_Full example with comments can be found in `content-directory-schemas/examples/createVideo.ts` and ran with `yarn workspace cd-schemas example:createChannel`_
+_Full example with comments can be found in `content-directory-schemas/examples/createVideo.ts` and ran with `yarn workspace @joystream/cd-schemas example:createChannel`_
 
 
-#### Update channel title
+#### Update channel handle
 
 
 ```
 ```
-import { InputParser } from 'cd-schemas'
-import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { InputParser } from '@joystream/cd-schemas'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
 // ...
 // ...
 
 
 async function main() {
 async function main() {
   // ...
   // ...
 
 
   const channelUpdateInput: Partial<ChannelEntity> = {
   const channelUpdateInput: Partial<ChannelEntity> = {
-    title: 'Updated channel title',
+    handle: 'Updated channel handle',
   }
   }
 
 
   const parser = InputParser.createWithKnownSchemas(api)
   const parser = InputParser.createWithKnownSchemas(api)
 
 
-  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ title: 'Example channel' }, 'Channel')
+  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ handle: 'Example channel' }, 'Channel')
 
 
   const updateOperations = await parser.getEntityUpdateOperations(channelUpdateInput, 'Channel', CHANNEL_ID)
   const updateOperations = await parser.getEntityUpdateOperations(channelUpdateInput, 'Channel', CHANNEL_ID)
 
 
@@ -266,7 +266,7 @@ async function main() {
     .signAndSend(SENDER_KEYPAIR)
     .signAndSend(SENDER_KEYPAIR)
 }
 }
 ```
 ```
-_Full example with comments can be found in `content-directory-schemas/examples/updateChannelTitle.ts` and ran with `yarn workspace cd-schemas example:updateChannelTitle`_
+_Full example with comments can be found in `content-directory-schemas/examples/updateChannelHandle.ts` and ran with `yarn workspace @joystream/cd-schemas example:updateChannelHandle`_
 
 
 Note: Updates can also inlucde `new` and `existing` keywords. In case `new` is specified inside the update - `CreateEntity` and `AddSchemaSupportToEntity` operations will be included as part of the operations returned by `InputParser.getEntityUpdateOperations`.
 Note: Updates can also inlucde `new` and `existing` keywords. In case `new` is specified inside the update - `CreateEntity` and `AddSchemaSupportToEntity` operations will be included as part of the operations returned by `InputParser.getEntityUpdateOperations`.
 
 

+ 5 - 5
content-directory-schemas/examples/createChannel.ts

@@ -1,9 +1,9 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 import { types as joyTypes } from '@joystream/types'
 import { Keyring } from '@polkadot/keyring'
 import { Keyring } from '@polkadot/keyring'
-// Import input parser and channel entity from cd-schemas (we use it as library here)
-import { InputParser } from 'cd-schemas'
-import { ChannelEntity } from 'cd-schemas/types/entities'
+// Import input parser and channel entity from @joystream/cd-schemas (we use it as library here)
+import { InputParser } from '@joystream/cd-schemas'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities'
 
 
 async function main() {
 async function main() {
   // Initialize the api
   // Initialize the api
@@ -16,14 +16,14 @@ async function main() {
   const [ALICE] = keyring.getPairs()
   const [ALICE] = keyring.getPairs()
 
 
   const channel: ChannelEntity = {
   const channel: ChannelEntity = {
-    title: 'Example channel',
+    handle: 'Example channel',
     description: 'This is an example channel',
     description: 'This is an example channel',
     // We can use "existing" syntax to reference either an on-chain entity or other entity that's part of the same batch.
     // We can use "existing" syntax to reference either an on-chain entity or other entity that's part of the same batch.
     // Here we reference language that we assume was added by initialization script (initialize:dev), as it is part of
     // Here we reference language that we assume was added by initialization script (initialize:dev), as it is part of
     // input/entityBatches/LanguageBatch.json
     // input/entityBatches/LanguageBatch.json
     language: { existing: { code: 'EN' } },
     language: { existing: { code: 'EN' } },
     coverPhotoUrl: '',
     coverPhotoUrl: '',
-    avatarPhotoURL: '',
+    avatarPhotoUrl: '',
     isPublic: true,
     isPublic: true,
   }
   }
   // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
   // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)

+ 6 - 6
content-directory-schemas/examples/createChannelWithoutTransaction.ts

@@ -1,10 +1,10 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 import { types as joyTypes } from '@joystream/types'
 import { Keyring } from '@polkadot/keyring'
 import { Keyring } from '@polkadot/keyring'
-// Import input parser and channel entity from cd-schemas (we use it as library here)
-import { InputParser } from 'cd-schemas'
-import { ChannelEntity } from 'cd-schemas/types/entities'
-import { FlattenRelations } from 'cd-schemas/types/utility'
+// Import input parser and channel entity from @joystream/cd-schemas (we use it as library here)
+import { InputParser } from '@joystream/cd-schemas'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities'
+import { FlattenRelations } from '@joystream/cd-schemas/types/utility'
 import { EntityId } from '@joystream/types/content-directory'
 import { EntityId } from '@joystream/types/content-directory'
 
 
 // Alternative way of creating a channel using separate extrinsics (instead of contentDirectory.transaction)
 // Alternative way of creating a channel using separate extrinsics (instead of contentDirectory.transaction)
@@ -26,11 +26,11 @@ async function main() {
 
 
   // We use FlattenRelations to exlude { new } and { existing } (which are not allowed if we want to parse only a single entity)
   // We use FlattenRelations to exlude { new } and { existing } (which are not allowed if we want to parse only a single entity)
   const channel: FlattenRelations<ChannelEntity> = {
   const channel: FlattenRelations<ChannelEntity> = {
-    title: 'Example channel 2',
+    handle: 'Example channel 2',
     description: 'This is an example channel',
     description: 'This is an example channel',
     language: languageEntityId,
     language: languageEntityId,
     coverPhotoUrl: '',
     coverPhotoUrl: '',
-    avatarPhotoURL: '',
+    avatarPhotoUrl: '',
     isPublic: true,
     isPublic: true,
   }
   }
 
 

+ 6 - 6
content-directory-schemas/examples/createVideo.ts

@@ -1,9 +1,9 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 import { types as joyTypes } from '@joystream/types'
 import { Keyring } from '@polkadot/keyring'
 import { Keyring } from '@polkadot/keyring'
-// Import input parser and video entity from cd-schemas (we use it as library here)
-import { InputParser } from 'cd-schemas'
-import { VideoEntity } from 'cd-schemas/types/entities/VideoEntity'
+// Import input parser and video entity from @joystream/cd-schemas (we use it as library here)
+import { InputParser } from '@joystream/cd-schemas'
+import { VideoEntity } from '@joystream/cd-schemas/types/entities/VideoEntity'
 
 
 async function main() {
 async function main() {
   // Initialize the api
   // Initialize the api
@@ -22,9 +22,9 @@ async function main() {
     // (those referenced here are part of inputs/entityBatches)
     // (those referenced here are part of inputs/entityBatches)
     language: { existing: { code: 'EN' } },
     language: { existing: { code: 'EN' } },
     category: { existing: { name: 'Education' } },
     category: { existing: { name: 'Education' } },
-    // We use the same "existing" syntax to reference a channel by unique property (title)
+    // We use the same "existing" syntax to reference a channel by unique property (handle)
     // In this case it's a channel that we created in createChannel example
     // In this case it's a channel that we created in createChannel example
-    channel: { existing: { title: 'Example channel' } },
+    channel: { existing: { handle: 'Example channel' } },
     media: {
     media: {
       // We use "new" syntax to sygnalize we want to create a new VideoMedia entity that will be related to this Video entity
       // We use "new" syntax to sygnalize we want to create a new VideoMedia entity that will be related to this Video entity
       new: {
       new: {
@@ -46,7 +46,7 @@ async function main() {
       },
       },
     },
     },
     duration: 3600,
     duration: 3600,
-    thumbnailURL: '',
+    thumbnailUrl: '',
     isExplicit: false,
     isExplicit: false,
     isPublic: true,
     isPublic: true,
   }
   }

+ 5 - 5
content-directory-schemas/examples/updateChannelTitle.ts

@@ -1,9 +1,9 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 import { types as joyTypes } from '@joystream/types'
 import { Keyring } from '@polkadot/keyring'
 import { Keyring } from '@polkadot/keyring'
-// Import input parser and channel entity from cd-schemas (we use it as library here)
-import { InputParser } from 'cd-schemas'
-import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+// Import input parser and channel entity from @joystream/cd-schemas (we use it as library here)
+import { InputParser } from '@joystream/cd-schemas'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities/ChannelEntity'
 
 
 async function main() {
 async function main() {
   // Initialize the api
   // Initialize the api
@@ -17,7 +17,7 @@ async function main() {
 
 
   // Create partial channel entity, only containing the fields we wish to update
   // Create partial channel entity, only containing the fields we wish to update
   const channelUpdateInput: Partial<ChannelEntity> = {
   const channelUpdateInput: Partial<ChannelEntity> = {
-    title: 'Updated channel title',
+    handle: 'Updated channel handle',
   }
   }
 
 
   // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
   // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
@@ -25,7 +25,7 @@ async function main() {
 
 
   // We can reuse InputParser's `findEntityIdByUniqueQuery` method to find entityId of the channel we
   // We can reuse InputParser's `findEntityIdByUniqueQuery` method to find entityId of the channel we
   // created in ./createChannel.ts example (normally we would probably use some other way to do it, ie.: query node)
   // created in ./createChannel.ts example (normally we would probably use some other way to do it, ie.: query node)
-  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ title: 'Example channel' }, 'Channel')
+  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ handle: 'Example channel' }, 'Channel')
 
 
   // Use getEntityUpdateOperations to parse the update input
   // Use getEntityUpdateOperations to parse the update input
   const updateOperations = await parser.getEntityUpdateOperations(
   const updateOperations = await parser.getEntityUpdateOperations(

+ 6 - 6
content-directory-schemas/examples/updateChannelTitleWithoutTransaction.ts

@@ -1,10 +1,10 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 import { types as joyTypes } from '@joystream/types'
 import { Keyring } from '@polkadot/keyring'
 import { Keyring } from '@polkadot/keyring'
-// Import input parser and channel entity from cd-schemas (we use it as library here)
-import { InputParser } from 'cd-schemas'
-import { ChannelEntity } from 'cd-schemas/types/entities'
-import { FlattenRelations } from 'cd-schemas/types/utility'
+// Import input parser and channel entity from @joystream/cd-schemas (we use it as library here)
+import { InputParser } from '@joystream/cd-schemas'
+import { ChannelEntity } from '@joystream/cd-schemas/types/entities'
+import { FlattenRelations } from '@joystream/cd-schemas/types/utility'
 
 
 // Alternative way of update a channel using updateEntityPropertyValues extrinsic
 // Alternative way of update a channel using updateEntityPropertyValues extrinsic
 async function main() {
 async function main() {
@@ -19,7 +19,7 @@ async function main() {
 
 
   // Create partial channel entity, only containing the fields we wish to update
   // Create partial channel entity, only containing the fields we wish to update
   const channelUpdateInput: Partial<FlattenRelations<ChannelEntity>> = {
   const channelUpdateInput: Partial<FlattenRelations<ChannelEntity>> = {
-    title: 'Updated channel title 2',
+    handle: 'Updated channel handle 2',
   }
   }
 
 
   // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
   // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
@@ -28,7 +28,7 @@ async function main() {
   // We can reuse InputParser's `findEntityIdByUniqueQuery` method to find entityId of the channel we
   // We can reuse InputParser's `findEntityIdByUniqueQuery` method to find entityId of the channel we
   // created in ./createChannelWithoutTransaction.ts example
   // created in ./createChannelWithoutTransaction.ts example
   // (normally we would probably use some other way to do it, ie.: query node)
   // (normally we would probably use some other way to do it, ie.: query node)
-  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ title: 'Example channel 2' }, 'Channel')
+  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ handle: 'Example channel 2' }, 'Channel')
 
 
   // We use parser to create input property values map
   // We use parser to create input property values map
   const newPropertyValues = await parser.parseToInputEntityValuesMap(channelUpdateInput, 'Channel')
   const newPropertyValues = await parser.parseToInputEntityValuesMap(channelUpdateInput, 'Channel')

+ 2 - 2
content-directory-schemas/inputs/classes/ChannelClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "Channel",
   "name": "Channel",
   "description": "A channel belonging to certain member. Members can publish certain type of content (ie. videos) through channels.",
   "description": "A channel belonging to certain member. Members can publish certain type of content (ie. videos) through channels.",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 25,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/ContentCategoryClass.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "ContentCategory",
   "name": "ContentCategory",
   "description": "A category the content may be published under",
   "description": "A category the content may be published under",
-  "maximum_entities_count": 100,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 500,
+  "default_entity_creation_voucher_upper_bound": 500
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/FeaturedVideoClass.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "FeaturedVideo",
   "name": "FeaturedVideo",
   "description": "Featured video references",
   "description": "Featured video references",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 10,
+  "default_entity_creation_voucher_upper_bound": 10
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/HttpMediaLocationClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "HttpMediaLocation",
   "name": "HttpMediaLocation",
   "description": "An object describing http location of media object",
   "description": "An object describing http location of media object",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/JoystreamMediaLocationClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "JoystreamMediaLocation",
   "name": "JoystreamMediaLocation",
   "description": "An object describing location of media object in a format specific to Joystream platform",
   "description": "An object describing location of media object in a format specific to Joystream platform",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/KnownLicenseClass.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "KnownLicense",
   "name": "KnownLicense",
   "description": "A commonly recognized license (ie. CC_BY_SA)",
   "description": "A commonly recognized license (ie. CC_BY_SA)",
-  "maximum_entities_count": 100,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 500,
+  "default_entity_creation_voucher_upper_bound": 500
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/LanguageClass.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "Language",
   "name": "Language",
   "description": "A language in which the content on the platform may be published",
   "description": "A language in which the content on the platform may be published",
-  "maximum_entities_count": 100,
-  "default_entity_creation_voucher_upper_bound": 50
+  "maximum_entities_count": 500,
+  "default_entity_creation_voucher_upper_bound": 500
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/LicenseClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "License",
   "name": "License",
   "description": "Describes a license the media can be published under",
   "description": "Describes a license the media can be published under",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/MediaLocationClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "MediaLocation",
   "name": "MediaLocation",
   "description": "An object describing how the related media object can be accessed",
   "description": "An object describing how the related media object can be accessed",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/UserDefinedLicenseClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "UserDefinedLicense",
   "name": "UserDefinedLicense",
   "description": "Custom license defined by the user",
   "description": "Custom license defined by the user",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/VideoClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "Video",
   "name": "Video",
   "description": "Describes a Video",
   "description": "Describes a Video",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 2 - 2
content-directory-schemas/inputs/classes/VideoMediaClass.json

@@ -1,7 +1,7 @@
 {
 {
   "name": "VideoMedia",
   "name": "VideoMedia",
   "description": "Describes a video media object",
   "description": "Describes a video media object",
-  "maximum_entities_count": 400,
-  "default_entity_creation_voucher_upper_bound": 50,
+  "maximum_entities_count": 5000,
+  "default_entity_creation_voucher_upper_bound": 100,
   "class_permissions": { "any_member": true }
   "class_permissions": { "any_member": true }
 }
 }

+ 0 - 13
content-directory-schemas/inputs/entityBatches/ChannelBatch.json

@@ -1,13 +0,0 @@
-{
-  "className": "Channel",
-  "entries": [
-    {
-      "title": "Joystream Cartoons",
-      "description": "Joystream Cartoons channel",
-      "language": { "existing": { "code": "EN" } },
-      "coverPhotoUrl": "https://user-images.githubusercontent.com/4144334/91547902-7e90db00-e91c-11ea-9f5c-45d4921928d5.png",
-      "avatarPhotoURL": "https://user-images.githubusercontent.com/4144334/91546674-ba2aa580-e91a-11ea-96e2-abc7654c0461.png",
-      "isPublic": true
-    }
-  ]
-}

+ 56 - 6
content-directory-schemas/inputs/entityBatches/KnownLicenseBatch.json

@@ -1,11 +1,61 @@
 {
 {
   "className": "KnownLicense",
   "className": "KnownLicense",
   "entries": [
   "entries": [
-    { "code": "CC_BY" },
-    { "code": "CC_BY_SA" },
-    { "code": "CC_BY_ND" },
-    { "code": "CC_BY_NC" },
-    { "code": "CC_BY_NC_SA" },
-    { "code": "CC_BY_NC_ND" }
+    {
+      "code": "PDM",
+      "name": "Public Domain",
+      "description": "For items which are not protected by copyright. This is not a license, but rather a copyright status. Some government-produced works, items with expired copyrights, and those which are ineligible for copyright protection may be included in this category.",
+      "url": "https://creativecommons.org/share-your-work/public-domain/pdm",
+      "attributionRequired": false
+    },
+    {
+      "code": "CC0",
+      "name": "Public Domain Dedication",
+      "description": "The CC0 (Public Domain Dedication) License allows creators to waive all rights to their creations and release them into the Public Domain.",
+      "url": "https://creativecommons.org/share-your-work/public-domain/cc0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY",
+      "name": "Creative Commons Attribution License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Read the License Deed for more information.",
+      "url": "https://creativecommons.org/licenses/by/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_SA",
+      "name": "Creative Commons Attribution-ShareAlike License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Any derivative works must be distributed under the same license. Read the License Deed for more information.",
+      "url": "https://creativecommons.org/licenses/by-sa/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_ND",
+      "name": "Creative Commons Attribution-NoDerivs License",
+      "description": "Sharing this content is permitted, but attribution must be provided. You may not remix, transform, or build upon the material. Read the License Deed for more information.",
+      "url": "https://creativecommons.org/licenses/by-nd/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_NC",
+      "name": "Creative Commons Attribution-NonCommercial License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Commercial use is not permitted. Read the License Deed for more information.",
+      "url": "https://creativecommons.org/licenses/by-nc/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_NC_SA",
+      "name": "Creative Commons Attribution-NonCommercial-ShareAlike License",
+      "description": "Sharing and adapting this content is permitted, but attribution must be provided. Any derivative works must be distributed under the same license. Commercial use is not permitted. Read the License Deed for more information.",
+      "url": "https://creativecommons.org/licenses/by-nc-sa/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_NC_ND",
+      "name": "Creative Commons Attribution-NonCommercial-NoDerivs License",
+      "description": "Sharing this content is permitted, but attribution must be provided. You may not remix, transform, or build upon the material. Commercial use is not permitted. Read the License Deed for more information.",
+      "url": "https://creativecommons.org/licenses/by-nc-nd/4.0",
+      "attributionRequired": true
+    }
   ]
   ]
 }
 }

+ 0 - 63
content-directory-schemas/inputs/entityBatches/VideoBatch.json

@@ -1,63 +0,0 @@
-{
-  "className": "Video",
-  "entries": [
-    {
-      "title": "Caminades 2",
-      "description": "Caminandes 2: Gran Dillama",
-      "language": { "existing": { "code": "EN" } },
-      "category": { "existing": { "name": "Film & Animation" } },
-      "channel": { "existing": { "title": "Joystream Cartoons" } },
-      "duration": 146,
-      "hasMarketing": false,
-      "isPublic": true,
-      "media": {
-        "new": {
-          "encoding": { "existing": { "name": "H.264_MP4" } },
-          "location": {
-            "new": {
-              "httpMediaLocation": {
-                "new": {
-                  "url": "http://www.caminandes.com/download/02_gran_dillama_1080p.zip"
-                }
-              }
-            }
-          },
-          "pixelWidth": 1920,
-          "pixelHeight": 1080
-        }
-      },
-      "thumbnailURL": "http://www.caminandes.com/wp-content/uploads/2016/02/web_header4.png",
-      "isExplicit": false,
-      "license": { "new": { "knownLicense": { "existing": { "code": "CC_BY" } } } }
-    },
-    {
-      "title": "Caminades 3",
-      "description": "Caminandes 3: Llamigos",
-      "language": { "existing": { "code": "EN" } },
-      "category": { "existing": { "name": "Film & Animation" } },
-      "channel": { "existing": { "title": "Joystream Cartoons" } },
-      "duration": 150,
-      "hasMarketing": false,
-      "isPublic": true,
-      "media": {
-        "new": {
-          "encoding": { "existing": { "name": "H.264_MP4" } },
-          "location": {
-            "new": {
-              "httpMediaLocation": {
-                "new": {
-                  "url": "http://www.caminandes.com/download/03_caminandes_llamigos_1080p.mp4"
-                }
-              }
-            }
-          },
-          "pixelWidth": 1920,
-          "pixelHeight": 1080
-        }
-      },
-      "thumbnailURL": "http://www.caminandes.com/wp-content/uploads/2016/02/web_header4.png",
-      "isExplicit": false,
-      "license": { "new": { "knownLicense": { "existing": { "code": "CC_BY" } } } }
-    }
-  ]
-}

+ 5 - 6
content-directory-schemas/inputs/schemas/ChannelSchema.json

@@ -2,8 +2,8 @@
   "className": "Channel",
   "className": "Channel",
   "newProperties": [
   "newProperties": [
     {
     {
-      "name": "title",
-      "description": "The title of the Channel",
+      "name": "handle",
+      "description": "The handle of the Channel",
       "required": true,
       "required": true,
       "unique": true,
       "unique": true,
       "property_type": { "Single": { "Text": 64 } }
       "property_type": { "Single": { "Text": 64 } }
@@ -17,13 +17,13 @@
     {
     {
       "name": "coverPhotoUrl",
       "name": "coverPhotoUrl",
       "description": "Url for Channel's cover (background) photo. Recommended ratio: 16:9.",
       "description": "Url for Channel's cover (background) photo. Recommended ratio: 16:9.",
-      "required": true,
+      "required": false,
       "property_type": { "Single": { "Text": 256 } }
       "property_type": { "Single": { "Text": 256 } }
     },
     },
     {
     {
-      "name": "avatarPhotoURL",
+      "name": "avatarPhotoUrl",
       "description": "Channel's avatar photo.",
       "description": "Channel's avatar photo.",
-      "required": true,
+      "required": false,
       "property_type": { "Single": { "Text": 256 } }
       "property_type": { "Single": { "Text": 256 } }
     },
     },
     {
     {
@@ -36,7 +36,6 @@
       "name": "isCensored",
       "name": "isCensored",
       "description": "Channel censorship status set by the Curator.",
       "description": "Channel censorship status set by the Curator.",
       "required": false,
       "required": false,
-      "unique": true,
       "property_type": { "Single": "Bool" },
       "property_type": { "Single": "Bool" },
       "locking_policy": { "is_locked_from_controller": true }
       "locking_policy": { "is_locked_from_controller": true }
     },
     },

+ 9 - 0
content-directory-schemas/inputs/schemas/KnownLicenseSchema.json

@@ -38,6 +38,15 @@
         "Single": { "Text": 256 }
         "Single": { "Text": 256 }
       },
       },
       "locking_policy": { "is_locked_from_controller": true }
       "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "attributionRequired",
+      "description": "Whether this license requires an attribution",
+      "required": false,
+      "property_type": {
+        "Single": "Bool"
+      },
+      "locking_policy": { "is_locked_from_controller": true }
     }
     }
   ]
   ]
 }
 }

+ 6 - 0
content-directory-schemas/inputs/schemas/LicenseSchema.json

@@ -12,6 +12,12 @@
       "description": "Reference to user-defined license",
       "description": "Reference to user-defined license",
       "required": false,
       "required": false,
       "property_type": { "Single": { "Reference": { "className": "UserDefinedLicense", "sameOwner": true } } }
       "property_type": { "Single": { "Reference": { "className": "UserDefinedLicense", "sameOwner": true } } }
+    },
+    {
+      "name": "attribution",
+      "description": "Attribution (if required by the license)",
+      "required": false,
+      "property_type": { "Single": { "Text": 512 } }
     }
     }
   ]
   ]
 }
 }

+ 2 - 3
content-directory-schemas/inputs/schemas/VideoSchema.json

@@ -38,7 +38,7 @@
       "property_type": { "Single": "Uint16" }
       "property_type": { "Single": "Uint16" }
     },
     },
     {
     {
-      "name": "thumbnailURL",
+      "name": "thumbnailUrl",
       "description": "Video thumbnail url (recommended ratio: 16:9)",
       "description": "Video thumbnail url (recommended ratio: 16:9)",
       "required": true,
       "required": true,
       "property_type": { "Single": { "Text": 256 } }
       "property_type": { "Single": { "Text": 256 } }
@@ -67,7 +67,7 @@
       "name": "publishedBeforeJoystream",
       "name": "publishedBeforeJoystream",
       "description": "If the Video was published on other platform before beeing published on Joystream - the original publication date",
       "description": "If the Video was published on other platform before beeing published on Joystream - the original publication date",
       "required": false,
       "required": false,
-      "property_type": { "Single": "Uint32" }
+      "property_type": { "Single": "Int32" }
     },
     },
     {
     {
       "name": "isPublic",
       "name": "isPublic",
@@ -92,7 +92,6 @@
       "name": "isCensored",
       "name": "isCensored",
       "description": "Video censorship status set by the Curator.",
       "description": "Video censorship status set by the Curator.",
       "required": false,
       "required": false,
-      "unique": true,
       "property_type": { "Single": "Bool" },
       "property_type": { "Single": "Bool" },
       "locking_policy": { "is_locked_from_controller": true }
       "locking_policy": { "is_locked_from_controller": true }
     }
     }

+ 3 - 3
content-directory-schemas/package.json

@@ -1,5 +1,5 @@
 {
 {
-  "name": "cd-schemas",
+  "name": "@joystream/cd-schemas",
   "version": "0.1.0",
   "version": "0.1.0",
   "description": "JSON schemas, inputs and related tooling for Joystream content directory 2.0",
   "description": "JSON schemas, inputs and related tooling for Joystream content directory 2.0",
   "author": "Joystream contributors",
   "author": "Joystream contributors",
@@ -19,9 +19,9 @@
     "initialize:dev": "yarn initialize:alice-as-lead && yarn initialize:content-dir",
     "initialize:dev": "yarn initialize:alice-as-lead && yarn initialize:content-dir",
     "example:createChannel": "ts-node ./examples/createChannel.ts",
     "example:createChannel": "ts-node ./examples/createChannel.ts",
     "example:createVideo": "ts-node ./examples/createVideo.ts",
     "example:createVideo": "ts-node ./examples/createVideo.ts",
-    "example:updateChannelTitle": "ts-node ./examples/updateChannelTitle.ts",
+    "example:updateChannelHandle": "ts-node ./examples/updateChannelHandle.ts",
     "example:createChannelWithoutTransaction": "ts-node ./examples/createChannelWithoutTransaction.ts",
     "example:createChannelWithoutTransaction": "ts-node ./examples/createChannelWithoutTransaction.ts",
-    "example:updateChannelTitlelWithoutTransaction": "ts-node ./examples/updateChannelTitleWithoutTransaction.ts"
+    "example:updateChannelHandlelWithoutTransaction": "ts-node ./examples/updateChannelHandleWithoutTransaction.ts"
   },
   },
   "dependencies": {
   "dependencies": {
     "ajv": "6.12.5",
     "ajv": "6.12.5",

+ 1 - 1
content-directory-schemas/src/helpers/InputParser.ts

@@ -194,7 +194,7 @@ export class InputParser {
         const schemaPropertyType = schema.newProperties.find((p) => p.name === propertyName)!.property_type
         const schemaPropertyType = schema.newProperties.find((p) => p.name === propertyName)!.property_type
         // Handle entities "nested" via "new"
         // Handle entities "nested" via "new"
         if (isSingle(schemaPropertyType) && isReference(schemaPropertyType.Single)) {
         if (isSingle(schemaPropertyType) && isReference(schemaPropertyType.Single)) {
-          if (Object.keys(propertyValue).includes('new')) {
+          if (propertyValue !== null && Object.keys(propertyValue).includes('new')) {
             const refEntitySchema = this.schemaByClassName(schemaPropertyType.Single.Reference.className)
             const refEntitySchema = this.schemaByClassName(schemaPropertyType.Single.Reference.className)
             this.includeEntityInputInUniqueQueryMap(propertyValue.new, refEntitySchema)
             this.includeEntityInputInUniqueQueryMap(propertyValue.new, refEntitySchema)
           }
           }

+ 25 - 15
docker-compose.yml

@@ -44,7 +44,7 @@ services:
       dockerfile: apps.Dockerfile
       dockerfile: apps.Dockerfile
     ports:
     ports:
       - '127.0.0.1:3001:3001'
       - '127.0.0.1:3001:3001'
-    command: colossus --dev --ws-provider ${WS_PROVIDER_ENDPOINT_URI} --ipfs-host ipfs
+    command: colossus --dev --ws-provider ws://joystream-node:9944 --ipfs-host ipfs
     environment:
     environment:
       - DEBUG=*
       - DEBUG=*
 
 
@@ -55,10 +55,13 @@ services:
       - "127.0.0.1:${DB_PORT}:5432"
       - "127.0.0.1:${DB_PORT}:5432"
     volumes:
     volumes:
       - /var/lib/postgresql/data
       - /var/lib/postgresql/data
+    env_file:
+      # relative to working directory where docker-compose was run from
+      - .env
     environment:
     environment:
       POSTGRES_USER: ${DB_USER}
       POSTGRES_USER: ${DB_USER}
       POSTGRES_PASSWORD: ${DB_PASS}
       POSTGRES_PASSWORD: ${DB_PASS}
-      POSTGRES_DB: ${DB_NAME}
+      POSTGRES_DB: ${INDEXER_DB_NAME}
 
 
   graphql-server:
   graphql-server:
     image: joystream/apps
     image: joystream/apps
@@ -67,10 +70,11 @@ services:
       context: .
       context: .
       dockerfile: apps.Dockerfile
       dockerfile: apps.Dockerfile
     env_file:
     env_file:
-      # relative to working directory where docker-compose was run from 
+      # relative to working directory where docker-compose was run from
       - .env
       - .env
     environment:
     environment:
       - DB_HOST=db
       - DB_HOST=db
+      - DB_NAME=${PROCESSOR_DB_NAME}
     ports:
     ports:
       - "127.0.0.1:8081:${GRAPHQL_SERVER_PORT}"
       - "127.0.0.1:8081:${GRAPHQL_SERVER_PORT}"
     depends_on: 
     depends_on: 
@@ -84,14 +88,14 @@ services:
       context: .
       context: .
       dockerfile: apps.Dockerfile
       dockerfile: apps.Dockerfile
     env_file:
     env_file:
-      # relative to working directory where docker-compose was run from 
+      # relative to working directory where docker-compose was run from
       - .env
       - .env
     environment:
     environment:
-      - INDEXER_ENDPOINT_URL=http://indexer-api-gateway:4000/graphql
-      - DB_HOST=db
+      - INDEXER_ENDPOINT_URL=http://indexer-api-gateway:${WARTHOG_APP_PORT}/graphql
       - TYPEORM_HOST=db
       - TYPEORM_HOST=db
+      - TYPEORM_DATABASE=${PROCESSOR_DB_NAME}
       - DEBUG=index-builder:*
       - DEBUG=index-builder:*
-      - WS_PROVIDER_ENDPOINT_URI=${WS_PROVIDER_ENDPOINT_URI}
+      - WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944
     depends_on:
     depends_on:
       - indexer-api-gateway
       - indexer-api-gateway
     command: ["workspace", "query-node-root", "processor:start"]
     command: ["workspace", "query-node-root", "processor:start"]
@@ -103,15 +107,16 @@ services:
       context: .
       context: .
       dockerfile: apps.Dockerfile
       dockerfile: apps.Dockerfile
     env_file:
     env_file:
-      # relative to working directory where docker-compose was run from 
-      - .env 
+      # relative to working directory where docker-compose was run from
+      - .env
     environment:
     environment:
       - TYPEORM_HOST=db
       - TYPEORM_HOST=db
+      - TYPEORM_DATABASE=${INDEXER_DB_NAME}
       - INDEXER_WORKERS=5
       - INDEXER_WORKERS=5
       - PROCESSOR_POLL_INTERVAL=1000 # refresh every second 
       - PROCESSOR_POLL_INTERVAL=1000 # refresh every second 
       - REDIS_URI=redis://redis:6379/0
       - REDIS_URI=redis://redis:6379/0
       - DEBUG=index-builder:*
       - DEBUG=index-builder:*
-      - WS_PROVIDER_ENDPOINT_URI=${WS_PROVIDER_ENDPOINT_URI}
+      - WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944
     depends_on: 
     depends_on: 
       - db
       - db
     command: ["workspace", "query-node-root", "indexer:start"] 
     command: ["workspace", "query-node-root", "indexer:start"] 
@@ -119,16 +124,21 @@ services:
   indexer-api-gateway:
   indexer-api-gateway:
     image: joystream/hydra-indexer-gateway:latest
     image: joystream/hydra-indexer-gateway:latest
     restart: unless-stopped
     restart: unless-stopped
+    env_file:
+      # relative to working directory where docker-compose was run from
+      - .env
     environment:
     environment:
-      - WARTHOG_STARTER_DB_DATABASE=${DB_NAME}
-      - WARTHOG_STARTER_DB_HOST=db 
+      - WARTHOG_STARTER_DB_DATABASE=${INDEXER_DB_NAME}
+      - WARTHOG_STARTER_DB_HOST=db
       - WARTHOG_STARTER_DB_PASSWORD=${DB_PASS}
       - WARTHOG_STARTER_DB_PASSWORD=${DB_PASS}
       - WARTHOG_STARTER_DB_PORT=${DB_PORT}
       - WARTHOG_STARTER_DB_PORT=${DB_PORT}
       - WARTHOG_STARTER_DB_USERNAME=${DB_USER}
       - WARTHOG_STARTER_DB_USERNAME=${DB_USER}
-      - WARTHOG_STARTER_REDIS_URI=redis://redis:6379/0 
-      - PORT=4000
+      - WARTHOG_STARTER_REDIS_URI=redis://redis:6379/0
+      - WARTHOG_APP_PORT=${WARTHOG_APP_PORT}
+      - PORT=${WARTHOG_APP_PORT}
+      - DEBUG=*
     ports:
     ports:
-      - "127.0.0.1:4000:4000"
+      - "127.0.0.1:4000:4002"
     depends_on:
     depends_on:
       - redis
       - redis
       - db
       - db

+ 1 - 1
node/Cargo.toml

@@ -3,7 +3,7 @@ authors = ['Joystream contributors']
 build = 'build.rs'
 build = 'build.rs'
 edition = '2018'
 edition = '2018'
 name = 'joystream-node'
 name = 'joystream-node'
-version = '3.4.1'
+version = '3.5.0'
 default-run = "joystream-node"
 default-run = "joystream-node"
 
 
 [[bin]]
 [[bin]]

+ 2 - 2
package.json

@@ -4,7 +4,7 @@
   "version": "1.0.0",
   "version": "1.0.0",
   "license": "GPL-3.0-only",
   "license": "GPL-3.0-only",
   "scripts": {
   "scripts": {
-    "postinstall": "yarn workspace @joystream/types build && yarn workspace cd-schemas generate:all && yarn workspace cd-schemas build && yarn workspace @joystream/cli build",
+    "postinstall": "yarn workspace @joystream/types build && yarn workspace @joystream/cd-schemas generate:all && yarn workspace @joystream/cd-schemas build && yarn workspace @joystream/cli build",
     "build": "./build.sh",
     "build": "./build.sh",
     "start": "./start.sh",
     "start": "./start.sh",
     "cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
     "cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
@@ -36,7 +36,7 @@
     "babel-core": "^7.0.0-bridge.0",
     "babel-core": "^7.0.0-bridge.0",
     "typescript": "^3.9.7",
     "typescript": "^3.9.7",
     "bn.js": "^5.1.2",
     "bn.js": "^5.1.2",
-    "@dzlzv/hydra-indexer-lib": "0.0.19-legacy.1.26.1"
+    "@dzlzv/hydra-indexer-lib": "0.0.21-legacy.1.26.1"
   },
   },
   "devDependencies": {
   "devDependencies": {
     "eslint": "^7.6.0",
     "eslint": "^7.6.0",

+ 0 - 62
query-node/.env

@@ -1,62 +0,0 @@
-COMPOSE_PROJECT_NAME=joystream
-
-# 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
-# this env var is deprecated!
-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=4002
-GRAPHQL_SERVER_HOST=localhost
-WARTHOG_APP_PORT=4002
-WARTHOG_APP_HOST=localhost

+ 5 - 1
query-node/README.md

@@ -11,6 +11,10 @@ $ cd query-node
 $ yarn build
 $ yarn build
 ```
 ```
 
 
+## Starting services
+
+To start services defined in the project docker-compose.yml, you should run docker-compose from the project root folder to use the correct .env file
+
 ## Run mapping processor
 ## Run mapping processor
 
 
 Before running mappings make sure indexer(`yarn indexer:start`) and indexer-api-server (mappings get the chain data from this graphql server) are both running:
 Before running mappings make sure indexer(`yarn indexer:start`) and indexer-api-server (mappings get the chain data from this graphql server) are both running:
@@ -26,7 +30,7 @@ Once processor start to store event data you will be able to query this data fro
 ```graphql
 ```graphql
 query {
 query {
   channels {
   channels {
-    title
+    handle
   }
   }
 }
 }
 ```
 ```

+ 19 - 1
query-node/build.sh

@@ -4,8 +4,26 @@ set -e
 SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
 SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
 cd $SCRIPT_PATH
 cd $SCRIPT_PATH
 
 
+# Load and export variables from root .env file into shell environment
+set -a
+. ../.env
+set +a
+
 yarn clean
 yarn clean
-yarn codegen:all
+
+# We generate the code for each service separately to be able to specify
+# separate database names.
+
+# Build indexer customizing DB name
+DB_NAME=${INDEXER_DB_NAME} yarn codegen:indexer
+
+# Build graphql-server customizing DB name
+DB_NAME=${PROCESSOR_DB_NAME} yarn codegen:server
+
+# We run yarn again to ensure processor and indexer dependencies are installed
+# and are inline with root workspace resolutions
 yarn
 yarn
+
 ln -s ../../../../../node_modules/typeorm/cli.js generated/graphql-server/node_modules/.bin/typeorm || :
 ln -s ../../../../../node_modules/typeorm/cli.js generated/graphql-server/node_modules/.bin/typeorm || :
+
 yarn tsc --build tsconfig.json
 yarn tsc --build tsconfig.json

+ 13 - 0
query-node/db-migrate.sh

@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+cd $SCRIPT_PATH
+
+set -a
+. ../.env
+set +a
+
+yarn workspace query-node-root db:indexer:migrate
+yarn workspace query-node-root db:schema:migrate
+TYPEORM_DATABASE=${PROCESSOR_DB_NAME} yarn workspace query-node-root db:indexer:migrate

+ 14 - 7
query-node/mappings/content-directory/content-dir-consts.ts

@@ -1,4 +1,4 @@
-import { IPropertyWithId } from '../types'
+import { IKnownClass, IPropertyWithId } from '../types'
 
 
 // Content directory predefined class names
 // Content directory predefined class names
 export enum ContentDirectoryKnownClasses {
 export enum ContentDirectoryKnownClasses {
@@ -14,10 +14,11 @@ export enum ContentDirectoryKnownClasses {
   VIDEO = 'Video',
   VIDEO = 'Video',
   VIDEOMEDIA = 'VideoMedia',
   VIDEOMEDIA = 'VideoMedia',
   VIDEOMEDIAENCODING = 'VideoMediaEncoding',
   VIDEOMEDIAENCODING = 'VideoMediaEncoding',
+  FEATUREDVIDEOS = 'FeaturedVideo',
 }
 }
 
 
 // Predefined content-directory classes, classId may change after the runtime seeding
 // Predefined content-directory classes, classId may change after the runtime seeding
-export const contentDirectoryClassNamesWithId: { classId: number; name: string }[] = [
+export const contentDirectoryClassNamesWithId: IKnownClass[] = [
   { name: ContentDirectoryKnownClasses.CHANNEL, classId: 1 },
   { name: ContentDirectoryKnownClasses.CHANNEL, classId: 1 },
   { name: ContentDirectoryKnownClasses.CATEGORY, classId: 2 },
   { name: ContentDirectoryKnownClasses.CATEGORY, classId: 2 },
   { name: ContentDirectoryKnownClasses.HTTPMEDIALOCATION, classId: 3 },
   { name: ContentDirectoryKnownClasses.HTTPMEDIALOCATION, classId: 3 },
@@ -30,6 +31,7 @@ export const contentDirectoryClassNamesWithId: { classId: number; name: string }
   { name: ContentDirectoryKnownClasses.VIDEO, classId: 10 },
   { name: ContentDirectoryKnownClasses.VIDEO, classId: 10 },
   { name: ContentDirectoryKnownClasses.VIDEOMEDIA, classId: 11 },
   { name: ContentDirectoryKnownClasses.VIDEOMEDIA, classId: 11 },
   { name: ContentDirectoryKnownClasses.VIDEOMEDIAENCODING, classId: 12 },
   { name: ContentDirectoryKnownClasses.VIDEOMEDIAENCODING, classId: 12 },
+  { name: ContentDirectoryKnownClasses.FEATUREDVIDEOS, classId: 13 },
 ]
 ]
 
 
 export const categoryPropertyNamesWithId: IPropertyWithId = {
 export const categoryPropertyNamesWithId: IPropertyWithId = {
@@ -38,18 +40,19 @@ export const categoryPropertyNamesWithId: IPropertyWithId = {
 }
 }
 
 
 export const channelPropertyNamesWithId: IPropertyWithId = {
 export const channelPropertyNamesWithId: IPropertyWithId = {
-  0: { name: 'title', type: 'string', required: false },
+  0: { name: 'handle', type: 'string', required: true },
   1: { name: 'description', type: 'string', required: false },
   1: { name: 'description', type: 'string', required: false },
-  2: { name: 'coverPhotoURL', type: 'string', required: false },
-  3: { name: 'avatarPhotoURL', type: 'string', required: false },
+  2: { name: 'coverPhotoUrl', type: 'string', required: false },
+  3: { name: 'avatarPhotoUrl', type: 'string', required: false },
   4: { name: 'isPublic', type: 'boolean', required: true },
   4: { name: 'isPublic', type: 'boolean', required: true },
   5: { name: 'isCurated', type: 'boolean', required: false },
   5: { name: 'isCurated', type: 'boolean', required: false },
-  6: { name: 'language', type: 'number', required: true },
+  6: { name: 'language', type: 'number', required: false },
 }
 }
 
 
 export const licensePropertyNamesWithId: IPropertyWithId = {
 export const licensePropertyNamesWithId: IPropertyWithId = {
   0: { name: 'knownLicense', type: 'number', required: false },
   0: { name: 'knownLicense', type: 'number', required: false },
   1: { name: 'userDefinedLicense', type: 'number', required: false },
   1: { name: 'userDefinedLicense', type: 'number', required: false },
+  2: { name: 'attribution', type: 'string', required: false },
 }
 }
 
 
 export const knownLicensePropertyNamesWIthId: IPropertyWithId = {
 export const knownLicensePropertyNamesWIthId: IPropertyWithId = {
@@ -103,7 +106,7 @@ export const videoPropertyNamesWithId: IPropertyWithId = {
   3: { name: 'description', type: 'string', required: false },
   3: { name: 'description', type: 'string', required: false },
   4: { name: 'duration', type: 'number', required: true },
   4: { name: 'duration', type: 'number', required: true },
   5: { name: 'skippableIntroDuration', type: 'number', required: false },
   5: { name: 'skippableIntroDuration', type: 'number', required: false },
-  6: { name: 'thumbnailURL', type: 'string', required: true },
+  6: { name: 'thumbnailUrl', type: 'string', required: true },
   7: { name: 'language', type: 'number', required: false },
   7: { name: 'language', type: 'number', required: false },
   // referenced entity's id
   // referenced entity's id
   8: { name: 'media', type: 'number', required: true },
   8: { name: 'media', type: 'number', required: true },
@@ -114,3 +117,7 @@ export const videoPropertyNamesWithId: IPropertyWithId = {
   13: { name: 'license', type: 'number', required: true },
   13: { name: 'license', type: 'number', required: true },
   14: { name: 'isCurated', type: 'boolean', required: true },
   14: { name: 'isCurated', type: 'boolean', required: true },
 }
 }
+
+export const featuredVideoPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'video', type: 'number', required: true },
+}

+ 13 - 4
query-node/mappings/content-directory/decode.ts

@@ -10,8 +10,13 @@ import {
 } from '../types'
 } from '../types'
 import Debug from 'debug'
 import Debug from 'debug'
 
 
-import { ParametrizedClassPropertyValue, UpdatePropertyValuesOperation } from '@joystream/types/content-directory'
+import {
+  OperationType,
+  ParametrizedClassPropertyValue,
+  UpdatePropertyValuesOperation,
+} from '@joystream/types/content-directory'
 import { createType } from '@joystream/types'
 import { createType } from '@joystream/types'
+import { Vec } from '@polkadot/types'
 
 
 const debug = Debug('mappings:cd:decode')
 const debug = Debug('mappings:cd:decode')
 
 
@@ -108,9 +113,12 @@ function getEntityProperties(propertyValues: ParametrizedClassPropertyValue[]):
   return properties
   return properties
 }
 }
 
 
-function getOperations({ extrinsic }: SubstrateEvent): IBatchOperation {
-  const operations = createType('Vec<OperationType>', extrinsic!.args[1].value as any)
+function getOperations(event: SubstrateEvent): Vec<OperationType> {
+  if (!event.extrinsic) throw Error(`No extrinsic found for ${event.id}`)
+  return createType('Vec<OperationType>', (event.extrinsic.args[1].value as unknown) as Vec<OperationType>)
+}
 
 
+function getOperationsByTypes(operations: OperationType[]): IBatchOperation {
   const updatePropertyValuesOperations: IEntity[] = []
   const updatePropertyValuesOperations: IEntity[] = []
   const addSchemaSupportToEntityOperations: IEntity[] = []
   const addSchemaSupportToEntityOperations: IEntity[] = []
   const createEntityOperations: ICreateEntityOperation[] = []
   const createEntityOperations: ICreateEntityOperation[] = []
@@ -160,6 +168,7 @@ export const decode = {
   getClassEntity,
   getClassEntity,
   setEntityPropertyValues,
   setEntityPropertyValues,
   getEntityProperties,
   getEntityProperties,
-  getOperations,
+  getOperationsByTypes,
   setProperties,
   setProperties,
+  getOperations,
 }
 }

+ 105 - 46
query-node/mappings/content-directory/entity/create.ts

@@ -1,18 +1,19 @@
 import { DB } from '../../../generated/indexer'
 import { DB } from '../../../generated/indexer'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
 import { Category } from '../../../generated/graphql-server/src/modules/category/category.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 { KnownLicenseEntity } from '../../../generated/graphql-server/src/modules/known-license-entity/known-license-entity.model'
+import { UserDefinedLicenseEntity } from '../../../generated/graphql-server/src/modules/user-defined-license-entity/user-defined-license-entity.model'
+import { JoystreamMediaLocationEntity } from '../../../generated/graphql-server/src/modules/joystream-media-location-entity/joystream-media-location-entity.model'
+import { HttpMediaLocationEntity } from '../../../generated/graphql-server/src/modules/http-media-location-entity/http-media-location-entity.model'
 import { VideoMedia } from '../../../generated/graphql-server/src/modules/video-media/video-media.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 { Video } from '../../../generated/graphql-server/src/modules/video/video.model'
 import { Block, Network } from '../../../generated/graphql-server/src/modules/block/block.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 { 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 { 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 { ClassEntity } from '../../../generated/graphql-server/src/modules/class-entity/class-entity.model'
-import { License } from '../../../generated/graphql-server/src/modules/license/license.model'
-import { MediaLocation } from '../../../generated/graphql-server/src/modules/media-location/media-location.model'
+import { LicenseEntity } from '../../../generated/graphql-server/src/modules/license-entity/license-entity.model'
+import { MediaLocationEntity } from '../../../generated/graphql-server/src/modules/media-location-entity/media-location-entity.model'
+import { FeaturedVideo } from '../../../generated/graphql-server/src/modules/featured-video/featured-video.model'
 
 
 import { contentDirectoryClassNamesWithId } from '../content-dir-consts'
 import { contentDirectoryClassNamesWithId } from '../content-dir-consts'
 import {
 import {
@@ -22,6 +23,7 @@ import {
   ICreateEntityOperation,
   ICreateEntityOperation,
   IDBBlockId,
   IDBBlockId,
   IEntity,
   IEntity,
+  IFeaturedVideo,
   IHttpMediaLocation,
   IHttpMediaLocation,
   IJoystreamMediaLocation,
   IJoystreamMediaLocation,
   IKnownLicense,
   IKnownLicense,
@@ -35,6 +37,12 @@ import {
 } from '../../types'
 } from '../../types'
 import { getOrCreate } from '../get-or-create'
 import { getOrCreate } from '../get-or-create'
 import BN from 'bn.js'
 import BN from 'bn.js'
+import {
+  HttpMediaLocation,
+  JoystreamMediaLocation,
+  KnownLicense,
+  UserDefinedLicense,
+} from '../../../generated/graphql-server/src/modules/variants/variants.model'
 
 
 async function createBlockOrGetFromDatabase(db: DB, blockNumber: number): Promise<Block> {
 async function createBlockOrGetFromDatabase(db: DB, blockNumber: number): Promise<Block> {
   let b = await db.get(Block, { where: { block: blockNumber } })
   let b = await db.get(Block, { where: { block: blockNumber } })
@@ -59,16 +67,16 @@ async function createChannel(
 
 
   channel.version = block
   channel.version = block
   channel.id = id
   channel.id = id
-  channel.title = p.title
+  channel.handle = p.handle
   channel.description = p.description
   channel.description = p.description
-  channel.isCurated = p.isCurated || false
+  channel.isCurated = !!p.isCurated
   channel.isPublic = p.isPublic
   channel.isPublic = p.isPublic
-  channel.coverPhotoUrl = p.coverPhotoURL
-  channel.avatarPhotoUrl = p.avatarPhotoURL
+  channel.coverPhotoUrl = p.coverPhotoUrl
+  channel.avatarPhotoUrl = p.avatarPhotoUrl
 
 
   channel.happenedIn = await createBlockOrGetFromDatabase(db, block)
   channel.happenedIn = await createBlockOrGetFromDatabase(db, block)
   const { language } = p
   const { language } = p
-  if (language !== undefined) {
+  if (language) {
     channel.language = await getOrCreate.language(
     channel.language = await getOrCreate.language(
       { db, block, id },
       { db, block, id },
       classEntityMap,
       classEntityMap,
@@ -95,11 +103,11 @@ async function createCategory({ db, block, id }: IDBBlockId, p: ICategory): Prom
   return category
   return category
 }
 }
 
 
-async function createKnownLicense({ db, block, id }: IDBBlockId, p: IKnownLicense): Promise<KnownLicense> {
-  const record = await db.get(KnownLicense, { where: { id } })
+async function createKnownLicense({ db, block, id }: IDBBlockId, p: IKnownLicense): Promise<KnownLicenseEntity> {
+  const record = await db.get(KnownLicenseEntity, { where: { id } })
   if (record) return record
   if (record) return record
 
 
-  const knownLicence = new KnownLicense()
+  const knownLicence = new KnownLicenseEntity()
 
 
   knownLicence.id = id
   knownLicence.id = id
   knownLicence.code = p.code
   knownLicence.code = p.code
@@ -115,28 +123,28 @@ async function createKnownLicense({ db, block, id }: IDBBlockId, p: IKnownLicens
 async function createUserDefinedLicense(
 async function createUserDefinedLicense(
   { db, block, id }: IDBBlockId,
   { db, block, id }: IDBBlockId,
   p: IUserDefinedLicense
   p: IUserDefinedLicense
-): Promise<UserDefinedLicense> {
-  const record = await db.get(UserDefinedLicense, { where: { id } })
+): Promise<UserDefinedLicenseEntity> {
+  const record = await db.get(UserDefinedLicenseEntity, { where: { id } })
   if (record) return record
   if (record) return record
 
 
-  const userDefinedLicense = new UserDefinedLicense()
+  const userDefinedLicense = new UserDefinedLicenseEntity()
 
 
   userDefinedLicense.id = id
   userDefinedLicense.id = id
   userDefinedLicense.content = p.content
   userDefinedLicense.content = p.content
   userDefinedLicense.version = block
   userDefinedLicense.version = block
   userDefinedLicense.happenedIn = await createBlockOrGetFromDatabase(db, block)
   userDefinedLicense.happenedIn = await createBlockOrGetFromDatabase(db, block)
-  await db.save<UserDefinedLicense>(userDefinedLicense)
+  await db.save<UserDefinedLicenseEntity>(userDefinedLicense)
   return userDefinedLicense
   return userDefinedLicense
 }
 }
 
 
 async function createJoystreamMediaLocation(
 async function createJoystreamMediaLocation(
   { db, block, id }: IDBBlockId,
   { db, block, id }: IDBBlockId,
   p: IJoystreamMediaLocation
   p: IJoystreamMediaLocation
-): Promise<JoystreamMediaLocation> {
-  const record = await db.get(JoystreamMediaLocation, { where: { id } })
+): Promise<JoystreamMediaLocationEntity> {
+  const record = await db.get(JoystreamMediaLocationEntity, { where: { id } })
   if (record) return record
   if (record) return record
 
 
-  const joyMediaLoc = new JoystreamMediaLocation()
+  const joyMediaLoc = new JoystreamMediaLocationEntity()
 
 
   joyMediaLoc.id = id
   joyMediaLoc.id = id
   joyMediaLoc.dataObjectId = p.dataObjectId
   joyMediaLoc.dataObjectId = p.dataObjectId
@@ -149,11 +157,11 @@ async function createJoystreamMediaLocation(
 async function createHttpMediaLocation(
 async function createHttpMediaLocation(
   { db, block, id }: IDBBlockId,
   { db, block, id }: IDBBlockId,
   p: IHttpMediaLocation
   p: IHttpMediaLocation
-): Promise<HttpMediaLocation> {
-  const record = await db.get(HttpMediaLocation, { where: { id } })
+): Promise<HttpMediaLocationEntity> {
+  const record = await db.get(HttpMediaLocationEntity, { where: { id } })
   if (record) return record
   if (record) return record
 
 
-  const httpMediaLoc = new HttpMediaLocation()
+  const httpMediaLoc = new HttpMediaLocationEntity()
 
 
   httpMediaLoc.id = id
   httpMediaLoc.id = id
   httpMediaLoc.url = p.url
   httpMediaLoc.url = p.url
@@ -187,16 +195,29 @@ async function createVideoMedia(
     )
     )
   }
   }
   if (location !== undefined) {
   if (location !== undefined) {
-    videoMedia.location = await getOrCreate.mediaLocation(
+    const m = await getOrCreate.mediaLocation(
       { db, block, id },
       { db, block, id },
       classEntityMap,
       classEntityMap,
       location,
       location,
       nextEntityIdBeforeTransaction
       nextEntityIdBeforeTransaction
     )
     )
+    videoMedia.locationEntity = m
+    const { httpMediaLocation, joystreamMediaLocation } = m
+    if (httpMediaLocation) {
+      const mediaLoc = new HttpMediaLocation()
+      mediaLoc.port = httpMediaLocation.port
+      mediaLoc.url = httpMediaLocation.url
+      videoMedia.location = mediaLoc
+    }
+    if (joystreamMediaLocation) {
+      const mediaLoc = new JoystreamMediaLocation()
+      mediaLoc.dataObjectId = joystreamMediaLocation.dataObjectId
+      videoMedia.location = mediaLoc
+    }
   }
   }
 
 
   videoMedia.happenedIn = await createBlockOrGetFromDatabase(db, block)
   videoMedia.happenedIn = await createBlockOrGetFromDatabase(db, block)
-  await db.save(videoMedia)
+  await db.save<VideoMedia>(videoMedia)
   return videoMedia
   return videoMedia
 }
 }
 
 
@@ -216,14 +237,14 @@ async function createVideo(
   video.description = p.description
   video.description = p.description
   video.duration = p.duration
   video.duration = p.duration
   video.hasMarketing = p.hasMarketing
   video.hasMarketing = p.hasMarketing
-  // TODO: needs to be handled correctly, from runtime CurationStatus is coming
-  video.isCurated = p.isCurated || true
+  video.isCurated = !!p.isCurated
   video.isExplicit = p.isExplicit
   video.isExplicit = p.isExplicit
   video.isPublic = p.isPublic
   video.isPublic = p.isPublic
   video.publishedBeforeJoystream = p.publishedBeforeJoystream
   video.publishedBeforeJoystream = p.publishedBeforeJoystream
   video.skippableIntroDuration = p.skippableIntroDuration
   video.skippableIntroDuration = p.skippableIntroDuration
-  video.thumbnailUrl = p.thumbnailURL
+  video.thumbnailUrl = p.thumbnailUrl
   video.version = block
   video.version = block
+  video.isFeatured = false
 
 
   const { language, license, category, channel, media } = p
   const { language, license, category, channel, media } = p
   if (language !== undefined) {
   if (language !== undefined) {
@@ -234,8 +255,9 @@ async function createVideo(
       nextEntityIdBeforeTransaction
       nextEntityIdBeforeTransaction
     )
     )
   }
   }
-  if (license !== undefined) {
-    video.license = await getOrCreate.license({ db, block, id }, classEntityMap, license, nextEntityIdBeforeTransaction)
+  if (license) {
+    const lic = await getOrCreate.license({ db, block, id }, classEntityMap, license, nextEntityIdBeforeTransaction)
+    video.license = lic
   }
   }
   if (category !== undefined) {
   if (category !== undefined) {
     video.category = await getOrCreate.category(
     video.category = await getOrCreate.category(
@@ -293,32 +315,45 @@ async function createLicense(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   p: ILicense,
   p: ILicense,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<License> {
-  const record = await db.get(License, { where: { id } })
+): Promise<LicenseEntity> {
+  const record = await db.get(LicenseEntity, { where: { id } })
   if (record) return record
   if (record) return record
 
 
-  const { knownLicense, userDefinedLicense } = p
+  const license = new LicenseEntity()
 
 
-  const license = new License()
-  license.id = id
-  if (knownLicense !== undefined) {
-    license.knownLicense = await getOrCreate.knownLicense(
+  if (p.knownLicense) {
+    const kLicense = await getOrCreate.knownLicense(
       { db, block, id },
       { db, block, id },
       classEntityMap,
       classEntityMap,
-      knownLicense,
+      p.knownLicense,
       nextEntityIdBeforeTransaction
       nextEntityIdBeforeTransaction
     )
     )
+    const k = new KnownLicense()
+    k.code = kLicense.code
+    k.description = kLicense.description
+    k.name = kLicense.name
+    k.url = kLicense.url
+    // Set the license type
+    license.type = k
   }
   }
-  if (userDefinedLicense !== undefined) {
-    license.userdefinedLicense = await getOrCreate.userDefinedLicense(
+  if (p.userDefinedLicense) {
+    const { content } = await getOrCreate.userDefinedLicense(
       { db, block, id },
       { db, block, id },
       classEntityMap,
       classEntityMap,
-      userDefinedLicense,
+      p.userDefinedLicense,
       nextEntityIdBeforeTransaction
       nextEntityIdBeforeTransaction
     )
     )
+    const u = new UserDefinedLicense()
+    u.content = content
+    // Set the license type
+    license.type = u
   }
   }
+
+  license.id = id
+  license.attribution = p.attribution
   license.happenedIn = await createBlockOrGetFromDatabase(db, block)
   license.happenedIn = await createBlockOrGetFromDatabase(db, block)
-  await db.save<License>(license)
+
+  await db.save<LicenseEntity>(license)
   return license
   return license
 }
 }
 
 
@@ -327,10 +362,10 @@ async function createMediaLocation(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   p: IMediaLocation,
   p: IMediaLocation,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<MediaLocation> {
+): Promise<MediaLocationEntity> {
   const { httpMediaLocation, joystreamMediaLocation } = p
   const { httpMediaLocation, joystreamMediaLocation } = p
 
 
-  const location = new MediaLocation()
+  const location = new MediaLocationEntity()
   location.id = id
   location.id = id
   if (httpMediaLocation !== undefined) {
   if (httpMediaLocation !== undefined) {
     location.httpMediaLocation = await getOrCreate.httpMediaLocation(
     location.httpMediaLocation = await getOrCreate.httpMediaLocation(
@@ -349,10 +384,33 @@ async function createMediaLocation(
     )
     )
   }
   }
   location.happenedIn = await createBlockOrGetFromDatabase(db, block)
   location.happenedIn = await createBlockOrGetFromDatabase(db, block)
-  await db.save<License>(location)
+  await db.save<MediaLocationEntity>(location)
   return location
   return location
 }
 }
 
 
+async function createFeaturedVideo(
+  { db, block, id }: IDBBlockId,
+  classEntityMap: ClassEntityMap,
+  p: IFeaturedVideo,
+  nextEntityIdBeforeTransaction: number
+): Promise<void> {
+  const featuredVideo = new FeaturedVideo()
+
+  featuredVideo.video = await getOrCreate.video(
+    { db, block, id },
+    classEntityMap,
+    p.video!,
+    nextEntityIdBeforeTransaction
+  )
+
+  featuredVideo.id = id
+  featuredVideo.version = block
+  featuredVideo.video.isFeatured = true
+
+  await db.save<Video>(featuredVideo.video)
+  await db.save<FeaturedVideo>(featuredVideo)
+}
+
 async function getClassName(
 async function getClassName(
   db: DB,
   db: DB,
   entity: IEntity,
   entity: IEntity,
@@ -394,4 +452,5 @@ export {
   createMediaLocation,
   createMediaLocation,
   createBlockOrGetFromDatabase,
   createBlockOrGetFromDatabase,
   getClassName,
   getClassName,
+  createFeaturedVideo,
 }
 }

+ 41 - 37
query-node/mappings/content-directory/entity/index.ts

@@ -16,6 +16,7 @@ import {
   updateVideoMediaEncodingEntityPropertyValues,
   updateVideoMediaEncodingEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
+  updateFeaturedVideoEntityPropertyValues,
 } from './update'
 } from './update'
 import {
 import {
   removeCategory,
   removeCategory,
@@ -30,6 +31,7 @@ import {
   removeVideoMediaEncoding,
   removeVideoMediaEncoding,
   removeLicense,
   removeLicense,
   removeMediaLocation,
   removeMediaLocation,
+  removeFeaturedVideo,
 } from './remove'
 } from './remove'
 import {
 import {
   createCategory,
   createCategory,
@@ -43,6 +45,7 @@ import {
   createLanguage,
   createLanguage,
   createVideoMediaEncoding,
   createVideoMediaEncoding,
   createBlockOrGetFromDatabase,
   createBlockOrGetFromDatabase,
+  createFeaturedVideo,
 } from './create'
 } from './create'
 import {
 import {
   categoryPropertyNamesWithId,
   categoryPropertyNamesWithId,
@@ -54,8 +57,8 @@ import {
   userDefinedLicensePropertyNamesWithId,
   userDefinedLicensePropertyNamesWithId,
   videoMediaEncodingPropertyNamesWithId,
   videoMediaEncodingPropertyNamesWithId,
   videoPropertyNamesWithId,
   videoPropertyNamesWithId,
-  contentDirectoryClassNamesWithId,
   ContentDirectoryKnownClasses,
   ContentDirectoryKnownClasses,
+  featuredVideoPropertyNamesWithId,
 } from '../content-dir-consts'
 } from '../content-dir-consts'
 
 
 import {
 import {
@@ -74,8 +77,9 @@ import {
   IEntity,
   IEntity,
   ILicense,
   ILicense,
   IMediaLocation,
   IMediaLocation,
+  IFeaturedVideo,
 } from '../../types'
 } from '../../types'
-import { getOrCreate } from '../get-or-create'
+import { getOrCreate, getKnownClass } from '../get-or-create'
 
 
 const debug = Debug('mappings:content-directory')
 const debug = Debug('mappings:content-directory')
 
 
@@ -86,22 +90,13 @@ async function contentDirectory_EntitySchemaSupportAdded(db: DB, event: Substrat
 
 
   const { blockNumber: block } = event
   const { blockNumber: block } = event
   const entityId = decode.stringIfyEntityId(event)
   const entityId = decode.stringIfyEntityId(event)
-  const classEntity = await db.get(ClassEntity, { where: { id: entityId } })
 
 
-  if (classEntity === undefined) {
-    console.log(`Class not found for the EntityId: ${entityId}`)
-    return
-  }
-
-  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === classEntity.classId)
-  if (cls === undefined) {
-    console.log('Not recognized class')
-    return
-  }
+  const [knownClass] = await getKnownClass(db, { where: { id: entityId } })
+  if (!knownClass) return
 
 
   const arg: IDBBlockId = { db, block, id: entityId }
   const arg: IDBBlockId = { db, block, id: entityId }
 
 
-  switch (cls.name) {
+  switch (knownClass.name) {
     case ContentDirectoryKnownClasses.CHANNEL:
     case ContentDirectoryKnownClasses.CHANNEL:
       await createChannel(
       await createChannel(
         arg,
         arg,
@@ -168,9 +163,17 @@ async function contentDirectory_EntitySchemaSupportAdded(db: DB, event: Substrat
         decode.setProperties<IVideoMediaEncoding>(event, videoMediaEncodingPropertyNamesWithId)
         decode.setProperties<IVideoMediaEncoding>(event, videoMediaEncodingPropertyNamesWithId)
       )
       )
       break
       break
+    case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+      await createFeaturedVideo(
+        arg,
+        new Map<string, IEntity[]>(),
+        decode.setProperties<IFeaturedVideo>(event, featuredVideoPropertyNamesWithId),
+        0
+      )
+      break
 
 
     default:
     default:
-      throw new Error(`Unknown class name: ${cls.name}`)
+      throw new Error(`Unknown class name: ${knownClass.name}`)
   }
   }
 }
 }
 
 
@@ -181,19 +184,10 @@ async function contentDirectory_EntityRemoved(db: DB, event: SubstrateEvent): Pr
   const entityId = decode.stringIfyEntityId(event)
   const entityId = decode.stringIfyEntityId(event)
   const where: IWhereCond = { where: { id: entityId } }
   const where: IWhereCond = { where: { id: entityId } }
 
 
-  const classEntity = await db.get(ClassEntity, where)
-  if (classEntity === undefined) {
-    console.log(`Class not found for the EntityId: ${entityId}`)
-    return
-  }
-
-  const cls = contentDirectoryClassNamesWithId.find((c) => c.classId === classEntity.classId)
-  if (cls === undefined) {
-    console.log('Unknown class')
-    return
-  }
+  const [knownClass, classEntity] = await getKnownClass(db, where)
+  if (!knownClass) return
 
 
-  switch (cls.name) {
+  switch (knownClass.name) {
     case ContentDirectoryKnownClasses.CHANNEL:
     case ContentDirectoryKnownClasses.CHANNEL:
       await removeChannel(db, where)
       await removeChannel(db, where)
       break
       break
@@ -241,9 +235,14 @@ async function contentDirectory_EntityRemoved(db: DB, event: SubstrateEvent): Pr
       await removeMediaLocation(db, where)
       await removeMediaLocation(db, where)
       break
       break
 
 
+    case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+      await removeFeaturedVideo(db, where)
+      break
+
     default:
     default:
-      throw new Error(`Unknown class name: ${cls.name}`)
+      throw new Error(`Unknown class name: ${knownClass.name}`)
   }
   }
+  await db.remove<ClassEntity>(classEntity)
 }
 }
 
 
 // eslint-disable-next-line @typescript-eslint/naming-convention
 // eslint-disable-next-line @typescript-eslint/naming-convention
@@ -273,21 +272,17 @@ async function contentDirectory_EntityPropertyValuesUpdated(db: DB, event: Subst
 
 
   const { 2: newPropertyValues } = extrinsic.args
   const { 2: newPropertyValues } = extrinsic.args
   const entityId = decode.stringIfyEntityId(event)
   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 } }
   const where: IWhereCond = { where: { id: entityId } }
 
 
+  const [knownClass] = await getKnownClass(db, where)
+  if (!knownClass) return
+
   // TODO: change setProperties method signature to accecpt SubstrateExtrinsic, then remove the following
   // 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
   // line. The reason we push the same arg is beacuse of the setProperties method check the 3rd indices
   // to get properties values
   // to get properties values
   extrinsic.args.push(newPropertyValues)
   extrinsic.args.push(newPropertyValues)
 
 
-  switch (cls.name) {
+  switch (knownClass.name) {
     case ContentDirectoryKnownClasses.CHANNEL:
     case ContentDirectoryKnownClasses.CHANNEL:
       updateChannelEntityPropertyValues(db, where, decode.setProperties<IChannel>(event, channelPropertyNamesWithId), 0)
       updateChannelEntityPropertyValues(db, where, decode.setProperties<IChannel>(event, channelPropertyNamesWithId), 0)
       break
       break
@@ -379,8 +374,17 @@ async function contentDirectory_EntityPropertyValuesUpdated(db: DB, event: Subst
       )
       )
       break
       break
 
 
+    case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+      await updateFeaturedVideoEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IFeaturedVideo>(event, featuredVideoPropertyNamesWithId),
+        0
+      )
+      break
+
     default:
     default:
-      throw new Error(`Unknown class name: ${cls.name}`)
+      throw new Error(`Unknown class name: ${knownClass.name}`)
   }
   }
 }
 }
 
 

+ 77 - 46
query-node/mappings/content-directory/entity/remove.ts

@@ -1,97 +1,127 @@
+import assert from 'assert'
+import Debug from 'debug'
+
 import { DB } from '../../../generated/indexer'
 import { DB } from '../../../generated/indexer'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
 import { Category } from '../../../generated/graphql-server/src/modules/category/category.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 { KnownLicenseEntity } from '../../../generated/graphql-server/src/modules/known-license-entity/known-license-entity.model'
+import { UserDefinedLicenseEntity } from '../../../generated/graphql-server/src/modules/user-defined-license-entity/user-defined-license-entity.model'
+import { JoystreamMediaLocationEntity } from '../../../generated/graphql-server/src/modules/joystream-media-location-entity/joystream-media-location-entity.model'
+import { HttpMediaLocationEntity } from '../../../generated/graphql-server/src/modules/http-media-location-entity/http-media-location-entity.model'
 import { VideoMedia } from '../../../generated/graphql-server/src/modules/video-media/video-media.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 { Video } from '../../../generated/graphql-server/src/modules/video/video.model'
 import { Language } from '../../../generated/graphql-server/src/modules/language/language.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 { VideoMediaEncoding } from '../../../generated/graphql-server/src/modules/video-media-encoding/video-media-encoding.model'
-import { License } from '../../../generated/graphql-server/src/modules/license/license.model'
-import { MediaLocation } from '../../../generated/graphql-server/src/modules/media-location/media-location.model'
+import { LicenseEntity } from '../../../generated/graphql-server/src/modules/license-entity/license-entity.model'
+import { MediaLocationEntity } from '../../../generated/graphql-server/src/modules/media-location-entity/media-location-entity.model'
+import { FeaturedVideo } from '../../../generated/graphql-server/src/modules/featured-video/featured-video.model'
 
 
 import { IWhereCond } from '../../types'
 import { IWhereCond } from '../../types'
 
 
+const debug = Debug(`mappings:remove-entity`)
+
+function assertKeyViolation(entityName: string, entityId: string) {
+  assert(false, `Can not remove ${entityName}(${entityId})! There are references to this entity`)
+}
+
+function logEntityNotFound(className: string, where: IWhereCond) {
+  debug(`${className}(${where.where.id}) not found. This happen when schema support is not added for the entity.`)
+}
+
 async function removeChannel(db: DB, where: IWhereCond): Promise<void> {
 async function removeChannel(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Channel, where)
   const record = await db.get(Channel, where)
-  if (record === undefined) throw Error(`Channel not found`)
-  if (record.videos) record.videos.map(async (v) => await removeVideo(db, { where: { id: v.id } }))
+  if (!record) return logEntityNotFound(`Channel`, where)
+  if (record.videos && record.videos.length) assertKeyViolation(`Channel`, record.id)
   await db.remove<Channel>(record)
   await db.remove<Channel>(record)
 }
 }
+
 async function removeCategory(db: DB, where: IWhereCond): Promise<void> {
 async function removeCategory(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Category, where)
   const record = await db.get(Category, where)
-  if (record === undefined) throw Error(`Category not found`)
-  if (record.videos) record.videos.map(async (v) => await removeVideo(db, { where: { id: v.id } }))
+  if (!record) return logEntityNotFound(`Category`, where)
+  if (record.videos && record.videos.length) assertKeyViolation(`Category`, record.id)
   await db.remove<Category>(record)
   await db.remove<Category>(record)
 }
 }
 async function removeVideoMedia(db: DB, where: IWhereCond): Promise<void> {
 async function removeVideoMedia(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(VideoMedia, where)
   const record = await db.get(VideoMedia, where)
-  if (record === undefined) throw Error(`VideoMedia not found`)
-  if (record.video) await db.remove<Video>(record.video)
+  if (!record) return logEntityNotFound(`VideoMedia`, where)
+  if (record.video) assertKeyViolation(`VideoMedia`, record.id)
   await db.remove<VideoMedia>(record)
   await db.remove<VideoMedia>(record)
 }
 }
+
 async function removeVideo(db: DB, where: IWhereCond): Promise<void> {
 async function removeVideo(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Video, where)
   const record = await db.get(Video, where)
-  if (record === undefined) throw Error(`Video not found`)
+  if (!record) return logEntityNotFound(`Video`, where)
   await db.remove<Video>(record)
   await db.remove<Video>(record)
 }
 }
 
 
 async function removeLicense(db: DB, where: IWhereCond): Promise<void> {
 async function removeLicense(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(License, where)
-  if (record === undefined) throw Error(`License not found`)
-  // Remove all the videos under this license
-  if (record.videolicense) record.videolicense.map(async (v) => await removeVideo(db, { where: { id: v.id } }))
-  await db.remove<License>(record)
+  const record = await db.get(LicenseEntity, where)
+  if (!record) return logEntityNotFound(`License`, where)
+  if (record.videolicense && record.videolicense.length) assertKeyViolation(`License`, record.id)
+  await db.remove<LicenseEntity>(record)
 }
 }
+
 async function removeUserDefinedLicense(db: DB, where: IWhereCond): Promise<void> {
 async function removeUserDefinedLicense(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(UserDefinedLicense, where)
-  if (record === undefined) throw Error(`UserDefinedLicense not found`)
-  if (record.licenseuserdefinedLicense)
-    record.licenseuserdefinedLicense.map(async (l) => await removeLicense(db, { where: { id: l.id } }))
-  await db.remove<UserDefinedLicense>(record)
+  const record = await db.get(UserDefinedLicenseEntity, where)
+  if (!record) return logEntityNotFound(`UserDefinedLicense`, where)
+  await db.remove<UserDefinedLicenseEntity>(record)
 }
 }
+
 async function removeKnownLicense(db: DB, where: IWhereCond): Promise<void> {
 async function removeKnownLicense(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(KnownLicense, where)
-  if (record === undefined) throw Error(`KnownLicense not found`)
-  if (record.licenseknownLicense)
-    record.licenseknownLicense.map(async (k) => await removeLicense(db, { where: { id: k.id } }))
-  await db.remove<KnownLicense>(record)
+  const record = await db.get(KnownLicenseEntity, where)
+  if (!record) return logEntityNotFound(`KnownLicense`, where)
+  await db.remove<KnownLicenseEntity>(record)
 }
 }
 async function removeMediaLocation(db: DB, where: IWhereCond): Promise<void> {
 async function removeMediaLocation(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(MediaLocation, where)
-  if (record === undefined) throw Error(`MediaLocation not found`)
-  if (record.videoMedia) await removeVideo(db, { where: { id: record.videoMedia.id } })
-  await db.remove<MediaLocation>(record)
+  const record = await db.get(MediaLocationEntity, where)
+  if (!record) return logEntityNotFound(`MediaLocation`, where)
+  if (record.videoMedia) assertKeyViolation('MediaLocation', record.id)
+  await db.remove<MediaLocationEntity>(record)
 }
 }
+
 async function removeHttpMediaLocation(db: DB, where: IWhereCond): Promise<void> {
 async function removeHttpMediaLocation(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(HttpMediaLocation, where)
-  if (record === undefined) throw Error(`HttpMediaLocation not found`)
-  if (record.medialocationhttpMediaLocation)
-    record.medialocationhttpMediaLocation.map(async (v) => await removeMediaLocation(db, { where: { id: v.id } }))
-  await db.remove<HttpMediaLocation>(record)
+  const record = await db.get(HttpMediaLocationEntity, where)
+  if (!record) return logEntityNotFound(`HttpMediaLocation`, where)
+  if (record.medialocationentityhttpMediaLocation && record.medialocationentityhttpMediaLocation.length) {
+    assertKeyViolation('HttpMediaLocation', record.id)
+  }
+  await db.remove<HttpMediaLocationEntity>(record)
 }
 }
+
 async function removeJoystreamMediaLocation(db: DB, where: IWhereCond): Promise<void> {
 async function removeJoystreamMediaLocation(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(JoystreamMediaLocation, where)
-  if (record === undefined) throw Error(`JoystreamMediaLocation not found`)
-  if (record.medialocationjoystreamMediaLocation)
-    record.medialocationjoystreamMediaLocation.map(async (v) => await removeVideo(db, { where: { id: v.id } }))
-  await db.remove<JoystreamMediaLocation>(record)
+  const record = await db.get(JoystreamMediaLocationEntity, where)
+  if (!record) return logEntityNotFound(`JoystreamMediaLocation`, where)
+  if (record.medialocationentityjoystreamMediaLocation && record.medialocationentityjoystreamMediaLocation.length) {
+    assertKeyViolation('JoystreamMediaLocation', record.id)
+  }
+  await db.remove<JoystreamMediaLocationEntity>(record)
 }
 }
+
 async function removeLanguage(db: DB, where: IWhereCond): Promise<void> {
 async function removeLanguage(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Language, where)
   const record = await db.get(Language, where)
-  if (record === undefined) throw Error(`Language not found`)
-  if (record.channellanguage) record.channellanguage.map(async (c) => await removeChannel(db, { where: { id: c.id } }))
-  if (record.videolanguage) record.videolanguage.map(async (v) => await removeVideo(db, { where: { id: v.id } }))
+  if (!record) return logEntityNotFound(`Language`, where)
+  if (record.channellanguage && record.channellanguage.length) assertKeyViolation('Language', record.id)
+  if (record.videolanguage && record.videolanguage.length) assertKeyViolation('Language', record.id)
   await db.remove<Language>(record)
   await db.remove<Language>(record)
 }
 }
+
 async function removeVideoMediaEncoding(db: DB, where: IWhereCond): Promise<void> {
 async function removeVideoMediaEncoding(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(VideoMediaEncoding, where)
   const record = await db.get(VideoMediaEncoding, where)
-  if (record === undefined) throw Error(`Language not found`)
+  if (!record) return logEntityNotFound(`VideoMediaEncoding`, where)
   await db.remove<VideoMediaEncoding>(record)
   await db.remove<VideoMediaEncoding>(record)
 }
 }
 
 
+async function removeFeaturedVideo(db: DB, where: IWhereCond): Promise<void> {
+  const record = await db.get(FeaturedVideo, { ...where, relations: ['video'] })
+  if (!record) return logEntityNotFound(`FeaturedVideo`, where)
+
+  record.video.isFeatured = false
+  record.video.featured = undefined
+
+  await db.save<Video>(record.video)
+  await db.remove<FeaturedVideo>(record)
+}
+
 export {
 export {
   removeCategory,
   removeCategory,
   removeChannel,
   removeChannel,
@@ -105,4 +135,5 @@ export {
   removeVideoMediaEncoding,
   removeVideoMediaEncoding,
   removeMediaLocation,
   removeMediaLocation,
   removeLicense,
   removeLicense,
+  removeFeaturedVideo,
 }
 }

+ 105 - 33
query-node/mappings/content-directory/entity/update.ts

@@ -1,20 +1,22 @@
 import { DB } from '../../../generated/indexer'
 import { DB } from '../../../generated/indexer'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
 import { Category } from '../../../generated/graphql-server/src/modules/category/category.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 { KnownLicenseEntity } from '../../../generated/graphql-server/src/modules/known-license-entity/known-license-entity.model'
+import { UserDefinedLicenseEntity } from '../../../generated/graphql-server/src/modules/user-defined-license-entity/user-defined-license-entity.model'
 import { VideoMedia } from '../../../generated/graphql-server/src/modules/video-media/video-media.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 { Video } from '../../../generated/graphql-server/src/modules/video/video.model'
 import { Language } from '../../../generated/graphql-server/src/modules/language/language.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 { VideoMediaEncoding } from '../../../generated/graphql-server/src/modules/video-media-encoding/video-media-encoding.model'
-import { License } from '../../../generated/graphql-server/src/modules/license/license.model'
-import { MediaLocation } from '../../../generated/graphql-server/src/modules/media-location/media-location.model'
+import { LicenseEntity } from '../../../generated/graphql-server/src/modules/license-entity/license-entity.model'
+import { MediaLocationEntity } from '../../../generated/graphql-server/src/modules/media-location-entity/media-location-entity.model'
+import { HttpMediaLocationEntity } from '../../../generated/graphql-server/src/modules/http-media-location-entity/http-media-location-entity.model'
+import { JoystreamMediaLocationEntity } from '../../../generated/graphql-server/src/modules/joystream-media-location-entity/joystream-media-location-entity.model'
+import { FeaturedVideo } from '../../../generated/graphql-server/src/modules/featured-video/featured-video.model'
 
 
 import {
 import {
   ICategory,
   ICategory,
   IChannel,
   IChannel,
+  IFeaturedVideo,
   IHttpMediaLocation,
   IHttpMediaLocation,
   IJoystreamMediaLocation,
   IJoystreamMediaLocation,
   IKnownLicense,
   IKnownLicense,
@@ -28,6 +30,12 @@ import {
   IVideoMediaEncoding,
   IVideoMediaEncoding,
   IWhereCond,
   IWhereCond,
 } from '../../types'
 } from '../../types'
+import {
+  HttpMediaLocation,
+  JoystreamMediaLocation,
+  KnownLicense,
+  UserDefinedLicense,
+} from '../../../generated/graphql-server/src/modules/variants/variants.model'
 
 
 function getEntityIdFromReferencedField(ref: IReference, entityIdBeforeTransaction: number): string {
 function getEntityIdFromReferencedField(ref: IReference, entityIdBeforeTransaction: number): string {
   const { entityId, existing } = ref
   const { entityId, existing } = ref
@@ -42,18 +50,18 @@ async function updateMediaLocationEntityPropertyValues(
   entityIdBeforeTransaction: number
   entityIdBeforeTransaction: number
 ): Promise<void> {
 ): Promise<void> {
   const { httpMediaLocation, joystreamMediaLocation } = props
   const { httpMediaLocation, joystreamMediaLocation } = props
-  const record = await db.get(MediaLocation, where)
+  const record = await db.get(MediaLocationEntity, where)
   if (record === undefined) throw Error(`MediaLocation entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`MediaLocation entity not found: ${where.where.id}`)
 
 
   if (httpMediaLocation) {
   if (httpMediaLocation) {
     const id = getEntityIdFromReferencedField(httpMediaLocation, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(httpMediaLocation, entityIdBeforeTransaction)
-    record.httpMediaLocation = await db.get(HttpMediaLocation, { where: { id } })
+    record.httpMediaLocation = await db.get(HttpMediaLocationEntity, { where: { id } })
   }
   }
   if (joystreamMediaLocation) {
   if (joystreamMediaLocation) {
     const id = getEntityIdFromReferencedField(joystreamMediaLocation, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(joystreamMediaLocation, entityIdBeforeTransaction)
-    record.joystreamMediaLocation = await db.get(JoystreamMediaLocation, { where: { id } })
+    record.joystreamMediaLocation = await db.get(JoystreamMediaLocationEntity, { where: { id } })
   }
   }
-  await db.save<MediaLocation>(record)
+  await db.save<MediaLocationEntity>(record)
 }
 }
 
 
 async function updateLicenseEntityPropertyValues(
 async function updateLicenseEntityPropertyValues(
@@ -62,19 +70,36 @@ async function updateLicenseEntityPropertyValues(
   props: ILicense,
   props: ILicense,
   entityIdBeforeTransaction: number
   entityIdBeforeTransaction: number
 ): Promise<void> {
 ): Promise<void> {
-  const record = await db.get(License, where)
+  const record = await db.get(LicenseEntity, where)
   if (record === undefined) throw Error(`License entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`License entity not found: ${where.where.id}`)
 
 
   const { knownLicense, userDefinedLicense } = props
   const { knownLicense, userDefinedLicense } = props
   if (knownLicense) {
   if (knownLicense) {
     const id = getEntityIdFromReferencedField(knownLicense, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(knownLicense, entityIdBeforeTransaction)
-    record.knownLicense = await db.get(KnownLicense, { where: { id } })
+    const kLicense = await db.get(KnownLicenseEntity, { where: { id } })
+    if (!kLicense) throw Error(`KnownLicense not found ${id}`)
+
+    const k = new KnownLicense()
+    k.code = kLicense.code
+    k.description = kLicense.description
+    k.name = kLicense.name
+    k.url = kLicense.url
+    // Set the license type
+    record.type = k
   }
   }
   if (userDefinedLicense) {
   if (userDefinedLicense) {
     const id = getEntityIdFromReferencedField(userDefinedLicense, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(userDefinedLicense, entityIdBeforeTransaction)
-    record.userdefinedLicense = await db.get(UserDefinedLicense, { where: { id } })
+    const udl = await db.get(UserDefinedLicenseEntity, { where: { id } })
+    if (!udl) throw Error(`UserDefinedLicense not found ${id}`)
+
+    const u = new UserDefinedLicense()
+    u.content = udl.content
+    // Set the license type
+    record.type = u
   }
   }
-  await db.save<License>(record)
+
+  record.attribution = props.attribution || record.attribution
+  await db.save<LicenseEntity>(record)
 }
 }
 
 
 async function updateCategoryEntityPropertyValues(db: DB, where: IWhereCond, props: ICategory): Promise<void> {
 async function updateCategoryEntityPropertyValues(db: DB, where: IWhereCond, props: ICategory): Promise<void> {
@@ -83,6 +108,7 @@ async function updateCategoryEntityPropertyValues(db: DB, where: IWhereCond, pro
   Object.assign(record, props)
   Object.assign(record, props)
   await db.save<Category>(record)
   await db.save<Category>(record)
 }
 }
+
 async function updateChannelEntityPropertyValues(
 async function updateChannelEntityPropertyValues(
   db: DB,
   db: DB,
   where: IWhereCond,
   where: IWhereCond,
@@ -92,8 +118,8 @@ async function updateChannelEntityPropertyValues(
   const record = await db.get(Channel, where)
   const record = await db.get(Channel, where)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
 
 
-  let lang: Language | undefined
-  if (props.language !== undefined) {
+  let lang: Language | undefined = record.language
+  if (props.language) {
     const id = getEntityIdFromReferencedField(props.language, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(props.language, entityIdBeforeTransaction)
     lang = await db.get(Language, { where: { id } })
     lang = await db.get(Language, { where: { id } })
     if (lang === undefined) throw Error(`Language entity not found: ${id}`)
     if (lang === undefined) throw Error(`Language entity not found: ${id}`)
@@ -101,9 +127,10 @@ async function updateChannelEntityPropertyValues(
   }
   }
   Object.assign(record, props)
   Object.assign(record, props)
 
 
-  record.language = lang || record.language
+  record.language = lang
   await db.save<Channel>(record)
   await db.save<Channel>(record)
 }
 }
+
 async function updateVideoMediaEntityPropertyValues(
 async function updateVideoMediaEntityPropertyValues(
   db: DB,
   db: DB,
   where: IWhereCond,
   where: IWhereCond,
@@ -114,7 +141,7 @@ async function updateVideoMediaEntityPropertyValues(
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
 
 
   let enco: VideoMediaEncoding | undefined
   let enco: VideoMediaEncoding | undefined
-  let mediaLoc: MediaLocation | undefined
+  let mediaLoc: HttpMediaLocation | JoystreamMediaLocation = record.location
   const { encoding, location } = props
   const { encoding, location } = props
   if (encoding) {
   if (encoding) {
     const id = getEntityIdFromReferencedField(encoding, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(encoding, entityIdBeforeTransaction)
@@ -122,18 +149,31 @@ async function updateVideoMediaEntityPropertyValues(
     if (enco === undefined) throw Error(`VideoMediaEncoding entity not found: ${id}`)
     if (enco === undefined) throw Error(`VideoMediaEncoding entity not found: ${id}`)
     props.encoding = undefined
     props.encoding = undefined
   }
   }
+
   if (location) {
   if (location) {
     const id = getEntityIdFromReferencedField(location, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(location, entityIdBeforeTransaction)
-    mediaLoc = await db.get(MediaLocation, { where: { id } })
-    if (!mediaLoc) throw Error(`MediaLocation entity not found: ${id}`)
+    const mLoc = await db.get(MediaLocationEntity, { where: { id } })
+    if (!mLoc) throw Error(`MediaLocation entity not found: ${id}`)
+    const { httpMediaLocation, joystreamMediaLocation } = mLoc
+
+    if (httpMediaLocation) {
+      mediaLoc = new HttpMediaLocation()
+      mediaLoc.url = httpMediaLocation.url
+      mediaLoc.port = httpMediaLocation.port
+    }
+    if (joystreamMediaLocation) {
+      mediaLoc = new JoystreamMediaLocation()
+      mediaLoc.dataObjectId = joystreamMediaLocation.dataObjectId
+    }
     props.location = undefined
     props.location = undefined
   }
   }
   Object.assign(record, props)
   Object.assign(record, props)
 
 
   record.encoding = enco || record.encoding
   record.encoding = enco || record.encoding
-  record.location = mediaLoc || record.location
+  record.location = mediaLoc
   await db.save<VideoMedia>(record)
   await db.save<VideoMedia>(record)
 }
 }
+
 async function updateVideoEntityPropertyValues(
 async function updateVideoEntityPropertyValues(
   db: DB,
   db: DB,
   where: IWhereCond,
   where: IWhereCond,
@@ -147,7 +187,7 @@ async function updateVideoEntityPropertyValues(
   let cat: Category | undefined
   let cat: Category | undefined
   let lang: Language | undefined
   let lang: Language | undefined
   let vMedia: VideoMedia | undefined
   let vMedia: VideoMedia | undefined
-  let lic: License | undefined
+
   const { channel, category, language, media, license } = props
   const { channel, category, language, media, license } = props
   if (channel) {
   if (channel) {
     const id = getEntityIdFromReferencedField(channel, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(channel, entityIdBeforeTransaction)
@@ -169,8 +209,9 @@ async function updateVideoEntityPropertyValues(
   }
   }
   if (license) {
   if (license) {
     const id = getEntityIdFromReferencedField(license, entityIdBeforeTransaction)
     const id = getEntityIdFromReferencedField(license, entityIdBeforeTransaction)
-    lic = await db.get(License, { where: { id } })
-    if (!lic) throw Error(`License entity not found: ${id}`)
+    const licenseEntity = await db.get(LicenseEntity, { where: { id } })
+    if (!licenseEntity) throw Error(`License entity not found: ${id}`)
+    record.license = licenseEntity
     props.license = undefined
     props.license = undefined
   }
   }
   if (language) {
   if (language) {
@@ -185,36 +226,38 @@ async function updateVideoEntityPropertyValues(
   record.channel = chann || record.channel
   record.channel = chann || record.channel
   record.category = cat || record.category
   record.category = cat || record.category
   record.media = vMedia || record.media
   record.media = vMedia || record.media
-  record.license = lic || record.license
   record.language = lang
   record.language = lang
 
 
   await db.save<Video>(record)
   await db.save<Video>(record)
 }
 }
+
 async function updateUserDefinedLicenseEntityPropertyValues(
 async function updateUserDefinedLicenseEntityPropertyValues(
   db: DB,
   db: DB,
   where: IWhereCond,
   where: IWhereCond,
   props: IUserDefinedLicense
   props: IUserDefinedLicense
 ): Promise<void> {
 ): Promise<void> {
-  const record = await db.get(UserDefinedLicense, where)
+  const record = await db.get(UserDefinedLicenseEntity, where)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   Object.assign(record, props)
   Object.assign(record, props)
-  await db.save<UserDefinedLicense>(record)
+  await db.save<UserDefinedLicenseEntity>(record)
 }
 }
+
 async function updateKnownLicenseEntityPropertyValues(db: DB, where: IWhereCond, props: IKnownLicense): Promise<void> {
 async function updateKnownLicenseEntityPropertyValues(db: DB, where: IWhereCond, props: IKnownLicense): Promise<void> {
-  const record = await db.get(KnownLicense, where)
+  const record = await db.get(KnownLicenseEntity, where)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   Object.assign(record, props)
   Object.assign(record, props)
-  await db.save<KnownLicense>(record)
+  await db.save<KnownLicenseEntity>(record)
 }
 }
+
 async function updateHttpMediaLocationEntityPropertyValues(
 async function updateHttpMediaLocationEntityPropertyValues(
   db: DB,
   db: DB,
   where: IWhereCond,
   where: IWhereCond,
   props: IHttpMediaLocation
   props: IHttpMediaLocation
 ): Promise<void> {
 ): Promise<void> {
-  const record = await db.get(HttpMediaLocation, where)
+  const record = await db.get(HttpMediaLocationEntity, where)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   Object.assign(record, props)
   Object.assign(record, props)
-  await db.save<HttpMediaLocation>(record)
+  await db.save<HttpMediaLocationEntity>(record)
 }
 }
 
 
 async function updateJoystreamMediaLocationEntityPropertyValues(
 async function updateJoystreamMediaLocationEntityPropertyValues(
@@ -222,17 +265,19 @@ async function updateJoystreamMediaLocationEntityPropertyValues(
   where: IWhereCond,
   where: IWhereCond,
   props: IJoystreamMediaLocation
   props: IJoystreamMediaLocation
 ): Promise<void> {
 ): Promise<void> {
-  const record = await db.get(JoystreamMediaLocation, where)
+  const record = await db.get(JoystreamMediaLocationEntity, where)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   Object.assign(record, props)
   Object.assign(record, props)
-  await db.save<JoystreamMediaLocation>(record)
+  await db.save<JoystreamMediaLocationEntity>(record)
 }
 }
+
 async function updateLanguageEntityPropertyValues(db: DB, where: IWhereCond, props: ILanguage): Promise<void> {
 async function updateLanguageEntityPropertyValues(db: DB, where: IWhereCond, props: ILanguage): Promise<void> {
   const record = await db.get(Language, where)
   const record = await db.get(Language, where)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   if (record === undefined) throw Error(`Entity not found: ${where.where.id}`)
   Object.assign(record, props)
   Object.assign(record, props)
   await db.save<Language>(record)
   await db.save<Language>(record)
 }
 }
+
 async function updateVideoMediaEncodingEntityPropertyValues(
 async function updateVideoMediaEncodingEntityPropertyValues(
   db: DB,
   db: DB,
   where: IWhereCond,
   where: IWhereCond,
@@ -244,6 +289,32 @@ async function updateVideoMediaEncodingEntityPropertyValues(
   await db.save<VideoMediaEncoding>(record)
   await db.save<VideoMediaEncoding>(record)
 }
 }
 
 
+async function updateFeaturedVideoEntityPropertyValues(
+  db: DB,
+  where: IWhereCond,
+  props: IFeaturedVideo,
+  entityIdBeforeTransaction: number
+): Promise<void> {
+  const record = await db.get(FeaturedVideo, { ...where, relations: ['video'] })
+  if (record === undefined) throw Error(`FeaturedVideo entity not found: ${where.where.id}`)
+
+  if (props.video) {
+    const id = getEntityIdFromReferencedField(props.video, entityIdBeforeTransaction)
+    const video = await db.get(Video, { where: { id } })
+    if (!video) throw Error(`Video entity not found: ${id}`)
+
+    // Update old video isFeatured to false
+    record.video.isFeatured = false
+    await db.save<Video>(record.video)
+
+    video.isFeatured = true
+    record.video = video
+
+    await db.save<Video>(video)
+    await db.save<FeaturedVideo>(record)
+  }
+}
+
 export {
 export {
   updateCategoryEntityPropertyValues,
   updateCategoryEntityPropertyValues,
   updateChannelEntityPropertyValues,
   updateChannelEntityPropertyValues,
@@ -257,4 +328,5 @@ export {
   updateVideoMediaEncodingEntityPropertyValues,
   updateVideoMediaEncodingEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
+  updateFeaturedVideoEntityPropertyValues,
 }
 }

+ 82 - 38
query-node/mappings/content-directory/get-or-create.ts

@@ -1,20 +1,23 @@
 import { Channel } from '../../generated/graphql-server/src/modules/channel/channel.model'
 import { Channel } from '../../generated/graphql-server/src/modules/channel/channel.model'
 import { Category } from '../../generated/graphql-server/src/modules/category/category.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 { KnownLicenseEntity } from '../../generated/graphql-server/src/modules/known-license-entity/known-license-entity.model'
+import { UserDefinedLicenseEntity } from '../../generated/graphql-server/src/modules/user-defined-license-entity/user-defined-license-entity.model'
+import { JoystreamMediaLocationEntity } from '../../generated/graphql-server/src/modules/joystream-media-location-entity/joystream-media-location-entity.model'
+import { HttpMediaLocationEntity } from '../../generated/graphql-server/src/modules/http-media-location-entity/http-media-location-entity.model'
 import { VideoMedia } from '../../generated/graphql-server/src/modules/video-media/video-media.model'
 import { VideoMedia } from '../../generated/graphql-server/src/modules/video-media/video-media.model'
 import { Language } from '../../generated/graphql-server/src/modules/language/language.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 { VideoMediaEncoding } from '../../generated/graphql-server/src/modules/video-media-encoding/video-media-encoding.model'
-import { License } from '../../generated/graphql-server/src/modules/license/license.model'
-import { MediaLocation } from '../../generated/graphql-server/src/modules/media-location/media-location.model'
+import { LicenseEntity } from '../../generated/graphql-server/src/modules/license-entity/license-entity.model'
+import { MediaLocationEntity } from '../../generated/graphql-server/src/modules/media-location-entity/media-location-entity.model'
+import { Video } from '../../generated/graphql-server/src/modules/video/video.model'
 import { NextEntityId } from '../../generated/graphql-server/src/modules/next-entity-id/next-entity-id.model'
 import { NextEntityId } from '../../generated/graphql-server/src/modules/next-entity-id/next-entity-id.model'
+import { ClassEntity } from '../../generated/graphql-server/src/modules/class-entity/class-entity.model'
 
 
 import { decode } from './decode'
 import { decode } from './decode'
 import {
 import {
   categoryPropertyNamesWithId,
   categoryPropertyNamesWithId,
   channelPropertyNamesWithId,
   channelPropertyNamesWithId,
+  contentDirectoryClassNamesWithId,
   httpMediaLocationPropertyNamesWithId,
   httpMediaLocationPropertyNamesWithId,
   joystreamMediaLocationPropertyNamesWithId,
   joystreamMediaLocationPropertyNamesWithId,
   knownLicensePropertyNamesWIthId,
   knownLicensePropertyNamesWIthId,
@@ -33,14 +36,17 @@ import {
   IEntity,
   IEntity,
   IHttpMediaLocation,
   IHttpMediaLocation,
   IJoystreamMediaLocation,
   IJoystreamMediaLocation,
+  IKnownClass,
   IKnownLicense,
   IKnownLicense,
   ILanguage,
   ILanguage,
   ILicense,
   ILicense,
   IMediaLocation,
   IMediaLocation,
   IReference,
   IReference,
   IUserDefinedLicense,
   IUserDefinedLicense,
+  IVideo,
   IVideoMedia,
   IVideoMedia,
   IVideoMediaEncoding,
   IVideoMediaEncoding,
+  IWhereCond,
 } from '../types'
 } from '../types'
 
 
 import {
 import {
@@ -55,6 +61,7 @@ import {
   createVideoMediaEncoding,
   createVideoMediaEncoding,
   createLicense,
   createLicense,
   createMediaLocation,
   createMediaLocation,
+  createVideo,
 } from './entity/create'
 } from './entity/create'
 
 
 import { DB } from '../../generated/indexer'
 import { DB } from '../../generated/indexer'
@@ -76,7 +83,12 @@ function findEntity(entityId: number, className: string, classEntityMap: ClassEn
   if (newlyCreatedEntities === undefined) throw Error(`Couldn't find '${className}' entities in the classEntityMap`)
   if (newlyCreatedEntities === undefined) throw Error(`Couldn't find '${className}' entities in the classEntityMap`)
   const entity = newlyCreatedEntities.find((e) => e.indexOf === entityId)
   const entity = newlyCreatedEntities.find((e) => e.indexOf === entityId)
   if (!entity) throw Error(`Unknown ${className} entity id: ${entityId}`)
   if (!entity) throw Error(`Unknown ${className} entity id: ${entityId}`)
-  removeInsertedEntity(className, entityId, classEntityMap)
+
+  // Remove the inserted entity from the list
+  classEntityMap.set(
+    className,
+    newlyCreatedEntities.filter((e) => e.entityId !== entityId)
+  )
   return entity
   return entity
 }
 }
 
 
@@ -167,17 +179,17 @@ async function knownLicense(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   knownLicense: IReference,
   knownLicense: IReference,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<KnownLicense> {
-  let kLicense: KnownLicense | undefined
+): Promise<KnownLicenseEntity> {
+  let kLicense: KnownLicenseEntity | undefined
   const { entityId, existing } = knownLicense
   const { entityId, existing } = knownLicense
   if (existing) {
   if (existing) {
-    kLicense = await db.get(KnownLicense, { where: { id: entityId.toString() } })
+    kLicense = await db.get(KnownLicenseEntity, { where: { id: entityId.toString() } })
     if (!kLicense) throw Error(`KnownLicense entity not found`)
     if (!kLicense) throw Error(`KnownLicense entity not found`)
     return kLicense
     return kLicense
   }
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   // could be created in the transaction
   // could be created in the transaction
-  kLicense = await db.get(KnownLicense, { where: { id } })
+  kLicense = await db.get(KnownLicenseEntity, { where: { id } })
   if (kLicense) return kLicense
   if (kLicense) return kLicense
 
 
   const { properties } = findEntity(entityId, 'KnownLicense', classEntityMap)
   const { properties } = findEntity(entityId, 'KnownLicense', classEntityMap)
@@ -191,17 +203,17 @@ async function userDefinedLicense(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   userDefinedLicense: IReference,
   userDefinedLicense: IReference,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<UserDefinedLicense> {
-  let udLicense: UserDefinedLicense | undefined
+): Promise<UserDefinedLicenseEntity> {
+  let udLicense: UserDefinedLicenseEntity | undefined
   const { entityId, existing } = userDefinedLicense
   const { entityId, existing } = userDefinedLicense
   if (existing) {
   if (existing) {
-    udLicense = await db.get(UserDefinedLicense, { where: { id: entityId.toString() } })
+    udLicense = await db.get(UserDefinedLicenseEntity, { where: { id: entityId.toString() } })
     if (!udLicense) throw Error(`UserDefinedLicense entity not found`)
     if (!udLicense) throw Error(`UserDefinedLicense entity not found`)
     return udLicense
     return udLicense
   }
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   // could be created in the transaction
   // could be created in the transaction
-  udLicense = await db.get(UserDefinedLicense, {
+  udLicense = await db.get(UserDefinedLicenseEntity, {
     where: { id },
     where: { id },
   })
   })
   if (udLicense) return udLicense
   if (udLicense) return udLicense
@@ -273,19 +285,19 @@ async function httpMediaLocation(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   httpMediaLoc: IReference,
   httpMediaLoc: IReference,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<HttpMediaLocation | undefined> {
-  let loc: HttpMediaLocation | undefined
+): Promise<HttpMediaLocationEntity | undefined> {
+  let loc: HttpMediaLocationEntity | undefined
   const { entityId, existing } = httpMediaLoc
   const { entityId, existing } = httpMediaLoc
 
 
   if (existing) {
   if (existing) {
-    loc = await db.get(HttpMediaLocation, { where: { id: entityId.toString() } })
+    loc = await db.get(HttpMediaLocationEntity, { where: { id: entityId.toString() } })
     if (!loc) throw Error(`HttpMediaLocation entity not found`)
     if (!loc) throw Error(`HttpMediaLocation entity not found`)
     return loc
     return loc
   }
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
 
 
   // could be created in the transaction
   // could be created in the transaction
-  loc = await db.get(HttpMediaLocation, {
+  loc = await db.get(HttpMediaLocationEntity, {
     where: { id },
     where: { id },
   })
   })
   if (loc) return loc
   if (loc) return loc
@@ -302,12 +314,12 @@ async function joystreamMediaLocation(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   joyMediaLoc: IReference,
   joyMediaLoc: IReference,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<JoystreamMediaLocation | undefined> {
-  let loc: JoystreamMediaLocation | undefined
+): Promise<JoystreamMediaLocationEntity | undefined> {
+  let loc: JoystreamMediaLocationEntity | undefined
   const { entityId, existing } = joyMediaLoc
   const { entityId, existing } = joyMediaLoc
 
 
   if (existing) {
   if (existing) {
-    loc = await db.get(JoystreamMediaLocation, { where: { id: entityId.toString() } })
+    loc = await db.get(JoystreamMediaLocationEntity, { where: { id: entityId.toString() } })
     if (!loc) throw Error(`JoystreamMediaLocation entity not found`)
     if (!loc) throw Error(`JoystreamMediaLocation entity not found`)
     return loc
     return loc
   }
   }
@@ -315,7 +327,7 @@ async function joystreamMediaLocation(
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
 
 
   // could be created in the transaction
   // could be created in the transaction
-  loc = await db.get(JoystreamMediaLocation, {
+  loc = await db.get(JoystreamMediaLocationEntity, {
     where: { id },
     where: { id },
   })
   })
   if (loc) return loc
   if (loc) return loc
@@ -332,19 +344,19 @@ async function license(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   license: IReference,
   license: IReference,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<License> {
-  let lic: License | undefined
+): Promise<LicenseEntity> {
+  let lic: LicenseEntity | undefined
   const { entityId, existing } = license
   const { entityId, existing } = license
 
 
   if (existing) {
   if (existing) {
-    lic = await db.get(License, { where: { id: entityId.toString() } })
+    lic = await db.get(LicenseEntity, { where: { id: entityId.toString() } })
     if (!lic) throw Error(`License entity not found`)
     if (!lic) throw Error(`License entity not found`)
     return lic
     return lic
   }
   }
 
 
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   // could be created in the transaction
   // could be created in the transaction
-  lic = await db.get(License, { where: { id } })
+  lic = await db.get(LicenseEntity, { where: { id } })
   if (lic) return lic
   if (lic) return lic
 
 
   const { properties } = findEntity(entityId, 'License', classEntityMap)
   const { properties } = findEntity(entityId, 'License', classEntityMap)
@@ -361,21 +373,24 @@ async function mediaLocation(
   classEntityMap: ClassEntityMap,
   classEntityMap: ClassEntityMap,
   location: IReference,
   location: IReference,
   nextEntityIdBeforeTransaction: number
   nextEntityIdBeforeTransaction: number
-): Promise<MediaLocation> {
-  let loc: MediaLocation | undefined
+): Promise<MediaLocationEntity> {
+  let loc: MediaLocationEntity | undefined
   const { entityId, existing } = location
   const { entityId, existing } = location
   if (existing) {
   if (existing) {
-    loc = await db.get(MediaLocation, { where: { id: entityId.toString() } })
+    loc = await db.get(MediaLocationEntity, { where: { id: entityId.toString() } })
     if (!loc) throw Error(`MediaLocation entity not found`)
     if (!loc) throw Error(`MediaLocation entity not found`)
     return loc
     return loc
   }
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
 
 
   // could be created in the transaction
   // could be created in the transaction
-  loc = await db.get(MediaLocation, {
+  loc = await db.get(MediaLocationEntity, {
     where: { id },
     where: { id },
+    relations: ['httpMediaLocation', 'joystreamMediaLocation'],
   })
   })
-  if (loc) return loc
+  if (loc) {
+    return loc
+  }
 
 
   const { properties } = findEntity(entityId, 'MediaLocation', classEntityMap)
   const { properties } = findEntity(entityId, 'MediaLocation', classEntityMap)
   return await createMediaLocation(
   return await createMediaLocation(
@@ -386,15 +401,43 @@ async function mediaLocation(
   )
   )
 }
 }
 
 
-function removeInsertedEntity(key: string, insertedEntityId: number, classEntityMap: ClassEntityMap) {
-  const newlyCreatedEntities = classEntityMap.get(key)
-  // Remove the inserted entity from the list
-  classEntityMap.set(
-    key,
-    newlyCreatedEntities!.filter((e) => e.entityId !== insertedEntityId)
+async function video(
+  { db, block }: IDBBlockId,
+  classEntityMap: ClassEntityMap,
+  video: IReference,
+  nextEntityIdBeforeTransaction: number
+): Promise<Video> {
+  const { existing, entityId } = video
+  if (existing) {
+    const v = await db.get(Video, { where: { id: entityId.toString() } })
+    if (!v) throw Error(`Video not found. id ${entityId}`)
+    return v
+  }
+
+  const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
+  const v = await db.get(Video, { where: { id } })
+  if (v) return v
+
+  const { properties } = findEntity(entityId, 'MediaVideo', classEntityMap)
+  return await createVideo(
+    { db, block, id },
+    classEntityMap,
+    decode.setEntityPropertyValues<IVideo>(properties, videoPropertyNamesWithId),
+    nextEntityIdBeforeTransaction
   )
   )
 }
 }
 
 
+export async function getKnownClass(db: DB, where: IWhereCond): Promise<[IKnownClass | undefined, ClassEntity]> {
+  const ce = await db.get(ClassEntity, where)
+  if (!ce) {
+    throw Error(`Class not found for the EntityId: ${where.where.id} or the entity has not been created.`)
+  }
+
+  const knownClass = contentDirectoryClassNamesWithId.find((c) => c.classId === ce.classId)
+  if (!knownClass) console.log('Unknown class')
+  return [knownClass, ce]
+}
+
 export const getOrCreate = {
 export const getOrCreate = {
   language,
   language,
   videoMediaEncoding,
   videoMediaEncoding,
@@ -408,4 +451,5 @@ export const getOrCreate = {
   license,
   license,
   mediaLocation,
   mediaLocation,
   nextEntityId,
   nextEntityId,
+  video,
 }
 }

+ 1 - 1
query-node/mappings/content-directory/mapping.ts

@@ -4,4 +4,4 @@ export {
   contentDirectory_EntityCreated,
   contentDirectory_EntityCreated,
   contentDirectory_EntityPropertyValuesUpdated,
   contentDirectory_EntityPropertyValuesUpdated,
 } from './entity'
 } from './entity'
-export { contentDirectory_TransactionCompleted } from './transaction'
+export { contentDirectory_TransactionCompleted, contentDirectory_TransactionFailed } from './transaction'

+ 42 - 18
query-node/mappings/content-directory/transaction.ts

@@ -7,11 +7,13 @@ import { ClassEntity } from '../../generated/graphql-server/src/modules/class-en
 import { decode } from './decode'
 import { decode } from './decode'
 import {
 import {
   ClassEntityMap,
   ClassEntityMap,
+  IBatchOperation,
   ICategory,
   ICategory,
   IChannel,
   IChannel,
   ICreateEntityOperation,
   ICreateEntityOperation,
   IDBBlockId,
   IDBBlockId,
   IEntity,
   IEntity,
+  IFeaturedVideo,
   IHttpMediaLocation,
   IHttpMediaLocation,
   IJoystreamMediaLocation,
   IJoystreamMediaLocation,
   IKnownLicense,
   IKnownLicense,
@@ -38,6 +40,7 @@ import {
   ContentDirectoryKnownClasses,
   ContentDirectoryKnownClasses,
   licensePropertyNamesWithId,
   licensePropertyNamesWithId,
   mediaLocationPropertyNamesWithId,
   mediaLocationPropertyNamesWithId,
+  featuredVideoPropertyNamesWithId,
 } from './content-dir-consts'
 } from './content-dir-consts'
 import {
 import {
   updateCategoryEntityPropertyValues,
   updateCategoryEntityPropertyValues,
@@ -52,6 +55,7 @@ import {
   updateVideoMediaEncodingEntityPropertyValues,
   updateVideoMediaEncodingEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
+  updateFeaturedVideoEntityPropertyValues,
 } from './entity/update'
 } from './entity/update'
 
 
 import {
 import {
@@ -69,6 +73,7 @@ import {
   createLicense,
   createLicense,
   createMediaLocation,
   createMediaLocation,
   createBlockOrGetFromDatabase,
   createBlockOrGetFromDatabase,
+  createFeaturedVideo,
 } from './entity/create'
 } from './entity/create'
 import { getOrCreate } from './get-or-create'
 import { getOrCreate } from './get-or-create'
 
 
@@ -81,33 +86,35 @@ async function getNextEntityId(db: DB): Promise<number> {
   return e.nextId
   return e.nextId
 }
 }
 
 
+// eslint-disable-next-line @typescript-eslint/naming-convention
+export async function contentDirectory_TransactionFailed(db: DB, event: SubstrateEvent): Promise<void> {
+  debug(`TransactionFailed event: ${JSON.stringify(event)}`)
+
+  const failedOperationIndex = event.params[1].value as number
+  const operations = decode.getOperations(event)
+
+  const successfulOperations = operations.toArray().slice(0, failedOperationIndex)
+  if (!successfulOperations.length) return // No succesfull operations
+
+  await applyOperations(decode.getOperationsByTypes(successfulOperations), db, event)
+}
+
 // eslint-disable-next-line @typescript-eslint/naming-convention
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function contentDirectory_TransactionCompleted(db: DB, event: SubstrateEvent): Promise<void> {
 export async function contentDirectory_TransactionCompleted(db: DB, event: SubstrateEvent): Promise<void> {
   debug(`TransactionCompleted event: ${JSON.stringify(event)}`)
   debug(`TransactionCompleted 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 operations = decode.getOperations(event)
 
 
-  const {
-    addSchemaSupportToEntityOperations,
-    createEntityOperations,
-    updatePropertyValuesOperations,
-  } = decode.getOperations(event)
+  await applyOperations(decode.getOperationsByTypes(operations), db, event)
+}
 
 
+async function applyOperations(operations: IBatchOperation, db: DB, event: SubstrateEvent) {
+  const { addSchemaSupportToEntityOperations, createEntityOperations, updatePropertyValuesOperations } = operations
   // Create entities before adding schema support
   // 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
   // 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
   // Channel, Video etc.). For example if there is a property update operation there is no class id
-  await batchCreateClassEntities(db, block, createEntityOperations)
-
-  await batchAddSchemaSupportToEntity(db, createEntityOperations, addSchemaSupportToEntityOperations, block)
-
+  await batchCreateClassEntities(db, event.blockNumber, createEntityOperations)
+  await batchAddSchemaSupportToEntity(db, createEntityOperations, addSchemaSupportToEntityOperations, event.blockNumber)
   await batchUpdatePropertyValue(db, createEntityOperations, updatePropertyValuesOperations)
   await batchUpdatePropertyValue(db, createEntityOperations, updatePropertyValuesOperations)
 }
 }
 
 
@@ -256,6 +263,15 @@ async function batchAddSchemaSupportToEntity(
           )
           )
           break
           break
 
 
+        case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+          await createFeaturedVideo(
+            arg,
+            classEntityMap,
+            decode.setEntityPropertyValues<IFeaturedVideo>(properties, featuredVideoPropertyNamesWithId),
+            nextEntityIdBeforeTransaction
+          )
+          break
+
         default:
         default:
           console.log(`Unknown class name: ${className}`)
           console.log(`Unknown class name: ${className}`)
           break
           break
@@ -384,6 +400,14 @@ async function batchUpdatePropertyValue(db: DB, createEntityOperations: ICreateE
           entityIdBeforeTransaction
           entityIdBeforeTransaction
         )
         )
         break
         break
+      case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+        await updateFeaturedVideoEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IFeaturedVideo>(properties, featuredVideoPropertyNamesWithId),
+          entityIdBeforeTransaction
+        )
+        break
 
 
       default:
       default:
         console.log(`Unknown class name: ${className}`)
         console.log(`Unknown class name: ${className}`)

+ 16 - 6
query-node/mappings/types.ts

@@ -40,12 +40,12 @@ export interface IReference {
 }
 }
 
 
 export interface IChannel {
 export interface IChannel {
-  title: string
+  handle: string
   description: string
   description: string
-  coverPhotoURL: string
-  avatarPhotoURL: string
+  coverPhotoUrl: string
+  avatarPhotoUrl: string
   isPublic: boolean
   isPublic: boolean
-  isCurated: boolean
+  isCurated?: boolean
   language?: IReference
   language?: IReference
 }
 }
 
 
@@ -100,14 +100,14 @@ export interface IVideo {
   description: string
   description: string
   duration: number
   duration: number
   skippableIntroDuration?: number
   skippableIntroDuration?: number
-  thumbnailURL: string
+  thumbnailUrl: string
   language?: IReference
   language?: IReference
   // referenced entity's id
   // referenced entity's id
   media?: IReference
   media?: IReference
   hasMarketing?: boolean
   hasMarketing?: boolean
   publishedBeforeJoystream?: number
   publishedBeforeJoystream?: number
   isPublic: boolean
   isPublic: boolean
-  isCurated: boolean
+  isCurated?: boolean
   isExplicit: boolean
   isExplicit: boolean
   license?: IReference
   license?: IReference
 }
 }
@@ -115,6 +115,7 @@ export interface IVideo {
 export interface ILicense {
 export interface ILicense {
   knownLicense?: IReference
   knownLicense?: IReference
   userDefinedLicense?: IReference
   userDefinedLicense?: IReference
+  attribution?: string
 }
 }
 
 
 export interface IMediaLocation {
 export interface IMediaLocation {
@@ -197,3 +198,12 @@ export interface IDBBlockId {
 }
 }
 
 
 export type ClassEntityMap = Map<string, IEntity[]>
 export type ClassEntityMap = Map<string, IEntity[]>
+
+export interface IFeaturedVideo {
+  video?: IReference
+}
+
+export interface IKnownClass {
+  name: string
+  classId: number
+}

+ 6 - 7
query-node/package.json

@@ -6,17 +6,16 @@
 		"build": "./build.sh",
 		"build": "./build.sh",
 		"test": "echo \"Error: no test specified\" && exit 1",
 		"test": "echo \"Error: no test specified\" && exit 1",
 		"clean": "rm -rf ./generated",
 		"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)",
+		"processor:start": "./processor-start.sh",
+		"indexer:start": "(cd ./generated/indexer && yarn && DEBUG=${DEBUG} yarn start:indexer --env ../../../.env)",
 		"server:start:dev": "(cd ./generated/graphql-server && yarn start:dev)",
 		"server:start:dev": "(cd ./generated/graphql-server && yarn start:dev)",
 		"server:start:prod": "(cd ./generated/graphql-server && yarn start:prod)",
 		"server:start:prod": "(cd ./generated/graphql-server && yarn start:prod)",
 		"configure": "(cd ./generated/graphql-server && yarn config:dev)",
 		"configure": "(cd ./generated/graphql-server && yarn config:dev)",
-		"db:up": "docker-compose up -d db",
+		"db:up": "(cd ../ && docker-compose up -d db)",
 		"db:drop": "(cd ./generated/graphql-server && yarn db:drop)",
 		"db:drop": "(cd ./generated/graphql-server && yarn db:drop)",
+		"db:migrate": "./db-migrate.sh",
 		"db:schema:migrate": "(cd ./generated/graphql-server && yarn db:create && yarn db:sync && yarn db:migrate)",
 		"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:indexer:migrate": "(cd ./generated/indexer && yarn db:migrate)",
-		"db:migrate": "yarn db:schema:migrate && yarn db:indexer:migrate",
-		"codegen:all": "yarn hydra-cli codegen --no-install && cp indexer-tsconfig.json generated/indexer/tsconfig.json",
 		"codegen:indexer": "yarn hydra-cli codegen --no-install --no-graphql && cp indexer-tsconfig.json generated/indexer/tsconfig.json",
 		"codegen:indexer": "yarn hydra-cli codegen --no-install --no-graphql && cp indexer-tsconfig.json generated/indexer/tsconfig.json",
 		"codegen:server": "yarn hydra-cli codegen --no-install --no-indexer",
 		"codegen:server": "yarn hydra-cli codegen --no-install --no-indexer",
 		"cd-classes": "ts-node scripts/get-class-id-and-name.ts"
 		"cd-classes": "ts-node scripts/get-class-id-and-name.ts"
@@ -24,10 +23,10 @@
 	"author": "",
 	"author": "",
 	"license": "ISC",
 	"license": "ISC",
 	"devDependencies": {
 	"devDependencies": {
-		"@dzlzv/hydra-cli": "^0.0.20"
+		"@dzlzv/hydra-cli": "^0.0.24"
 	},
 	},
 	"dependencies": {
 	"dependencies": {
-		"@dzlzv/hydra-indexer-lib": "^0.0.19-legacy.1.26.1",
+		"@dzlzv/hydra-indexer-lib": "^0.0.21-legacy.1.26.1",
 		"@joystream/types": "^0.14.0",
 		"@joystream/types": "^0.14.0",
 		"@types/bn.js": "^4.11.6",
 		"@types/bn.js": "^4.11.6",
 		"@types/debug": "^4.1.5",
 		"@types/debug": "^4.1.5",

+ 15 - 0
query-node/processor-start.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+set -e
+
+SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
+cd $SCRIPT_PATH
+
+# set +a
+# . ../.env
+# export TYPEORM_DATABASE=${PROCESSOR_DB_NAME}
+
+export TYPEORM_DATABASE=${PROCESSOR_DB_NAME:=query_node_processor}
+
+cd ./generated/indexer
+yarn
+DEBUG=${DEBUG} yarn start:processor --env ../../../.env

+ 21 - 10
query-node/run-tests.sh

@@ -4,31 +4,42 @@ set -e
 SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
 SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
 cd $SCRIPT_PATH
 cd $SCRIPT_PATH
 
 
+# Only run codegen if no generated files found
+[ ! -d "generated/" ] && yarn build
+
+# Make sure typeorm is available.. it get removed when yarn is run again
+# typeorm commandline is used by db:migrate step below.
+ln -s ../../../../../node_modules/typeorm/cli.js generated/graphql-server/node_modules/.bin/typeorm || :
+
+set -a
+. ../.env
+set +a
+
+# Clean start
+docker-compose down -v
+
 function cleanup() {
 function cleanup() {
     # Show tail end of logs for the processor and indexer containers to
     # Show tail end of logs for the processor and indexer containers to
     # see any possible errors
     # see any possible errors
     (echo "## Processor Logs ##" && docker logs joystream_processor_1 --tail 50) || :
     (echo "## Processor Logs ##" && docker logs joystream_processor_1 --tail 50) || :
     (echo "## Indexer Logs ##" && docker logs joystream_indexer_1 --tail 50) || :
     (echo "## Indexer Logs ##" && docker logs joystream_indexer_1 --tail 50) || :
+    (echo "## Indexer API Gateway Logs ##" && docker logs joystream_indexer-api-gateway_1 --tail 50) || :
     docker-compose down -v
     docker-compose down -v
 }
 }
 
 
 trap cleanup EXIT
 trap cleanup EXIT
 
 
+# We expect docker image to be started by test runner
 export WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944/
 export WS_PROVIDER_ENDPOINT_URI=ws://joystream-node:9944/
 
 
-# Only run codegen if no generated files found
-[ ! -d "generated/" ] && yarn build
-
-# Make sure typeorm is available.. it get removed again when yarn is run again
-# typeorm commandline is used by db:migrate step below.
-ln -s ../../../../../node_modules/typeorm/cli.js generated/graphql-server/node_modules/.bin/typeorm || :
+# Bring up db
+docker-compose up -d db
 
 
-# clean start
-docker-compose down -v
+# Migrate the databases
+yarn workspace query-node-root db:migrate
 
 
-docker-compose up -d db
-yarn db:migrate
 docker-compose up -d graphql-server
 docker-compose up -d graphql-server
+
 # Starting up processor will bring up all services it depends on
 # Starting up processor will bring up all services it depends on
 docker-compose up -d processor
 docker-compose up -d processor
 
 

+ 80 - 32
query-node/schema.graphql

@@ -18,7 +18,7 @@ type Member @entity {
   id: ID!
   id: ID!
 
 
   "The unique handle chosen by member"
   "The unique handle chosen by member"
-  handle: String @unique @fulltext(query: "handles")
+  handle: String @unique @fulltext(query: "membersByHandle")
 
 
   "A Url to member's Avatar image"
   "A Url to member's Avatar image"
   avatarUri: String
   avatarUri: String
@@ -84,16 +84,16 @@ type Channel @entity {
   # owner: Member!
   # owner: Member!
 
 
   "The title of the Channel"
   "The title of the Channel"
-  title: String! @fulltext(query: "titles")
+  handle: String! @fulltext(query: "search")
 
 
   "The description of a Channel"
   "The description of a Channel"
   description: String!
   description: String!
 
 
   "Url for Channel's cover (background) photo. Recommended ratio: 16:9."
   "Url for Channel's cover (background) photo. Recommended ratio: 16:9."
-  coverPhotoURL: String!
+  coverPhotoUrl: String
 
 
   "Channel's avatar photo."
   "Channel's avatar photo."
-  avatarPhotoURL: String!
+  avatarPhotoUrl: String
 
 
   "Flag signaling whether a channel is public."
   "Flag signaling whether a channel is public."
   isPublic: Boolean!
   isPublic: Boolean!
@@ -102,7 +102,7 @@ type Channel @entity {
   isCurated: Boolean!
   isCurated: Boolean!
 
 
   "The primary langauge of the channel's content"
   "The primary langauge of the channel's content"
-  language: Language!
+  language: Language
 
 
   videos: [Video!] @derivedFrom(field: "channel")
   videos: [Video!] @derivedFrom(field: "channel")
 
 
@@ -114,7 +114,7 @@ type Category @entity {
   id: ID!
   id: ID!
 
 
   "The name of the category"
   "The name of the category"
-  name: String! @unique @fulltext(query: "names")
+  name: String! @unique @fulltext(query: "categoriesByName")
 
 
   "The description of the category"
   "The description of the category"
   description: String
   description: String
@@ -134,7 +134,7 @@ type VideoMediaEncoding @entity {
   happenedIn: Block!
   happenedIn: Block!
 }
 }
 
 
-type KnownLicense @entity {
+type KnownLicenseEntity @entity {
   "Runtime entity identifier (EntityId)"
   "Runtime entity identifier (EntityId)"
   id: ID!
   id: ID!
 
 
@@ -153,7 +153,7 @@ type KnownLicense @entity {
   happenedIn: Block!
   happenedIn: Block!
 }
 }
 
 
-type UserDefinedLicense @entity {
+type UserDefinedLicenseEntity @entity {
   "Runtime entity identifier (EntityId)"
   "Runtime entity identifier (EntityId)"
   id: ID!
   id: ID!
 
 
@@ -163,39 +163,24 @@ type UserDefinedLicense @entity {
   happenedIn: Block!
   happenedIn: Block!
 }
 }
 
 
-type License @entity {
-  "Runtime entity identifier (EntityId)"
-  id: ID!
-
-  # One of the following field will be non-null
-
-  "Reference to a known license"
-  knownLicense: KnownLicense
-
-  "Reference to user-defined license"
-  userdefinedLicense: UserDefinedLicense
-
-  happenedIn: Block!
-}
-
-type MediaLocation @entity {
+type MediaLocationEntity @entity {
   "Runtime entity identifier (EntityId)"
   "Runtime entity identifier (EntityId)"
   id: ID!
   id: ID!
 
 
   # One of the following field will be non-null
   # One of the following field will be non-null
 
 
   "A reference to HttpMediaLocation"
   "A reference to HttpMediaLocation"
-  httpMediaLocation: HttpMediaLocation
+  httpMediaLocation: HttpMediaLocationEntity
 
 
   "A reference to JoystreamMediaLocation"
   "A reference to JoystreamMediaLocation"
-  joystreamMediaLocation: JoystreamMediaLocation
+  joystreamMediaLocation: JoystreamMediaLocationEntity
 
 
-  videoMedia: VideoMedia @derivedFrom(field: "location")
+  videoMedia: VideoMedia @derivedFrom(field: "locationEntity")
 
 
   happenedIn: Block!
   happenedIn: Block!
 }
 }
 
 
-type JoystreamMediaLocation @entity {
+type JoystreamMediaLocationEntity @entity {
   "Runtime entity identifier (EntityId)"
   "Runtime entity identifier (EntityId)"
   id: ID!
   id: ID!
 
 
@@ -205,7 +190,7 @@ type JoystreamMediaLocation @entity {
   happenedIn: Block!
   happenedIn: Block!
 }
 }
 
 
-type HttpMediaLocation @entity {
+type HttpMediaLocationEntity @entity {
   "Runtime entity identifier (EntityId)"
   "Runtime entity identifier (EntityId)"
   id: ID!
   id: ID!
 
 
@@ -239,6 +224,8 @@ type VideoMedia @entity {
   "Location of the video media object"
   "Location of the video media object"
   location: MediaLocation!
   location: MediaLocation!
 
 
+  locationEntity: MediaLocationEntity
+
   happenedIn: Block!
   happenedIn: Block!
 }
 }
 
 
@@ -253,7 +240,7 @@ type Video @entity {
   category: Category!
   category: Category!
 
 
   "The title of the video"
   "The title of the video"
-  title: String! @fulltext(query: "titles")
+  title: String! @fulltext(query: "search")
 
 
   "The description of the Video"
   "The description of the Video"
   description: String!
   description: String!
@@ -265,7 +252,7 @@ type Video @entity {
   skippableIntroDuration: Int
   skippableIntroDuration: Int
 
 
   "Video thumbnail url (recommended ratio: 16:9)"
   "Video thumbnail url (recommended ratio: 16:9)"
-  thumbnailURL: String!
+  thumbnailUrl: String!
 
 
   "Video's main langauge"
   "Video's main langauge"
   language: Language
   language: Language
@@ -288,7 +275,68 @@ type Video @entity {
   "Whether the Video contains explicit material."
   "Whether the Video contains explicit material."
   isExplicit: Boolean!
   isExplicit: Boolean!
 
 
-  license: License!
+  license: LicenseEntity!
+
+  happenedIn: Block!
+
+  "Is video featured or not"
+  isFeatured: Boolean!
+
+  featured: FeaturedVideo @derivedFrom(field: "video")
+}
+
+type JoystreamMediaLocation @variant {
+  "Id of the data object in the Joystream runtime dataDirectory module"
+  dataObjectId: String!
+}
+
+type HttpMediaLocation @variant {
+  "The http url pointing to the media"
+  url: String!
+
+  "The port to use when connecting to the http url (defaults to 80)"
+  port: Int
+}
+
+union MediaLocation = HttpMediaLocation | JoystreamMediaLocation
+
+type KnownLicense @variant {
+  "Short, commonly recognized code of the licence (ie. CC_BY_SA)"
+  code: String!
+
+  "Full, descriptive name of the license (ie. Creative Commons - Attribution-NonCommercial-NoDerivs)"
+  name: String
+
+  "Short description of the license conditions"
+  description: String
+
+  "An url pointing to full license content"
+  url: String
+}
+
+type UserDefinedLicense @variant {
+  "Custom license content"
+  content: String!
+}
+
+union License = KnownLicense | UserDefinedLicense
+
+type LicenseEntity @entity {
+  "Runtime entity identifier (EntityId)"
+  id: ID!
+
+  type: License!
+
+  "Attribution (if required by the license)"
+  attribution: String
 
 
   happenedIn: Block!
   happenedIn: Block!
 }
 }
+
+type FeaturedVideo @entity {
+  "Runtime entity identifier (EntityId)"
+  id: ID!
+
+  "Reference to a video"
+  video: Video!
+}

+ 0 - 974
query-node/typedefs.json

@@ -1,974 +0,0 @@
-{
-    "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"
-    },
-    "ClassOf": {
-        "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"
-    },
-    "EntityOf": {
-        "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"
-        }
-    },
-    "InputEntityValuesMap": "BTreeMap<PropertyId,InputPropertyValue>",
-    "ClassPermissionsType": "Null",
-    "ClassPropertyValue": "Null",
-    "Operation": "Null",
-    "ReferenceConstraint": "Null"
-}

+ 110 - 45
runtime-modules/content-directory/src/lib.rs

@@ -142,7 +142,11 @@ use codec::{Codec, Decode, Encode};
 use frame_support::storage::IterableStorageMap;
 use frame_support::storage::IterableStorageMap;
 
 
 use frame_support::{
 use frame_support::{
-    decl_event, decl_module, decl_storage, dispatch::DispatchResult, ensure, traits::Get, Parameter,
+    decl_event, decl_module, decl_storage,
+    dispatch::{DispatchError, DispatchResult},
+    ensure,
+    traits::Get,
+    Parameter,
 };
 };
 #[cfg(feature = "std")]
 #[cfg(feature = "std")]
 pub use serde::{Deserialize, Serialize};
 pub use serde::{Deserialize, Serialize};
@@ -1625,64 +1629,115 @@ decl_module! {
             Ok(())
             Ok(())
         }
         }
 
 
-        /// Batch transaction
-        #[weight = 10_000_000] // TODO: adjust weight
-        pub fn transaction(origin, actor: Actor<T::CuratorGroupId, T::CuratorId, T::MemberId>, operations: Vec<OperationType<T>>) -> DispatchResult {
+       /// Batch transaction
+       #[weight = 10_000_000] // TODO: adjust weight
+       pub fn transaction(origin, actor: Actor<T::CuratorGroupId, T::CuratorId, T::MemberId>, operations: Vec<OperationType<T>>) -> DispatchResult {
 
 
-            // Ensure maximum number of operations during atomic batching limit not reached
-            Self::ensure_number_of_operations_during_atomic_batching_limit_not_reached(&operations)?;
+           // Ensure maximum number of operations during atomic batching limit not reached
+           Self::ensure_number_of_operations_during_atomic_batching_limit_not_reached(&operations)?;
 
 
-            //
-            // == MUTATION SAFE ==
-            //
+           //
+           // == MUTATION SAFE ==
+           //
 
 
-            // This BTreeMap holds the T::EntityId of the entity created as a result of executing a `CreateEntity` `Operation`
-            let mut entity_created_in_operation = BTreeMap::new();
+           // This BTreeMap holds the T::EntityId of the entity created as a result of executing a `CreateEntity` `Operation`
+           let mut entity_created_in_operation = BTreeMap::new();
 
 
-            // Create raw origin
-            let raw_origin = origin.into().map_err(|_| Error::<T>::OriginCanNotBeMadeIntoRawOrigin)?;
+           // Create raw origin
+           let raw_origin = origin.into().map_err(|_| Error::<T>::OriginCanNotBeMadeIntoRawOrigin)?;
 
 
-            for (index, operation_type) in operations.into_iter().enumerate() {
-                let origin = T::Origin::from(raw_origin.clone());
-                match operation_type {
-                    OperationType::CreateEntity(create_entity_operation) => {
-                        Self::create_entity(origin, create_entity_operation.class_id, actor)?;
+           for (index, operation_type) in operations.into_iter().enumerate() {
+               let origin = T::Origin::from(raw_origin.clone());
+               match operation_type {
+                   OperationType::CreateEntity(create_entity_operation) => {
+                        Self::ensure_transaction_failed_event(
+                            Self::create_entity(origin, create_entity_operation.class_id, actor),
+                            actor,
+                            index
+                        )?;
 
 
                         // entity id of newly created entity
                         // entity id of newly created entity
                         let entity_id = Self::next_entity_id() - T::EntityId::one();
                         let entity_id = Self::next_entity_id() - T::EntityId::one();
                         entity_created_in_operation.insert(index, entity_id);
                         entity_created_in_operation.insert(index, entity_id);
-                    },
-                    OperationType::AddSchemaSupportToEntity(add_schema_support_to_entity_operation) => {
-                        let entity_id = operations::parametrized_entity_to_entity_id(
-                            &entity_created_in_operation, add_schema_support_to_entity_operation.entity_id
-                        )?;
-                        let schema_id = add_schema_support_to_entity_operation.schema_id;
-                        let property_values = operations::parametrized_property_values_to_property_values(
-                            &entity_created_in_operation, add_schema_support_to_entity_operation.parametrized_property_values
+                   },
+                   OperationType::AddSchemaSupportToEntity(add_schema_support_to_entity_operation) => {
+                       let entity_id =
+                            Self::ensure_transaction_failed_event(
+                                operations::parametrized_entity_to_entity_id(
+                                    &entity_created_in_operation, add_schema_support_to_entity_operation.entity_id
+                                ),
+                                actor,
+                                index
+                            )?;
+
+                       let schema_id = add_schema_support_to_entity_operation.schema_id;
+
+                       let property_values =
+                            Self::ensure_transaction_failed_event(
+                                operations::parametrized_property_values_to_property_values(
+                                    &entity_created_in_operation, add_schema_support_to_entity_operation.parametrized_property_values
+                                ),
+                                actor,
+                                index
+                            )?;
+                        Self::ensure_transaction_failed_event(
+                            Self::add_schema_support_to_entity(origin, actor, entity_id, schema_id, property_values),
+                            actor,
+                            index
                         )?;
                         )?;
-                        Self::add_schema_support_to_entity(origin, actor, entity_id, schema_id, property_values)?;
-                    },
-                    OperationType::UpdatePropertyValues(update_property_values_operation) => {
-                        let entity_id = operations::parametrized_entity_to_entity_id(
-                            &entity_created_in_operation, update_property_values_operation.entity_id
-                        )?;
-                        let property_values = operations::parametrized_property_values_to_property_values(
-                            &entity_created_in_operation, update_property_values_operation.new_parametrized_property_values
-                        )?;
-                        Self::update_entity_property_values(origin, actor, entity_id, property_values)?;
-                    },
-                }
-            }
-
-            // Trigger event
-            Self::deposit_event(RawEvent::TransactionCompleted(actor));
-
-            Ok(())
-        }
+                   },
+                   OperationType::UpdatePropertyValues(update_property_values_operation) => {
+                       let entity_id =
+                            Self::ensure_transaction_failed_event(
+                                operations::parametrized_entity_to_entity_id(
+                                    &entity_created_in_operation, update_property_values_operation.entity_id
+                                ),
+                                actor,
+                                index
+                            )?;
+
+                       let property_values =
+                            Self::ensure_transaction_failed_event(
+                                operations::parametrized_property_values_to_property_values(
+                                    &entity_created_in_operation, update_property_values_operation.new_parametrized_property_values
+                                ),
+                                actor,
+                                index
+                            )?;
+
+                       Self::ensure_transaction_failed_event(
+                            Self::update_entity_property_values(origin, actor, entity_id, property_values),
+                            actor,
+                            index
+                       )?;
+                   },
+               }
+           }
+
+           // Trigger event
+           Self::deposit_event(RawEvent::TransactionCompleted(actor));
+
+           Ok(())
+       }
     }
     }
 }
 }
 
 
 impl<T: Trait> Module<T> {
 impl<T: Trait> Module<T> {
+    /// Deposits an `TransactionFailed` event if an error during `transaction` extrinsic execution occured
+    fn ensure_transaction_failed_event<R, E: Into<DispatchError>>(
+        result: Result<R, E>,
+        actor: Actor<T::CuratorGroupId, T::CuratorId, T::MemberId>,
+        index: usize,
+    ) -> Result<R, DispatchError> {
+        match result {
+            Err(e) => {
+                Self::deposit_event(RawEvent::TransactionFailed(actor, index as u32));
+                Err(e.into())
+            }
+            Ok(result) => Ok(result),
+        }
+    }
+
     /// Updates corresponding `Entity` `reference_counter` by `reference_counter_delta`.
     /// Updates corresponding `Entity` `reference_counter` by `reference_counter_delta`.
     fn update_entity_rc(
     fn update_entity_rc(
         entity_id: T::EntityId,
         entity_id: T::EntityId,
@@ -2809,6 +2864,14 @@ impl<T: Trait> Module<T> {
     }
     }
 }
 }
 
 
+impl<T: Trait> Module<T> {
+    pub fn set_initial_ids_to_one() {
+        <NextEntityId<T>>::put(T::EntityId::one());
+        <NextClassId<T>>::put(T::ClassId::one());
+        <NextCuratorGroupId<T>>::put(T::CuratorGroupId::one());
+    }
+}
+
 decl_event!(
 decl_event!(
     pub enum Event<T>
     pub enum Event<T>
     where
     where
@@ -2827,6 +2890,7 @@ decl_event!(
         Nonce = <T as Trait>::Nonce,
         Nonce = <T as Trait>::Nonce,
         SideEffects = Option<ReferenceCounterSideEffects<T>>,
         SideEffects = Option<ReferenceCounterSideEffects<T>>,
         SideEffect = Option<(<T as Trait>::EntityId, EntityReferenceCounterSideEffect)>,
         SideEffect = Option<(<T as Trait>::EntityId, EntityReferenceCounterSideEffect)>,
+        FailedAt = u32,
     {
     {
         CuratorGroupAdded(CuratorGroupId),
         CuratorGroupAdded(CuratorGroupId),
         CuratorGroupRemoved(CuratorGroupId),
         CuratorGroupRemoved(CuratorGroupId),
@@ -2851,5 +2915,6 @@ decl_event!(
         InsertedAtVectorIndex(Actor, EntityId, PropertyId, VecMaxLength, Nonce, SideEffect),
         InsertedAtVectorIndex(Actor, EntityId, PropertyId, VecMaxLength, Nonce, SideEffect),
         EntityOwnershipTransfered(EntityId, EntityController, SideEffects),
         EntityOwnershipTransfered(EntityId, EntityController, SideEffects),
         TransactionCompleted(Actor),
         TransactionCompleted(Actor),
+        TransactionFailed(Actor, FailedAt),
     }
     }
 );
 );

+ 2 - 1
runtime-modules/content-directory/src/mock.rs

@@ -433,13 +433,14 @@ type RawTestEvent = RawEvent<
     Nonce,
     Nonce,
     Option<ReferenceCounterSideEffects<Runtime>>,
     Option<ReferenceCounterSideEffects<Runtime>>,
     Option<(EntityId, EntityReferenceCounterSideEffect)>,
     Option<(EntityId, EntityReferenceCounterSideEffect)>,
+    u32,
 >;
 >;
 
 
 pub fn get_test_event(raw_event: RawTestEvent) -> TestEvent {
 pub fn get_test_event(raw_event: RawTestEvent) -> TestEvent {
     TestEvent::test_events(raw_event)
     TestEvent::test_events(raw_event)
 }
 }
 
 
-pub fn assert_event_success(tested_event: TestEvent, number_of_events_after_call: usize) {
+pub fn assert_event(tested_event: TestEvent, number_of_events_after_call: usize) {
     // Ensure  runtime events length is equal to expected number of events after call
     // Ensure  runtime events length is equal to expected number of events after call
     assert_eq!(System::events().len(), number_of_events_after_call);
     assert_eq!(System::events().len(), number_of_events_after_call);
 
 

+ 1 - 1
runtime-modules/content-directory/src/tests.rs

@@ -145,7 +145,7 @@ pub fn add_entity_schemas_support() -> (
     ));
     ));
 
 
     // Last event checked
     // Last event checked
-    assert_event_success(
+    assert_event(
         entity_schema_support_added_event,
         entity_schema_support_added_event,
         number_of_events_before_calls + 2,
         number_of_events_before_calls + 2,
     );
     );

+ 1 - 1
runtime-modules/content-directory/src/tests/add_class_schema.rs

@@ -52,7 +52,7 @@ fn add_class_schema_success() {
             get_test_event(RawEvent::ClassSchemaAdded(FIRST_CLASS_ID, SECOND_SCHEMA_ID));
             get_test_event(RawEvent::ClassSchemaAdded(FIRST_CLASS_ID, SECOND_SCHEMA_ID));
 
 
         // Last event checked
         // Last event checked
-        assert_event_success(class_schema_added_event, number_of_events_before_call + 2);
+        assert_event(class_schema_added_event, number_of_events_before_call + 2);
     })
     })
 }
 }
 
 

+ 1 - 1
runtime-modules/content-directory/src/tests/add_curator_group.rs

@@ -31,7 +31,7 @@ fn add_curator_group_success() {
             get_test_event(RawEvent::CuratorGroupAdded(FIRST_CURATOR_GROUP_ID));
             get_test_event(RawEvent::CuratorGroupAdded(FIRST_CURATOR_GROUP_ID));
 
 
         // Event checked
         // Event checked
-        assert_event_success(
+        assert_event(
             curator_group_created_event,
             curator_group_created_event,
             number_of_events_before_call + 1,
             number_of_events_before_call + 1,
         );
         );

+ 1 - 1
runtime-modules/content-directory/src/tests/add_curator_to_group.rs

@@ -31,7 +31,7 @@ fn add_curator_to_group_success() {
         ));
         ));
 
 
         // Event checked
         // Event checked
-        assert_event_success(
+        assert_event(
             curator_group_curator_added_event,
             curator_group_curator_added_event,
             number_of_events_before_call + 1,
             number_of_events_before_call + 1,
         );
         );

+ 1 - 1
runtime-modules/content-directory/src/tests/add_maintainer_to_class.rs

@@ -35,7 +35,7 @@ fn add_maintainer_to_class_success() {
         ));
         ));
 
 
         // Event checked
         // Event checked
-        assert_event_success(maintainer_added_event, number_of_events_before_call + 1);
+        assert_event(maintainer_added_event, number_of_events_before_call + 1);
     })
     })
 }
 }
 
 

+ 1 - 1
runtime-modules/content-directory/src/tests/clear_entity_property_vector.rs

@@ -53,7 +53,7 @@ fn clear_entity_property_vector_success() {
         ));
         ));
 
 
         // Last event checked
         // Last event checked
-        assert_event_success(
+        assert_event(
             entity_property_vector_cleared_event,
             entity_property_vector_cleared_event,
             number_of_events_before_calls + 1,
             number_of_events_before_calls + 1,
         );
         );

+ 1 - 1
runtime-modules/content-directory/src/tests/create_class.rs

@@ -23,7 +23,7 @@ fn create_class_success() {
         let class_created_event = get_test_event(RawEvent::ClassCreated(FIRST_CLASS_ID));
         let class_created_event = get_test_event(RawEvent::ClassCreated(FIRST_CLASS_ID));
 
 
         // Event checked
         // Event checked
-        assert_event_success(class_created_event, number_of_events_before_call + 1);
+        assert_event(class_created_event, number_of_events_before_call + 1);
     })
     })
 }
 }
 
 

+ 1 - 1
runtime-modules/content-directory/src/tests/create_entity.rs

@@ -71,7 +71,7 @@ fn create_entity_success() {
             get_test_event(RawEvent::EntityCreated(actor, next_entity_id() - 1));
             get_test_event(RawEvent::EntityCreated(actor, next_entity_id() - 1));
 
 
         // Last event checked
         // Last event checked
-        assert_event_success(entity_created_event, number_of_events_before_call + 1);
+        assert_event(entity_created_event, number_of_events_before_call + 1);
     })
     })
 }
 }
 
 

+ 1 - 1
runtime-modules/content-directory/src/tests/insert_at_entity_property_vector.rs

@@ -64,7 +64,7 @@ fn insert_at_entity_property_vector_success() {
         ));
         ));
 
 
         // Last event checked
         // Last event checked
-        assert_event_success(
+        assert_event(
             inserted_at_vector_index_event,
             inserted_at_vector_index_event,
             number_of_events_before_calls + 1,
             number_of_events_before_calls + 1,
         );
         );

+ 1 - 1
runtime-modules/content-directory/src/tests/remove_at_entity_property_vector.rs

@@ -59,7 +59,7 @@ fn remove_at_entity_property_vector_success() {
         ));
         ));
 
 
         // Last event checked
         // Last event checked
-        assert_event_success(
+        assert_event(
             removed_at_vector_index_event,
             removed_at_vector_index_event,
             number_of_events_before_calls + 1,
             number_of_events_before_calls + 1,
         );
         );

+ 1 - 1
runtime-modules/content-directory/src/tests/remove_curator_from_group.rs

@@ -44,7 +44,7 @@ fn remove_curator_from_group_success() {
         ));
         ));
 
 
         // Event checked
         // Event checked
-        assert_event_success(
+        assert_event(
             curator_group_curator_removed_event,
             curator_group_curator_removed_event,
             number_of_events_before_call + 1,
             number_of_events_before_call + 1,
         );
         );

+ 1 - 1
runtime-modules/content-directory/src/tests/remove_curator_group.rs

@@ -24,7 +24,7 @@ fn remove_curator_group_success() {
             get_test_event(RawEvent::CuratorGroupRemoved(FIRST_CURATOR_GROUP_ID));
             get_test_event(RawEvent::CuratorGroupRemoved(FIRST_CURATOR_GROUP_ID));
 
 
         // Event checked
         // Event checked
-        assert_event_success(
+        assert_event(
             curator_group_removed_event,
             curator_group_removed_event,
             number_of_events_before_call + 1,
             number_of_events_before_call + 1,
         );
         );

+ 1 - 1
runtime-modules/content-directory/src/tests/remove_entity.rs

@@ -38,7 +38,7 @@ fn remove_entity_success() {
             get_test_event(RawEvent::EntityRemoved(actor, next_entity_id() - 1));
             get_test_event(RawEvent::EntityRemoved(actor, next_entity_id() - 1));
 
 
         // Last event checked
         // Last event checked
-        assert_event_success(entity_removed_event, number_of_events_before_call + 1);
+        assert_event(entity_removed_event, number_of_events_before_call + 1);
     })
     })
 }
 }
 
 

+ 1 - 1
runtime-modules/content-directory/src/tests/remove_maintainer_from_class.rs

@@ -54,7 +54,7 @@ fn remove_maintainer_from_class_success() {
         ));
         ));
 
 
         // Event checked
         // Event checked
-        assert_event_success(maintainer_removed_event, number_of_events_before_call + 1);
+        assert_event(maintainer_removed_event, number_of_events_before_call + 1);
     })
     })
 }
 }
 
 

+ 1 - 1
runtime-modules/content-directory/src/tests/set_curator_group_status.rs

@@ -31,7 +31,7 @@ fn set_curator_group_status_success() {
         ));
         ));
 
 
         // Event checked
         // Event checked
-        assert_event_success(
+        assert_event(
             curator_group_status_set_event,
             curator_group_status_set_event,
             number_of_events_before_call + 1,
             number_of_events_before_call + 1,
         );
         );

+ 53 - 4
runtime-modules/content-directory/src/tests/transaction.rs

@@ -62,12 +62,11 @@ fn transaction_success() {
 
 
         // Runtime tested state after call
         // Runtime tested state after call
 
 
-        let entity_ownership_transfered_event =
-            get_test_event(RawEvent::TransactionCompleted(actor));
+        let transaction_completed_event = get_test_event(RawEvent::TransactionCompleted(actor));
 
 
         // Last event checked
         // Last event checked
-        assert_event_success(
-            entity_ownership_transfered_event,
+        assert_event(
+            transaction_completed_event,
             number_of_events_before_calls + operations_count + 1,
             number_of_events_before_calls + operations_count + 1,
         );
         );
     })
     })
@@ -104,3 +103,53 @@ fn transaction_limit_reached() {
         );
         );
     })
     })
 }
 }
+
+#[test]
+fn transaction_failed() {
+    with_test_externalities(|| {
+        // Create class with default permissions
+        assert_ok!(create_simple_class(LEAD_ORIGIN, ClassType::Valid));
+
+        let operation = OperationType::CreateEntity(CreateEntityOperation {
+            class_id: FIRST_CLASS_ID,
+        });
+
+        let failed_operation = OperationType::CreateEntity(CreateEntityOperation {
+            class_id: UNKNOWN_CLASS_ID,
+        });
+
+        let operations = vec![
+            operation.clone(),
+            operation.clone(),
+            failed_operation,
+            operation,
+        ];
+
+        // Runtime state before tested call
+
+        // Events number before tested call
+        let number_of_events_before_call = System::events().len();
+
+        let actor = Actor::Lead;
+
+        // Make an attempt to complete transaction with CreateEntity operation, when provided class_id does not exist on runtime level
+        let transaction_result = transaction(LEAD_ORIGIN, actor, operations.clone());
+
+        let failed_operation_index = 2;
+
+        // Failure checked
+
+        // Ensure  call result is equal to expected error
+        assert_err!(transaction_result, Error::<Runtime>::ClassNotFound);
+
+        let transaction_failed_event =
+            get_test_event(RawEvent::TransactionFailed(actor, failed_operation_index));
+
+        // Last event checked
+        assert_event(
+            transaction_failed_event,
+            // two operations succeded and one TransactionFailed event
+            number_of_events_before_call + operations[..failed_operation_index as usize].len() + 1,
+        );
+    })
+}

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