Browse Source

Giza mappings: Storage<->Content integration

Leszek Wiesner 3 years ago
parent
commit
d100462f34

+ 18 - 37
query-node/mappings/content/channel.ts

@@ -2,19 +2,21 @@
 eslint-disable @typescript-eslint/naming-convention
 */
 import { EventContext, StoreContext } from '@joystream/hydra-common'
-import { AccountId } from '@polkadot/types/interfaces'
-import { Option } from '@polkadot/types/codec'
 import { Content } from '../generated/types'
 import { convertContentActorToChannelOwner, processChannelMetadata } from './utils'
-import { AssetNone, Channel, ChannelCategory } from 'query-node/dist/model'
+import { AssetNone, Channel, ChannelCategory, StorageDataObject } from 'query-node/dist/model'
 import { deserializeMetadata, inconsistentState, logger } from '../common'
 import { ChannelCategoryMetadata, ChannelMetadata } from '@joystream/metadata-protobuf'
 import { integrateMeta } from '@joystream/metadata-protobuf/utils'
+import { In } from 'typeorm'
+import { removeDataObject } from '../storage/utils'
 
 export async function content_ChannelCreated(ctx: EventContext & StoreContext): Promise<void> {
   const { store, event } = ctx
   // read event data
-  const [contentActor, channelId, , channelCreationParameters] = new Content.ChannelCreatedEvent(event).params
+  const [contentActor, channelId, runtimeChannel, channelCreationParameters] = new Content.ChannelCreatedEvent(
+    event
+  ).params
 
   // create entity
   const channel = new Channel({
@@ -23,6 +25,8 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext):
     isCensored: false,
     videos: [],
     createdInBlock: event.blockNumber,
+    rewardAccount: channelCreationParameters.reward_account.unwrapOr(undefined)?.toString(),
+    deletionPrizeDestAccount: runtimeChannel.deletion_prize_source_account_id.toString(),
     // assets
     coverPhoto: new AssetNone(),
     avatarPhoto: new AssetNone(),
@@ -63,7 +67,7 @@ export async function content_ChannelUpdated(ctx: EventContext & StoreContext):
   //  update metadata if it was changed
   if (newMetadataBytes) {
     const newMetadata = deserializeMetadata(ChannelMetadata, newMetadataBytes) || {}
-    await processChannelMetadata(ctx, channel, newMetadata, channelUpdateParameters.assets.unwrapOr([]))
+    await processChannelMetadata(ctx, channel, newMetadata, channelUpdateParameters.assets.unwrapOr(undefined))
   }
 
   // prepare changed reward account
@@ -72,7 +76,7 @@ export async function content_ChannelUpdated(ctx: EventContext & StoreContext):
   // reward account change happened?
   if (newRewardAccount) {
     // this will change the `channel`!
-    handleChannelRewardAccountChange(channel, newRewardAccount)
+    channel.rewardAccount = newRewardAccount.unwrapOr(undefined)?.toString()
   }
 
   // set last update time
@@ -86,18 +90,14 @@ export async function content_ChannelUpdated(ctx: EventContext & StoreContext):
 }
 
 export async function content_ChannelAssetsRemoved({ store, event }: EventContext & StoreContext): Promise<void> {
-  // TODO: Storage v2 integration
-  // // read event data
-  // const [, , contentIds] = new Content.ChannelAssetsRemovedEvent(event).params
-  // const assets = await store.getMany(StorageDataObject, {
-  //   where: {
-  //     id: In(contentIds.toArray().map((item) => item.toString())),
-  //   },
-  // })
-  // // delete assets
-  // await Promise.all(assets.map((a) => store.remove<StorageDataObject>(a)))
-  // // emit log event
-  // logger.info('Channel assets have been removed', { ids: contentIds })
+  const [, , dataObjectIds] = new Content.ChannelAssetsRemovedEvent(event).params
+  const assets = await store.getMany(StorageDataObject, {
+    where: {
+      id: In(Array.from(dataObjectIds).map((item) => item.toString())),
+    },
+  })
+  await Promise.all(assets.map((a) => removeDataObject(store, a)))
+  logger.info('Channel assets have been removed', { ids: dataObjectIds.toJSON() })
 }
 
 export async function content_ChannelCensorshipStatusUpdated({
@@ -209,22 +209,3 @@ export async function content_ChannelCategoryDeleted({ store, event }: EventCont
   // emit log event
   logger.info('Channel category has been deleted', { id: channelCategory.id })
 }
-
-/// //////////////// Helpers ////////////////////////////////////////////////////
-
-function handleChannelRewardAccountChange(
-  channel: Channel, // will be modified inside of the function!
-  reward_account: Option<AccountId>
-) {
-  const rewardAccount = reward_account.unwrapOr(null)
-
-  // new different reward account set?
-  if (rewardAccount) {
-    channel.rewardAccount = rewardAccount.toString()
-    return
-  }
-
-  // reward account removed
-
-  channel.rewardAccount = undefined // plan deletion (will have effect when saved to db)
-}

+ 153 - 98
query-node/mappings/content/utils.ts

@@ -1,5 +1,5 @@
 import { DatabaseManager, EventContext, StoreContext } from '@joystream/hydra-common'
-import { FindConditions } from 'typeorm'
+import { FindConditions, Raw } from 'typeorm'
 import {
   IVideoMetadata,
   IPublishedBeforeJoystream,
@@ -8,7 +8,7 @@ import {
   IChannelMetadata,
 } from '@joystream/metadata-protobuf'
 import { integrateMeta, isSet, isValidLanguageCode } from '@joystream/metadata-protobuf/utils'
-import { invalidMetadata, inconsistentState, logger } from '../common'
+import { invalidMetadata, inconsistentState, unexpectedData, logger } from '../common'
 import {
   // primary entities
   CuratorGroup,
@@ -25,23 +25,25 @@ import {
   VideoMediaEncoding,
   ChannelCategory,
   AssetNone,
+  AssetExternal,
+  AssetJoystreamStorage,
+  StorageDataObject,
 } from 'query-node/dist/model'
 // Joystream types
-import { NewAsset, ContentActor } from '@joystream/types/augment'
+import { NewAssets, ContentActor } from '@joystream/types/augment'
 import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types'
 import BN from 'bn.js'
+import { getMostRecentlyCreatedDataObjects } from '../storage/utils'
+import { DataObjectCreationParameters as ObjectCreationParams } from '@joystream/types/storage'
+import { registry } from '@joystream/types'
 
 export async function processChannelMetadata(
   ctx: EventContext & StoreContext,
   channel: Channel,
   meta: DecodedMetadataObject<IChannelMetadata>,
-  assets: NewAsset[]
+  assets?: NewAssets
 ): Promise<Channel> {
-  // TODO: Assets processing (Storage v2)
-  // const assetsOwner = new DataObjectOwnerChannel()
-  // assetsOwner.channelId = channel.id
-
-  // const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
+  const processedAssets = assets ? await processNewAssets(ctx, assets) : []
 
   integrateMeta(channel, meta, ['title', 'description', 'isPublic'])
 
@@ -52,21 +54,21 @@ export async function processChannelMetadata(
 
   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
-  //   }
-  // }
-
-  // // prepare avatar photo asset if needed
-  // if (isSet(meta.avatarPhoto)) {
-  //   const asset = findAssetByIndex(processedAssets, meta.avatarPhoto, 'channel avatar photo')
-  //   if (asset) {
-  //     channel.avatarPhoto = asset
-  //   }
-  // }
+  // prepare cover photo asset if needed
+  if (isSet(meta.coverPhoto)) {
+    const asset = findAssetByIndex(processedAssets, meta.coverPhoto, 'channel cover photo')
+    if (asset) {
+      channel.coverPhoto = asset
+    }
+  }
+
+  // prepare avatar photo asset if needed
+  if (isSet(meta.avatarPhoto)) {
+    const asset = findAssetByIndex(processedAssets, meta.avatarPhoto, 'channel avatar photo')
+    if (asset) {
+      channel.avatarPhoto = asset
+    }
+  }
 
   // prepare language if needed
   if (isSet(meta.language)) {
@@ -78,16 +80,11 @@ export async function processChannelMetadata(
 
 export async function processVideoMetadata(
   ctx: EventContext & StoreContext,
-  channel: Channel,
   video: Video,
   meta: DecodedMetadataObject<IVideoMetadata>,
-  assets: NewAsset[]
+  assets?: NewAssets
 ): Promise<Video> {
-  // TODO: Assets processing (Storage v2)
-  // const assetsOwner = new DataObjectOwnerChannel()
-  // assetsOwner.channelId = channel.id
-
-  // const processedAssets = await Promise.all(assets.map((asset) => processNewAsset(ctx, asset, assetsOwner)))
+  const processedAssets = assets ? await processNewAssets(ctx, assets) : []
 
   integrateMeta(video, meta, ['title', 'description', 'duration', 'hasMarketing', 'isExplicit', 'isPublic'])
 
@@ -99,7 +96,7 @@ export async function processVideoMetadata(
   // 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)
+    const videoSize = extractVideoSize(assets, meta.video)
     video.mediaMetadata = await processVideoMediaMetadata(ctx, video.mediaMetadata, meta, videoSize)
   }
 
@@ -110,21 +107,21 @@ export async function processVideoMetadata(
 
   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
-  //   }
-  // }
-
-  // // prepare video asset if needed
-  // if (isSet(meta.video)) {
-  //   const asset = findAssetByIndex(processedAssets, meta.video, 'video')
-  //   if (asset) {
-  //     video.media = asset
-  //   }
-  // }
+  // prepare thumbnail photo asset if needed
+  if (isSet(meta.thumbnailPhoto)) {
+    const asset = findAssetByIndex(processedAssets, meta.thumbnailPhoto, 'thumbnail photo')
+    if (asset) {
+      video.thumbnailPhoto = asset
+    }
+  }
+
+  // prepare video asset if needed
+  if (isSet(meta.video)) {
+    const asset = findAssetByIndex(processedAssets, meta.video, 'video')
+    if (asset) {
+      video.media = asset
+    }
+  }
 
   // prepare language if needed
   if (isSet(meta.language)) {
@@ -279,57 +276,64 @@ function processPublishedBeforeJoystream(
   return new Date(timestamp)
 }
 
-// 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 processNewAssets(ctx: EventContext & StoreContext, assets: NewAssets): Promise<Array<typeof Asset>> {
+  if (assets.isUrls) {
+    return assets.asUrls.map((assetUrls) => {
+      const resultAsset = new AssetExternal()
+      resultAsset.urls = JSON.stringify(assetUrls.map((u) => u.toString()))
+      return resultAsset
+    })
+  } else if (assets.isUpload) {
+    const assetsUploaded = assets.asUpload.object_creation_list.length
+    // FIXME: Ideally the runtime would provide object ids in ChannelCreated/VideoCreated/ChannelUpdated(...) events
+    const objects = await getMostRecentlyCreatedDataObjects(ctx.store, assetsUploaded)
+    return objects.map((o) => {
+      const resultAsset = new AssetJoystreamStorage()
+      resultAsset.dataObjectId = o.id
+      return resultAsset
+    })
+  } else {
+    unexpectedData('Unrecognized assets type', assets.type)
+  }
+}
+
+function extractVideoSize(assets: NewAssets | undefined, assetIndex: number | null | undefined): number | undefined {
+  // escape if no assetIndex is set
+  if (!isSet(assetIndex)) {
+    return undefined
+  }
+
+  // index provided, but there are no assets
+  if (!assets) {
+    invalidMetadata(`Non-existing asset video size extraction requested - no assets were uploaded!`, {
+      assetIndex,
+    })
+    return undefined
+  }
+
+  // cannot extract size from other asset types than "Upload"
+  if (!assets.isUpload) {
+    return undefined
+  }
+
+  const dataObjectsParams = assets.asUpload.object_creation_list
+
+  // ensure asset index is valid
+  if (assetIndex >= dataObjectsParams.length) {
+    invalidMetadata(`Non-existing asset video size extraction requested`, {
+      assetsProvided: dataObjectsParams.length,
+      assetIndex,
+    })
+    return undefined
+  }
+
+  // extract video size from objectParams
+  const objectParams = assets.asUpload.object_creation_list[assetIndex]
+  const params = new ObjectCreationParams(registry, objectParams.toJSON() as any)
+  const videoSize = params.getField('size').toNumber()
+
+  return videoSize
+}
 
 async function processLanguage(
   ctx: EventContext & StoreContext,
@@ -464,3 +468,54 @@ async function processChannelCategory(
 
   return category
 }
+
+// Needs to be done every time before data object is removed!
+export async function unsetAssetRelations(store: DatabaseManager, dataObject: StorageDataObject): Promise<void> {
+  const channelAssets = ['avatarPhoto', 'coverPhoto'] as const
+  const videoAssets = ['thumbnailPhoto', 'media'] as const
+
+  // NOTE: we don't need to retrieve multiple channels/videos via `store.getMany()` because dataObject
+  // is allowed to be associated only with one channel/video in runtime
+  const channel = await store.get(Channel, {
+    where: channelAssets.map((assetName) => ({
+      [assetName]: Raw((alias) => `${alias}::json->'dataObjectId' = :id`, {
+        id: dataObject.id,
+      }),
+    })),
+  })
+  const video = await store.get(Video, {
+    where: videoAssets.map((assetName) => ({
+      [assetName]: Raw((alias) => `${alias}::json->'dataObjectId' = :id`, {
+        id: dataObject.id,
+      }),
+    })),
+  })
+
+  if (channel) {
+    channelAssets.forEach((assetName) => {
+      if (channel[assetName] && (channel[assetName] as AssetJoystreamStorage).dataObjectId === dataObject.id) {
+        channel[assetName] = new AssetNone()
+      }
+    })
+    await store.save<Channel>(channel)
+
+    // emit log event
+    logger.info('Content has been disconnected from Channel', {
+      channelId: channel.id.toString(),
+      dataObjectId: dataObject.id,
+    })
+  } else if (video) {
+    videoAssets.forEach((assetName) => {
+      if (video[assetName] && (video[assetName] as AssetJoystreamStorage).dataObjectId === dataObject.id) {
+        video[assetName] = new AssetNone()
+      }
+    })
+    await store.save<Video>(video)
+
+    // emit log event
+    logger.info('Content has been disconnected from Video', {
+      videoId: video.id.toString(),
+      dataObjectId: dataObject.id,
+    })
+  }
+}

+ 2 - 2
query-node/mappings/content/video.ts

@@ -114,7 +114,7 @@ export async function content_VideoCreated(ctx: EventContext & StoreContext): Pr
   })
   // deserialize & process metadata
   const metadata = deserializeMetadata(VideoMetadata, videoCreationParameters.meta) || {}
-  await processVideoMetadata(ctx, channel, video, metadata, videoCreationParameters.assets)
+  await processVideoMetadata(ctx, video, metadata, videoCreationParameters.assets)
 
   // save video
   await store.save<Video>(video)
@@ -145,7 +145,7 @@ export async function content_VideoUpdated(ctx: EventContext & StoreContext): Pr
   // update metadata if it was changed
   if (newMetadataBytes) {
     const newMetadata = deserializeMetadata(VideoMetadata, newMetadataBytes) || {}
-    await processVideoMetadata(ctx, video.channel, video, newMetadata, videoUpdateParameters.assets.unwrapOr([]))
+    await processVideoMetadata(ctx, video, newMetadata, videoUpdateParameters.assets.unwrapOr(undefined))
   }
 
   // set last update time

+ 2 - 1
query-node/mappings/genesis-data/storageSystem.json

@@ -6,5 +6,6 @@
   "uploadingBlocked": false,
   "dataObjectFeePerMb": 0,
   "storageBucketMaxObjectsCountLimit": 0,
-  "storageBucketMaxObjectsSizeLimit": 0
+  "storageBucketMaxObjectsSizeLimit": 0,
+  "nextDataObjectId": 0
 }

+ 4 - 24
query-node/mappings/storage/index.ts

@@ -27,10 +27,8 @@ import {
   StorageBagStorageAssignment,
 } from 'query-node/dist/model'
 import BN from 'bn.js'
-import { getById, bytesToString } from '../common'
+import { getById } from '../common'
 import { BTreeSet } from '@polkadot/types'
-import { DataObjectCreationParameters } from '@joystream/types/storage'
-import { registry } from '@joystream/types'
 import { In } from 'typeorm'
 import _ from 'lodash'
 import { DataObjectId, BagId, DynamicBagId, StaticBagId } from '@joystream/types/augment/all'
@@ -39,6 +37,7 @@ import {
   processDistributionOperatorMetadata,
   processStorageOperatorMetadata,
 } from './metadata'
+import { createDataObjects, getStorageSystem, removeDataObject } from './utils'
 
 async function getDataObjectsInBag(
   store: DatabaseManager,
@@ -180,15 +179,6 @@ async function getDistributionBucketFamilyWithMetadata(
   return family
 }
 
-async function getStorageSystem(store: DatabaseManager) {
-  const storageSystem = await store.get(StorageSystemParameters, {})
-  if (!storageSystem) {
-    throw new Error('Storage system entity is missing!')
-  }
-
-  return storageSystem
-}
-
 // STORAGE BUCKETS
 
 export async function storage_StorageBucketCreated({ event, store }: EventContext & StoreContext): Promise<void> {
@@ -366,17 +356,7 @@ export async function storage_DataObjectsUploaded({ event, store }: EventContext
   const [dataObjectIds, uploadParams] = new Storage.DataObjectsUploadedEvent(event).params
   const { bagId, objectCreationList } = uploadParams
   const storageBag = await getBag(store, bagId)
-  const dataObjects = dataObjectIds.map((objectId, i) => {
-    const objectParams = new DataObjectCreationParameters(registry, objectCreationList[i].toJSON() as any)
-    return new StorageDataObject({
-      id: objectId.toString(),
-      isAccepted: false,
-      ipfsHash: bytesToString(objectParams.ipfsContentId),
-      size: new BN(objectParams.getField('size').toString()),
-      storageBag,
-    })
-  })
-  await Promise.all(dataObjects.map((o) => store.save<StorageDataObject>(o)))
+  await createDataObjects(store, objectCreationList, storageBag, dataObjectIds)
 }
 
 export async function storage_PendingDataObjectsAccepted({ event, store }: EventContext & StoreContext): Promise<void> {
@@ -405,7 +385,7 @@ export async function storage_DataObjectsMoved({ event, store }: EventContext &
 export async function storage_DataObjectsDeleted({ event, store }: EventContext & StoreContext): Promise<void> {
   const [, bagId, dataObjectIds] = new Storage.DataObjectsDeletedEvent(event).params
   const dataObjects = await getDataObjectsInBag(store, bagId, dataObjectIds)
-  await Promise.all(dataObjects.map((o) => store.remove<StorageDataObject>(o)))
+  await Promise.all(dataObjects.map((o) => removeDataObject(store, o)))
 }
 
 // DISTRIBUTION FAMILY

+ 71 - 0
query-node/mappings/storage/utils.ts

@@ -0,0 +1,71 @@
+import { DatabaseManager } from '@joystream/hydra-common'
+import { DataObjectCreationParameters } from '@joystream/types/augment'
+import { registry } from '@joystream/types'
+import { DataObjectCreationParameters as ObjectCreationParams } from '@joystream/types/storage'
+import { StorageBag, StorageDataObject, StorageSystemParameters } from 'query-node/dist/model'
+import BN from 'bn.js'
+import { bytesToString, inconsistentState } from '../common'
+import { In } from 'typeorm'
+import { unsetAssetRelations } from '../content/utils'
+
+export async function getStorageSystem(store: DatabaseManager): Promise<StorageSystemParameters> {
+  const storageSystem = await store.get(StorageSystemParameters, {})
+  if (!storageSystem) {
+    throw new Error('Storage system entity is missing!')
+  }
+
+  return storageSystem
+}
+
+export async function createDataObjects(
+  store: DatabaseManager,
+  objectsParams: DataObjectCreationParameters[],
+  storageBag: StorageBag,
+  objectIds?: BN[]
+): Promise<StorageDataObject[]> {
+  const storageSystem = await getStorageSystem(store)
+
+  const dataObjects = objectsParams.map((objectParams, i) => {
+    const params = new ObjectCreationParams(registry, objectParams.toJSON() as any)
+    const objectId = objectIds ? objectIds[i] : storageSystem.nextDataObjectId
+    const object = new StorageDataObject({
+      id: objectId.toString(),
+      isAccepted: false,
+      ipfsHash: bytesToString(objectParams.ipfsContentId),
+      size: new BN(params.getField('size').toString()),
+      storageBag,
+    })
+    if (objectId.gte(storageSystem.nextDataObjectId)) {
+      storageSystem.nextDataObjectId = objectId.addn(1)
+    }
+    return object
+  })
+
+  await Promise.all(dataObjects.map((o) => store.save<StorageDataObject>(o)))
+  await store.save<StorageSystemParameters>(storageSystem)
+
+  return dataObjects
+}
+
+export async function getMostRecentlyCreatedDataObjects(
+  store: DatabaseManager,
+  numberOfObjects: number
+): Promise<StorageDataObject[]> {
+  const storageSystem = await getStorageSystem(store)
+  const objectIds = Array.from({ length: numberOfObjects }, (v, k) =>
+    storageSystem.nextDataObjectId.subn(k + 1).toString()
+  )
+  const objects = await store.getMany(StorageDataObject, { where: { id: In(objectIds) } })
+  if (objects.length < numberOfObjects) {
+    inconsistentState(`Could not get ${numberOfObjects} most recently created data objects`, {
+      expected: numberOfObjects,
+      got: objects.length,
+    })
+  }
+  return objects.sort((a, b) => new BN(a.id).cmp(new BN(b.id)))
+}
+
+export async function removeDataObject(store: DatabaseManager, object: StorageDataObject): Promise<void> {
+  await unsetAssetRelations(store, object)
+  await store.save<StorageDataObject>(object)
+}

+ 10 - 4
query-node/schemas/content.graphql

@@ -53,17 +53,21 @@ type Channel @entity {
   "Reward account where revenue is sent if set."
   rewardAccount: String
 
+  "Destination account for the prize associated with channel deletion"
+  deletionPrizeDestAccount: String!
+
   "The title of the Channel"
   title: String @fulltext(query: "search")
 
   "The description of a Channel"
   description: String
 
+  # FIXME: Due to https://github.com/Joystream/hydra/issues/434, Asset is currently non-optional (use AssetNone to unset it)
   "Channel's cover (background) photo asset. Recommended ratio: 16:9."
-  coverPhoto: Asset
+  coverPhoto: Asset!
 
   "Channel's avatar photo asset."
-  avatarPhoto: Asset
+  avatarPhoto: Asset!
 
   ##########################
 
@@ -125,8 +129,9 @@ type Video @entity {
   "Video duration in seconds"
   duration: Int
 
+# FIXME: Due to https://github.com/Joystream/hydra/issues/434, Asset is currently non-optional (use AssetNone to unset it)
   "Video thumbnail asset (recommended ratio: 16:9)"
-  thumbnailPhoto: Asset
+  thumbnailPhoto: Asset!
 
   ##########################
 
@@ -151,8 +156,9 @@ type Video @entity {
   "License under the video is published"
   license: License
 
+  # FIXME: Due to https://github.com/Joystream/hydra/issues/434, Asset is currently non-optional (use AssetNone to unset it)
   "Video media asset"
-  media: Asset
+  media: Asset!
 
   ##########################
 

+ 3 - 0
query-node/schemas/storage.graphql

@@ -20,6 +20,9 @@ type StorageSystemParameters @entity {
 
   "Global max. size of objects a storage bucket can store (can also be further limitted the provider)"
   storageBucketMaxObjectsSizeLimit: BigInt!
+
+  "ID of the next data object when created"
+  nextDataObjectId: BigInt!
 }
 
 type StorageBucketOperatorStatusMissing @variant {