|
@@ -1,352 +1,212 @@
|
|
|
-// 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 { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common'
|
|
|
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,
|
|
|
+ IVideoMetadata,
|
|
|
+ IPublishedBeforeJoystream,
|
|
|
+ ILicense,
|
|
|
+ IMediaType,
|
|
|
+ IChannelMetadata,
|
|
|
} from '@joystream/metadata-protobuf'
|
|
|
-
|
|
|
-import { Content } from '../../../generated/types'
|
|
|
-
|
|
|
-import { invalidMetadata, inconsistentState, logger, prepareDataObject, getNextId } from '../common'
|
|
|
-
|
|
|
+import { integrateMeta, isSet, isValidLanguageCode } from '@joystream/metadata-protobuf/utils'
|
|
|
+import { invalidMetadata, inconsistentState, logger } from '../../common'
|
|
|
import {
|
|
|
// primary entities
|
|
|
CuratorGroup,
|
|
|
Channel,
|
|
|
- ChannelCategory,
|
|
|
Video,
|
|
|
VideoCategory,
|
|
|
-
|
|
|
// secondary entities
|
|
|
Language,
|
|
|
License,
|
|
|
- VideoMediaEncoding,
|
|
|
VideoMediaMetadata,
|
|
|
-
|
|
|
// asset
|
|
|
- DataObjectOwner,
|
|
|
- DataObjectOwnerMember,
|
|
|
- DataObjectOwnerChannel,
|
|
|
- DataObject,
|
|
|
- LiaisonJudgement,
|
|
|
- AssetAvailability,
|
|
|
+ Asset,
|
|
|
Membership,
|
|
|
-} from 'query-node'
|
|
|
-
|
|
|
+ VideoMediaEncoding,
|
|
|
+ ChannelCategory,
|
|
|
+ AssetNone,
|
|
|
+} from 'query-node/dist/model'
|
|
|
// Joystream types
|
|
|
-import { ChannelId, ContentParameters, NewAsset, ContentActor } from '@joystream/types/augment'
|
|
|
+import { NewAsset, ContentActor } from '@joystream/types/augment'
|
|
|
+import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
|
|
|
+import BN from 'bn.js'
|
|
|
+
|
|
|
+export async function processChannelMetadata(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ channel: Channel,
|
|
|
+ meta: DecodedMetadataObject<IChannelMetadata>,
|
|
|
+ assets: NewAsset[]
|
|
|
+): Promise<Channel> {
|
|
|
+ // TODO: Assets processing (Storage v2)
|
|
|
+ // const assetsOwner = new DataObjectOwnerChannel()
|
|
|
+ // assetsOwner.channelId = channel.id
|
|
|
|
|
|
-import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
|
|
|
-import { registry } from '@joystream/types'
|
|
|
+ // const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
|
|
|
|
|
|
-/*
|
|
|
- Asset either stored in storage or describing list of URLs.
|
|
|
-*/
|
|
|
-type AssetStorageOrUrls = DataObject | string[]
|
|
|
+ integrateMeta(channel, meta, ['title', 'description', 'isPublic'])
|
|
|
|
|
|
-/*
|
|
|
- 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
|
|
|
+ // prepare channel category if needed
|
|
|
+ if (isSet(meta.category)) {
|
|
|
+ channel.category = await processChannelCategory(ctx, channel.category, parseInt(meta.category))
|
|
|
}
|
|
|
|
|
|
- return true
|
|
|
-}
|
|
|
+ channel.coverPhoto = new AssetNone()
|
|
|
+ channel.avatarPhoto = new AssetNone()
|
|
|
+ // // prepare cover photo asset if needed
|
|
|
+ // if (isSet(meta.coverPhoto)) {
|
|
|
+ // const asset = findAssetByIndex(processedAssets, meta.coverPhoto, 'channel cover photo')
|
|
|
+ // if (asset) {
|
|
|
+ // channel.coverPhoto = asset
|
|
|
+ // }
|
|
|
+ // }
|
|
|
|
|
|
-export interface IReadProtobufArguments {
|
|
|
- metadata: Bytes
|
|
|
- db: DatabaseManager
|
|
|
- event: SubstrateEvent
|
|
|
-}
|
|
|
+ // // prepare avatar photo asset if needed
|
|
|
+ // if (isSet(meta.avatarPhoto)) {
|
|
|
+ // const asset = findAssetByIndex(processedAssets, meta.avatarPhoto, 'channel avatar photo')
|
|
|
+ // if (asset) {
|
|
|
+ // channel.avatarPhoto = asset
|
|
|
+ // }
|
|
|
+ // }
|
|
|
|
|
|
-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<T> {
|
|
|
- static newUnset<T>(): PropertyChange<T> {
|
|
|
- return new PropertyChange<T>('unset')
|
|
|
+ // prepare language if needed
|
|
|
+ if (isSet(meta.language)) {
|
|
|
+ channel.language = await processLanguage(ctx, channel.language, meta.language)
|
|
|
}
|
|
|
|
|
|
- static newNoChange<T>(): PropertyChange<T> {
|
|
|
- return new PropertyChange<T>('nochange')
|
|
|
- }
|
|
|
+ return channel
|
|
|
+}
|
|
|
|
|
|
- static newChange<T>(value: T): PropertyChange<T> {
|
|
|
- return new PropertyChange<T>('change', value)
|
|
|
- }
|
|
|
+export async function processVideoMetadata(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ channel: Channel,
|
|
|
+ video: Video,
|
|
|
+ meta: DecodedMetadataObject<IVideoMetadata>,
|
|
|
+ assets: NewAsset[]
|
|
|
+): Promise<Video> {
|
|
|
+ // TODO: Assets processing (Storage v2)
|
|
|
+ // const assetsOwner = new DataObjectOwnerChannel()
|
|
|
+ // assetsOwner.channelId = channel.id
|
|
|
|
|
|
- /*
|
|
|
- Determines property change from the given object property.
|
|
|
- */
|
|
|
- static fromObjectProperty<T, Key extends string, ChangedObject extends { [key in Key]?: T }>(
|
|
|
- object: ChangedObject,
|
|
|
- key: Key
|
|
|
- ): PropertyChange<T> {
|
|
|
- if (!(key in object)) {
|
|
|
- return PropertyChange.newNoChange<T>()
|
|
|
- }
|
|
|
+ // const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
|
|
|
|
|
|
- if (object[key] === undefined) {
|
|
|
- return PropertyChange.newUnset<T>()
|
|
|
- }
|
|
|
+ integrateMeta(video, meta, ['title', 'description', 'duration', 'hasMarketing', 'isExplicit', 'isPublic'])
|
|
|
|
|
|
- return PropertyChange.newChange<T>(object[key] as T)
|
|
|
+ // prepare video category if needed
|
|
|
+ if (meta.category) {
|
|
|
+ video.category = await processVideoCategory(ctx, video.category, parseInt(meta.category))
|
|
|
}
|
|
|
|
|
|
- private type: string
|
|
|
- private value?: T
|
|
|
-
|
|
|
- private constructor(type: 'change' | 'nochange' | 'unset', value?: T) {
|
|
|
- this.type = type
|
|
|
- this.value = value
|
|
|
+ // prepare media meta information if needed
|
|
|
+ if (isSet(meta.mediaType) || isSet(meta.mediaPixelWidth) || isSet(meta.mediaPixelHeight)) {
|
|
|
+ // prepare video file size if poosible
|
|
|
+ const videoSize = 0 // TODO: extractVideoSize(assets, meta.video)
|
|
|
+ video.mediaMetadata = await processVideoMediaMetadata(ctx, video.mediaMetadata, meta, videoSize)
|
|
|
}
|
|
|
|
|
|
- public isUnset(): boolean {
|
|
|
- return this.type === 'unset'
|
|
|
+ // prepare license if needed
|
|
|
+ if (isSet(meta.license)) {
|
|
|
+ await updateVideoLicense(ctx, video, meta.license)
|
|
|
}
|
|
|
|
|
|
- public isNoChange(): boolean {
|
|
|
- return this.type === 'nochange'
|
|
|
- }
|
|
|
+ video.thumbnailPhoto = new AssetNone()
|
|
|
+ video.media = new AssetNone()
|
|
|
+ // // prepare thumbnail photo asset if needed
|
|
|
+ // if (isSet(meta.thumbnailPhoto)) {
|
|
|
+ // const asset = findAssetByIndex(processedAssets, meta.thumbnailPhoto, 'thumbnail photo')
|
|
|
+ // if (asset) {
|
|
|
+ // video.thumbnailPhoto = asset
|
|
|
+ // }
|
|
|
+ // }
|
|
|
|
|
|
- public isValue(): boolean {
|
|
|
- return this.type === 'change'
|
|
|
- }
|
|
|
+ // // prepare video asset if needed
|
|
|
+ // if (isSet(meta.video)) {
|
|
|
+ // const asset = findAssetByIndex(processedAssets, meta.video, 'video')
|
|
|
+ // if (asset) {
|
|
|
+ // video.media = asset
|
|
|
+ // }
|
|
|
+ // }
|
|
|
|
|
|
- public getValue(): T | undefined {
|
|
|
- return this.type === 'change' ? this.value : undefined
|
|
|
+ // prepare language if needed
|
|
|
+ if (isSet(meta.language)) {
|
|
|
+ video.language = await processLanguage(ctx, video.language, meta.language)
|
|
|
}
|
|
|
|
|
|
- /*
|
|
|
- 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
|
|
|
+ if (isSet(meta.publishedBeforeJoystream)) {
|
|
|
+ video.publishedBeforeJoystream = processPublishedBeforeJoystream(
|
|
|
+ ctx,
|
|
|
+ video.publishedBeforeJoystream,
|
|
|
+ meta.publishedBeforeJoystream
|
|
|
+ )
|
|
|
}
|
|
|
-}
|
|
|
|
|
|
-export interface RawVideoMetadata {
|
|
|
- encoding: {
|
|
|
- codecName: PropertyChange<string>
|
|
|
- container: PropertyChange<string>
|
|
|
- mimeMediaType: PropertyChange<string>
|
|
|
- }
|
|
|
- pixelWidth: PropertyChange<number>
|
|
|
- pixelHeight: PropertyChange<number>
|
|
|
- size: PropertyChange<number>
|
|
|
+ return video
|
|
|
}
|
|
|
|
|
|
-/*
|
|
|
- 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<T extends ChannelCategory | VideoCategory>(
|
|
|
- type: T,
|
|
|
- parameters: IReadProtobufArguments
|
|
|
-): Promise<Partial<T>> {
|
|
|
- // 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<ChannelCategoryMetadata.AsObject>(meta) as Partial<T>
|
|
|
-
|
|
|
- return result
|
|
|
- }
|
|
|
-
|
|
|
- // process video category
|
|
|
- if (type instanceof VideoCategory) {
|
|
|
- const meta = VideoCategoryMetadata.deserializeBinary(metaU8a)
|
|
|
- const result = convertMetadataToObject<VideoCategoryMetadata.AsObject>(meta) as Partial<T>
|
|
|
+function findAssetByIndex(assets: typeof Asset[], index: number, name?: string): typeof Asset | null {
|
|
|
+ if (assets[index]) {
|
|
|
+ return assets[index]
|
|
|
+ } else {
|
|
|
+ invalidMetadata(`Invalid${name ? ' ' + name : ''} asset index`, {
|
|
|
+ numberOfAssets: assets.length,
|
|
|
+ requestedAssetIndex: index,
|
|
|
+ })
|
|
|
|
|
|
- return result
|
|
|
+ return null
|
|
|
}
|
|
|
-
|
|
|
- // 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<T extends Channel | Video>(
|
|
|
- type: T,
|
|
|
- parameters: IReadProtobufArgumentsWithAssets
|
|
|
-): Promise<Partial<T>> {
|
|
|
- // 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<ChannelMetadata.AsObject>(meta)
|
|
|
- const result = (metaAsObject as any) as Partial<Channel>
|
|
|
-
|
|
|
- // 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<T>
|
|
|
- }
|
|
|
-
|
|
|
- // process video
|
|
|
- if (type instanceof Video) {
|
|
|
- const meta = VideoMetadata.deserializeBinary(metaU8a)
|
|
|
- const metaAsObject = convertMetadataToObject<VideoMetadata.AsObject>(meta)
|
|
|
- const result = (metaAsObject as any) as Partial<Video>
|
|
|
-
|
|
|
- // prepare video category if needed
|
|
|
- if ('category' in metaAsObject) {
|
|
|
- const category = await prepareVideoCategory(metaAsObject.category, parameters.db)
|
|
|
- delete metaAsObject.category // make sure temporary value will not interfere
|
|
|
- category.integrateInto(result, 'category')
|
|
|
- }
|
|
|
-
|
|
|
- // prepare media meta information if needed
|
|
|
- if ('mediaType' in metaAsObject || 'mediaPixelWidth' in metaAsObject || 'mediaPixelHeight' in metaAsObject) {
|
|
|
- // prepare video file size if poosible
|
|
|
- const videoSize = extractVideoSize(parameters.assets, metaAsObject.video)
|
|
|
-
|
|
|
- // NOTE: type hack - `RawVideoMetadata` is inserted instead of VideoMediaMetadata - it should be edited in `video.ts`
|
|
|
- // see `integrateVideoMetadata()` in `video.ts` for more info
|
|
|
- result.mediaMetadata = (prepareVideoMetadata(
|
|
|
- metaAsObject,
|
|
|
- videoSize,
|
|
|
- parameters.event.blockNumber
|
|
|
- ) as unknown) as VideoMediaMetadata
|
|
|
-
|
|
|
- // remove extra values
|
|
|
- delete metaAsObject.mediaType
|
|
|
- delete metaAsObject.mediaPixelWidth
|
|
|
- delete metaAsObject.mediaPixelHeight
|
|
|
- }
|
|
|
-
|
|
|
- // prepare license if needed
|
|
|
- if ('license' in metaAsObject) {
|
|
|
- result.license = await prepareLicense(parameters.db, metaAsObject.license, parameters.event)
|
|
|
- }
|
|
|
-
|
|
|
- // prepare thumbnail photo asset if needed
|
|
|
- if ('thumbnailPhoto' in metaAsObject) {
|
|
|
- const asset = await extractAsset({
|
|
|
- assetIndex: metaAsObject.thumbnailPhoto,
|
|
|
- assets: parameters.assets,
|
|
|
- db: parameters.db,
|
|
|
- event: parameters.event,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
- integrateAsset('thumbnailPhoto', result, asset) // changes `result` inline!
|
|
|
- delete metaAsObject.thumbnailPhoto
|
|
|
- }
|
|
|
-
|
|
|
- // prepare video asset if needed
|
|
|
- if ('video' in metaAsObject) {
|
|
|
- const asset = await extractAsset({
|
|
|
- assetIndex: metaAsObject.video,
|
|
|
- assets: parameters.assets,
|
|
|
- db: parameters.db,
|
|
|
- event: parameters.event,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
- integrateAsset('media', result, asset) // changes `result` inline!
|
|
|
- delete metaAsObject.video
|
|
|
- }
|
|
|
+async function processVideoMediaEncoding(
|
|
|
+ { store, event }: StoreContext & EventContext,
|
|
|
+ existingVideoMediaEncoding: VideoMediaEncoding | undefined,
|
|
|
+ metadata: DecodedMetadataObject<IMediaType>
|
|
|
+): Promise<VideoMediaEncoding> {
|
|
|
+ const encoding =
|
|
|
+ existingVideoMediaEncoding ||
|
|
|
+ new VideoMediaEncoding({
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ createdById: '1',
|
|
|
+ updatedById: '1',
|
|
|
+ })
|
|
|
+ // integrate media encoding-related data
|
|
|
+ integrateMeta(encoding, metadata, ['codecName', 'container', 'mimeMediaType'])
|
|
|
+ encoding.updatedAt = new Date(event.blockTimestamp)
|
|
|
+ await store.save<VideoMediaEncoding>(encoding)
|
|
|
|
|
|
- // 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 encoding
|
|
|
+}
|
|
|
|
|
|
- if (metaAsObject.publishedBeforeJoystream) {
|
|
|
- const publishedBeforeJoystream = handlePublishedBeforeJoystream(result, metaAsObject.publishedBeforeJoystream)
|
|
|
- delete metaAsObject.publishedBeforeJoystream // make sure temporary value will not interfere
|
|
|
- publishedBeforeJoystream.integrateInto(result, 'publishedBeforeJoystream')
|
|
|
- }
|
|
|
+async function processVideoMediaMetadata(
|
|
|
+ ctx: StoreContext & EventContext,
|
|
|
+ existingVideoMedia: VideoMediaMetadata | undefined,
|
|
|
+ metadata: DecodedMetadataObject<IVideoMetadata>,
|
|
|
+ videoSize: number | undefined
|
|
|
+): Promise<VideoMediaMetadata> {
|
|
|
+ const { store, event } = ctx
|
|
|
+ const videoMedia =
|
|
|
+ existingVideoMedia ||
|
|
|
+ new VideoMediaMetadata({
|
|
|
+ createdInBlock: event.blockNumber,
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ createdById: '1',
|
|
|
+ updatedById: '1',
|
|
|
+ })
|
|
|
|
|
|
- return result as Partial<T>
|
|
|
+ // integrate media-related data
|
|
|
+ const mediaMetadata = {
|
|
|
+ size: isSet(videoSize) ? new BN(videoSize.toString()) : undefined,
|
|
|
+ pixelWidth: metadata.mediaPixelWidth,
|
|
|
+ pixelHeight: metadata.mediaPixelHeight,
|
|
|
}
|
|
|
+ integrateMeta(videoMedia, mediaMetadata, ['pixelWidth', 'pixelHeight', 'size'])
|
|
|
+ videoMedia.updatedAt = new Date(event.blockTimestamp)
|
|
|
+ videoMedia.encoding = await processVideoMediaEncoding(ctx, videoMedia.encoding, metadata.mediaType || {})
|
|
|
+ await store.save<VideoMediaMetadata>(videoMedia)
|
|
|
|
|
|
- // this should never happen
|
|
|
- logger.error('Not implemented metadata type', { type })
|
|
|
- throw new Error(`Not implemented metadata type`)
|
|
|
+ return videoMedia
|
|
|
}
|
|
|
|
|
|
export async function convertContentActorToChannelOwner(
|
|
|
- db: DatabaseManager,
|
|
|
+ store: DatabaseManager,
|
|
|
contentActor: ContentActor
|
|
|
): Promise<{
|
|
|
ownerMember?: Membership
|
|
@@ -354,7 +214,7 @@ export async function convertContentActorToChannelOwner(
|
|
|
}> {
|
|
|
if (contentActor.isMember) {
|
|
|
const memberId = contentActor.asMember.toNumber()
|
|
|
- const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
|
|
|
+ const member = await store.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
|
|
|
|
|
|
// ensure member exists
|
|
|
if (!member) {
|
|
@@ -369,7 +229,7 @@ export async function convertContentActorToChannelOwner(
|
|
|
|
|
|
if (contentActor.isCurator) {
|
|
|
const curatorGroupId = contentActor.asCurator[0].toNumber()
|
|
|
- const curatorGroup = await db.get(CuratorGroup, {
|
|
|
+ const curatorGroup = await store.get(CuratorGroup, {
|
|
|
where: { id: curatorGroupId.toString() } as FindConditions<CuratorGroup>,
|
|
|
})
|
|
|
|
|
@@ -390,384 +250,217 @@ export async function convertContentActorToChannelOwner(
|
|
|
throw new Error('Not-implemented ContentActor type used')
|
|
|
}
|
|
|
|
|
|
-export function convertContentActorToDataObjectOwner(
|
|
|
- contentActor: ContentActor,
|
|
|
- channelId: number
|
|
|
-): typeof DataObjectOwner {
|
|
|
- const owner = new DataObjectOwnerChannel()
|
|
|
- owner.channel = channelId
|
|
|
-
|
|
|
- return owner
|
|
|
-
|
|
|
- /* contentActor is irrelevant now -> all video/channel content belongs to the channel
|
|
|
- if (contentActor.isMember) {
|
|
|
- const owner = new DataObjectOwnerMember()
|
|
|
- owner.member = contentActor.asMember.toBn()
|
|
|
-
|
|
|
- return owner
|
|
|
- }
|
|
|
-
|
|
|
- if (contentActor.isLead || contentActor.isCurator) {
|
|
|
- const owner = new DataObjectOwnerChannel()
|
|
|
- owner.channel = channelId
|
|
|
-
|
|
|
- return owner
|
|
|
+function processPublishedBeforeJoystream(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentValue: Date | undefined,
|
|
|
+ metadata: DecodedMetadataObject<IPublishedBeforeJoystream>
|
|
|
+): Date | undefined {
|
|
|
+ if (!isSet(metadata)) {
|
|
|
+ return currentValue
|
|
|
}
|
|
|
|
|
|
- logger.error('Not implemented ContentActor type', {contentActor: contentActor.toString()})
|
|
|
- throw new Error('Not-implemented ContentActor type used')
|
|
|
- */
|
|
|
-}
|
|
|
-
|
|
|
-function handlePublishedBeforeJoystream(
|
|
|
- video: Partial<Video>,
|
|
|
- metadata: PublishedBeforeJoystreamMetadata.AsObject
|
|
|
-): PropertyChange<Date> {
|
|
|
- // is publish being unset
|
|
|
- if ('isPublished' in metadata && !metadata.isPublished) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
+ // Property is beeing unset
|
|
|
+ if (!metadata.isPublished) {
|
|
|
+ return undefined
|
|
|
}
|
|
|
|
|
|
// try to parse timestamp from publish date
|
|
|
- const timestamp = metadata.date ? Date.parse(metadata.date) : NaN
|
|
|
+ const timestamp = isSet(metadata.date) ? Date.parse(metadata.date) : NaN
|
|
|
|
|
|
// ensure date is valid
|
|
|
if (isNaN(timestamp)) {
|
|
|
invalidMetadata(`Invalid date used for publishedBeforeJoystream`, {
|
|
|
timestamp,
|
|
|
})
|
|
|
- return PropertyChange.newNoChange()
|
|
|
+ return currentValue
|
|
|
}
|
|
|
|
|
|
// set new date
|
|
|
- return PropertyChange.newChange(new Date(timestamp))
|
|
|
-}
|
|
|
-
|
|
|
-interface IConvertAssetParameters {
|
|
|
- rawAsset: NewAsset
|
|
|
- db: DatabaseManager
|
|
|
- event: SubstrateEvent
|
|
|
- contentOwner: typeof DataObjectOwner
|
|
|
-}
|
|
|
-
|
|
|
-/*
|
|
|
- Converts event asset into data object or list of URLs fit to be saved to db.
|
|
|
-*/
|
|
|
-async function convertAsset(parameters: IConvertAssetParameters): Promise<AssetStorageOrUrls> {
|
|
|
- // is asset describing list of URLs?
|
|
|
- if (parameters.rawAsset.isUrls) {
|
|
|
- const urls = parameters.rawAsset.asUrls.toArray().map((item) => item.toString())
|
|
|
-
|
|
|
- return urls
|
|
|
- }
|
|
|
-
|
|
|
- // !parameters.rawAsset.isUrls && parameters.rawAsset.isUpload // asset is in storage
|
|
|
-
|
|
|
- // prepare data object
|
|
|
- const contentParameters: ContentParameters = parameters.rawAsset.asUpload
|
|
|
- const dataObject = await prepareDataObject(
|
|
|
- parameters.db,
|
|
|
- contentParameters,
|
|
|
- parameters.event,
|
|
|
- parameters.contentOwner
|
|
|
- )
|
|
|
-
|
|
|
- return dataObject
|
|
|
-}
|
|
|
-
|
|
|
-interface IExtractAssetParameters {
|
|
|
- assetIndex: number | undefined
|
|
|
- assets: NewAsset[]
|
|
|
- db: DatabaseManager
|
|
|
- event: SubstrateEvent
|
|
|
- contentOwner: typeof DataObjectOwner
|
|
|
-}
|
|
|
-
|
|
|
-/*
|
|
|
- Selects asset from provided set of assets and prepares asset data fit to be saved to db.
|
|
|
-*/
|
|
|
-async function extractAsset(parameters: IExtractAssetParameters): Promise<PropertyChange<AssetStorageOrUrls>> {
|
|
|
- // is asset being unset?
|
|
|
- if (parameters.assetIndex === undefined) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
- }
|
|
|
-
|
|
|
- // ensure asset index is valid
|
|
|
- if (parameters.assetIndex >= parameters.assets.length) {
|
|
|
- invalidMetadata(`Non-existing asset extraction requested`, {
|
|
|
- assetsProvided: parameters.assets.length,
|
|
|
- assetIndex: parameters.assetIndex,
|
|
|
- })
|
|
|
- return PropertyChange.newNoChange()
|
|
|
- }
|
|
|
-
|
|
|
- // convert asset to data object record
|
|
|
- const asset = await convertAsset({
|
|
|
- rawAsset: parameters.assets[parameters.assetIndex],
|
|
|
- db: parameters.db,
|
|
|
- event: parameters.event,
|
|
|
- contentOwner: parameters.contentOwner,
|
|
|
- })
|
|
|
-
|
|
|
- return PropertyChange.newChange(asset)
|
|
|
-}
|
|
|
-
|
|
|
-/*
|
|
|
- As a temporary messure to overcome yet-to-be-implemented features in Hydra, we are using redudant information
|
|
|
- to describe asset state. This function introduces all redudant data needed to be saved to db.
|
|
|
-
|
|
|
- Changes `result` argument!
|
|
|
-*/
|
|
|
-function integrateAsset<T>(propertyName: string, result: Object, asset: PropertyChange<AssetStorageOrUrls>): void {
|
|
|
- // helpers - property names
|
|
|
- const nameUrl = propertyName + 'Urls'
|
|
|
- const nameDataObject = propertyName + 'DataObject'
|
|
|
- const nameAvailability = propertyName + 'Availability'
|
|
|
-
|
|
|
- if (asset.isNoChange()) {
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- if (asset.isUnset()) {
|
|
|
- result[nameUrl] = []
|
|
|
- result[nameAvailability] = AssetAvailability.INVALID
|
|
|
- result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
|
|
|
-
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- const newValue = asset.getValue() as AssetStorageOrUrls
|
|
|
-
|
|
|
- // is asset available on external URL(s)
|
|
|
- if (!isAssetInStorage(newValue)) {
|
|
|
- // (un)set asset's properties
|
|
|
- result[nameUrl] = newValue
|
|
|
- result[nameAvailability] = AssetAvailability.ACCEPTED
|
|
|
- result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
|
|
|
-
|
|
|
- return
|
|
|
- }
|
|
|
-
|
|
|
- // asset saved in storage
|
|
|
-
|
|
|
- // prepare conversion table between liaison judgment and asset availability
|
|
|
- const conversionTable = {
|
|
|
- [LiaisonJudgement.ACCEPTED]: AssetAvailability.ACCEPTED,
|
|
|
- [LiaisonJudgement.PENDING]: AssetAvailability.PENDING,
|
|
|
- }
|
|
|
-
|
|
|
- // (un)set asset's properties
|
|
|
- result[nameUrl] = [] // plan deletion (will have effect when saved to db)
|
|
|
- result[nameAvailability] = conversionTable[newValue.liaisonJudgement]
|
|
|
- result[nameDataObject] = newValue
|
|
|
+ return new Date(timestamp)
|
|
|
}
|
|
|
|
|
|
-function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): number | undefined {
|
|
|
- // escape if no asset is required
|
|
|
- if (assetIndex === undefined) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- // ensure asset index is valid
|
|
|
- if (assetIndex > assets.length) {
|
|
|
- invalidMetadata(`Non-existing asset video size extraction requested`, { assetsProvided: assets.length, assetIndex })
|
|
|
- return undefined
|
|
|
- }
|
|
|
-
|
|
|
- const rawAsset = assets[assetIndex]
|
|
|
-
|
|
|
- // escape if asset is describing URLs (can't get size)
|
|
|
- if (rawAsset.isUrls) {
|
|
|
- return undefined
|
|
|
+// TODO: Assets processing (Storage v2)
|
|
|
+// async function processNewAsset(
|
|
|
+// ctx: EventContext & StoreContext,
|
|
|
+// asset: NewAsset,
|
|
|
+// owner: typeof DataObjectOwner
|
|
|
+// ): Promise<typeof Asset> {
|
|
|
+// if (asset.isUrls) {
|
|
|
+// const urls = asset.asUrls.toArray().map((url) => url.toString())
|
|
|
+// const resultAsset = new AssetExternal()
|
|
|
+// resultAsset.urls = JSON.stringify(urls)
|
|
|
+// return resultAsset
|
|
|
+// } else if (asset.isUpload) {
|
|
|
+// const contentParameters: ContentParameters = asset.asUpload
|
|
|
+// const dataObject = await createDataObject(ctx, contentParameters, owner)
|
|
|
+
|
|
|
+// const resultAsset = new AssetJoystreamStorage()
|
|
|
+// resultAsset.dataObjectId = dataObject.id
|
|
|
+// return resultAsset
|
|
|
+// } else {
|
|
|
+// unexpectedData('Unrecognized asset type', asset.type)
|
|
|
+// }
|
|
|
+// }
|
|
|
+
|
|
|
+// function extractVideoSize(assets: NewAsset[], assetIndex: number | null | undefined): number | undefined {
|
|
|
+// // escape if no asset is required
|
|
|
+// if (!isSet(assetIndex)) {
|
|
|
+// return undefined
|
|
|
+// }
|
|
|
+
|
|
|
+// // ensure asset index is valid
|
|
|
+// if (assetIndex > assets.length) {
|
|
|
+// invalidMetadata(`Non-existing asset video size extraction requested`, { assetsProvided: assets.length, assetIndex })
|
|
|
+// return undefined
|
|
|
+// }
|
|
|
+
|
|
|
+// const rawAsset = assets[assetIndex]
|
|
|
+
|
|
|
+// // escape if asset is describing URLs (can't get size)
|
|
|
+// if (rawAsset.isUrls) {
|
|
|
+// return undefined
|
|
|
+// }
|
|
|
+
|
|
|
+// // !rawAsset.isUrls && rawAsset.isUpload // asset is in storage
|
|
|
+
|
|
|
+// // convert generic content parameters coming from processor to custom Joystream data type
|
|
|
+// const customContentParameters = new Custom_ContentParameters(registry, rawAsset.asUpload.toJSON() as any)
|
|
|
+// // extract video size
|
|
|
+// const videoSize = customContentParameters.size_in_bytes.toNumber()
|
|
|
+
|
|
|
+// return videoSize
|
|
|
+// }
|
|
|
+
|
|
|
+async function processLanguage(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentLanguage: Language | undefined,
|
|
|
+ languageIso: string | undefined
|
|
|
+): Promise<Language | undefined> {
|
|
|
+ const { event, store } = ctx
|
|
|
+
|
|
|
+ if (!isSet(languageIso)) {
|
|
|
+ return currentLanguage
|
|
|
}
|
|
|
|
|
|
- // !rawAsset.isUrls && rawAsset.isUpload // asset is in storage
|
|
|
-
|
|
|
- // convert generic content parameters coming from processor to custom Joystream data type
|
|
|
- const customContentParameters = new Custom_ContentParameters(registry, rawAsset.asUpload.toJSON() as any)
|
|
|
- // extract video size
|
|
|
- const videoSize = customContentParameters.size_in_bytes.toNumber()
|
|
|
-
|
|
|
- return videoSize
|
|
|
-}
|
|
|
-
|
|
|
-async function prepareLanguage(
|
|
|
- languageIso: string | undefined,
|
|
|
- db: DatabaseManager,
|
|
|
- event: SubstrateEvent
|
|
|
-): Promise<PropertyChange<Language>> {
|
|
|
- // is language being unset?
|
|
|
- if (languageIso === undefined) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
- }
|
|
|
-
|
|
|
- // validate language string
|
|
|
- const isValidIso = ISO6391.validate(languageIso)
|
|
|
-
|
|
|
// ensure language string is valid
|
|
|
- if (!isValidIso) {
|
|
|
+ if (!isValidLanguageCode(languageIso)) {
|
|
|
invalidMetadata(`Invalid language ISO-639-1 provided`, languageIso)
|
|
|
- return PropertyChange.newNoChange()
|
|
|
+ return currentLanguage
|
|
|
}
|
|
|
|
|
|
// load language
|
|
|
- const language = await db.get(Language, { where: { iso: languageIso } as FindConditions<Language> })
|
|
|
+ const existingLanguage = await store.get(Language, { where: { iso: languageIso } })
|
|
|
|
|
|
// return existing language if any
|
|
|
- if (language) {
|
|
|
- return PropertyChange.newChange(language)
|
|
|
+ if (existingLanguage) {
|
|
|
+ return existingLanguage
|
|
|
}
|
|
|
|
|
|
// create new language
|
|
|
const newLanguage = new Language({
|
|
|
- // set id as iso to overcome current graphql filtering limitations (so we can use query `videos(where: {languageId_eq: 'en'})`)
|
|
|
- // id: await getNextId(db),
|
|
|
- id: languageIso,
|
|
|
iso: languageIso,
|
|
|
createdInBlock: event.blockNumber,
|
|
|
-
|
|
|
- createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
|
|
|
- updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
|
|
|
-
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ updatedAt: new Date(event.blockTimestamp),
|
|
|
// TODO: remove these lines after Hydra auto-fills the values when cascading save (remove them on all places)
|
|
|
createdById: '1',
|
|
|
updatedById: '1',
|
|
|
})
|
|
|
|
|
|
- await db.save<Language>(newLanguage)
|
|
|
+ await store.save<Language>(newLanguage)
|
|
|
|
|
|
- return PropertyChange.newChange(newLanguage)
|
|
|
+ return newLanguage
|
|
|
}
|
|
|
|
|
|
-async function prepareLicense(
|
|
|
- db: DatabaseManager,
|
|
|
- licenseProtobuf: LicenseMetadata.AsObject | undefined,
|
|
|
- event: SubstrateEvent
|
|
|
-): Promise<License | undefined> {
|
|
|
- // NOTE: Deletion of any previous license should take place in appropriate event handling function
|
|
|
- // and not here even it might appear so.
|
|
|
-
|
|
|
- // is license being unset?
|
|
|
- if (licenseProtobuf === undefined) {
|
|
|
- return undefined
|
|
|
- }
|
|
|
+async function updateVideoLicense(
|
|
|
+ ctx: StoreContext & EventContext,
|
|
|
+ video: Video,
|
|
|
+ licenseMetadata: ILicense | null | undefined
|
|
|
+): Promise<void> {
|
|
|
+ const { store, event } = ctx
|
|
|
|
|
|
- // license is meant to be deleted
|
|
|
- if (isLicenseEmpty(licenseProtobuf)) {
|
|
|
- return new License({})
|
|
|
+ if (!isSet(licenseMetadata)) {
|
|
|
+ return
|
|
|
}
|
|
|
|
|
|
- // crete new license
|
|
|
- const license = new License({
|
|
|
- ...licenseProtobuf,
|
|
|
- id: await getNextId(db),
|
|
|
+ const previousLicense = video.license
|
|
|
+ let license: License | null = null
|
|
|
|
|
|
- createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
|
|
|
- updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
|
|
|
+ if (!isLicenseEmpty(licenseMetadata)) {
|
|
|
+ // license is meant to be created/updated
|
|
|
+ license =
|
|
|
+ previousLicense ||
|
|
|
+ new License({
|
|
|
+ createdAt: new Date(event.blockTimestamp),
|
|
|
+ createdById: '1',
|
|
|
+ updatedById: '1',
|
|
|
+ })
|
|
|
+ license.updatedAt = new Date(event.blockTimestamp)
|
|
|
+ integrateMeta(license, licenseMetadata, ['attribution', 'code', 'customText'])
|
|
|
+ await store.save<License>(license)
|
|
|
+ }
|
|
|
|
|
|
- createdById: '1',
|
|
|
- updatedById: '1',
|
|
|
- })
|
|
|
+ // Update license (and potentially remove foreign key reference)
|
|
|
+ // FIXME: Note that we MUST to provide "null" here in order to unset a relation,
|
|
|
+ // See: https://github.com/Joystream/hydra/issues/435
|
|
|
+ video.license = license as License | undefined
|
|
|
+ video.updatedAt = new Date(ctx.event.blockTimestamp)
|
|
|
+ await store.save<Video>(video)
|
|
|
|
|
|
- return license
|
|
|
+ // Safely remove previous license if needed
|
|
|
+ if (previousLicense && !license) {
|
|
|
+ await store.remove<License>(previousLicense)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
Checks if protobof contains license with some fields filled or is empty object (`{}` or `{someKey: undefined, ...}`).
|
|
|
Empty object means deletion is requested.
|
|
|
*/
|
|
|
-function isLicenseEmpty(licenseObject: LicenseMetadata.AsObject): boolean {
|
|
|
- const somePropertySet = Object.entries(licenseObject).reduce((acc, [key, value]) => {
|
|
|
- return acc || value !== undefined
|
|
|
- }, false)
|
|
|
+function isLicenseEmpty(licenseObject: ILicense): boolean {
|
|
|
+ const somePropertySet = Object.values(licenseObject).some((v) => isSet(v))
|
|
|
|
|
|
return !somePropertySet
|
|
|
}
|
|
|
|
|
|
-function prepareVideoMetadata(
|
|
|
- videoProtobuf: VideoMetadata.AsObject,
|
|
|
- videoSize: number | undefined,
|
|
|
- blockNumber: number
|
|
|
-): RawVideoMetadata {
|
|
|
- const rawMeta = {
|
|
|
- encoding: {
|
|
|
- codecName: PropertyChange.fromObjectProperty<string, 'codecName', MediaTypeMetadata.AsObject>(
|
|
|
- videoProtobuf.mediaType || {},
|
|
|
- 'codecName'
|
|
|
- ),
|
|
|
- container: PropertyChange.fromObjectProperty<string, 'container', MediaTypeMetadata.AsObject>(
|
|
|
- videoProtobuf.mediaType || {},
|
|
|
- 'container'
|
|
|
- ),
|
|
|
- mimeMediaType: PropertyChange.fromObjectProperty<string, 'mimeMediaType', MediaTypeMetadata.AsObject>(
|
|
|
- videoProtobuf.mediaType || {},
|
|
|
- 'mimeMediaType'
|
|
|
- ),
|
|
|
- },
|
|
|
- pixelWidth: PropertyChange.fromObjectProperty<number, 'mediaPixelWidth', VideoMetadata.AsObject>(
|
|
|
- videoProtobuf,
|
|
|
- 'mediaPixelWidth'
|
|
|
- ),
|
|
|
- pixelHeight: PropertyChange.fromObjectProperty<number, 'mediaPixelHeight', VideoMetadata.AsObject>(
|
|
|
- videoProtobuf,
|
|
|
- 'mediaPixelHeight'
|
|
|
- ),
|
|
|
- size: videoSize === undefined ? PropertyChange.newNoChange() : PropertyChange.newChange(videoSize),
|
|
|
- } as RawVideoMetadata
|
|
|
-
|
|
|
- return rawMeta
|
|
|
-}
|
|
|
-
|
|
|
-async function prepareVideoCategory(
|
|
|
- categoryId: number | undefined,
|
|
|
- db: DatabaseManager
|
|
|
-): Promise<PropertyChange<VideoCategory>> {
|
|
|
- // is category being unset?
|
|
|
- if (categoryId === undefined) {
|
|
|
- return PropertyChange.newUnset()
|
|
|
- }
|
|
|
+async function processVideoCategory(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentCategory: VideoCategory | undefined,
|
|
|
+ categoryId: number
|
|
|
+): Promise<VideoCategory | undefined> {
|
|
|
+ const { store } = ctx
|
|
|
|
|
|
// load video category
|
|
|
- const category = await db.get(VideoCategory, {
|
|
|
- where: { id: categoryId.toString() } as FindConditions<VideoCategory>,
|
|
|
+ const category = await store.get(VideoCategory, {
|
|
|
+ where: { id: categoryId.toString() },
|
|
|
})
|
|
|
|
|
|
// ensure video category exists
|
|
|
if (!category) {
|
|
|
invalidMetadata('Non-existing video category association with video requested', categoryId)
|
|
|
- return PropertyChange.newNoChange()
|
|
|
+ return currentCategory
|
|
|
}
|
|
|
|
|
|
- return PropertyChange.newChange(category)
|
|
|
+ return category
|
|
|
}
|
|
|
|
|
|
-function convertMetadataToObject<T extends Object>(metadata: jspb.Message): T {
|
|
|
- const metaAsObject = metadata.toObject()
|
|
|
- const result = {} as T
|
|
|
-
|
|
|
- for (const key in metaAsObject) {
|
|
|
- const funcNameBase = key.charAt(0).toUpperCase() + key.slice(1)
|
|
|
- const hasFuncName = 'has' + funcNameBase
|
|
|
- const isSet =
|
|
|
- funcNameBase === 'PersonsList' // there is no `VideoMetadata.hasPersonsList` method from unkown reason -> create exception
|
|
|
- ? true
|
|
|
- : metadata[hasFuncName]()
|
|
|
-
|
|
|
- if (!isSet) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- const getFuncName = 'get' + funcNameBase
|
|
|
- const value = metadata[getFuncName]()
|
|
|
+async function processChannelCategory(
|
|
|
+ ctx: EventContext & StoreContext,
|
|
|
+ currentCategory: ChannelCategory | undefined,
|
|
|
+ categoryId: number
|
|
|
+): Promise<ChannelCategory | undefined> {
|
|
|
+ const { store } = ctx
|
|
|
|
|
|
- // TODO: check that recursion trully works
|
|
|
- if (value instanceof jspb.Message) {
|
|
|
- result[key] = convertMetadataToObject(value)
|
|
|
- continue
|
|
|
- }
|
|
|
+ // load video category
|
|
|
+ const category = await store.get(ChannelCategory, {
|
|
|
+ where: { id: categoryId.toString() },
|
|
|
+ })
|
|
|
|
|
|
- result[key] = metaAsObject[key]
|
|
|
+ // ensure video category exists
|
|
|
+ if (!category) {
|
|
|
+ invalidMetadata('Non-existing channel category association with channel requested', categoryId)
|
|
|
+ return currentCategory
|
|
|
}
|
|
|
|
|
|
- return result
|
|
|
+ return category
|
|
|
}
|