|
@@ -1,435 +0,0 @@
|
|
|
-import { AddClassSchema, Property } from '../../types/extrinsics/AddClassSchema'
|
|
|
-import { createType } from '@joystream/types'
|
|
|
-import {
|
|
|
- InputEntityValuesMap,
|
|
|
- ClassId,
|
|
|
- OperationType,
|
|
|
- ParametrizedPropertyValue,
|
|
|
- PropertyId,
|
|
|
- PropertyType,
|
|
|
- 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 { getInitializationInputs, getInputs } from './inputs'
|
|
|
-
|
|
|
-type SimpleEntityValue = string | boolean | number | string[] | boolean[] | number[] | undefined | null
|
|
|
-// Input without "new" or "extising" keywords
|
|
|
-type SimpleEntityInput = { [K: string]: SimpleEntityValue }
|
|
|
-
|
|
|
-export class InputParser {
|
|
|
- private api: ApiPromise
|
|
|
- private classInputs: CreateClass[]
|
|
|
- private schemaInputs: AddClassSchema[]
|
|
|
- private batchInputs: EntityBatch[]
|
|
|
- private createEntityOperations: OperationType[] = []
|
|
|
- private addSchemaToEntityOprations: OperationType[] = []
|
|
|
- private updateEntityPropertyValuesOperations: OperationType[] = []
|
|
|
- private entityIndexByUniqueQueryMap = new Map<string, number>()
|
|
|
- private entityIdByUniqueQueryMap = new Map<string, number>()
|
|
|
- private entityByUniqueQueryCurrentIndex = 0
|
|
|
- private classIdByNameMap = new Map<string, number>()
|
|
|
-
|
|
|
- static createWithInitialInputs(api: ApiPromise): InputParser {
|
|
|
- const { classInputs, schemaInputs, entityBatchInputs } = getInitializationInputs()
|
|
|
- return new InputParser(api, classInputs, schemaInputs, entityBatchInputs)
|
|
|
- }
|
|
|
-
|
|
|
- static createWithKnownSchemas(api: ApiPromise, entityBatches?: EntityBatch[]): InputParser {
|
|
|
- return new InputParser(
|
|
|
- api,
|
|
|
- [],
|
|
|
- getInputs<AddClassSchema>('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 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())
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- 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]
|
|
|
- if (!className) {
|
|
|
- // Class not found - skip
|
|
|
- return
|
|
|
- }
|
|
|
- let schema: AddClassSchema
|
|
|
- try {
|
|
|
- schema = this.schemaByClassName(className)
|
|
|
- } catch (e) {
|
|
|
- // Input schema not found - skip
|
|
|
- return
|
|
|
- }
|
|
|
- const valuesEntries = Array.from(entity.getField('values').entries())
|
|
|
- schema.newProperties.forEach(({ name, unique }, index) => {
|
|
|
- if (!unique) {
|
|
|
- return // Skip non-unique properties
|
|
|
- }
|
|
|
- const storedValue = valuesEntries.find(([propertyId]) => propertyId.toNumber() === index)?.[1]
|
|
|
- if (
|
|
|
- storedValue === undefined ||
|
|
|
- // If unique value is Bool, it's almost definitely empty, so we skip it
|
|
|
- (storedValue.isOfType('Single') && storedValue.asType('Single').isOfType('Bool'))
|
|
|
- ) {
|
|
|
- // Skip empty values (not all unique properties are required)
|
|
|
- return
|
|
|
- }
|
|
|
- const simpleValue = storedValue.getValue().toJSON()
|
|
|
- const hash = this.getUniqueQueryHash({ [name]: simpleValue }, schema.className)
|
|
|
- this.entityIdByUniqueQueryMap.set(hash, entityId.toNumber())
|
|
|
- })
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
- 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
|
|
|
- }
|
|
|
-
|
|
|
- // Seatch for entity by { [uniquePropName]: [uniquePropVal] } on chain
|
|
|
- async findEntityIdByUniqueQuery(uniquePropVal: Record<string, any>, className: string): Promise<number> {
|
|
|
- const hash = this.getUniqueQueryHash(uniquePropVal, className)
|
|
|
- let foundId = this.entityIdByUniqueQueryMap.get(hash)
|
|
|
- if (foundId === undefined) {
|
|
|
- // 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
|
|
|
- }
|
|
|
-
|
|
|
- async getClassIdByName(className: string): Promise<number> {
|
|
|
- let classId = this.classIdByNameMap.get(className)
|
|
|
- if (classId === undefined) {
|
|
|
- // 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 async parsePropertyType(propertyType: Property['property_type']): Promise<PropertyType> {
|
|
|
- if (isSingle(propertyType) && isReference(propertyType.Single)) {
|
|
|
- const { className, sameOwner } = propertyType.Single.Reference
|
|
|
- const classId = await 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 (propertyValue !== null && 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 async createParametrizedPropertyValues(
|
|
|
- entityInput: Record<string, any>,
|
|
|
- schema: AddClassSchema,
|
|
|
- customHandler?: (property: Property, value: any) => Promise<ParametrizedPropertyValue | undefined>
|
|
|
- ): Promise<ParametrizedClassPropertyValue[]> {
|
|
|
- const filteredInput = Object.entries(entityInput).filter(([, pValue]) => pValue !== undefined)
|
|
|
- const parametrizedClassPropValues: ParametrizedClassPropertyValue[] = []
|
|
|
-
|
|
|
- for (const [propertyName, propertyValue] of filteredInput) {
|
|
|
- const schemaPropertyIndex = schema.newProperties.findIndex((p) => p.name === propertyName)
|
|
|
- const schemaProperty = schema.newProperties[schemaPropertyIndex]
|
|
|
-
|
|
|
- let value = customHandler && (await customHandler(schemaProperty, propertyValue))
|
|
|
- if (value === undefined) {
|
|
|
- if (propertyValue === null) {
|
|
|
- // Optional values: (can be cleared by setting them to Bool(false)):
|
|
|
- value = createType('ParametrizedPropertyValue', { InputPropertyValue: { Single: { Bool: false } } })
|
|
|
- } else {
|
|
|
- value = createType('ParametrizedPropertyValue', {
|
|
|
- InputPropertyValue: (await this.parsePropertyType(schemaProperty.property_type)).toInputPropertyValue(
|
|
|
- propertyValue
|
|
|
- ),
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- parametrizedClassPropValues.push(
|
|
|
- createType('ParametrizedClassPropertyValue', {
|
|
|
- in_class_index: schemaPropertyIndex,
|
|
|
- value,
|
|
|
- })
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
- return parametrizedClassPropValues
|
|
|
- }
|
|
|
-
|
|
|
- private async existingEntityQueryToParametrizedPropertyValue(className: string, uniquePropVal: Record<string, any>) {
|
|
|
- try {
|
|
|
- // First - try to find in existing batches
|
|
|
- const entityIndex = this.findEntityIndexByUniqueQuery(uniquePropVal, className)
|
|
|
- return createType('ParametrizedPropertyValue', { InternalEntityJustAdded: entityIndex })
|
|
|
- } catch (e) {
|
|
|
- // If not found - fallback to chain search
|
|
|
- const entityId = await this.findEntityIdByUniqueQuery(uniquePropVal, className)
|
|
|
- return createType('ParametrizedPropertyValue', {
|
|
|
- InputPropertyValue: { Single: { Reference: entityId } },
|
|
|
- })
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // parseEntityInput Overloads
|
|
|
- private parseEntityInput(entityInput: Record<string, any>, schema: AddClassSchema): Promise<number>
|
|
|
- private parseEntityInput(
|
|
|
- entityInput: Record<string, any>,
|
|
|
- schema: AddClassSchema,
|
|
|
- updatedEntityId: number
|
|
|
- ): Promise<void>
|
|
|
-
|
|
|
- // Parse entity input. Speficy "updatedEntityId" only if want to parse into update operation!
|
|
|
- private async parseEntityInput(
|
|
|
- entityInput: Record<string, any>,
|
|
|
- schema: AddClassSchema,
|
|
|
- updatedEntityId?: number
|
|
|
- ): Promise<void | number> {
|
|
|
- const parametrizedPropertyValues = await this.createParametrizedPropertyValues(
|
|
|
- entityInput,
|
|
|
- schema,
|
|
|
- async (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 (value !== null && Object.keys(value).includes('new')) {
|
|
|
- const entityIndex = await this.parseEntityInput(value.new, refEntitySchema)
|
|
|
- return createType('ParametrizedPropertyValue', { InternalEntityJustAdded: entityIndex })
|
|
|
- } else if (value !== null && Object.keys(value).includes('existing')) {
|
|
|
- return this.existingEntityQueryToParametrizedPropertyValue(refEntitySchema.className, value.existing)
|
|
|
- }
|
|
|
- }
|
|
|
- return undefined
|
|
|
- }
|
|
|
- )
|
|
|
-
|
|
|
- if (updatedEntityId) {
|
|
|
- // Update operation
|
|
|
- this.updateEntityPropertyValuesOperations.push(
|
|
|
- createType('OperationType', {
|
|
|
- UpdatePropertyValues: {
|
|
|
- entity_id: { ExistingEntity: updatedEntityId },
|
|
|
- new_parametrized_property_values: parametrizedPropertyValues,
|
|
|
- },
|
|
|
- })
|
|
|
- )
|
|
|
- } else {
|
|
|
- // Add operations (createEntity, AddSchemaSupportToEntity)
|
|
|
- const createEntityOperationIndex = this.createEntityOperations.length
|
|
|
- const classId = await this.getClassIdByName(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.updateEntityPropertyValuesOperations = []
|
|
|
-
|
|
|
- this.entityByUniqueQueryCurrentIndex = 0
|
|
|
- }
|
|
|
-
|
|
|
- public async getEntityBatchOperations() {
|
|
|
- // 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
|
|
|
- for (const batch of this.batchInputs) {
|
|
|
- const entitySchema = this.schemaByClassName(batch.className)
|
|
|
- for (const entityInput of batch.entries) {
|
|
|
- await this.parseEntityInput(entityInput, entitySchema)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- const operations = [...this.createEntityOperations, ...this.addSchemaToEntityOprations]
|
|
|
- this.reset()
|
|
|
-
|
|
|
- return operations
|
|
|
- }
|
|
|
-
|
|
|
- public async getEntityUpdateOperations(
|
|
|
- input: Record<string, any>,
|
|
|
- className: string,
|
|
|
- entityId: number
|
|
|
- ): Promise<OperationType[]> {
|
|
|
- const schema = this.schemaByClassName(className)
|
|
|
- await this.parseEntityInput(input, schema, entityId)
|
|
|
- const operations = [
|
|
|
- ...this.createEntityOperations,
|
|
|
- ...this.addSchemaToEntityOprations,
|
|
|
- ...this.updateEntityPropertyValuesOperations,
|
|
|
- ]
|
|
|
- this.reset()
|
|
|
-
|
|
|
- return operations
|
|
|
- }
|
|
|
-
|
|
|
- public async parseAddClassSchemaExtrinsic(inputData: AddClassSchema) {
|
|
|
- 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),
|
|
|
- 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))
|
|
|
- }
|
|
|
-
|
|
|
- // 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)
|
|
|
- }
|
|
|
-}
|