Browse Source

Merge pull request #1877 from Joystream/babylon

Update Olympia from Babylon
shamil-gadelshin 4 years ago
parent
commit
a99b164c9a
100 changed files with 1762 additions and 1888 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. 3 3
      build.sh
  6. 225 63
      cli/README.md
  7. 1 1
      cli/package.json
  8. 0 3
      cli/src/@types/@ffmpeg-installer/ffmpeg/index.d.ts
  9. 3 0
      cli/src/@types/@ffprobe-installer/ffprobe/index.d.ts
  10. 5 2
      cli/src/base/AccountsCommandBase.ts
  11. 33 13
      cli/src/base/ContentDirectoryCommandBase.ts
  12. 10 5
      cli/src/base/MediaCommandBase.ts
  13. 19 4
      cli/src/commands/account/choose.ts
  14. 3 3
      cli/src/commands/content-directory/addClassSchema.ts
  15. 3 3
      cli/src/commands/content-directory/createClass.ts
  16. 2 2
      cli/src/commands/content-directory/entity.ts
  17. 15 8
      cli/src/commands/content-directory/initialize.ts
  18. 2 2
      cli/src/commands/content-directory/updateClassPermissions.ts
  19. 12 7
      cli/src/commands/media/createChannel.ts
  20. 3 3
      cli/src/commands/media/curateContent.ts
  21. 36 0
      cli/src/commands/media/featuredVideos.ts
  22. 2 2
      cli/src/commands/media/myChannels.ts
  23. 1 1
      cli/src/commands/media/myVideos.ts
  24. 3 3
      cli/src/commands/media/removeChannel.ts
  25. 1 1
      cli/src/commands/media/removeVideo.ts
  26. 79 0
      cli/src/commands/media/setFeaturedVideos.ts
  27. 7 6
      cli/src/commands/media/updateChannel.ts
  28. 4 4
      cli/src/commands/media/updateVideo.ts
  29. 2 2
      cli/src/commands/media/updateVideoLicense.ts
  30. 131 73
      cli/src/commands/media/uploadVideo.ts
  31. 11 7
      cli/src/helpers/InputOutput.ts
  32. 40 35
      cli/src/helpers/JsonSchemaPrompt.ts
  33. 1 1
      cli/src/helpers/display.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. 6 0
      content-directory-schemas/inputs/classes/FeaturedVideoClass.json
  41. 18 0
      content-directory-schemas/inputs/classes/index.js
  42. 0 13
      content-directory-schemas/inputs/entityBatches/ChannelBatch.json
  43. 48 6
      content-directory-schemas/inputs/entityBatches/KnownLicenseBatch.json
  44. 0 63
      content-directory-schemas/inputs/entityBatches/VideoBatch.json
  45. 5 6
      content-directory-schemas/inputs/schemas/ChannelSchema.json
  46. 12 0
      content-directory-schemas/inputs/schemas/FeaturedVideoSchema.json
  47. 9 0
      content-directory-schemas/inputs/schemas/KnownLicenseSchema.json
  48. 6 0
      content-directory-schemas/inputs/schemas/LicenseSchema.json
  49. 2 3
      content-directory-schemas/inputs/schemas/VideoSchema.json
  50. 3 3
      content-directory-schemas/package.json
  51. 2 7
      content-directory-schemas/scripts/initializeContentDir.ts
  52. 1 1
      content-directory-schemas/scripts/inputSchemasToEntitySchemas.ts
  53. 4 8
      content-directory-schemas/src/helpers/InputParser.ts
  54. 25 5
      content-directory-schemas/src/helpers/inputs.ts
  55. 1 1
      content-directory-schemas/src/index.ts
  56. 25 15
      docker-compose.yml
  57. 2 2
      package.json
  58. 0 62
      query-node/.env
  59. 5 1
      query-node/README.md
  60. 19 1
      query-node/build.sh
  61. 13 0
      query-node/db-migrate.sh
  62. 64 57
      query-node/mappings/content-directory/content-dir-consts.ts
  63. 29 13
      query-node/mappings/content-directory/decode.ts
  64. 105 46
      query-node/mappings/content-directory/entity/create.ts
  65. 29 3
      query-node/mappings/content-directory/entity/index.ts
  66. 69 46
      query-node/mappings/content-directory/entity/remove.ts
  67. 105 33
      query-node/mappings/content-directory/entity/update.ts
  68. 69 40
      query-node/mappings/content-directory/get-or-create.ts
  69. 1 1
      query-node/mappings/content-directory/mapping.ts
  70. 45 21
      query-node/mappings/content-directory/transaction.ts
  71. 19 9
      query-node/mappings/types.ts
  72. 6 7
      query-node/package.json
  73. 15 0
      query-node/processor-start.sh
  74. 21 10
      query-node/run-tests.sh
  75. 80 32
      query-node/schema.graphql
  76. 0 974
      query-node/typedefs.json
  77. 102 45
      runtime-modules/content-directory/src/lib.rs
  78. 2 1
      runtime-modules/content-directory/src/mock.rs
  79. 1 1
      runtime-modules/content-directory/src/tests.rs
  80. 1 1
      runtime-modules/content-directory/src/tests/add_class_schema.rs
  81. 1 1
      runtime-modules/content-directory/src/tests/add_curator_group.rs
  82. 1 1
      runtime-modules/content-directory/src/tests/add_curator_to_group.rs
  83. 1 1
      runtime-modules/content-directory/src/tests/add_maintainer_to_class.rs
  84. 1 1
      runtime-modules/content-directory/src/tests/clear_entity_property_vector.rs
  85. 1 1
      runtime-modules/content-directory/src/tests/create_class.rs
  86. 1 1
      runtime-modules/content-directory/src/tests/create_entity.rs
  87. 1 1
      runtime-modules/content-directory/src/tests/insert_at_entity_property_vector.rs
  88. 1 1
      runtime-modules/content-directory/src/tests/remove_at_entity_property_vector.rs
  89. 1 1
      runtime-modules/content-directory/src/tests/remove_curator_from_group.rs
  90. 1 1
      runtime-modules/content-directory/src/tests/remove_curator_group.rs
  91. 1 1
      runtime-modules/content-directory/src/tests/remove_entity.rs
  92. 1 1
      runtime-modules/content-directory/src/tests/remove_maintainer_from_class.rs
  93. 1 1
      runtime-modules/content-directory/src/tests/set_curator_group_status.rs
  94. 53 4
      runtime-modules/content-directory/src/tests/transaction.rs
  95. 1 1
      runtime-modules/content-directory/src/tests/transfer_entity_ownership.rs
  96. 1 1
      runtime-modules/content-directory/src/tests/update_class_permissions.rs
  97. 1 1
      runtime-modules/content-directory/src/tests/update_class_schema_status.rs
  98. 2 2
      runtime-modules/content-directory/src/tests/update_entity_creation_voucher.rs
  99. 1 1
      runtime-modules/content-directory/src/tests/update_entity_permissions.rs
  100. 1 1
      runtime-modules/content-directory/src/tests/update_entity_property_values.rs

+ 9 - 16
.env

@@ -1,17 +1,14 @@
 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_PASS=postgres
 DB_HOST=localhost
 DB_PORT=5432
+
 DEBUG=index-builder:*
 TYPEORM_LOGGING=error
 
@@ -20,16 +17,12 @@ TYPEORM_LOGGING=error
 ###########################
 
 # 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.
 # 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
 
@@ -38,10 +31,10 @@ REDIS_URI=redis://localhost:6379/0
 ###########################
 
 # 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_ENDPOINT_URL=http://localhost:4100/graphql
+INDEXER_ENDPOINT_URL=http://localhost:4000/graphql
 
 # Block height from which the processor starts. Note that if
 # 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
       run: |
         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
             gzip joystream-node-docker-image.tar
             cp joystream-node-docker-image.tar.gz ~/docker-images/
-            echo "::set-env name=NEW_BUILD::true"
+            echo "NEW_BUILD=true" >> $GITHUB_ENV
           fi
 
       - name: Save joystream/node image to Artifacts

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

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

+ 3 - 3
build.sh

@@ -4,8 +4,8 @@ set -e
 
 yarn
 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 query-node-root build
 yarn workspace storage-node build
@@ -52,4 +52,4 @@ do
 
    * )     break;;
   esac
-done
+done

+ 225 - 63
cli/README.md

@@ -87,17 +87,26 @@ When using the CLI for the first time there are a few common steps you might wan
 * [`joystream-cli content-directory:curatorGroups`](#joystream-cli-content-directorycuratorgroups)
 * [`joystream-cli content-directory:entities CLASSNAME [PROPERTIES]`](#joystream-cli-content-directoryentities-classname-properties)
 * [`joystream-cli content-directory:entity ID`](#joystream-cli-content-directoryentity-id)
+* [`joystream-cli content-directory:initialize`](#joystream-cli-content-directoryinitialize)
+* [`joystream-cli content-directory:removeCuratorFromGroup [GROUPID] [CURATORID]`](#joystream-cli-content-directoryremovecuratorfromgroup-groupid-curatorid)
 * [`joystream-cli content-directory:removeCuratorGroup [ID]`](#joystream-cli-content-directoryremovecuratorgroup-id)
+* [`joystream-cli content-directory:removeEntity ID`](#joystream-cli-content-directoryremoveentity-id)
 * [`joystream-cli content-directory:removeMaintainerFromClass [CLASSNAME] [GROUPID]`](#joystream-cli-content-directoryremovemaintainerfromclass-classname-groupid)
 * [`joystream-cli content-directory:setCuratorGroupStatus [ID] [STATUS]`](#joystream-cli-content-directorysetcuratorgroupstatus-id-status)
 * [`joystream-cli content-directory:updateClassPermissions [CLASSNAME]`](#joystream-cli-content-directoryupdateclasspermissions-classname)
 * [`joystream-cli council:info`](#joystream-cli-councilinfo)
 * [`joystream-cli help [COMMAND]`](#joystream-cli-help-command)
 * [`joystream-cli media:createChannel`](#joystream-cli-mediacreatechannel)
+* [`joystream-cli media:curateContent`](#joystream-cli-mediacuratecontent)
+* [`joystream-cli media:featuredVideos`](#joystream-cli-mediafeaturedvideos)
 * [`joystream-cli media:myChannels`](#joystream-cli-mediamychannels)
 * [`joystream-cli media:myVideos`](#joystream-cli-mediamyvideos)
+* [`joystream-cli media:removeChannel [ID]`](#joystream-cli-mediaremovechannel-id)
+* [`joystream-cli media:removeVideo [ID]`](#joystream-cli-mediaremovevideo-id)
+* [`joystream-cli media:setFeaturedVideos VIDEOIDS`](#joystream-cli-mediasetfeaturedvideos-videoids)
 * [`joystream-cli media:updateChannel [ID]`](#joystream-cli-mediaupdatechannel-id)
 * [`joystream-cli media:updateVideo [ID]`](#joystream-cli-mediaupdatevideo-id)
+* [`joystream-cli media:updateVideoLicense [ID]`](#joystream-cli-mediaupdatevideolicense-id)
 * [`joystream-cli media:uploadVideo FILEPATH`](#joystream-cli-mediauploadvideo-filepath)
 * [`joystream-cli working-groups:application WGAPPLICATIONID`](#joystream-cli-working-groupsapplication-wgapplicationid)
 * [`joystream-cli working-groups:createOpening`](#joystream-cli-working-groupscreateopening)
@@ -109,6 +118,7 @@ When using the CLI for the first time there are a few common steps you might wan
 * [`joystream-cli working-groups:opening WGOPENINGID`](#joystream-cli-working-groupsopening-wgopeningid)
 * [`joystream-cli working-groups:openings`](#joystream-cli-working-groupsopenings)
 * [`joystream-cli working-groups:overview`](#joystream-cli-working-groupsoverview)
+* [`joystream-cli working-groups:setDefaultGroup`](#joystream-cli-working-groupssetdefaultgroup)
 * [`joystream-cli working-groups:slashWorker WORKERID`](#joystream-cli-working-groupsslashworker-workerid)
 * [`joystream-cli working-groups:startAcceptingApplications WGOPENINGID`](#joystream-cli-working-groupsstartacceptingapplications-wgopeningid)
 * [`joystream-cli working-groups:startReviewPeriod WGOPENINGID`](#joystream-cli-working-groupsstartreviewperiod-wgopeningid)
@@ -126,7 +136,8 @@ USAGE
   $ joystream-cli account:choose
 
 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)_
@@ -319,7 +330,9 @@ USAGE
 
 OPTIONS
   -i, --input=input    Path to JSON file to use as input (if not specified - the input can be provided interactively)
-  -o, --output=output  Path where the output JSON file should be placed (can be then reused as input)
+
+  -o, --output=output  Path to the directory where the output JSON file should be placed (the output file can be then
+                       reused as input)
 ```
 
 _See code: [src/commands/content-directory/addClassSchema.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/addClassSchema.ts)_
@@ -389,7 +402,9 @@ USAGE
 
 OPTIONS
   -i, --input=input    Path to JSON file to use as input (if not specified - the input can be provided interactively)
-  -o, --output=output  Path where the output JSON file should be placed (can be then reused as input)
+
+  -o, --output=output  Path to the directory where the output JSON file should be placed (the output file can be then
+                       reused as input)
 ```
 
 _See code: [src/commands/content-directory/createClass.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/createClass.ts)_
@@ -469,6 +484,35 @@ ARGUMENTS
 
 _See code: [src/commands/content-directory/entity.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/entity.ts)_
 
+## `joystream-cli content-directory:initialize`
+
+Initialize content directory with input data from @joystream/content library or custom, provided one. Requires lead access.
+
+```
+USAGE
+  $ joystream-cli content-directory:initialize
+
+OPTIONS
+  --rootInputsDir=rootInputsDir  Custom inputs directory (must follow @joystream/content directory structure)
+```
+
+_See code: [src/commands/content-directory/initialize.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/initialize.ts)_
+
+## `joystream-cli content-directory:removeCuratorFromGroup [GROUPID] [CURATORID]`
+
+Remove Curator from Curator Group.
+
+```
+USAGE
+  $ joystream-cli content-directory:removeCuratorFromGroup [GROUPID] [CURATORID]
+
+ARGUMENTS
+  GROUPID    ID of the Curator Group
+  CURATORID  ID of the curator
+```
+
+_See code: [src/commands/content-directory/removeCuratorFromGroup.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/removeCuratorFromGroup.ts)_
+
 ## `joystream-cli content-directory:removeCuratorGroup [ID]`
 
 Remove existing Curator Group.
@@ -483,6 +527,23 @@ ARGUMENTS
 
 _See code: [src/commands/content-directory/removeCuratorGroup.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/removeCuratorGroup.ts)_
 
+## `joystream-cli content-directory:removeEntity ID`
+
+Removes a single entity by id (can be executed in Member, Curator or Lead context)
+
+```
+USAGE
+  $ joystream-cli content-directory:removeEntity ID
+
+ARGUMENTS
+  ID  ID of the entity to remove
+
+OPTIONS
+  --context=(Member|Curator|Lead)  Actor context to execute the command in (Member/Curator/Lead)
+```
+
+_See code: [src/commands/content-directory/removeEntity.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/content-directory/removeEntity.ts)_
+
 ## `joystream-cli content-directory:removeMaintainerFromClass [CLASSNAME] [GROUPID]`
 
 Remove maintainer (Curator Group) from class.
@@ -565,11 +626,42 @@ USAGE
 
 OPTIONS
   -i, --input=input    Path to JSON file to use as input (if not specified - the input can be provided interactively)
-  -o, --output=output  Path where the output JSON file should be placed (can be then reused as input)
+
+  -o, --output=output  Path to the directory where the output JSON file should be placed (the output file can be then
+                       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)_
 
+## `joystream-cli media:curateContent`
+
+Set the curation status of given entity (Channel/Video). Requires Curator access.
+
+```
+USAGE
+  $ joystream-cli media:curateContent
+
+OPTIONS
+  -c, --className=(Channel|Video)   (required) Name of the class of the entity to curate (Channel/Video)
+  -s, --status=(Accepted|Censored)  (required) Specifies the curation status (Accepted/Censored)
+  --id=id                           (required) ID of the entity to curate
+```
+
+_See code: [src/commands/media/curateContent.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/curateContent.ts)_
+
+## `joystream-cli media:featuredVideos`
+
+Show a list of currently featured videos.
+
+```
+USAGE
+  $ joystream-cli media:featuredVideos
+```
+
+_See code: [src/commands/media/featuredVideos.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/featuredVideos.ts)_
+
 ## `joystream-cli media:myChannels`
 
 Show the list of channels associated with current account's membership.
@@ -595,6 +687,51 @@ OPTIONS
 
 _See code: [src/commands/media/myVideos.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/myVideos.ts)_
 
+## `joystream-cli media:removeChannel [ID]`
+
+Removes a channel (required controller access).
+
+```
+USAGE
+  $ joystream-cli media:removeChannel [ID]
+
+ARGUMENTS
+  ID  ID of the Channel entity
+```
+
+_See code: [src/commands/media/removeChannel.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/removeChannel.ts)_
+
+## `joystream-cli media:removeVideo [ID]`
+
+Remove given Video entity and associated entities (VideoMedia, License) from content directory.
+
+```
+USAGE
+  $ joystream-cli media:removeVideo [ID]
+
+ARGUMENTS
+  ID  ID of the Video entity
+```
+
+_See code: [src/commands/media/removeVideo.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/removeVideo.ts)_
+
+## `joystream-cli media:setFeaturedVideos VIDEOIDS`
+
+Set currently featured videos (requires lead/maintainer access).
+
+```
+USAGE
+  $ joystream-cli media:setFeaturedVideos VIDEOIDS
+
+ARGUMENTS
+  VIDEOIDS  Comma-separated video ids
+
+OPTIONS
+  --add  If provided - currently featured videos will not be removed.
+```
+
+_See code: [src/commands/media/setFeaturedVideos.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/setFeaturedVideos.ts)_
+
 ## `joystream-cli media:updateChannel [ID]`
 
 Update one of the owned channels on Joystream (requires a membership).
@@ -608,14 +745,18 @@ ARGUMENTS
 
 OPTIONS
   -i, --input=input    Path to JSON file to use as input (if not specified - the input can be provided interactively)
-  -o, --output=output  Path where the output JSON file should be placed (can be then reused as input)
+
+  -o, --output=output  Path to the directory where the output JSON file should be placed (the output file can be then
+                       reused as input)
+
+  --asCurator          Provide this flag in order to use Curator context for the update
 ```
 
 _See code: [src/commands/media/updateChannel.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/updateChannel.ts)_
 
 ## `joystream-cli media:updateVideo [ID]`
 
-Update existing video information (requires a membership).
+Update existing video information (requires controller/maintainer access).
 
 ```
 USAGE
@@ -623,10 +764,27 @@ USAGE
 
 ARGUMENTS
   ID  ID of the Video to update
+
+OPTIONS
+  --asCurator  Specify in order to update the video as curator
 ```
 
 _See code: [src/commands/media/updateVideo.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/updateVideo.ts)_
 
+## `joystream-cli media:updateVideoLicense [ID]`
+
+Update existing video license (requires controller/maintainer access).
+
+```
+USAGE
+  $ joystream-cli media:updateVideoLicense [ID]
+
+ARGUMENTS
+  ID  ID of the Video
+```
+
+_See code: [src/commands/media/updateVideoLicense.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/media/updateVideoLicense.ts)_
+
 ## `joystream-cli media:uploadVideo FILEPATH`
 
 Upload a new Video to a channel (requires a membership).
@@ -641,6 +799,10 @@ ARGUMENTS
 OPTIONS
   -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)
+
+  -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)_
@@ -657,9 +819,8 @@ ARGUMENTS
   WGAPPLICATIONID  Working Group Application ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/application.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/application.ts)_
@@ -673,19 +834,20 @@ USAGE
   $ joystream-cli working-groups:createOpening
 
 OPTIONS
-  -c, --createDraftOnly      If provided - the extrinsic will not be executed. Use this flag if you only want to create
-                             a draft.
+  -e, --edit                               If provided along with --input - launches in edit mode allowing to modify the
+                                           input before sending the exstinsic
 
-  -d, --useDraft             Whether to create the opening from existing draft.
-                             If provided without --draftName - the list of choices will be displayed.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 
-  -g, --group=group          (required) [default: storageProviders] The working group context in which the command
-                             should be executed
-                             Available values are: storageProviders, curators.
+  -i, --input=input                        Path to JSON file to use as input (if not specified - the input can be
+                                           provided interactively)
 
-  -n, --draftName=draftName  Name of the draft to create the opening from.
+  -o, --output=output                      Path to the file where the output JSON should be saved (this output can be
+                                           then reused as input)
 
-  -s, --skipPrompts          Whether to skip all prompts when adding from draft (will use all default values)
+  --dryRun                                 If provided along with --output - skips sending the actual extrinsic(can be
+                                           used to generate a "draft" which can be provided as input later)
 ```
 
 _See code: [src/commands/working-groups/createOpening.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/createOpening.ts)_
@@ -702,9 +864,8 @@ ARGUMENTS
   WORKERID  Worker ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/decreaseWorkerStake.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/decreaseWorkerStake.ts)_
@@ -721,9 +882,8 @@ ARGUMENTS
   WORKERID  Worker ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/evictWorker.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/evictWorker.ts)_
@@ -740,9 +900,8 @@ ARGUMENTS
   WGOPENINGID  Working Group Opening ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/fillOpening.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/fillOpening.ts)_
@@ -756,9 +915,8 @@ USAGE
   $ joystream-cli working-groups:increaseStake
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/increaseStake.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/increaseStake.ts)_
@@ -772,9 +930,8 @@ USAGE
   $ joystream-cli working-groups:leaveRole
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/leaveRole.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/leaveRole.ts)_
@@ -791,9 +948,8 @@ ARGUMENTS
   WGOPENINGID  Working Group Opening ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/opening.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/opening.ts)_
@@ -807,9 +963,8 @@ USAGE
   $ joystream-cli working-groups:openings
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/openings.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/openings.ts)_
@@ -823,13 +978,27 @@ USAGE
   $ joystream-cli working-groups:overview
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/overview.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/overview.ts)_
 
+## `joystream-cli working-groups:setDefaultGroup`
+
+Change the default group context for working-groups commands.
+
+```
+USAGE
+  $ joystream-cli working-groups:setDefaultGroup
+
+OPTIONS
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
+```
+
+_See code: [src/commands/working-groups/setDefaultGroup.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/setDefaultGroup.ts)_
+
 ## `joystream-cli working-groups:slashWorker WORKERID`
 
 Slashes given worker stake. Requires lead access.
@@ -842,9 +1011,8 @@ ARGUMENTS
   WORKERID  Worker ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/slashWorker.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/slashWorker.ts)_
@@ -861,9 +1029,8 @@ ARGUMENTS
   WGOPENINGID  Working Group Opening ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/startAcceptingApplications.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/startAcceptingApplications.ts)_
@@ -880,9 +1047,8 @@ ARGUMENTS
   WGOPENINGID  Working Group Opening ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/startReviewPeriod.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/startReviewPeriod.ts)_
@@ -899,9 +1065,8 @@ ARGUMENTS
   WGAPPLICATIONID  Working Group Application ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/terminateApplication.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/terminateApplication.ts)_
@@ -918,9 +1083,8 @@ ARGUMENTS
   ACCOUNTADDRESS  New reward account address (if omitted, one of the existing CLI accounts can be selected)
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/updateRewardAccount.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/updateRewardAccount.ts)_
@@ -937,9 +1101,8 @@ ARGUMENTS
   ACCOUNTADDRESS  New role account address (if omitted, one of the existing CLI accounts can be selected)
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/updateRoleAccount.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/updateRoleAccount.ts)_
@@ -956,9 +1119,8 @@ ARGUMENTS
   WORKERID  Worker ID
 
 OPTIONS
-  -g, --group=group  (required) [default: storageProviders] The working group context in which the command should be
-                     executed
-                     Available values are: storageProviders, curators.
+  -g, --group=(storageProviders|curators)  The working group context in which the command should be executed
+                                           Available values are: storageProviders, curators.
 ```
 
 _See code: [src/commands/working-groups/updateWorkerReward.ts](https://github.com/Joystream/joystream/blob/master/cli/src/commands/working-groups/updateWorkerReward.ts)_

+ 1 - 1
cli/package.json

@@ -9,7 +9,7 @@
   "bugs": "https://github.com/Joystream/joystream/issues",
   "dependencies": {
     "@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",
     "@oclif/command": "^1.5.19",
     "@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
   }
 
-  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)
   }
 

+ 33 - 13
cli/src/base/ContentDirectoryCommandBase.ts

@@ -1,7 +1,7 @@
 import ExitCodes from '../ExitCodes'
 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 {
   Class,
@@ -11,10 +11,13 @@ import {
   Entity,
   EntityId,
   Actor,
+  PropertyType,
 } from '@joystream/types/content-directory'
 import { Worker } from '@joystream/types/working-group'
 import { CLIError } from '@oclif/errors'
 import { Codec } from '@polkadot/types/types'
+import AbstractInt from '@polkadot/types/codec/AbstractInt'
+import { AnyJson } from '@polkadot/types/types/helpers'
 import _ from 'lodash'
 import { RolesCommandBase } from './WorkingGroupsCommandBase'
 import { createType } from '@joystream/types'
@@ -24,6 +27,8 @@ import { flags } from '@oclif/command'
 const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
 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
  */
@@ -234,14 +239,14 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     }
 
     if (requireSchema && !entity.supported_schemas.toArray().length) {
-      this.error(`${requiredClass || ''}Entity of id ${id} has no schema support added!`)
+      this.error(`${requiredClass || ''} entity of id ${id} has no schema support added!`)
     }
 
     return entity
   }
 
-  async getAndParseKnownEntity<T>(id: string | number): Promise<FlattenRelations<T>> {
-    const entity = await this.getEntity(id)
+  async getAndParseKnownEntity<T>(id: string | number, className?: string): Promise<FlattenRelations<T>> {
+    const entity = await this.getEntity(id, className)
     return this.parseToKnownEntityJson<T>(entity)
   }
 
@@ -278,7 +283,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
       choices: entityEntries.map(([id, entity]) => {
         const parsedEntityPropertyValues = this.parseEntityPropertyValues(entity, entityClass)
         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"
         }
       }),
@@ -298,31 +303,46 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     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(
     entity: Entity,
     entityClass: Class,
     includedProperties?: string[]
-  ): Record<string, { value: Codec; type: string }> {
+  ): Record<string, ParsedPropertyValue> {
     const { properties } = entityClass
     return Array.from(entity.getField('values').entries()).reduce((columns, [propId, propValue]) => {
       const prop = properties[propId.toNumber()]
       const propName = prop.name.toString()
       const included = !includedProperties || includedProperties.some((p) => p.toLowerCase() === propName.toLowerCase())
+      const { type: propType, subtype: propSubtype } = prop.property_type
 
       if (included) {
         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
-    }, {} as Record<string, { value: Codec; type: string }>)
+    }, {} as Record<string, ParsedPropertyValue>)
   }
 
   async parseToKnownEntityJson<T>(entity: Entity): Promise<FlattenRelations<T>> {
     const entityClass = (await this.classEntryByNameOrId(entity.class_id.toString()))[1]
     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>
   }
 
@@ -337,7 +357,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
     const defaultValues = entityClass.properties
       .map((p) => p.name.toString())
       .reduce((d, propName) => {
-        if (includedProps?.includes(propName)) {
+        if (!includedProps || includedProps.includes(propName)) {
           d[propName] = chalk.grey('[not set]')
         }
         return d
@@ -349,7 +369,7 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
         'ID': id.toString(),
         ...defaultValues,
         ..._.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>[]

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

@@ -1,5 +1,5 @@
 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 { DistinctQuestion } from 'inquirer'
 import path from 'path'
@@ -12,7 +12,7 @@ const MAX_USER_LICENSE_CONTENT_LENGTH = 4096
  */
 export default abstract class MediaCommandBase extends ContentDirectoryCommandBase {
   async promptForNewLicense(): Promise<VideoEntity['license']> {
-    let license: VideoEntity['license']
+    let licenseInput: LicenseEntity
     const licenseType: 'known' | 'custom' = await this.simplePrompt({
       type: 'list',
       message: 'Choose license type',
@@ -22,7 +22,12 @@ export default abstract class MediaCommandBase extends ContentDirectoryCommandBa
       ],
     })
     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 {
       let licenseContent: null | string = null
       while (licenseContent === null) {
@@ -38,10 +43,10 @@ export default abstract class MediaCommandBase extends ContentDirectoryCommandBa
           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> {

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

@@ -9,13 +9,19 @@ export default class AccountChoose extends AccountsCommandBase {
   static flags = {
     showSpecial: flags.boolean({
       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,
     }),
   }
 
   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()
 
     this.log(chalk.white(`Found ${accounts.length} existing accounts...\n`))
@@ -25,9 +31,18 @@ export default class AccountChoose extends AccountsCommandBase {
       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)
-    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 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 { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 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 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 { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 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(
         propertyValues,
         (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}>`)}`
       )
     )
   }

+ 15 - 8
cli/src/commands/content-directory/initialize.ts

@@ -1,21 +1,28 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { CreateClass } from 'cd-schemas/types/extrinsics/CreateClass'
-import { getInputs, InputParser, ExtrinsicsHelper } from 'cd-schemas'
-import { AddClassSchema } from 'cd-schemas/types/extrinsics/AddClassSchema'
-import { EntityBatch } from 'cd-schemas/types/EntityBatch'
+import { InputParser, ExtrinsicsHelper, getInitializationInputs } from '@joystream/cd-schemas'
+import { flags } from '@oclif/command'
 
 export default class InitializeCommand extends ContentDirectoryCommandBase {
   static description =
-    'Initialize content directory with input data from @joystream/content library. Requires lead access.'
+    'Initialize content directory with input data from @joystream/content library or custom, provided one. Requires lead access.'
+
+  static flags = {
+    rootInputsDir: flags.string({
+      required: false,
+      description: 'Custom inputs directory (must follow @joystream/content directory structure)',
+    }),
+  }
 
   async run() {
     const account = await this.getRequiredSelectedAccount()
     await this.requireLead()
     await this.requestAccountDecoding(account)
 
-    const classInputs = getInputs<CreateClass>('classes').map(({ data }) => data)
-    const schemaInputs = getInputs<AddClassSchema>('schemas').map(({ data }) => data)
-    const entityBatchInputs = getInputs<EntityBatch>('entityBatches').map(({ data }) => data)
+    const {
+      flags: { rootInputsDir },
+    } = this.parse(InitializeCommand)
+
+    const { classInputs, schemaInputs, entityBatchInputs } = getInitializationInputs(rootInputsDir)
 
     const currentClasses = await this.getApi().availableClasses()
 

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

@@ -1,8 +1,8 @@
 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 { 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'
 
 export default class UpdateClassPermissionsCommand extends ContentDirectoryCommandBase {

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

@@ -1,15 +1,19 @@
 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 { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 
+import { flags } from '@oclif/command'
+import _ from 'lodash'
+
 export default class CreateChannelCommand extends ContentDirectoryCommandBase {
   static description = 'Create a new channel on Joystream (requires a membership).'
   static flags = {
     ...IOFlags,
+    confirm: flags.boolean({ char: 'y', name: 'confirm', required: false, description: 'Confirm the provided input' }),
   }
 
   async run() {
@@ -21,7 +25,7 @@ export default class CreateChannelCommand extends ContentDirectoryCommandBase {
 
     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)
     if (!inputJson) {
@@ -32,14 +36,15 @@ export default class CreateChannelCommand extends ContentDirectoryCommandBase {
 
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, undefined, customPrompts)
 
-      inputJson = await prompter.promptAll(true)
+      inputJson = await prompter.promptAll()
     }
 
     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) {
-      saveOutputJson(output, `${inputJson.title}Channel.json`, inputJson)
+      saveOutputJson(output, `${_.startCase(inputJson.handle)}Channel.json`, inputJson)
       const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
         {
           className: 'Channel',

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

@@ -1,8 +1,8 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { InputParser } from 'cd-schemas'
+import { InputParser } from '@joystream/cd-schemas'
 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 STATUSES = ['Accepted', 'Censored'] as const

+ 36 - 0
cli/src/commands/media/featuredVideos.ts

@@ -0,0 +1,36 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+import { displayTable } from '../../helpers/display'
+import { FeaturedVideoEntity, VideoEntity } from '@joystream/cd-schemas/types/entities'
+import chalk from 'chalk'
+
+export default class FeaturedVideosCommand extends ContentDirectoryCommandBase {
+  static description = 'Show a list of currently featured videos.'
+
+  async run() {
+    const featuredEntries = await this.entitiesByClassAndOwner('FeaturedVideo')
+    const featured = await Promise.all(
+      featuredEntries
+        .filter(([, entity]) => entity.supported_schemas.toArray().length) // Ignore FeaturedVideo entities without schema
+        .map(([, entity]) => this.parseToKnownEntityJson<FeaturedVideoEntity>(entity))
+    )
+
+    const videoIds: number[] = featured.map(({ video: videoId }) => videoId)
+
+    const videos = await Promise.all(videoIds.map((videoId) => this.getAndParseKnownEntity<VideoEntity>(videoId)))
+
+    if (videos.length) {
+      displayTable(
+        videos.map(({ title, channel }, index) => ({
+          featuredVideoEntityId: featuredEntries[index][0].toNumber(),
+          videoId: videoIds[index],
+          channelId: channel,
+          title,
+        })),
+        3
+      )
+      this.log(`\nTIP: Use ${chalk.bold('content-directory:entity ID')} command to see more details about given video`)
+    } else {
+      this.log(`No videos have been featured yet! Set some with ${chalk.bold('media:setFeaturedVideos')}`)
+    }
+  }
+}

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

@@ -1,5 +1,5 @@
 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 chalk from 'chalk'
 
@@ -9,7 +9,7 @@ export default class MyChannelsCommand extends ContentDirectoryCommandBase {
   async run() {
     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)
 

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

@@ -1,5 +1,5 @@
 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 chalk from 'chalk'
 import { flags } from '@oclif/command'

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

@@ -1,7 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { Entity } from '@joystream/types/content-directory'
 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 {
   static description = 'Removes a channel (required controller access).'
@@ -29,13 +29,13 @@ export default class RemoveChannelCommand extends ContentDirectoryCommandBase {
       channelId = parseInt(id)
       channelEntity = await this.getEntity(channelId, 'Channel', memberId)
     } 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()
       channelEntity = channel
     }
     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()
     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 { 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'
 
 export default class RemoveVideoCommand extends ContentDirectoryCommandBase {

+ 79 - 0
cli/src/commands/media/setFeaturedVideos.ts

@@ -0,0 +1,79 @@
+import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
+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 { createType } from '@joystream/types'
+
+export default class SetFeaturedVideosCommand extends ContentDirectoryCommandBase {
+  static description = 'Set currently featured videos (requires lead/maintainer access).'
+  static args = [
+    {
+      name: 'videoIds',
+      required: true,
+      description: 'Comma-separated video ids',
+    },
+  ]
+
+  static flags = {
+    add: flags.boolean({
+      description: 'If provided - currently featured videos will not be removed.',
+      required: false,
+    }),
+  }
+
+  async run() {
+    const account = await this.getRequiredSelectedAccount()
+    let actor = createType('Actor', { Lead: null })
+    try {
+      await this.getRequiredLead()
+    } catch (e) {
+      actor = await this.getCuratorContext(['FeaturedVideo'])
+    }
+
+    await this.requestAccountDecoding(account)
+
+    const {
+      args: { videoIds },
+      flags: { add },
+    } = this.parse(SetFeaturedVideosCommand)
+
+    const ids: number[] = videoIds.split(',').map((id: string) => parseInt(id))
+
+    const videos: [number, FlattenRelations<VideoEntity>][] = (
+      await Promise.all(ids.map((id) => this.getAndParseKnownEntity<VideoEntity>(id, 'Video')))
+    ).map((video, index) => [ids[index], video])
+
+    this.log(
+      `Featured videos that will ${add ? 'be added to' : 'replace'} existing ones:`,
+      videos.map(([id, { title }]) => ({ id, title }))
+    )
+
+    await this.requireConfirmation('Do you confirm the provided input?')
+
+    if (!add) {
+      const currentlyFeaturedIds = (await this.entitiesByClassAndOwner('FeaturedVideo')).map(([id]) => id.toNumber())
+      const removeTxs = currentlyFeaturedIds.map((id) =>
+        this.getOriginalApi().tx.contentDirectory.removeEntity(actor, id)
+      )
+
+      if (currentlyFeaturedIds.length) {
+        this.log(`Removing existing FeaturedVideo entities (${currentlyFeaturedIds.join(', ')})...`)
+
+        const txHelper = new ExtrinsicsHelper(this.getOriginalApi())
+        await txHelper.sendAndCheck(account, removeTxs, 'The removal of existing FeaturedVideo entities failed')
+      }
+    }
+
+    this.log('Adding new FeaturedVideo entities...')
+    const featuredVideoEntries = videos.map(([id]) => ({ video: id }))
+    const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi(), [
+      {
+        className: 'FeaturedVideo',
+        entries: featuredVideoEntries,
+      },
+    ])
+    const operations = await inputParser.getEntityBatchOperations()
+    await this.sendAndFollowNamedTx(account, 'contentDirectory', 'transaction', [actor, operations])
+  }
+}

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

@@ -1,13 +1,14 @@
 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 { JSONSchema } from '@apidevtools/json-schema-ref-parser'
 import { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { Actor, Entity } from '@joystream/types/content-directory'
 import { flags } from '@oclif/command'
 import { createType } from '@joystream/types'
+import _ from 'lodash'
 
 export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
   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)
       channelEntity = await this.getEntity(channelId, 'Channel', memberId)
     } 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()
       channelEntity = channel
     }
@@ -80,14 +81,14 @@ export default class UpdateChannelCommand extends ContentDirectoryCommandBase {
 
       const prompter = new JsonSchemaPrompter<ChannelEntity>(channelJsonSchema, currentValues, customPrompts)
 
-      inputJson = await prompter.promptAll(true)
+      inputJson = await prompter.promptAll()
     }
 
     this.jsonPrettyPrint(JSON.stringify(inputJson))
     const confirmed = await this.simplePrompt({ type: 'confirm', message: 'Do you confirm the provided input?' })
 
     if (confirmed) {
-      saveOutputJson(output, `${inputJson.title}Channel.json`, inputJson)
+      saveOutputJson(output, `${_.startCase(inputJson.handle)}Channel.json`, inputJson)
       const inputParser = InputParser.createWithKnownSchemas(this.getOriginalApi())
       const updateOperations = await inputParser.getEntityUpdateOperations(inputJson, 'Channel', channelId)
       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 { JsonSchemaCustomPrompts, JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { Actor, Entity } from '@joystream/types/content-directory'
@@ -83,7 +83,7 @@ export default class UpdateVideoCommand extends MediaCommandBase {
       'category',
       'title',
       'description',
-      'thumbnailURL',
+      'thumbnailUrl',
       'duration',
       'isPublic',
       'isExplicit',

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

@@ -1,6 +1,6 @@
 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 { 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 { JsonSchemaPrompter } from '../../helpers/JsonSchemaPrompt'
 import { flags } from '@oclif/command'
@@ -17,14 +17,15 @@ import ipfsHttpClient from 'ipfs-http-client'
 import first from 'it-first'
 import last from 'it-last'
 import toBuffer from 'it-to-buffer'
-import ffmpegInstaller from '@ffmpeg-installer/ffmpeg'
+import ffprobeInstaller from '@ffprobe-installer/ffprobe'
 import ffmpeg from 'fluent-ffmpeg'
 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 MAX_FILE_SIZE = 500 * 1024 * 1024
+const MAX_FILE_SIZE = 2000 * 1024 * 1024
 
 type VideoMetadata = {
   width?: number
@@ -37,13 +38,14 @@ type VideoMetadata = {
 export default class UploadVideoCommand extends MediaCommandBase {
   static description = 'Upload a new Video to a channel (requires a membership).'
   static flags = {
-    // TODO: ...IOFlags, - providing input as json
+    input: IOFlags.input,
     channel: flags.integer({
       char: 'c',
       required: false,
       description:
         '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 = [
@@ -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() {
     const account = await this.getRequiredSelectedAccount()
     const memberId = await this.getRequiredMemberId()
@@ -226,7 +339,7 @@ export default class UploadVideoCommand extends MediaCommandBase {
 
     const {
       args: { filePath },
-      flags: { channel: inputChannelId },
+      flags: { channel: inputChannelId, input, confirm },
     } = this.parse(UploadVideoCommand)
 
     // Basic file validation
@@ -255,7 +368,7 @@ export default class UploadVideoCommand extends MediaCommandBase {
       channelId = await this.promptForEntityId(
         'Select a channel to publish the video under',
         'Channel',
-        'title',
+        'handle',
         memberId
       )
     } else {
@@ -303,71 +416,16 @@ export default class UploadVideoCommand extends MediaCommandBase {
 
     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
     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 Ajv from 'ajv'
 import $RefParser, { JSONSchema } from '@apidevtools/json-schema-ref-parser'
-import { getSchemasLocation } from 'cd-schemas'
+import { getSchemasLocation } from '@joystream/cd-schemas'
 import chalk from 'chalk'
 
 // 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 })
     }
     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
@@ -53,6 +48,15 @@ export async function getInputJson<T>(inputPath?: string, schema?: JSONSchema, s
   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 {
   if (outputPath) {
     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 chalk from 'chalk'
 import { BOOL_PROMPT_OPTIONS } from './prompting'
-import { getSchemasLocation } from 'cd-schemas'
+import { getSchemasLocation } from '@joystream/cd-schemas'
 import path from 'path'
 
 type CustomPromptMethod = () => Promise<any>
@@ -106,7 +106,11 @@ export class JsonSchemaPrompter<JsonResult> {
     if (schema.oneOf) {
       const oneOf = schema.oneOf as JSONSchema[]
       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) {
         _.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))
 
         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) {
           value[pName] = await this.prompt(pSchema, objectPropertyPath)
@@ -207,14 +206,11 @@ export class JsonSchemaPrompter<JsonResult> {
     let currItem = 0
     const result = []
     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) {
         break
       }
@@ -228,20 +224,17 @@ export class JsonSchemaPrompter<JsonResult> {
   }
 
   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
   }
@@ -291,4 +284,16 @@ export class JsonSchemaPrompter<JsonResult> {
     await this.prompt(mainSchema.properties![p] as JSONSchema, p, customPrompt)
     return this.filledObject[p] as Exclude<JsonResult[P], undefined>
   }
+
+  async inquirerSinglePrompt(question: DistinctQuestion) {
+    const { result } = await inquirer.prompt([
+      {
+        ...question,
+        name: 'result',
+        prefix: '',
+      },
+    ])
+
+    return result
+  }
 }

+ 1 - 1
cli/src/helpers/display.ts

@@ -44,7 +44,7 @@ export function displayTable(rows: { [k: string]: string | number }[], cellHoriz
   const maxLength = (columnName: string) =>
     rows.reduce((maxLength, row) => {
       const val = row[columnName]
-      const valLength = typeof val === 'string' ? val.length : val.toString().length
+      const valLength = typeof val === 'string' ? val.length : val !== undefined ? val.toString().length : 0
       return Math.max(maxLength, valLength)
     }, columnName.length)
   const columnDef = (columnName: string) => ({

+ 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:
 
 ```
-yarn workspace cd-schemas initialize:dev
+yarn workspace @joystream/cd-schemas initialize:dev
 ```
 
 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
 
-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
 
@@ -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:
 
 ```
-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`.
@@ -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:
 
 ```
-yarn workspace cd-schemas generate:types
+yarn workspace @joystream/cd-schemas generate:types
 ```
 
 This command will generate:
@@ -153,19 +153,19 @@ The best way to ilustrate this would be by providing some examples:
 
 #### 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...
 
   async main() {
     // Initialize the api, SENDER_KEYPAIR and SENDER_MEMBER_ID...
 
     const channel: ChannelEntity = {
-      title: 'Example channel',
+      handle: 'Example channel',
       description: 'This is an example channel',
       language: { existing: { code: 'EN' } },
       coverPhotoUrl: '',
-      avatarPhotoURL: '',
+      avatarPhotoUrl: '',
       isPublic: true,
     }
 
@@ -182,12 +182,12 @@ The best way to ilustrate this would be by providing some examples:
       .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
 ```
-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() {
@@ -198,7 +198,7 @@ async main() {
     description: 'This is an example video',
     language: { existing: { code: 'EN' } },
     category: { existing: { name: 'Education' } },
-    channel: { existing: { title: 'Example channel' } },
+    channel: { existing: { handle: 'Example channel' } },
     media: {
       new: {
         encoding: { existing: { name: 'H.263_MP4' } },
@@ -221,7 +221,7 @@ async main() {
       },
     },
     duration: 3600,
-    thumbnailURL: '',
+    thumbnailUrl: '',
     isExplicit: false,
     isPublic: true,
   }
@@ -239,25 +239,25 @@ async main() {
     .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() {
   // ...
 
   const channelUpdateInput: Partial<ChannelEntity> = {
-    title: 'Updated channel title',
+    handle: 'Updated channel handle',
   }
 
   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)
 
@@ -266,7 +266,7 @@ async function main() {
     .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`.
 

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

@@ -1,9 +1,9 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 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() {
   // Initialize the api
@@ -16,14 +16,14 @@ async function main() {
   const [ALICE] = keyring.getPairs()
 
   const channel: ChannelEntity = {
-    title: 'Example channel',
+    handle: '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.
     // Here we reference language that we assume was added by initialization script (initialize:dev), as it is part of
     // input/entityBatches/LanguageBatch.json
     language: { existing: { code: 'EN' } },
     coverPhotoUrl: '',
-    avatarPhotoURL: '',
+    avatarPhotoUrl: '',
     isPublic: true,
   }
   // 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 { types as joyTypes } from '@joystream/types'
 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'
 
 // 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)
   const channel: FlattenRelations<ChannelEntity> = {
-    title: 'Example channel 2',
+    handle: 'Example channel 2',
     description: 'This is an example channel',
     language: languageEntityId,
     coverPhotoUrl: '',
-    avatarPhotoURL: '',
+    avatarPhotoUrl: '',
     isPublic: true,
   }
 

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

@@ -1,9 +1,9 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 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() {
   // Initialize the api
@@ -22,9 +22,9 @@ async function main() {
     // (those referenced here are part of inputs/entityBatches)
     language: { existing: { code: 'EN' } },
     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
-    channel: { existing: { title: 'Example channel' } },
+    channel: { existing: { handle: 'Example channel' } },
     media: {
       // We use "new" syntax to sygnalize we want to create a new VideoMedia entity that will be related to this Video entity
       new: {
@@ -46,7 +46,7 @@ async function main() {
       },
     },
     duration: 3600,
-    thumbnailURL: '',
+    thumbnailUrl: '',
     isExplicit: false,
     isPublic: true,
   }

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

@@ -1,9 +1,9 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 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() {
   // Initialize the api
@@ -17,7 +17,7 @@ async function main() {
 
   // Create partial channel entity, only containing the fields we wish to update
   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)
@@ -25,7 +25,7 @@ async function main() {
 
   // 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)
-  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
   const updateOperations = await parser.getEntityUpdateOperations(

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

@@ -1,10 +1,10 @@
 import { ApiPromise, WsProvider } from '@polkadot/api'
 import { types as joyTypes } from '@joystream/types'
 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
 async function main() {
@@ -19,7 +19,7 @@ async function main() {
 
   // Create partial channel entity, only containing the fields we wish to update
   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)
@@ -28,7 +28,7 @@ async function main() {
   // We can reuse InputParser's `findEntityIdByUniqueQuery` method to find entityId of the channel we
   // created in ./createChannelWithoutTransaction.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 2' }, 'Channel')
+  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ handle: 'Example channel 2' }, 'Channel')
 
   // We use parser to create input property values map
   const newPropertyValues = await parser.parseToInputEntityValuesMap(channelUpdateInput, 'Channel')

+ 6 - 0
content-directory-schemas/inputs/classes/FeaturedVideoClass.json

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

+ 18 - 0
content-directory-schemas/inputs/classes/index.js

@@ -0,0 +1,18 @@
+const EXPECTED_CLASS_ORDER = [
+  'Channel',
+  'ContentCategory',
+  'HttpMediaLocation',
+  'JoystreamMediaLocation',
+  'KnownLicense',
+  'Language',
+  'License',
+  'MediaLocation',
+  'UserDefinedLicense',
+  'Video',
+  'VideoMedia',
+  'VideoMediaEncoding',
+  'FeaturedVideo',
+]
+
+// Exports class input jsons in a predictable order
+module.exports = EXPECTED_CLASS_ORDER.map((className) => require(`./${className}Class.json`))

+ 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
-    }
-  ]
-}

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

@@ -1,11 +1,53 @@
 {
   "className": "KnownLicense",
   "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",
+      "url": "https://creativecommons.org/share-your-work/public-domain/pdm",
+      "attributionRequired": false
+    },
+    {
+      "code": "CC0",
+      "name": "Public Domain Dedication",
+      "url": "https://creativecommons.org/share-your-work/public-domain/cc0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY",
+      "name": "Creative Commons Attribution License",
+      "url": "https://creativecommons.org/licenses/by/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_SA",
+      "name": "Creative Commons Attribution-ShareAlike License",
+      "url": "https://creativecommons.org/licenses/by-sa/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_ND",
+      "name": "Creative Commons Attribution-NoDerivs License",
+      "url": "https://creativecommons.org/licenses/by-nd/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_NC",
+      "name": "Creative Commons Attribution-NonCommercial License",
+      "url": "https://creativecommons.org/licenses/by-nc/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_NC_SA",
+      "name": "Creative Commons Attribution-NonCommercial-ShareAlike License",
+      "url": "https://creativecommons.org/licenses/by-nc-sa/4.0",
+      "attributionRequired": true
+    },
+    {
+      "code": "CC_BY_NC_ND",
+      "name": "Creative Commons Attribution-NonCommercial-NoDerivs License",
+      "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",
   "newProperties": [
     {
-      "name": "title",
-      "description": "The title of the Channel",
+      "name": "handle",
+      "description": "The handle of the Channel",
       "required": true,
       "unique": true,
       "property_type": { "Single": { "Text": 64 } }
@@ -17,13 +17,13 @@
     {
       "name": "coverPhotoUrl",
       "description": "Url for Channel's cover (background) photo. Recommended ratio: 16:9.",
-      "required": true,
+      "required": false,
       "property_type": { "Single": { "Text": 256 } }
     },
     {
-      "name": "avatarPhotoURL",
+      "name": "avatarPhotoUrl",
       "description": "Channel's avatar photo.",
-      "required": true,
+      "required": false,
       "property_type": { "Single": { "Text": 256 } }
     },
     {
@@ -36,7 +36,6 @@
       "name": "isCensored",
       "description": "Channel censorship status set by the Curator.",
       "required": false,
-      "unique": true,
       "property_type": { "Single": "Bool" },
       "locking_policy": { "is_locked_from_controller": true }
     },

+ 12 - 0
content-directory-schemas/inputs/schemas/FeaturedVideoSchema.json

@@ -0,0 +1,12 @@
+{
+  "className": "FeaturedVideo",
+  "newProperties": [
+    {
+      "name": "video",
+      "description": "Reference to a video",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": { "Reference": { "className": "Video" } } }
+    }
+  ]
+}

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

@@ -38,6 +38,15 @@
         "Single": { "Text": 256 }
       },
       "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",
       "required": false,
       "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" }
     },
     {
-      "name": "thumbnailURL",
+      "name": "thumbnailUrl",
       "description": "Video thumbnail url (recommended ratio: 16:9)",
       "required": true,
       "property_type": { "Single": { "Text": 256 } }
@@ -67,7 +67,7 @@
       "name": "publishedBeforeJoystream",
       "description": "If the Video was published on other platform before beeing published on Joystream - the original publication date",
       "required": false,
-      "property_type": { "Single": "Uint32" }
+      "property_type": { "Single": "Int32" }
     },
     {
       "name": "isPublic",
@@ -92,7 +92,6 @@
       "name": "isCensored",
       "description": "Video censorship status set by the Curator.",
       "required": false,
-      "unique": true,
       "property_type": { "Single": "Bool" },
       "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",
   "description": "JSON schemas, inputs and related tooling for Joystream content directory 2.0",
   "author": "Joystream contributors",
@@ -19,9 +19,9 @@
     "initialize:dev": "yarn initialize:alice-as-lead && yarn initialize:content-dir",
     "example:createChannel": "ts-node ./examples/createChannel.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:updateChannelTitlelWithoutTransaction": "ts-node ./examples/updateChannelTitleWithoutTransaction.ts"
+    "example:updateChannelHandlelWithoutTransaction": "ts-node ./examples/updateChannelHandleWithoutTransaction.ts"
   },
   "dependencies": {
     "ajv": "6.12.5",

+ 2 - 7
content-directory-schemas/scripts/initializeContentDir.ts

@@ -1,20 +1,15 @@
-import { CreateClass } from '../types/extrinsics/CreateClass'
-import { AddClassSchema } from '../types/extrinsics/AddClassSchema'
 import { types } from '@joystream/types'
 import { ApiPromise, WsProvider } from '@polkadot/api'
-import { getInputs } from '../src/helpers/inputs'
+import { getInitializationInputs } from '../src/helpers/inputs'
 import fs from 'fs'
 import path from 'path'
-import { EntityBatch } from '../types/EntityBatch'
 import { InputParser } from '../src/helpers/InputParser'
 import { ExtrinsicsHelper, getAlicePair } from '../src/helpers/extrinsics'
 
 // Save entity operations output here for easier debugging
 const ENTITY_OPERATIONS_OUTPUT_PATH = path.join(__dirname, '../operations.json')
 
-const classInputs = getInputs<CreateClass>('classes').map(({ data }) => data)
-const schemaInputs = getInputs<AddClassSchema>('schemas').map(({ data }) => data)
-const entityBatchInputs = getInputs<EntityBatch>('entityBatches').map(({ data }) => data)
+const { classInputs, schemaInputs, entityBatchInputs } = getInitializationInputs()
 
 async function main() {
   // Init api

+ 1 - 1
content-directory-schemas/scripts/inputSchemasToEntitySchemas.ts

@@ -42,7 +42,7 @@ const HashPropertyDef = ({ Hash: maxLength }: HashProperty): JSONSchema7 => ({
 
 const ReferencePropertyDef = ({ Reference: ref }: ReferenceProperty): JSONSchema7 => ({
   'oneOf': [
-    onePropertyObjectDef('new', { '$ref': `./${ref.className}Entity.schema.json` }),
+    onePropertyObjectDef('new', { '$ref': `../entities/${ref.className}Entity.schema.json` }),
     onePropertyObjectDef('existing', { '$ref': `../entityReferences/${ref.className}Ref.schema.json` }),
     PRIMITIVE_PROPERTY_DEFS.definitions.Uint64 as JSONSchema7,
   ],

+ 4 - 8
content-directory-schemas/src/helpers/InputParser.ts

@@ -18,7 +18,7 @@ import { ApiPromise } from '@polkadot/api'
 import { JoyBTreeSet } from '@joystream/types/common'
 import { CreateClass } from '../../types/extrinsics/CreateClass'
 import { EntityBatch } from '../../types/EntityBatch'
-import { getInputs } from './inputs'
+import { getInitializationInputs, getInputs } from './inputs'
 
 type SimpleEntityValue = string | boolean | number | string[] | boolean[] | number[] | undefined | null
 // Input without "new" or "extising" keywords
@@ -38,12 +38,8 @@ export class InputParser {
   private classIdByNameMap = new Map<string, number>()
 
   static createWithInitialInputs(api: ApiPromise): InputParser {
-    return new InputParser(
-      api,
-      getInputs<CreateClass>('classes').map(({ data }) => data),
-      getInputs<AddClassSchema>('schemas').map(({ data }) => data),
-      getInputs<EntityBatch>('entityBatches').map(({ data }) => data)
-    )
+    const { classInputs, schemaInputs, entityBatchInputs } = getInitializationInputs()
+    return new InputParser(api, classInputs, schemaInputs, entityBatchInputs)
   }
 
   static createWithKnownSchemas(api: ApiPromise, entityBatches?: EntityBatch[]): InputParser {
@@ -198,7 +194,7 @@ export class InputParser {
         const schemaPropertyType = schema.newProperties.find((p) => p.name === propertyName)!.property_type
         // Handle entities "nested" via "new"
         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)
             this.includeEntityInputInUniqueQueryMap(propertyValue.new, refEntitySchema)
           }

+ 25 - 5
content-directory-schemas/src/helpers/inputs.ts

@@ -1,5 +1,7 @@
 import path from 'path'
 import fs from 'fs'
+import { CreateClass, AddClassSchema } from '../../types/extrinsics'
+import { EntityBatch } from '../../types/EntityBatch'
 
 export const INPUTS_LOCATION = path.join(__dirname, '../../inputs')
 export const INPUT_TYPES = ['classes', 'schemas', 'entityBatches'] as const
@@ -9,12 +11,30 @@ export type FetchedInput<Schema = any> = { fileName: string; data: Schema }
 
 export const getInputsLocation = (inputType: InputType) => path.join(INPUTS_LOCATION, inputType)
 
-export function getInputs<Schema = any>(inputType: InputType): FetchedInput<Schema>[] {
-  return fs.readdirSync(getInputsLocation(inputType)).map((fileName) => {
-    const inputJson = fs.readFileSync(path.join(INPUTS_LOCATION, inputType, fileName)).toString()
-    return {
+export function getInputs<Schema = any>(
+  inputType: InputType,
+  rootInputsLocation = INPUTS_LOCATION
+): FetchedInput<Schema>[] {
+  const inputs: FetchedInput<Schema>[] = []
+  fs.readdirSync(path.join(rootInputsLocation, inputType)).forEach((fileName) => {
+    const inputFilePath = path.join(rootInputsLocation, inputType, fileName)
+    if (path.extname(inputFilePath) !== '.json') {
+      return
+    }
+    const inputJson = fs.readFileSync(inputFilePath).toString()
+    inputs.push({
       fileName,
       data: JSON.parse(inputJson) as Schema,
-    }
+    })
   })
+  return inputs
+}
+
+export function getInitializationInputs(rootInputsLocation = INPUTS_LOCATION) {
+  return {
+    // eslint-disable-next-line @typescript-eslint/no-var-requires
+    classInputs: require('../../inputs/classes/index.js') as CreateClass[],
+    schemaInputs: getInputs<AddClassSchema>('schemas').map(({ data }) => data),
+    entityBatchInputs: getInputs<EntityBatch>('entityBatches').map(({ data }) => data),
+  }
 }

+ 1 - 1
content-directory-schemas/src/index.ts

@@ -1,6 +1,6 @@
 export { ExtrinsicsHelper, getAlicePair } from './helpers/extrinsics'
 export { InputParser } from './helpers/InputParser'
-export { getInputs, getInputsLocation } from './helpers/inputs'
+export { getInputs, getInitializationInputs, getInputsLocation } from './helpers/inputs'
 export { isReference, isSingle } from './helpers/propertyType'
 export { getSchemasLocation } from './helpers/schemas'
 export { default as initializeContentDir } from './helpers/initialize'

+ 25 - 15
docker-compose.yml

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

+ 2 - 2
package.json

@@ -4,7 +4,7 @@
   "version": "1.0.0",
   "license": "GPL-3.0-only",
   "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",
     "start": "./start.sh",
     "cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
@@ -36,7 +36,7 @@
     "babel-core": "^7.0.0-bridge.0",
     "typescript": "^3.9.7",
     "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": {
     "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
 ```
 
+## 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
 
 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
 query {
   channels {
-    title
+    handle
   }
 }
 ```

+ 19 - 1
query-node/build.sh

@@ -4,8 +4,26 @@ set -e
 SCRIPT_PATH="$(dirname "${BASH_SOURCE[0]}")"
 cd $SCRIPT_PATH
 
+# Load and export variables from root .env file into shell environment
+set -a
+. ../.env
+set +a
+
 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
+
 ln -s ../../../../../node_modules/typeorm/cli.js generated/graphql-server/node_modules/.bin/typeorm || :
+
 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

+ 64 - 57
query-node/mappings/content-directory/content-dir-consts.ts

@@ -1,4 +1,4 @@
-import { IPropertyIdWithName } from '../types'
+import { IPropertyWithId } from '../types'
 
 // Content directory predefined class names
 export enum ContentDirectoryKnownClasses {
@@ -14,6 +14,7 @@ export enum ContentDirectoryKnownClasses {
   VIDEO = 'Video',
   VIDEOMEDIA = 'VideoMedia',
   VIDEOMEDIAENCODING = 'VideoMediaEncoding',
+  FEATUREDVIDEOS = 'FeaturedVideo',
 }
 
 // Predefined content-directory classes, classId may change after the runtime seeding
@@ -30,87 +31,93 @@ export const contentDirectoryClassNamesWithId: { classId: number; name: string }
   { name: ContentDirectoryKnownClasses.VIDEO, classId: 10 },
   { name: ContentDirectoryKnownClasses.VIDEOMEDIA, classId: 11 },
   { name: ContentDirectoryKnownClasses.VIDEOMEDIAENCODING, classId: 12 },
+  { name: ContentDirectoryKnownClasses.FEATUREDVIDEOS, classId: 13 },
 ]
 
-export const CategoryPropertyNamesWithId: IPropertyIdWithName = {
-  0: 'name',
-  1: 'description',
+export const categoryPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'name', type: 'string', required: true },
+  1: { name: 'description', type: 'string', required: false },
 }
 
-export const channelPropertyNamesWithId: IPropertyIdWithName = {
-  0: 'title',
-  1: 'description',
-  2: 'coverPhotoURL',
-  3: 'avatarPhotoURL',
-  4: 'isPublic',
-  5: 'isCurated',
-  6: 'language',
+export const channelPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'handle', type: 'string', required: true },
+  1: { name: 'description', 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 },
+  5: { name: 'isCurated', type: 'boolean', required: false },
+  6: { name: 'language', type: 'number', required: false },
 }
 
-export const licensePropertyNamesWithId: IPropertyIdWithName = {
-  0: 'knownLicense',
-  1: 'userDefinedLicense',
+export const licensePropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'knownLicense', type: 'number', required: false },
+  1: { name: 'userDefinedLicense', type: 'number', required: false },
+  2: { name: 'attribution', type: 'string', required: false },
 }
 
-export const knownLicensePropertyNamesWIthId: IPropertyIdWithName = {
-  0: 'code',
-  1: 'name',
-  2: 'description',
-  3: 'url',
+export const knownLicensePropertyNamesWIthId: IPropertyWithId = {
+  0: { name: 'code', type: 'string', required: true },
+  1: { name: 'name', type: 'string', required: false },
+  2: { name: 'description', type: 'string', required: false },
+  3: { name: 'url', type: 'string', required: false },
 }
 
-export const languagePropertyNamesWIthId: IPropertyIdWithName = {
-  0: 'name',
-  1: 'code',
+export const languagePropertyNamesWIthId: IPropertyWithId = {
+  0: { name: 'name', type: 'string', required: true },
+  1: { name: 'code', type: 'string', required: true },
 }
 
-export const userDefinedLicensePropertyNamesWithId: IPropertyIdWithName = {
-  0: 'content',
+export const userDefinedLicensePropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'content', type: 'string', required: false },
 }
 
-export const mediaLocationPropertyNamesWithId: IPropertyIdWithName = {
-  0: 'httpMediaLocation',
-  1: 'joystreamMediaLocation',
+export const mediaLocationPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'httpMediaLocation', type: 'number', required: false },
+  1: { name: 'joystreamMediaLocation', type: 'number', required: false },
 }
 
-export const joystreamMediaLocationPropertyNamesWithId: IPropertyIdWithName = {
-  0: 'dataObjectId',
+export const joystreamMediaLocationPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'dataObjectId', type: 'string', required: true },
 }
 
-export const httpMediaLocationPropertyNamesWithId: IPropertyIdWithName = {
-  0: 'url',
-  1: 'port',
+export const httpMediaLocationPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'url', type: 'string', required: false },
+  1: { name: 'port', type: 'number', required: false },
 }
 
-export const videoMediaEncodingPropertyNamesWithId: IPropertyIdWithName = {
-  0: 'name',
+export const videoMediaEncodingPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'name', type: 'string', required: true },
 }
 
-export const videoMediaPropertyNamesWithId: IPropertyIdWithName = {
-  0: 'encoding',
-  1: 'pixelWidth',
-  2: 'pixelHeight',
-  3: 'size',
-  4: 'location',
+export const videoMediaPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'encoding', type: 'number', required: true },
+  1: { name: 'pixelWidth', type: 'number', required: true },
+  2: { name: 'pixelHeight', type: 'number', required: true },
+  3: { name: 'size', type: 'number', required: false },
+  4: { name: 'location', type: 'number', required: true },
 }
 
-export const videoPropertyNamesWithId: IPropertyIdWithName = {
+export const videoPropertyNamesWithId: IPropertyWithId = {
   // referenced entity's id
-  0: 'channel',
+  0: { name: 'channel', type: 'number', required: true },
   // referenced entity's id
-  1: 'category',
-  2: 'title',
-  3: 'description',
-  4: 'duration',
-  5: 'skippableIntroDuration',
-  6: 'thumbnailURL',
-  7: 'language',
+  1: { name: 'category', type: 'number', required: true },
+  2: { name: 'title', type: 'string', required: false },
+  3: { name: 'description', type: 'string', required: false },
+  4: { name: 'duration', type: 'number', required: true },
+  5: { name: 'skippableIntroDuration', type: 'number', required: false },
+  6: { name: 'thumbnailUrl', type: 'string', required: true },
+  7: { name: 'language', type: 'number', required: false },
   // referenced entity's id
-  8: 'media',
-  9: 'hasMarketing',
-  10: 'publishedBeforeJoystream',
-  11: 'isPublic',
-  12: 'isExplicit',
-  13: 'license',
-  14: 'isCurated',
+  8: { name: 'media', type: 'number', required: true },
+  9: { name: 'hasMarketing', type: 'boolean', required: false },
+  10: { name: 'publishedBeforeJoystream', type: 'number', required: false },
+  11: { name: 'isPublic', type: 'boolean', required: true },
+  12: { name: 'isExplicit', type: 'boolean', required: true },
+  13: { name: 'license', type: 'number', required: true },
+  14: { name: 'isCurated', type: 'boolean', required: true },
+}
+
+export const featuredVideoPropertyNamesWithId: IPropertyWithId = {
+  0: { name: 'video', type: 'number', required: true },
 }

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

@@ -1,17 +1,22 @@
 import { SubstrateEvent } from '../../generated/indexer'
 import {
-  IPropertyIdWithName,
   IClassEntity,
   IProperty,
   IBatchOperation,
   ICreateEntityOperation,
   IEntity,
   IReference,
+  IPropertyWithId,
 } from '../types'
 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 { Vec } from '@polkadot/types'
 
 const debug = Debug('mappings:cd:decode')
 
@@ -20,18 +25,23 @@ function stringIfyEntityId(event: SubstrateEvent): string {
   return entityId.value as string
 }
 
-function setProperties<T>({ extrinsic, blockNumber }: SubstrateEvent, propNamesWithId: IPropertyIdWithName): T {
+function setProperties<T>({ extrinsic, blockNumber }: SubstrateEvent, propNamesWithId: IPropertyWithId): T {
   if (extrinsic === undefined) throw Error('Undefined extrinsic')
 
   const { 3: newPropertyValues } = extrinsic!.args
   const properties: { [key: string]: any; reference?: IReference } = {}
 
   for (const [k, v] of Object.entries(newPropertyValues.value)) {
-    const propertyName = propNamesWithId[k]
+    const prop = propNamesWithId[k]
     const singlePropVal = createType('InputPropertyValue', v as any).asType('Single')
-    properties[propertyName] = singlePropVal.isOfType('Reference')
-      ? { entityId: singlePropVal.asType('Reference').toJSON(), existing: true }
-      : singlePropVal.value.toJSON()
+
+    if (singlePropVal.isOfType('Reference')) {
+      properties[prop.name] = { entityId: singlePropVal.asType('Reference').toJSON(), existing: true }
+    } else {
+      const val = singlePropVal.value.toJSON()
+      if (typeof val !== prop.type && !prop.required) properties[prop.name] = undefined
+      else properties[prop.name] = val
+    }
   }
   properties.version = blockNumber
 
@@ -54,16 +64,18 @@ function getClassEntity(event: SubstrateEvent): IClassEntity {
  * @param properties
  * @param propertyNamesWithId
  */
-function setEntityPropertyValues<T>(properties: IProperty[], propertyNamesWithId: IPropertyIdWithName): T {
+function setEntityPropertyValues<T>(properties: IProperty[], propertyNamesWithId: IPropertyWithId): T {
   const entityProperties: { [key: string]: any; reference?: IReference } = {}
 
   for (const [propId, propName] of Object.entries(propertyNamesWithId)) {
     // get the property value by id
     const p = properties.find((p) => p.id === propId)
     if (!p) continue
-    entityProperties[propName] = p.reference ? p.reference : p.value
+
+    if (typeof p.value !== propName.type && !propName.required) entityProperties[propName.name] = undefined
+    else entityProperties[propName.name] = p.reference ? p.reference : p.value
   }
-  // debug(`Entity properties ${JSON.stringify(entityProperties)}`)
+  debug(`Entity properties: ${JSON.stringify(entityProperties)}`)
   return entityProperties as T
 }
 
@@ -101,9 +113,12 @@ function getEntityProperties(propertyValues: ParametrizedClassPropertyValue[]):
   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 addSchemaSupportToEntityOperations: IEntity[] = []
   const createEntityOperations: ICreateEntityOperation[] = []
@@ -153,6 +168,7 @@ export const decode = {
   getClassEntity,
   setEntityPropertyValues,
   getEntityProperties,
-  getOperations,
+  getOperationsByTypes,
   setProperties,
+  getOperations,
 }

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

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

+ 29 - 3
query-node/mappings/content-directory/entity/index.ts

@@ -16,6 +16,7 @@ import {
   updateVideoMediaEncodingEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
+  updateFeaturedVideoEntityPropertyValues,
 } from './update'
 import {
   removeCategory,
@@ -30,6 +31,7 @@ import {
   removeVideoMediaEncoding,
   removeLicense,
   removeMediaLocation,
+  removeFeaturedVideo,
 } from './remove'
 import {
   createCategory,
@@ -43,9 +45,10 @@ import {
   createLanguage,
   createVideoMediaEncoding,
   createBlockOrGetFromDatabase,
+  createFeaturedVideo,
 } from './create'
 import {
-  CategoryPropertyNamesWithId,
+  categoryPropertyNamesWithId,
   channelPropertyNamesWithId,
   httpMediaLocationPropertyNamesWithId,
   joystreamMediaLocationPropertyNamesWithId,
@@ -56,6 +59,7 @@ import {
   videoPropertyNamesWithId,
   contentDirectoryClassNamesWithId,
   ContentDirectoryKnownClasses,
+  featuredVideoPropertyNamesWithId,
 } from '../content-dir-consts'
 
 import {
@@ -74,6 +78,7 @@ import {
   IEntity,
   ILicense,
   IMediaLocation,
+  IFeaturedVideo,
 } from '../../types'
 import { getOrCreate } from '../get-or-create'
 
@@ -112,7 +117,7 @@ async function contentDirectory_EntitySchemaSupportAdded(db: DB, event: Substrat
       break
 
     case ContentDirectoryKnownClasses.CATEGORY:
-      await createCategory(arg, decode.setProperties<ICategory>(event, CategoryPropertyNamesWithId))
+      await createCategory(arg, decode.setProperties<ICategory>(event, categoryPropertyNamesWithId))
       break
 
     case ContentDirectoryKnownClasses.KNOWNLICENSE:
@@ -168,6 +173,14 @@ async function contentDirectory_EntitySchemaSupportAdded(db: DB, event: Substrat
         decode.setProperties<IVideoMediaEncoding>(event, videoMediaEncodingPropertyNamesWithId)
       )
       break
+    case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+      await createFeaturedVideo(
+        arg,
+        new Map<string, IEntity[]>(),
+        decode.setProperties<IFeaturedVideo>(event, featuredVideoPropertyNamesWithId),
+        0
+      )
+      break
 
     default:
       throw new Error(`Unknown class name: ${cls.name}`)
@@ -241,6 +254,10 @@ async function contentDirectory_EntityRemoved(db: DB, event: SubstrateEvent): Pr
       await removeMediaLocation(db, where)
       break
 
+    case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+      await removeFeaturedVideo(db, where)
+      break
+
     default:
       throw new Error(`Unknown class name: ${cls.name}`)
   }
@@ -296,7 +313,7 @@ async function contentDirectory_EntityPropertyValuesUpdated(db: DB, event: Subst
       await updateCategoryEntityPropertyValues(
         db,
         where,
-        decode.setProperties<ICategory>(event, CategoryPropertyNamesWithId)
+        decode.setProperties<ICategory>(event, categoryPropertyNamesWithId)
       )
       break
 
@@ -379,6 +396,15 @@ async function contentDirectory_EntityPropertyValuesUpdated(db: DB, event: Subst
       )
       break
 
+    case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+      await updateFeaturedVideoEntityPropertyValues(
+        db,
+        where,
+        decode.setProperties<IFeaturedVideo>(event, featuredVideoPropertyNamesWithId),
+        0
+      )
+      break
+
     default:
       throw new Error(`Unknown class name: ${cls.name}`)
   }

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

@@ -1,97 +1,119 @@
+import assert from 'assert'
+
 import { DB } from '../../../generated/indexer'
 import { Channel } from '../../../generated/graphql-server/src/modules/channel/channel.model'
 import { Category } from '../../../generated/graphql-server/src/modules/category/category.model'
-import { KnownLicense } from '../../../generated/graphql-server/src/modules/known-license/known-license.model'
-import { UserDefinedLicense } from '../../../generated/graphql-server/src/modules/user-defined-license/user-defined-license.model'
-import { JoystreamMediaLocation } from '../../../generated/graphql-server/src/modules/joystream-media-location/joystream-media-location.model'
-import { HttpMediaLocation } from '../../../generated/graphql-server/src/modules/http-media-location/http-media-location.model'
+import { 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 { Video } from '../../../generated/graphql-server/src/modules/video/video.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 { 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'
 
+function assertKeyViolation(entityName: string, entityId: string) {
+  assert(false, `Can not remove ${entityName}(${entityId})! There are references to this entity`)
+}
+
 async function removeChannel(db: DB, where: IWhereCond): Promise<void> {
   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) throw Error(`Channel(${where.where.id}) not found`)
+  if (record.videos && record.videos.length) assertKeyViolation(`Channel`, record.id)
   await db.remove<Channel>(record)
 }
+
 async function removeCategory(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Category, where)
-  if (record === undefined) throw Error(`Category not found`)
-  if (record.videos) record.videos.map(async (v) => await removeVideo(db, { where: { id: v.id } }))
+  if (!record) throw Error(`Category(${where.where.id}) not found`)
+  if (record.videos && record.videos.length) assertKeyViolation(`Category`, record.id)
   await db.remove<Category>(record)
 }
 async function removeVideoMedia(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(VideoMedia, where)
-  if (record === undefined) throw Error(`VideoMedia not found`)
-  if (record.video) await db.remove<Video>(record.video)
+  if (!record) throw Error(`VideoMedia(${where.where.id}) not found`)
+  if (record.video) assertKeyViolation(`VideoMedia`, record.id)
   await db.remove<VideoMedia>(record)
 }
 async function removeVideo(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Video, where)
-  if (record === undefined) throw Error(`Video not found`)
+  if (!record) throw Error(`Video(${where.where.id}) not found`)
   await db.remove<Video>(record)
 }
 
 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) throw Error(`License(${where.where.id}) not found`)
+  if (record.videolicense && record.videolicense.length) assertKeyViolation(`License`, record.id)
+  await db.remove<LicenseEntity>(record)
 }
+
 async function removeUserDefinedLicense(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(UserDefinedLicense, where)
-  if (record === undefined) throw Error(`UserDefinedLicense not found`)
-  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) throw Error(`UserDefinedLicense(${where.where.id}) not found`)
+  await db.remove<UserDefinedLicenseEntity>(record)
 }
+
 async function removeKnownLicense(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(KnownLicense, where)
-  if (record === undefined) throw Error(`KnownLicense not found`)
-  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) throw Error(`KnownLicense(${where.where.id}) not found`)
+  await db.remove<KnownLicenseEntity>(record)
 }
 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) throw Error(`MediaLocation(${where.where.id}) not found`)
+  if (record.videoMedia) assertKeyViolation('MediaLocation', record.id)
+  await db.remove<MediaLocationEntity>(record)
 }
+
 async function removeHttpMediaLocation(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(HttpMediaLocation, where)
-  if (record === undefined) throw Error(`HttpMediaLocation not found`)
-  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) throw Error(`HttpMediaLocation(${where.where.id}) not found`)
+  if (record.medialocationentityhttpMediaLocation && record.medialocationentityhttpMediaLocation.length) {
+    assertKeyViolation('HttpMediaLocation', record.id)
+  }
+  await db.remove<HttpMediaLocationEntity>(record)
 }
+
 async function removeJoystreamMediaLocation(db: DB, where: IWhereCond): Promise<void> {
-  const record = await db.get(JoystreamMediaLocation, where)
-  if (record === undefined) throw Error(`JoystreamMediaLocation not found`)
-  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) throw Error(`JoystreamMediaLocation(${where.where.id}) not found`)
+  if (record.medialocationentityjoystreamMediaLocation && record.medialocationentityjoystreamMediaLocation.length) {
+    assertKeyViolation('JoystreamMediaLocation', record.id)
+  }
+  await db.remove<JoystreamMediaLocationEntity>(record)
 }
+
 async function removeLanguage(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(Language, where)
-  if (record === undefined) throw Error(`Language not found`)
-  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) throw Error(`Language(${where.where.id}) not found`)
+  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)
 }
+
 async function removeVideoMediaEncoding(db: DB, where: IWhereCond): Promise<void> {
   const record = await db.get(VideoMediaEncoding, where)
-  if (record === undefined) throw Error(`Language not found`)
+  if (!record) throw Error(`VideoMediaEncoding(${where.where.id}) not found`)
   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) throw Error(`FeaturedVideo(${where.where.id}) not found`)
+
+  record.video.isFeatured = false
+  record.video.featured = undefined
+
+  await db.save<Video>(record.video)
+  await db.remove<FeaturedVideo>(record)
+}
+
 export {
   removeCategory,
   removeChannel,
@@ -105,4 +127,5 @@ export {
   removeVideoMediaEncoding,
   removeMediaLocation,
   removeLicense,
+  removeFeaturedVideo,
 }

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

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

+ 69 - 40
query-node/mappings/content-directory/get-or-create.ts

@@ -1,19 +1,20 @@
 import { Channel } from '../../generated/graphql-server/src/modules/channel/channel.model'
 import { Category } from '../../generated/graphql-server/src/modules/category/category.model'
-import { KnownLicense } from '../../generated/graphql-server/src/modules/known-license/known-license.model'
-import { UserDefinedLicense } from '../../generated/graphql-server/src/modules/user-defined-license/user-defined-license.model'
-import { JoystreamMediaLocation } from '../../generated/graphql-server/src/modules/joystream-media-location/joystream-media-location.model'
-import { HttpMediaLocation } from '../../generated/graphql-server/src/modules/http-media-location/http-media-location.model'
+import { 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 { 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 { 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 { decode } from './decode'
 import {
-  CategoryPropertyNamesWithId,
+  categoryPropertyNamesWithId,
   channelPropertyNamesWithId,
   httpMediaLocationPropertyNamesWithId,
   joystreamMediaLocationPropertyNamesWithId,
@@ -39,6 +40,7 @@ import {
   IMediaLocation,
   IReference,
   IUserDefinedLicense,
+  IVideo,
   IVideoMedia,
   IVideoMediaEncoding,
 } from '../types'
@@ -55,6 +57,7 @@ import {
   createVideoMediaEncoding,
   createLicense,
   createMediaLocation,
+  createVideo,
 } from './entity/create'
 
 import { DB } from '../../generated/indexer'
@@ -76,7 +79,12 @@ function findEntity(entityId: number, className: string, classEntityMap: ClassEn
   if (newlyCreatedEntities === undefined) throw Error(`Couldn't find '${className}' entities in the classEntityMap`)
   const entity = newlyCreatedEntities.find((e) => e.indexOf === 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
 }
 
@@ -167,17 +175,17 @@ async function knownLicense(
   classEntityMap: ClassEntityMap,
   knownLicense: IReference,
   nextEntityIdBeforeTransaction: number
-): Promise<KnownLicense> {
-  let kLicense: KnownLicense | undefined
+): Promise<KnownLicenseEntity> {
+  let kLicense: KnownLicenseEntity | undefined
   const { entityId, existing } = knownLicense
   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`)
     return kLicense
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   // could be created in the transaction
-  kLicense = await db.get(KnownLicense, { where: { id } })
+  kLicense = await db.get(KnownLicenseEntity, { where: { id } })
   if (kLicense) return kLicense
 
   const { properties } = findEntity(entityId, 'KnownLicense', classEntityMap)
@@ -191,17 +199,17 @@ async function userDefinedLicense(
   classEntityMap: ClassEntityMap,
   userDefinedLicense: IReference,
   nextEntityIdBeforeTransaction: number
-): Promise<UserDefinedLicense> {
-  let udLicense: UserDefinedLicense | undefined
+): Promise<UserDefinedLicenseEntity> {
+  let udLicense: UserDefinedLicenseEntity | undefined
   const { entityId, existing } = userDefinedLicense
   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`)
     return udLicense
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   // could be created in the transaction
-  udLicense = await db.get(UserDefinedLicense, {
+  udLicense = await db.get(UserDefinedLicenseEntity, {
     where: { id },
   })
   if (udLicense) return udLicense
@@ -264,7 +272,7 @@ async function category(
   const { properties } = findEntity(entityId, 'Category', classEntityMap)
   return await createCategory(
     { db, block, id },
-    decode.setEntityPropertyValues<ICategory>(properties, CategoryPropertyNamesWithId)
+    decode.setEntityPropertyValues<ICategory>(properties, categoryPropertyNamesWithId)
   )
 }
 
@@ -273,19 +281,19 @@ async function httpMediaLocation(
   classEntityMap: ClassEntityMap,
   httpMediaLoc: IReference,
   nextEntityIdBeforeTransaction: number
-): Promise<HttpMediaLocation | undefined> {
-  let loc: HttpMediaLocation | undefined
+): Promise<HttpMediaLocationEntity | undefined> {
+  let loc: HttpMediaLocationEntity | undefined
   const { entityId, existing } = httpMediaLoc
 
   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`)
     return loc
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
 
   // could be created in the transaction
-  loc = await db.get(HttpMediaLocation, {
+  loc = await db.get(HttpMediaLocationEntity, {
     where: { id },
   })
   if (loc) return loc
@@ -302,12 +310,12 @@ async function joystreamMediaLocation(
   classEntityMap: ClassEntityMap,
   joyMediaLoc: IReference,
   nextEntityIdBeforeTransaction: number
-): Promise<JoystreamMediaLocation | undefined> {
-  let loc: JoystreamMediaLocation | undefined
+): Promise<JoystreamMediaLocationEntity | undefined> {
+  let loc: JoystreamMediaLocationEntity | undefined
   const { entityId, existing } = joyMediaLoc
 
   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`)
     return loc
   }
@@ -315,7 +323,7 @@ async function joystreamMediaLocation(
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
 
   // could be created in the transaction
-  loc = await db.get(JoystreamMediaLocation, {
+  loc = await db.get(JoystreamMediaLocationEntity, {
     where: { id },
   })
   if (loc) return loc
@@ -332,19 +340,19 @@ async function license(
   classEntityMap: ClassEntityMap,
   license: IReference,
   nextEntityIdBeforeTransaction: number
-): Promise<License> {
-  let lic: License | undefined
+): Promise<LicenseEntity> {
+  let lic: LicenseEntity | undefined
   const { entityId, existing } = license
 
   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`)
     return lic
   }
 
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
   // could be created in the transaction
-  lic = await db.get(License, { where: { id } })
+  lic = await db.get(LicenseEntity, { where: { id } })
   if (lic) return lic
 
   const { properties } = findEntity(entityId, 'License', classEntityMap)
@@ -361,21 +369,24 @@ async function mediaLocation(
   classEntityMap: ClassEntityMap,
   location: IReference,
   nextEntityIdBeforeTransaction: number
-): Promise<MediaLocation> {
-  let loc: MediaLocation | undefined
+): Promise<MediaLocationEntity> {
+  let loc: MediaLocationEntity | undefined
   const { entityId, existing } = location
   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`)
     return loc
   }
   const id = generateEntityIdFromIndex(nextEntityIdBeforeTransaction + entityId)
 
   // could be created in the transaction
-  loc = await db.get(MediaLocation, {
+  loc = await db.get(MediaLocationEntity, {
     where: { id },
+    relations: ['httpMediaLocation', 'joystreamMediaLocation'],
   })
-  if (loc) return loc
+  if (loc) {
+    return loc
+  }
 
   const { properties } = findEntity(entityId, 'MediaLocation', classEntityMap)
   return await createMediaLocation(
@@ -386,12 +397,29 @@ 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
   )
 }
 
@@ -408,4 +436,5 @@ export const getOrCreate = {
   license,
   mediaLocation,
   nextEntityId,
+  video,
 }

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

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

+ 45 - 21
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 {
   ClassEntityMap,
+  IBatchOperation,
   ICategory,
   IChannel,
   ICreateEntityOperation,
   IDBBlockId,
   IEntity,
+  IFeaturedVideo,
   IHttpMediaLocation,
   IJoystreamMediaLocation,
   IKnownLicense,
@@ -25,7 +27,7 @@ import {
   IWhereCond,
 } from '../types'
 import {
-  CategoryPropertyNamesWithId,
+  categoryPropertyNamesWithId,
   channelPropertyNamesWithId,
   knownLicensePropertyNamesWIthId,
   userDefinedLicensePropertyNamesWithId,
@@ -38,6 +40,7 @@ import {
   ContentDirectoryKnownClasses,
   licensePropertyNamesWithId,
   mediaLocationPropertyNamesWithId,
+  featuredVideoPropertyNamesWithId,
 } from './content-dir-consts'
 import {
   updateCategoryEntityPropertyValues,
@@ -52,6 +55,7 @@ import {
   updateVideoMediaEncodingEntityPropertyValues,
   updateLicenseEntityPropertyValues,
   updateMediaLocationEntityPropertyValues,
+  updateFeaturedVideoEntityPropertyValues,
 } from './entity/update'
 
 import {
@@ -69,6 +73,7 @@ import {
   createLicense,
   createMediaLocation,
   createBlockOrGetFromDatabase,
+  createFeaturedVideo,
 } from './entity/create'
 import { getOrCreate } from './get-or-create'
 
@@ -81,33 +86,35 @@ async function getNextEntityId(db: DB): Promise<number> {
   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
 export async function contentDirectory_TransactionCompleted(db: DB, event: SubstrateEvent): Promise<void> {
   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
   // We need this to know which entity belongs to which class(we will need to know to update/create
   // Channel, Video etc.). For example if there is a property update operation there is no class id
-  await batchCreateClassEntities(db, block, createEntityOperations)
-
-  await batchAddSchemaSupportToEntity(db, createEntityOperations, addSchemaSupportToEntityOperations, block)
-
+  await batchCreateClassEntities(db, event.blockNumber, createEntityOperations)
+  await batchAddSchemaSupportToEntity(db, createEntityOperations, addSchemaSupportToEntityOperations, event.blockNumber)
   await batchUpdatePropertyValue(db, createEntityOperations, updatePropertyValuesOperations)
 }
 
@@ -167,7 +174,7 @@ async function batchAddSchemaSupportToEntity(
 
       switch (className) {
         case ContentDirectoryKnownClasses.CATEGORY:
-          await createCategory(arg, decode.setEntityPropertyValues<ICategory>(properties, CategoryPropertyNamesWithId))
+          await createCategory(arg, decode.setEntityPropertyValues<ICategory>(properties, categoryPropertyNamesWithId))
           break
 
         case ContentDirectoryKnownClasses.CHANNEL:
@@ -256,6 +263,15 @@ async function batchAddSchemaSupportToEntity(
           )
           break
 
+        case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+          await createFeaturedVideo(
+            arg,
+            classEntityMap,
+            decode.setEntityPropertyValues<IFeaturedVideo>(properties, featuredVideoPropertyNamesWithId),
+            nextEntityIdBeforeTransaction
+          )
+          break
+
         default:
           console.log(`Unknown class name: ${className}`)
           break
@@ -299,7 +315,7 @@ async function batchUpdatePropertyValue(db: DB, createEntityOperations: ICreateE
         await updateCategoryEntityPropertyValues(
           db,
           where,
-          decode.setEntityPropertyValues<ICategory>(properties, CategoryPropertyNamesWithId)
+          decode.setEntityPropertyValues<ICategory>(properties, categoryPropertyNamesWithId)
         )
         break
 
@@ -384,6 +400,14 @@ async function batchUpdatePropertyValue(db: DB, createEntityOperations: ICreateE
           entityIdBeforeTransaction
         )
         break
+      case ContentDirectoryKnownClasses.FEATUREDVIDEOS:
+        await updateFeaturedVideoEntityPropertyValues(
+          db,
+          where,
+          decode.setEntityPropertyValues<IFeaturedVideo>(properties, featuredVideoPropertyNamesWithId),
+          entityIdBeforeTransaction
+        )
+        break
 
       default:
         console.log(`Unknown class name: ${className}`)

+ 19 - 9
query-node/mappings/types.ts

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

+ 6 - 7
query-node/package.json

@@ -6,17 +6,16 @@
 		"build": "./build.sh",
 		"test": "echo \"Error: no test specified\" && exit 1",
 		"clean": "rm -rf ./generated",
-		"processor:start": "(cd ./generated/indexer && yarn && DEBUG=${DEBUG} yarn start:processor)",
-		"indexer:start": "(cd ./generated/indexer && yarn && DEBUG=${DEBUG} yarn start:indexer)",
+		"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:prod": "(cd ./generated/graphql-server && yarn start:prod)",
 		"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:migrate": "./db-migrate.sh",
 		"db:schema:migrate": "(cd ./generated/graphql-server && yarn db:create && yarn db:sync && yarn db:migrate)",
 		"db:indexer:migrate": "(cd ./generated/indexer && yarn db:migrate)",
-		"db:migrate": "yarn db:schema:migrate && yarn db:indexer:migrate",
-		"codegen:all": "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:server": "yarn hydra-cli codegen --no-install --no-indexer",
 		"cd-classes": "ts-node scripts/get-class-id-and-name.ts"
@@ -24,10 +23,10 @@
 	"author": "",
 	"license": "ISC",
 	"devDependencies": {
-		"@dzlzv/hydra-cli": "^0.0.20"
+		"@dzlzv/hydra-cli": "^0.0.24"
 	},
 	"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",
 		"@types/bn.js": "^4.11.6",
 		"@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]}")"
 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() {
     # Show tail end of logs for the processor and indexer containers to
     # see any possible errors
     (echo "## Processor Logs ##" && docker logs joystream_processor_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
 }
 
 trap cleanup EXIT
 
+# We expect docker image to be started by test runner
 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
+
 # Starting up processor will bring up all services it depends on
 docker-compose up -d processor
 

+ 80 - 32
query-node/schema.graphql

@@ -18,7 +18,7 @@ type Member @entity {
   id: ID!
 
   "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"
   avatarUri: String
@@ -84,16 +84,16 @@ type Channel @entity {
   # owner: Member!
 
   "The title of the Channel"
-  title: String! @fulltext(query: "titles")
+  handle: String! @fulltext(query: "search")
 
   "The description of a Channel"
   description: String!
 
   "Url for Channel's cover (background) photo. Recommended ratio: 16:9."
-  coverPhotoURL: String!
+  coverPhotoUrl: String
 
   "Channel's avatar photo."
-  avatarPhotoURL: String!
+  avatarPhotoUrl: String
 
   "Flag signaling whether a channel is public."
   isPublic: Boolean!
@@ -102,7 +102,7 @@ type Channel @entity {
   isCurated: Boolean!
 
   "The primary langauge of the channel's content"
-  language: Language!
+  language: Language
 
   videos: [Video!] @derivedFrom(field: "channel")
 
@@ -114,7 +114,7 @@ type Category @entity {
   id: ID!
 
   "The name of the category"
-  name: String! @unique @fulltext(query: "names")
+  name: String! @unique @fulltext(query: "categoriesByName")
 
   "The description of the category"
   description: String
@@ -134,7 +134,7 @@ type VideoMediaEncoding @entity {
   happenedIn: Block!
 }
 
-type KnownLicense @entity {
+type KnownLicenseEntity @entity {
   "Runtime entity identifier (EntityId)"
   id: ID!
 
@@ -153,7 +153,7 @@ type KnownLicense @entity {
   happenedIn: Block!
 }
 
-type UserDefinedLicense @entity {
+type UserDefinedLicenseEntity @entity {
   "Runtime entity identifier (EntityId)"
   id: ID!
 
@@ -163,39 +163,24 @@ type UserDefinedLicense @entity {
   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)"
   id: ID!
 
   # One of the following field will be non-null
 
   "A reference to HttpMediaLocation"
-  httpMediaLocation: HttpMediaLocation
+  httpMediaLocation: HttpMediaLocationEntity
 
   "A reference to JoystreamMediaLocation"
-  joystreamMediaLocation: JoystreamMediaLocation
+  joystreamMediaLocation: JoystreamMediaLocationEntity
 
-  videoMedia: VideoMedia @derivedFrom(field: "location")
+  videoMedia: VideoMedia @derivedFrom(field: "locationEntity")
 
   happenedIn: Block!
 }
 
-type JoystreamMediaLocation @entity {
+type JoystreamMediaLocationEntity @entity {
   "Runtime entity identifier (EntityId)"
   id: ID!
 
@@ -205,7 +190,7 @@ type JoystreamMediaLocation @entity {
   happenedIn: Block!
 }
 
-type HttpMediaLocation @entity {
+type HttpMediaLocationEntity @entity {
   "Runtime entity identifier (EntityId)"
   id: ID!
 
@@ -239,6 +224,8 @@ type VideoMedia @entity {
   "Location of the video media object"
   location: MediaLocation!
 
+  locationEntity: MediaLocationEntity
+
   happenedIn: Block!
 }
 
@@ -253,7 +240,7 @@ type Video @entity {
   category: Category!
 
   "The title of the video"
-  title: String! @fulltext(query: "titles")
+  title: String! @fulltext(query: "search")
 
   "The description of the Video"
   description: String!
@@ -265,7 +252,7 @@ type Video @entity {
   skippableIntroDuration: Int
 
   "Video thumbnail url (recommended ratio: 16:9)"
-  thumbnailURL: String!
+  thumbnailUrl: String!
 
   "Video's main langauge"
   language: Language
@@ -288,7 +275,68 @@ type Video @entity {
   "Whether the Video contains explicit material."
   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!
 }
+
+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"
-}

+ 102 - 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::{
-    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,
 };
 use frame_system::ensure_signed;
 #[cfg(feature = "std")]
@@ -1628,64 +1632,115 @@ decl_module! {
             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
                         let entity_id = Self::next_entity_id() - T::EntityId::one();
                         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
+                   },
+                   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
                         )?;
-                        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
-                        )?;
-                        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> {
+    /// 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`.
     fn update_entity_rc(
         entity_id: T::EntityId,
@@ -2830,6 +2885,7 @@ decl_event!(
         Nonce = <T as Trait>::Nonce,
         SideEffects = Option<ReferenceCounterSideEffects<T>>,
         SideEffect = Option<(<T as Trait>::EntityId, EntityReferenceCounterSideEffect)>,
+        FailedAt = u32,
     {
         CuratorGroupAdded(CuratorGroupId),
         CuratorGroupRemoved(CuratorGroupId),
@@ -2854,5 +2910,6 @@ decl_event!(
         InsertedAtVectorIndex(Actor, EntityId, PropertyId, VecMaxLength, Nonce, SideEffect),
         EntityOwnershipTransfered(EntityId, EntityController, SideEffects),
         TransactionCompleted(Actor),
+        TransactionFailed(Actor, FailedAt),
     }
 );

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

@@ -434,13 +434,14 @@ type RawTestEvent = RawEvent<
     Nonce,
     Option<ReferenceCounterSideEffects<Runtime>>,
     Option<(EntityId, EntityReferenceCounterSideEffect)>,
+    u32,
 >;
 
 pub fn get_test_event(raw_event: RawTestEvent) -> TestEvent {
     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
     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
-    assert_event_success(
+    assert_event(
         entity_schema_support_added_event,
         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));
 
         // 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));
 
         // Event checked
-        assert_event_success(
+        assert_event(
             curator_group_created_event,
             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
-        assert_event_success(
+        assert_event(
             curator_group_curator_added_event,
             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
-        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
-        assert_event_success(
+        assert_event(
             entity_property_vector_cleared_event,
             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));
 
         // 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));
 
         // 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
-        assert_event_success(
+        assert_event(
             inserted_at_vector_index_event,
             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
-        assert_event_success(
+        assert_event(
             removed_at_vector_index_event,
             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
-        assert_event_success(
+        assert_event(
             curator_group_curator_removed_event,
             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));
 
         // Event checked
-        assert_event_success(
+        assert_event(
             curator_group_removed_event,
             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));
 
         // 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
-        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
-        assert_event_success(
+        assert_event(
             curator_group_status_set_event,
             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
 
-        let entity_ownership_transfered_event =
-            get_test_event(RawEvent::TransactionCompleted(actor));
+        let transaction_completed_event = get_test_event(RawEvent::TransactionCompleted(actor));
 
         // Last event checked
-        assert_event_success(
-            entity_ownership_transfered_event,
+        assert_event(
+            transaction_completed_event,
             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,
+        );
+    })
+}

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

@@ -51,7 +51,7 @@ fn transfer_entity_ownership_success() {
         );
 
         // Last event checked
-        assert_event_success(
+        assert_event(
             entity_ownership_transfered_event,
             number_of_events_before_calls + 1,
         );

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

@@ -103,7 +103,7 @@ fn update_class_permissions_success() {
         );
 
         // Event checked
-        assert_event_success(
+        assert_event(
             class_permissions_updated_event,
             number_of_events_before_call + 2,
         );

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

@@ -48,7 +48,7 @@ fn update_class_schema_status_success() {
         ));
 
         // Last event checked
-        assert_event_success(
+        assert_event(
             class_schema_status_updated_event,
             number_of_events_before_call + 1,
         );

+ 2 - 2
runtime-modules/content-directory/src/tests/update_entity_creation_voucher.rs

@@ -40,7 +40,7 @@ fn create_entity_creation_voucher_success() {
         );
 
         // Last event checked
-        assert_event_success(
+        assert_event(
             entity_creation_voucher_created_event,
             number_of_events_before_call + 1,
         );
@@ -101,7 +101,7 @@ fn update_entity_creation_voucher_success() {
         );
 
         // Last event checked
-        assert_event_success(
+        assert_event(
             entity_creation_voucher_created_event,
             number_of_events_before_call + 1,
         );

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

@@ -43,7 +43,7 @@ fn update_entity_permissions_success() {
             get_test_event(RawEvent::EntityPermissionsUpdated(FIRST_ENTITY_ID));
 
         // Last event checked
-        assert_event_success(
+        assert_event(
             entity_permissions_updated_event,
             number_of_events_before_call + 1,
         );

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

@@ -55,7 +55,7 @@ fn update_entity_property_values_success() {
         );
 
         // Last event checked
-        assert_event_success(
+        assert_event(
             entity_property_values_updated_event,
             number_of_events_before_calls + 1,
         );

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