// TODO: finish db cascade on save/remove; right now there is manually added `cascade: ["insert", "update"]` directive // to all relations in `query-node/generated/graphql-server/src/modules/**/*.model.ts`. That should ensure all records // are saved on one `db.save(...)` call. Missing features // - find a proper way to cascade on remove or implement custom removals for every entity // - convert manual changes done to `*model.ts` file into some patch or bash commands that can be executed // every time query node codegen is run (that will overwrite said manual changes) // - verify in integration tests that the records are trully created/updated/removed as expected import { SubstrateEvent } from '@dzlzv/hydra-common' import { DatabaseManager } from '@dzlzv/hydra-db-utils' import { Bytes } from '@polkadot/types' import ISO6391 from 'iso-639-1' import { u64 } from '@polkadot/types/primitive' import { FindConditions } from 'typeorm' import * as jspb from 'google-protobuf' import { fixBlockTimestamp } from '../eventFix' // protobuf definitions import { ChannelMetadata, ChannelCategoryMetadata, PublishedBeforeJoystream as PublishedBeforeJoystreamMetadata, License as LicenseMetadata, MediaType as MediaTypeMetadata, VideoMetadata, VideoCategoryMetadata, } from '@joystream/content-metadata-protobuf' import { Content } from '../../../generated/types' import { invalidMetadata, inconsistentState, logger, prepareDataObject, getNextId } from '../common' import { // primary entities CuratorGroup, Channel, ChannelCategory, Video, VideoCategory, // secondary entities Language, License, VideoMediaEncoding, VideoMediaMetadata, // asset DataObjectOwner, DataObjectOwnerMember, DataObjectOwnerChannel, DataObject, LiaisonJudgement, AssetAvailability, Membership, } from 'query-node' // Joystream types import { ChannelId, ContentParameters, NewAsset, ContentActor } from '@joystream/types/augment' import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage' import { registry } from '@joystream/types' /* Asset either stored in storage or describing list of URLs. */ type AssetStorageOrUrls = DataObject | string[] /* Type guard differentiating asset stored in storage from asset describing a list of URLs. */ function isAssetInStorage(dataObject: AssetStorageOrUrls): dataObject is DataObject { if (Array.isArray(dataObject)) { return false } return true } export interface IReadProtobufArguments { metadata: Bytes db: DatabaseManager event: SubstrateEvent } export interface IReadProtobufArgumentsWithAssets extends IReadProtobufArguments { assets: NewAsset[] // assets provided in event contentOwner: typeof DataObjectOwner } /* This class represents one of 3 possible states when changing property read from metadata. NoChange - don't change anything (used when invalid metadata are encountered) Unset - unset the value (used when the unset is requested in runtime) Change - set the new value */ export class PropertyChange { static newUnset(): PropertyChange { return new PropertyChange('unset') } static newNoChange(): PropertyChange { return new PropertyChange('nochange') } static newChange(value: T): PropertyChange { return new PropertyChange('change', value) } /* Determines property change from the given object property. */ static fromObjectProperty( object: ChangedObject, key: Key ): PropertyChange { if (!(key in object)) { return PropertyChange.newNoChange() } if (object[key] === undefined) { return PropertyChange.newUnset() } return PropertyChange.newChange(object[key] as T) } private type: string private value?: T private constructor(type: 'change' | 'nochange' | 'unset', value?: T) { this.type = type this.value = value } public isUnset(): boolean { return this.type === 'unset' } public isNoChange(): boolean { return this.type === 'nochange' } public isValue(): boolean { return this.type === 'change' } public getValue(): T | undefined { return this.type === 'change' ? this.value : undefined } /* Integrates the value into the given dictionary. */ public integrateInto(object: Object, key: string): void { if (this.isNoChange()) { return } if (this.isUnset()) { delete object[key] return } object[key] = this.value } } export interface RawVideoMetadata { encoding: { codecName: PropertyChange container: PropertyChange mimeMediaType: PropertyChange } pixelWidth: PropertyChange pixelHeight: PropertyChange size: PropertyChange } /* Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db. */ export async function readProtobuf( type: T, parameters: IReadProtobufArguments ): Promise> { // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length) const metaU8a = parameters.metadata.toU8a(true) // process channel category if (type instanceof ChannelCategory) { const meta = ChannelCategoryMetadata.deserializeBinary(metaU8a) const result = convertMetadataToObject(meta) as Partial return result } // process video category if (type instanceof VideoCategory) { const meta = VideoCategoryMetadata.deserializeBinary(metaU8a) const result = convertMetadataToObject(meta) as Partial return result } // this should never happen logger.error('Not implemented metadata type', { type }) throw new Error(`Not implemented metadata type`) } /* Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db. In addition it handles any assets associated with the metadata. */ export async function readProtobufWithAssets( type: T, parameters: IReadProtobufArgumentsWithAssets ): Promise> { // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length) const metaU8a = parameters.metadata.toU8a(true) // process channel if (type instanceof Channel) { const meta = ChannelMetadata.deserializeBinary(metaU8a) const metaAsObject = convertMetadataToObject(meta) const result = (metaAsObject as any) as Partial // prepare cover photo asset if needed if ('coverPhoto' in metaAsObject) { const asset = await extractAsset({ assetIndex: metaAsObject.coverPhoto, assets: parameters.assets, db: parameters.db, event: parameters.event, contentOwner: parameters.contentOwner, }) integrateAsset('coverPhoto', result, asset) // changes `result` inline! delete metaAsObject.coverPhoto } // prepare avatar photo asset if needed if ('avatarPhoto' in metaAsObject) { const asset = await extractAsset({ assetIndex: metaAsObject.avatarPhoto, assets: parameters.assets, db: parameters.db, event: parameters.event, contentOwner: parameters.contentOwner, }) integrateAsset('avatarPhoto', result, asset) // changes `result` inline! delete metaAsObject.avatarPhoto } // prepare language if needed if ('language' in metaAsObject) { const language = await prepareLanguage(metaAsObject.language, parameters.db, parameters.event) delete metaAsObject.language // make sure temporary value will not interfere language.integrateInto(result, 'language') } return result as Partial } // process video if (type instanceof Video) { const meta = VideoMetadata.deserializeBinary(metaU8a) const metaAsObject = convertMetadataToObject(meta) const result = (metaAsObject as any) as Partial