123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225 |
- import { AddClassSchema, Property } from '../../types/extrinsics/AddClassSchema'
- import { FetchedInput } from './inputs'
- 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'
- export class InputParser {
- private api: ApiPromise
- private classInputs: FetchedInput<CreateClass>[]
- private schemaInputs: FetchedInput<AddClassSchema>[]
- private batchInputs: FetchedInput<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
- constructor(
- api: ApiPromise,
- classInputs?: FetchedInput<CreateClass>[],
- schemaInputs?: FetchedInput<AddClassSchema>[],
- batchInputs?: FetchedInput<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.data
- }
- 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).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)) {
- const refEntitySchema = this.schemaByClassName(schemaPropertyType.Single.Reference.className)
- if (Object.keys(propertyValue).includes('new')) {
- 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 parseEntityInput(entityInput: Record<string, any>, schema: AddClassSchema) {
- const parametrizedPropertyValues = Object.entries(entityInput).map(([propertyName, propertyValue]) => {
- const schemaPropertyIndex = schema.newProperties.findIndex((p) => p.name === propertyName)
- const schemaPropertyType = schema.newProperties[schemaPropertyIndex].property_type
- let value: ParametrizedPropertyValue
- // Handle references
- if (isSingle(schemaPropertyType) && isReference(schemaPropertyType.Single)) {
- const refEntitySchema = this.schemaByClassName(schemaPropertyType.Single.Reference.className)
- let entityIndex: number
- if (Object.keys(propertyValue).includes('new')) {
- entityIndex = this.parseEntityInput(propertyValue.new, refEntitySchema)
- } else if (Object.keys(propertyValue).includes('existing')) {
- entityIndex = this.findEntityIndexByUniqueQuery(propertyValue.existing, refEntitySchema.className)
- } else {
- throw new Error(`Invalid reference property value: ${JSON.stringify(propertyValue)}`)
- }
- value = createType('ParametrizedPropertyValue', { InternalEntityJustAdded: entityIndex })
- } else {
- value = createType('ParametrizedPropertyValue', {
- InputPropertyValue: this.parsePropertyType(schemaPropertyType).toInputPropertyValue(propertyValue).toJSON(),
- })
- }
- return {
- in_class_index: schemaPropertyIndex,
- value: value.toJSON(),
- }
- })
- // 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(({ data: batch }) => {
- const entitySchema = this.schemaByClassName(batch.className)
- batch.entries.forEach((entityInput) => this.includeEntityInputInUniqueQueryMap(entityInput, entitySchema))
- })
- // Then - parse into actual operations
- this.batchInputs.forEach(({ data: 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 getAddSchemaExtrinsics() {
- await this.initializeClassMap()
- return this.schemaInputs.map(({ data: schema }) => {
- const classId = this.getClassIdByName(schema.className)
- const newProperties = schema.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, schema.existingProperties),
- newProperties
- )
- })
- }
- public getCreateClassExntrinsics() {
- return this.classInputs.map(({ data: aClass }) =>
- this.api.tx.contentDirectory.createClass(
- aClass.name,
- aClass.description,
- aClass.class_permissions || {},
- aClass.maximum_entities_count,
- aClass.default_entity_creation_voucher_upper_bound
- )
- )
- }
- }
|