import Debug from 'debug' import { DB, SubstrateEvent } from '../../generated/indexer' import { NextEntityId } from '../../generated/graphql-server/src/modules/next-entity-id/next-entity-id.model' import { ClassEntity } from '../../generated/graphql-server/src/modules/class-entity/class-entity.model' import { decode } from './decode' import { ClassEntityMap, ICategory, IChannel, ICreateEntityOperation, IDBBlockId, IEntity, IHttpMediaLocation, IJoystreamMediaLocation, IKnownLicense, ILanguage, ILicense, IMediaLocation, IUserDefinedLicense, IVideo, IVideoMedia, IVideoMediaEncoding, IWhereCond, } from '../types' import { CategoryPropertyNamesWithId, channelPropertyNamesWithId, knownLicensePropertyNamesWIthId, userDefinedLicensePropertyNamesWithId, joystreamMediaLocationPropertyNamesWithId, httpMediaLocationPropertyNamesWithId, videoMediaPropertyNamesWithId, videoMediaEncodingPropertyNamesWithId, videoPropertyNamesWithId, languagePropertyNamesWIthId, ContentDirectoryKnownClasses, licensePropertyNamesWithId, mediaLocationPropertyNamesWithId, } from './content-dir-consts' import { updateCategoryEntityPropertyValues, updateChannelEntityPropertyValues, updateVideoMediaEntityPropertyValues, updateVideoEntityPropertyValues, updateUserDefinedLicenseEntityPropertyValues, updateHttpMediaLocationEntityPropertyValues, updateJoystreamMediaLocationEntityPropertyValues, updateKnownLicenseEntityPropertyValues, updateLanguageEntityPropertyValues, updateVideoMediaEncodingEntityPropertyValues, updateLicenseEntityPropertyValues, updateMediaLocationEntityPropertyValues, } from './entity/update' import { createCategory, createChannel, createVideoMedia, createVideo, createUserDefinedLicense, createKnownLicense, createHttpMediaLocation, createJoystreamMediaLocation, createLanguage, createVideoMediaEncoding, getClassName, createLicense, createMediaLocation, createBlockOrGetFromDatabase, } from './entity/create' import { getOrCreate } from './get-or-create' const debug = Debug('mappings:cd:transaction') async function getNextEntityId(db: DB): Promise { const e = await db.get(NextEntityId, { where: { id: '1' } }) // Entity creation happens before addSchemaSupport so this should never happen if (!e) throw Error(`NextEntityId table doesn't have any record`) return e.nextId } // eslint-disable-next-line @typescript-eslint/naming-convention export async function contentDirectory_TransactionCompleted(db: DB, event: SubstrateEvent): Promise { debug(`TransactionCompleted event: ${JSON.stringify(event)}`) const { extrinsic, blockNumber: block } = event if (!extrinsic) { throw Error(`No extrinsic found for the event: ${event.id}`) } const { 1: operations } = extrinsic.args if (operations.name.toString() !== 'operations') { throw Error(`Could not found 'operations' in the extrinsic.args[1]`) } const { addSchemaSupportToEntityOperations, createEntityOperations, updatePropertyValuesOperations, } = decode.getOperations(event) // Create entities before adding schema support // We need this to know which entity belongs to which class(we will need to know to update/create // Channel, Video etc.). For example if there is a property update operation there is no class id await batchCreateClassEntities(db, block, createEntityOperations) await batchAddSchemaSupportToEntity(db, createEntityOperations, addSchemaSupportToEntityOperations, block) await batchUpdatePropertyValue(db, createEntityOperations, updatePropertyValuesOperations) } async function batchCreateClassEntities(db: DB, block: number, operations: ICreateEntityOperation[]): Promise { const nId = await db.get(NextEntityId, { where: { id: '1' } }) let nextId = nId ? nId.nextId : 1 // start entity id from 1 for (const { classId } of operations) { const c = new ClassEntity({ id: nextId.toString(), // entity id classId: classId, version: block, happenedIn: await createBlockOrGetFromDatabase(db, block), }) await db.save(c) nextId++ } await getOrCreate.nextEntityId(db, nextId) } /** * * @param db database connection * @param createEntityOperations: Entity creations with in the same transaction * @param entities List of entities that schema support is added for * @param block block number */ async function batchAddSchemaSupportToEntity( db: DB, createEntityOperations: ICreateEntityOperation[], entities: IEntity[], block: number ) { const classEntityMap: ClassEntityMap = new Map() for (const entity of entities) { const className = await getClassName(db, entity, createEntityOperations) if (className !== undefined) { const es = classEntityMap.get(className) classEntityMap.set(className, es ? [...es, entity] : [entity]) } } // This is a copy of classEntityMap, we will use it to keep track of items. // We will remove items from this list whenever we insert them into db const doneList: ClassEntityMap = new Map(classEntityMap.entries()) const nextEntityIdBeforeTransaction = (await getNextEntityId(db)) - createEntityOperations.length for (const [className, entities] of classEntityMap) { for (const entity of entities) { const { entityId, indexOf, properties } = entity // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const id = entityId !== undefined ? entityId : indexOf! + nextEntityIdBeforeTransaction const arg: IDBBlockId = { db, block, id: id.toString() } switch (className) { case ContentDirectoryKnownClasses.CATEGORY: await createCategory(arg, decode.setEntityPropertyValues(properties, CategoryPropertyNamesWithId)) break case ContentDirectoryKnownClasses.CHANNEL: await createChannel( arg, doneList, decode.setEntityPropertyValues(properties, channelPropertyNamesWithId), nextEntityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.KNOWNLICENSE: await createKnownLicense( arg, decode.setEntityPropertyValues(properties, knownLicensePropertyNamesWIthId) ) break case ContentDirectoryKnownClasses.USERDEFINEDLICENSE: await createUserDefinedLicense( arg, decode.setEntityPropertyValues(properties, userDefinedLicensePropertyNamesWithId) ) break case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION: await createJoystreamMediaLocation( arg, decode.setEntityPropertyValues( properties, joystreamMediaLocationPropertyNamesWithId ) ) break case ContentDirectoryKnownClasses.HTTPMEDIALOCATION: await createHttpMediaLocation( arg, decode.setEntityPropertyValues(properties, httpMediaLocationPropertyNamesWithId) ) break case ContentDirectoryKnownClasses.VIDEOMEDIA: await createVideoMedia( arg, doneList, decode.setEntityPropertyValues(properties, videoMediaPropertyNamesWithId), nextEntityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.VIDEO: await createVideo( arg, doneList, decode.setEntityPropertyValues(properties, videoPropertyNamesWithId), nextEntityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.LANGUAGE: await createLanguage(arg, decode.setEntityPropertyValues(properties, languagePropertyNamesWIthId)) break case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING: await createVideoMediaEncoding( arg, decode.setEntityPropertyValues(properties, videoMediaEncodingPropertyNamesWithId) ) break case ContentDirectoryKnownClasses.LICENSE: await createLicense( arg, classEntityMap, decode.setEntityPropertyValues(properties, licensePropertyNamesWithId), nextEntityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.MEDIALOCATION: await createMediaLocation( arg, classEntityMap, decode.setEntityPropertyValues(properties, mediaLocationPropertyNamesWithId), nextEntityIdBeforeTransaction ) break default: console.log(`Unknown class name: ${className}`) break } } } } /** * Batch update operations for entity properties values update * @param db database connection * @param createEntityOperations Entity creations with in the same transaction * @param entities list of entities those properties values updated */ async function batchUpdatePropertyValue(db: DB, createEntityOperations: ICreateEntityOperation[], entities: IEntity[]) { const entityIdBeforeTransaction = (await getNextEntityId(db)) - createEntityOperations.length for (const entity of entities) { const { entityId, indexOf, properties } = entity // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const id = entityId ? entityId.toString() : entityIdBeforeTransaction - indexOf! const where: IWhereCond = { where: { id: id.toString() } } const className = await getClassName(db, entity, createEntityOperations) if (className === undefined) { console.log(`Can not update entity properties values. Unknown class name`) return } switch (className) { case ContentDirectoryKnownClasses.CHANNEL: await updateChannelEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, channelPropertyNamesWithId), entityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.CATEGORY: await updateCategoryEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, CategoryPropertyNamesWithId) ) break case ContentDirectoryKnownClasses.KNOWNLICENSE: await updateKnownLicenseEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, knownLicensePropertyNamesWIthId) ) break case ContentDirectoryKnownClasses.USERDEFINEDLICENSE: await updateUserDefinedLicenseEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, userDefinedLicensePropertyNamesWithId) ) break case ContentDirectoryKnownClasses.JOYSTREAMMEDIALOCATION: await updateJoystreamMediaLocationEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, joystreamMediaLocationPropertyNamesWithId) ) break case ContentDirectoryKnownClasses.HTTPMEDIALOCATION: await updateHttpMediaLocationEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, httpMediaLocationPropertyNamesWithId) ) break case ContentDirectoryKnownClasses.VIDEOMEDIA: await updateVideoMediaEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, videoPropertyNamesWithId), entityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.VIDEO: await updateVideoEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, videoPropertyNamesWithId), entityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.LANGUAGE: await updateLanguageEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, languagePropertyNamesWIthId) ) break case ContentDirectoryKnownClasses.VIDEOMEDIAENCODING: await updateVideoMediaEncodingEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, videoMediaEncodingPropertyNamesWithId) ) break case ContentDirectoryKnownClasses.LICENSE: await updateLicenseEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, licensePropertyNamesWithId), entityIdBeforeTransaction ) break case ContentDirectoryKnownClasses.MEDIALOCATION: await updateMediaLocationEntityPropertyValues( db, where, decode.setEntityPropertyValues(properties, mediaLocationPropertyNamesWithId), entityIdBeforeTransaction ) break default: console.log(`Unknown class name: ${className}`) break } } }