Browse Source

Merge branch 'sumer' into sumer-new-substrate-update-apps

Mokhtar Naamani 3 years ago
parent
commit
ea2cecbe7e
3 changed files with 139 additions and 50 deletions
  1. 10 0
      query-node/mappings/src/common.ts
  2. 128 50
      query-node/mappings/src/content/utils.ts
  3. 1 0
      query-node/package.json

+ 10 - 0
query-node/mappings/src/common.ts

@@ -28,6 +28,16 @@ export function inconsistentState(extraInfo: string, data?: unknown): void {
   logger.error(errorMessage, data)
   logger.error(errorMessage, data)
 }
 }
 
 
+/*
+  Reports that metadata inserted by the user are not entirely valid, but the problem can be overcome.
+*/
+export function invalidMetadata(extraInfo: string, data?: unknown): void {
+  const errorMessage = 'Invalid metadata: ' + extraInfo
+
+  // log error
+  logger.info(errorMessage, data)
+}
+
 /*
 /*
   Prepares data object from content parameters.
   Prepares data object from content parameters.
 */
 */

+ 128 - 50
query-node/mappings/src/content/utils.ts

@@ -9,10 +9,10 @@
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { DatabaseManager } from '@dzlzv/hydra-db-utils'
 import { DatabaseManager } from '@dzlzv/hydra-db-utils'
 import { Bytes } from '@polkadot/types'
 import { Bytes } from '@polkadot/types'
-import ISO6391 from 'iso-639-1';
-import { u64 } from '@polkadot/types/primitive';
+import ISO6391 from 'iso-639-1'
+import { u64 } from '@polkadot/types/primitive'
 import { FindConditions } from 'typeorm'
 import { FindConditions } from 'typeorm'
-import * as jspb from "google-protobuf";
+import * as jspb from "google-protobuf"
 
 
 // protobuf definitions
 // protobuf definitions
 import {
 import {
@@ -30,7 +30,7 @@ import {
 } from '../../../generated/types'
 } from '../../../generated/types'
 
 
 import {
 import {
-  inconsistentState,
+  invalidMetadata,
   logger,
   logger,
   prepareDataObject,
   prepareDataObject,
 } from '../common'
 } from '../common'
@@ -99,6 +99,69 @@ export interface IReadProtobufArgumentsWithAssets extends IReadProtobufArguments
   contentOwner: typeof DataObjectOwner
   contentOwner: typeof DataObjectOwner
 }
 }
 
 
+/*
+  This class represents one of 3 possible states when changing property read from metadata.
+  NoChange - don't change anything (used when invalid metadata are encountered)
+  Unset - unset the value (used when the unset is requested in runtime)
+  Change - set the new value
+*/
+export class PropertyChange<T> {
+
+  static newUnset<T>() {
+    return new PropertyChange<T>('unset')
+  }
+
+  static newNoChange<T>() {
+    return new PropertyChange<T>('nochange')
+  }
+
+  static newChange<T>(value: T) {
+    return new PropertyChange<T>('change', value)
+  }
+
+  private type: string
+  private value?: T
+
+  private constructor(type: 'change' | 'nochange' | 'unset', value?: T) {
+    this.type = type
+    this.value = value
+  }
+
+  public isUnset(): boolean {
+    return this.type == 'unset'
+  }
+
+  public isNoChange(): boolean {
+    return this.type == 'nochange'
+  }
+
+  public isValue(): boolean {
+    return this.type == 'change'
+  }
+
+  public getValue(): T | undefined {
+    return this.type == 'change'
+      ? this.value
+      : undefined
+  }
+
+  /*
+    Integrates the value into the given dictionary.
+  */
+  public integrateInto(object: Object, key: string): void {
+    if (this.isNoChange()) {
+      return
+    }
+
+    if (this.isUnset()) {
+      delete object[key]
+      return
+    }
+
+    object[key] = this.value
+  }
+}
+
 /*
 /*
   Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db.
   Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db.
 */
 */
@@ -107,7 +170,7 @@ export async function readProtobuf<T extends ChannelCategory | VideoCategory>(
   parameters: IReadProtobufArguments,
   parameters: IReadProtobufArguments,
 ): Promise<Partial<T>> {
 ): Promise<Partial<T>> {
   // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
   // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
-  const metaU8a = parameters.metadata.toU8a(true);
+  const metaU8a = parameters.metadata.toU8a(true)
 
 
   // process channel category
   // process channel category
   if (type instanceof ChannelCategory) {
   if (type instanceof ChannelCategory) {
@@ -140,7 +203,7 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
   parameters: IReadProtobufArgumentsWithAssets,
   parameters: IReadProtobufArgumentsWithAssets,
 ): Promise<Partial<T>> {
 ): Promise<Partial<T>> {
   // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
   // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
-  const metaU8a = parameters.metadata.toU8a(true);
+  const metaU8a = parameters.metadata.toU8a(true)
 
 
   // process channel
   // process channel
   if (type instanceof Channel) {
   if (type instanceof Channel) {
@@ -177,7 +240,9 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
 
 
     // prepare language if needed
     // prepare language if needed
     if ('language' in metaAsObject) {
     if ('language' in metaAsObject) {
-      result.language = await prepareLanguage(metaAsObject.language, parameters.db, parameters.blockNumber)
+      const language = await prepareLanguage(metaAsObject.language, parameters.db, parameters.blockNumber)
+      delete metaAsObject.language // make sure temporary value will not interfere
+      language.integrateInto(result, 'language')
     }
     }
 
 
     return result as Partial<T>
     return result as Partial<T>
@@ -191,16 +256,22 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
 
 
     // prepare video category if needed
     // prepare video category if needed
     if ('category' in metaAsObject) {
     if ('category' in metaAsObject) {
-      result.category = await prepareVideoCategory(metaAsObject.category, parameters.db)
+      const category = await prepareVideoCategory(metaAsObject.category, parameters.db)
+      delete metaAsObject.category // make sure temporary value will not interfere
+      category.integrateInto(result, 'category')
     }
     }
 
 
     // prepare media meta information if needed
     // prepare media meta information if needed
-    if ('mediaType' in metaAsObject) {
+    if ('mediaType' in metaAsObject || 'mediaPixelWidth' in metaAsObject || 'mediaPixelHeight' in metaAsObject) {
       // prepare video file size if poosible
       // prepare video file size if poosible
       const videoSize = await extractVideoSize(parameters.assets, metaAsObject.video)
       const videoSize = await extractVideoSize(parameters.assets, metaAsObject.video)
 
 
       result.mediaMetadata = await prepareVideoMetadata(metaAsObject, videoSize, parameters.blockNumber)
       result.mediaMetadata = await prepareVideoMetadata(metaAsObject, videoSize, parameters.blockNumber)
+
+      // remove extra values
       delete metaAsObject.mediaType
       delete metaAsObject.mediaType
+      delete metaAsObject.mediaPixelWidth
+      delete metaAsObject.mediaPixelHeight
     }
     }
 
 
     // prepare license if needed
     // prepare license if needed
@@ -217,7 +288,7 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
         blockNumber: parameters.blockNumber,
         blockNumber: parameters.blockNumber,
         contentOwner: parameters.contentOwner,
         contentOwner: parameters.contentOwner,
       })
       })
-      integrateAsset('thumbnail', result, asset) // changes `result` inline!
+      integrateAsset('thumbnailPhoto', result, asset) // changes `result` inline!
       delete metaAsObject.thumbnailPhoto
       delete metaAsObject.thumbnailPhoto
     }
     }
 
 
@@ -236,7 +307,9 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
 
 
     // prepare language if needed
     // prepare language if needed
     if ('language' in metaAsObject) {
     if ('language' in metaAsObject) {
-      result.language = await prepareLanguage(metaAsObject.language, parameters.db, parameters.blockNumber)
+      const language = await prepareLanguage(metaAsObject.language, parameters.db, parameters.blockNumber)
+      delete metaAsObject.language // make sure temporary value will not interfere
+      language.integrateInto(result, 'language')
     }
     }
 
 
     // prepare information about media published somewhere else before Joystream if needed.
     // prepare information about media published somewhere else before Joystream if needed.
@@ -265,12 +338,8 @@ export async function convertContentActorToChannelOwner(db: DatabaseManager, con
 
 
     // ensure member exists
     // ensure member exists
     if (!member) {
     if (!member) {
-      inconsistentState(`Actor is non-existing member`, memberId)
-      return {
-        // this will clear fields
-        ownerMember: undefined,
-        ownerCuratorGroup: undefined,
-      }
+      invalidMetadata(`Actor is non-existing member`, memberId)
+      return {} // this will keep fields unchanged
     }
     }
 
 
     return {
     return {
@@ -285,12 +354,8 @@ export async function convertContentActorToChannelOwner(db: DatabaseManager, con
 
 
     // ensure curator group exists
     // ensure curator group exists
     if (!curatorGroup) {
     if (!curatorGroup) {
-      inconsistentState('Actor is non-existing curator group', curatorGroupId)
-      return {
-        // this will clear fields
-        ownerMember: undefined,
-        ownerCuratorGroup: undefined,
-      }
+      invalidMetadata('Actor is non-existing curator group', curatorGroupId)
+      return {} // this will keep fields unchanged
     }
     }
 
 
     return {
     return {
@@ -381,28 +446,30 @@ interface IExtractAssetParameters {
 /*
 /*
   Selects asset from provided set of assets and prepares asset data fit to be saved to db.
   Selects asset from provided set of assets and prepares asset data fit to be saved to db.
 */
 */
-async function extractAsset(parameters: IExtractAssetParameters): Promise<AssetStorageOrUrls | undefined> {
+async function extractAsset(parameters: IExtractAssetParameters): Promise<PropertyChange<AssetStorageOrUrls>> {
   // is asset being unset?
   // is asset being unset?
   if (parameters.assetIndex === undefined) {
   if (parameters.assetIndex === undefined) {
-    return undefined
+    return PropertyChange.newUnset()
   }
   }
 
 
   // ensure asset index is valid
   // ensure asset index is valid
   if (parameters.assetIndex >= parameters.assets.length) {
   if (parameters.assetIndex >= parameters.assets.length) {
-    inconsistentState(`Non-existing asset extraction requested`, {
+    invalidMetadata(`Non-existing asset extraction requested`, {
       assetsProvided: parameters.assets.length,
       assetsProvided: parameters.assets.length,
       assetIndex: parameters.assetIndex,
       assetIndex: parameters.assetIndex,
     })
     })
-    return undefined
+    return PropertyChange.newNoChange()
   }
   }
 
 
   // convert asset to data object record
   // convert asset to data object record
-  return convertAsset({
+  const asset = await convertAsset({
     rawAsset: parameters.assets[parameters.assetIndex],
     rawAsset: parameters.assets[parameters.assetIndex],
     db: parameters.db,
     db: parameters.db,
     blockNumber: parameters.blockNumber,
     blockNumber: parameters.blockNumber,
     contentOwner: parameters.contentOwner,
     contentOwner: parameters.contentOwner,
   })
   })
+
+  return PropertyChange.newChange(asset)
 }
 }
 
 
 /*
 /*
@@ -411,30 +478,38 @@ async function extractAsset(parameters: IExtractAssetParameters): Promise<AssetS
 
 
   Changes `result` argument!
   Changes `result` argument!
 */
 */
-function integrateAsset<T>(propertyName: string, result: Object, asset: AssetStorageOrUrls | undefined) {
+function integrateAsset<T>(propertyName: string, result: Object, asset: PropertyChange<AssetStorageOrUrls>): void {
   // helpers - property names
   // helpers - property names
   const nameUrl = propertyName + 'Urls'
   const nameUrl = propertyName + 'Urls'
   const nameDataObject = propertyName + 'DataObject'
   const nameDataObject = propertyName + 'DataObject'
   const nameAvailability = propertyName + 'Availability'
   const nameAvailability = propertyName + 'Availability'
 
 
-  if (asset === undefined) {
+  if (asset.isNoChange()) {
+    return
+  }
+
+  if (asset.isUnset()) {
     result[nameUrl] = []
     result[nameUrl] = []
     result[nameAvailability] = AssetAvailability.INVALID
     result[nameAvailability] = AssetAvailability.INVALID
     result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
     result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
 
 
-    return result
+    return
   }
   }
 
 
-  // is asset saved in storage?
-  if (!isAssetInStorage(asset)) {
+  const newValue = asset.getValue() as AssetStorageOrUrls
+
+  // is asset available on external URL(s)
+  if (!isAssetInStorage(newValue)) {
     // (un)set asset's properties
     // (un)set asset's properties
-    result[nameUrl] = asset
+    result[nameUrl] = newValue
     result[nameAvailability] = AssetAvailability.ACCEPTED
     result[nameAvailability] = AssetAvailability.ACCEPTED
     result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
     result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
 
 
-    return result
+    return
   }
   }
 
 
+  // asset saved in storage
+
   // prepare conversion table between liaison judgment and asset availability
   // prepare conversion table between liaison judgment and asset availability
   const conversionTable = {
   const conversionTable = {
     [LiaisonJudgement.ACCEPTED]: AssetAvailability.ACCEPTED,
     [LiaisonJudgement.ACCEPTED]: AssetAvailability.ACCEPTED,
@@ -443,8 +518,8 @@ function integrateAsset<T>(propertyName: string, result: Object, asset: AssetSto
 
 
   // (un)set asset's properties
   // (un)set asset's properties
   result[nameUrl] = [] // plan deletion (will have effect when saved to db)
   result[nameUrl] = [] // plan deletion (will have effect when saved to db)
-  result[nameAvailability] = conversionTable[asset.liaisonJudgement]
-  result[nameDataObject] = asset
+  result[nameAvailability] = conversionTable[newValue.liaisonJudgement]
+  result[nameDataObject] = newValue
 }
 }
 
 
 async function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): Promise<number | undefined> {
 async function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): Promise<number | undefined> {
@@ -455,7 +530,7 @@ async function extractVideoSize(assets: NewAsset[], assetIndex: number | undefin
 
 
   // ensure asset index is valid
   // ensure asset index is valid
   if (assetIndex > assets.length) {
   if (assetIndex > assets.length) {
-    inconsistentState(`Non-existing asset video size extraction requested`, {assetsProvided: assets.length, assetIndex})
+    invalidMetadata(`Non-existing asset video size extraction requested`, {assetsProvided: assets.length, assetIndex})
     return undefined
     return undefined
   }
   }
 
 
@@ -476,19 +551,19 @@ async function extractVideoSize(assets: NewAsset[], assetIndex: number | undefin
   return videoSize
   return videoSize
 }
 }
 
 
-async function prepareLanguage(languageIso: string | undefined, db: DatabaseManager, blockNumber: number): Promise<Language | undefined> {
+async function prepareLanguage(languageIso: string | undefined, db: DatabaseManager, blockNumber: number): Promise<PropertyChange<Language>> {
   // is language being unset?
   // is language being unset?
   if (languageIso === undefined) {
   if (languageIso === undefined) {
-    return undefined
+    return PropertyChange.newUnset()
   }
   }
 
 
   // validate language string
   // validate language string
-  const isValidIso = ISO6391.validate(languageIso);
+  const isValidIso = ISO6391.validate(languageIso)
 
 
   // ensure language string is valid
   // ensure language string is valid
   if (!isValidIso) {
   if (!isValidIso) {
-    inconsistentState(`Invalid language ISO-639-1 provided`, languageIso)
-    return undefined
+    invalidMetadata(`Invalid language ISO-639-1 provided`, languageIso)
+    return PropertyChange.newNoChange()
   }
   }
 
 
   // load language
   // load language
@@ -496,7 +571,7 @@ async function prepareLanguage(languageIso: string | undefined, db: DatabaseMana
 
 
   // return existing language if any
   // return existing language if any
   if (language) {
   if (language) {
-    return language;
+    return PropertyChange.newChange(language)
   }
   }
 
 
 
 
@@ -512,7 +587,7 @@ async function prepareLanguage(languageIso: string | undefined, db: DatabaseMana
 
 
   await db.save<Language>(newLanguage)
   await db.save<Language>(newLanguage)
 
 
-  return newLanguage
+  return PropertyChange.newChange(newLanguage)
 }
 }
 
 
 async function prepareLicense(licenseProtobuf: LicenseMetadata.AsObject | undefined): Promise<License | undefined> {
 async function prepareLicense(licenseProtobuf: LicenseMetadata.AsObject | undefined): Promise<License | undefined> {
@@ -536,6 +611,9 @@ async function prepareLicense(licenseProtobuf: LicenseMetadata.AsObject | undefi
 }
 }
 
 
 async function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject, videoSize: number | undefined, blockNumber: number): Promise<VideoMediaMetadata> {
 async function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject, videoSize: number | undefined, blockNumber: number): Promise<VideoMediaMetadata> {
+  // TODO: handle situations when only some metadata are set (e.g. pixelWidth and mediaType is defined, but pixelHeight is missing)
+  // TODO: handle update of VideoMediaEncoding and VideoMediaMetadata
+  //       right now when only some of mediaType(mb partial), mediaPixelWidth, or mediaPixelHeight is set, the update discards previous values
   // create new encoding info
   // create new encoding info
   const encoding = new VideoMediaEncoding({
   const encoding = new VideoMediaEncoding({
     ...videoProtobuf.mediaType,
     ...videoProtobuf.mediaType,
@@ -563,10 +641,10 @@ async function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject, video
   return videoMeta
   return videoMeta
 }
 }
 
 
-async function prepareVideoCategory(categoryId: number | undefined, db: DatabaseManager): Promise<VideoCategory | undefined> {
+async function prepareVideoCategory(categoryId: number | undefined, db: DatabaseManager): Promise<PropertyChange<VideoCategory>> {
   // is category being unset?
   // is category being unset?
   if (categoryId === undefined) {
   if (categoryId === undefined) {
-    return undefined
+    return PropertyChange.newUnset()
   }
   }
 
 
   // load video category
   // load video category
@@ -574,11 +652,11 @@ async function prepareVideoCategory(categoryId: number | undefined, db: Database
 
 
   // ensure video category exists
   // ensure video category exists
   if (!category) {
   if (!category) {
-    inconsistentState('Non-existing video category association with video requested', categoryId)
-    return undefined
+    invalidMetadata('Non-existing video category association with video requested', categoryId)
+    return PropertyChange.newNoChange()
   }
   }
 
 
-  return category
+  return PropertyChange.newChange(category)
 }
 }
 
 
 function convertMetadataToObject<T extends Object>(metadata: jspb.Message): T {
 function convertMetadataToObject<T extends Object>(metadata: jspb.Message): T {

+ 1 - 0
query-node/package.json

@@ -21,6 +21,7 @@
     "db:processor:migrate": "hydra-processor migrate --env ../.env",
     "db:processor:migrate": "hydra-processor migrate --env ../.env",
     "db:migrate": "yarn db:schema:migrate && yarn db:processor:migrate",
     "db:migrate": "yarn db:schema:migrate && yarn db:processor:migrate",
     "db:bootstrap": "yarn db:create && yarn db:prepare && yarn db:migrate",
     "db:bootstrap": "yarn db:create && yarn db:prepare && yarn db:migrate",
+    "db:reset": "yarn db:drop && yarn db:prepare && yarn db:migrate",
     "bootstrap": "yarn codegen && yarn db:drop && yarn db:bootstrap",
     "bootstrap": "yarn codegen && yarn db:drop && yarn db:bootstrap",
     "codegen": "hydra-cli codegen",
     "codegen": "hydra-cli codegen",
     "codegen:noinstall": "hydra-cli codegen --no-install",
     "codegen:noinstall": "hydra-cli codegen --no-install",