InputParser.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import { AddClassSchema, Property } from '../../types/extrinsics/AddClassSchema'
  2. import { FetchedInput } from './inputs'
  3. import { createType } from '@joystream/types'
  4. import { blake2AsHex } from '@polkadot/util-crypto'
  5. import {
  6. ClassId,
  7. OperationType,
  8. ParametrizedPropertyValue,
  9. PropertyId,
  10. PropertyType,
  11. } from '@joystream/types/content-directory'
  12. import { isSingle, isReference } from './propertyType'
  13. import { ApiPromise } from '@polkadot/api'
  14. import { JoyBTreeSet } from '@joystream/types/common'
  15. import { CreateClass } from 'types/extrinsics/CreateClass'
  16. import { EntityBatch } from 'types/EntityBatch'
  17. export class InputParser {
  18. private api: ApiPromise
  19. private classInputs: FetchedInput<CreateClass>[]
  20. private schemaInputs: FetchedInput<AddClassSchema>[]
  21. private batchInputs: FetchedInput<EntityBatch>[]
  22. private createEntityOperations: OperationType[] = []
  23. private addSchemaToEntityOprations: OperationType[] = []
  24. private entityIndexByUniqueQueryMap = new Map<string, number>()
  25. private entityByUniqueQueryCurrentIndex = 0
  26. private classIdByNameMap = new Map<string, number>()
  27. private classMapInitialized = false
  28. constructor(
  29. api: ApiPromise,
  30. classInputs?: FetchedInput<CreateClass>[],
  31. schemaInputs?: FetchedInput<AddClassSchema>[],
  32. batchInputs?: FetchedInput<EntityBatch>[]
  33. ) {
  34. this.api = api
  35. this.classInputs = classInputs || []
  36. this.schemaInputs = schemaInputs || []
  37. this.batchInputs = batchInputs || []
  38. }
  39. private async initializeClassMap() {
  40. if (this.classMapInitialized) {
  41. return
  42. }
  43. const classEntries = await this.api.query.contentDirectory.classById.entries()
  44. classEntries.forEach(([key, aClass]) => {
  45. this.classIdByNameMap.set(aClass.name.toString(), (key.args[0] as ClassId).toNumber())
  46. })
  47. this.classMapInitialized = true
  48. }
  49. private schemaByClassName(className: string) {
  50. const foundSchema = this.schemaInputs.find(({ data }) => data.className === className)
  51. if (!foundSchema) {
  52. throw new Error(`Schema not found by class name: ${className}`)
  53. }
  54. return foundSchema.data
  55. }
  56. private getUniqueQueryHash(uniquePropVal: Record<string, any>, className: string) {
  57. return blake2AsHex(JSON.stringify([className, uniquePropVal]))
  58. }
  59. private findEntityIndexByUniqueQuery(uniquePropVal: Record<string, any>, className: string) {
  60. const hash = this.getUniqueQueryHash(uniquePropVal, className)
  61. const foundIndex = this.entityIndexByUniqueQueryMap.get(hash)
  62. if (foundIndex === undefined) {
  63. throw new Error(
  64. `findEntityIndexByUniqueQuery failed for class ${className} and query: ${JSON.stringify(uniquePropVal)}`
  65. )
  66. }
  67. return foundIndex
  68. }
  69. private getClassIdByName(className: string): number {
  70. const classId = this.classIdByNameMap.get(className)
  71. if (classId === undefined) {
  72. throw new Error(`Could not find class id by name: "${className}"!`)
  73. }
  74. return classId
  75. }
  76. private parsePropertyType(propertyType: Property['property_type']): PropertyType {
  77. if (isSingle(propertyType) && isReference(propertyType.Single)) {
  78. const { className, sameOwner } = propertyType.Single.Reference
  79. const classId = this.getClassIdByName(className)
  80. return createType('PropertyType', { Single: { Reference: [classId, sameOwner] } })
  81. }
  82. // Types other than reference are fully compatible
  83. return createType('PropertyType', propertyType)
  84. }
  85. private includeEntityInputInUniqueQueryMap(entityInput: Record<string, any>, schema: AddClassSchema) {
  86. Object.entries(entityInput).forEach(([propertyName, propertyValue]) => {
  87. const schemaPropertyType = schema.newProperties.find((p) => p.name === propertyName)!.property_type
  88. // Handle entities "nested" via "new"
  89. if (isSingle(schemaPropertyType) && isReference(schemaPropertyType.Single)) {
  90. const refEntitySchema = this.schemaByClassName(schemaPropertyType.Single.Reference.className)
  91. if (Object.keys(propertyValue).includes('new')) {
  92. this.includeEntityInputInUniqueQueryMap(propertyValue.new, refEntitySchema)
  93. }
  94. }
  95. })
  96. // Add entries to entityIndexByUniqueQueryMap
  97. schema.newProperties
  98. .filter((p) => p.unique)
  99. .forEach(({ name }) => {
  100. if (entityInput[name] === undefined) {
  101. // Skip empty values (not all unique properties are required)
  102. return
  103. }
  104. const hash = this.getUniqueQueryHash({ [name]: entityInput[name] }, schema.className)
  105. this.entityIndexByUniqueQueryMap.set(hash, this.entityByUniqueQueryCurrentIndex)
  106. })
  107. ++this.entityByUniqueQueryCurrentIndex
  108. }
  109. private parseEntityInput(entityInput: Record<string, any>, schema: AddClassSchema) {
  110. const parametrizedPropertyValues = Object.entries(entityInput).map(([propertyName, propertyValue]) => {
  111. const schemaPropertyIndex = schema.newProperties.findIndex((p) => p.name === propertyName)
  112. const schemaPropertyType = schema.newProperties[schemaPropertyIndex].property_type
  113. let value: ParametrizedPropertyValue
  114. // Handle references
  115. if (isSingle(schemaPropertyType) && isReference(schemaPropertyType.Single)) {
  116. const refEntitySchema = this.schemaByClassName(schemaPropertyType.Single.Reference.className)
  117. let entityIndex: number
  118. if (Object.keys(propertyValue).includes('new')) {
  119. entityIndex = this.parseEntityInput(propertyValue.new, refEntitySchema)
  120. } else if (Object.keys(propertyValue).includes('existing')) {
  121. entityIndex = this.findEntityIndexByUniqueQuery(propertyValue.existing, refEntitySchema.className)
  122. } else {
  123. throw new Error(`Invalid reference property value: ${JSON.stringify(propertyValue)}`)
  124. }
  125. value = createType('ParametrizedPropertyValue', { InternalEntityJustAdded: entityIndex })
  126. } else {
  127. value = createType('ParametrizedPropertyValue', {
  128. InputPropertyValue: this.parsePropertyType(schemaPropertyType).toInputPropertyValue(propertyValue).toJSON(),
  129. })
  130. }
  131. return {
  132. in_class_index: schemaPropertyIndex,
  133. value: value.toJSON(),
  134. }
  135. })
  136. // Add operations
  137. const createEntityOperationIndex = this.createEntityOperations.length
  138. const classId = this.classIdByNameMap.get(schema.className)
  139. this.createEntityOperations.push(createType('OperationType', { CreateEntity: { class_id: classId } }))
  140. this.addSchemaToEntityOprations.push(
  141. createType('OperationType', {
  142. AddSchemaSupportToEntity: {
  143. schema_id: 0,
  144. entity_id: { InternalEntityJustAdded: createEntityOperationIndex },
  145. parametrized_property_values: parametrizedPropertyValues,
  146. },
  147. })
  148. )
  149. // Return CreateEntity operation index
  150. return createEntityOperationIndex
  151. }
  152. private reset() {
  153. this.entityIndexByUniqueQueryMap = new Map<string, number>()
  154. this.classIdByNameMap = new Map<string, number>()
  155. this.createEntityOperations = []
  156. this.addSchemaToEntityOprations = []
  157. this.entityByUniqueQueryCurrentIndex = 0
  158. }
  159. public async getEntityBatchOperations() {
  160. await this.initializeClassMap()
  161. // First - create entityUniqueQueryMap to allow referencing any entity at any point
  162. this.batchInputs.forEach(({ data: batch }) => {
  163. const entitySchema = this.schemaByClassName(batch.className)
  164. batch.entries.forEach((entityInput) => this.includeEntityInputInUniqueQueryMap(entityInput, entitySchema))
  165. })
  166. // Then - parse into actual operations
  167. this.batchInputs.forEach(({ data: batch }) => {
  168. const entitySchema = this.schemaByClassName(batch.className)
  169. batch.entries.forEach((entityInput) => this.parseEntityInput(entityInput, entitySchema))
  170. })
  171. const operations = [...this.createEntityOperations, ...this.addSchemaToEntityOprations]
  172. this.reset()
  173. return operations
  174. }
  175. public async getAddSchemaExtrinsics() {
  176. await this.initializeClassMap()
  177. return this.schemaInputs.map(({ data: schema }) => {
  178. const classId = this.getClassIdByName(schema.className)
  179. const newProperties = schema.newProperties.map((p) => ({
  180. ...p,
  181. // Parse different format for Reference (and potentially other propTypes in the future)
  182. property_type: this.parsePropertyType(p.property_type).toJSON(),
  183. }))
  184. return this.api.tx.contentDirectory.addClassSchema(
  185. classId,
  186. new (JoyBTreeSet(PropertyId))(this.api.registry, schema.existingProperties),
  187. newProperties
  188. )
  189. })
  190. }
  191. public getCreateClassExntrinsics() {
  192. return this.classInputs.map(({ data: aClass }) =>
  193. this.api.tx.contentDirectory.createClass(
  194. aClass.name,
  195. aClass.description,
  196. aClass.class_permissions || {},
  197. aClass.maximum_entities_count,
  198. aClass.default_entity_creation_voucher_upper_bound
  199. )
  200. )
  201. }
  202. }