Browse Source

Merge branch 'query_node_predictable_ids' into query_node_create_update_time

ondratra 3 years ago
parent
commit
9bee44d0be
51 changed files with 996 additions and 408 deletions
  1. 4 4
      cli/README.md
  2. 16 6
      cli/src/base/ApiCommandBase.ts
  3. 2 2
      cli/src/base/UploadCommandBase.ts
  4. 1 1
      cli/src/commands/account/create.ts
  5. 4 2
      cli/src/commands/account/export.ts
  6. 1 1
      cli/src/commands/account/import.ts
  7. 1 1
      cli/src/commands/account/transferTokens.ts
  8. 1 1
      cli/src/commands/content/addCuratorToGroup.ts
  9. 9 1
      cli/src/commands/content/createChannel.ts
  10. 9 1
      cli/src/commands/content/createChannelCategory.ts
  11. 1 1
      cli/src/commands/content/createCuratorGroup.ts
  12. 10 1
      cli/src/commands/content/createVideo.ts
  13. 9 1
      cli/src/commands/content/createVideoCategory.ts
  14. 1 1
      cli/src/commands/content/reuploadAssets.ts
  15. 1 1
      cli/src/commands/content/setCuratorGroupStatus.ts
  16. 1 1
      cli/src/commands/content/updateChannelCensorshipStatus.ts
  17. 1 1
      cli/src/commands/content/updateVideoCensorshipStatus.ts
  18. 2 2
      cli/src/commands/content/videos.ts
  19. 2 2
      cli/src/commands/working-groups/createOpening.ts
  20. 1 1
      cli/src/commands/working-groups/fillOpening.ts
  21. 1 1
      cli/src/commands/working-groups/leaveRole.ts
  22. 1 1
      cli/src/commands/working-groups/setDefaultGroup.ts
  23. 1 1
      cli/src/commands/working-groups/slashWorker.ts
  24. 1 1
      cli/src/commands/working-groups/terminateApplication.ts
  25. 1 1
      cli/src/commands/working-groups/updateRewardAccount.ts
  26. 1 1
      cli/src/commands/working-groups/updateRoleAccount.ts
  27. 1 1
      cli/src/commands/working-groups/updateRoleStorage.ts
  28. 1 1
      cli/src/helpers/InputOutput.ts
  29. 1 1
      cli/tsconfig.json
  30. 34 7
      pioneer/packages/joy-tokenomics/src/Overview/SpendingAndStakeDistributionTable.tsx
  31. 30 14
      pioneer/packages/joy-tokenomics/src/Overview/TokenomicsCharts.tsx
  32. 12 0
      pioneer/packages/joy-tokenomics/src/Overview/index.tsx
  33. 4 2
      pioneer/packages/joy-utils/src/transport/tokenomics.ts
  34. 1 0
      pioneer/packages/joy-utils/src/types/tokenomics.ts
  35. 80 78
      query-node/generated/graphql-server/generated/binding.ts
  36. 152 140
      query-node/generated/graphql-server/generated/classes.ts
  37. 84 80
      query-node/generated/graphql-server/generated/schema.graphql
  38. 2 0
      query-node/generated/graphql-server/model/index.ts
  39. 14 0
      query-node/generated/graphql-server/src/modules/next-entity-id/next-entity-id.model.ts
  40. 128 0
      query-node/generated/graphql-server/src/modules/next-entity-id/next-entity-id.resolver.ts
  41. 28 0
      query-node/generated/graphql-server/src/modules/next-entity-id/next-entity-id.service.ts
  42. 70 0
      query-node/generated/types/members.ts
  43. 1 0
      query-node/manifest.yml
  44. 113 13
      query-node/mappings/src/common.ts
  45. 0 2
      query-node/mappings/src/content/channel.ts
  46. 23 5
      query-node/mappings/src/content/utils.ts
  47. 42 17
      query-node/mappings/src/content/video.ts
  48. 82 7
      query-node/mappings/src/membership.ts
  49. 1 1
      query-node/mappings/src/storage.ts
  50. 1 1
      query-node/mappings/src/workingGroup.ts
  51. 8 0
      query-node/schema.graphql

+ 4 - 4
cli/README.md

@@ -45,7 +45,7 @@ $ npm install -g @joystream/cli
 $ joystream-cli COMMAND
 running command...
 $ joystream-cli (-v|--version|version)
-@joystream/cli/0.5.0 linux-x64 node-v13.12.0
+@joystream/cli/0.5.0 linux-x64 node-v14.16.1
 $ joystream-cli --help [COMMAND]
 USAGE
   $ joystream-cli COMMAND
@@ -314,7 +314,7 @@ EXAMPLES
   $ joystream-cli autocomplete --refresh-cache
 ```
 
-_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v0.2.0/src/commands/autocomplete/index.ts)_
+_See code: [@oclif/plugin-autocomplete](https://github.com/oclif/plugin-autocomplete/blob/v0.2.1/src/commands/autocomplete/index.ts)_
 
 ## `joystream-cli content:addCuratorToGroup [GROUPID] [CURATORID]`
 
@@ -506,7 +506,7 @@ _See code: [src/commands/content/removeCuratorFromGroup.ts](https://github.com/J
 
 ## `joystream-cli content:reuploadAssets`
 
-Allows reuploading assets that were not succesfully uploaded during channel/video creation
+Allows reuploading assets that were not successfully uploaded during channel/video creation
 
 ```
 USAGE
@@ -707,7 +707,7 @@ OPTIONS
   --all  see all commands in CLI
 ```
 
-_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.3/src/commands/help.ts)_
+_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.2/src/commands/help.ts)_
 
 ## `joystream-cli working-groups:application WGAPPLICATIONID`
 

+ 16 - 6
cli/src/base/ApiCommandBase.ts

@@ -6,7 +6,7 @@ import { getTypeDef, Option, Tuple, TypeRegistry } from '@polkadot/types'
 import { Registry, Codec, CodecArg, TypeDef, TypeDefInfo } from '@polkadot/types/types'
 
 import { Vec, Struct, Enum } from '@polkadot/types/codec'
-import { ApiPromise, WsProvider } from '@polkadot/api'
+import { ApiPromise, SubmittableResult, WsProvider } from '@polkadot/api'
 import { KeyringPair } from '@polkadot/keyring/types'
 import chalk from 'chalk'
 import { InterfaceTypes } from '@polkadot/types/types/registry'
@@ -16,6 +16,7 @@ import { SubmittableExtrinsic } from '@polkadot/api/types'
 import { DistinctQuestion } from 'inquirer'
 import { BOOL_PROMPT_OPTIONS } from '../helpers/prompting'
 import { DispatchError } from '@polkadot/types/interfaces/system'
+import { Event } from '@polkadot/types/interfaces'
 
 export class ExtrinsicFailedError extends Error {}
 
@@ -351,7 +352,7 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     return values
   }
 
-  sendExtrinsic(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>) {
+  sendExtrinsic(account: KeyringPair, tx: SubmittableExtrinsic<'promise'>): Promise<SubmittableResult> {
     return new Promise((resolve, reject) => {
       let unsubscribe: () => void
       tx.signAndSend(account, {}, (result) => {
@@ -401,11 +402,11 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     account: KeyringPair,
     tx: SubmittableExtrinsic<'promise'>,
     warnOnly = false // If specified - only warning will be displayed in case of failure (instead of error beeing thrown)
-  ): Promise<boolean> {
+  ): Promise<SubmittableResult | false> {
     try {
-      await this.sendExtrinsic(account, tx)
+      const res = await this.sendExtrinsic(account, tx)
       this.log(chalk.green(`Extrinsic successful!`))
-      return true
+      return res
     } catch (e) {
       if (e instanceof ExtrinsicFailedError && warnOnly) {
         this.warn(`Extrinsic failed! ${e.message}`)
@@ -424,12 +425,21 @@ export default abstract class ApiCommandBase extends StateAwareCommandBase {
     method: string,
     params: CodecArg[],
     warnOnly = false
-  ): Promise<boolean> {
+  ): Promise<SubmittableResult | false> {
     this.log(chalk.magentaBright(`\nSending ${module}.${method} extrinsic...`))
     const tx = await this.getOriginalApi().tx[module][method](...params)
     return await this.sendAndFollowTx(account, tx, warnOnly)
   }
 
+  // TODO:
+  // Switch to:
+  // public findEvent<S extends keyof AugmentedEvents<'promise'> & string, M extends keyof AugmentedEvents<'promise'>[S] & string>
+  //          (result: SubmittableResult, section: S, method: M): Event | undefined {
+  // Once augment-api is supported
+  public findEvent(result: SubmittableResult, section: string, method: string): Event | undefined {
+    return result.findRecord(section, method)?.event
+  }
+
   async buildAndSendExtrinsic(
     account: KeyringPair,
     module: string,

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

@@ -263,7 +263,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
     )
     if (rejectedAssetsOutput.length) {
       this.warn(
-        `Some assets were not uploaded succesfully. Try reuploading them with ${chalk.magentaBright(
+        `Some assets were not uploaded successfully. Try reuploading them with ${chalk.magentaBright(
           'content:reuploadAssets'
         )}!`
       )
@@ -271,7 +271,7 @@ export default abstract class UploadCommandBase extends ContentDirectoryCommandB
       const outputPath = inputFilePath.replace('.json', `${outputFilePostfix}.json`)
       try {
         fs.writeFileSync(outputPath, JSON.stringify(rejectedAssetsOutput, null, 4))
-        this.log(`Rejected content ids succesfully saved to: ${chalk.magentaBright(outputPath)}!`)
+        this.log(`Rejected content ids successfully saved to: ${chalk.magentaBright(outputPath)}!`)
       } catch (e) {
         console.error(e)
         this.warn(

+ 1 - 1
cli/src/commands/account/create.ts

@@ -40,7 +40,7 @@ export default class AccountCreate extends AccountsCommandBase {
 
     this.saveAccount(keys, password)
 
-    this.log(chalk.greenBright(`\nAccount succesfully created!`))
+    this.log(chalk.greenBright(`\nAccount successfully created!`))
     this.log(chalk.magentaBright(`${chalk.bold('Name:    ')}${args.name}`))
     this.log(chalk.magentaBright(`${chalk.bold('Address: ')}${keys.address}`))
   }

+ 4 - 2
cli/src/commands/account/export.ts

@@ -59,7 +59,9 @@ export default class AccountExport extends AccountsCommandBase {
         this.error(`Failed to create the export folder (${destPath})`, { exit: ExitCodes.FsOperationFailed })
       }
       for (const account of accounts) this.exportAccount(account, destPath)
-      this.log(chalk.greenBright(`All accounts succesfully exported succesfully to: ${chalk.magentaBright(destPath)}!`))
+      this.log(
+        chalk.greenBright(`All accounts successfully exported successfully to: ${chalk.magentaBright(destPath)}!`)
+      )
     } else {
       const destPath: string = args.path
       const choosenAccount: NamedKeyringPair = await this.promptForAccount(
@@ -68,7 +70,7 @@ export default class AccountExport extends AccountsCommandBase {
         'Select an account to export'
       )
       const exportedFilePath: string = this.exportAccount(choosenAccount, destPath)
-      this.log(chalk.greenBright(`Account succesfully exported to: ${chalk.magentaBright(exportedFilePath)}`))
+      this.log(chalk.greenBright(`Account successfully exported to: ${chalk.magentaBright(exportedFilePath)}`))
     }
   }
 }

+ 1 - 1
cli/src/commands/account/import.ts

@@ -37,7 +37,7 @@ export default class AccountImport extends AccountsCommandBase {
       })
     }
 
-    this.log(chalk.bold.greenBright(`ACCOUNT IMPORTED SUCCESFULLY!`))
+    this.log(chalk.bold.greenBright(`ACCOUNT IMPORTED SUCCESSFULLY!`))
     this.log(chalk.bold.magentaBright(`NAME:    `), accountName)
     this.log(chalk.bold.magentaBright(`ADDRESS: `), accountAddress)
   }

+ 1 - 1
cli/src/commands/account/transferTokens.ts

@@ -58,7 +58,7 @@ export default class AccountTransferTokens extends AccountsCommandBase {
 
     try {
       const txHash: Hash = await tx.signAndSend(selectedAccount)
-      this.log(chalk.greenBright('Transaction succesfully sent!'))
+      this.log(chalk.greenBright('Transaction successfully sent!'))
       this.log(chalk.magentaBright('Hash:', txHash.toString()))
     } catch (e) {
       this.error('Could not send the transaction.', { exit: ExitCodes.UnexpectedException })

+ 1 - 1
cli/src/commands/content/addCuratorToGroup.ts

@@ -39,7 +39,7 @@ export default class AddCuratorToGroupCommand extends ContentDirectoryCommandBas
 
     console.log(
       chalk.green(
-        `Curator ${chalk.magentaBright(curatorId)} succesfully added to group ${chalk.magentaBright(groupId)}!`
+        `Curator ${chalk.magentaBright(curatorId)} successfully added to group ${chalk.magentaBright(groupId)}!`
       )
     )
   }

+ 9 - 1
cli/src/commands/content/createChannel.ts

@@ -7,6 +7,7 @@ import { ChannelCreationParameters } from '@joystream/types/content'
 import { ChannelInputSchema } from '../../json-schemas/ContentDirectory'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import UploadCommandBase from '../../base/UploadCommandBase'
+import chalk from 'chalk'
 
 export default class CreateChannelCommand extends UploadCommandBase {
   static description = 'Create channel inside content directory.'
@@ -55,7 +56,14 @@ export default class CreateChannelCommand extends UploadCommandBase {
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 
-    await this.sendAndFollowNamedTx(account, 'content', 'createChannel', [actor, channelCreationParameters])
+    const result = await this.sendAndFollowNamedTx(account, 'content', 'createChannel', [
+      actor,
+      channelCreationParameters,
+    ])
+    if (result) {
+      const event = this.findEvent(result, 'content', 'ChannelCreated')
+      this.log(chalk.green(`Channel with id ${chalk.cyanBright(event?.data[1].toString())} successfully created!`))
+    }
 
     await this.uploadAssets(inputAssets, input)
   }

+ 9 - 1
cli/src/commands/content/createChannelCategory.ts

@@ -6,6 +6,7 @@ import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { ChannelCategoryCreationParameters } from '@joystream/types/content'
 import { ChannelCategoryInputSchema } from '../../json-schemas/ContentDirectory'
+import chalk from 'chalk'
 
 export default class CreateChannelCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create channel category inside content directory.'
@@ -38,9 +39,16 @@ export default class CreateChannelCategoryCommand extends ContentDirectoryComman
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 
-    await this.sendAndFollowNamedTx(currentAccount, 'content', 'createChannelCategory', [
+    const result = await this.sendAndFollowNamedTx(currentAccount, 'content', 'createChannelCategory', [
       actor,
       channelCategoryCreationParameters,
     ])
+
+    if (result) {
+      const event = this.findEvent(result, 'content', 'ChannelCategoryCreated')
+      this.log(
+        chalk.green(`ChannelCategory with id ${chalk.cyanBright(event?.data[0].toString())} successfully created!`)
+      )
+    }
   }
 }

+ 1 - 1
cli/src/commands/content/createCuratorGroup.ts

@@ -13,6 +13,6 @@ export default class CreateCuratorGroupCommand extends ContentDirectoryCommandBa
     await this.buildAndSendExtrinsic(account, 'content', 'createCuratorGroup')
 
     const newGroupId = (await this.getApi().nextCuratorGroupId()) - 1
-    console.log(chalk.green(`New group succesfully created! (ID: ${chalk.magentaBright(newGroupId)})`))
+    console.log(chalk.green(`New group successfully created! (ID: ${chalk.magentaBright(newGroupId)})`))
   }
 }

+ 10 - 1
cli/src/commands/content/createVideo.ts

@@ -7,6 +7,7 @@ import { flags } from '@oclif/command'
 import { VideoCreationParameters } from '@joystream/types/content'
 import { MediaType, VideoMetadata } from '@joystream/content-metadata-protobuf'
 import { VideoInputSchema } from '../../json-schemas/ContentDirectory'
+import chalk from 'chalk'
 
 export default class CreateVideoCommand extends UploadCommandBase {
   static description = 'Create video under specific channel inside content directory.'
@@ -78,7 +79,15 @@ export default class CreateVideoCommand extends UploadCommandBase {
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 
-    await this.sendAndFollowNamedTx(account, 'content', 'createVideo', [actor, channelId, videoCreationParameters])
+    const result = await this.sendAndFollowNamedTx(account, 'content', 'createVideo', [
+      actor,
+      channelId,
+      videoCreationParameters,
+    ])
+    if (result) {
+      const event = this.findEvent(result, 'content', 'VideoCreated')
+      this.log(chalk.green(`Video with id ${chalk.cyanBright(event?.data[2].toString())} successfully created!`))
+    }
 
     // Upload assets
     await this.uploadAssets(inputAssets, input)

+ 9 - 1
cli/src/commands/content/createVideoCategory.ts

@@ -6,6 +6,7 @@ import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { VideoCategoryCreationParameters } from '@joystream/types/content'
 import { VideoCategoryInputSchema } from '../../json-schemas/ContentDirectory'
+import chalk from 'chalk'
 
 export default class CreateVideoCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create video category inside content directory.'
@@ -38,9 +39,16 @@ export default class CreateVideoCategoryCommand extends ContentDirectoryCommandB
 
     await this.requireConfirmation('Do you confirm the provided input?', true)
 
-    await this.sendAndFollowNamedTx(currentAccount, 'content', 'createVideoCategory', [
+    const result = await this.sendAndFollowNamedTx(currentAccount, 'content', 'createVideoCategory', [
       actor,
       videoCategoryCreationParameters,
     ])
+
+    if (result) {
+      const event = this.findEvent(result, 'content', 'VideoCategoryCreated')
+      this.log(
+        chalk.green(`VideoCategory with id ${chalk.cyanBright(event?.data[1].toString())} successfully created!`)
+      )
+    }
   }
 }

+ 1 - 1
cli/src/commands/content/reuploadAssets.ts

@@ -6,7 +6,7 @@ import { flags } from '@oclif/command'
 import { ContentId } from '@joystream/types/storage'
 
 export default class ReuploadVideoAssetsCommand extends UploadCommandBase {
-  static description = 'Allows reuploading assets that were not succesfully uploaded during channel/video creation'
+  static description = 'Allows reuploading assets that were not successfully uploaded during channel/video creation'
 
   static flags = {
     input: flags.string({

+ 1 - 1
cli/src/commands/content/setCuratorGroupStatus.ts

@@ -52,7 +52,7 @@ export default class SetCuratorGroupStatusCommand extends ContentDirectoryComman
 
     console.log(
       chalk.green(
-        `Curator Group ${chalk.magentaBright(id)} status succesfully changed to: ${chalk.magentaBright(
+        `Curator Group ${chalk.magentaBright(id)} status successfully changed to: ${chalk.magentaBright(
           status ? 'Active' : 'Inactive'
         )}!`
       )

+ 1 - 1
cli/src/commands/content/updateChannelCensorshipStatus.ts

@@ -70,7 +70,7 @@ export default class UpdateChannelCensorshipStatusCommand extends ContentDirecto
 
     console.log(
       chalk.green(
-        `Channel ${chalk.magentaBright(id)} censorship status succesfully changed to: ${chalk.magentaBright(
+        `Channel ${chalk.magentaBright(id)} censorship status successfully changed to: ${chalk.magentaBright(
           status ? 'Censored' : 'Not censored'
         )}!`
       )

+ 1 - 1
cli/src/commands/content/updateVideoCensorshipStatus.ts

@@ -71,7 +71,7 @@ export default class UpdateVideoCensorshipStatusCommand extends ContentDirectory
 
     console.log(
       chalk.green(
-        `Video ${chalk.magentaBright(id)} censorship status succesfully changed to: ${chalk.magentaBright(
+        `Video ${chalk.magentaBright(id)} censorship status successfully changed to: ${chalk.magentaBright(
           status ? 'Censored' : 'Not censored'
         )}!`
       )

+ 2 - 2
cli/src/commands/content/videos.ts

@@ -8,7 +8,7 @@ export default class VideosCommand extends ContentDirectoryCommandBase {
   static args = [
     {
       name: 'channelId',
-      required: true,
+      required: false,
       description: 'ID of the Channel',
     },
   ]
@@ -34,7 +34,7 @@ export default class VideosCommand extends ContentDirectoryCommandBase {
         3
       )
     } else {
-      this.log('There are no videos yet')
+      this.log(`There are no videos${channelId ? ' in this channel' : ''} yet`)
     }
   }
 }

+ 2 - 2
cli/src/commands/working-groups/createOpening.ts

@@ -198,7 +198,7 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
       if (output) {
         try {
           saveOutputJsonToFile(output, rememberedInput)
-          this.log(chalk.green(`Output succesfully saved in: ${chalk.magentaBright(output)}!`))
+          this.log(chalk.green(`Output successfully saved in: ${chalk.magentaBright(output)}!`))
         } catch (e) {
           this.warn(`Could not save output to ${output}!`)
         }
@@ -218,7 +218,7 @@ export default class WorkingGroupsCreateOpening extends WorkingGroupsCommandBase
 
       // Display a success message on success or ask to try again on error
       if (txSuccess) {
-        this.log(chalk.green('Opening succesfully created!'))
+        this.log(chalk.green('Opening successfully created!'))
         tryAgain = false
       } else {
         tryAgain = await this.simplePrompt({ type: 'confirm', message: 'Try again with remembered input?' })

+ 1 - 1
cli/src/commands/working-groups/fillOpening.ts

@@ -39,7 +39,7 @@ export default class WorkingGroupsFillOpening extends WorkingGroupsCommandBase {
       rewardPolicyOpt,
     ])
 
-    this.log(chalk.green(`Opening ${chalk.magentaBright(openingId)} succesfully filled!`))
+    this.log(chalk.green(`Opening ${chalk.magentaBright(openingId)} successfully filled!`))
     this.log(
       chalk.green('Accepted working group application IDs: ') +
         chalk.magentaBright(applicationIds.length ? applicationIds.join(chalk.green(', ')) : 'NONE')

+ 1 - 1
cli/src/commands/working-groups/leaveRole.ts

@@ -23,6 +23,6 @@ export default class WorkingGroupsLeaveRole extends WorkingGroupsCommandBase {
 
     await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'leaveRole', [worker.workerId, rationale])
 
-    this.log(chalk.green(`Succesfully left the role! (worker id: ${chalk.magentaBright(worker.workerId.toNumber())})`))
+    this.log(chalk.green(`Successfully left the role! (worker id: ${chalk.magentaBright(worker.workerId.toNumber())})`))
   }
 }

+ 1 - 1
cli/src/commands/working-groups/setDefaultGroup.ts

@@ -17,6 +17,6 @@ export default class SetDefaultGroupCommand extends WorkingGroupsCommandBase {
 
     await this.setPreservedState({ defaultWorkingGroup: group })
 
-    this.log(chalk.green(`${chalk.magentaBright(group)} succesfully set as default working group context`))
+    this.log(chalk.green(`${chalk.magentaBright(group)} successfully set as default working group context`))
   }
 }

+ 1 - 1
cli/src/commands/working-groups/slashWorker.ts

@@ -45,7 +45,7 @@ export default class WorkingGroupsSlashWorker extends WorkingGroupsCommandBase {
       chalk.green(
         `${chalk.magentaBright(formatBalance(balance))} from worker ${chalk.magentaBright(
           workerId
-        )} stake has been succesfully slashed!`
+        )} stake has been successfully slashed!`
       )
     )
   }

+ 1 - 1
cli/src/commands/working-groups/terminateApplication.ts

@@ -32,6 +32,6 @@ export default class WorkingGroupsTerminateApplication extends WorkingGroupsComm
 
     await this.sendAndFollowNamedTx(account, apiModuleByGroup[this.group], 'terminateApplication', [applicationId])
 
-    this.log(chalk.green(`Application ${chalk.magentaBright(applicationId)} has been succesfully terminated!`))
+    this.log(chalk.green(`Application ${chalk.magentaBright(applicationId)} has been successfully terminated!`))
   }
 }

+ 1 - 1
cli/src/commands/working-groups/updateRewardAccount.ts

@@ -43,6 +43,6 @@ export default class WorkingGroupsUpdateRewardAccount extends WorkingGroupsComma
       newRewardAccount,
     ])
 
-    this.log(chalk.green(`Succesfully updated the reward account to: ${chalk.magentaBright(newRewardAccount)})`))
+    this.log(chalk.green(`Successfully updated the reward account to: ${chalk.magentaBright(newRewardAccount)})`))
   }
 }

+ 1 - 1
cli/src/commands/working-groups/updateRoleAccount.ts

@@ -37,7 +37,7 @@ export default class WorkingGroupsUpdateRoleAccount extends WorkingGroupsCommand
       newRoleAccount,
     ])
 
-    this.log(chalk.green(`Succesfully updated the role account to: ${chalk.magentaBright(newRoleAccount)})`))
+    this.log(chalk.green(`Successfully updated the role account to: ${chalk.magentaBright(newRoleAccount)})`))
 
     const matchingAccount = cliAccounts.find((account) => account.address === newRoleAccount)
     if (matchingAccount) {

+ 1 - 1
cli/src/commands/working-groups/updateRoleStorage.ts

@@ -31,6 +31,6 @@ export default class WorkingGroupsUpdateRoleStorage extends WorkingGroupsCommand
       storage,
     ])
 
-    this.log(chalk.green(`Succesfully updated the associated worker storage to: ${chalk.magentaBright(storage)})`))
+    this.log(chalk.green(`Successfully updated the associated worker storage to: ${chalk.magentaBright(storage)})`))
   }
 }

+ 1 - 1
cli/src/helpers/InputOutput.ts

@@ -60,7 +60,7 @@ export function saveOutputJson(outputPath: string | undefined, fileName: string,
     }
     saveOutputJsonToFile(outputFilePath, data)
 
-    console.log(`${chalk.green('Output succesfully saved to:')} ${chalk.magentaBright(outputFilePath)}`)
+    console.log(`${chalk.green('Output successfully saved to:')} ${chalk.magentaBright(outputFilePath)}`)
   }
 }
 

+ 1 - 1
cli/tsconfig.json

@@ -12,7 +12,7 @@
     "noUnusedLocals": true,
     "baseUrl": ".",
     "paths": {
-      "@polkadot/types/augment": ["../types/augment-codec/augment-types.ts"],
+      "@polkadot/types/augment": ["../types/augment-codec/augment-types.ts"]
     },
     "resolveJsonModule": true,
     "skipLibCheck": true

+ 34 - 7
pioneer/packages/joy-tokenomics/src/Overview/SpendingAndStakeDistributionTable.tsx

@@ -5,6 +5,8 @@ import { useWindowDimensions } from '../../../joy-utils/src/react/hooks';
 
 import { TokenomicsData, StatusServerData } from '@polkadot/joy-utils/src/types/tokenomics';
 
+import { COLORS } from './index';
+
 const round = (num: number): number => Math.round((num + Number.EPSILON) * 100) / 100;
 
 const applyCss = (columns: number[]): string => {
@@ -123,7 +125,8 @@ type TokenomicsGroup =
   'validators' |
   'council' |
   'storageProviders' |
-  'contentCurators'
+  'contentCurators' |
+  'operations'
 
 const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; statusData?: StatusServerData | null}> = ({ data, statusData }) => {
   const { width } = useWindowDimensions();
@@ -167,7 +170,7 @@ const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; status
           groupStake={data && `${data.validators.totalStake}`}
           groupStakeDollar={displayStatusData('validators', 'totalStake')}
           stakeShare={data && `${round(data.validators.stakeShare * 100)}`}
-          color='rgb(246, 109, 68)'
+          color={COLORS.VALIDATOR}
         />
         <SpendingAndStakeTableRow
           role={width <= 1015 ? 'Council' : 'Council Members'}
@@ -179,7 +182,7 @@ const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; status
           groupStake={data && `${data.council.totalStake}`}
           groupStakeDollar={displayStatusData('council', 'totalStake')}
           stakeShare={data && `${round(data.council.stakeShare * 100)}`}
-          color='rgb(254, 174, 101)'
+          color={COLORS.COUNCIL_MEMBER}
         />
         <SpendingAndStakeTableRow
           role={width <= 1015 ? 'Storage' : 'Storage Providers'}
@@ -191,7 +194,7 @@ const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; status
           groupStake={data && `${data.storageProviders.totalStake}`}
           groupStakeDollar={displayStatusData('storageProviders', 'totalStake')}
           stakeShare={data && `${round(data.storageProviders.stakeShare * 100)}`}
-          color='rgb(230, 246, 157)'
+          color={COLORS.STORAGE_PROVIDER}
         />
         <SpendingAndStakeTableRow
           role={width <= 1015 ? 'S. Lead' : width <= 1050 ? 'Storage Lead' : 'Storage Provider Lead'}
@@ -203,7 +206,7 @@ const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; status
           groupStake={data && `${data.storageProviders.lead.totalStake}`}
           groupStakeDollar={displayStatusData('storageProviders', 'totalStake', true)}
           stakeShare={data && `${round(data.storageProviders.lead.stakeShare * 100)}`}
-          color='rgb(170, 222, 167)'
+          color={COLORS.STORAGE_LEAD}
         />
         <SpendingAndStakeTableRow
           role={width <= 1015 ? 'Curators' : 'Content Curators'}
@@ -215,7 +218,7 @@ const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; status
           groupStake={data && `${data.contentCurators.totalStake}`}
           groupStakeDollar={displayStatusData('contentCurators', 'totalStake')}
           stakeShare={data && `${round(data.contentCurators.stakeShare * 100)}`}
-          color='rgb(100, 194, 166)'
+          color={COLORS.CONTENT_CURATOR}
         />
         <SpendingAndStakeTableRow
           role={width <= 1015 ? 'C. Lead' : 'Curators Lead'}
@@ -227,7 +230,31 @@ const SpendingAndStakeDistributionTable: React.FC<{data?: TokenomicsData; status
           groupStake={data && `${data.contentCurators.lead.totalStake}`}
           groupStakeDollar={displayStatusData('contentCurators', 'totalStake', true)}
           stakeShare={data && `${round(data.contentCurators.lead.stakeShare * 100)}`}
-          color='rgb(100, 160, 190)'
+          color={COLORS.CURATOR_LEAD}
+        />
+        <SpendingAndStakeTableRow
+          role='Operations'
+          helpContent='The current Operations members, and the sum of their projected rewards and stakes.'
+          numberOfActors={data && `${data.operations.number}`}
+          groupEarning={data && `${Math.round(data.operations.rewardsPerWeek)}`}
+          groupEarningDollar={displayStatusData('operations', 'rewardsPerWeek')}
+          earningShare={data && `${round(data.operations.rewardsShare * 100)}`}
+          groupStake={data && `${data.operations.totalStake}`}
+          groupStakeDollar={displayStatusData('operations', 'totalStake')}
+          stakeShare={data && `${round(data.operations.stakeShare * 100)}`}
+          color={COLORS.OPERATIONS}
+        />
+        <SpendingAndStakeTableRow
+          role='Operations Lead'
+          helpContent='Current Operations Lead, and their projected reward and stake.'
+          numberOfActors={data && `${data.operations.lead.number}`}
+          groupEarning={data && `${Math.round(data.operations.lead.rewardsPerWeek)}`}
+          groupEarningDollar={displayStatusData('operations', 'rewardsPerWeek', true)}
+          earningShare={data && `${round(data.operations.lead.rewardsShare * 100)}`}
+          groupStake={data && `${data.operations.lead.totalStake}`}
+          groupStakeDollar={displayStatusData('operations', 'totalStake', true)}
+          stakeShare={data && `${round(data.operations.lead.stakeShare * 100)}`}
+          color={COLORS.OPERATIONS_LEAD}
         />
         <SpendingAndStakeTableRow
           role='TOTAL'

+ 30 - 14
pioneer/packages/joy-tokenomics/src/Overview/TokenomicsCharts.tsx

@@ -4,6 +4,7 @@ import PieChart from '../../../react-components/src/Chart/PieChart';
 import styled from 'styled-components';
 
 import { TokenomicsData } from '@polkadot/joy-utils/src/types/tokenomics';
+import { COLORS } from './index';
 
 const StyledPieChart = styled(PieChart)`
   width:15rem;
@@ -31,29 +32,37 @@ const TokenomicsCharts: React.FC<{data?: TokenomicsData; className?: string}> =
       {data ? <ChartContainer>
         <StyledPieChart
           values={[{
-            colors: ['rgb(246, 109, 68)'],
+            colors: [COLORS.VALIDATOR],
             label: 'Validators',
             value: data.validators.rewardsShare * 100
           }, {
-            colors: ['rgb(254, 174, 101)'],
+            colors: [COLORS.COUNCIL_MEMBER],
             label: 'Council',
             value: data.council.rewardsShare * 100
           }, {
-            colors: ['rgb(230, 246, 157)'],
+            colors: [COLORS.STORAGE_PROVIDER],
             label: 'Storage Providers',
             value: data.storageProviders.rewardsShare * 100
           }, {
-            colors: ['rgb(170, 222, 167)'],
+            colors: [COLORS.STORAGE_LEAD],
             label: 'Storage Lead',
             value: data.storageProviders.lead.rewardsShare * 100
           }, {
-            colors: ['rgb(100, 194, 166)'],
+            colors: [COLORS.CONTENT_CURATOR],
             label: 'Content Curators',
             value: data.contentCurators.rewardsShare * 100
           }, {
-            colors: ['rgb(100, 160, 190)'],
+            colors: [COLORS.CURATOR_LEAD],
             label: 'Content Curators Lead',
             value: data.contentCurators.lead.rewardsShare * 100
+          }, {
+            colors: [COLORS.OPERATIONS],
+            label: 'Operations',
+            value: data.operations.rewardsShare * 100
+          }, {
+            colors: [COLORS.OPERATIONS_LEAD],
+            label: 'Operations Lead',
+            value: data.operations.lead.rewardsShare * 100
           }
           ]} />
         <Label as='div'>
@@ -64,30 +73,37 @@ const TokenomicsCharts: React.FC<{data?: TokenomicsData; className?: string}> =
       {data ? <ChartContainer>
         <StyledPieChart
           values={[{
-            colors: ['rgb(246, 109, 68)'],
+            colors: [COLORS.VALIDATOR],
             label: 'Validators',
             value: data.validators.stakeShare * 100
           }, {
-            colors: ['rgb(254, 174, 101)'],
+            colors: [COLORS.COUNCIL_MEMBER],
             label: 'Council',
             value: data.council.stakeShare * 100
           }, {
-            colors: ['rgb(230, 246, 157)'],
+            colors: [COLORS.STORAGE_PROVIDER],
             label: 'Storage Providers',
             value: data.storageProviders.stakeShare * 100
           }, {
-            colors: ['rgb(170, 222, 167)'],
+            colors: [COLORS.STORAGE_LEAD],
             label: 'Storage Lead',
             value: data.storageProviders.lead.stakeShare * 100
           }, {
-            colors: ['rgb(100, 194, 166)'],
+            colors: [COLORS.CONTENT_CURATOR],
             label: 'Content Curators',
             value: data.contentCurators.stakeShare * 100
-          },
-          {
-            colors: ['rgb(100, 160, 190)'],
+          }, {
+            colors: [COLORS.CURATOR_LEAD],
             label: 'Content Curators Lead',
             value: data.contentCurators.lead.stakeShare * 100
+          }, {
+            colors: [COLORS.OPERATIONS],
+            label: 'Operations',
+            value: data.operations.stakeShare * 100
+          }, {
+            colors: [COLORS.OPERATIONS_LEAD],
+            label: 'Operations Lead',
+            value: data.operations.lead.stakeShare * 100
           }
           ]} />
         <Label as='div'>

+ 12 - 0
pioneer/packages/joy-tokenomics/src/Overview/index.tsx

@@ -38,6 +38,17 @@ const StyledTokenomicsCharts = styled(TokenomicsCharts)`
   }
 `;
 
+const COLORS = {
+  VALIDATOR: '#ff9800',
+  COUNCIL_MEMBER: '#ffc107',
+  STORAGE_PROVIDER: '#ffeb3b',
+  STORAGE_LEAD: '#cddc39',
+  CONTENT_CURATOR: '#8bc34a',
+  CURATOR_LEAD: '#4caf50',
+  OPERATIONS: '#009688',
+  OPERATIONS_LEAD: '#00bcd4'
+};
+
 const Overview: React.FC = () => {
   const transport = useTransport();
   const [statusDataValue, statusDataError] = usePromise<StatusServerData | undefined>(() => fetch('https://status.joystream.org/status').then((res) => res.json().then((data) => data as StatusServerData)), undefined, []);
@@ -57,3 +68,4 @@ const Overview: React.FC = () => {
 };
 
 export default Overview;
+export { COLORS };

+ 4 - 2
pioneer/packages/joy-utils/src/transport/tokenomics.ts

@@ -227,7 +227,8 @@ export default class TokenomicsTransport extends BaseTransport {
       await this.getCouncilData();
     const workingGroupsData = {
       storageProviders: await this.getWorkingGroupData('Storage'),
-      curators: await this.getWorkingGroupData('Content')
+      curators: await this.getWorkingGroupData('Content'),
+      operations: await this.getWorkingGroupData('Operations')
     };
     const { numberOfValidators, numberOfNominators, totalValidatorStake, validatorRewardsPerWeek, totalIssuance } =
       await this.getValidatorData();
@@ -291,7 +292,8 @@ export default class TokenomicsTransport extends BaseTransport {
         stakeShare: totalCouncilStake / currentlyStakedTokens
       },
       storageProviders: resolveGroupData(workingGroupsData.storageProviders),
-      contentCurators: resolveGroupData(workingGroupsData.curators)
+      contentCurators: resolveGroupData(workingGroupsData.curators),
+      operations: resolveGroupData(workingGroupsData.operations)
     };
   }
 }

+ 1 - 0
pioneer/packages/joy-utils/src/types/tokenomics.ts

@@ -37,6 +37,7 @@ export type TokenomicsData = {
   };
   storageProviders: WorkingGroupTokenomicsData;
   contentCurators: WorkingGroupTokenomicsData;
+  operations: WorkingGroupTokenomicsData;
 }
 
 export type StatusServerData = {

+ 80 - 78
query-node/generated/graphql-server/generated/binding.ts

@@ -12,9 +12,6 @@ export interface Query {
     dataObjects: <T = Array<DataObject>>(args: { offset?: Int | null, limit?: Int | null, where?: DataObjectWhereInput | null, orderBy?: DataObjectOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
     dataObjectByUniqueInput: <T = DataObject | null>(args: { where: DataObjectWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T | null> ,
     dataObjectsConnection: <T = DataObjectConnection>(args: { first?: Int | null, after?: String | null, last?: Int | null, before?: String | null, where?: DataObjectWhereInput | null, orderBy?: DataObjectOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
-    featuredVideos: <T = Array<FeaturedVideo>>(args: { offset?: Int | null, limit?: Int | null, where?: FeaturedVideoWhereInput | null, orderBy?: FeaturedVideoOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
-    featuredVideoByUniqueInput: <T = FeaturedVideo | null>(args: { where: FeaturedVideoWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T | null> ,
-    featuredVideosConnection: <T = FeaturedVideoConnection>(args: { first?: Int | null, after?: String | null, last?: Int | null, before?: String | null, where?: FeaturedVideoWhereInput | null, orderBy?: FeaturedVideoOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
     channelCategories: <T = Array<ChannelCategory>>(args: { offset?: Int | null, limit?: Int | null, where?: ChannelCategoryWhereInput | null, orderBy?: ChannelCategoryOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
     channelCategoryByUniqueInput: <T = ChannelCategory | null>(args: { where: ChannelCategoryWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T | null> ,
     channelCategoriesConnection: <T = ChannelCategoryConnection>(args: { first?: Int | null, after?: String | null, last?: Int | null, before?: String | null, where?: ChannelCategoryWhereInput | null, orderBy?: ChannelCategoryOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
@@ -30,6 +27,9 @@ export interface Query {
     memberships: <T = Array<Membership>>(args: { offset?: Int | null, limit?: Int | null, where?: MembershipWhereInput | null, orderBy?: MembershipOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
     membershipByUniqueInput: <T = Membership | null>(args: { where: MembershipWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T | null> ,
     membershipsConnection: <T = MembershipConnection>(args: { first?: Int | null, after?: String | null, last?: Int | null, before?: String | null, where?: MembershipWhereInput | null, orderBy?: MembershipOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    nextEntityIds: <T = Array<NextEntityId>>(args: { offset?: Int | null, limit?: Int | null, where?: NextEntityIdWhereInput | null, orderBy?: NextEntityIdOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
+    nextEntityIdByUniqueInput: <T = NextEntityId | null>(args: { where: NextEntityIdWhereUniqueInput }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T | null> ,
+    nextEntityIdsConnection: <T = NextEntityIdConnection>(args: { first?: Int | null, after?: String | null, last?: Int | null, before?: String | null, where?: NextEntityIdWhereInput | null, orderBy?: NextEntityIdOrderByInput | null }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
     channelCategoriesByName: <T = Array<ChannelCategoriesByNameFTSOutput>>(args: { whereChannelCategory?: ChannelCategoryWhereInput | null, skip?: Int | null, limit?: Int | null, text: String }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
     membersByHandle: <T = Array<MembersByHandleFTSOutput>>(args: { whereMembership?: MembershipWhereInput | null, skip?: Int | null, limit?: Int | null, text: String }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
     search: <T = Array<SearchFTSOutput>>(args: { whereVideo?: VideoWhereInput | null, whereChannel?: ChannelWhereInput | null, skip?: Int | null, limit?: Int | null, text: String }, info?: GraphQLResolveInfo | string, options?: Options) => Promise<T> ,
@@ -115,15 +115,6 @@ export type DataObjectOrderByInput =   'createdAt_ASC' |
   'joystreamContentId_ASC' |
   'joystreamContentId_DESC'
 
-export type FeaturedVideoOrderByInput =   'createdAt_ASC' |
-  'createdAt_DESC' |
-  'updatedAt_ASC' |
-  'updatedAt_DESC' |
-  'deletedAt_ASC' |
-  'deletedAt_DESC' |
-  'videoId_ASC' |
-  'videoId_DESC'
-
 export type ChannelCategoryOrderByInput =   'createdAt_ASC' |
   'createdAt_DESC' |
   'updatedAt_ASC' |
@@ -224,6 +215,15 @@ export type MembershipOrderByInput =   'createdAt_ASC' |
   'subscription_ASC' |
   'subscription_DESC'
 
+export type NextEntityIdOrderByInput =   'createdAt_ASC' |
+  'createdAt_DESC' |
+  'updatedAt_ASC' |
+  'updatedAt_DESC' |
+  'deletedAt_ASC' |
+  'deletedAt_DESC' |
+  'nextId_ASC' |
+  'nextId_DESC'
+
 export type VideoCategoryOrderByInput =   'createdAt_ASC' |
   'createdAt_DESC' |
   'updatedAt_ASC' |
@@ -722,47 +722,6 @@ export interface DataObjectWhereUniqueInput {
   id: ID_Output
 }
 
-export interface FeaturedVideoCreateInput {
-  videoId: ID_Output
-}
-
-export interface FeaturedVideoUpdateInput {
-  videoId?: ID_Input | null
-}
-
-export interface FeaturedVideoWhereInput {
-  id_eq?: ID_Input | null
-  id_in?: ID_Output[] | ID_Output | null
-  createdAt_eq?: DateTime | null
-  createdAt_lt?: DateTime | null
-  createdAt_lte?: DateTime | null
-  createdAt_gt?: DateTime | null
-  createdAt_gte?: DateTime | null
-  createdById_eq?: ID_Input | null
-  createdById_in?: ID_Output[] | ID_Output | null
-  updatedAt_eq?: DateTime | null
-  updatedAt_lt?: DateTime | null
-  updatedAt_lte?: DateTime | null
-  updatedAt_gt?: DateTime | null
-  updatedAt_gte?: DateTime | null
-  updatedById_eq?: ID_Input | null
-  updatedById_in?: ID_Output[] | ID_Output | null
-  deletedAt_all?: Boolean | null
-  deletedAt_eq?: DateTime | null
-  deletedAt_lt?: DateTime | null
-  deletedAt_lte?: DateTime | null
-  deletedAt_gt?: DateTime | null
-  deletedAt_gte?: DateTime | null
-  deletedById_eq?: ID_Input | null
-  deletedById_in?: ID_Output[] | ID_Output | null
-  videoId_eq?: ID_Input | null
-  videoId_in?: ID_Output[] | ID_Output | null
-}
-
-export interface FeaturedVideoWhereUniqueInput {
-  id: ID_Output
-}
-
 export interface ChannelCategoryCreateInput {
   name?: String | null
   createdInBlock: Float
@@ -1129,6 +1088,51 @@ export interface MembershipWhereUniqueInput {
   handle?: String | null
 }
 
+export interface NextEntityIdCreateInput {
+  nextId: Float
+}
+
+export interface NextEntityIdUpdateInput {
+  nextId?: Float | null
+}
+
+export interface NextEntityIdWhereInput {
+  id_eq?: ID_Input | null
+  id_in?: ID_Output[] | ID_Output | null
+  createdAt_eq?: DateTime | null
+  createdAt_lt?: DateTime | null
+  createdAt_lte?: DateTime | null
+  createdAt_gt?: DateTime | null
+  createdAt_gte?: DateTime | null
+  createdById_eq?: ID_Input | null
+  createdById_in?: ID_Output[] | ID_Output | null
+  updatedAt_eq?: DateTime | null
+  updatedAt_lt?: DateTime | null
+  updatedAt_lte?: DateTime | null
+  updatedAt_gt?: DateTime | null
+  updatedAt_gte?: DateTime | null
+  updatedById_eq?: ID_Input | null
+  updatedById_in?: ID_Output[] | ID_Output | null
+  deletedAt_all?: Boolean | null
+  deletedAt_eq?: DateTime | null
+  deletedAt_lt?: DateTime | null
+  deletedAt_lte?: DateTime | null
+  deletedAt_gt?: DateTime | null
+  deletedAt_gte?: DateTime | null
+  deletedById_eq?: ID_Input | null
+  deletedById_in?: ID_Output[] | ID_Output | null
+  nextId_eq?: Float | null
+  nextId_gt?: Float | null
+  nextId_gte?: Float | null
+  nextId_lt?: Float | null
+  nextId_lte?: Float | null
+  nextId_in?: Float[] | Float | null
+}
+
+export interface NextEntityIdWhereUniqueInput {
+  id: ID_Output
+}
+
 export interface VideoCategoryCreateInput {
   name?: String | null
   createdInBlock: Float
@@ -1628,30 +1632,6 @@ export interface DataObjectOwnerWorkingGroup {
   workingGroup: Int
 }
 
-export interface FeaturedVideo extends BaseGraphQLObject {
-  id: ID_Output
-  createdAt: DateTime
-  createdById: String
-  updatedAt?: DateTime | null
-  updatedById?: String | null
-  deletedAt?: DateTime | null
-  deletedById?: String | null
-  version: Int
-  video: Video
-  videoId: String
-}
-
-export interface FeaturedVideoConnection {
-  totalCount: Int
-  edges: Array<FeaturedVideoEdge>
-  pageInfo: PageInfo
-}
-
-export interface FeaturedVideoEdge {
-  node: FeaturedVideo
-  cursor: String
-}
-
 export interface Channel extends BaseGraphQLObject {
   id: ID_Output
   createdAt: DateTime
@@ -1827,6 +1807,29 @@ export interface MembershipEdge {
   cursor: String
 }
 
+export interface NextEntityId extends BaseGraphQLObject {
+  id: ID_Output
+  createdAt: DateTime
+  createdById: String
+  updatedAt?: DateTime | null
+  updatedById?: String | null
+  deletedAt?: DateTime | null
+  deletedById?: String | null
+  version: Int
+  nextId: Float
+}
+
+export interface NextEntityIdConnection {
+  totalCount: Int
+  edges: Array<NextEntityIdEdge>
+  pageInfo: PageInfo
+}
+
+export interface NextEntityIdEdge {
+  node: NextEntityId
+  cursor: String
+}
+
 export interface PageInfo {
   hasNextPage: Boolean
   hasPreviousPage: Boolean
@@ -1889,7 +1892,6 @@ export interface Video extends BaseGraphQLObject {
   mediaMetadataId?: String | null
   createdInBlock: Int
   isFeatured: Boolean
-  featured?: FeaturedVideo | null
 }
 
 export interface VideoCategoriesByNameFTSOutput {

+ 152 - 140
query-node/generated/graphql-server/generated/classes.ts

@@ -43,8 +43,6 @@ import { VideoMediaEncoding } from "../src/modules/video-media-encoding/video-me
 // @ts-ignore
 import { VideoMediaMetadata } from "../src/modules/video-media-metadata/video-media-metadata.model";
 // @ts-ignore
-import { FeaturedVideo } from "../src/modules/featured-video/featured-video.model";
-// @ts-ignore
 import { Video } from "../src/modules/video/video.model";
 // @ts-ignore
 import { Language } from "../src/modules/language/language.model";
@@ -54,6 +52,8 @@ import { Channel } from "../src/modules/channel/channel.model";
 import { DataObject } from "../src/modules/data-object/data-object.model";
 // @ts-ignore
 import { Worker } from "../src/modules/worker/worker.model";
+// @ts-ignore
+import { NextEntityId } from "../src/modules/next-entity-id/next-entity-id.model";
 
 export enum DataObjectOwnerChannelOrderByEnum {
   createdAt_ASC = "createdAt_ASC",
@@ -2309,144 +2309,6 @@ export class VideoMediaMetadataUpdateArgs {
   @TypeGraphQLField() where!: VideoMediaMetadataWhereUniqueInput;
 }
 
-export enum FeaturedVideoOrderByEnum {
-  createdAt_ASC = "createdAt_ASC",
-  createdAt_DESC = "createdAt_DESC",
-
-  updatedAt_ASC = "updatedAt_ASC",
-  updatedAt_DESC = "updatedAt_DESC",
-
-  deletedAt_ASC = "deletedAt_ASC",
-  deletedAt_DESC = "deletedAt_DESC",
-
-  videoId_ASC = "videoId_ASC",
-  videoId_DESC = "videoId_DESC"
-}
-
-registerEnumType(FeaturedVideoOrderByEnum, {
-  name: "FeaturedVideoOrderByInput"
-});
-
-@TypeGraphQLInputType()
-export class FeaturedVideoWhereInput {
-  @TypeGraphQLField(() => ID, { nullable: true })
-  id_eq?: string;
-
-  @TypeGraphQLField(() => [ID], { nullable: true })
-  id_in?: string[];
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  createdAt_eq?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  createdAt_lt?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  createdAt_lte?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  createdAt_gt?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  createdAt_gte?: Date;
-
-  @TypeGraphQLField(() => ID, { nullable: true })
-  createdById_eq?: string;
-
-  @TypeGraphQLField(() => [ID], { nullable: true })
-  createdById_in?: string[];
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  updatedAt_eq?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  updatedAt_lt?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  updatedAt_lte?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  updatedAt_gt?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  updatedAt_gte?: Date;
-
-  @TypeGraphQLField(() => ID, { nullable: true })
-  updatedById_eq?: string;
-
-  @TypeGraphQLField(() => [ID], { nullable: true })
-  updatedById_in?: string[];
-
-  @TypeGraphQLField({ nullable: true })
-  deletedAt_all?: Boolean;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  deletedAt_eq?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  deletedAt_lt?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  deletedAt_lte?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  deletedAt_gt?: Date;
-
-  @TypeGraphQLField(() => DateTime, { nullable: true })
-  deletedAt_gte?: Date;
-
-  @TypeGraphQLField(() => ID, { nullable: true })
-  deletedById_eq?: string;
-
-  @TypeGraphQLField(() => [ID], { nullable: true })
-  deletedById_in?: string[];
-
-  @TypeGraphQLField(() => ID, { nullable: true })
-  videoId_eq?: string;
-
-  @TypeGraphQLField(() => [ID], { nullable: true })
-  videoId_in?: string[];
-}
-
-@TypeGraphQLInputType()
-export class FeaturedVideoWhereUniqueInput {
-  @TypeGraphQLField(() => ID)
-  id?: string;
-}
-
-@TypeGraphQLInputType()
-export class FeaturedVideoCreateInput {
-  @TypeGraphQLField(() => ID)
-  videoId!: string;
-}
-
-@TypeGraphQLInputType()
-export class FeaturedVideoUpdateInput {
-  @TypeGraphQLField(() => ID, { nullable: true })
-  videoId?: string;
-}
-
-@ArgsType()
-export class FeaturedVideoWhereArgs extends PaginationArgs {
-  @TypeGraphQLField(() => FeaturedVideoWhereInput, { nullable: true })
-  where?: FeaturedVideoWhereInput;
-
-  @TypeGraphQLField(() => FeaturedVideoOrderByEnum, { nullable: true })
-  orderBy?: FeaturedVideoOrderByEnum;
-}
-
-@ArgsType()
-export class FeaturedVideoCreateManyArgs {
-  @TypeGraphQLField(() => [FeaturedVideoCreateInput])
-  data!: FeaturedVideoCreateInput[];
-}
-
-@ArgsType()
-export class FeaturedVideoUpdateArgs {
-  @TypeGraphQLField() data!: FeaturedVideoUpdateInput;
-  @TypeGraphQLField() where!: FeaturedVideoWhereUniqueInput;
-}
-
 export enum VideoOrderByEnum {
   createdAt_ASC = "createdAt_ASC",
   createdAt_DESC = "createdAt_DESC",
@@ -3967,3 +3829,153 @@ export class WorkerUpdateArgs {
   @TypeGraphQLField() data!: WorkerUpdateInput;
   @TypeGraphQLField() where!: WorkerWhereUniqueInput;
 }
+
+export enum NextEntityIdOrderByEnum {
+  createdAt_ASC = "createdAt_ASC",
+  createdAt_DESC = "createdAt_DESC",
+
+  updatedAt_ASC = "updatedAt_ASC",
+  updatedAt_DESC = "updatedAt_DESC",
+
+  deletedAt_ASC = "deletedAt_ASC",
+  deletedAt_DESC = "deletedAt_DESC",
+
+  nextId_ASC = "nextId_ASC",
+  nextId_DESC = "nextId_DESC"
+}
+
+registerEnumType(NextEntityIdOrderByEnum, {
+  name: "NextEntityIdOrderByInput"
+});
+
+@TypeGraphQLInputType()
+export class NextEntityIdWhereInput {
+  @TypeGraphQLField(() => ID, { nullable: true })
+  id_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  id_in?: string[];
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  createdAt_eq?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  createdAt_lt?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  createdAt_lte?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  createdAt_gt?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  createdAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  createdById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  createdById_in?: string[];
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  updatedAt_eq?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  updatedAt_lt?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  updatedAt_lte?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  updatedAt_gt?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  updatedAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  updatedById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  updatedById_in?: string[];
+
+  @TypeGraphQLField({ nullable: true })
+  deletedAt_all?: Boolean;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  deletedAt_eq?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  deletedAt_lt?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  deletedAt_lte?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  deletedAt_gt?: Date;
+
+  @TypeGraphQLField(() => DateTime, { nullable: true })
+  deletedAt_gte?: Date;
+
+  @TypeGraphQLField(() => ID, { nullable: true })
+  deletedById_eq?: string;
+
+  @TypeGraphQLField(() => [ID], { nullable: true })
+  deletedById_in?: string[];
+
+  @TypeGraphQLField(() => Float, { nullable: true })
+  nextId_eq?: number;
+
+  @TypeGraphQLField(() => Float, { nullable: true })
+  nextId_gt?: number;
+
+  @TypeGraphQLField(() => Float, { nullable: true })
+  nextId_gte?: number;
+
+  @TypeGraphQLField(() => Float, { nullable: true })
+  nextId_lt?: number;
+
+  @TypeGraphQLField(() => Float, { nullable: true })
+  nextId_lte?: number;
+
+  @TypeGraphQLField(() => [Float], { nullable: true })
+  nextId_in?: number[];
+}
+
+@TypeGraphQLInputType()
+export class NextEntityIdWhereUniqueInput {
+  @TypeGraphQLField(() => ID)
+  id?: string;
+}
+
+@TypeGraphQLInputType()
+export class NextEntityIdCreateInput {
+  @TypeGraphQLField()
+  nextId!: number;
+}
+
+@TypeGraphQLInputType()
+export class NextEntityIdUpdateInput {
+  @TypeGraphQLField({ nullable: true })
+  nextId?: number;
+}
+
+@ArgsType()
+export class NextEntityIdWhereArgs extends PaginationArgs {
+  @TypeGraphQLField(() => NextEntityIdWhereInput, { nullable: true })
+  where?: NextEntityIdWhereInput;
+
+  @TypeGraphQLField(() => NextEntityIdOrderByEnum, { nullable: true })
+  orderBy?: NextEntityIdOrderByEnum;
+}
+
+@ArgsType()
+export class NextEntityIdCreateManyArgs {
+  @TypeGraphQLField(() => [NextEntityIdCreateInput])
+  data!: NextEntityIdCreateInput[];
+}
+
+@ArgsType()
+export class NextEntityIdUpdateArgs {
+  @TypeGraphQLField() data!: NextEntityIdUpdateInput;
+  @TypeGraphQLField() where!: NextEntityIdWhereUniqueInput;
+}

+ 84 - 80
query-node/generated/graphql-server/generated/schema.graphql

@@ -586,82 +586,6 @@ interface DeleteResponse {
   id: ID!
 }
 
-type FeaturedVideo implements BaseGraphQLObject {
-  id: ID!
-  createdAt: DateTime!
-  createdById: String!
-  updatedAt: DateTime
-  updatedById: String
-  deletedAt: DateTime
-  deletedById: String
-  version: Int!
-  video: Video!
-  videoId: String!
-}
-
-type FeaturedVideoConnection {
-  totalCount: Int!
-  edges: [FeaturedVideoEdge!]!
-  pageInfo: PageInfo!
-}
-
-input FeaturedVideoCreateInput {
-  videoId: ID!
-}
-
-type FeaturedVideoEdge {
-  node: FeaturedVideo!
-  cursor: String!
-}
-
-enum FeaturedVideoOrderByInput {
-  createdAt_ASC
-  createdAt_DESC
-  updatedAt_ASC
-  updatedAt_DESC
-  deletedAt_ASC
-  deletedAt_DESC
-  videoId_ASC
-  videoId_DESC
-}
-
-input FeaturedVideoUpdateInput {
-  videoId: ID
-}
-
-input FeaturedVideoWhereInput {
-  id_eq: ID
-  id_in: [ID!]
-  createdAt_eq: DateTime
-  createdAt_lt: DateTime
-  createdAt_lte: DateTime
-  createdAt_gt: DateTime
-  createdAt_gte: DateTime
-  createdById_eq: ID
-  createdById_in: [ID!]
-  updatedAt_eq: DateTime
-  updatedAt_lt: DateTime
-  updatedAt_lte: DateTime
-  updatedAt_gt: DateTime
-  updatedAt_gte: DateTime
-  updatedById_eq: ID
-  updatedById_in: [ID!]
-  deletedAt_all: Boolean
-  deletedAt_eq: DateTime
-  deletedAt_lt: DateTime
-  deletedAt_lte: DateTime
-  deletedAt_gt: DateTime
-  deletedAt_gte: DateTime
-  deletedById_eq: ID
-  deletedById_in: [ID!]
-  videoId_eq: ID
-  videoId_in: [ID!]
-}
-
-input FeaturedVideoWhereUniqueInput {
-  id: ID!
-}
-
 type Channel implements BaseGraphQLObject {
   id: ID!
   createdAt: DateTime!
@@ -1364,6 +1288,87 @@ input MembershipWhereUniqueInput {
   handle: String
 }
 
+type NextEntityId implements BaseGraphQLObject {
+  id: ID!
+  createdAt: DateTime!
+  createdById: String!
+  updatedAt: DateTime
+  updatedById: String
+  deletedAt: DateTime
+  deletedById: String
+  version: Int!
+
+  """Next deterministic id for entities without custom id"""
+  nextId: Float!
+}
+
+type NextEntityIdConnection {
+  totalCount: Int!
+  edges: [NextEntityIdEdge!]!
+  pageInfo: PageInfo!
+}
+
+input NextEntityIdCreateInput {
+  nextId: Float!
+}
+
+type NextEntityIdEdge {
+  node: NextEntityId!
+  cursor: String!
+}
+
+enum NextEntityIdOrderByInput {
+  createdAt_ASC
+  createdAt_DESC
+  updatedAt_ASC
+  updatedAt_DESC
+  deletedAt_ASC
+  deletedAt_DESC
+  nextId_ASC
+  nextId_DESC
+}
+
+input NextEntityIdUpdateInput {
+  nextId: Float
+}
+
+input NextEntityIdWhereInput {
+  id_eq: ID
+  id_in: [ID!]
+  createdAt_eq: DateTime
+  createdAt_lt: DateTime
+  createdAt_lte: DateTime
+  createdAt_gt: DateTime
+  createdAt_gte: DateTime
+  createdById_eq: ID
+  createdById_in: [ID!]
+  updatedAt_eq: DateTime
+  updatedAt_lt: DateTime
+  updatedAt_lte: DateTime
+  updatedAt_gt: DateTime
+  updatedAt_gte: DateTime
+  updatedById_eq: ID
+  updatedById_in: [ID!]
+  deletedAt_all: Boolean
+  deletedAt_eq: DateTime
+  deletedAt_lt: DateTime
+  deletedAt_lte: DateTime
+  deletedAt_gt: DateTime
+  deletedAt_gte: DateTime
+  deletedById_eq: ID
+  deletedById_in: [ID!]
+  nextId_eq: Float
+  nextId_gt: Float
+  nextId_gte: Float
+  nextId_lt: Float
+  nextId_lte: Float
+  nextId_in: [Float!]
+}
+
+input NextEntityIdWhereUniqueInput {
+  id: ID!
+}
+
 type PageInfo {
   hasNextPage: Boolean!
   hasPreviousPage: Boolean!
@@ -1385,9 +1390,6 @@ type Query {
   dataObjects(offset: Int, limit: Int = 50, where: DataObjectWhereInput, orderBy: DataObjectOrderByInput): [DataObject!]!
   dataObjectByUniqueInput(where: DataObjectWhereUniqueInput!): DataObject
   dataObjectsConnection(first: Int, after: String, last: Int, before: String, where: DataObjectWhereInput, orderBy: DataObjectOrderByInput): DataObjectConnection!
-  featuredVideos(offset: Int, limit: Int = 50, where: FeaturedVideoWhereInput, orderBy: FeaturedVideoOrderByInput): [FeaturedVideo!]!
-  featuredVideoByUniqueInput(where: FeaturedVideoWhereUniqueInput!): FeaturedVideo
-  featuredVideosConnection(first: Int, after: String, last: Int, before: String, where: FeaturedVideoWhereInput, orderBy: FeaturedVideoOrderByInput): FeaturedVideoConnection!
   channelCategories(offset: Int, limit: Int = 50, where: ChannelCategoryWhereInput, orderBy: ChannelCategoryOrderByInput): [ChannelCategory!]!
   channelCategoryByUniqueInput(where: ChannelCategoryWhereUniqueInput!): ChannelCategory
   channelCategoriesConnection(first: Int, after: String, last: Int, before: String, where: ChannelCategoryWhereInput, orderBy: ChannelCategoryOrderByInput): ChannelCategoryConnection!
@@ -1403,6 +1405,9 @@ type Query {
   memberships(offset: Int, limit: Int = 50, where: MembershipWhereInput, orderBy: MembershipOrderByInput): [Membership!]!
   membershipByUniqueInput(where: MembershipWhereUniqueInput!): Membership
   membershipsConnection(first: Int, after: String, last: Int, before: String, where: MembershipWhereInput, orderBy: MembershipOrderByInput): MembershipConnection!
+  nextEntityIds(offset: Int, limit: Int = 50, where: NextEntityIdWhereInput, orderBy: NextEntityIdOrderByInput): [NextEntityId!]!
+  nextEntityIdByUniqueInput(where: NextEntityIdWhereUniqueInput!): NextEntityId
+  nextEntityIdsConnection(first: Int, after: String, last: Int, before: String, where: NextEntityIdWhereInput, orderBy: NextEntityIdOrderByInput): NextEntityIdConnection!
   channelCategoriesByName(whereChannelCategory: ChannelCategoryWhereInput, skip: Int = 0, limit: Int = 5, text: String!): [ChannelCategoriesByNameFTSOutput!]!
   membersByHandle(whereMembership: MembershipWhereInput, skip: Int = 0, limit: Int = 5, text: String!): [MembersByHandleFTSOutput!]!
   search(whereVideo: VideoWhereInput, whereChannel: ChannelWhereInput, skip: Int = 0, limit: Int = 5, text: String!): [SearchFTSOutput!]!
@@ -1506,7 +1511,6 @@ type Video implements BaseGraphQLObject {
 
   """Is video featured or not"""
   isFeatured: Boolean!
-  featured: FeaturedVideo
 }
 
 type VideoCategoriesByNameFTSOutput {

+ 2 - 0
query-node/generated/graphql-server/model/index.ts

@@ -12,6 +12,8 @@ import { License } from '../src/modules/license/license.model';
 export { License };
 import { Membership } from '../src/modules/membership/membership.model';
 export { Membership };
+import { NextEntityId } from '../src/modules/next-entity-id/next-entity-id.model';
+export { NextEntityId };
 import { Video } from '../src/modules/video/video.model';
 export { Video };
 import { VideoCategory } from '../src/modules/video-category/video-category.model';

+ 14 - 0
query-node/generated/graphql-server/src/modules/next-entity-id/next-entity-id.model.ts

@@ -0,0 +1,14 @@
+import { BaseModel, FloatField, Model, StringField } from 'warthog';
+
+@Model({ api: {} })
+export class NextEntityId extends BaseModel {
+  @FloatField({
+    description: `Next deterministic id for entities without custom id`,
+  })
+  nextId!: number;
+
+  constructor(init?: Partial<NextEntityId>) {
+    super();
+    Object.assign(this, init);
+  }
+}

+ 128 - 0
query-node/generated/graphql-server/src/modules/next-entity-id/next-entity-id.resolver.ts

@@ -0,0 +1,128 @@
+import {
+  Arg,
+  Args,
+  Mutation,
+  Query,
+  Root,
+  Resolver,
+  FieldResolver,
+  ObjectType,
+  Field,
+  Int,
+  ArgsType,
+  Info,
+} from 'type-graphql';
+import graphqlFields from 'graphql-fields';
+import { Inject } from 'typedi';
+import { Min } from 'class-validator';
+import { Fields, StandardDeleteResponse, UserId, PageInfo, RawFields } from 'warthog';
+
+import {
+  NextEntityIdCreateInput,
+  NextEntityIdCreateManyArgs,
+  NextEntityIdUpdateArgs,
+  NextEntityIdWhereArgs,
+  NextEntityIdWhereInput,
+  NextEntityIdWhereUniqueInput,
+  NextEntityIdOrderByEnum,
+} from '../../../generated';
+
+import { NextEntityId } from './next-entity-id.model';
+import { NextEntityIdService } from './next-entity-id.service';
+
+@ObjectType()
+export class NextEntityIdEdge {
+  @Field(() => NextEntityId, { nullable: false })
+  node!: NextEntityId;
+
+  @Field(() => String, { nullable: false })
+  cursor!: string;
+}
+
+@ObjectType()
+export class NextEntityIdConnection {
+  @Field(() => Int, { nullable: false })
+  totalCount!: number;
+
+  @Field(() => [NextEntityIdEdge], { nullable: false })
+  edges!: NextEntityIdEdge[];
+
+  @Field(() => PageInfo, { nullable: false })
+  pageInfo!: PageInfo;
+}
+
+@ArgsType()
+export class ConnectionPageInputOptions {
+  @Field(() => Int, { nullable: true })
+  @Min(0)
+  first?: number;
+
+  @Field(() => String, { nullable: true })
+  after?: string; // V3: TODO: should we make a RelayCursor scalar?
+
+  @Field(() => Int, { nullable: true })
+  @Min(0)
+  last?: number;
+
+  @Field(() => String, { nullable: true })
+  before?: string;
+}
+
+@ArgsType()
+export class NextEntityIdConnectionWhereArgs extends ConnectionPageInputOptions {
+  @Field(() => NextEntityIdWhereInput, { nullable: true })
+  where?: NextEntityIdWhereInput;
+
+  @Field(() => NextEntityIdOrderByEnum, { nullable: true })
+  orderBy?: NextEntityIdOrderByEnum;
+}
+
+@Resolver(NextEntityId)
+export class NextEntityIdResolver {
+  constructor(@Inject('NextEntityIdService') public readonly service: NextEntityIdService) {}
+
+  @Query(() => [NextEntityId])
+  async nextEntityIds(
+    @Args() { where, orderBy, limit, offset }: NextEntityIdWhereArgs,
+    @Fields() fields: string[]
+  ): Promise<NextEntityId[]> {
+    return this.service.find<NextEntityIdWhereInput>(where, orderBy, limit, offset, fields);
+  }
+
+  @Query(() => NextEntityId, { nullable: true })
+  async nextEntityIdByUniqueInput(
+    @Arg('where') where: NextEntityIdWhereUniqueInput,
+    @Fields() fields: string[]
+  ): Promise<NextEntityId | null> {
+    const result = await this.service.find(where, undefined, 1, 0, fields);
+    return result && result.length >= 1 ? result[0] : null;
+  }
+
+  @Query(() => NextEntityIdConnection)
+  async nextEntityIdsConnection(
+    @Args() { where, orderBy, ...pageOptions }: NextEntityIdConnectionWhereArgs,
+    @Info() info: any
+  ): Promise<NextEntityIdConnection> {
+    const rawFields = graphqlFields(info, {}, { excludedFields: ['__typename'] });
+
+    let result: any = {
+      totalCount: 0,
+      edges: [],
+      pageInfo: {
+        hasNextPage: false,
+        hasPreviousPage: false,
+      },
+    };
+    // If the related database table does not have any records then an error is thrown to the client
+    // by warthog
+    try {
+      result = await this.service.findConnection<NextEntityIdWhereInput>(where, orderBy, pageOptions, rawFields);
+    } catch (err) {
+      console.log(err);
+      // TODO: should continue to return this on `Error: Items is empty` or throw the error
+      if (!(err.message as string).includes('Items is empty')) throw err;
+    }
+
+    return result as Promise<NextEntityIdConnection>;
+  }
+}

+ 28 - 0
query-node/generated/graphql-server/src/modules/next-entity-id/next-entity-id.service.ts

@@ -0,0 +1,28 @@
+import { Service } from 'typedi';
+import { Repository } from 'typeorm';
+import { InjectRepository } from 'typeorm-typedi-extensions';
+import { BaseService, WhereInput } from 'warthog';
+
+import { NextEntityId } from './next-entity-id.model';
+
+@Service('NextEntityIdService')
+export class NextEntityIdService extends BaseService<NextEntityId> {
+  constructor(@InjectRepository(NextEntityId) protected readonly repository: Repository<NextEntityId>) {
+    super(NextEntityId, repository);
+  }
+
+  async find<W extends WhereInput>(
+    where?: any,
+    orderBy?: string,
+    limit?: number,
+    offset?: number,
+    fields?: string[]
+  ): Promise<NextEntityId[]> {
+    let f = fields;
+    if (f == undefined) {
+      f = [];
+    }
+
+    return super.find<W>(where, orderBy, limit, offset, f);
+  }
+}

+ 70 - 0
query-node/generated/types/members.ts

@@ -613,4 +613,74 @@ export namespace Members {
       ]);
     }
   }
+  /**
+   *  Update member's all or some of handle, avatar and about text.
+   */
+  export class UpdateMembershipCall {
+    public readonly extrinsic: SubstrateExtrinsic;
+    public readonly expectedArgTypes = [
+      "MemberId",
+      "Option<Bytes>",
+      "Option<Bytes>",
+      "Option<Bytes>",
+    ];
+
+    constructor(public readonly ctx: SubstrateEvent) {
+      if (ctx.extrinsic === undefined) {
+        throw new Error(`No call data has been provided`);
+      }
+      this.extrinsic = ctx.extrinsic;
+    }
+
+    get args(): UpdateMembership_Args {
+      return new UpdateMembership_Args(this.extrinsic);
+    }
+
+    validateArgs(): boolean {
+      if (this.expectedArgTypes.length !== this.extrinsic.args.length) {
+        return false;
+      }
+      let valid = true;
+      this.expectedArgTypes.forEach((type, i) => {
+        if (type !== this.extrinsic.args[i].type) {
+          valid = false;
+        }
+      });
+      return valid;
+    }
+  }
+
+  class UpdateMembership_Args {
+    constructor(public readonly extrinsic: SubstrateExtrinsic) {}
+
+    get memberId(): MemberId {
+      return createTypeUnsafe<MemberId & Codec>(typeRegistry, "MemberId", [
+        this.extrinsic.args[0].value,
+      ]);
+    }
+
+    get handle(): Option<Bytes> {
+      return createTypeUnsafe<Option<Bytes> & Codec>(
+        typeRegistry,
+        "Option<Bytes>",
+        [this.extrinsic.args[1].value]
+      );
+    }
+
+    get avatarUri(): Option<Bytes> {
+      return createTypeUnsafe<Option<Bytes> & Codec>(
+        typeRegistry,
+        "Option<Bytes>",
+        [this.extrinsic.args[2].value]
+      );
+    }
+
+    get about(): Option<Bytes> {
+      return createTypeUnsafe<Option<Bytes> & Codec>(
+        typeRegistry,
+        "Option<Bytes>",
+        [this.extrinsic.args[3].value]
+      );
+    }
+  }
 }

+ 1 - 0
query-node/manifest.yml

@@ -74,6 +74,7 @@ typegen:
     - members.changeMemberHandle
     - members.setRootAccount
     - members.setControllerAccount
+    - members.updateMembership
 
     # content directory
     - content.create_curator_group

+ 113 - 13
query-node/mappings/src/common.ts

@@ -1,8 +1,10 @@
 import { SubstrateEvent } from '@dzlzv/hydra-common'
 import { DatabaseManager } from '@dzlzv/hydra-db-utils'
-import { u64 } from '@polkadot/types/primitive';
-import * as crypto from 'crypto'
+import { u64 } from '@polkadot/types/primitive'
 import { fixBlockTimestamp } from './eventFix'
+=======
+import { SubstrateExtrinsic, ExtrinsicArg } from '@dzlzv/hydra-common'
+>>>>>>> query_node_predictable_ids
 
 // Asset
 import {
@@ -10,6 +12,7 @@ import {
   DataObject,
   LiaisonJudgement,
   Network,
+  NextEntityId,
 } from 'query-node'
 import {
   ContentParameters,
@@ -45,23 +48,27 @@ export function invalidMetadata(extraInfo: string, data?: unknown): void {
 /*
   Creates a predictable and unique ID for the given content.
 */
-export function createPredictableId(event: SubstrateEvent, content: string | Object): string {
-  const contentType = typeof content == 'string'
-    ? content
-    : JSON.stringify(content)
+export async function createPredictableId(db: DatabaseManager): Promise<string> {
+  // load or create record
+  const existingRecord = await db.get(NextEntityId, {}) || new NextEntityId({id: '0', nextId: 0})
 
-  const id = `${event.blockNumber}_${event.index}_${contentType}`
+  // remember id
+  const entityId = existingRecord.nextId
 
-  return crypto
-    .createHash('sha256')
-    .update(id, 'utf-8')
-    .digest('base64')
+  // increment id
+  existingRecord.nextId = existingRecord.nextId + 1
+
+  // save record
+  await db.save<NextEntityId>(existingRecord)
+
+  return entityId.toString()
 }
 
 /*
   Prepares data object from content parameters.
 */
 export async function prepareDataObject(
+  db: DatabaseManager,
   contentParameters: ContentParameters,
   event: SubstrateEvent,
   owner: typeof DataObjectOwner,
@@ -70,6 +77,7 @@ export async function prepareDataObject(
   const customContentParameters = new Custom_ContentParameters(registry, contentParameters.toJSON() as any)
 
   const dataObject = new DataObject({
+    id: await createPredictableId(db),
     owner,
     createdInBlock: event.blockNumber,
     typeId: contentParameters.type_id.toNumber(),
@@ -85,11 +93,103 @@ export async function prepareDataObject(
     updatedById: '1',
   })
 
-  dataObject.id = createPredictableId(event, dataObject)
-
   return dataObject
 }
 
+/////////////////// Sudo extrinsic calls ///////////////////////////////////////
+
+// soft-peg interface for typegen-generated `*Call` types
+export interface IGenericExtrinsicObject<T> {
+  readonly extrinsic: SubstrateExtrinsic
+  readonly expectedArgTypes: string[]
+  args: T
+}
+
+// arguments for calling extrinsic as sudo
+export interface ISudoCallArgs<T> extends ExtrinsicArg {
+  args: T
+  callIndex: string
+}
+
+/*
+  Extracts extrinsic arguments from the Substrate event. Supports both direct extrinsic calls and sudo calls.
+*/
+export function extractExtrinsicArgs<DataParams, EventObject extends IGenericExtrinsicObject<DataParams>>(
+  rawEvent: SubstrateEvent,
+  callFactory: new (event: SubstrateEvent) => EventObject,
+
+  // in ideal world this parameter would not be needed, but there is no way to associate parameters
+  // used in sudo to extrinsic parameters without it
+  argsIndeces: Record<keyof DataParams, number>,
+): EventObject['args'] { // this is equal to DataParams but only this notation works properly
+  // escape when extrinsic info is not available
+  if (!rawEvent.extrinsic) {
+    throw 'Invalid event - no extrinsic set' // this should never happen
+  }
+
+  // regural extrinsic call?
+  if (rawEvent.extrinsic.section != 'sudo') {
+    return (new callFactory(rawEvent)).args
+  }
+
+  // sudo extrinsic call
+
+  const callArgs = extractSudoCallParameters<DataParams>(rawEvent)
+
+  // convert naming convention (underscore_names to camelCase)
+  const clearArgs = Object.keys(callArgs.args).reduce((acc, key) => {
+    const formattedName = key.replace(/_([a-z])/g, tmp => tmp[1].toUpperCase())
+
+    acc[formattedName] = callArgs.args[key]
+
+    return acc
+  }, {} as DataParams)
+
+  // prepare partial event object
+  const partialEvent = {
+    extrinsic: {
+      args: Object.keys(argsIndeces).reduce((acc, key) => {
+        acc[(argsIndeces)[key]] = {
+          value: clearArgs[key]
+        }
+
+        return acc
+      }, [] as unknown[]),
+    } as unknown as SubstrateExtrinsic
+  } as SubstrateEvent
+
+  // create event object and extract processed args
+  const finalArgs = (new callFactory(partialEvent)).args
+
+  return finalArgs
+}
+
+/*
+  Extracts extrinsic call parameters used inside of sudo call.
+*/
+export function extractSudoCallParameters<DataParams>(rawEvent: SubstrateEvent): ISudoCallArgs<DataParams> {
+  if (!rawEvent.extrinsic) {
+    throw 'Invalid event - no extrinsic set' // this should never happen
+  }
+
+  // see Substrate's sudo frame for more info about sudo extrinsics and `call` argument index
+  const argIndex = false
+    || (rawEvent.extrinsic.method == 'sudoAs' && 1) // who, *call*
+    || (rawEvent.extrinsic.method == 'sudo' && 0) // *call*
+    || (rawEvent.extrinsic.method == 'sudoUncheckedWeight' && 0) // *call*, _weight
+
+  // ensure `call` argument was found
+  if (argIndex === false) {
+    // this could possibly happen in sometime in future if new sudo options are introduced in Substrate
+    throw 'Not implemented situation with sudo'
+  }
+
+  // typecast call arguments
+  const callArgs = rawEvent.extrinsic.args[argIndex].value as unknown as ISudoCallArgs<DataParams>
+
+  return callArgs
+}
+
 /////////////////// Logger /////////////////////////////////////////////////////
 
 /*

+ 0 - 2
query-node/mappings/src/content/channel.ts

@@ -197,7 +197,6 @@ export async function content_ChannelCategoryCreated(
 ) {
   // read event data
   const {channelCategoryCreationParameters, channelCategoryId} = new Content.ChannelCategoryCreatedEvent(event).data
-  const {actor: contentActor} = new Content.CreateChannelCategoryCall(event).args
 
   // read metadata
   const protobufContent = await readProtobuf(
@@ -240,7 +239,6 @@ export async function content_ChannelCategoryUpdated(
   const {
     channelCategoryId,
     channelCategoryUpdateParameters,
-    contentActor,
   } = new Content.ChannelCategoryUpdatedEvent(event).data
 
   // load channel category

+ 23 - 5
query-node/mappings/src/content/utils.ts

@@ -314,7 +314,7 @@ export async function readProtobufWithAssets<T extends Channel | Video>(
 
     // prepare license if needed
     if ('license' in metaAsObject) {
-      result.license = await prepareLicense(metaAsObject.license, parameters.event)
+      result.license = await prepareLicense(parameters.db, metaAsObject.license, parameters.event)
     }
 
     // prepare thumbnail photo asset if needed
@@ -480,6 +480,7 @@ async function convertAsset(parameters: IConvertAssetParameters): Promise<AssetS
   // prepare data object
   const contentParameters: ContentParameters = parameters.rawAsset.asUpload
   const dataObject = await prepareDataObject(
+    parameters.db,
     contentParameters,
     parameters.event,
     parameters.contentOwner,
@@ -634,6 +635,7 @@ async function prepareLanguage(
 
   // create new language
   const newLanguage = new Language({
+    id: await createPredictableId(db),
     iso: languageIso,
     createdInBlock: event.blockNumber,
 
@@ -645,14 +647,13 @@ async function prepareLanguage(
     updatedById: '1',
   })
 
-  newLanguage.id = createPredictableId(event, newLanguage)
-
   await db.save<Language>(newLanguage)
 
   return PropertyChange.newChange(newLanguage)
 }
 
 async function prepareLicense(
+  db: DatabaseManager,
   licenseProtobuf: LicenseMetadata.AsObject | undefined,
   event: SubstrateEvent,
 ): Promise<License | undefined> {
@@ -664,9 +665,15 @@ async function prepareLicense(
     return undefined
   }
 
+  // license is meant to be deleted
+  if (isLicenseEmpty(licenseProtobuf)) {
+    return new License({})
+  }
+
   // crete new license
   const license = new License({
     ...licenseProtobuf,
+    id: await createPredictableId(db),
 
     createdAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
     updatedAt: new Date(fixBlockTimestamp(event.blockTimestamp).toNumber()),
@@ -675,11 +682,22 @@ async function prepareLicense(
     updatedById: '1',
   })
 
-  license.id = createPredictableId(event, license)
-
   return license
 }
 
+/*
+  Checks if protobof contains license with some fields filled or is empty object (`{}` or `{someKey: undefined, ...}`).
+  Empty object means deletion is requested.
+*/
+function isLicenseEmpty(licenseObject: LicenseMetadata.AsObject): boolean {
+    let somePropertySet = Object.entries(licenseObject).reduce((acc, [key, value]) => {
+        return acc || value !== undefined
+    }, false)
+
+    return !somePropertySet
+}
+
+
 function prepareVideoMetadata(videoProtobuf: VideoMetadata.AsObject, videoSize: number | undefined, blockNumber: number): RawVideoMetadata {
   const rawMeta = {
     encoding: {

+ 42 - 17
query-node/mappings/src/content/video.ts

@@ -187,7 +187,12 @@ export async function content_VideoCreated(
   }
 
   // prepare video media metadata (if any)
-  const fixedProtobuf = integrateVideoMediaMetadata(null, protobufContent, event)
+  const fixedProtobuf = await integrateVideoMediaMetadata(db, null, protobufContent, event)
+
+  const licenseIsEmpty = fixedProtobuf.license && !Object.keys(fixedProtobuf.license).length
+  if (licenseIsEmpty) { // license deletion was requested - ignore it and consider it empty
+    delete fixedProtobuf.license
+  }
 
   // create new video
   const video = new Video({
@@ -260,7 +265,7 @@ export async function content_VideoUpdated(
     )
 
     // prepare video media metadata (if any)
-    const fixedProtobuf = integrateVideoMediaMetadata(video, protobufContent, event)
+    const fixedProtobuf = await integrateVideoMediaMetadata(db, video, protobufContent, event)
 
     // remember original license
     const originalLicense = video.license
@@ -272,17 +277,9 @@ export async function content_VideoUpdated(
 
     // license has changed - plan old license delete
     if (originalLicense && video.license != originalLicense) {
-      video.license = video.license
-        ? new License({
-          ...originalLicense,
-          ...video.license,
-          createdAt: originalLicense.createdAt, // keep original createdAt time
-        }) // update existing license
-        : undefined // unset license
-
-      if (!video.license) { // delete old license when requested
-        licenseToDelete = originalLicense
-      }
+      ([video.license, licenseToDelete] = handleLicenseUpdate(originalLicense, video.license))
+    } else if (!Object.keys(video.license || {}).length) { // license deletion was requested event no license exists?
+      delete video.license // ensure license is empty
     }
   }
 
@@ -429,11 +426,12 @@ export async function content_FeaturedVideosSet(
   NOTE: type hack - `RawVideoMetadata` is accepted for `metadata` instead of `Partial<Video>`
         see `prepareVideoMetadata()` in `utils.ts` for more info
 */
-function integrateVideoMediaMetadata(
+async function integrateVideoMediaMetadata(
+  db: DatabaseManager,
   existingRecord: Video | null,
   metadata: Partial<Video>,
   event: SubstrateEvent,
-): Partial<Video> {
+): Promise<Partial<Video>> {
   if (!metadata.mediaMetadata) {
     return metadata
   }
@@ -479,10 +477,10 @@ function integrateVideoMediaMetadata(
 
   // ensure predictable ids
   if (!mediaMetadata.encoding.id) {
-    mediaMetadata.encoding.id = createPredictableId(event, mediaMetadata.encoding)
+    mediaMetadata.encoding.id = await createPredictableId(db)
   }
   if (!mediaMetadata.id) {
-    mediaMetadata.id = createPredictableId(event, mediaMetadata)
+    mediaMetadata.id = await createPredictableId(db)
   }
 
   /////////////////// update updatedAt if needed ///////////////////////////////
@@ -510,3 +508,30 @@ function integrateVideoMediaMetadata(
     mediaMetadata
   }
 }
+
+// returns tuple `[newLicenseForVideo, oldLicenseToBeDeleted]`
+function handleLicenseUpdate(originalLicense, newLicense): [License | undefined, License | null] {
+  const isNewEmpty = !Object.keys(newLicense).length
+
+  if (!originalLicense && isNewEmpty) {
+    return [undefined, null]
+  }
+
+  if (!originalLicense) { // && !isNewEmpty
+    return [newLicense, null]
+  }
+
+  if (!isNewEmpty) { // && originalLicense
+    return [
+      new License({
+        ...originalLicense,
+        ...newLicense,
+      }),
+      null
+    ]
+  }
+
+  // originalLicense && isNewEmpty
+
+  return [originalLicense, null]
+}

+ 82 - 7
query-node/mappings/src/membership.ts

@@ -9,6 +9,8 @@ import { FindConditions } from 'typeorm'
 import {
   inconsistentState,
   logger,
+  extractExtrinsicArgs,
+  extractSudoCallParameters,
 } from './common'
 import { Members } from '../../generated/types'
 import { MembershipEntryMethod, Membership } from 'query-node'
@@ -18,7 +20,15 @@ import { EntryMethod } from '@joystream/types/augment'
 export async function members_MemberRegistered(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
   const { accountId, memberId, entryMethod } = new Members.MemberRegisteredEvent(event).data
-  const { avatarUri, about, handle } = new Members.BuyMembershipCall(event).args
+  const { avatarUri, about, handle } = extractExtrinsicArgs(
+    event,
+    Members.BuyMembershipCall,
+    {
+      handle: 1,
+      avatarUri: 2,
+      about: 3,
+    },
+  )
 
   // create new membership
   const member = new Membership({
@@ -47,7 +57,11 @@ export async function members_MemberRegistered(db: DatabaseManager, event: Subst
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberUpdatedAboutText(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { text, memberId } = new Members.ChangeMemberAboutTextCall(event).args
+  const { text, memberId } = isUpdateMembershipExtrinsic(event)
+    ? unpackUpdateMembershipOptions(
+        extractExtrinsicArgs(event, Members.UpdateMembershipCall, {memberId: 0, about: 3})
+      )
+    : extractExtrinsicArgs(event, Members.ChangeMemberAboutTextCall, {memberId: 0, text: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -73,7 +87,11 @@ export async function members_MemberUpdatedAboutText(db: DatabaseManager, event:
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberUpdatedAvatar(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { uri, memberId } = new Members.ChangeMemberAvatarCall(event).args
+  const { uri, memberId } = isUpdateMembershipExtrinsic(event)
+    ? unpackUpdateMembershipOptions(
+        extractExtrinsicArgs(event, Members.UpdateMembershipCall, {memberId: 0, avatarUri: 2})
+      )
+    : extractExtrinsicArgs(event, Members.ChangeMemberAvatarCall, {memberId: 0, uri: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -99,7 +117,11 @@ export async function members_MemberUpdatedAvatar(db: DatabaseManager, event: Su
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberUpdatedHandle(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { handle, memberId } = new Members.ChangeMemberHandleCall(event).args
+  const { handle, memberId } = isUpdateMembershipExtrinsic(event)
+    ? unpackUpdateMembershipOptions(
+        extractExtrinsicArgs(event, Members.UpdateMembershipCall, {memberId: 0, handle: 1})
+      )
+    : extractExtrinsicArgs(event, Members.ChangeMemberHandleCall, {memberId: 0, handle: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -125,7 +147,7 @@ export async function members_MemberUpdatedHandle(db: DatabaseManager, event: Su
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberSetRootAccount(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { newRootAccount, memberId } = new Members.SetRootAccountCall(event).args
+  const { newRootAccount, memberId } = extractExtrinsicArgs(event, Members.SetRootAccountCall, {memberId: 0, newRootAccount: 1})
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -151,7 +173,11 @@ export async function members_MemberSetRootAccount(db: DatabaseManager, event: S
 // eslint-disable-next-line @typescript-eslint/naming-convention
 export async function members_MemberSetControllerAccount(db: DatabaseManager, event: SubstrateEvent): Promise<void> {
   // read event data
-  const { newControllerAccount, memberId } = new Members.SetControllerAccountCall(event).args
+  const { newControllerAccount, memberId } = extractExtrinsicArgs(
+    event,
+    Members.SetControllerAccountCall,
+    {memberId: 0, newControllerAccount: 1},
+  )
 
   // load member
   const member = await db.get(Membership, { where: { id: memberId.toString() } as FindConditions<Membership> })
@@ -184,7 +210,14 @@ function convertBytesToString(b: Bytes | null): string {
     return ''
   }
 
-  return Buffer.from(b.toU8a(true)).toString()
+  const result = Buffer.from(b.toU8a(true)).toString()
+
+  // prevent utf-8 null character
+  if (result.match(/^\0$/)) {
+    return ''
+  }
+
+  return result
 }
 
 function convertEntryMethod(entryMethod: EntryMethod): MembershipEntryMethod {
@@ -207,3 +240,45 @@ function convertEntryMethod(entryMethod: EntryMethod): MembershipEntryMethod {
   logger.error('Not implemented entry method', {entryMethod: entryMethod.toString()})
   throw 'Not implemented entry method'
 }
+
+/*
+  Returns true if event is emitted inside of `update_membership` extrinsic.
+*/
+function isUpdateMembershipExtrinsic(event: SubstrateEvent): boolean {
+  if (!event.extrinsic) { // this should never happen
+    return false
+  }
+
+  if (event.extrinsic.method == 'updateMembership') {
+    return true
+  }
+
+  // no sudo was used to update membership -> this is not updateMembership
+  if (event.extrinsic.section != 'sudo') {
+    return false
+  }
+
+  const sudoCallParameters = extractSudoCallParameters<unknown[]>(event)
+
+  // very trivial check if update_membership extrinsic was used
+  return sudoCallParameters.args.length == 4 // memberId, handle, avatarUri, about
+}
+
+interface IUnpackedUpdateMembershipOptions {
+  memberId: MemberId
+  handle: Bytes
+  uri: Bytes
+  text: Bytes
+}
+
+/*
+  Returns unwrapped data + unite naming of uri/avatarUri and about/text
+*/
+function unpackUpdateMembershipOptions(args: Members.UpdateMembershipCall['args']): IUnpackedUpdateMembershipOptions {
+  return {
+    memberId: args.memberId,
+    handle: args.handle.unwrapOrDefault(),
+    uri: args.avatarUri.unwrapOrDefault(),
+    text: args.about.unwrapOrDefault(),
+  }
+}

+ 1 - 1
query-node/mappings/src/storage.ts

@@ -45,7 +45,7 @@ export async function dataDirectory_ContentAdded(db: DatabaseManager, event: Sub
   // save all content objects
   for (let parameters of contentParameters) {
     const owner = convertStorageObjectOwner(storageObjectOwner)
-    const dataObject = await prepareDataObject(parameters, event, owner)
+    const dataObject = await prepareDataObject(db, parameters, event, owner)
 
     // fill in auto-generated fields
     dataObject.createdAt = new Date(fixBlockTimestamp(event.blockTimestamp).toNumber())

+ 1 - 1
query-node/mappings/src/workingGroup.ts

@@ -183,7 +183,7 @@ async function createWorker(
 ): Promise<void> {
   // create entity
   const newWorker = new Worker({
-    id: createPredictableId(event, workerType),
+    id: await createPredictableId(db),
     workerId: workerId.toString(),
     type: workerType,
     isActive: true,

+ 8 - 0
query-node/schema.graphql

@@ -400,3 +400,11 @@ type Worker @entity {
 
   dataObjects: [DataObject!]! @derivedFrom(field: "liaison")
 }
+
+type NextEntityId @entity {
+  "Unique identifier"
+  id: ID!
+
+  "Next deterministic id for entities without custom id"
+  nextId: Int!
+}