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[] private schemaInputs: FetchedInput[] private batchInputs: FetchedInput[] private createEntityOperations: OperationType[] = [] private addSchemaToEntityOprations: OperationType[] = [] private entityIndexByUniqueQueryMap = new Map() private entityByUniqueQueryCurrentIndex = 0 private classIdByNameMap = new Map() private classMapInitialized = false constructor( api: ApiPromise, classInputs?: FetchedInput[], schemaInputs?: FetchedInput[], batchInputs?: FetchedInput[] ) { 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, className: string) { return blake2AsHex(JSON.stringify([className, uniquePropVal])) } private findEntityIndexByUniqueQuery(uniquePropVal: Record, 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, 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, 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() this.classIdByNameMap = new Map() 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 ) ) } }