Browse Source

InputParser - helper for addSchemaSupportToEntity and updateEntityPropertyValues (+ example)

Leszek Wiesner 4 years ago
parent
commit
2b9affa320

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

@@ -3,7 +3,7 @@ import { types as joyTypes } from '@joystream/types'
 import { Keyring } from '@polkadot/keyring'
 // Import input parser and channel entity from cd-schemas (we use it as library here)
 import { InputParser } from 'cd-schemas'
-import { ChannelEntity } from 'cd-schemas/types/entities/ChannelEntity'
+import { ChannelEntity } from 'cd-schemas/types/entities'
 
 async function main() {
   // Initialize the api

+ 68 - 0
content-directory-schemas/examples/createChannelWithoutTransaction.ts

@@ -0,0 +1,68 @@
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { types as joyTypes } from '@joystream/types'
+import { Keyring } from '@polkadot/keyring'
+// Import input parser and channel entity from cd-schemas (we use it as library here)
+import { InputParser } from 'cd-schemas'
+import { ChannelEntity } from 'cd-schemas/types/entities'
+import { FlattenRelations } from 'cd-schemas/types/utility'
+import { EntityId } from '@joystream/types/content-directory'
+
+// Alternative way of creating a channel using separate extrinsics (instead of contentDirectory.transaction)
+async function main() {
+  // Initialize the api
+  const provider = new WsProvider('ws://127.0.0.1:9944')
+  const api = await ApiPromise.create({ provider, types: joyTypes })
+
+  // Get Alice keypair
+  const keyring = new Keyring()
+  keyring.addFromUri('//Alice', undefined, 'sr25519')
+  const [ALICE] = keyring.getPairs()
+
+  const parser = InputParser.createWithKnownSchemas(api)
+
+  // In this case we need to fetch some data first (like classId and language entity id)
+  const classId = await parser.getClassIdByName('Channel')
+  const languageEntityId = await parser.findEntityIdByUniqueQuery({ code: 'EN' }, 'Language')
+
+  // We use FlattenRelations to exlude { new } and { existing } (which are not allowed if we want to parse only a single entity)
+  const channel: FlattenRelations<ChannelEntity> = {
+    title: 'Example channel 2',
+    description: 'This is an example channel',
+    language: languageEntityId,
+    coverPhotoUrl: '',
+    avatarPhotoURL: '',
+    isPublic: true,
+  }
+
+  // In this case we use some basic callback to retrieve entityId from the extrinsc event
+  const entityId = await new Promise<EntityId>((resolve, reject) => {
+    api.tx.contentDirectory.createEntity(classId, { Member: 0 }).signAndSend(ALICE, {}, (res) => {
+      if (res.isError) {
+        reject(new Error(res.status.type))
+      }
+      res.events.forEach(({ event: e }) => {
+        if (e.method === 'EntityCreated') {
+          resolve(e.data[1] as EntityId)
+        }
+        if (e.method === 'ExtrinsicFailed') {
+          reject(new Error('Extrinsic failed'))
+        }
+      })
+    })
+  })
+
+  const inputPropertyValuesMap = await parser.parseToInputEntityValuesMap({ ...channel }, 'Channel')
+  // Having entityId we can create and send addSchemaSupport tx
+  await api.tx.contentDirectory
+    .addSchemaSupportToEntity(
+      { Member: 0 }, // Context (in this case we assume it's Alice's member id)
+      entityId,
+      0, // Schema (currently we have one schema per class, so it can be just 0)
+      inputPropertyValuesMap
+    )
+    .signAndSend(ALICE)
+}
+
+main()
+  .then(() => process.exit())
+  .catch(console.error)

+ 47 - 0
content-directory-schemas/examples/updateChannelTitleWithoutTransaction.ts

@@ -0,0 +1,47 @@
+import { ApiPromise, WsProvider } from '@polkadot/api'
+import { types as joyTypes } from '@joystream/types'
+import { Keyring } from '@polkadot/keyring'
+// Import input parser and channel entity from cd-schemas (we use it as library here)
+import { InputParser } from 'cd-schemas'
+import { ChannelEntity } from 'cd-schemas/types/entities'
+import { FlattenRelations } from 'cd-schemas/types/utility'
+
+// Alternative way of update a channel using updateEntityPropertyValues extrinsic
+async function main() {
+  // Initialize the api
+  const provider = new WsProvider('ws://127.0.0.1:9944')
+  const api = await ApiPromise.create({ provider, types: joyTypes })
+
+  // Get Alice keypair
+  const keyring = new Keyring()
+  keyring.addFromUri('//Alice', undefined, 'sr25519')
+  const [ALICE] = keyring.getPairs()
+
+  // Create partial channel entity, only containing the fields we wish to update
+  const channelUpdateInput: Partial<FlattenRelations<ChannelEntity>> = {
+    title: 'Updated channel title 2',
+  }
+
+  // Create the parser with known entity schemas (the ones in content-directory-schemas/inputs)
+  const parser = InputParser.createWithKnownSchemas(api)
+
+  // We can reuse InputParser's `findEntityIdByUniqueQuery` method to find entityId of the channel we
+  // created in ./createChannelWithoutTransaction.ts example
+  // (normally we would probably use some other way to do it, ie.: query node)
+  const CHANNEL_ID = await parser.findEntityIdByUniqueQuery({ title: 'Example channel 2' }, 'Channel')
+
+  // We use parser to create input property values map
+  const newPropertyValues = await parser.parseToInputEntityValuesMap(channelUpdateInput, 'Channel')
+
+  await api.tx.contentDirectory
+    .updateEntityPropertyValues(
+      { Member: 0 }, // We use member with id 0 as actor (in this case we assume this is Alice)
+      CHANNEL_ID,
+      newPropertyValues
+    )
+    .signAndSend(ALICE)
+}
+
+main()
+  .then(() => process.exit())
+  .catch(console.error)

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

@@ -19,7 +19,9 @@
     "initialize:dev": "yarn initialize:alice-as-lead && yarn initialize:content-dir",
     "example:createChannel": "ts-node ./examples/createChannel.ts",
     "example:createVideo": "ts-node ./examples/createVideo.ts",
-    "example:updateChannelTitle": "ts-node ./examples/updateChannelTitle.ts"
+    "example:updateChannelTitle": "ts-node ./examples/updateChannelTitle.ts",
+    "example:createChannelWithoutTransaction": "ts-node ./examples/createChannelWithoutTransaction.ts",
+    "example:updateChannelTitlelWithoutTransaction": "ts-node ./examples/updateChannelTitleWithoutTransaction.ts"
   },
   "dependencies": {
     "ajv": "6.12.5",

+ 73 - 43
content-directory-schemas/src/helpers/InputParser.ts

@@ -1,7 +1,7 @@
 import { AddClassSchema, Property } from '../../types/extrinsics/AddClassSchema'
 import { createType } from '@joystream/types'
-import { blake2AsHex } from '@polkadot/util-crypto'
 import {
+  InputEntityValuesMap,
   ClassId,
   OperationType,
   ParametrizedPropertyValue,
@@ -10,14 +10,19 @@ import {
   EntityId,
   Entity,
   ParametrizedClassPropertyValue,
+  InputPropertyValue,
 } from '@joystream/types/content-directory'
+import { blake2AsHex } from '@polkadot/util-crypto'
 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'
-import { SubmittableExtrinsic } from '@polkadot/api/types'
+
+type SimpleEntityValue = string | boolean | number | string[] | boolean[] | number[] | undefined
+// Input without "new" or "extising" keywords
+type SimpleEntityInput = { [K: string]: SimpleEntityValue }
 
 export class InputParser {
   private api: ApiPromise
@@ -31,8 +36,6 @@ export class InputParser {
   private entityIdByUniqueQueryMap = new Map<string, number>()
   private entityByUniqueQueryCurrentIndex = 0
   private classIdByNameMap = new Map<string, number>()
-  private classMapInitialized = false
-  private entityIdByUniqueQueryMapInitialized = false
 
   static createWithInitialInputs(api: ApiPromise): InputParser {
     return new InputParser(
@@ -64,30 +67,28 @@ export class InputParser {
     this.batchInputs = batchInputs || []
   }
 
-  private async initializeClassMap() {
-    if (this.classMapInitialized) {
-      return
-    }
+  private async loadClassMap() {
+    this.classIdByNameMap = new Map<string, number>()
+
     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
   }
 
-  // Initialize entityIdByUniqueQueryMap with entities fetched from the chain
-  private async initializeEntityIdByUniqueQueryMap() {
-    if (this.entityIdByUniqueQueryMapInitialized) {
-      return
-    }
-
-    await this.initializeClassMap() // Initialize if not yet initialized
+  private async loadEntityIdByUniqueQueryMap() {
+    this.entityIdByUniqueQueryMap = new Map<string, number>()
 
     // Get entity entries
     const entityEntries: [EntityId, Entity][] = (
       await this.api.query.contentDirectory.entityById.entries()
     ).map(([storageKey, entity]) => [storageKey.args[0] as EntityId, entity])
 
+    // Since we use classMap directly we need to make sure it's loaded first
+    if (!this.classIdByNameMap.size) {
+      await this.loadClassMap()
+    }
+
     entityEntries.forEach(([entityId, entity]) => {
       const classId = entity.class_id.toNumber()
       const className = Array.from(this.classIdByNameMap.entries()).find(([, id]) => id === classId)?.[0]
@@ -121,8 +122,6 @@ export class InputParser {
         this.entityIdByUniqueQueryMap.set(hash, entityId.toNumber())
       })
     })
-
-    this.entityIdByUniqueQueryMapInitialized = true
   }
 
   private schemaByClassName(className: string) {
@@ -152,30 +151,40 @@ export class InputParser {
 
   // Seatch for entity by { [uniquePropName]: [uniquePropVal] } on chain
   async findEntityIdByUniqueQuery(uniquePropVal: Record<string, any>, className: string): Promise<number> {
-    await this.initializeEntityIdByUniqueQueryMap()
     const hash = this.getUniqueQueryHash(uniquePropVal, className)
-    const foundId = this.entityIdByUniqueQueryMap.get(hash)
+    let foundId = this.entityIdByUniqueQueryMap.get(hash)
     if (foundId === undefined) {
-      throw new Error(
-        `findEntityIdByUniqueQuery failed for class ${className} and query: ${JSON.stringify(uniquePropVal)}`
-      )
+      // Try to re-load the map and find again
+      await this.loadEntityIdByUniqueQueryMap()
+      foundId = this.entityIdByUniqueQueryMap.get(hash)
+      if (foundId === undefined) {
+        // If still not found - throw
+        throw new Error(
+          `findEntityIdByUniqueQuery failed for class ${className} and query: ${JSON.stringify(uniquePropVal)}`
+        )
+      }
     }
-
     return foundId
   }
 
-  private getClassIdByName(className: string): number {
-    const classId = this.classIdByNameMap.get(className)
+  async getClassIdByName(className: string): Promise<number> {
+    let classId = this.classIdByNameMap.get(className)
     if (classId === undefined) {
-      throw new Error(`Could not find class id by name: "${className}"!`)
+      // Try to re-load the map
+      await this.loadClassMap()
+      classId = this.classIdByNameMap.get(className)
+      if (classId === undefined) {
+        // If still not found - throw
+        throw new Error(`Could not find class id by name: "${className}"!`)
+      }
     }
     return classId
   }
 
-  private parsePropertyType(propertyType: Property['property_type']): PropertyType {
+  private async parsePropertyType(propertyType: Property['property_type']): Promise<PropertyType> {
     if (isSingle(propertyType) && isReference(propertyType.Single)) {
       const { className, sameOwner } = propertyType.Single.Reference
-      const classId = this.getClassIdByName(className)
+      const classId = await this.getClassIdByName(className)
       return createType('PropertyType', { Single: { Reference: [classId, sameOwner] } })
     }
     // Types other than reference are fully compatible
@@ -224,7 +233,9 @@ export class InputParser {
       let value = customHandler && (await customHandler(schemaProperty, propertyValue))
       if (value === undefined) {
         value = createType('ParametrizedPropertyValue', {
-          InputPropertyValue: this.parsePropertyType(schemaProperty.property_type).toInputPropertyValue(propertyValue),
+          InputPropertyValue: (await this.parsePropertyType(schemaProperty.property_type)).toInputPropertyValue(
+            propertyValue
+          ),
         })
       }
 
@@ -299,7 +310,7 @@ export class InputParser {
     } else {
       // Add operations (createEntity, AddSchemaSupportToEntity)
       const createEntityOperationIndex = this.createEntityOperations.length
-      const classId = this.getClassIdByName(schema.className)
+      const classId = await this.getClassIdByName(schema.className)
       this.createEntityOperations.push(createType('OperationType', { CreateEntity: { class_id: classId } }))
       this.addSchemaToEntityOprations.push(
         createType('OperationType', {
@@ -318,10 +329,7 @@ export class InputParser {
 
   private reset() {
     this.entityIndexByUniqueQueryMap = new Map<string, number>()
-    this.entityIdByUniqueQueryMapInitialized = false
-
     this.classIdByNameMap = new Map<string, number>()
-    this.classMapInitialized = false
 
     this.createEntityOperations = []
     this.addSchemaToEntityOprations = []
@@ -331,7 +339,6 @@ export class InputParser {
   }
 
   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)
@@ -356,7 +363,6 @@ export class InputParser {
     className: string,
     entityId: number
   ): Promise<OperationType[]> {
-    await this.initializeClassMap()
     const schema = this.schemaByClassName(className)
     await this.parseEntityInput(input, schema, entityId)
     const operations = [
@@ -370,13 +376,14 @@ export class InputParser {
   }
 
   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(),
-    }))
+    const classId = await this.getClassIdByName(inputData.className)
+    const newProperties = await Promise.all(
+      inputData.newProperties.map(async (p) => ({
+        ...p,
+        // Parse different format for Reference (and potentially other propTypes in the future)
+        property_type: (await this.parsePropertyType(p.property_type)).toJSON(),
+      }))
+    )
     return this.api.tx.contentDirectory.addClassSchema(
       classId,
       new (JoyBTreeSet(PropertyId))(this.api.registry, inputData.existingProperties),
@@ -401,4 +408,27 @@ export class InputParser {
   public getCreateClassExntrinsics() {
     return this.classInputs.map((data) => this.parseCreateClassExtrinsic(data))
   }
+
+  // Helper parser for "standalone" extrinsics like addSchemaSupportToEntity / updateEntityPropertyValues
+  public async parseToInputEntityValuesMap(
+    inputData: SimpleEntityInput,
+    className: string
+  ): Promise<InputEntityValuesMap> {
+    await this.parseEntityInput(inputData, this.schemaByClassName(className))
+    const inputPropValMap = new Map<PropertyId, InputPropertyValue>()
+
+    const [operation] = this.addSchemaToEntityOprations
+    operation
+      .asType('AddSchemaSupportToEntity')
+      .parametrized_property_values /* First we need to sort by propertyId, since otherwise there will be issues
+      when encoding the BTreeMap (similar to BTreeSet) */
+      .sort((a, b) => a.in_class_index.toNumber() - b.in_class_index.toNumber())
+      .map((pcpv) => {
+        inputPropValMap.set(pcpv.in_class_index, pcpv.value.asType('InputPropertyValue'))
+      })
+
+    this.reset()
+
+    return createType('InputEntityValuesMap', inputPropValMap)
+  }
 }

+ 1 - 1
content-directory-schemas/tsconfig.json

@@ -23,5 +23,5 @@
       "@polkadot/api/augment": ["../types/augment-codec/augment-api.ts"]
     }
   },
-  "include": [ "src/**/*", "scripts/**/*", "typings/**/*" ]
+  "include": [ "src/**/*", "scripts/**/*", "typings/**/*", "examples/**/*" ]
 }

File diff suppressed because it is too large
+ 1 - 1
types/augment-codec/all.ts


File diff suppressed because it is too large
+ 0 - 0
types/augment-codec/augment-types.ts


+ 1 - 0
types/augment/all/defs.json

@@ -966,6 +966,7 @@
             "AddSchemaSupportToEntity": "AddSchemaSupportToEntityOperation"
         }
     },
+    "InputEntityValuesMap": "BTreeMap<PropertyId,InputPropertyValue>",
     "ClassPermissionsType": "Null",
     "ClassPropertyValue": "Null",
     "Operation": "Null",

+ 3 - 0
types/augment/all/types.ts

@@ -565,6 +565,9 @@ export interface InboundReferenceCounter extends Struct {
   readonly same_owner: u32;
 }
 
+/** @name InputEntityValuesMap */
+export interface InputEntityValuesMap extends BTreeMap<PropertyId, InputPropertyValue> {}
+
 /** @name InputPropertyValue */
 export interface InputPropertyValue extends Enum {
   readonly isSingle: boolean;

File diff suppressed because it is too large
+ 0 - 0
types/augment/augment-types.ts


+ 3 - 0
types/src/content-directory/index.ts

@@ -269,6 +269,8 @@ export class ClassPropertyValue extends Null {}
 export class Operation extends Null {}
 export class ReferenceConstraint extends Null {}
 
+export class InputEntityValuesMap extends BTreeMap.with(PropertyId, InputPropertyValue) {}
+
 export const contentDirectoryTypes = {
   Nonce,
   EntityId,
@@ -316,6 +318,7 @@ export const contentDirectoryTypes = {
   UpdatePropertyValuesOperation,
   AddSchemaSupportToEntityOperation,
   OperationType,
+  InputEntityValuesMap,
   // Versioned store relicts - to be removed:
   ClassPermissionsType,
   ClassPropertyValue,

+ 3 - 1
types/src/index.ts

@@ -15,7 +15,7 @@ import media from './media'
 import proposals from './proposals'
 import contentDirectory from './content-directory'
 import { InterfaceTypes } from '@polkadot/types/types/registry'
-import { TypeRegistry, Text, UInt, Null, bool, Option, Vec, BTreeSet } from '@polkadot/types'
+import { TypeRegistry, Text, UInt, Null, bool, Option, Vec, BTreeSet, BTreeMap } from '@polkadot/types'
 import { ExtendedEnum } from './JoyEnum'
 import { ExtendedStruct } from './JoyStruct'
 import BN from 'bn.js'
@@ -87,6 +87,8 @@ type CreateInterface_NoOption<T extends Codec> =
       ? boolean
       : T extends Vec<infer S> | BTreeSet<infer S>
       ? CreateInterface<S>[]
+      : T extends BTreeMap<infer K, infer V>
+      ? Map<K, V>
       : any)
 
 // Wrapper for CreateInterface_NoOption that includes resolving an Option

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