utils.ts 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773
  1. // TODO: finish db cascade on save/remove; right now there is manually added `cascade: ["insert", "update"]` directive
  2. // to all relations in `query-node/generated/graphql-server/src/modules/**/*.model.ts`. That should ensure all records
  3. // are saved on one `db.save(...)` call. Missing features
  4. // - find a proper way to cascade on remove or implement custom removals for every entity
  5. // - convert manual changes done to `*model.ts` file into some patch or bash commands that can be executed
  6. // every time query node codegen is run (that will overwrite said manual changes)
  7. // - verify in integration tests that the records are trully created/updated/removed as expected
  8. import { SubstrateEvent } from '@dzlzv/hydra-common'
  9. import { DatabaseManager } from '@dzlzv/hydra-db-utils'
  10. import { Bytes } from '@polkadot/types'
  11. import ISO6391 from 'iso-639-1'
  12. import { u64 } from '@polkadot/types/primitive'
  13. import { FindConditions } from 'typeorm'
  14. import * as jspb from 'google-protobuf'
  15. import { fixBlockTimestamp } from '../eventFix'
  16. // protobuf definitions
  17. import {
  18. ChannelMetadata,
  19. ChannelCategoryMetadata,
  20. PublishedBeforeJoystream as PublishedBeforeJoystreamMetadata,
  21. License as LicenseMetadata,
  22. MediaType as MediaTypeMetadata,
  23. VideoMetadata,
  24. VideoCategoryMetadata,
  25. } from '@joystream/content-metadata-protobuf'
  26. import { Content } from '../../../generated/types'
  27. import { invalidMetadata, inconsistentState, logger, prepareDataObject, getNextId } from '../common'
  28. import {
  29. // primary entities
  30. CuratorGroup,
  31. Channel,
  32. ChannelCategory,
  33. Video,
  34. VideoCategory,
  35. // secondary entities
  36. Language,
  37. License,
  38. VideoMediaEncoding,
  39. VideoMediaMetadata,
  40. // asset
  41. DataObjectOwner,
  42. DataObjectOwnerMember,
  43. DataObjectOwnerChannel,
  44. DataObject,
  45. LiaisonJudgement,
  46. AssetAvailability,
  47. Membership,
  48. } from 'query-node'
  49. // Joystream types
  50. import { ChannelId, ContentParameters, NewAsset, ContentActor } from '@joystream/types/augment'
  51. import { ContentParameters as Custom_ContentParameters } from '@joystream/types/storage'
  52. import { registry } from '@joystream/types'
  53. /*
  54. Asset either stored in storage or describing list of URLs.
  55. */
  56. type AssetStorageOrUrls = DataObject | string[]
  57. /*
  58. Type guard differentiating asset stored in storage from asset describing a list of URLs.
  59. */
  60. function isAssetInStorage(dataObject: AssetStorageOrUrls): dataObject is DataObject {
  61. if (Array.isArray(dataObject)) {
  62. return false
  63. }
  64. return true
  65. }
  66. export interface IReadProtobufArguments {
  67. metadata: Bytes
  68. db: DatabaseManager
  69. event: SubstrateEvent
  70. }
  71. export interface IReadProtobufArgumentsWithAssets extends IReadProtobufArguments {
  72. assets: NewAsset[] // assets provided in event
  73. contentOwner: typeof DataObjectOwner
  74. }
  75. /*
  76. This class represents one of 3 possible states when changing property read from metadata.
  77. NoChange - don't change anything (used when invalid metadata are encountered)
  78. Unset - unset the value (used when the unset is requested in runtime)
  79. Change - set the new value
  80. */
  81. export class PropertyChange<T> {
  82. static newUnset<T>(): PropertyChange<T> {
  83. return new PropertyChange<T>('unset')
  84. }
  85. static newNoChange<T>(): PropertyChange<T> {
  86. return new PropertyChange<T>('nochange')
  87. }
  88. static newChange<T>(value: T): PropertyChange<T> {
  89. return new PropertyChange<T>('change', value)
  90. }
  91. /*
  92. Determines property change from the given object property.
  93. */
  94. static fromObjectProperty<T, Key extends string, ChangedObject extends { [key in Key]?: T }>(
  95. object: ChangedObject,
  96. key: Key
  97. ): PropertyChange<T> {
  98. if (!(key in object)) {
  99. return PropertyChange.newNoChange<T>()
  100. }
  101. if (object[key] === undefined) {
  102. return PropertyChange.newUnset<T>()
  103. }
  104. return PropertyChange.newChange<T>(object[key] as T)
  105. }
  106. private type: string
  107. private value?: T
  108. private constructor(type: 'change' | 'nochange' | 'unset', value?: T) {
  109. this.type = type
  110. this.value = value
  111. }
  112. public isUnset(): boolean {
  113. return this.type === 'unset'
  114. }
  115. public isNoChange(): boolean {
  116. return this.type === 'nochange'
  117. }
  118. public isValue(): boolean {
  119. return this.type === 'change'
  120. }
  121. public getValue(): T | undefined {
  122. return this.type === 'change' ? this.value : undefined
  123. }
  124. /*
  125. Integrates the value into the given dictionary.
  126. */
  127. public integrateInto(object: Object, key: string): void {
  128. if (this.isNoChange()) {
  129. return
  130. }
  131. if (this.isUnset()) {
  132. delete object[key]
  133. return
  134. }
  135. object[key] = this.value
  136. }
  137. }
  138. export interface RawVideoMetadata {
  139. encoding: {
  140. codecName: PropertyChange<string>
  141. container: PropertyChange<string>
  142. mimeMediaType: PropertyChange<string>
  143. }
  144. pixelWidth: PropertyChange<number>
  145. pixelHeight: PropertyChange<number>
  146. size: PropertyChange<number>
  147. }
  148. /*
  149. Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db.
  150. */
  151. export async function readProtobuf<T extends ChannelCategory | VideoCategory>(
  152. type: T,
  153. parameters: IReadProtobufArguments
  154. ): Promise<Partial<T>> {
  155. // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
  156. const metaU8a = parameters.metadata.toU8a(true)
  157. // process channel category
  158. if (type instanceof ChannelCategory) {
  159. const meta = ChannelCategoryMetadata.deserializeBinary(metaU8a)
  160. const result = convertMetadataToObject<ChannelCategoryMetadata.AsObject>(meta) as Partial<T>
  161. return result
  162. }
  163. // process video category
  164. if (type instanceof VideoCategory) {
  165. const meta = VideoCategoryMetadata.deserializeBinary(metaU8a)
  166. const result = convertMetadataToObject<VideoCategoryMetadata.AsObject>(meta) as Partial<T>
  167. return result
  168. }
  169. // this should never happen
  170. logger.error('Not implemented metadata type', { type })
  171. throw new Error(`Not implemented metadata type`)
  172. }
  173. /*
  174. Reads information from the event and protobuf metadata and constructs changeset that's fit to be used when saving to db.
  175. In addition it handles any assets associated with the metadata.
  176. */
  177. export async function readProtobufWithAssets<T extends Channel | Video>(
  178. type: T,
  179. parameters: IReadProtobufArgumentsWithAssets
  180. ): Promise<Partial<T>> {
  181. // true option here is crucial, it indicates that we want just the underlying bytes (by default it will also include bytes encoding the length)
  182. const metaU8a = parameters.metadata.toU8a(true)
  183. // process channel
  184. if (type instanceof Channel) {
  185. const meta = ChannelMetadata.deserializeBinary(metaU8a)
  186. const metaAsObject = convertMetadataToObject<ChannelMetadata.AsObject>(meta)
  187. const result = (metaAsObject as any) as Partial<Channel>
  188. // prepare cover photo asset if needed
  189. if ('coverPhoto' in metaAsObject) {
  190. const asset = await extractAsset({
  191. assetIndex: metaAsObject.coverPhoto,
  192. assets: parameters.assets,
  193. db: parameters.db,
  194. event: parameters.event,
  195. contentOwner: parameters.contentOwner,
  196. })
  197. integrateAsset('coverPhoto', result, asset) // changes `result` inline!
  198. delete metaAsObject.coverPhoto
  199. }
  200. // prepare avatar photo asset if needed
  201. if ('avatarPhoto' in metaAsObject) {
  202. const asset = await extractAsset({
  203. assetIndex: metaAsObject.avatarPhoto,
  204. assets: parameters.assets,
  205. db: parameters.db,
  206. event: parameters.event,
  207. contentOwner: parameters.contentOwner,
  208. })
  209. integrateAsset('avatarPhoto', result, asset) // changes `result` inline!
  210. delete metaAsObject.avatarPhoto
  211. }
  212. // prepare language if needed
  213. if ('language' in metaAsObject) {
  214. const language = await prepareLanguage(metaAsObject.language, parameters.db, parameters.event)
  215. delete metaAsObject.language // make sure temporary value will not interfere
  216. language.integrateInto(result, 'language')
  217. }
  218. return result as Partial<T>
  219. }
  220. // process video
  221. if (type instanceof Video) {
  222. const meta = VideoMetadata.deserializeBinary(metaU8a)
  223. const metaAsObject = convertMetadataToObject<VideoMetadata.AsObject>(meta)
  224. const result = (metaAsObject as any) as Partial<Video>
  225. // prepare video category if needed
  226. if ('category' in metaAsObject) {
  227. const category = await prepareVideoCategory(metaAsObject.category, parameters.db)
  228. delete metaAsObject.category // make sure temporary value will not interfere
  229. category.integrateInto(result, 'category')
  230. }
  231. // prepare media meta information if needed
  232. if ('mediaType' in metaAsObject || 'mediaPixelWidth' in metaAsObject || 'mediaPixelHeight' in metaAsObject) {
  233. // prepare video file size if poosible
  234. const videoSize = extractVideoSize(parameters.assets, metaAsObject.video)
  235. // NOTE: type hack - `RawVideoMetadata` is inserted instead of VideoMediaMetadata - it should be edited in `video.ts`
  236. // see `integrateVideoMetadata()` in `video.ts` for more info
  237. result.mediaMetadata = (prepareVideoMetadata(
  238. metaAsObject,
  239. videoSize,
  240. parameters.event.blockNumber
  241. ) as unknown) as VideoMediaMetadata
  242. // remove extra values
  243. delete metaAsObject.mediaType
  244. delete metaAsObject.mediaPixelWidth
  245. delete metaAsObject.mediaPixelHeight
  246. }
  247. // prepare license if needed
  248. if ('license' in metaAsObject) {
  249. result.license = await prepareLicense(parameters.db, metaAsObject.license, parameters.event)
  250. }
  251. // prepare thumbnail photo asset if needed
  252. if ('thumbnailPhoto' in metaAsObject) {
  253. const asset = await extractAsset({
  254. assetIndex: metaAsObject.thumbnailPhoto,
  255. assets: parameters.assets,
  256. db: parameters.db,
  257. event: parameters.event,
  258. contentOwner: parameters.contentOwner,
  259. })
  260. integrateAsset('thumbnailPhoto', result, asset) // changes `result` inline!
  261. delete metaAsObject.thumbnailPhoto
  262. }
  263. // prepare video asset if needed
  264. if ('video' in metaAsObject) {
  265. const asset = await extractAsset({
  266. assetIndex: metaAsObject.video,
  267. assets: parameters.assets,
  268. db: parameters.db,
  269. event: parameters.event,
  270. contentOwner: parameters.contentOwner,
  271. })
  272. integrateAsset('media', result, asset) // changes `result` inline!
  273. delete metaAsObject.video
  274. }
  275. // prepare language if needed
  276. if ('language' in metaAsObject) {
  277. const language = await prepareLanguage(metaAsObject.language, parameters.db, parameters.event)
  278. delete metaAsObject.language // make sure temporary value will not interfere
  279. language.integrateInto(result, 'language')
  280. }
  281. if (metaAsObject.publishedBeforeJoystream) {
  282. const publishedBeforeJoystream = handlePublishedBeforeJoystream(result, metaAsObject.publishedBeforeJoystream)
  283. delete metaAsObject.publishedBeforeJoystream // make sure temporary value will not interfere
  284. publishedBeforeJoystream.integrateInto(result, 'publishedBeforeJoystream')
  285. }
  286. return result as Partial<T>
  287. }
  288. // this should never happen
  289. logger.error('Not implemented metadata type', { type })
  290. throw new Error(`Not implemented metadata type`)
  291. }
  292. export async function convertContentActorToChannelOwner(
  293. db: DatabaseManager,
  294. contentActor: ContentActor
  295. ): Promise<{
  296. ownerMember?: Membership
  297. ownerCuratorGroup?: CuratorGroup
  298. }> {
  299. if (contentActor.isMember) {
  300. const memberId = contentActor.asMember.toNumber()
  301. const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
  302. // ensure member exists
  303. if (!member) {
  304. return inconsistentState(`Actor is non-existing member`, memberId)
  305. }
  306. return {
  307. ownerMember: member,
  308. ownerCuratorGroup: undefined, // this will clear the field
  309. }
  310. }
  311. if (contentActor.isCurator) {
  312. const curatorGroupId = contentActor.asCurator[0].toNumber()
  313. const curatorGroup = await db.get(CuratorGroup, {
  314. where: { id: curatorGroupId.toString() } as FindConditions<CuratorGroup>,
  315. })
  316. // ensure curator group exists
  317. if (!curatorGroup) {
  318. return inconsistentState('Actor is non-existing curator group', curatorGroupId)
  319. }
  320. return {
  321. ownerMember: undefined, // this will clear the field
  322. ownerCuratorGroup: curatorGroup,
  323. }
  324. }
  325. // TODO: contentActor.isLead
  326. logger.error('Not implemented ContentActor type', { contentActor: contentActor.toString() })
  327. throw new Error('Not-implemented ContentActor type used')
  328. }
  329. export function convertContentActorToDataObjectOwner(
  330. contentActor: ContentActor,
  331. channelId: number
  332. ): typeof DataObjectOwner {
  333. const owner = new DataObjectOwnerChannel()
  334. owner.channel = channelId
  335. return owner
  336. /* contentActor is irrelevant now -> all video/channel content belongs to the channel
  337. if (contentActor.isMember) {
  338. const owner = new DataObjectOwnerMember()
  339. owner.member = contentActor.asMember.toBn()
  340. return owner
  341. }
  342. if (contentActor.isLead || contentActor.isCurator) {
  343. const owner = new DataObjectOwnerChannel()
  344. owner.channel = channelId
  345. return owner
  346. }
  347. logger.error('Not implemented ContentActor type', {contentActor: contentActor.toString()})
  348. throw new Error('Not-implemented ContentActor type used')
  349. */
  350. }
  351. function handlePublishedBeforeJoystream(
  352. video: Partial<Video>,
  353. metadata: PublishedBeforeJoystreamMetadata.AsObject
  354. ): PropertyChange<Date> {
  355. // is publish being unset
  356. if ('isPublished' in metadata && !metadata.isPublished) {
  357. return PropertyChange.newUnset()
  358. }
  359. // try to parse timestamp from publish date
  360. const timestamp = metadata.date ? Date.parse(metadata.date) : NaN
  361. // ensure date is valid
  362. if (isNaN(timestamp)) {
  363. invalidMetadata(`Invalid date used for publishedBeforeJoystream`, {
  364. timestamp,
  365. })
  366. return PropertyChange.newNoChange()
  367. }
  368. // set new date
  369. return PropertyChange.newChange(new Date(timestamp))
  370. }
  371. interface IConvertAssetParameters {
  372. rawAsset: NewAsset
  373. db: DatabaseManager
  374. event: SubstrateEvent
  375. contentOwner: typeof DataObjectOwner
  376. }
  377. /*
  378. Converts event asset into data object or list of URLs fit to be saved to db.
  379. */
  380. async function convertAsset(parameters: IConvertAssetParameters): Promise<AssetStorageOrUrls> {
  381. // is asset describing list of URLs?
  382. if (parameters.rawAsset.isUrls) {
  383. const urls = parameters.rawAsset.asUrls.toArray().map((item) => item.toString())
  384. return urls
  385. }
  386. // !parameters.rawAsset.isUrls && parameters.rawAsset.isUpload // asset is in storage
  387. // prepare data object
  388. const contentParameters: ContentParameters = parameters.rawAsset.asUpload
  389. const dataObject = await prepareDataObject(
  390. parameters.db,
  391. contentParameters,
  392. parameters.event,
  393. parameters.contentOwner
  394. )
  395. return dataObject
  396. }
  397. interface IExtractAssetParameters {
  398. assetIndex: number | undefined
  399. assets: NewAsset[]
  400. db: DatabaseManager
  401. event: SubstrateEvent
  402. contentOwner: typeof DataObjectOwner
  403. }
  404. /*
  405. Selects asset from provided set of assets and prepares asset data fit to be saved to db.
  406. */
  407. async function extractAsset(parameters: IExtractAssetParameters): Promise<PropertyChange<AssetStorageOrUrls>> {
  408. // is asset being unset?
  409. if (parameters.assetIndex === undefined) {
  410. return PropertyChange.newUnset()
  411. }
  412. // ensure asset index is valid
  413. if (parameters.assetIndex >= parameters.assets.length) {
  414. invalidMetadata(`Non-existing asset extraction requested`, {
  415. assetsProvided: parameters.assets.length,
  416. assetIndex: parameters.assetIndex,
  417. })
  418. return PropertyChange.newNoChange()
  419. }
  420. // convert asset to data object record
  421. const asset = await convertAsset({
  422. rawAsset: parameters.assets[parameters.assetIndex],
  423. db: parameters.db,
  424. event: parameters.event,
  425. contentOwner: parameters.contentOwner,
  426. })
  427. return PropertyChange.newChange(asset)
  428. }
  429. /*
  430. As a temporary messure to overcome yet-to-be-implemented features in Hydra, we are using redudant information
  431. to describe asset state. This function introduces all redudant data needed to be saved to db.
  432. Changes `result` argument!
  433. */
  434. function integrateAsset<T>(propertyName: string, result: Object, asset: PropertyChange<AssetStorageOrUrls>): void {
  435. // helpers - property names
  436. const nameUrl = propertyName + 'Urls'
  437. const nameDataObject = propertyName + 'DataObject'
  438. const nameAvailability = propertyName + 'Availability'
  439. if (asset.isNoChange()) {
  440. return
  441. }
  442. if (asset.isUnset()) {
  443. result[nameUrl] = []
  444. result[nameAvailability] = AssetAvailability.INVALID
  445. result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
  446. return
  447. }
  448. const newValue = asset.getValue() as AssetStorageOrUrls
  449. // is asset available on external URL(s)
  450. if (!isAssetInStorage(newValue)) {
  451. // (un)set asset's properties
  452. result[nameUrl] = newValue
  453. result[nameAvailability] = AssetAvailability.ACCEPTED
  454. result[nameDataObject] = undefined // plan deletion (will have effect when saved to db)
  455. return
  456. }
  457. // asset saved in storage
  458. // prepare conversion table between liaison judgment and asset availability
  459. const conversionTable = {
  460. [LiaisonJudgement.ACCEPTED]: AssetAvailability.ACCEPTED,
  461. [LiaisonJudgement.PENDING]: AssetAvailability.PENDING,
  462. }
  463. // (un)set asset's properties
  464. result[nameUrl] = [] // plan deletion (will have effect when saved to db)
  465. result[nameAvailability] = conversionTable[newValue.liaisonJudgement]
  466. result[nameDataObject] = newValue
  467. }
  468. function extractVideoSize(assets: NewAsset[], assetIndex: number | undefined): number | undefined {
  469. // escape if no asset is required
  470. if (assetIndex === undefined) {
  471. return undefined
  472. }
  473. // ensure asset index is valid
  474. if (assetIndex > assets.length) {
  475. invalidMetadata(`Non-existing asset video size extraction requested`, { assetsProvided: assets.length, assetIndex })
  476. return undefined
  477. }
  478. const rawAsset = assets[assetIndex]
  479. // escape if asset is describing URLs (can't get size)
  480. if (rawAsset.isUrls) {
  481. return undefined
  482. }
  483. // !rawAsset.isUrls && rawAsset.isUpload // asset is in storage
  484. // convert generic content parameters coming from processor to custom Joystream data type
  485. const customContentParameters = new Custom_ContentParameters(registry, rawAsset.asUpload.toJSON() as any)
  486. // extract video size
  487. const videoSize = customContentParameters.size_in_bytes.toNumber()
  488. return videoSize
  489. }
  490. async function prepareLanguage(
  491. languageIso: string | undefined,
  492. db: DatabaseManager,
  493. event: SubstrateEvent
  494. ): Promise<PropertyChange<Language>> {
  495. // is language being unset?
  496. if (languageIso === undefined) {
  497. return PropertyChange.newUnset()
  498. }
  499. // validate language string
  500. const isValidIso = ISO6391.validate(languageIso)
  501. // ensure language string is valid
  502. if (!isValidIso) {
  503. invalidMetadata(`Invalid language ISO-639-1 provided`, languageIso)
  504. return PropertyChange.newNoChange()
  505. }
  506. // load language
  507. const language = await db.get(Language, { where: { iso: languageIso } as FindConditions<Language> })
  508. // return existing language if any
  509. if (language) {
  510. return PropertyChange.newChange(language)
  511. }
  512. // create new language
  513. const newLanguage = new Language({
  514. // set id as iso to overcome current graphql filtering limitations (so we can use query `videos(where: {languageId_eq: 'en'})`)
  515. // id: await getNextId(db),
  516. id: languageIso,
  517. iso: languageIso,
  518. createdInBlock: event.blockNumber,
  519. createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  520. updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  521. // TODO: remove these lines after Hydra auto-fills the values when cascading save (remove them on all places)
  522. createdById: '1',
  523. updatedById: '1',
  524. })
  525. await db.save<Language>(newLanguage)
  526. return PropertyChange.newChange(newLanguage)
  527. }
  528. async function prepareLicense(
  529. db: DatabaseManager,
  530. licenseProtobuf: LicenseMetadata.AsObject | undefined,
  531. event: SubstrateEvent
  532. ): Promise<License | undefined> {
  533. // NOTE: Deletion of any previous license should take place in appropriate event handling function
  534. // and not here even it might appear so.
  535. // is license being unset?
  536. if (licenseProtobuf === undefined) {
  537. return undefined
  538. }
  539. // license is meant to be deleted
  540. if (isLicenseEmpty(licenseProtobuf)) {
  541. return new License({})
  542. }
  543. // crete new license
  544. const license = new License({
  545. ...licenseProtobuf,
  546. id: await getNextId(db),
  547. createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  548. updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
  549. createdById: '1',
  550. updatedById: '1',
  551. })
  552. return license
  553. }
  554. /*
  555. Checks if protobof contains license with some fields filled or is empty object (`{}` or `{someKey: undefined, ...}`).
  556. Empty object means deletion is requested.
  557. */
  558. function isLicenseEmpty(licenseObject: LicenseMetadata.AsObject): boolean {
  559. const somePropertySet = Object.entries(licenseObject).reduce((acc, [key, value]) => {
  560. return acc || value !== undefined
  561. }, false)
  562. return !somePropertySet
  563. }
  564. function prepareVideoMetadata(
  565. videoProtobuf: VideoMetadata.AsObject,
  566. videoSize: number | undefined,
  567. blockNumber: number
  568. ): RawVideoMetadata {
  569. const rawMeta = {
  570. encoding: {
  571. codecName: PropertyChange.fromObjectProperty<string, 'codecName', MediaTypeMetadata.AsObject>(
  572. videoProtobuf.mediaType || {},
  573. 'codecName'
  574. ),
  575. container: PropertyChange.fromObjectProperty<string, 'container', MediaTypeMetadata.AsObject>(
  576. videoProtobuf.mediaType || {},
  577. 'container'
  578. ),
  579. mimeMediaType: PropertyChange.fromObjectProperty<string, 'mimeMediaType', MediaTypeMetadata.AsObject>(
  580. videoProtobuf.mediaType || {},
  581. 'mimeMediaType'
  582. ),
  583. },
  584. pixelWidth: PropertyChange.fromObjectProperty<number, 'mediaPixelWidth', VideoMetadata.AsObject>(
  585. videoProtobuf,
  586. 'mediaPixelWidth'
  587. ),
  588. pixelHeight: PropertyChange.fromObjectProperty<number, 'mediaPixelHeight', VideoMetadata.AsObject>(
  589. videoProtobuf,
  590. 'mediaPixelHeight'
  591. ),
  592. size: videoSize === undefined ? PropertyChange.newNoChange() : PropertyChange.newChange(videoSize),
  593. } as RawVideoMetadata
  594. return rawMeta
  595. }
  596. async function prepareVideoCategory(
  597. categoryId: number | undefined,
  598. db: DatabaseManager
  599. ): Promise<PropertyChange<VideoCategory>> {
  600. // is category being unset?
  601. if (categoryId === undefined) {
  602. return PropertyChange.newUnset()
  603. }
  604. // load video category
  605. const category = await db.get(VideoCategory, {
  606. where: { id: categoryId.toString() } as FindConditions<VideoCategory>,
  607. })
  608. // ensure video category exists
  609. if (!category) {
  610. invalidMetadata('Non-existing video category association with video requested', categoryId)
  611. return PropertyChange.newNoChange()
  612. }
  613. return PropertyChange.newChange(category)
  614. }
  615. function convertMetadataToObject<T extends Object>(metadata: jspb.Message): T {
  616. const metaAsObject = metadata.toObject()
  617. const result = {} as T
  618. for (const key in metaAsObject) {
  619. const funcNameBase = key.charAt(0).toUpperCase() + key.slice(1)
  620. const hasFuncName = 'has' + funcNameBase
  621. const isSet =
  622. funcNameBase === 'PersonsList' // there is no `VideoMetadata.hasPersonsList` method from unkown reason -> create exception
  623. ? true
  624. : metadata[hasFuncName]()
  625. if (!isSet) {
  626. continue
  627. }
  628. const getFuncName = 'get' + funcNameBase
  629. const value = metadata[getFuncName]()
  630. // TODO: check that recursion trully works
  631. if (value instanceof jspb.Message) {
  632. result[key] = convertMetadataToObject(value)
  633. continue
  634. }
  635. result[key] = metaAsObject[key]
  636. }
  637. return result
  638. }