Browse Source

Merge pull request #1463 from Lezek123/content-directory-schemas

Content directory: JSON schemas and inputs + related tooling
Mokhtar Naamani 4 years ago
parent
commit
bbcf064177
62 changed files with 2087 additions and 192 deletions
  1. 20 0
      .github/workflows/content-directory-schemas.yml
  2. 10 0
      content-directory-schemas/.gitignore
  3. 152 0
      content-directory-schemas/README.md
  4. 7 0
      content-directory-schemas/inputs/classes/ChannelClass.json
  5. 6 0
      content-directory-schemas/inputs/classes/ContentCategoryClass.json
  6. 7 0
      content-directory-schemas/inputs/classes/CurationStatusClass.json
  7. 7 0
      content-directory-schemas/inputs/classes/HttpMediaLocationClass.json
  8. 7 0
      content-directory-schemas/inputs/classes/JoystreamMediaLocationClass.json
  9. 6 0
      content-directory-schemas/inputs/classes/KnownLicenseClass.json
  10. 6 0
      content-directory-schemas/inputs/classes/LanguageClass.json
  11. 7 0
      content-directory-schemas/inputs/classes/LicenseClass.json
  12. 7 0
      content-directory-schemas/inputs/classes/MediaLocationClass.json
  13. 7 0
      content-directory-schemas/inputs/classes/UserDefinedLicenseClass.json
  14. 7 0
      content-directory-schemas/inputs/classes/VideoClass.json
  15. 7 0
      content-directory-schemas/inputs/classes/VideoMediaClass.json
  16. 6 0
      content-directory-schemas/inputs/classes/VideoMediaEncodingClass.json
  17. 13 0
      content-directory-schemas/inputs/entityBatches/ChannelBatch.json
  18. 7 0
      content-directory-schemas/inputs/entityBatches/ContentCategoryBatch.json
  19. 11 0
      content-directory-schemas/inputs/entityBatches/KnownLicenseBatch.json
  20. 8 0
      content-directory-schemas/inputs/entityBatches/LanguageBatch.json
  21. 63 0
      content-directory-schemas/inputs/entityBatches/VideoBatch.json
  22. 4 0
      content-directory-schemas/inputs/entityBatches/VideoMediaEncodingBatch.json
  23. 50 0
      content-directory-schemas/inputs/schemas/ChannelSchema.json
  24. 20 0
      content-directory-schemas/inputs/schemas/ContentCategorySchema.json
  25. 27 0
      content-directory-schemas/inputs/schemas/CurationStatusSchema.json
  26. 20 0
      content-directory-schemas/inputs/schemas/HttpMediaLocationSchema.json
  27. 13 0
      content-directory-schemas/inputs/schemas/JoystreamMediaLocationSchema.json
  28. 43 0
      content-directory-schemas/inputs/schemas/KnownLicenseSchema.json
  29. 20 0
      content-directory-schemas/inputs/schemas/LanguageSchema.json
  30. 17 0
      content-directory-schemas/inputs/schemas/LicenseSchema.json
  31. 19 0
      content-directory-schemas/inputs/schemas/MediaLocationSchema.json
  32. 13 0
      content-directory-schemas/inputs/schemas/UserDefinedLicenseSchema.json
  33. 13 0
      content-directory-schemas/inputs/schemas/VideoMediaEncodingSchema.json
  34. 40 0
      content-directory-schemas/inputs/schemas/VideoMediaSchema.json
  35. 101 0
      content-directory-schemas/inputs/schemas/VideoSchema.json
  36. 43 0
      content-directory-schemas/package.json
  37. 154 0
      content-directory-schemas/schemas/extrinsics/AddClassSchema.schema.json
  38. 44 0
      content-directory-schemas/schemas/extrinsics/CreateClass.schema.json
  39. 38 0
      content-directory-schemas/schemas/propertyValidationDefs.schema.json
  40. 88 0
      content-directory-schemas/scripts/devInitAliceLead.ts
  41. 67 0
      content-directory-schemas/scripts/initializeContentDir.ts
  42. 150 0
      content-directory-schemas/scripts/inputSchemasToEntitySchemas.ts
  43. 45 0
      content-directory-schemas/scripts/schemasToTS.ts
  44. 85 0
      content-directory-schemas/scripts/validate.ts
  45. 270 0
      content-directory-schemas/src/helpers/InputParser.ts
  46. 55 0
      content-directory-schemas/src/helpers/extrinsics.ts
  47. 20 0
      content-directory-schemas/src/helpers/inputs.ts
  48. 16 0
      content-directory-schemas/src/helpers/propertyType.ts
  49. 8 0
      content-directory-schemas/src/helpers/schemas.ts
  50. 5 0
      content-directory-schemas/src/index.ts
  51. 27 0
      content-directory-schemas/tsconfig.json
  52. 7 0
      content-directory-schemas/tsconfig.lib.json
  53. 4 0
      content-directory-schemas/types/EntityBatch.d.ts
  54. 67 0
      content-directory-schemas/types/extrinsics/AddClassSchema.d.ts
  55. 30 0
      content-directory-schemas/types/extrinsics/CreateClass.d.ts
  56. 1 0
      content-directory-schemas/types/utility.d.ts
  57. 2 0
      content-directory-schemas/typings/@joystream/prettier-config/index.d.ts
  58. 60 0
      content-directory-schemas/vscode-recommended.settings.json
  59. 3 2
      package.json
  60. 16 1
      types/src/content-directory/index.ts
  61. 0 188
      utils/api-examples/scripts/init-new-content-directory.js
  62. 11 1
      yarn.lock

+ 20 - 0
.github/workflows/content-directory-schemas.yml

@@ -0,0 +1,20 @@
+name: content-directory-schemas
+on: [pull_request, push]
+
+jobs:
+  schemas_checks:
+    name: Checks
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        node-version: [12.x]
+    steps:
+    - uses: actions/checkout@v1
+    - name: Use Node.js ${{ matrix.node-version }}
+      uses: actions/setup-node@v1
+      with:
+        node-version: ${{ matrix.node-version }}
+    - name: validate
+      run: |
+        yarn install --frozen-lockfile
+        yarn workspace cd-schemas checks --quiet

+ 10 - 0
content-directory-schemas/.gitignore

@@ -0,0 +1,10 @@
+operations.json
+
+# Auto-generated
+schemas/entities
+schemas/entityBatches
+schemas/entityReferences
+types/entities
+
+# Build
+lib

+ 152 - 0
content-directory-schemas/README.md

@@ -0,0 +1,152 @@
+# Content directory json schemas and inputs
+
+## Definitions
+
+In order to make this documentation as clear as possible it is important to make a strict distinction between two types of schemas:
+
+- `json-schemas` mean files with `.schema.json` extension. This is a common standard for describing how to validate other `json` files or objects (ie. a `package.json` file may be an example of a file that can be supported by a `json-schema`). A documentation of this standard can be found here: https://json-schema.org/
+- `runtime-scheams` means schemas as they are "understood" by the `content-directory` runtime module, so schemas that can be added to classes via `api.tx.contentDirectory.addClassSchema` and linked to entities via `api.tx.contentDirectory.addSchemaSupportToEntity`
+
+## Content directory input
+
+### Initializing content directory
+
+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
+```
+
+This will handle:
+
+- Creating a membership for `ALICE` (if not already created)
+- Setting (hiring) `ALICE` as content curators lead (if not already set)
+- Creating classes in the runtime based on `inputs/classes` json inputs (if the content directory is currently empty)
+- Creating schemas in the runtime based on `inputs/schemas` and adding them to the related classes
+- Creating entities based on `inputs/entityBatches`. Those json inputs allow describing entities and relationships between them in a simplified way and are then converted into one huge `api.tx.contentDirectory.transaction` call (this is further described in _**Entity batches**_ section).
+
+### Input files naming
+
+In order to get the full benefit of the tooling, in some cases you may need to respect a specific pattern of file naming:
+
+Each input file name should end with `Class`, `Schema` or `Batch` (depending on the input type, ie. `LanguageBatch`).
+It is also recommended that each of those file names starts with a class name (currently in `entityBatches` there's no distinction between schemas and classes, as it is assumed there will be a one-to-one relationship between them)
+
+### `json-schemas` support for json inputs in `VSCode`
+
+In order to link json files inside `inputs` directory to `json-schemas` inside `schemas` and have them validated in real-time by the IDE, follow the steps below:
+
+**If you don't have `.vscode/settings.json` in the root monorepo workspace yet:**
+
+1. Create `.vscode` directory inside your monorepo workspace
+1. Copy `vscode-recommended.settings.json` into this `.vscode` directory and rename it to `settings.json`.
+
+**If you already have the `.vscode/settings.json` file in the root monorepo workspace:**
+
+1. Copy the settings from `vscode-recommended.settings.json` and merge them with the existing `.vscode/settings.json`
+
+Now all the json files matching `*Class.json`, `*Schema.json`, `*{EntityName}Batch.json` patters will be linked to the correct `json schemas`. If you edit any file inside `inputs` or add a new one that follows the naming pattern (described in _Input files naming_), you should get the benefit of autocompleted properties, validated input, on-hover tooltips with property descriptions etc.
+
+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.
+
+### Entity batches
+
+The concept of entity batches (`inputs/entityBatches`) basically provides an easy way of describing complex input to content directory (ie. many entities related to each other in many ways) without the need to deal with lower-level, hard-to-validate runtime operations like `CreateEntity` and `AddSchemaSupportEntity` and trying to glue them together into a huge `api.tx.contentDirectory.transaction` call.
+
+Instead, the script that initializes the content directory (`scripts/initializeContentDir.ts`) is able to generate the complex `api.tx.contentDirectory.transaction` call based on a more human-readable input provided in `inputs/entityBatches`.
+
+This input can be provided as a simple json array of objects matching `{ [propertyName]: propertyValue}` structure.
+
+For example, in order to describe creating entities as simple as `Language`, which only has `Code` and `Name` properties, we can just create an array of objects like:
+
+```
+[
+  { "Code": "EN", "Name": "English" },
+  { "Code": "RU", "Name": "Russian" },
+  { "Code": "DE", "Name": "German" }
+]
+```
+
+_(This is the actual content of `inputs/entityBatches/LanguageBatch.json`)_
+
+#### Related entities
+
+There also exists a specific syntax for defining relations between entities in batches.
+We can do it by either using `"new"` or `"existing"` keyword.
+
+- The `"new"` keyword allows describing a scenario where related entity should be created **along with** the main entity and then referenced by it. An example of this could be `Video` and `VideoMedia` which have a one-to-one relationship and it doesn't make much sense to specify them in separate batches. Instead, we can use a syntax like:
+
+```
+{
+  "title": "Awesome video",
+  /* other Video properties... */
+  "media": { "new": {
+    "pixelWidth": 1024,
+    "pixelHeight": 764,
+    /* other VideoMedia object properties... */
+  }
+}
+```
+
+- The `"existing"` keyword allows referencing an entity created as part of any other batch inside `inputs/entityBatches`. We can do it by specifying the value of **any unique property of the referenced entity**. So, for example to reference a `Language` entity from `VideoBatch.json` file, we use this syntax:
+
+```
+{
+  "title": "Awesome video",
+  /* other Video properties... */
+  "language": { "existing": { "Code": "EN" } }
+}
+```
+
+## `json-schemas` and tooling
+
+### Entity `json-schemas`
+
+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
+```
+
+Those `json-schemas` are currently mainly used for validating the inputs inside `inputs/entityBatches`.
+
+The generated `json-schemas` include:
+
+- `schemas/entities` - `json-schemas` that provide validation for given entity (ie. `Video`) input. They can, for example, check if the `title` property in a json object is a string that is no longer than `64` characters. They are used to validate a single entity in `inputs/entityBatches`, but can also be re-used to provide "frontend" validation of any entity input to the content directory (ie. input provided to/via `joystream-cli`).
+- `schemas/entityReferences` - `json-schemas` that describe how an entity of given class can be referenced. Currently they are used for providing an easy way of referencing entites between batches in `inputs/entityBatches`. For more details on how entities can be referenced in batches, read the _**Entity batches**_ section.
+- `schemas/entityBatches` - very simple `json-schemas` that basically just provide `array` wrappers over `schemas/entities`. Those are the actual `json-schemas` that can be linked to json input files inside `inputs/entityBatches` (ie. via `.vscode/settings.json`)
+
+### Typescript support
+
+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
+```
+
+This command will generate:
+
+- `types/entities` based on `schemas/entities`, providing typescript interfaces for entities like `Video` etc. (note that this interface will include a peculiar way of describing entity relationships, further described in _**Entity batches**_ section)
+- `types/extrinsics` based on `schemas/extrinsics`, providing typescript interfaces for input to extrinsics like `AddClassSchema` and `CreateClass`
+
+The most obvious use-case of those interfaces currently is that when we're parsing any json files inside `inputs` using a Typescript code, we can assert that the resulting object will be of given type, ie.:
+
+```
+const createClassInput = JSON.parse(fs.readFileSync('/path/to/inputs/LanguageClass.json')) as CreateClass
+```
+
+Besides that, a Typescript code can be written to generate some inputs (ie. using a loop) that can then can be used to create classes/schemas or insert entities into the content directory.
+
+There are a lot of other potential use-cases, but for the purpose of this documentation it should be enough to mention there exists this very easy way of converting `.schema.json` files into Typescript interfaces.
+
+## Current limitations
+
+Some limitations that should be dealt with in the nearest future:
+
+- Filename restrictions described in **_Input files naming_** section
+- Some code runs on the assumption that there is only one schema for each class, which is very limiting
+- `Vector<Reference>` property type is not yet supported when parsing entity batches

+ 7 - 0
content-directory-schemas/inputs/classes/ChannelClass.json

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

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

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

+ 7 - 0
content-directory-schemas/inputs/classes/CurationStatusClass.json

@@ -0,0 +1,7 @@
+{
+  "name": "CurationStatus",
+  "description": "Curation status of a related entity (ie. Video or Channel)",
+  "maximum_entities_count": 400,
+  "default_entity_creation_voucher_upper_bound": 50,
+  "class_permissions": { "any_member": false }
+}

+ 7 - 0
content-directory-schemas/inputs/classes/HttpMediaLocationClass.json

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

+ 7 - 0
content-directory-schemas/inputs/classes/JoystreamMediaLocationClass.json

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

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

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

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

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

+ 7 - 0
content-directory-schemas/inputs/classes/LicenseClass.json

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

+ 7 - 0
content-directory-schemas/inputs/classes/MediaLocationClass.json

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

+ 7 - 0
content-directory-schemas/inputs/classes/UserDefinedLicenseClass.json

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

+ 7 - 0
content-directory-schemas/inputs/classes/VideoClass.json

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

+ 7 - 0
content-directory-schemas/inputs/classes/VideoMediaClass.json

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

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

@@ -0,0 +1,6 @@
+{
+  "name": "VideoMediaEncoding",
+  "description": "Available encoding format for the video media",
+  "maximum_entities_count": 100,
+  "default_entity_creation_voucher_upper_bound": 50
+}

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

@@ -0,0 +1,13 @@
+{
+  "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
+    }
+  ]
+}

+ 7 - 0
content-directory-schemas/inputs/entityBatches/ContentCategoryBatch.json

@@ -0,0 +1,7 @@
+{
+  "className": "ContentCategory",
+  "entries": [
+    { "name": "Cartoon", "description": "Content which is a cartoon or related to cartoons" },
+    { "name": "Sports", "description": "Content related to sports" }
+  ]
+}

+ 11 - 0
content-directory-schemas/inputs/entityBatches/KnownLicenseBatch.json

@@ -0,0 +1,11 @@
+{
+  "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" }
+  ]
+}

+ 8 - 0
content-directory-schemas/inputs/entityBatches/LanguageBatch.json

@@ -0,0 +1,8 @@
+{
+  "className": "Language",
+  "entries": [
+    { "code": "EN", "name": "English" },
+    { "code": "RU", "name": "Russian" },
+    { "code": "DE", "name": "German" }
+  ]
+}

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

@@ -0,0 +1,63 @@
+{
+  "className": "Video",
+  "entries": [
+    {
+      "title": "Caminades 2",
+      "description": "Caminandes 2: Gran Dillama",
+      "language": { "existing": { "code": "EN" } },
+      "category": { "existing": { "name": "Cartoon" } },
+      "channel": { "existing": { "title": "Joystream Cartoons" } },
+      "duration": 146,
+      "hasMarketing": false,
+      "isPublic": true,
+      "media": {
+        "new": {
+          "encoding": { "existing": { "name": "MPEG4" } },
+          "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": "Cartoon" } },
+      "channel": { "existing": { "title": "Joystream Cartoons" } },
+      "duration": 150,
+      "hasMarketing": false,
+      "isPublic": true,
+      "media": {
+        "new": {
+          "encoding": { "existing": { "name": "MPEG4" } },
+          "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" } } } }
+    }
+  ]
+}

+ 4 - 0
content-directory-schemas/inputs/entityBatches/VideoMediaEncodingBatch.json

@@ -0,0 +1,4 @@
+{
+  "className": "VideoMediaEncoding",
+  "entries": [{ "name": "MPEG4" }]
+}

+ 50 - 0
content-directory-schemas/inputs/schemas/ChannelSchema.json

@@ -0,0 +1,50 @@
+{
+  "className": "Channel",
+  "newProperties": [
+    {
+      "name": "title",
+      "description": "The title of the Channel",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": { "Text": 64 } }
+    },
+    {
+      "name": "description",
+      "description": "The description of a Channel",
+      "required": true,
+      "property_type": { "Single": { "Text": 1024 } }
+    },
+    {
+      "name": "coverPhotoUrl",
+      "description": "Url for Channel's cover (background) photo. Recommended ratio: 16:9.",
+      "required": true,
+      "property_type": { "Single": { "Text": 256 } }
+    },
+    {
+      "name": "avatarPhotoURL",
+      "description": "Channel's avatar photo.",
+      "required": true,
+      "property_type": { "Single": { "Text": 256 } }
+    },
+    {
+      "name": "isPublic",
+      "description": "Flag signaling whether a channel is public.",
+      "required": true,
+      "property_type": { "Single": "Bool" }
+    },
+    {
+      "name": "curationStatus",
+      "description": "Channel curation status set by the Curator",
+      "required": false,
+      "unique": true,
+      "property_type": { "Single": { "Reference": { "className": "CurationStatus" } } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "language",
+      "description": "The primary langauge of the channel's content",
+      "required": false,
+      "property_type": { "Single": { "Reference": { "className": "Language" } } }
+    }
+  ]
+}

+ 20 - 0
content-directory-schemas/inputs/schemas/ContentCategorySchema.json

@@ -0,0 +1,20 @@
+{
+  "className": "ContentCategory",
+  "newProperties": [
+    {
+      "name": "name",
+      "description": "The name of the category",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": { "Text": 64 } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "description",
+      "description": "The description of the category",
+      "required": false,
+      "property_type": { "Single": { "Text": 1024 } },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 27 - 0
content-directory-schemas/inputs/schemas/CurationStatusSchema.json

@@ -0,0 +1,27 @@
+{
+  "className": "CurationStatus",
+  "newProperties": [
+    {
+      "name": "approved",
+      "description": "Indicates whether the content was approved by the Curator",
+      "required": false,
+      "property_type": { "Single": "Bool" },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "comment",
+      "description": "Short, optional comment from the Curator",
+      "required": false,
+      "property_type": { "Single": { "Text": 256 } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "entityId",
+      "description": "ID of the curated entity. It's not a relation to prevent removal lock and allow different types of entities. Used to confirm the validity of Content => CurationStatus relation.",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": "Uint64" },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 20 - 0
content-directory-schemas/inputs/schemas/HttpMediaLocationSchema.json

@@ -0,0 +1,20 @@
+{
+  "className": "HttpMediaLocation",
+  "newProperties": [
+    {
+      "name": "url",
+      "description": "The http url pointing to the media",
+      "required": true,
+      "unique": false,
+      "property_type": { "Single": { "Text": 256 } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "port",
+      "description": "The port to use when connecting to the http url (defaults to 80)",
+      "required": false,
+      "property_type": { "Single": "Uint16" },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 13 - 0
content-directory-schemas/inputs/schemas/JoystreamMediaLocationSchema.json

@@ -0,0 +1,13 @@
+{
+  "className": "JoystreamMediaLocation",
+  "newProperties": [
+    {
+      "name": "dataObjectId",
+      "description": "Id of the data object in the Joystream runtime dataDirectory module",
+      "property_type": { "Single": { "Text": 48 } },
+      "required": true,
+      "unique": true,
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

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

@@ -0,0 +1,43 @@
+{
+  "className": "KnownLicense",
+  "newProperties": [
+    {
+      "name": "code",
+      "description": "Short, commonly recognized code of the licence (ie. CC_BY_SA)",
+      "required": true,
+      "unique": true,
+      "property_type": {
+        "Single": { "Text": 16 }
+      },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "name",
+      "description": "Full, descriptive name of the license (ie. Creative Commons - Attribution-NonCommercial-NoDerivs)",
+      "required": false,
+      "unique": true,
+      "property_type": {
+        "Single": { "Text": 64 }
+      },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "description",
+      "description": "Short description of the license conditions",
+      "required": false,
+      "property_type": {
+        "Single": { "Text": 1024 }
+      },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "url",
+      "description": "An url pointing to full license content",
+      "required": false,
+      "property_type": {
+        "Single": { "Text": 256 }
+      },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 20 - 0
content-directory-schemas/inputs/schemas/LanguageSchema.json

@@ -0,0 +1,20 @@
+{
+  "className": "Language",
+  "newProperties": [
+    {
+      "name": "name",
+      "description": "The name of the language (ie. English)",
+      "required": true,
+      "property_type": { "Single": { "Text": 64 } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "code",
+      "description": "ISO 639-1 code of the language (ie. en)",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": { "Text": 2 } },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

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

@@ -0,0 +1,17 @@
+{
+  "className": "License",
+  "newProperties": [
+    {
+      "name": "knownLicense",
+      "description": "Reference to a known license",
+      "required": false,
+      "property_type": { "Single": { "Reference": { "className": "KnownLicense" } } }
+    },
+    {
+      "name": "userDefinedLicense",
+      "description": "Reference to user-defined license",
+      "required": false,
+      "property_type": { "Single": { "Reference": { "className": "UserDefinedLicense", "sameOwner": true } } }
+    }
+  ]
+}

+ 19 - 0
content-directory-schemas/inputs/schemas/MediaLocationSchema.json

@@ -0,0 +1,19 @@
+{
+  "className": "MediaLocation",
+  "newProperties": [
+    {
+      "name": "httpMediaLocation",
+      "description": "A reference to HttpMediaLocation",
+      "required": false,
+      "property_type": { "Single": { "Reference": { "className": "HttpMediaLocation", "sameOwner": true } } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "joystreamMediaLocation",
+      "description": "A reference to JoystreamMediaLocation",
+      "required": false,
+      "property_type": { "Single": { "Reference": { "className": "JoystreamMediaLocation", "sameOwner": true } } },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 13 - 0
content-directory-schemas/inputs/schemas/UserDefinedLicenseSchema.json

@@ -0,0 +1,13 @@
+{
+  "className": "UserDefinedLicense",
+  "newProperties": [
+    {
+      "name": "content",
+      "description": "Custom license content",
+      "required": true,
+      "property_type": {
+        "Single": { "Text": 4096 }
+      }
+    }
+  ]
+}

+ 13 - 0
content-directory-schemas/inputs/schemas/VideoMediaEncodingSchema.json

@@ -0,0 +1,13 @@
+{
+  "className": "VideoMediaEncoding",
+  "newProperties": [
+    {
+      "name": "name",
+      "description": "The name of the encoding format (ie. H264_mpeg4)",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": { "Text": 32 } },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 40 - 0
content-directory-schemas/inputs/schemas/VideoMediaSchema.json

@@ -0,0 +1,40 @@
+{
+  "className": "VideoMedia",
+  "newProperties": [
+    {
+      "name": "encoding",
+      "description": "Encoding of the video media object",
+      "required": true,
+      "property_type": { "Single": { "Reference": { "className": "VideoMediaEncoding" } } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "pixelWidth",
+      "description": "Video media width in pixels",
+      "required": true,
+      "property_type": { "Single": "Uint16" },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "pixelHeight",
+      "description": "Video media height in pixels",
+      "required": true,
+      "property_type": { "Single": "Uint16" },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "size",
+      "description": "Video media size in bytes",
+      "required": false,
+      "property_type": { "Single": "Uint64" },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "location",
+      "description": "Location of the video media object",
+      "required": true,
+      "property_type": { "Single": { "Reference": { "className": "MediaLocation", "sameOwner": true } } },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 101 - 0
content-directory-schemas/inputs/schemas/VideoSchema.json

@@ -0,0 +1,101 @@
+{
+  "className": "Video",
+  "newProperties": [
+    {
+      "name": "channel",
+      "description": "Reference to member's channel",
+      "required": true,
+      "property_type": { "Single": { "Reference": { "className": "Channel", "sameOwner": true } } }
+    },
+    {
+      "name": "category",
+      "description": "Reference to a video category",
+      "required": true,
+      "property_type": { "Single": { "Reference": { "className": "ContentCategory" } } }
+    },
+    {
+      "name": "title",
+      "description": "The title of the video",
+      "required": true,
+      "property_type": { "Single": { "Text": 64 } }
+    },
+    {
+      "name": "description",
+      "description": "The description of the Video",
+      "required": true,
+      "property_type": { "Single": { "Text": 1024 } }
+    },
+    {
+      "name": "duration",
+      "description": "Video duration in seconds",
+      "required": true,
+      "property_type": { "Single": "Uint32" }
+    },
+    {
+      "name": "skippableIntroDuration",
+      "description": "Video's kippable intro duration in seconds",
+      "required": false,
+      "property_type": { "Single": "Uint16" }
+    },
+    {
+      "name": "thumbnailURL",
+      "description": "Video thumbnail url (recommended ratio: 16:9)",
+      "required": true,
+      "property_type": { "Single": { "Text": 256 } }
+    },
+    {
+      "name": "language",
+      "description": "Video's main langauge",
+      "required": false,
+      "property_type": { "Single": { "Reference": { "className": "Language" } } }
+    },
+    {
+      "name": "media",
+      "description": "Reference to VideoMedia",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": { "Reference": { "className": "VideoMedia", "sameOwner": true } } },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "hasMarketing",
+      "description": "Whether or not Video contains marketing",
+      "required": false,
+      "property_type": { "Single": "Bool" }
+    },
+    {
+      "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" }
+    },
+    {
+      "name": "isPublic",
+      "description": "Whether the Video is supposed to be publically displayed",
+      "required": true,
+      "property_type": { "Single": "Bool" }
+    },
+    {
+      "name": "isExplicit",
+      "description": "Whether the Video contains explicit material.",
+      "required": true,
+      "property_type": { "Single": "Bool" },
+      "locking_policy": { "is_locked_from_controller": true }
+    },
+    {
+      "name": "license",
+      "description": "A License the Video is published under",
+      "required": true,
+      "unique": true,
+      "property_type": { "Single": { "Reference": { "className": "License", "sameOwner": true } } }
+    },
+    {
+      "name": "curationStatus",
+      "description": "Video curation status set by the Curator",
+      "required": false,
+      "unique": true,
+      "property_type": { "Single": { "Reference": { "className": "CurationStatus" } } },
+      "locking_policy": { "is_locked_from_controller": true }
+    }
+  ]
+}

+ 43 - 0
content-directory-schemas/package.json

@@ -0,0 +1,43 @@
+{
+  "name": "cd-schemas",
+  "version": "0.1.0",
+  "description": "JSON schemas, inputs and related tooling for Joystream content directory 2.0",
+  "author": "Joystream contributors",
+  "main": "lib/index.js",
+  "scripts": {
+    "build": "tsc --build tsconfig.lib.json",
+    "lint": "eslint ./ --ext .ts --ignore-path .gitignore",
+    "ts-check": "tsc --noEmit --pretty",
+    "pretty": "prettier ./ --write --ignore-path .gitignore",
+    "validate": "ts-node ./scripts/validate.ts",
+    "checks": "yarn ts-check && prettier ./ --check --ignore-path .gitignore && yarn validate && yarn lint",
+    "generate:types": "ts-node --files ./scripts/schemasToTS.ts",
+    "generate:entity-schemas": "ts-node ./scripts/inputSchemasToEntitySchemas.ts",
+    "generate:all": "yarn generate:entity-schemas && yarn generate:types",
+    "initialize:alice-as-lead": "ts-node ./scripts/devInitAliceLead.ts",
+    "initialize:content-dir": "ts-node ./scripts/initializeContentDir.ts",
+    "initialize:dev": "yarn initialize:alice-as-lead && yarn initialize:content-dir"
+  },
+  "dependencies": {
+    "ajv": "6.12.5",
+    "@joystream/prettier-config": "*",
+    "@polkadot/api": "1.26.1",
+    "@polkadot/keyring": "^3.0.1",
+    "@joystream/types": "^0.14.0",
+    "@apidevtools/json-schema-ref-parser": "^9.0.6"
+  },
+  "devDependencies": {
+    "ts-node": "^8.8.2",
+    "typescript": "^3.9.7",
+    "json-schema-to-typescript": "^9.1.1"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git+https://github.com/Joystream/joystream.git"
+  },
+  "license": "GPL-3.0-only",
+  "bugs": {
+    "url": "https://github.com/Joystream/joystream/issues"
+  },
+  "homepage": "https://github.com/Joystream/joystream"
+}

+ 154 - 0
content-directory-schemas/schemas/extrinsics/AddClassSchema.schema.json

@@ -0,0 +1,154 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://joystream.org/AddClassSchema.schema.json",
+  "title": "AddClassSchema",
+  "description": "JSON schema to describe a new schema for a certain class in Joystream network",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["className", "newProperties"],
+  "properties": {
+    "className": { "type": "string" },
+    "existingProperties": {
+      "type": "array",
+      "uniqueItems": true,
+      "items": { "$ref": "#/definitions/PropertyInSchemIndex" }
+    },
+    "newProperties": {
+      "type": "array",
+      "uniqueItems": true,
+      "items": { "$ref": "#/definitions/Property" }
+    }
+  },
+  "definitions": {
+    "ClassId": {
+      "type": "integer",
+      "minimum": 1
+    },
+    "PropertyInSchemIndex": {
+      "type": "integer",
+      "minimum": 0
+    },
+    "DefaultBoolean": {
+      "type": "boolean",
+      "default": false
+    },
+    "Property": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["name", "property_type"],
+      "properties": {
+        "property_type": {
+          "oneOf": [{ "$ref": "#/definitions/SinglePropertyVariant" }, { "$ref": "#/definitions/VecPropertyVariant" }]
+        },
+        "name": { "$ref": "#/definitions/PropertyName" },
+        "description": { "$ref": "#/definitions/PropertyDescription" },
+        "required": { "$ref": "#/definitions/DefaultBoolean" },
+        "unique": { "$ref": "#/definitions/DefaultBoolean" },
+        "locking_policy": { "$ref": "#/definitions/LockingPolicy" }
+      }
+    },
+    "PropertyName": {
+      "type": "string",
+      "minLength": 1,
+      "maxLength": 100
+    },
+    "PropertyDescription": {
+      "type": "string",
+      "minLength": 0,
+      "default": ""
+    },
+    "SinglePropertyType": {
+      "oneOf": [
+        { "$ref": "#/definitions/PrimitiveProperty", "description": "Primitive property (bool/integer)" },
+        { "$ref": "#/definitions/TextProperty" },
+        { "$ref": "#/definitions/HashProperty" },
+        { "$ref": "#/definitions/ReferenceProperty" }
+      ]
+    },
+    "SinglePropertyVariant": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["Single"],
+      "properties": {
+        "Single": { "$ref": "#/definitions/SinglePropertyType" }
+      }
+    },
+    "VecPropertyType": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["vec_type", "max_length"],
+      "properties": {
+        "vec_type": { "$ref": "#/definitions/SinglePropertyType" },
+        "max_length": { "$ref": "#/definitions/MaxVecItems" }
+      }
+    },
+    "VecPropertyVariant": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["Vector"],
+      "properties": {
+        "Vector": { "$ref": "#/definitions/VecPropertyType" }
+      }
+    },
+    "PrimitiveProperty": {
+      "type": "string",
+      "enum": ["Bool", "Uint16", "Uint32", "Uint64", "Int16", "Int32", "Int64"]
+    },
+    "TextProperty": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["Text"],
+      "properties": {
+        "Text": { "$ref": "#/definitions/MaxTextLength" }
+      }
+    },
+    "HashProperty": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["Hash"],
+      "properties": {
+        "Hash": { "$ref": "#/definitions/MaxTextLength" }
+      }
+    },
+    "MaxTextLength": {
+      "type": "integer",
+      "minimum": 1,
+      "maximum": 65535
+    },
+    "MaxVecItems": {
+      "type": "integer",
+      "minimum": 1,
+      "maximum": 65535
+    },
+    "ReferenceProperty": {
+      "type": "object",
+      "additionalProperties": false,
+      "required": ["Reference"],
+      "properties": {
+        "Reference": {
+          "type": "object",
+          "additionalProperties": false,
+          "required": ["className"],
+          "properties": {
+            "className": {
+              "type": "string",
+              "description": "Referenced class name"
+            },
+            "sameOwner": {
+              "$ref": "#/definitions/DefaultBoolean",
+              "description": "Whether same owner (controller) is required"
+            }
+          }
+        }
+      }
+    },
+    "LockingPolicy": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "is_locked_from_maintainer": { "$ref": "#/definitions/DefaultBoolean" },
+        "is_locked_from_controller": { "$ref": "#/definitions/DefaultBoolean" }
+      }
+    }
+  }
+}

+ 44 - 0
content-directory-schemas/schemas/extrinsics/CreateClass.schema.json

@@ -0,0 +1,44 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://joystream.org/CreateClass.schema.json",
+  "title": "CreateClass",
+  "description": "JSON schema to describe a new class for Joystream network",
+  "type": "object",
+  "additionalProperties": false,
+  "required": ["name", "description", "maximum_entities_count", "default_entity_creation_voucher_upper_bound"],
+  "properties": {
+    "name": {
+      "type": "string",
+      "description": "Name of this class. Required property."
+    },
+    "description": {
+      "type": "string",
+      "description": "Description of this class."
+    },
+    "class_permissions": {
+      "type": "object",
+      "additionalProperties": false,
+      "properties": {
+        "any_member": { "$ref": "#/definitions/DefaultBoolean" },
+        "entity_creation_blocked": { "$ref": "#/definitions/DefaultBoolean" },
+        "all_entity_property_values_locked": { "$ref": "#/definitions/DefaultBoolean" },
+        "maintainers": {
+          "type": "array",
+          "uniqueItems": true,
+          "items": {
+            "type": "integer"
+          },
+          "default": []
+        }
+      }
+    },
+    "maximum_entities_count": { "type": "integer" },
+    "default_entity_creation_voucher_upper_bound": { "type": "integer" }
+  },
+  "definitions": {
+    "DefaultBoolean": {
+      "type": "boolean",
+      "default": false
+    }
+  }
+}

+ 38 - 0
content-directory-schemas/schemas/propertyValidationDefs.schema.json

@@ -0,0 +1,38 @@
+{
+  "$schema": "http://json-schema.org/draft-07/schema",
+  "$id": "https://joystream.org/propertyValidationDefs.schema.json",
+  "title": "propertyValidationDefs",
+  "description": "JSON schema containing definitions for different property types input",
+  "definitions": {
+    "Bool": {
+      "type": "boolean"
+    },
+    "Uint16": {
+      "type": "integer",
+      "minimum": 0,
+      "maximum": 65535
+    },
+    "Uint32": {
+      "type": "integer",
+      "minimum": 0,
+      "maximum": 4294967295
+    },
+    "Uint64": {
+      "type": "integer",
+      "minimum": 0
+    },
+    "Int16": {
+      "type": "integer",
+      "minimum": -32768,
+      "maximum": 32767
+    },
+    "Int32": {
+      "type": "integer",
+      "minimum": -2147483648,
+      "maximum": 2147483647
+    },
+    "Int64": {
+      "type": "integer"
+    }
+  }
+}

+ 88 - 0
content-directory-schemas/scripts/devInitAliceLead.ts

@@ -0,0 +1,88 @@
+import { types } from '@joystream/types'
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { ExtrinsicsHelper, getAlicePair } from '../src/helpers/extrinsics'
+
+async function main() {
+  // Init api
+  const WS_URI = process.env.WS_URI || 'ws://127.0.0.1:9944'
+  console.log(`Initializing the api (${WS_URI})...`)
+  const provider = new WsProvider(WS_URI)
+  const api = await ApiPromise.create({ provider, types })
+
+  const ALICE = getAlicePair()
+
+  const txHelper = new ExtrinsicsHelper(api)
+
+  const sudo = (tx: SubmittableExtrinsic<'promise'>) => api.tx.sudo.sudo(tx)
+  const extrinsics: SubmittableExtrinsic<'promise'>[] = []
+
+  // Create membership if not already created
+  let aliceMemberId: number | undefined = (await api.query.members.memberIdsByControllerAccountId(ALICE.address))
+    .toArray()[0]
+    ?.toNumber()
+
+  if (aliceMemberId === undefined) {
+    console.log('Perparing Alice member account creation extrinsic...')
+    aliceMemberId = (await api.query.members.nextMemberId()).toNumber()
+    extrinsics.push(api.tx.members.buyMembership(0, 'alice', null, null))
+  } else {
+    console.log(`Alice member id found: ${aliceMemberId}...`)
+  }
+
+  // Set Alice as lead if lead not already set
+  if ((await api.query.contentDirectoryWorkingGroup.currentLead()).isNone) {
+    const newOpeningId = (await api.query.contentDirectoryWorkingGroup.nextOpeningId()).toNumber()
+    const newApplicationId = (await api.query.contentDirectoryWorkingGroup.nextApplicationId()).toNumber()
+    // Create curator lead opening
+    console.log('Perparing Create Curator Lead Opening extrinsic...')
+    extrinsics.push(
+      sudo(
+        api.tx.contentDirectoryWorkingGroup.addOpening(
+          { CurrentBlock: null }, // activate_at
+          { max_review_period_length: 9999 }, // OpeningPolicyCommitment
+          'api-examples curator opening', // human_readable_text
+          'Leader' // opening_type
+        )
+      )
+    )
+
+    // Apply to lead opening
+    console.log('Perparing Apply to Curator Lead Opening as Alice extrinsic...')
+    extrinsics.push(
+      api.tx.contentDirectoryWorkingGroup.applyOnOpening(
+        aliceMemberId, // member id
+        newOpeningId, // opening id
+        ALICE.address, // address
+        null, // opt role stake
+        null, // opt appl. stake
+        'api-examples curator opening appl.' // human_readable_text
+      )
+    )
+
+    // Begin review period
+    console.log('Perparing Begin Applicant Review extrinsic...')
+    extrinsics.push(sudo(api.tx.contentDirectoryWorkingGroup.beginApplicantReview(newOpeningId)))
+
+    // Fill opening
+    console.log('Perparing Fill Opening extrinsic...')
+    extrinsics.push(
+      sudo(
+        api.tx.contentDirectoryWorkingGroup.fillOpening(
+          newOpeningId, // opening id
+          api.createType('ApplicationIdSet', [newApplicationId]), // succesful applicants
+          null // reward policy
+        )
+      )
+    )
+
+    console.log('Sending extrinsics...')
+    await txHelper.sendAndCheck(ALICE, extrinsics, 'Failed to initialize Alice as Content Curators Lead!')
+  } else {
+    console.log('Curators lead already exists, skipping...')
+  }
+}
+
+main()
+  .then(() => process.exit())
+  .catch((e) => console.error(e))

+ 67 - 0
content-directory-schemas/scripts/initializeContentDir.ts

@@ -0,0 +1,67 @@
+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 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)
+
+async function main() {
+  // Init api
+  const WS_URI = process.env.WS_URI || 'ws://127.0.0.1:9944'
+  console.log(`Initializing the api (${WS_URI})...`)
+  const provider = new WsProvider(WS_URI)
+  const api = await ApiPromise.create({ provider, types })
+
+  const ALICE = getAlicePair()
+
+  // Emptiness check
+  if ((await api.query.contentDirectory.nextClassId()).toNumber() > 1) {
+    console.log('Content directory is not empty! Skipping...')
+    process.exit()
+  }
+
+  const txHelper = new ExtrinsicsHelper(api)
+  const parser = new InputParser(api, classInputs, schemaInputs, entityBatchInputs)
+
+  console.log(`Initializing classes (${classInputs.length} input files found)...\n`)
+  const classExtrinsics = parser.getCreateClassExntrinsics()
+  await txHelper.sendAndCheck(ALICE, classExtrinsics, 'Class initialization failed!')
+
+  console.log(`Initializing schemas (${schemaInputs.length} input files found)...\n`)
+  const schemaExtrinsics = await parser.getAddSchemaExtrinsics()
+  await txHelper.sendAndCheck(ALICE, schemaExtrinsics, 'Schemas initialization failed!')
+
+  console.log(`Initializing entities (${entityBatchInputs.length} input files found)`)
+  const entityOperations = await parser.getEntityBatchOperations()
+  // Save operations in operations.json (for reference in case of errors)
+  console.log(`Saving entity batch operations in ${ENTITY_OPERATIONS_OUTPUT_PATH}...`)
+  fs.writeFileSync(
+    ENTITY_OPERATIONS_OUTPUT_PATH,
+    JSON.stringify(
+      entityOperations.map((o) => o.toJSON()),
+      null,
+      4
+    )
+  )
+  console.log('Sending Transaction extrinsic...')
+  await txHelper.sendAndCheck(
+    ALICE,
+    [api.tx.contentDirectory.transaction({ Lead: null }, entityOperations)],
+    'Entity initialization failed!'
+  )
+}
+
+main()
+  .then(() => process.exit())
+  .catch((e) => console.error(e))

+ 150 - 0
content-directory-schemas/scripts/inputSchemasToEntitySchemas.ts

@@ -0,0 +1,150 @@
+import fs from 'fs'
+import path from 'path'
+import {
+  AddClassSchema,
+  HashProperty,
+  Property,
+  ReferenceProperty,
+  SinglePropertyVariant,
+  TextProperty,
+  VecPropertyVariant,
+} from '../types/extrinsics/AddClassSchema'
+import PRIMITIVE_PROPERTY_DEFS from '../schemas/propertyValidationDefs.schema.json'
+import { getInputs } from '../src/helpers/inputs'
+import { getSchemasLocation, SCHEMA_TYPES } from '../src/helpers/schemas'
+import { JSONSchema7 } from 'json-schema'
+
+const schemaInputs = getInputs<AddClassSchema>('schemas')
+
+const strictObjectDef = (def: Record<string, any>): JSONSchema7 => ({
+  type: 'object',
+  additionalProperties: false,
+  ...def,
+})
+
+const onePropertyObjectDef = (propertyName: string, propertyDef: Record<string, any>): JSONSchema7 =>
+  strictObjectDef({
+    required: [propertyName],
+    properties: {
+      [propertyName]: propertyDef,
+    },
+  })
+
+const TextPropertyDef = ({ Text: maxLength }: TextProperty): JSONSchema7 => ({
+  type: 'string',
+  maxLength,
+})
+
+const HashPropertyDef = ({ Hash: maxLength }: HashProperty): JSONSchema7 => ({
+  type: 'string',
+  maxLength,
+})
+
+const ReferencePropertyDef = ({ Reference: ref }: ReferenceProperty): JSONSchema7 => ({
+  'oneOf': [
+    onePropertyObjectDef('new', { '$ref': `./${ref.className}Entity.schema.json` }),
+    onePropertyObjectDef('existing', { '$ref': `../entityReferences/${ref.className}Ref.schema.json` }),
+    PRIMITIVE_PROPERTY_DEFS.definitions.Uint64 as JSONSchema7,
+  ],
+})
+
+const SinglePropertyDef = ({ Single: singlePropType }: SinglePropertyVariant): JSONSchema7 => {
+  if (typeof singlePropType === 'string') {
+    return PRIMITIVE_PROPERTY_DEFS.definitions[singlePropType] as JSONSchema7
+  } else if ((singlePropType as TextProperty).Text) {
+    return TextPropertyDef(singlePropType as TextProperty)
+  } else if ((singlePropType as HashProperty).Hash) {
+    return HashPropertyDef(singlePropType as HashProperty)
+  } else if ((singlePropType as ReferenceProperty).Reference) {
+    return ReferencePropertyDef(singlePropType as ReferenceProperty)
+  }
+
+  throw new Error(`Unknown single proprty type: ${JSON.stringify(singlePropType)}`)
+}
+
+const VecPropertyDef = ({ Vector: vec }: VecPropertyVariant): JSONSchema7 => ({
+  type: 'array',
+  maxItems: vec.max_length,
+  'items': SinglePropertyDef({ Single: vec.vec_type }),
+})
+
+const PropertyDef = ({ property_type: propertyType, description }: Property): JSONSchema7 => ({
+  ...((propertyType as SinglePropertyVariant).Single
+    ? SinglePropertyDef(propertyType as SinglePropertyVariant)
+    : VecPropertyDef(propertyType as VecPropertyVariant)),
+  description,
+})
+
+// Mkdir entity schemas directories if they do not exist
+SCHEMA_TYPES.forEach((type) => {
+  if (!fs.existsSync(getSchemasLocation(type))) {
+    fs.mkdirSync(getSchemasLocation(type))
+  }
+})
+
+// Run schema conversion:
+schemaInputs.forEach(({ fileName, data: inputData }) => {
+  const schemaName = fileName.replace('Schema.json', '')
+
+  if (inputData.newProperties && !inputData.existingProperties) {
+    const properites = inputData.newProperties
+    const propertiesObj = properites.reduce((pObj, p) => {
+      pObj[p.name] = PropertyDef(p)
+      return pObj
+    }, {} as Record<string, ReturnType<typeof PropertyDef>>)
+
+    const EntitySchema: JSONSchema7 = {
+      $schema: 'http://json-schema.org/draft-07/schema',
+      $id: `https://joystream.org/entities/${schemaName}Entity.schema.json`,
+      title: `${schemaName}Entity`,
+      description: `JSON schema for entities based on ${schemaName} runtime schema`,
+      ...strictObjectDef({
+        required: properites.filter((p) => p.required).map((p) => p.name),
+        properties: propertiesObj,
+      }),
+    }
+
+    const ReferenceSchema: JSONSchema7 = {
+      $schema: 'http://json-schema.org/draft-07/schema',
+      $id: `https://joystream.org/entityReferences/${schemaName}Ref.schema.json`,
+      title: `${schemaName}Reference`,
+      description: `JSON schema for reference to ${schemaName} entity based on runtime schema`,
+      anyOf: [
+        ...properites.filter((p) => p.required && p.unique).map((p) => onePropertyObjectDef(p.name, PropertyDef(p))),
+        PRIMITIVE_PROPERTY_DEFS.definitions.Uint64 as JSONSchema7,
+      ],
+    }
+
+    const BatchSchema: JSONSchema7 = {
+      $schema: 'http://json-schema.org/draft-07/schema',
+      $id: `https://joystream.org/entityBatches/${schemaName}Batch.schema.json`,
+      title: `${schemaName}Batch`,
+      description: `JSON schema for batch of entities based on ${schemaName} runtime schema`,
+      ...strictObjectDef({
+        required: ['className', 'entries'],
+        properties: {
+          className: { type: 'string' },
+          entries: {
+            type: 'array',
+            items: { '$ref': `../entities/${schemaName}Entity.schema.json` },
+          },
+        },
+      }),
+    }
+
+    const entitySchemaPath = path.join(getSchemasLocation('entities'), `${schemaName}Entity.schema.json`)
+    fs.writeFileSync(entitySchemaPath, JSON.stringify(EntitySchema, undefined, 4))
+    console.log(`${entitySchemaPath} succesfully generated!`)
+
+    const entityReferenceSchemaPath = path.join(getSchemasLocation('entityReferences'), `${schemaName}Ref.schema.json`)
+    fs.writeFileSync(entityReferenceSchemaPath, JSON.stringify(ReferenceSchema, undefined, 4))
+    console.log(`${entityReferenceSchemaPath} succesfully generated!`)
+
+    const batchOfEntitiesSchemaPath = path.join(getSchemasLocation('entityBatches'), `${schemaName}Batch.schema.json`)
+    fs.writeFileSync(batchOfEntitiesSchemaPath, JSON.stringify(BatchSchema, undefined, 4))
+    console.log(`${batchOfEntitiesSchemaPath} succesfully generated!`)
+  } else {
+    console.log('WARNING: Schemas with "existingProperties" not supported yet!')
+    console.log('Skipping...')
+  }
+})

+ 45 - 0
content-directory-schemas/scripts/schemasToTS.ts

@@ -0,0 +1,45 @@
+import fs from 'fs'
+import path from 'path'
+import { compileFromFile } from 'json-schema-to-typescript'
+// TODO: This will require publishing @joystream/prettier-config if we want to include it in joystream-js
+import prettierConfig from '@joystream/prettier-config'
+
+const SCHEMAS_LOCATION = path.join(__dirname, '../schemas')
+const OUTPUT_TYPES_LOCATION = path.join(__dirname, '../types')
+
+const SUBDIRS_INCLUDED = ['extrinsics', 'entities'] as const
+
+async function main() {
+  for (const subdirName of fs.readdirSync(SCHEMAS_LOCATION)) {
+    if (!SUBDIRS_INCLUDED.includes(subdirName as any)) {
+      console.log(`Subdir/filename not included: ${subdirName} - skipping...`)
+      continue
+    }
+    const schemaSubdir = subdirName as typeof SUBDIRS_INCLUDED[number]
+    for (const schemaFilename of fs.readdirSync(path.join(SCHEMAS_LOCATION, schemaSubdir))) {
+      const schemaFilePath = path.join(SCHEMAS_LOCATION, schemaSubdir, schemaFilename)
+      const outputFilename = schemaFilename.replace('.schema.json', '.d.ts')
+      const outputDir = path.join(OUTPUT_TYPES_LOCATION, schemaSubdir)
+      if (!fs.existsSync(outputDir)) {
+        fs.mkdirSync(outputDir)
+      }
+      const outputFilePath = path.join(outputDir, outputFilename)
+      try {
+        await compileFromFile(schemaFilePath, {
+          cwd: path.join(SCHEMAS_LOCATION, schemaSubdir),
+          style: prettierConfig,
+        }).then((ts) => {
+          fs.writeFileSync(outputFilePath, ts)
+          console.log(`${outputFilePath} succesfully generated!`)
+        })
+      } catch (e) {
+        console.log(`${outputFilePath} compilation FAILED!`)
+        console.error(e)
+      }
+    }
+  }
+}
+
+main()
+  .then(() => process.exit())
+  .catch((e) => console.error(e))

+ 85 - 0
content-directory-schemas/scripts/validate.ts

@@ -0,0 +1,85 @@
+// TODO: Add entity batches validation
+import Ajv from 'ajv'
+import { FetchedInput, getInputs, InputType, INPUT_TYPES } from '../src/helpers/inputs'
+import path from 'path'
+import fs from 'fs'
+import $RefParser from '@apidevtools/json-schema-ref-parser'
+
+const SCHEMAS_LOCATION = path.join(__dirname, '../schemas')
+
+const ajv = new Ajv({ allErrors: true })
+
+const validateJsonSchema = (jsonSchemaShortPath: string, jsonSchema: Record<string, unknown>) => {
+  if (!ajv.validateSchema(jsonSchema)) {
+    console.log(`\nERROR! ${jsonSchemaShortPath} - schema validation failed!`)
+    console.log(ajv.errorsText(undefined, { separator: '\n' }))
+    console.log('\n')
+    process.exitCode = 100
+
+    return false
+  }
+
+  return true
+}
+
+const validateInputAgainstSchema = (input: FetchedInput, jsonSchema: Record<string, unknown>) => {
+  if (!ajv.validate(jsonSchema, input.data)) {
+    console.log(`\nERROR! ${input.fileName} - validation failed!`)
+    console.log(ajv.errorsText(undefined, { separator: '\n' }))
+    console.log('\n')
+    process.exitCode = 100
+
+    return false
+  }
+
+  return true
+}
+
+const getJsonSchemaForInput = (inputType: InputType, input: FetchedInput) => {
+  let schemaLocation = ''
+  if (inputType === 'classes') {
+    schemaLocation = path.join(SCHEMAS_LOCATION, 'extrinsics', 'CreateClass.schema.json')
+  }
+  if (inputType === 'schemas') {
+    schemaLocation = path.join(SCHEMAS_LOCATION, 'extrinsics', 'AddClassSchema.schema.json')
+  }
+  if (inputType === 'entityBatches') {
+    const jsonSchemaFilename = input.fileName.replace('.json', '.schema.json')
+    schemaLocation = path.join(SCHEMAS_LOCATION, 'entityBatches', jsonSchemaFilename)
+  }
+
+  return {
+    jsonSchemaPath: schemaLocation,
+    jsonSchema: JSON.parse(fs.readFileSync(schemaLocation).toString()),
+  }
+}
+
+async function main() {
+  const alreadyValidatedJsonSchemas = new Map<string, boolean>()
+  for (const inputType of INPUT_TYPES) {
+    console.log(`Validating inputs/${inputType} and related json-schemas...\n`)
+    for (const input of getInputs(inputType)) {
+      let { jsonSchemaPath, jsonSchema } = getJsonSchemaForInput(inputType, input)
+      jsonSchema = await $RefParser.dereference(jsonSchemaPath, jsonSchema)
+      const jsonSchemaShortPath = path.relative(path.join(SCHEMAS_LOCATION, '..'), jsonSchemaPath)
+      // Validate the schema itself
+      let isJsonSchemaValid = alreadyValidatedJsonSchemas.get(jsonSchemaShortPath)
+      if (isJsonSchemaValid === undefined) {
+        console.log(`Validating ${jsonSchemaShortPath}...`)
+        isJsonSchemaValid = validateJsonSchema(jsonSchemaShortPath, jsonSchema)
+        alreadyValidatedJsonSchemas.set(jsonSchemaShortPath, isJsonSchemaValid)
+      }
+      if (!isJsonSchemaValid) {
+        return
+      }
+      console.log(`Validating inputs/${inputType}/${input.fileName}...`)
+      validateInputAgainstSchema(input, jsonSchema)
+    }
+
+    console.log('\n\n')
+  }
+}
+
+main()
+  .then(() => process.exit())
+  .catch((e) => console.error(e))

+ 270 - 0
content-directory-schemas/src/helpers/InputParser.ts

@@ -0,0 +1,270 @@
+import { AddClassSchema, Property } from '../../types/extrinsics/AddClassSchema'
+import { createType } from '@joystream/types'
+import { blake2AsHex } from '@polkadot/util-crypto'
+import {
+  ClassId,
+  OperationType,
+  ParametrizedPropertyValue,
+  PropertyId,
+  PropertyType,
+} from '@joystream/types/content-directory'
+import { isSingle, isReference } from './propertyType'
+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'
+
+export class InputParser {
+  private api: ApiPromise
+  private classInputs: CreateClass[]
+  private schemaInputs: AddClassSchema[]
+  private batchInputs: EntityBatch[]
+  private createEntityOperations: OperationType[] = []
+  private addSchemaToEntityOprations: OperationType[] = []
+  private entityIndexByUniqueQueryMap = new Map<string, number>()
+  private entityByUniqueQueryCurrentIndex = 0
+  private classIdByNameMap = new Map<string, number>()
+  private classMapInitialized = false
+
+  static createWithKnownSchemas(api: ApiPromise, entityBatches?: EntityBatch[]) {
+    return new InputParser(
+      api,
+      [],
+      getInputs('schemas').map(({ data }) => data),
+      entityBatches
+    )
+  }
+
+  constructor(
+    api: ApiPromise,
+    classInputs?: CreateClass[],
+    schemaInputs?: AddClassSchema[],
+    batchInputs?: EntityBatch[]
+  ) {
+    this.api = api
+    this.classInputs = classInputs || []
+    this.schemaInputs = schemaInputs || []
+    this.batchInputs = batchInputs || []
+  }
+
+  private async initializeClassMap() {
+    if (this.classMapInitialized) {
+      return
+    }
+    const classEntries = await this.api.query.contentDirectory.classById.entries()
+    classEntries.forEach(([key, aClass]) => {
+      this.classIdByNameMap.set(aClass.name.toString(), (key.args[0] as ClassId).toNumber())
+    })
+    this.classMapInitialized = true
+  }
+
+  private schemaByClassName(className: string) {
+    const foundSchema = this.schemaInputs.find((data) => data.className === className)
+    if (!foundSchema) {
+      throw new Error(`Schema not found by class name: ${className}`)
+    }
+
+    return foundSchema
+  }
+
+  private getUniqueQueryHash(uniquePropVal: Record<string, any>, className: string) {
+    return blake2AsHex(JSON.stringify([className, uniquePropVal]))
+  }
+
+  private findEntityIndexByUniqueQuery(uniquePropVal: Record<string, any>, className: string) {
+    const hash = this.getUniqueQueryHash(uniquePropVal, className)
+    const foundIndex = this.entityIndexByUniqueQueryMap.get(hash)
+    if (foundIndex === undefined) {
+      throw new Error(
+        `findEntityIndexByUniqueQuery failed for class ${className} and query: ${JSON.stringify(uniquePropVal)}`
+      )
+    }
+
+    return foundIndex
+  }
+
+  private getClassIdByName(className: string): number {
+    const classId = this.classIdByNameMap.get(className)
+    if (classId === undefined) {
+      throw new Error(`Could not find class id by name: "${className}"!`)
+    }
+    return classId
+  }
+
+  private parsePropertyType(propertyType: Property['property_type']): PropertyType {
+    if (isSingle(propertyType) && isReference(propertyType.Single)) {
+      const { className, sameOwner } = propertyType.Single.Reference
+      const classId = this.getClassIdByName(className)
+      return createType('PropertyType', { Single: { Reference: [classId, sameOwner] } })
+    }
+    // Types other than reference are fully compatible
+    return createType('PropertyType', propertyType)
+  }
+
+  private includeEntityInputInUniqueQueryMap(entityInput: Record<string, any>, schema: AddClassSchema) {
+    Object.entries(entityInput)
+      .filter(([, pValue]) => pValue !== undefined)
+      .forEach(([propertyName, propertyValue]) => {
+        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')) {
+            const refEntitySchema = this.schemaByClassName(schemaPropertyType.Single.Reference.className)
+            this.includeEntityInputInUniqueQueryMap(propertyValue.new, refEntitySchema)
+          }
+        }
+      })
+    // Add entries to entityIndexByUniqueQueryMap
+    schema.newProperties
+      .filter((p) => p.unique)
+      .forEach(({ name }) => {
+        if (entityInput[name] === undefined) {
+          // Skip empty values (not all unique properties are required)
+          return
+        }
+        const hash = this.getUniqueQueryHash({ [name]: entityInput[name] }, schema.className)
+        this.entityIndexByUniqueQueryMap.set(hash, this.entityByUniqueQueryCurrentIndex)
+      })
+    ++this.entityByUniqueQueryCurrentIndex
+  }
+
+  private createParametrizedPropertyValues(
+    entityInput: Record<string, any>,
+    schema: AddClassSchema,
+    customHandler?: (property: Property, value: any) => ParametrizedPropertyValue | undefined
+  ) {
+    return Object.entries(entityInput)
+      .filter(([, pValue]) => pValue !== undefined)
+      .map(([propertyName, propertyValue]) => {
+        const schemaPropertyIndex = schema.newProperties.findIndex((p) => p.name === propertyName)
+        const schemaProperty = schema.newProperties[schemaPropertyIndex]
+
+        let value = customHandler && customHandler(schemaProperty, propertyValue)
+        if (value === undefined) {
+          value = createType('ParametrizedPropertyValue', {
+            InputPropertyValue: this.parsePropertyType(schemaProperty.property_type)
+              .toInputPropertyValue(propertyValue)
+              .toJSON(),
+          })
+        }
+
+        return {
+          in_class_index: schemaPropertyIndex,
+          value: value.toJSON(),
+        }
+      })
+  }
+
+  private parseEntityInput(entityInput: Record<string, any>, schema: AddClassSchema) {
+    const parametrizedPropertyValues = this.createParametrizedPropertyValues(entityInput, schema, (property, value) => {
+      // Custom handler for references
+      const { property_type: propertyType } = property
+      if (isSingle(propertyType) && isReference(propertyType.Single)) {
+        const refEntitySchema = this.schemaByClassName(propertyType.Single.Reference.className)
+        if (Object.keys(value).includes('new')) {
+          const entityIndex = this.parseEntityInput(value.new, refEntitySchema)
+          return createType('ParametrizedPropertyValue', { InternalEntityJustAdded: entityIndex })
+        } else if (Object.keys(value).includes('existing')) {
+          const entityIndex = this.findEntityIndexByUniqueQuery(value.existing, refEntitySchema.className)
+          return createType('ParametrizedPropertyValue', { InternalEntityJustAdded: entityIndex })
+        }
+      }
+      return undefined
+    })
+
+    // Add operations
+    const createEntityOperationIndex = this.createEntityOperations.length
+    const classId = this.classIdByNameMap.get(schema.className)
+    this.createEntityOperations.push(createType('OperationType', { CreateEntity: { class_id: classId } }))
+    this.addSchemaToEntityOprations.push(
+      createType('OperationType', {
+        AddSchemaSupportToEntity: {
+          schema_id: 0,
+          entity_id: { InternalEntityJustAdded: createEntityOperationIndex },
+          parametrized_property_values: parametrizedPropertyValues,
+        },
+      })
+    )
+
+    // Return CreateEntity operation index
+    return createEntityOperationIndex
+  }
+
+  private reset() {
+    this.entityIndexByUniqueQueryMap = new Map<string, number>()
+    this.classIdByNameMap = new Map<string, number>()
+    this.createEntityOperations = []
+    this.addSchemaToEntityOprations = []
+    this.entityByUniqueQueryCurrentIndex = 0
+  }
+
+  public async getEntityBatchOperations() {
+    await this.initializeClassMap()
+    // First - create entityUniqueQueryMap to allow referencing any entity at any point
+    this.batchInputs.forEach((batch) => {
+      const entitySchema = this.schemaByClassName(batch.className)
+      batch.entries.forEach((entityInput) => this.includeEntityInputInUniqueQueryMap(entityInput, entitySchema))
+    })
+    // Then - parse into actual operations
+    this.batchInputs.forEach((batch) => {
+      const entitySchema = this.schemaByClassName(batch.className)
+      batch.entries.forEach((entityInput) => this.parseEntityInput(entityInput, entitySchema))
+    })
+
+    const operations = [...this.createEntityOperations, ...this.addSchemaToEntityOprations]
+    this.reset()
+
+    return operations
+  }
+
+  public async createEntityUpdateOperation(
+    entityInput: Record<string, any>,
+    className: string,
+    entityId: number
+  ): Promise<OperationType> {
+    await this.initializeClassMap()
+    const schema = this.schemaByClassName(className)
+    const parametrizedPropertyValues = this.createParametrizedPropertyValues(entityInput, schema)
+
+    return createType('OperationType', {
+      UpdatePropertyValues: {
+        entity_id: { ExistingEntity: entityId },
+        new_parametrized_property_values: parametrizedPropertyValues,
+      },
+    })
+  }
+
+  public async parseAddClassSchemaExtrinsic(inputData: AddClassSchema) {
+    await this.initializeClassMap() // Initialize if not yet initialized
+    const classId = this.getClassIdByName(inputData.className)
+    const newProperties = inputData.newProperties.map((p) => ({
+      ...p,
+      // Parse different format for Reference (and potentially other propTypes in the future)
+      property_type: this.parsePropertyType(p.property_type).toJSON(),
+    }))
+    return this.api.tx.contentDirectory.addClassSchema(
+      classId,
+      new (JoyBTreeSet(PropertyId))(this.api.registry, inputData.existingProperties),
+      newProperties
+    )
+  }
+
+  public parseCreateClassExtrinsic(inputData: CreateClass) {
+    return this.api.tx.contentDirectory.createClass(
+      inputData.name,
+      inputData.description,
+      inputData.class_permissions || {},
+      inputData.maximum_entities_count,
+      inputData.default_entity_creation_voucher_upper_bound
+    )
+  }
+
+  public async getAddSchemaExtrinsics() {
+    return await Promise.all(this.schemaInputs.map((data) => this.parseAddClassSchemaExtrinsic(data)))
+  }
+
+  public getCreateClassExntrinsics() {
+    return this.classInputs.map((data) => this.parseCreateClassExtrinsic(data))
+  }
+}

+ 55 - 0
content-directory-schemas/src/helpers/extrinsics.ts

@@ -0,0 +1,55 @@
+import { Keyring } from '@polkadot/keyring'
+import { KeyringPair } from '@polkadot/keyring/types'
+import { ApiPromise } from '@polkadot/api'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+
+export function getAlicePair() {
+  const keyring = new Keyring({ type: 'sr25519' })
+  keyring.addFromUri('//Alice', { name: 'Alice' })
+  const ALICE = keyring.getPairs()[0]
+
+  return ALICE
+}
+
+export class ExtrinsicsHelper {
+  api: ApiPromise
+  noncesByAddress: Map<string, number>
+
+  constructor(api: ApiPromise, initialNonces?: [string, number][]) {
+    this.api = api
+    this.noncesByAddress = new Map<string, number>(initialNonces)
+  }
+
+  private async nextNonce(address: string): Promise<number> {
+    const nonce = this.noncesByAddress.get(address) || (await this.api.query.system.account(address)).nonce.toNumber()
+    this.noncesByAddress.set(address, nonce + 1)
+
+    return nonce
+  }
+
+  async sendAndCheck(sender: KeyringPair, extrinsics: SubmittableExtrinsic<'promise'>[], errorMessage: string) {
+    const promises: Promise<void>[] = []
+    for (const tx of extrinsics) {
+      const nonce = await this.nextNonce(sender.address)
+      promises.push(
+        new Promise((resolve, reject) => {
+          tx.signAndSend(sender, { nonce }, (result) => {
+            if (result.isError) {
+              reject(new Error(errorMessage))
+            }
+            if (result.status.isInBlock) {
+              if (
+                result.events.some(({ event }) => event.section === 'system' && event.method === 'ExtrinsicSuccess')
+              ) {
+                resolve()
+              } else {
+                reject(new Error(errorMessage))
+              }
+            }
+          })
+        })
+      )
+    }
+    await Promise.all(promises)
+  }
+}

+ 20 - 0
content-directory-schemas/src/helpers/inputs.ts

@@ -0,0 +1,20 @@
+import path from 'path'
+import fs from 'fs'
+
+export const INPUTS_LOCATION = path.join(__dirname, '../../inputs')
+export const INPUT_TYPES = ['classes', 'schemas', 'entityBatches'] as const
+
+export type InputType = typeof INPUT_TYPES[number]
+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 {
+      fileName,
+      data: JSON.parse(inputJson) as Schema,
+    }
+  })
+}

+ 16 - 0
content-directory-schemas/src/helpers/propertyType.ts

@@ -0,0 +1,16 @@
+import {
+  Property,
+  ReferenceProperty,
+  SinglePropertyType,
+  SinglePropertyVariant,
+} from '../../types/extrinsics/AddClassSchema'
+
+type PropertyType = Property['property_type']
+
+export function isSingle(propertyType: PropertyType): propertyType is SinglePropertyVariant {
+  return (propertyType as SinglePropertyVariant).Single !== undefined
+}
+
+export function isReference(propertySubtype: SinglePropertyType): propertySubtype is ReferenceProperty {
+  return (propertySubtype as ReferenceProperty).Reference !== undefined
+}

+ 8 - 0
content-directory-schemas/src/helpers/schemas.ts

@@ -0,0 +1,8 @@
+import path from 'path'
+
+export const SCHEMAS_LOCATION = path.join(__dirname, '../../schemas')
+export const SCHEMA_TYPES = ['entities', 'entityBatches', 'entityReferences', 'extrinsics'] as const
+
+export type SchemaType = typeof SCHEMA_TYPES[number]
+
+export const getSchemasLocation = (schemaType: SchemaType) => path.join(SCHEMAS_LOCATION, schemaType)

+ 5 - 0
content-directory-schemas/src/index.ts

@@ -0,0 +1,5 @@
+export { ExtrinsicsHelper, getAlicePair } from './helpers/extrinsics'
+export { InputParser } from './helpers/InputParser'
+export { getInputs, getInputsLocation } from './helpers/inputs'
+export { isReference, isSingle } from './helpers/propertyType'
+export { getSchemasLocation } from './helpers/schemas'

+ 27 - 0
content-directory-schemas/tsconfig.json

@@ -0,0 +1,27 @@
+{
+  "compilerOptions": {
+    "target": "ES2017",
+    "module": "commonjs",
+    "strict": true,
+    "noImplicitAny": true,
+    "noImplicitReturns": true,
+    "moduleResolution": "node",
+    "allowSyntheticDefaultImports": true,     /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
+    "esModuleInterop": true,                  /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
+    "declaration": true,
+    "resolveJsonModule": true,
+    "types" : [
+      "node"
+    ],
+    "forceConsistentCasingInFileNames": true,
+    "baseUrl": ".",
+    "typeRoots": [
+      "typings"
+    ],
+    "paths": {
+      "@polkadot/types/augment": ["../types/augment-codec/augment-types.ts"],
+      "@polkadot/api/augment": ["../types/augment-codec/augment-api.ts"]
+    }
+  },
+  "include": [ "src/**/*", "scripts/**/*", "typings/**/*" ]
+}

+ 7 - 0
content-directory-schemas/tsconfig.lib.json

@@ -0,0 +1,7 @@
+{
+  "extends": "./tsconfig.json",
+  "include": ["src/**/*"],
+  "compilerOptions": {
+    "outDir": "lib"
+  }
+}

+ 4 - 0
content-directory-schemas/types/EntityBatch.d.ts

@@ -0,0 +1,4 @@
+export interface EntityBatch {
+  className: string
+  entries: Record<string, any>[]
+}

+ 67 - 0
content-directory-schemas/types/extrinsics/AddClassSchema.d.ts

@@ -0,0 +1,67 @@
+/* tslint:disable */
+/**
+ * This file was automatically generated by json-schema-to-typescript.
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+ * and run json-schema-to-typescript to regenerate this file.
+ */
+
+export type PropertyInSchemIndex = number
+export type SinglePropertyType =
+  | ('Bool' | 'Uint16' | 'Uint32' | 'Uint64' | 'Int16' | 'Int32' | 'Int64')
+  | TextProperty
+  | HashProperty
+  | ReferenceProperty
+export type MaxTextLength = number
+export type MaxVecItems = number
+export type PropertyName = string
+export type PropertyDescription = string
+export type DefaultBoolean = boolean
+
+/**
+ * JSON schema to describe a new schema for a certain class in Joystream network
+ */
+export interface AddClassSchema {
+  className: string
+  existingProperties?: PropertyInSchemIndex[]
+  newProperties: Property[]
+}
+export interface Property {
+  property_type: SinglePropertyVariant | VecPropertyVariant
+  name: PropertyName
+  description?: PropertyDescription
+  required?: DefaultBoolean
+  unique?: DefaultBoolean
+  locking_policy?: LockingPolicy
+}
+export interface SinglePropertyVariant {
+  Single: SinglePropertyType
+}
+export interface TextProperty {
+  Text: MaxTextLength
+}
+export interface HashProperty {
+  Hash: MaxTextLength
+}
+export interface ReferenceProperty {
+  Reference: {
+    /**
+     * Referenced class name
+     */
+    className: string
+    /**
+     * Whether same owner (controller) is required
+     */
+    sameOwner?: boolean
+  }
+}
+export interface VecPropertyVariant {
+  Vector: VecPropertyType
+}
+export interface VecPropertyType {
+  vec_type: SinglePropertyType
+  max_length: MaxVecItems
+}
+export interface LockingPolicy {
+  is_locked_from_maintainer?: DefaultBoolean
+  is_locked_from_controller?: DefaultBoolean
+}

+ 30 - 0
content-directory-schemas/types/extrinsics/CreateClass.d.ts

@@ -0,0 +1,30 @@
+/* tslint:disable */
+/**
+ * This file was automatically generated by json-schema-to-typescript.
+ * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
+ * and run json-schema-to-typescript to regenerate this file.
+ */
+
+export type DefaultBoolean = boolean
+
+/**
+ * JSON schema to describe a new class for Joystream network
+ */
+export interface CreateClass {
+  /**
+   * Name of this class. Required property.
+   */
+  name: string
+  /**
+   * Description of this class.
+   */
+  description: string
+  class_permissions?: {
+    any_member?: DefaultBoolean
+    entity_creation_blocked?: DefaultBoolean
+    all_entity_property_values_locked?: DefaultBoolean
+    maintainers?: number[]
+  }
+  maximum_entities_count: number
+  default_entity_creation_voucher_upper_bound: number
+}

+ 1 - 0
content-directory-schemas/types/utility.d.ts

@@ -0,0 +1 @@
+export type FlattenRelations<T> = { [K in keyof T]: Exclude<T[K], { new: any } | { existing: any }> }

+ 2 - 0
content-directory-schemas/typings/@joystream/prettier-config/index.d.ts

@@ -0,0 +1,2 @@
+// Prevent declaration file not found error
+declare module '@joystream/prettier-config'

+ 60 - 0
content-directory-schemas/vscode-recommended.settings.json

@@ -0,0 +1,60 @@
+{
+  "json.schemas": [
+    {
+      "fileMatch": ["*Schema.json"],
+      "url": "/content-directory-schemas/schemas/extrinsics/AddClassSchema.schema.json"
+    },
+    {
+      "fileMatch": ["*Class.json"],
+      "url": "/content-directory-schemas/schemas/extrinsics/CreateClass.schema.json"
+    },
+    {
+      "fileMatch": ["*/LanguageBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/LanguageBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/MediaLocationBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/MediaLocationBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/HttpMediaLocationBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/HttpMediaLocationBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/JoystreamMediaLocationBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/JoystreamMediaLocationBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/ContentCategoryBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/ContentCategoryBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/ChannelBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/ChannelBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/VideoMediaEncodingBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/VideoMediaEncodingBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/KnownLicenseBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/KnownLicenseBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/UserDefinedLicenseBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/UserDefinedLicenseBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/LicenseBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/LicenseBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/VideoMediaBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/VideoMediaBatch.schema.json"
+    },
+    {
+      "fileMatch": ["*/VideoBatch.json"],
+      "url": "/content-directory-schemas/schemas/entityBatches/VideoBatch.schema.json"
+    }
+  ]
+}

+ 3 - 2
package.json

@@ -4,7 +4,7 @@
   "version": "1.0.0",
   "license": "GPL-3.0-only",
   "scripts": {
-    "postinstall": "yarn workspace @joystream/types build",
+    "postinstall": "yarn workspace @joystream/types build && yarn workspace cd-schemas generate:all && yarn workspace cd-schemas build",
     "cargo-checks": "devops/git-hooks/pre-commit && devops/git-hooks/pre-push",
     "cargo-build": "scripts/cargo-build.sh"
   },
@@ -18,7 +18,8 @@
     "devops/prettier-config",
     "pioneer",
     "pioneer/packages/*",
-    "utils/api-examples"
+    "utils/api-examples",
+    "content-directory-schemas"
   ],
   "resolutions": {
     "@polkadot/api": "1.26.1",

+ 16 - 1
types/src/content-directory/index.ts

@@ -44,7 +44,22 @@ export class PropertyTypeVector extends JoyStructDecorated({
 export class PropertyType extends JoyEnum({
   Single: PropertyTypeSingle,
   Vector: PropertyTypeVector,
-}) {}
+}) {
+  toInputPropertyValue(value: any): InputPropertyValue {
+    const inputPwType: keyof typeof InputPropertyValue['typeDefinitions'] = this.type
+    const subtype = this.isOfType('Single') ? this.asType('Single').type : this.asType('Vector').vec_type.type
+
+    if (inputPwType === 'Single') {
+      const inputPwSubtype: keyof typeof InputValue['typeDefinitions'] = subtype === 'Hash' ? 'TextToHash' : subtype
+
+      return new InputPropertyValue(this.registry, { [inputPwType]: { [inputPwSubtype]: value } })
+    } else {
+      const inputPwSubtype: keyof typeof VecInputValue['typeDefinitions'] = subtype === 'Hash' ? 'TextToHash' : subtype
+
+      return new InputPropertyValue(this.registry, { [inputPwType]: { [inputPwSubtype]: value } })
+    }
+  }
+}
 
 export class PropertyLockingPolicy extends JoyStructDecorated({
   is_locked_from_maintainer: bool,

+ 0 - 188
utils/api-examples/scripts/init-new-content-directory.js

@@ -1,188 +0,0 @@
-/* global api, hashing, keyring, types, util, joy */
-
-// run this script with:
-// yarn script initNewContentDir
-//
-// or copy and paste the code into the pioneer javascript toolbox at:
-// https://testnet.joystream.org/#/js
-
-const script = async ({ api, keyring }) => {
-  // Get sudo addr
-  const sudoAddress = (await api.query.sudo.key()).toString()
-  let sudo
-  if (typeof window === 'undefined') {
-    // In node, get the keyPair if the keyring was provided
-    sudo = keyring.getPair(sudoAddress)
-  } else {
-    // Pioneer: let the UI Signer handle it
-    sudo = sudoAddress
-  }
-
-  let nonce = (await api.query.system.account(sudoAddress)).nonce.toNumber()
-
-  const NEW_OPENING_ID = await api.query.contentDirectoryWorkingGroup.nextOpeningId()
-  const NEW_CLASS_ID = await api.query.contentDirectory.nextClassId()
-  const ALICE_MEMBER_ID = 0 // We assume it exists
-
-  const sudoCall = (tx) => api.tx.sudo.sudo(tx).signAndSend(sudo, { nonce: nonce++ })
-
-  // Create curator lead opening
-  await sudoCall(
-    api.tx.contentDirectoryWorkingGroup.addOpening(
-      { CurrentBlock: null }, // activate_at
-      { commitment: { max_review_period_length: 9999 } }, // OpeningPolicyCommitment
-      'api-examples curator opening', // human_readable_text
-      { Leader: null } // opening_type
-    )
-  )
-
-  // Apply to lead opening
-  await api.tx.contentDirectoryWorkingGroup
-    .applyOnOpening(
-      ALICE_MEMBER_ID, // member id
-      NEW_OPENING_ID, // opening id
-      sudoAddress, // address
-      null, // opt role stake
-      null, // opt appl. stake
-      'api-examples curator opening appl.' // human_readable_text
-    )
-    .signAndSend(sudo, { nonce: nonce++ })
-
-  // Begin review period
-  await sudoCall(api.tx.contentDirectoryWorkingGroup.beginApplicantReview(NEW_OPENING_ID))
-
-  // Fill opening
-  await sudoCall(
-    api.tx.contentDirectoryWorkingGroup.fillOpening(
-      NEW_OPENING_ID, // opening id
-      [ALICE_MEMBER_ID], // succesful applicants
-      null // reward policy
-    )
-  )
-
-  // Create person class
-  await api.tx.contentDirectory
-    .createClass(
-      'Person',
-      'A class describing a person',
-      // ClassPermissions
-      {
-        any_member: true,
-        entity_creation_blocked: false,
-        all_entity_property_values_locked: false,
-        maintainers: [],
-      },
-      10, // maximum_entities_count
-      5 // default_entity_creation_voucher_upper_bound
-    )
-    .signAndSend(sudo, { nonce: nonce++ })
-
-  // Add schema to person class
-  await api.tx.contentDirectory
-    .addClassSchema(
-      NEW_CLASS_ID,
-      [], // existing_properties
-      // new_properties:
-      [
-        {
-          property_type: { Single: { Text: 64 } },
-          required: true,
-          unique: false,
-          name: 'firstname',
-          description: "Person's first name",
-          locking_policy: { is_locked_from_maintainer: false, is_locked_from_controller: false },
-        },
-        {
-          property_type: { Single: { Text: 64 } },
-          required: true,
-          unique: false,
-          name: 'lastname',
-          description: "Person's last name",
-          locking_policy: { is_locked_from_maintainer: false, is_locked_from_controller: false },
-        },
-        {
-          property_type: { Single: { Uint16: null } },
-          required: true,
-          unique: false,
-          name: 'age',
-          description: "Person's age",
-          locking_policy: { is_locked_from_maintainer: false, is_locked_from_controller: false },
-        },
-        {
-          property_type: { Vector: { vec_type: { Text: 32 }, max_length: 10 } },
-          required: false,
-          unique: false,
-          name: 'hobbys',
-          description: "Person's hobbys",
-          locking_policy: { is_locked_from_maintainer: false, is_locked_from_controller: false },
-        },
-      ]
-    )
-    .signAndSend(sudo, { nonce: nonce++ })
-
-  // Add another schema to person class
-  await api.tx.contentDirectory
-    .addClassSchema(
-      NEW_CLASS_ID,
-      [0, 1, 2, 3], // This still has to be in the right order (BTreeSet is part of the extrinsic metadata)
-      // new_properties:
-      [
-        {
-          property_type: { Single: { Text: 64 } },
-          required: true,
-          unique: true,
-          name: 'uniqueIdentifier',
-          description: "Person's unique identifier",
-          locking_policy: { is_locked_from_maintainer: false, is_locked_from_controller: false },
-        },
-      ]
-    )
-    .signAndSend(sudo, { nonce: nonce++ })
-
-  // Create person entity via "transaction" extrinsic
-  await api.tx.contentDirectory
-    .transaction(
-      { Member: ALICE_MEMBER_ID }, // actor
-      // operations:
-      [
-        { CreateEntity: { class_id: NEW_CLASS_ID } },
-        {
-          AddSchemaSupportToEntity: {
-            entity_id: { InternalEntityJustAdded: 0 },
-            schema_id: 1,
-            parametrized_property_values: [
-              {
-                in_class_index: 0,
-                value: { InputPropertyValue: { Single: { Text: 'John' } } },
-              },
-              {
-                in_class_index: 1,
-                value: { InputPropertyValue: { Single: { Text: 'Doe' } } },
-              },
-              {
-                in_class_index: 2,
-                value: { InputPropertyValue: { Single: { Uint16: 20 } } },
-              },
-              {
-                in_class_index: 3,
-                value: { InputPropertyValue: { Vector: { Text: ['blockchain', 'cryptocurrencies'] } } },
-              },
-              {
-                in_class_index: 4,
-                value: { InputPropertyValue: { Single: { Text: 'john_doe_unique_identifier' } } },
-              },
-            ],
-          },
-        },
-      ]
-    )
-    .signAndSend(sudo, { nonce: nonce++ })
-}
-
-if (typeof module === 'undefined') {
-  // Pioneer js-toolbox
-  script({ api, hashing, keyring, types, util, joy })
-} else {
-  // Node
-  module.exports = script
-}

+ 11 - 1
yarn.lock

@@ -14,7 +14,7 @@
   dependencies:
     "@open-web3/orml-type-definitions" "^0.5.1"
 
-"@apidevtools/json-schema-ref-parser@9.0.6":
+"@apidevtools/json-schema-ref-parser@9.0.6", "@apidevtools/json-schema-ref-parser@^9.0.6":
   version "9.0.6"
   resolved "https://registry.yarnpkg.com/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.0.6.tgz#5d9000a3ac1fd25404da886da6b266adcd99cf1c"
   integrity sha512-M3YgsLjI0lZxvrpeGVk9Ap032W6TPQkH6pRAZz81Ac3WUNF79VQooAFnp8umjvVzUmD93NkogxEwbSce7qMsUg==
@@ -5688,6 +5688,16 @@ ajv-keywords@^3.1.0, ajv-keywords@^3.4.1:
   resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d"
   integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==
 
+ajv@6.12.5:
+  version "6.12.5"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.5.tgz#19b0e8bae8f476e5ba666300387775fb1a00a4da"
+  integrity sha512-lRF8RORchjpKG50/WFf8xmg7sgCLFiYNNnqdKflk63whMQcWR5ngGjiSXkL9bjxy6B2npOK2HSMN49jEBMSkag==
+  dependencies:
+    fast-deep-equal "^3.1.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
 ajv@^6.1.0, ajv@^6.12.0, ajv@^6.12.2, ajv@^6.12.3:
   version "6.12.3"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.3.tgz#18c5af38a111ddeb4f2697bd78d68abc1cabd706"