Browse Source

Support for new asset-related runtime changes, QueryNodeApi separation

Leszek Wiesner 3 years ago
parent
commit
0074ef9028

+ 9 - 1
cli/scripts/content-test.sh

@@ -9,7 +9,7 @@ echo "{}" > ~/tmp/empty.json
 export AUTO_CONFIRM=true
 
 # Init content lead
-GROUP=contentDirectoryWorkingGroup yarn workspace api-scripts initialize-content-lead
+yarn workspace api-scripts initialize-content-lead
 # Test create/update/remove category
 yarn joystream-cli content:createVideoCategory -i ./examples/content/CreateCategory.json
 yarn joystream-cli content:createVideoCategory -i ./examples/content/CreateCategory.json
@@ -52,3 +52,11 @@ yarn joystream-cli content:channels
 yarn joystream-cli content:channel 1
 yarn joystream-cli content:curatorGroups
 yarn joystream-cli content:curatorGroup 1
+# Remove videos/channels/assets
+yarn joystream-cli content:removeChannelAssets -c 1 -o 0
+yarn joystream-cli content:deleteVideo -v 1 -f
+yarn joystream-cli content:deleteVideo -v 2 -f
+yarn joystream-cli content:deleteVideo -v 3 -f
+yarn joystream-cli content:deleteChannel -c 1 -f
+yarn joystream-cli content:deleteChannel -c 2 -f
+yarn joystream-cli content:deleteChannel -c 3 -f

+ 8 - 130
cli/src/Api.ts

@@ -21,7 +21,6 @@ import {
   StakingPolicyUnstakingPeriodKey,
   UnaugmentedApiPromise,
   CouncilInfo,
-  StorageNodeInfo,
 } from './Types'
 import { DeriveBalancesAll } from '@polkadot/api-derive/types'
 import { CLIError } from '@oclif/errors'
@@ -41,26 +40,7 @@ import {
   ChannelCategoryId,
   VideoCategoryId,
 } from '@joystream/types/content'
-import {
-  ApolloClient,
-  InMemoryCache,
-  HttpLink,
-  NormalizedCacheObject,
-  DocumentNode,
-  from,
-  ApolloLink,
-} from '@apollo/client/core'
-import { ErrorLink, onError } from '@apollo/client/link/error'
-import { Maybe } from './graphql/generated/schema'
 import { Observable } from '@polkadot/x-rxjs'
-import {
-  GetStorageNodesInfoByBagId,
-  GetStorageNodesInfoByBagIdQuery,
-  GetStorageNodesInfoByBagIdQueryVariables,
-} from './graphql/generated/queries'
-import ExitCodes from './ExitCodes'
-import { URL } from 'url'
-import fetch from 'cross-fetch'
 import { BagId, DataObject, DataObjectId } from '@joystream/types/storage'
 
 export const DEFAULT_API_URI = 'ws://localhost:9944/'
@@ -77,17 +57,11 @@ export const apiModuleByGroup = {
 // Api wrapper for handling most common api calls and allowing easy API implementation switch in the future
 export default class Api {
   private _api: ApiPromise
-  private _queryNode?: ApolloClient<NormalizedCacheObject>
   public isDevelopment = false
 
-  private constructor(
-    originalApi: ApiPromise,
-    isDevelopment: boolean,
-    queryNodeClient?: ApolloClient<NormalizedCacheObject>
-  ) {
+  private constructor(originalApi: ApiPromise, isDevelopment: boolean) {
     this.isDevelopment = isDevelopment
     this._api = originalApi
-    this._queryNode = queryNodeClient
   }
 
   public getOriginalApi(): ApiPromise {
@@ -119,83 +93,9 @@ export default class Api {
     return { api, properties, chainType }
   }
 
-  private static async createQueryNodeClient(uri: string, errorHandler?: ErrorLink.ErrorHandler) {
-    const links: ApolloLink[] = []
-    if (errorHandler) {
-      links.push(onError(errorHandler))
-    }
-    links.push(new HttpLink({ uri, fetch }))
-    return new ApolloClient({
-      link: from(links),
-      cache: new InMemoryCache(),
-      defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
-    })
-  }
-
-  static async create(
-    apiUri = DEFAULT_API_URI,
-    metadataCache: Record<string, any>,
-    queryNodeUri?: string,
-    queryNodeErrorHandler?: ErrorLink.ErrorHandler
-  ): Promise<Api> {
+  static async create(apiUri = DEFAULT_API_URI, metadataCache: Record<string, any>): Promise<Api> {
     const { api, chainType } = await Api.initApi(apiUri, metadataCache)
-    const queryNodeClient = queryNodeUri
-      ? await this.createQueryNodeClient(queryNodeUri, queryNodeErrorHandler)
-      : undefined
-    return new Api(api, chainType.isDevelopment || chainType.isLocal, queryNodeClient)
-  }
-
-  // Query-node: get entity by unique input
-  protected async uniqueEntityQuery<
-    QueryT extends { [k: string]: Maybe<Record<string, unknown>> | undefined },
-    VariablesT extends Record<string, unknown>
-  >(
-    query: DocumentNode,
-    variables: VariablesT,
-    resultKey: keyof QueryT
-  ): Promise<Required<QueryT>[keyof QueryT] | null | undefined> {
-    if (!this._queryNode) {
-      return undefined
-    }
-    try {
-      return (await this._queryNode.query<QueryT, VariablesT>({ query, variables })).data[resultKey] || null
-    } catch (e) {
-      return undefined
-    }
-  }
-
-  // Query-node: get entities by "non-unique" input and return first result
-  protected async firstEntityQuery<
-    QueryT extends { [k: string]: unknown[] },
-    VariablesT extends Record<string, unknown>
-  >(
-    query: DocumentNode,
-    variables: VariablesT,
-    resultKey: keyof QueryT
-  ): Promise<QueryT[keyof QueryT][number] | null | undefined> {
-    if (!this._queryNode) {
-      return undefined
-    }
-    try {
-      return (await this._queryNode.query<QueryT, VariablesT>({ query, variables })).data[resultKey][0] || null
-    } catch (e) {
-      return undefined
-    }
-  }
-
-  // Query-node: get multiple entities
-  protected async multipleEntitiesQuery<
-    QueryT extends { [k: string]: unknown[] },
-    VariablesT extends Record<string, unknown>
-  >(query: DocumentNode, variables: VariablesT, resultKey: keyof QueryT): Promise<QueryT[keyof QueryT] | undefined> {
-    if (!this._queryNode) {
-      return undefined
-    }
-    try {
-      return (await this._queryNode.query<QueryT, VariablesT>({ query, variables })).data[resultKey]
-    } catch (e) {
-      return undefined
-    }
+    return new Api(api, chainType.isDevelopment || chainType.isLocal)
   }
 
   async bestNumber(): Promise<number> {
@@ -649,32 +549,10 @@ export default class Api {
     return (await this.entriesByIds(this._api.query.content.videoCategoryById)).map(([id]) => id)
   }
 
-  async storageNodesInfoByBagId(bagId: string): Promise<StorageNodeInfo[]> {
-    const result = await this.multipleEntitiesQuery<
-      GetStorageNodesInfoByBagIdQuery,
-      GetStorageNodesInfoByBagIdQueryVariables
-    >(GetStorageNodesInfoByBagId, { bagId }, 'storageBuckets')
-
-    if (!result) {
-      throw new CLIError('Could not fetch storage buckets information from the query node!', {
-        exit: ExitCodes.QueryNodeError,
-      })
-    }
-
-    const validNodesInfo: StorageNodeInfo[] = []
-    for (const { operatorMetadata, id } of result) {
-      if (operatorMetadata?.nodeEndpoint) {
-        try {
-          const validUrl = new URL(operatorMetadata.nodeEndpoint)
-          validNodesInfo.push({
-            apiEndpoint: validUrl.toString().endsWith('/') ? validUrl.toString() : validUrl.toString() + '/',
-            bucketId: parseInt(id),
-          })
-        } catch (e) {
-          continue
-        }
-      }
-    }
-    return validNodesInfo
+  async dataObjectsInBag(bagId: BagId): Promise<[DataObjectId, DataObject][]> {
+    return (await this._api.query.storage.dataObjectsById.entries(bagId)).map(([{ args: [, dataObjectId] }, value]) => [
+      dataObjectId,
+      value,
+    ])
   }
 }

+ 121 - 0
cli/src/QueryNodeApi.ts

@@ -0,0 +1,121 @@
+import { StorageNodeInfo } from './Types'
+import {
+  ApolloClient,
+  InMemoryCache,
+  HttpLink,
+  NormalizedCacheObject,
+  DocumentNode,
+  from,
+  ApolloLink,
+} from '@apollo/client/core'
+import { ErrorLink, onError } from '@apollo/client/link/error'
+import { Maybe } from './graphql/generated/schema'
+import {
+  GetStorageNodesInfoByBagId,
+  GetStorageNodesInfoByBagIdQuery,
+  GetStorageNodesInfoByBagIdQueryVariables,
+  DataObjectInfoFragment,
+  GetDataObjectsByBagId,
+  GetDataObjectsByBagIdQuery,
+  GetDataObjectsByBagIdQueryVariables,
+  GetDataObjectsByVideoId,
+  GetDataObjectsByVideoIdQuery,
+  GetDataObjectsByVideoIdQueryVariables,
+  GetDataObjectsChannelId,
+  GetDataObjectsChannelIdQuery,
+  GetDataObjectsChannelIdQueryVariables,
+} from './graphql/generated/queries'
+import { URL } from 'url'
+import fetch from 'cross-fetch'
+
+export default class QueryNodeApi {
+  private _qnClient: ApolloClient<NormalizedCacheObject>
+
+  public constructor(uri?: string, errorHandler?: ErrorLink.ErrorHandler) {
+    const links: ApolloLink[] = []
+    if (errorHandler) {
+      links.push(onError(errorHandler))
+    }
+    links.push(new HttpLink({ uri, fetch }))
+    this._qnClient = new ApolloClient({
+      link: from(links),
+      cache: new InMemoryCache(),
+      defaultOptions: { query: { fetchPolicy: 'no-cache', errorPolicy: 'all' } },
+    })
+  }
+
+  // Get entity by unique input
+  protected async uniqueEntityQuery<
+    QueryT extends { [k: string]: Maybe<Record<string, unknown>> | undefined },
+    VariablesT extends Record<string, unknown>
+  >(
+    query: DocumentNode,
+    variables: VariablesT,
+    resultKey: keyof QueryT
+  ): Promise<Required<QueryT>[keyof QueryT] | null> {
+    return (await this._qnClient.query<QueryT, VariablesT>({ query, variables })).data[resultKey] || null
+  }
+
+  // Get entities by "non-unique" input and return first result
+  protected async firstEntityQuery<
+    QueryT extends { [k: string]: unknown[] },
+    VariablesT extends Record<string, unknown>
+  >(query: DocumentNode, variables: VariablesT, resultKey: keyof QueryT): Promise<QueryT[keyof QueryT][number] | null> {
+    return (await this._qnClient.query<QueryT, VariablesT>({ query, variables })).data[resultKey][0] || null
+  }
+
+  // Get multiple entities
+  protected async multipleEntitiesQuery<
+    QueryT extends { [k: string]: unknown[] },
+    VariablesT extends Record<string, unknown>
+  >(query: DocumentNode, variables: VariablesT, resultKey: keyof QueryT): Promise<QueryT[keyof QueryT]> {
+    return (await this._qnClient.query<QueryT, VariablesT>({ query, variables })).data[resultKey]
+  }
+
+  async dataObjectsByBagId(bagId: string): Promise<DataObjectInfoFragment[]> {
+    return this.multipleEntitiesQuery<GetDataObjectsByBagIdQuery, GetDataObjectsByBagIdQueryVariables>(
+      GetDataObjectsByBagId,
+      { bagId },
+      'storageDataObjects'
+    )
+  }
+
+  async dataObjectsByVideoId(videoId: string): Promise<DataObjectInfoFragment[]> {
+    return this.multipleEntitiesQuery<GetDataObjectsByVideoIdQuery, GetDataObjectsByVideoIdQueryVariables>(
+      GetDataObjectsByVideoId,
+      { videoId },
+      'storageDataObjects'
+    )
+  }
+
+  async dataObjectsByChannelId(channelId: string): Promise<DataObjectInfoFragment[]> {
+    return this.multipleEntitiesQuery<GetDataObjectsChannelIdQuery, GetDataObjectsChannelIdQueryVariables>(
+      GetDataObjectsChannelId,
+      { channelId },
+      'storageDataObjects'
+    )
+  }
+
+  async storageNodesInfoByBagId(bagId: string): Promise<StorageNodeInfo[]> {
+    const result = await this.multipleEntitiesQuery<
+      GetStorageNodesInfoByBagIdQuery,
+      GetStorageNodesInfoByBagIdQueryVariables
+    >(GetStorageNodesInfoByBagId, { bagId }, 'storageBuckets')
+
+    const validNodesInfo: StorageNodeInfo[] = []
+    for (const { operatorMetadata, id } of result) {
+      if (operatorMetadata?.nodeEndpoint) {
+        try {
+          const validUrl = new URL(operatorMetadata.nodeEndpoint)
+          validNodesInfo.push({
+            apiEndpoint: validUrl.toString().endsWith('/') ? validUrl.toString() : validUrl.toString() + '/',
+            bucketId: parseInt(id),
+          })
+        } catch (e) {
+          continue
+        }
+      }
+    }
+    return validNodesInfo
+  }
+}

+ 36 - 11
cli/src/base/ApiCommandBase.ts

@@ -15,6 +15,7 @@ import { AugmentedSubmittables, SubmittableExtrinsic, AugmentedEvents, Augmented
 import { DistinctQuestion } from 'inquirer'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
 import { DispatchError } from '@polkadot/types/interfaces/system'
+import QueryNodeApi from '../QueryNodeApi'
 
 export class ExtrinsicFailedError extends Error {}
 
@@ -22,17 +23,32 @@ export class ExtrinsicFailedError extends Error {}
  * Abstract base class for commands that require access to the API.
  */
 export default abstract class ApiCommandBase extends StateAwareCommandBase {
-  private api: Api | null = null
+  private api: Api | undefined
+  private queryNodeApi: QueryNodeApi | null | undefined
 
   // Command configuration
   protected requiresApiConnection = true
   protected requiresQueryNode = false
 
   getApi(): Api {
-    if (!this.api) throw new CLIError('Tried to get API before initialization.', { exit: ExitCodes.ApiError })
+    if (!this.api) {
+      throw new CLIError('Tried to access API before initialization.', { exit: ExitCodes.ApiError })
+    }
     return this.api
   }
 
+  getQNApi(): QueryNodeApi {
+    if (this.queryNodeApi === undefined) {
+      throw new CLIError('Tried to access QueryNodeApi before initialization.', { exit: ExitCodes.QueryNodeError })
+    }
+    if (this.queryNodeApi === null) {
+      throw new CLIError('Query node endpoint uri is required in order to run this command!', {
+        exit: ExitCodes.QueryNodeError,
+      })
+    }
+    return this.queryNodeApi
+  }
+
   // Shortcuts
   getOriginalApi(): ApiPromise {
     return this.getApi().getOriginalApi()
@@ -50,6 +66,11 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return this.getOriginalApi().createType(typeName, value)
   }
 
+  isQueryNodeUriSet(): boolean {
+    const { queryNodeUri } = this.getPreservedState()
+    return !!queryNodeUri
+  }
+
   async init(): Promise<void> {
     await super.init()
     if (this.requiresApiConnection) {
@@ -60,20 +81,18 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         apiUri = await this.promptForApiUri()
       }
 
-      let queryNodeUri: string = this.getPreservedState().queryNodeUri
+      let queryNodeUri: string | null | undefined = this.getPreservedState().queryNodeUri
 
-      if (this.requiresQueryNode && (!queryNodeUri || queryNodeUri === 'none')) {
+      if (this.requiresQueryNode && !queryNodeUri) {
         this.warn('Query node endpoint uri is required in order to run this command!')
         queryNodeUri = await this.promptForQueryNodeUri(true)
-      } else if (!queryNodeUri) {
+      } else if (queryNodeUri === undefined) {
         this.warn("You haven't provided a Joystream query node uri for the CLI to connect to yet!")
         queryNodeUri = await this.promptForQueryNodeUri()
       }
 
       const { metadataCache } = this.getPreservedState()
-      this.api = await Api.create(apiUri, metadataCache, queryNodeUri === 'none' ? undefined : queryNodeUri, (err) => {
-        this.warn(`Query node error: ${err.networkError?.message || err.graphQLErrors?.join('\n')}`)
-      })
+      this.api = await Api.create(apiUri, metadataCache)
 
       const { genesisHash, runtimeVersion } = this.getOriginalApi()
       const metadataKey = `${genesisHash}-${runtimeVersion.specVersion}`
@@ -82,6 +101,12 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         metadataCache[metadataKey] = await this.getOriginalApi().runtimeMetadata.toJSON()
         await this.setPreservedState({ metadataCache })
       }
+
+      this.queryNodeApi = queryNodeUri
+        ? new QueryNodeApi(queryNodeUri, (err) => {
+            this.warn(`Query node error: ${err.networkError?.message || err.graphQLErrors?.join('\n')}`)
+          })
+        : null
     }
   }
 
@@ -122,7 +147,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return selectedNodeUri
   }
 
-  async promptForQueryNodeUri(isRequired = false): Promise<string> {
+  async promptForQueryNodeUri(isRequired = false): Promise<string | null> {
     const choices = [
       {
         name: 'Local query node (http://localhost:8081/graphql)',
@@ -143,7 +168,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
         value: 'none',
       })
     }
-    let selectedUri = await this.simplePrompt({
+    let selectedUri: string = await this.simplePrompt({
       type: 'list',
       message: 'Choose a query node endpoint:',
       choices,
@@ -163,7 +188,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
 
     await this.setPreservedState({ queryNodeUri: selectedUri })
 
-    return selectedUri
+    return selectedUri === 'none' ? null : selectedUri
   }
 
   isApiUriValid(uri: string): boolean {

+ 2 - 2
cli/src/base/StateAwareCommandBase.ts

@@ -12,7 +12,7 @@ import { WorkingGroups } from '../Types'
 type StateObject = {
   selectedAccountFilename: string
   apiUri: string
-  queryNodeUri: string
+  queryNodeUri: string | null | undefined
   defaultWorkingGroup: WorkingGroups
   metadataCache: Record<string, any>
 }
@@ -21,7 +21,7 @@ type StateObject = {
 const DEFAULT_STATE: StateObject = {
   selectedAccountFilename: '',
   apiUri: '',
-  queryNodeUri: '',
+  queryNodeUri: undefined,
   defaultWorkingGroup: WorkingGroups.StorageProviders,
   metadataCache: {},
 }

+ 6 - 8
cli/src/base/UploadCommandBase.ts

@@ -27,7 +27,7 @@ import { KeyringPair } from '@polkadot/keyring/types'
 import FormData from 'form-data'
 import BN from 'bn.js'
 import { createTypeFromConstructor } from '@joystream/types'
-import { NewAssets } from '@joystream/types/content'
+import { StorageAssets } from '@joystream/types/content'
 
 ffmpeg.setFfprobePath(ffprobeInstaller.path)
 
@@ -159,7 +159,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
 
   async getRandomActiveStorageNodeInfo(bagId: string, retryTime = 6, retryCount = 5): Promise<StorageNodeInfo | null> {
     for (let i = 0; i <= retryCount; ++i) {
-      const nodesInfo = _.shuffle(await this.getApi().storageNodesInfoByBagId(bagId))
+      const nodesInfo = _.shuffle(await this.getQNApi().storageNodesInfoByBagId(bagId))
       for (const info of nodesInfo) {
         try {
           // TODO: Use a status endpoint once available?
@@ -336,7 +336,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
     return originalPaths.map((path) => (filteredPaths.includes(path as string) ? ++lastIndex : undefined))
   }
 
-  async prepareAssetsForExtrinsic(resolvedAssets: ResolvedAsset[]): Promise<NewAssets | undefined> {
+  async prepareAssetsForExtrinsic(resolvedAssets: ResolvedAsset[]): Promise<StorageAssets | undefined> {
     const feePerMB = await this.getOriginalApi().query.storage.dataObjectPerMegabyteFee()
     if (resolvedAssets.length) {
       const totalBytes = resolvedAssets
@@ -349,11 +349,9 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
         `Total fee of ${chalk.cyan(formatBalance(totalFee))} ` +
           `will have to be paid in order to store the provided assets. Are you sure you want to continue?`
       )
-      return createTypeFromConstructor(NewAssets, {
-        Upload: {
-          expected_data_size_fee: feePerMB,
-          object_creation_list: resolvedAssets.map((a) => a.parameters),
-        },
+      return createTypeFromConstructor(StorageAssets, {
+        expected_data_size_fee: feePerMB,
+        object_creation_list: resolvedAssets.map((a) => a.parameters),
       })
     }
 

+ 2 - 2
cli/src/commands/api/getQueryNodeEndpoint.ts

@@ -5,7 +5,7 @@ export default class ApiGetQueryNodeEndpoint extends StateAwareCommandBase {
   static description = 'Get current query node endpoint'
 
   async run(): Promise<void> {
-    const currentEndpoint: string = this.getPreservedState().queryNodeUri
-    this.log(chalk.green(currentEndpoint))
+    const currentEndpoint: string | null | undefined = this.getPreservedState().queryNodeUri
+    this.log(chalk.green(JSON.stringify(currentEndpoint)))
   }
 }

+ 4 - 5
cli/src/commands/api/setQueryNodeEndpoint.ts

@@ -19,17 +19,16 @@ export default class ApiSetQueryNodeEndpoint extends ApiCommandBase {
   async run(): Promise<void> {
     const { endpoint }: ApiSetQueryNodeEndpointArgs = this.parse(ApiSetQueryNodeEndpoint)
       .args as ApiSetQueryNodeEndpointArgs
-    let newEndpoint = ''
+    let newEndpoint: string | null = null
     if (endpoint) {
-      if (this.isQueryNodeUriValid(endpoint)) {
-        await this.setPreservedState({ queryNodeUri: endpoint })
-        newEndpoint = endpoint
-      } else {
+      if (!this.isQueryNodeUriValid(endpoint)) {
         this.error('Provided endpoint seems to be incorrect!', { exit: ExitCodes.InvalidInput })
       }
+      newEndpoint = endpoint
     } else {
       newEndpoint = await this.promptForQueryNodeUri()
     }
+    await this.setPreservedState({ queryNodeUri: endpoint })
     this.log(
       chalk.greenBright('Query node endpoint successfuly changed! New endpoint: ') + chalk.magentaBright(newEndpoint)
     )

+ 0 - 1
cli/src/commands/content/channel.ts

@@ -27,7 +27,6 @@ export default class ChannelCommand extends ContentDirectoryCommandBase {
 
       displayCollapsedRow({
         'NumberOfVideos': channel.num_videos.toNumber(),
-        'NumberOfAssets': channel.num_assets.toNumber(),
       })
     } else {
       this.error(`Channel not found by channel id: "${channelId}"!`)

+ 74 - 11
cli/src/commands/content/deleteChannel.ts

@@ -1,9 +1,15 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import { flags } from '@oclif/command'
 import chalk from 'chalk'
+import { createTypeFromConstructor, registry } from '@joystream/types'
+import { BagId, DataObjectId } from '@joystream/types/storage'
+import ExitCodes from '../../ExitCodes'
+import { formatBalance } from '@polkadot/util'
+import { JoyBTreeSet } from '@joystream/types/common'
+import BN from 'bn.js'
 
 export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
-  static description = 'Delete the channel (it cannot have any associated assets and videos).'
+  static description = 'Delete the channel and optionally all associated data objects.'
 
   static flags = {
     channelId: flags.integer({
@@ -11,11 +17,46 @@ export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
       required: true,
       description: 'ID of the Channel',
     }),
+    force: flags.boolean({
+      char: 'f',
+      default: false,
+      description: 'Force-remove all associated channel data objects',
+    }),
+  }
+
+  async getDataObjectsInfoFromQueryNode(channelId: number): Promise<[string, BN][]> {
+    const dataObjects = await this.getQNApi().dataObjectsByBagId(`dynamic:channel:${channelId}`)
+
+    if (dataObjects.length) {
+      this.log('Following data objects are still associated with the channel:')
+      dataObjects.forEach((o) => {
+        let parentStr = ''
+        if ('video' in o.type && o.type.video) {
+          parentStr = ` (video: ${o.type.video.id})`
+        }
+        this.log(`- ${o.id} - ${o.type.__typename}${parentStr}`)
+      })
+    }
+
+    return dataObjects.map((o) => [o.id, new BN(o.deletionPrize)])
+  }
+
+  async getDataObjectsInfoFromChain(channelId: number): Promise<[string, BN][]> {
+    const dataObjects = await this.getApi().dataObjectsInBag(
+      createTypeFromConstructor(BagId, { Dynamic: { Channel: channelId } })
+    )
+
+    if (dataObjects.length) {
+      const dataObjectIds = dataObjects.map(([id]) => id.toString())
+      this.log(`Following data objects are still associated with the channel: ${dataObjectIds.join(', ')}`)
+    }
+
+    return dataObjects.map(([id, o]) => [id.toString(), o.deletion_prize])
   }
 
   async run(): Promise<void> {
     const {
-      flags: { channelId },
+      flags: { channelId, force },
     } = this.parse(DeleteChannelCommand)
     // Context
     const account = await this.getRequiredSelectedAccount()
@@ -23,13 +64,6 @@ export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
     const actor = await this.getChannelOwnerActor(channel)
     await this.requestAccountDecoding(account)
 
-    if (channel.num_assets.toNumber()) {
-      this.error(
-        `This channel still has ${channel.num_assets.toNumber()} associated asset(s)!\n` +
-          `Delete the assets first using ${chalk.magentaBright('content:removeChannelAssets')} command`
-      )
-    }
-
     if (channel.num_videos.toNumber()) {
       this.error(
         `This channel still has ${channel.num_videos.toNumber()} associated video(s)!\n` +
@@ -37,8 +71,37 @@ export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
       )
     }
 
-    await this.requireConfirmation(`Are you sure you want to remove the channel with ID ${channelId.toString()}?`)
+    const dataObjectsInfo = this.isQueryNodeUriSet()
+      ? await this.getDataObjectsInfoFromQueryNode(channelId)
+      : await this.getDataObjectsInfoFromChain(channelId)
+
+    if (dataObjectsInfo.length) {
+      if (!force) {
+        this.error(`Cannot remove associated data objects unless ${chalk.magentaBright('--force')} flag is used`, {
+          exit: ExitCodes.InvalidInput,
+        })
+      }
+      const deletionPrize = dataObjectsInfo.reduce((sum, [, prize]) => sum.add(prize), new BN(0))
+      this.log(
+        `Data objects deletion prize of ${chalk.cyanBright(
+          formatBalance(deletionPrize)
+        )} will be transferred to ${chalk.magentaBright(channel.deletion_prize_source_account_id.toString())}`
+      )
+    }
+
+    await this.requireConfirmation(
+      `Are you sure you want to remove channel ${chalk.magentaBright(channelId.toString())}${
+        force ? ' and all associated data objects' : ''
+      }?`
+    )
 
-    await this.sendAndFollowNamedTx(account, 'content', 'deleteChannel', [actor, channelId])
+    await this.sendAndFollowNamedTx(account, 'content', 'deleteChannel', [
+      actor,
+      channelId,
+      new (JoyBTreeSet(DataObjectId))(
+        registry,
+        dataObjectsInfo.map(([id]) => id)
+      ),
+    ])
   }
 }

+ 49 - 17
cli/src/commands/content/deleteVideo.ts

@@ -3,11 +3,15 @@ import { flags } from '@oclif/command'
 import BN from 'bn.js'
 import chalk from 'chalk'
 import { formatBalance } from '@polkadot/util'
-import { createTypeFromConstructor } from '@joystream/types'
-import { BagId } from '@joystream/types/storage'
+import { registry } from '@joystream/types'
+import { DataObjectId } from '@joystream/types/storage'
+import ExitCodes from '../../ExitCodes'
+import { JoyBTreeSet } from '@joystream/types/common'
 
-export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
-  static description = 'Delete the video (it cannot have any associated data objects).'
+export default class DeleteVideoCommand extends ContentDirectoryCommandBase {
+  static description = 'Delete the video and optionally all associated data objects.'
+
+  protected requiresQueryNode = true
 
   static flags = {
     videoId: flags.integer({
@@ -15,12 +19,30 @@ export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
       required: true,
       description: 'ID of the Video',
     }),
+    force: flags.boolean({
+      char: 'f',
+      default: false,
+      description: 'Force-remove all associated video data objects',
+    }),
+  }
+
+  async getDataObjectsInfo(videoId: number): Promise<[string, BN][]> {
+    const dataObjects = await this.getQNApi().dataObjectsByVideoId(videoId.toString())
+
+    if (dataObjects.length) {
+      this.log('Following data objects are still associated with the video:')
+      dataObjects.forEach((o) => {
+        this.log(`${o.id} - ${o.type.__typename}`)
+      })
+    }
+
+    return dataObjects.map((o) => [o.id, new BN(o.deletionPrize)])
   }
 
   async run(): Promise<void> {
     const {
-      flags: { videoId },
-    } = this.parse(DeleteChannelCommand)
+      flags: { videoId, force },
+    } = this.parse(DeleteVideoCommand)
     // Context
     const account = await this.getRequiredSelectedAccount()
     const video = await this.getApi().videoById(videoId)
@@ -28,15 +50,14 @@ export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
     const actor = await this.getChannelOwnerActor(channel)
     await this.requestAccountDecoding(account)
 
-    const bagId = createTypeFromConstructor(BagId, { Dynamic: { Channel: video.in_channel } })
-
-    const dataObjects = await this.getApi().dataObjectsByIds(
-      bagId,
-      Array.from(video.maybe_data_objects_id_set.unwrapOr([]))
-    )
-
-    if (dataObjects.length) {
-      const deletionPrize = dataObjects.reduce((a, b) => a.add(b.deletion_prize), new BN(0))
+    const dataObjectsInfo = await this.getDataObjectsInfo(videoId)
+    if (dataObjectsInfo.length) {
+      if (!force) {
+        this.error(`Cannot remove associated data objects unless ${chalk.magentaBright('--force')} flag is used`, {
+          exit: ExitCodes.InvalidInput,
+        })
+      }
+      const deletionPrize = dataObjectsInfo.reduce((sum, [, prize]) => sum.add(prize), new BN(0))
       this.log(
         `Data objects deletion prize of ${chalk.cyanBright(
           formatBalance(deletionPrize)
@@ -44,8 +65,19 @@ export default class DeleteChannelCommand extends ContentDirectoryCommandBase {
       )
     }
 
-    await this.requireConfirmation(`Are you sure you want to remove the video with ID ${videoId.toString()}?`)
+    await this.requireConfirmation(
+      `Are you sure you want to remove video ${chalk.magentaBright(videoId)}${
+        force ? ' and all associated data objects' : ''
+      }?`
+    )
 
-    await this.sendAndFollowNamedTx(account, 'content', 'deleteVideo', [actor, videoId])
+    await this.sendAndFollowNamedTx(account, 'content', 'deleteVideo', [
+      actor,
+      videoId,
+      new (JoyBTreeSet(DataObjectId))(
+        registry,
+        dataObjectsInfo.map(([id]) => id)
+      ),
+    ])
   }
 }

+ 4 - 5
cli/src/commands/content/removeChannelAssets.ts

@@ -17,7 +17,7 @@ export default class RemoveChannelAssetsCommand extends ContentDirectoryCommandB
       char: 'o',
       required: true,
       multiple: true,
-      description: 'ID of the object to remove',
+      description: 'ID of an object to remove',
     }),
   }
 
@@ -31,14 +31,13 @@ export default class RemoveChannelAssetsCommand extends ContentDirectoryCommandB
     const actor = await this.getChannelOwnerActor(channel)
     await this.requestAccountDecoding(account)
 
-    this.log('Channel id:', channelId)
-    this.log('List of assets to remove:', objectIds)
+    this.jsonPrettyPrint(JSON.stringify({ channelId, assetsToRemove: objectIds }))
     await this.requireConfirmation('Do you confirm the provided input?', true)
 
-    await this.sendAndFollowNamedTx(account, 'content', 'removeChannelAssets', [
+    await this.sendAndFollowNamedTx(account, 'content', 'updateChannel', [
       actor,
       channelId,
-      new (JoyBTreeSet(DataObjectId))(registry, objectIds),
+      { assets_to_remove: new (JoyBTreeSet(DataObjectId))(registry, objectIds) },
     ])
   }
 }

+ 41 - 5
cli/src/commands/content/updateChannel.ts

@@ -3,11 +3,16 @@ import { asValidatedMetadata, metadataToBytes } from '../../helpers/serializatio
 import { ChannelInputParameters } from '../../Types'
 import { flags } from '@oclif/command'
 import UploadCommandBase from '../../base/UploadCommandBase'
-import { CreateInterface } from '@joystream/types'
+import { CreateInterface, registry } from '@joystream/types'
 import { ChannelUpdateParameters } from '@joystream/types/content'
 import { ChannelInputSchema } from '../../schemas/ContentDirectory'
 import { ChannelMetadata } from '@joystream/metadata-protobuf'
-
+import { DataObjectInfoFragment } from '../../graphql/generated/queries'
+import BN from 'bn.js'
+import { formatBalance } from '@polkadot/util'
+import chalk from 'chalk'
+import { JoyBTreeSet } from '@joystream/types/common'
+import { DataObjectId } from '@joystream/types/storage'
 export default class UpdateChannelCommand extends UploadCommandBase {
   static description = 'Update existing content directory channel.'
   static flags = {
@@ -39,6 +44,33 @@ export default class UpdateChannelCommand extends UploadCommandBase {
     }
   }
 
+  async getAssetsToRemove(
+    channelId: number,
+    coverPhotoIndex: number | undefined,
+    avatarPhotoIndex: number | undefined
+  ): Promise<string[]> {
+    let assetsToRemove: DataObjectInfoFragment[] = []
+    if (coverPhotoIndex !== undefined || avatarPhotoIndex !== undefined) {
+      const currentAssets = await this.getQNApi().dataObjectsByChannelId(channelId.toString())
+      const currentCovers = currentAssets.filter((a) => a.type.__typename === 'DataObjectTypeChannelCoverPhoto')
+      const currentAvatars = currentAssets.filter((a) => a.type.__typename === 'DataObjectTypeChannelAvatar')
+      if (currentCovers.length && coverPhotoIndex !== undefined) {
+        assetsToRemove = assetsToRemove.concat(currentCovers)
+      }
+      if (currentAvatars.length && avatarPhotoIndex !== undefined) {
+        assetsToRemove = assetsToRemove.concat(currentAvatars)
+      }
+      if (assetsToRemove.length) {
+        this.log(`\nData objects to be removed due to replacement:`)
+        assetsToRemove.forEach((a) => this.log(`- ${a.id} (${a.type.__typename})`))
+        const totalPrize = assetsToRemove.reduce((sum, { deletionPrize }) => sum.add(new BN(deletionPrize)), new BN(0))
+        this.log(`Total deletion prize: ${chalk.cyanBright(formatBalance(totalPrize))}\n`)
+      }
+    }
+
+    return assetsToRemove.map((a) => a.id)
+  }
+
   async run() {
     const {
       flags: { input },
@@ -65,14 +97,18 @@ export default class UpdateChannelCommand extends UploadCommandBase {
     meta.avatarPhoto = avatarPhotoIndex
 
     // Preare and send the extrinsic
-    const assets = await this.prepareAssetsForExtrinsic(resolvedAssets)
+    const assetsToUpload = await this.prepareAssetsForExtrinsic(resolvedAssets)
+    const assetsToRemove = await this.getAssetsToRemove(channelId, coverPhotoIndex, avatarPhotoIndex)
     const channelUpdateParameters: CreateInterface<ChannelUpdateParameters> = {
-      assets,
+      assets_to_upload: assetsToUpload,
+      assets_to_remove: new (JoyBTreeSet(DataObjectId))(registry, assetsToRemove),
       new_meta: metadataToBytes(ChannelMetadata, meta),
       reward_account: this.parseRewardAccountInput(rewardAccount),
     }
 
-    this.jsonPrettyPrint(JSON.stringify({ assets: assets?.toJSON(), metadata: meta, rewardAccount }))
+    this.jsonPrettyPrint(
+      JSON.stringify({ assetsToUpload: assetsToUpload?.toJSON(), assetsToRemove, metadata: meta, rewardAccount })
+    )
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 

+ 41 - 4
cli/src/commands/content/updateVideo.ts

@@ -3,10 +3,16 @@ import { VideoInputParameters } from '../../Types'
 import { asValidatedMetadata, metadataToBytes } from '../../helpers/serialization'
 import UploadCommandBase from '../../base/UploadCommandBase'
 import { flags } from '@oclif/command'
-import { CreateInterface } from '@joystream/types'
+import { CreateInterface, registry } from '@joystream/types'
 import { VideoUpdateParameters } from '@joystream/types/content'
 import { VideoInputSchema } from '../../schemas/ContentDirectory'
 import { VideoMetadata } from '@joystream/metadata-protobuf'
+import { DataObjectInfoFragment } from '../../graphql/generated/queries'
+import BN from 'bn.js'
+import { formatBalance } from '@polkadot/util'
+import { JoyBTreeSet } from '@joystream/types/common'
+import { DataObjectId } from '@joystream/types/storage'
+import chalk from 'chalk'
 
 export default class UpdateVideoCommand extends UploadCommandBase {
   static description = 'Update video under specific id.'
@@ -26,6 +32,33 @@ export default class UpdateVideoCommand extends UploadCommandBase {
     },
   ]
 
+  async getAssetsToRemove(
+    videoId: number,
+    videoIndex: number | undefined,
+    thumbnailIndex: number | undefined
+  ): Promise<string[]> {
+    let assetsToRemove: DataObjectInfoFragment[] = []
+    if (videoIndex !== undefined || thumbnailIndex !== undefined) {
+      const currentAssets = await this.getQNApi().dataObjectsByVideoId(videoId.toString())
+      const currentThumbs = currentAssets.filter((a) => a.type.__typename === 'DataObjectTypeVideoThumbnail')
+      const currentMedias = currentAssets.filter((a) => a.type.__typename === 'DataObjectTypeVideoMedia')
+      if (currentThumbs.length && thumbnailIndex !== undefined) {
+        assetsToRemove = assetsToRemove.concat(currentThumbs)
+      }
+      if (currentMedias.length && videoIndex !== undefined) {
+        assetsToRemove = assetsToRemove.concat(currentMedias)
+      }
+      if (assetsToRemove.length) {
+        this.log(`\nData objects to be removed due to replacement:`)
+        assetsToRemove.forEach((a) => this.log(`- ${a.id} (${a.type.__typename})`))
+        const totalPrize = assetsToRemove.reduce((sum, { deletionPrize }) => sum.add(new BN(deletionPrize)), new BN(0))
+        this.log(`Total deletion prize: ${chalk.cyanBright(formatBalance(totalPrize))}\n`)
+      }
+    }
+
+    return assetsToRemove.map((a) => a.id)
+  }
+
   async run() {
     const {
       flags: { input },
@@ -53,13 +86,17 @@ export default class UpdateVideoCommand extends UploadCommandBase {
     meta.thumbnailPhoto = thumbnailPhotoIndex
 
     // Preare and send the extrinsic
-    const assets = await this.prepareAssetsForExtrinsic(resolvedAssets)
+    const assetsToUpload = await this.prepareAssetsForExtrinsic(resolvedAssets)
+    const assetsToRemove = await this.getAssetsToRemove(videoId, videoIndex, thumbnailPhotoIndex)
     const videoUpdateParameters: CreateInterface<VideoUpdateParameters> = {
-      assets,
+      assets_to_upload: assetsToUpload,
       new_meta: metadataToBytes(VideoMetadata, meta),
+      assets_to_remove: new (JoyBTreeSet(DataObjectId))(registry, assetsToRemove),
     }
 
-    this.jsonPrettyPrint(JSON.stringify({ assets: assets?.toJSON(), newMetadata: meta }))
+    this.jsonPrettyPrint(
+      JSON.stringify({ assetsToUpload: assetsToUpload?.toJSON(), newMetadata: meta, assetsToRemove })
+    )
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 

+ 84 - 0
cli/src/graphql/generated/queries.ts

@@ -12,6 +12,36 @@ export type GetStorageNodesInfoByBagIdQueryVariables = Types.Exact<{
 
 export type GetStorageNodesInfoByBagIdQuery = { storageBuckets: Array<StorageNodeInfoFragment> }
 
+export type DataObjectInfoFragment = {
+  id: string
+  size: any
+  deletionPrize: any
+  type:
+    | { __typename: 'DataObjectTypeChannelAvatar'; channel?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeChannelCoverPhoto'; channel?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeVideoMedia'; video?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeVideoThumbnail'; video?: Types.Maybe<{ id: string }> }
+    | { __typename: 'DataObjectTypeUnknown' }
+}
+
+export type GetDataObjectsByBagIdQueryVariables = Types.Exact<{
+  bagId?: Types.Maybe<Types.Scalars['ID']>
+}>
+
+export type GetDataObjectsByBagIdQuery = { storageDataObjects: Array<DataObjectInfoFragment> }
+
+export type GetDataObjectsChannelIdQueryVariables = Types.Exact<{
+  channelId?: Types.Maybe<Types.Scalars['ID']>
+}>
+
+export type GetDataObjectsChannelIdQuery = { storageDataObjects: Array<DataObjectInfoFragment> }
+
+export type GetDataObjectsByVideoIdQueryVariables = Types.Exact<{
+  videoId?: Types.Maybe<Types.Scalars['ID']>
+}>
+
+export type GetDataObjectsByVideoIdQuery = { storageDataObjects: Array<DataObjectInfoFragment> }
+
 export const StorageNodeInfo = gql`
   fragment StorageNodeInfo on StorageBucket {
     id
@@ -20,6 +50,36 @@ export const StorageNodeInfo = gql`
     }
   }
 `
+export const DataObjectInfo = gql`
+  fragment DataObjectInfo on StorageDataObject {
+    id
+    size
+    deletionPrize
+    type {
+      __typename
+      ... on DataObjectTypeVideoMedia {
+        video {
+          id
+        }
+      }
+      ... on DataObjectTypeVideoThumbnail {
+        video {
+          id
+        }
+      }
+      ... on DataObjectTypeChannelAvatar {
+        channel {
+          id
+        }
+      }
+      ... on DataObjectTypeChannelCoverPhoto {
+        channel {
+          id
+        }
+      }
+    }
+  }
+`
 export const GetStorageNodesInfoByBagId = gql`
   query getStorageNodesInfoByBagId($bagId: String) {
     storageBuckets(
@@ -34,3 +94,27 @@ export const GetStorageNodesInfoByBagId = gql`
   }
   ${StorageNodeInfo}
 `
+export const GetDataObjectsByBagId = gql`
+  query getDataObjectsByBagId($bagId: ID) {
+    storageDataObjects(where: { storageBag: { id_eq: $bagId } }) {
+      ...DataObjectInfo
+    }
+  }
+  ${DataObjectInfo}
+`
+export const GetDataObjectsChannelId = gql`
+  query getDataObjectsChannelId($channelId: ID) {
+    storageDataObjects(where: { type_json: { channelId_eq: $channelId } }) {
+      ...DataObjectInfo
+    }
+  }
+  ${DataObjectInfo}
+`
+export const GetDataObjectsByVideoId = gql`
+  query getDataObjectsByVideoId($videoId: ID) {
+    storageDataObjects(where: { type_json: { videoId_eq: $videoId } }) {
+      ...DataObjectInfo
+    }
+  }
+  ${DataObjectInfo}
+`

+ 436 - 144
cli/src/graphql/generated/schema.ts

@@ -11,119 +11,10 @@ export type Scalars = {
   Float: number
   /** The javascript `Date` as string. Type represents date and time as the ISO Date string. */
   DateTime: any
-  /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
-  JSONObject: any
   /** GraphQL representation of BigInt */
   BigInt: any
-}
-
-export type Asset = AssetExternal | AssetJoystreamStorage | AssetNone
-
-export type AssetExternal = {
-  /** JSON array of the urls */
-  urls: Scalars['String']
-}
-
-export type AssetExternalCreateInput = {
-  urls: Scalars['String']
-}
-
-export type AssetExternalUpdateInput = {
-  urls?: Maybe<Scalars['String']>
-}
-
-export type AssetExternalWhereInput = {
-  id_eq?: Maybe<Scalars['ID']>
-  id_in?: Maybe<Array<Scalars['ID']>>
-  createdAt_eq?: Maybe<Scalars['DateTime']>
-  createdAt_lt?: Maybe<Scalars['DateTime']>
-  createdAt_lte?: Maybe<Scalars['DateTime']>
-  createdAt_gt?: Maybe<Scalars['DateTime']>
-  createdAt_gte?: Maybe<Scalars['DateTime']>
-  createdById_eq?: Maybe<Scalars['ID']>
-  createdById_in?: Maybe<Array<Scalars['ID']>>
-  updatedAt_eq?: Maybe<Scalars['DateTime']>
-  updatedAt_lt?: Maybe<Scalars['DateTime']>
-  updatedAt_lte?: Maybe<Scalars['DateTime']>
-  updatedAt_gt?: Maybe<Scalars['DateTime']>
-  updatedAt_gte?: Maybe<Scalars['DateTime']>
-  updatedById_eq?: Maybe<Scalars['ID']>
-  updatedById_in?: Maybe<Array<Scalars['ID']>>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['DateTime']>
-  deletedAt_lt?: Maybe<Scalars['DateTime']>
-  deletedAt_lte?: Maybe<Scalars['DateTime']>
-  deletedAt_gt?: Maybe<Scalars['DateTime']>
-  deletedAt_gte?: Maybe<Scalars['DateTime']>
-  deletedById_eq?: Maybe<Scalars['ID']>
-  deletedById_in?: Maybe<Array<Scalars['ID']>>
-  urls_eq?: Maybe<Scalars['String']>
-  urls_contains?: Maybe<Scalars['String']>
-  urls_startsWith?: Maybe<Scalars['String']>
-  urls_endsWith?: Maybe<Scalars['String']>
-  urls_in?: Maybe<Array<Scalars['String']>>
-  AND?: Maybe<Array<AssetExternalWhereInput>>
-  OR?: Maybe<Array<AssetExternalWhereInput>>
-}
-
-export type AssetExternalWhereUniqueInput = {
-  id: Scalars['ID']
-}
-
-export type AssetJoystreamStorage = {
-  /** Related data object */
-  dataObject?: Maybe<StorageDataObject>
-}
-
-export type AssetNone = {
-  phantom?: Maybe<Scalars['Int']>
-}
-
-export type AssetNoneCreateInput = {
-  phantom?: Maybe<Scalars['Float']>
-}
-
-export type AssetNoneUpdateInput = {
-  phantom?: Maybe<Scalars['Float']>
-}
-
-export type AssetNoneWhereInput = {
-  id_eq?: Maybe<Scalars['ID']>
-  id_in?: Maybe<Array<Scalars['ID']>>
-  createdAt_eq?: Maybe<Scalars['DateTime']>
-  createdAt_lt?: Maybe<Scalars['DateTime']>
-  createdAt_lte?: Maybe<Scalars['DateTime']>
-  createdAt_gt?: Maybe<Scalars['DateTime']>
-  createdAt_gte?: Maybe<Scalars['DateTime']>
-  createdById_eq?: Maybe<Scalars['ID']>
-  createdById_in?: Maybe<Array<Scalars['ID']>>
-  updatedAt_eq?: Maybe<Scalars['DateTime']>
-  updatedAt_lt?: Maybe<Scalars['DateTime']>
-  updatedAt_lte?: Maybe<Scalars['DateTime']>
-  updatedAt_gt?: Maybe<Scalars['DateTime']>
-  updatedAt_gte?: Maybe<Scalars['DateTime']>
-  updatedById_eq?: Maybe<Scalars['ID']>
-  updatedById_in?: Maybe<Array<Scalars['ID']>>
-  deletedAt_all?: Maybe<Scalars['Boolean']>
-  deletedAt_eq?: Maybe<Scalars['DateTime']>
-  deletedAt_lt?: Maybe<Scalars['DateTime']>
-  deletedAt_lte?: Maybe<Scalars['DateTime']>
-  deletedAt_gt?: Maybe<Scalars['DateTime']>
-  deletedAt_gte?: Maybe<Scalars['DateTime']>
-  deletedById_eq?: Maybe<Scalars['ID']>
-  deletedById_in?: Maybe<Array<Scalars['ID']>>
-  phantom_eq?: Maybe<Scalars['Int']>
-  phantom_gt?: Maybe<Scalars['Int']>
-  phantom_gte?: Maybe<Scalars['Int']>
-  phantom_lt?: Maybe<Scalars['Int']>
-  phantom_lte?: Maybe<Scalars['Int']>
-  phantom_in?: Maybe<Array<Scalars['Int']>>
-  AND?: Maybe<Array<AssetNoneWhereInput>>
-  OR?: Maybe<Array<AssetNoneWhereInput>>
-}
-
-export type AssetNoneWhereUniqueInput = {
-  id: Scalars['ID']
+  /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
+  JSONObject: any
 }
 
 export type BaseGraphQlObject = {
@@ -206,10 +97,10 @@ export type Channel = BaseGraphQlObject & {
   title?: Maybe<Scalars['String']>
   /** The description of a Channel */
   description?: Maybe<Scalars['String']>
-  /** Channel's cover (background) photo asset. Recommended ratio: 16:9. */
-  coverPhoto: Asset
-  /** Channel's avatar photo asset. */
-  avatarPhoto: Asset
+  coverPhoto?: Maybe<StorageDataObject>
+  coverPhotoId?: Maybe<Scalars['String']>
+  avatarPhoto?: Maybe<StorageDataObject>
+  avatarPhotoId?: Maybe<Scalars['String']>
   /** Flag signaling whether a channel is public. */
   isPublic?: Maybe<Scalars['Boolean']>
   /** Flag signaling whether a channel is censored. */
@@ -340,8 +231,8 @@ export type ChannelCreateInput = {
   deletionPrizeDestAccount: Scalars['String']
   title?: Maybe<Scalars['String']>
   description?: Maybe<Scalars['String']>
-  coverPhoto: Scalars['JSONObject']
-  avatarPhoto: Scalars['JSONObject']
+  coverPhoto?: Maybe<Scalars['ID']>
+  avatarPhoto?: Maybe<Scalars['ID']>
   isPublic?: Maybe<Scalars['Boolean']>
   isCensored: Scalars['Boolean']
   language?: Maybe<Scalars['ID']>
@@ -374,6 +265,10 @@ export enum ChannelOrderByInput {
   TitleDesc = 'title_DESC',
   DescriptionAsc = 'description_ASC',
   DescriptionDesc = 'description_DESC',
+  CoverPhotoAsc = 'coverPhoto_ASC',
+  CoverPhotoDesc = 'coverPhoto_DESC',
+  AvatarPhotoAsc = 'avatarPhoto_ASC',
+  AvatarPhotoDesc = 'avatarPhoto_DESC',
   IsPublicAsc = 'isPublic_ASC',
   IsPublicDesc = 'isPublic_DESC',
   IsCensoredAsc = 'isCensored_ASC',
@@ -392,8 +287,8 @@ export type ChannelUpdateInput = {
   deletionPrizeDestAccount?: Maybe<Scalars['String']>
   title?: Maybe<Scalars['String']>
   description?: Maybe<Scalars['String']>
-  coverPhoto?: Maybe<Scalars['JSONObject']>
-  avatarPhoto?: Maybe<Scalars['JSONObject']>
+  coverPhoto?: Maybe<Scalars['ID']>
+  avatarPhoto?: Maybe<Scalars['ID']>
   isPublic?: Maybe<Scalars['Boolean']>
   isCensored?: Maybe<Scalars['Boolean']>
   language?: Maybe<Scalars['ID']>
@@ -451,8 +346,10 @@ export type ChannelWhereInput = {
   description_startsWith?: Maybe<Scalars['String']>
   description_endsWith?: Maybe<Scalars['String']>
   description_in?: Maybe<Array<Scalars['String']>>
-  coverPhoto_json?: Maybe<Scalars['JSONObject']>
-  avatarPhoto_json?: Maybe<Scalars['JSONObject']>
+  coverPhoto_eq?: Maybe<Scalars['ID']>
+  coverPhoto_in?: Maybe<Array<Scalars['ID']>>
+  avatarPhoto_eq?: Maybe<Scalars['ID']>
+  avatarPhoto_in?: Maybe<Array<Scalars['ID']>>
   isPublic_eq?: Maybe<Scalars['Boolean']>
   isPublic_in?: Maybe<Array<Scalars['Boolean']>>
   isCensored_eq?: Maybe<Scalars['Boolean']>
@@ -468,6 +365,8 @@ export type ChannelWhereInput = {
   ownerMember?: Maybe<MembershipWhereInput>
   ownerCuratorGroup?: Maybe<CuratorGroupWhereInput>
   category?: Maybe<ChannelCategoryWhereInput>
+  coverPhoto?: Maybe<StorageDataObjectWhereInput>
+  avatarPhoto?: Maybe<StorageDataObjectWhereInput>
   language?: Maybe<LanguageWhereInput>
   videos_none?: Maybe<VideoWhereInput>
   videos_some?: Maybe<VideoWhereInput>
@@ -480,6 +379,16 @@ export type ChannelWhereUniqueInput = {
   id: Scalars['ID']
 }
 
+export enum Continent {
+  Af = 'AF',
+  Na = 'NA',
+  Oc = 'OC',
+  An = 'AN',
+  As = 'AS',
+  Eu = 'EU',
+  Sa = 'SA',
+}
+
 export type CuratorGroup = BaseGraphQlObject & {
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
@@ -566,6 +475,84 @@ export type CuratorGroupWhereUniqueInput = {
   id: Scalars['ID']
 }
 
+export type DataObjectType =
+  | DataObjectTypeChannelAvatar
+  | DataObjectTypeChannelCoverPhoto
+  | DataObjectTypeVideoMedia
+  | DataObjectTypeVideoThumbnail
+  | DataObjectTypeUnknown
+
+export type DataObjectTypeChannelAvatar = {
+  /** Related channel entity */
+  channel?: Maybe<Channel>
+}
+
+export type DataObjectTypeChannelCoverPhoto = {
+  /** Related channel entity */
+  channel?: Maybe<Channel>
+}
+
+export type DataObjectTypeUnknown = {
+  phantom?: Maybe<Scalars['Int']>
+}
+
+export type DataObjectTypeUnknownCreateInput = {
+  phantom?: Maybe<Scalars['Float']>
+}
+
+export type DataObjectTypeUnknownUpdateInput = {
+  phantom?: Maybe<Scalars['Float']>
+}
+
+export type DataObjectTypeUnknownWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  phantom_eq?: Maybe<Scalars['Int']>
+  phantom_gt?: Maybe<Scalars['Int']>
+  phantom_gte?: Maybe<Scalars['Int']>
+  phantom_lt?: Maybe<Scalars['Int']>
+  phantom_lte?: Maybe<Scalars['Int']>
+  phantom_in?: Maybe<Array<Scalars['Int']>>
+  AND?: Maybe<Array<DataObjectTypeUnknownWhereInput>>
+  OR?: Maybe<Array<DataObjectTypeUnknownWhereInput>>
+}
+
+export type DataObjectTypeUnknownWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
+export type DataObjectTypeVideoMedia = {
+  /** Related video entity */
+  video?: Maybe<Video>
+}
+
+export type DataObjectTypeVideoThumbnail = {
+  /** Related video entity */
+  video?: Maybe<Video>
+}
+
 export type DeleteResponse = {
   id: Scalars['ID']
 }
@@ -635,6 +622,90 @@ export type DistributionBucketFamilyEdge = {
   cursor: Scalars['String']
 }
 
+export type DistributionBucketFamilyGeographicArea = BaseGraphQlObject & {
+  id: Scalars['ID']
+  createdAt: Scalars['DateTime']
+  createdById: Scalars['String']
+  updatedAt?: Maybe<Scalars['DateTime']>
+  updatedById?: Maybe<Scalars['String']>
+  deletedAt?: Maybe<Scalars['DateTime']>
+  deletedById?: Maybe<Scalars['String']>
+  version: Scalars['Int']
+  /** Geographical area (continent / country / subdivision) */
+  area: GeographicalArea
+  distributionBucketFamilyMetadata: DistributionBucketFamilyMetadata
+  distributionBucketFamilyMetadataId: Scalars['String']
+}
+
+export type DistributionBucketFamilyGeographicAreaConnection = {
+  totalCount: Scalars['Int']
+  edges: Array<DistributionBucketFamilyGeographicAreaEdge>
+  pageInfo: PageInfo
+}
+
+export type DistributionBucketFamilyGeographicAreaCreateInput = {
+  area: Scalars['JSONObject']
+  distributionBucketFamilyMetadata: Scalars['ID']
+}
+
+export type DistributionBucketFamilyGeographicAreaEdge = {
+  node: DistributionBucketFamilyGeographicArea
+  cursor: Scalars['String']
+}
+
+export enum DistributionBucketFamilyGeographicAreaOrderByInput {
+  CreatedAtAsc = 'createdAt_ASC',
+  CreatedAtDesc = 'createdAt_DESC',
+  UpdatedAtAsc = 'updatedAt_ASC',
+  UpdatedAtDesc = 'updatedAt_DESC',
+  DeletedAtAsc = 'deletedAt_ASC',
+  DeletedAtDesc = 'deletedAt_DESC',
+  DistributionBucketFamilyMetadataAsc = 'distributionBucketFamilyMetadata_ASC',
+  DistributionBucketFamilyMetadataDesc = 'distributionBucketFamilyMetadata_DESC',
+}
+
+export type DistributionBucketFamilyGeographicAreaUpdateInput = {
+  area?: Maybe<Scalars['JSONObject']>
+  distributionBucketFamilyMetadata?: Maybe<Scalars['ID']>
+}
+
+export type DistributionBucketFamilyGeographicAreaWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  area_json?: Maybe<Scalars['JSONObject']>
+  distributionBucketFamilyMetadata_eq?: Maybe<Scalars['ID']>
+  distributionBucketFamilyMetadata_in?: Maybe<Array<Scalars['ID']>>
+  distributionBucketFamilyMetadata?: Maybe<DistributionBucketFamilyMetadataWhereInput>
+  AND?: Maybe<Array<DistributionBucketFamilyGeographicAreaWhereInput>>
+  OR?: Maybe<Array<DistributionBucketFamilyGeographicAreaWhereInput>>
+}
+
+export type DistributionBucketFamilyGeographicAreaWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
 export type DistributionBucketFamilyMetadata = BaseGraphQlObject & {
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
@@ -648,7 +719,9 @@ export type DistributionBucketFamilyMetadata = BaseGraphQlObject & {
   region?: Maybe<Scalars['String']>
   /** Optional, more specific description of the region covered by the family */
   description?: Maybe<Scalars['String']>
-  boundary: Array<GeoCoordinates>
+  areas: Array<DistributionBucketFamilyGeographicArea>
+  /** List of targets (hosts/ips) best suited latency measurements for the family */
+  latencyTestTargets?: Maybe<Array<Scalars['String']>>
   distributionbucketfamilymetadata?: Maybe<Array<DistributionBucketFamily>>
 }
 
@@ -661,6 +734,7 @@ export type DistributionBucketFamilyMetadataConnection = {
 export type DistributionBucketFamilyMetadataCreateInput = {
   region?: Maybe<Scalars['String']>
   description?: Maybe<Scalars['String']>
+  latencyTestTargets?: Maybe<Array<Scalars['String']>>
 }
 
 export type DistributionBucketFamilyMetadataEdge = {
@@ -684,6 +758,7 @@ export enum DistributionBucketFamilyMetadataOrderByInput {
 export type DistributionBucketFamilyMetadataUpdateInput = {
   region?: Maybe<Scalars['String']>
   description?: Maybe<Scalars['String']>
+  latencyTestTargets?: Maybe<Array<Scalars['String']>>
 }
 
 export type DistributionBucketFamilyMetadataWhereInput = {
@@ -721,9 +796,9 @@ export type DistributionBucketFamilyMetadataWhereInput = {
   description_startsWith?: Maybe<Scalars['String']>
   description_endsWith?: Maybe<Scalars['String']>
   description_in?: Maybe<Array<Scalars['String']>>
-  boundary_none?: Maybe<GeoCoordinatesWhereInput>
-  boundary_some?: Maybe<GeoCoordinatesWhereInput>
-  boundary_every?: Maybe<GeoCoordinatesWhereInput>
+  areas_none?: Maybe<DistributionBucketFamilyGeographicAreaWhereInput>
+  areas_some?: Maybe<DistributionBucketFamilyGeographicAreaWhereInput>
+  areas_every?: Maybe<DistributionBucketFamilyGeographicAreaWhereInput>
   distributionbucketfamilymetadata_none?: Maybe<DistributionBucketFamilyWhereInput>
   distributionbucketfamilymetadata_some?: Maybe<DistributionBucketFamilyWhereInput>
   distributionbucketfamilymetadata_every?: Maybe<DistributionBucketFamilyWhereInput>
@@ -1085,8 +1160,6 @@ export type GeoCoordinates = BaseGraphQlObject & {
   version: Scalars['Int']
   latitude: Scalars['Float']
   longitude: Scalars['Float']
-  boundarySourceBucketFamilyMeta?: Maybe<DistributionBucketFamilyMetadata>
-  boundarySourceBucketFamilyMetaId?: Maybe<Scalars['String']>
   nodelocationmetadatacoordinates?: Maybe<Array<NodeLocationMetadata>>
 }
 
@@ -1099,7 +1172,6 @@ export type GeoCoordinatesConnection = {
 export type GeoCoordinatesCreateInput = {
   latitude: Scalars['Float']
   longitude: Scalars['Float']
-  boundarySourceBucketFamilyMeta?: Maybe<Scalars['ID']>
 }
 
 export type GeoCoordinatesEdge = {
@@ -1118,14 +1190,11 @@ export enum GeoCoordinatesOrderByInput {
   LatitudeDesc = 'latitude_DESC',
   LongitudeAsc = 'longitude_ASC',
   LongitudeDesc = 'longitude_DESC',
-  BoundarySourceBucketFamilyMetaAsc = 'boundarySourceBucketFamilyMeta_ASC',
-  BoundarySourceBucketFamilyMetaDesc = 'boundarySourceBucketFamilyMeta_DESC',
 }
 
 export type GeoCoordinatesUpdateInput = {
   latitude?: Maybe<Scalars['Float']>
   longitude?: Maybe<Scalars['Float']>
-  boundarySourceBucketFamilyMeta?: Maybe<Scalars['ID']>
 }
 
 export type GeoCoordinatesWhereInput = {
@@ -1165,9 +1234,6 @@ export type GeoCoordinatesWhereInput = {
   longitude_lt?: Maybe<Scalars['Float']>
   longitude_lte?: Maybe<Scalars['Float']>
   longitude_in?: Maybe<Array<Scalars['Float']>>
-  boundarySourceBucketFamilyMeta_eq?: Maybe<Scalars['ID']>
-  boundarySourceBucketFamilyMeta_in?: Maybe<Array<Scalars['ID']>>
-  boundarySourceBucketFamilyMeta?: Maybe<DistributionBucketFamilyMetadataWhereInput>
   nodelocationmetadatacoordinates_none?: Maybe<NodeLocationMetadataWhereInput>
   nodelocationmetadatacoordinates_some?: Maybe<NodeLocationMetadataWhereInput>
   nodelocationmetadatacoordinates_every?: Maybe<NodeLocationMetadataWhereInput>
@@ -1179,6 +1245,157 @@ export type GeoCoordinatesWhereUniqueInput = {
   id: Scalars['ID']
 }
 
+export type GeographicalArea = GeographicalAreaContinent | GeographicalAreaCountry | GeographicalAreaSubdivistion
+
+export type GeographicalAreaContinent = {
+  code?: Maybe<Continent>
+}
+
+export type GeographicalAreaContinentCreateInput = {
+  code?: Maybe<Continent>
+}
+
+export type GeographicalAreaContinentUpdateInput = {
+  code?: Maybe<Continent>
+}
+
+export type GeographicalAreaContinentWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  code_eq?: Maybe<Continent>
+  code_in?: Maybe<Array<Continent>>
+  AND?: Maybe<Array<GeographicalAreaContinentWhereInput>>
+  OR?: Maybe<Array<GeographicalAreaContinentWhereInput>>
+}
+
+export type GeographicalAreaContinentWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
+export type GeographicalAreaCountry = {
+  /** ISO 3166-1 alpha-2 country code */
+  code?: Maybe<Scalars['String']>
+}
+
+export type GeographicalAreaCountryCreateInput = {
+  code?: Maybe<Scalars['String']>
+}
+
+export type GeographicalAreaCountryUpdateInput = {
+  code?: Maybe<Scalars['String']>
+}
+
+export type GeographicalAreaCountryWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  code_eq?: Maybe<Scalars['String']>
+  code_contains?: Maybe<Scalars['String']>
+  code_startsWith?: Maybe<Scalars['String']>
+  code_endsWith?: Maybe<Scalars['String']>
+  code_in?: Maybe<Array<Scalars['String']>>
+  AND?: Maybe<Array<GeographicalAreaCountryWhereInput>>
+  OR?: Maybe<Array<GeographicalAreaCountryWhereInput>>
+}
+
+export type GeographicalAreaCountryWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
+export type GeographicalAreaSubdivistion = {
+  /** ISO 3166-2 subdivision code */
+  code?: Maybe<Scalars['String']>
+}
+
+export type GeographicalAreaSubdivistionCreateInput = {
+  code?: Maybe<Scalars['String']>
+}
+
+export type GeographicalAreaSubdivistionUpdateInput = {
+  code?: Maybe<Scalars['String']>
+}
+
+export type GeographicalAreaSubdivistionWhereInput = {
+  id_eq?: Maybe<Scalars['ID']>
+  id_in?: Maybe<Array<Scalars['ID']>>
+  createdAt_eq?: Maybe<Scalars['DateTime']>
+  createdAt_lt?: Maybe<Scalars['DateTime']>
+  createdAt_lte?: Maybe<Scalars['DateTime']>
+  createdAt_gt?: Maybe<Scalars['DateTime']>
+  createdAt_gte?: Maybe<Scalars['DateTime']>
+  createdById_eq?: Maybe<Scalars['ID']>
+  createdById_in?: Maybe<Array<Scalars['ID']>>
+  updatedAt_eq?: Maybe<Scalars['DateTime']>
+  updatedAt_lt?: Maybe<Scalars['DateTime']>
+  updatedAt_lte?: Maybe<Scalars['DateTime']>
+  updatedAt_gt?: Maybe<Scalars['DateTime']>
+  updatedAt_gte?: Maybe<Scalars['DateTime']>
+  updatedById_eq?: Maybe<Scalars['ID']>
+  updatedById_in?: Maybe<Array<Scalars['ID']>>
+  deletedAt_all?: Maybe<Scalars['Boolean']>
+  deletedAt_eq?: Maybe<Scalars['DateTime']>
+  deletedAt_lt?: Maybe<Scalars['DateTime']>
+  deletedAt_lte?: Maybe<Scalars['DateTime']>
+  deletedAt_gt?: Maybe<Scalars['DateTime']>
+  deletedAt_gte?: Maybe<Scalars['DateTime']>
+  deletedById_eq?: Maybe<Scalars['ID']>
+  deletedById_in?: Maybe<Array<Scalars['ID']>>
+  code_eq?: Maybe<Scalars['String']>
+  code_contains?: Maybe<Scalars['String']>
+  code_startsWith?: Maybe<Scalars['String']>
+  code_endsWith?: Maybe<Scalars['String']>
+  code_in?: Maybe<Array<Scalars['String']>>
+  AND?: Maybe<Array<GeographicalAreaSubdivistionWhereInput>>
+  OR?: Maybe<Array<GeographicalAreaSubdivistionWhereInput>>
+}
+
+export type GeographicalAreaSubdivistionWhereUniqueInput = {
+  id: Scalars['ID']
+}
+
 export type Language = BaseGraphQlObject & {
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
@@ -1698,6 +1915,9 @@ export type Query = {
   curatorGroups: Array<CuratorGroup>
   curatorGroupByUniqueInput?: Maybe<CuratorGroup>
   curatorGroupsConnection: CuratorGroupConnection
+  distributionBucketFamilyGeographicAreas: Array<DistributionBucketFamilyGeographicArea>
+  distributionBucketFamilyGeographicAreaByUniqueInput?: Maybe<DistributionBucketFamilyGeographicArea>
+  distributionBucketFamilyGeographicAreasConnection: DistributionBucketFamilyGeographicAreaConnection
   distributionBucketFamilyMetadata: Array<DistributionBucketFamilyMetadata>
   distributionBucketFamilyMetadataByUniqueInput?: Maybe<DistributionBucketFamilyMetadata>
   distributionBucketFamilyMetadataConnection: DistributionBucketFamilyMetadataConnection
@@ -1830,6 +2050,26 @@ export type QueryCuratorGroupsConnectionArgs = {
   orderBy?: Maybe<Array<CuratorGroupOrderByInput>>
 }
 
+export type QueryDistributionBucketFamilyGeographicAreasArgs = {
+  offset?: Maybe<Scalars['Int']>
+  limit?: Maybe<Scalars['Int']>
+  where?: Maybe<DistributionBucketFamilyGeographicAreaWhereInput>
+  orderBy?: Maybe<Array<DistributionBucketFamilyGeographicAreaOrderByInput>>
+}
+
+export type QueryDistributionBucketFamilyGeographicAreaByUniqueInputArgs = {
+  where: DistributionBucketFamilyGeographicAreaWhereUniqueInput
+}
+
+export type QueryDistributionBucketFamilyGeographicAreasConnectionArgs = {
+  first?: Maybe<Scalars['Int']>
+  after?: Maybe<Scalars['String']>
+  last?: Maybe<Scalars['Int']>
+  before?: Maybe<Scalars['String']>
+  where?: Maybe<DistributionBucketFamilyGeographicAreaWhereInput>
+  orderBy?: Maybe<Array<DistributionBucketFamilyGeographicAreaOrderByInput>>
+}
+
 export type QueryDistributionBucketFamilyMetadataArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
@@ -3296,6 +3536,16 @@ export type StorageDataObject = BaseGraphQlObject & {
   storageBagId: Scalars['String']
   /** IPFS content hash */
   ipfsHash: Scalars['String']
+  /** The type of the asset that the data object represents (if known) */
+  type: DataObjectType
+  /** Prize for removing the data object */
+  deletionPrize: Scalars['BigInt']
+  /** If the object is no longer used as an asset - the time at which it was unset (if known) */
+  unsetAt?: Maybe<Scalars['DateTime']>
+  channelcoverPhoto?: Maybe<Array<Channel>>
+  channelavatarPhoto?: Maybe<Array<Channel>>
+  videothumbnailPhoto?: Maybe<Array<Video>>
+  videomedia?: Maybe<Array<Video>>
 }
 
 export type StorageDataObjectConnection = {
@@ -3309,6 +3559,9 @@ export type StorageDataObjectCreateInput = {
   size: Scalars['BigInt']
   storageBag: Scalars['ID']
   ipfsHash: Scalars['String']
+  type: Scalars['JSONObject']
+  deletionPrize: Scalars['BigInt']
+  unsetAt?: Maybe<Scalars['DateTime']>
 }
 
 export type StorageDataObjectEdge = {
@@ -3331,6 +3584,10 @@ export enum StorageDataObjectOrderByInput {
   StorageBagDesc = 'storageBag_DESC',
   IpfsHashAsc = 'ipfsHash_ASC',
   IpfsHashDesc = 'ipfsHash_DESC',
+  DeletionPrizeAsc = 'deletionPrize_ASC',
+  DeletionPrizeDesc = 'deletionPrize_DESC',
+  UnsetAtAsc = 'unsetAt_ASC',
+  UnsetAtDesc = 'unsetAt_DESC',
 }
 
 export type StorageDataObjectUpdateInput = {
@@ -3338,6 +3595,9 @@ export type StorageDataObjectUpdateInput = {
   size?: Maybe<Scalars['BigInt']>
   storageBag?: Maybe<Scalars['ID']>
   ipfsHash?: Maybe<Scalars['String']>
+  type?: Maybe<Scalars['JSONObject']>
+  deletionPrize?: Maybe<Scalars['BigInt']>
+  unsetAt?: Maybe<Scalars['DateTime']>
 }
 
 export type StorageDataObjectWhereInput = {
@@ -3380,7 +3640,31 @@ export type StorageDataObjectWhereInput = {
   ipfsHash_startsWith?: Maybe<Scalars['String']>
   ipfsHash_endsWith?: Maybe<Scalars['String']>
   ipfsHash_in?: Maybe<Array<Scalars['String']>>
+  type_json?: Maybe<Scalars['JSONObject']>
+  deletionPrize_eq?: Maybe<Scalars['BigInt']>
+  deletionPrize_gt?: Maybe<Scalars['BigInt']>
+  deletionPrize_gte?: Maybe<Scalars['BigInt']>
+  deletionPrize_lt?: Maybe<Scalars['BigInt']>
+  deletionPrize_lte?: Maybe<Scalars['BigInt']>
+  deletionPrize_in?: Maybe<Array<Scalars['BigInt']>>
+  unsetAt_eq?: Maybe<Scalars['DateTime']>
+  unsetAt_lt?: Maybe<Scalars['DateTime']>
+  unsetAt_lte?: Maybe<Scalars['DateTime']>
+  unsetAt_gt?: Maybe<Scalars['DateTime']>
+  unsetAt_gte?: Maybe<Scalars['DateTime']>
   storageBag?: Maybe<StorageBagWhereInput>
+  channelcoverPhoto_none?: Maybe<ChannelWhereInput>
+  channelcoverPhoto_some?: Maybe<ChannelWhereInput>
+  channelcoverPhoto_every?: Maybe<ChannelWhereInput>
+  channelavatarPhoto_none?: Maybe<ChannelWhereInput>
+  channelavatarPhoto_some?: Maybe<ChannelWhereInput>
+  channelavatarPhoto_every?: Maybe<ChannelWhereInput>
+  videothumbnailPhoto_none?: Maybe<VideoWhereInput>
+  videothumbnailPhoto_some?: Maybe<VideoWhereInput>
+  videothumbnailPhoto_every?: Maybe<VideoWhereInput>
+  videomedia_none?: Maybe<VideoWhereInput>
+  videomedia_some?: Maybe<VideoWhereInput>
+  videomedia_every?: Maybe<VideoWhereInput>
   AND?: Maybe<Array<StorageDataObjectWhereInput>>
   OR?: Maybe<Array<StorageDataObjectWhereInput>>
 }
@@ -3567,8 +3851,8 @@ export type Video = BaseGraphQlObject & {
   description?: Maybe<Scalars['String']>
   /** Video duration in seconds */
   duration?: Maybe<Scalars['Int']>
-  /** Video thumbnail asset (recommended ratio: 16:9) */
-  thumbnailPhoto: Asset
+  thumbnailPhoto?: Maybe<StorageDataObject>
+  thumbnailPhotoId?: Maybe<Scalars['String']>
   language?: Maybe<Language>
   languageId?: Maybe<Scalars['String']>
   /** Whether or not Video contains marketing */
@@ -3583,8 +3867,8 @@ export type Video = BaseGraphQlObject & {
   isExplicit?: Maybe<Scalars['Boolean']>
   license?: Maybe<License>
   licenseId?: Maybe<Scalars['String']>
-  /** Video media asset */
-  media: Asset
+  media?: Maybe<StorageDataObject>
+  mediaId?: Maybe<Scalars['String']>
   mediaMetadata?: Maybe<VideoMediaMetadata>
   mediaMetadataId?: Maybe<Scalars['String']>
   createdInBlock: Scalars['Int']
@@ -3709,7 +3993,7 @@ export type VideoCreateInput = {
   title?: Maybe<Scalars['String']>
   description?: Maybe<Scalars['String']>
   duration?: Maybe<Scalars['Float']>
-  thumbnailPhoto: Scalars['JSONObject']
+  thumbnailPhoto?: Maybe<Scalars['ID']>
   language?: Maybe<Scalars['ID']>
   hasMarketing?: Maybe<Scalars['Boolean']>
   publishedBeforeJoystream?: Maybe<Scalars['DateTime']>
@@ -3717,7 +4001,7 @@ export type VideoCreateInput = {
   isCensored: Scalars['Boolean']
   isExplicit?: Maybe<Scalars['Boolean']>
   license?: Maybe<Scalars['ID']>
-  media: Scalars['JSONObject']
+  media?: Maybe<Scalars['ID']>
   mediaMetadata?: Maybe<Scalars['ID']>
   createdInBlock: Scalars['Float']
   isFeatured: Scalars['Boolean']
@@ -3980,6 +4264,8 @@ export enum VideoOrderByInput {
   DescriptionDesc = 'description_DESC',
   DurationAsc = 'duration_ASC',
   DurationDesc = 'duration_DESC',
+  ThumbnailPhotoAsc = 'thumbnailPhoto_ASC',
+  ThumbnailPhotoDesc = 'thumbnailPhoto_DESC',
   LanguageAsc = 'language_ASC',
   LanguageDesc = 'language_DESC',
   HasMarketingAsc = 'hasMarketing_ASC',
@@ -3994,6 +4280,8 @@ export enum VideoOrderByInput {
   IsExplicitDesc = 'isExplicit_DESC',
   LicenseAsc = 'license_ASC',
   LicenseDesc = 'license_DESC',
+  MediaAsc = 'media_ASC',
+  MediaDesc = 'media_DESC',
   MediaMetadataAsc = 'mediaMetadata_ASC',
   MediaMetadataDesc = 'mediaMetadata_DESC',
   CreatedInBlockAsc = 'createdInBlock_ASC',
@@ -4008,7 +4296,7 @@ export type VideoUpdateInput = {
   title?: Maybe<Scalars['String']>
   description?: Maybe<Scalars['String']>
   duration?: Maybe<Scalars['Float']>
-  thumbnailPhoto?: Maybe<Scalars['JSONObject']>
+  thumbnailPhoto?: Maybe<Scalars['ID']>
   language?: Maybe<Scalars['ID']>
   hasMarketing?: Maybe<Scalars['Boolean']>
   publishedBeforeJoystream?: Maybe<Scalars['DateTime']>
@@ -4016,7 +4304,7 @@ export type VideoUpdateInput = {
   isCensored?: Maybe<Scalars['Boolean']>
   isExplicit?: Maybe<Scalars['Boolean']>
   license?: Maybe<Scalars['ID']>
-  media?: Maybe<Scalars['JSONObject']>
+  media?: Maybe<Scalars['ID']>
   mediaMetadata?: Maybe<Scalars['ID']>
   createdInBlock?: Maybe<Scalars['Float']>
   isFeatured?: Maybe<Scalars['Boolean']>
@@ -4067,7 +4355,8 @@ export type VideoWhereInput = {
   duration_lt?: Maybe<Scalars['Int']>
   duration_lte?: Maybe<Scalars['Int']>
   duration_in?: Maybe<Array<Scalars['Int']>>
-  thumbnailPhoto_json?: Maybe<Scalars['JSONObject']>
+  thumbnailPhoto_eq?: Maybe<Scalars['ID']>
+  thumbnailPhoto_in?: Maybe<Array<Scalars['ID']>>
   language_eq?: Maybe<Scalars['ID']>
   language_in?: Maybe<Array<Scalars['ID']>>
   hasMarketing_eq?: Maybe<Scalars['Boolean']>
@@ -4085,7 +4374,8 @@ export type VideoWhereInput = {
   isExplicit_in?: Maybe<Array<Scalars['Boolean']>>
   license_eq?: Maybe<Scalars['ID']>
   license_in?: Maybe<Array<Scalars['ID']>>
-  media_json?: Maybe<Scalars['JSONObject']>
+  media_eq?: Maybe<Scalars['ID']>
+  media_in?: Maybe<Array<Scalars['ID']>>
   mediaMetadata_eq?: Maybe<Scalars['ID']>
   mediaMetadata_in?: Maybe<Array<Scalars['ID']>>
   createdInBlock_eq?: Maybe<Scalars['Int']>
@@ -4098,8 +4388,10 @@ export type VideoWhereInput = {
   isFeatured_in?: Maybe<Array<Scalars['Boolean']>>
   channel?: Maybe<ChannelWhereInput>
   category?: Maybe<VideoCategoryWhereInput>
+  thumbnailPhoto?: Maybe<StorageDataObjectWhereInput>
   language?: Maybe<LanguageWhereInput>
   license?: Maybe<LicenseWhereInput>
+  media?: Maybe<StorageDataObjectWhereInput>
   mediaMetadata?: Maybe<VideoMediaMetadataWhereInput>
   AND?: Maybe<Array<VideoWhereInput>>
   OR?: Maybe<Array<VideoWhereInput>>

+ 32 - 0
cli/src/graphql/queries/storage.graphql

@@ -16,3 +16,35 @@ query getStorageNodesInfoByBagId($bagId: String) {
     ...StorageNodeInfo
   }
 }
+
+fragment DataObjectInfo on StorageDataObject {
+  id
+  size
+  deletionPrize
+  type {
+    __typename
+      ... on DataObjectTypeVideoMedia { video { id } }
+      ... on DataObjectTypeVideoThumbnail { video { id } }
+      ... on DataObjectTypeChannelAvatar { channel { id } }
+      ... on DataObjectTypeChannelCoverPhoto { channel { id } }
+  }
+}
+
+query getDataObjectsByBagId($bagId: ID) {
+  storageDataObjects(where: { storageBag: { id_eq: $bagId } }) {
+    ...DataObjectInfo
+  }
+}
+
+query getDataObjectsChannelId($channelId: ID) {
+  storageDataObjects(where: { type_json: { channelId_eq: $channelId } }) {
+    ...DataObjectInfo
+  }
+}
+
+query getDataObjectsByVideoId($videoId: ID) {
+  storageDataObjects(where: { type_json: { videoId_eq: $videoId } }) {
+    ...DataObjectInfo
+  }
+}
+