Browse Source

Set category moderators

Leszek Wiesner 3 years ago
parent
commit
4b4af896a2

+ 23 - 1
query-node/mappings/common.ts

@@ -1,7 +1,9 @@
-import { SubstrateEvent } from '@dzlzv/hydra-common'
+import { DatabaseManager, SubstrateEvent } from '@dzlzv/hydra-common'
 import { Network } from 'query-node/dist/src/modules/enums/enums'
 import { Event } from 'query-node/dist/src/modules/event/event.model'
 import { Bytes } from '@polkadot/types'
+import { WorkerId } from '@joystream/types/augment/all'
+import { Worker } from 'query-node/dist/model'
 
 export const CURRENT_NETWORK = Network.OLYMPIA
 
@@ -61,3 +63,23 @@ export function hasValuesForProperties<
   })
   return true
 }
+
+export type WorkingGroupModuleName =
+  | 'storageWorkingGroup'
+  | 'contentDirectoryWorkingGroup'
+  | 'forumWorkingGroup'
+  | 'membershipWorkingGroup'
+
+export async function getWorker(
+  db: DatabaseManager,
+  groupName: WorkingGroupModuleName,
+  runtimeId: WorkerId | number
+): Promise<Worker> {
+  const workerDbId = `${groupName}-${runtimeId}`
+  const worker = await db.get(Worker, { where: { id: workerDbId } })
+  if (!worker) {
+    throw new Error(`Worker not found by id ${workerDbId}`)
+  }
+
+  return worker
+}

+ 63 - 11
query-node/mappings/forum.ts

@@ -2,7 +2,7 @@
 eslint-disable @typescript-eslint/naming-convention
 */
 import { SubstrateEvent, DatabaseManager } from '@dzlzv/hydra-common'
-import { bytesToString, deserializeMetadata, genericEventFields } from './common'
+import { bytesToString, deserializeMetadata, genericEventFields, getWorker } from './common'
 import {
   CategoryCreatedEvent,
   CategoryStatusActive,
@@ -33,14 +33,17 @@ import {
   PostStatusLocked,
   PostOriginThreadReply,
   CategoryStickyThreadUpdateEvent,
+  CategoryMembershipOfModeratorUpdatedEvent,
+  PostModeratedEvent,
+  PostStatusModerated,
 } from 'query-node/dist/model'
 import { Forum } from './generated/types'
 import { PrivilegedActor } from '@joystream/types/augment/all'
 import { ForumPostMetadata } from '@joystream/metadata-protobuf'
 import { Not } from 'typeorm'
 
-async function getCategory(db: DatabaseManager, categoryId: string): Promise<ForumCategory> {
-  const category = await db.get(ForumCategory, { where: { id: categoryId } })
+async function getCategory(db: DatabaseManager, categoryId: string, relations?: string[]): Promise<ForumCategory> {
+  const category = await db.get(ForumCategory, { where: { id: categoryId }, relations })
   if (!category) {
     throw new Error(`Forum category not found by id: ${categoryId}`)
   }
@@ -57,6 +60,15 @@ async function getThread(db: DatabaseManager, threadId: string): Promise<ForumTh
   return thread
 }
 
+async function getPost(db: DatabaseManager, postId: string): Promise<ForumPost> {
+  const post = await db.get(ForumPost, { where: { id: postId } })
+  if (!post) {
+    throw new Error(`Forum post not found by id: ${postId.toString()}`)
+  }
+
+  return post
+}
+
 async function getPollAlternative(db: DatabaseManager, threadId: string, index: number) {
   const poll = await db.get(ForumPoll, { where: { thread: { id: threadId } }, relations: ['pollAlternatives'] })
   if (!poll) {
@@ -391,8 +403,55 @@ export async function forum_CategoryStickyThreadUpdate(db: DatabaseManager, even
   await db.save<CategoryStickyThreadUpdateEvent>(categoryStickyThreadUpdateEvent)
 }
 
+export async function forum_CategoryMembershipOfModeratorUpdated(
+  db: DatabaseManager,
+  event_: SubstrateEvent
+): Promise<void> {
+  const [moderatorId, categoryId, canModerate] = new Forum.CategoryMembershipOfModeratorUpdatedEvent(event_).params
+  const eventTime = new Date(event_.blockTimestamp)
+  const moderator = await getWorker(db, 'forumWorkingGroup', moderatorId.toNumber())
+  const category = await getCategory(db, categoryId.toString(), ['moderators'])
+
+  if (canModerate.valueOf()) {
+    category.moderators.push(moderator)
+    category.updatedAt = eventTime
+    await db.save<ForumCategory>(category)
+  } else {
+    category.moderators.splice(category.moderators.map((m) => m.id).indexOf(moderator.id), 1)
+    category.updatedAt = eventTime
+    await db.save<ForumCategory>(category)
+  }
+
+  const categoryMembershipOfModeratorUpdatedEvent = new CategoryMembershipOfModeratorUpdatedEvent({
+    ...genericEventFields(event_),
+    category,
+    moderator,
+    newCanModerateValue: canModerate.valueOf(),
+  })
+  await db.save<CategoryMembershipOfModeratorUpdatedEvent>(categoryMembershipOfModeratorUpdatedEvent)
+}
+
 export async function forum_PostModerated(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-  // TODO
+  const [postId, rationaleBytes, privilegedActor] = new Forum.PostModeratedEvent(event_).params
+  const eventTime = new Date(event_.blockTimestamp)
+  const actorWorker = await getActorWorker(db, privilegedActor)
+  const post = await getPost(db, postId.toString())
+
+  const postModeratedEvent = new PostModeratedEvent({
+    ...genericEventFields(event_),
+    actor: actorWorker,
+    post,
+    rationale: bytesToString(rationaleBytes),
+  })
+
+  await db.save<PostModeratedEvent>(postModeratedEvent)
+
+  const newStatus = new PostStatusModerated()
+  newStatus.postModeratedEventId = postModeratedEvent.id
+
+  post.updatedAt = eventTime
+  post.status = newStatus
+  await db.save<ForumPost>(post)
 }
 
 export async function forum_PostDeleted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
@@ -406,10 +465,3 @@ export async function forum_PostTextUpdated(db: DatabaseManager, event_: Substra
 export async function forum_PostReacted(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   // TODO
 }
-
-export async function forum_CategoryMembershipOfModeratorUpdated(
-  db: DatabaseManager,
-  event_: SubstrateEvent
-): Promise<void> {
-  // TODO
-}

+ 12 - 22
query-node/mappings/workingGroups.ts

@@ -15,7 +15,7 @@ import {
   WorkingGroupMetadataAction,
 } from '@joystream/metadata-protobuf'
 import { Bytes } from '@polkadot/types'
-import { deserializeMetadata, bytesToString, genericEventFields } from './common'
+import { deserializeMetadata, bytesToString, genericEventFields, getWorker, WorkingGroupModuleName } from './common'
 import BN from 'bn.js'
 import {
   WorkingGroupOpening,
@@ -70,7 +70,6 @@ import {
   BudgetSetEvent,
   BudgetSpendingEvent,
   LeaderSetEvent,
-  Event,
 } from 'query-node/dist/model'
 import { createType } from '@joystream/types'
 
@@ -111,15 +110,6 @@ async function getApplication(db: DatabaseManager, applicationDbId: string): Pro
   return application
 }
 
-async function getWorker(db: DatabaseManager, workerDbId: string): Promise<Worker> {
-  const worker = await db.get(Worker, { where: { id: workerDbId } })
-  if (!worker) {
-    throw new Error(`Worker not found by id ${workerDbId}`)
-  }
-
-  return worker
-}
-
 async function getApplicationFormQuestions(
   db: DatabaseManager,
   openingDbId: string
@@ -338,7 +328,7 @@ async function handleWorkingGroupMetadataAction(
 async function handleTerminatedWorker(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const [workerId, optPenalty, optRationale] = new WorkingGroups.TerminatedWorkerEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const EventConstructor = worker.isLead ? TerminatedLeaderEvent : TerminatedWorkerEvent
@@ -672,7 +662,7 @@ export async function workingGroups_WorkerRoleAccountUpdated(
 ): Promise<void> {
   const [workerId, accountId] = new WorkingGroups.WorkerRoleAccountUpdatedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const workerRoleAccountUpdatedEvent = new WorkerRoleAccountUpdatedEvent({
@@ -696,7 +686,7 @@ export async function workingGroups_WorkerRewardAccountUpdated(
 ): Promise<void> {
   const [workerId, accountId] = new WorkingGroups.WorkerRewardAccountUpdatedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const workerRewardAccountUpdatedEvent = new WorkerRewardAccountUpdatedEvent({
@@ -717,7 +707,7 @@ export async function workingGroups_WorkerRewardAccountUpdated(
 export async function workingGroups_StakeIncreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const [workerId, increaseAmount] = new WorkingGroups.StakeIncreasedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const stakeIncreasedEvent = new StakeIncreasedEvent({
@@ -738,7 +728,7 @@ export async function workingGroups_StakeIncreased(db: DatabaseManager, event_:
 export async function workingGroups_RewardPaid(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const [workerId, rewardAccountId, amount, rewardPaymentType] = new WorkingGroups.RewardPaidEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const rewardPaidEvent = new RewardPaidEvent({
@@ -765,7 +755,7 @@ export async function workingGroups_NewMissedRewardLevelReached(
 ): Promise<void> {
   const [workerId, newMissedRewardAmountOpt] = new WorkingGroups.NewMissedRewardLevelReachedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const newMissedRewardLevelReachedEvent = new NewMissedRewardLevelReachedEvent({
@@ -787,7 +777,7 @@ export async function workingGroups_NewMissedRewardLevelReached(
 export async function workingGroups_WorkerExited(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const [workerId] = new WorkingGroups.WorkerExitedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const workerExitedEvent = new WorkerExitedEvent({
@@ -836,7 +826,7 @@ export async function workingGroups_WorkerRewardAmountUpdated(
 ): Promise<void> {
   const [workerId, newRewardPerBlockOpt] = new WorkingGroups.WorkerRewardAmountUpdatedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const workerRewardAmountUpdatedEvent = new WorkerRewardAmountUpdatedEvent({
@@ -857,7 +847,7 @@ export async function workingGroups_WorkerRewardAmountUpdated(
 export async function workingGroups_StakeSlashed(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const [workerId, slashedAmount, requestedAmount, optRationale] = new WorkingGroups.StakeSlashedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const workerStakeSlashedEvent = new StakeSlashedEvent({
@@ -880,7 +870,7 @@ export async function workingGroups_StakeSlashed(db: DatabaseManager, event_: Su
 export async function workingGroups_StakeDecreased(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const [workerId, amount] = new WorkingGroups.StakeDecreasedEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const workerStakeDecreasedEvent = new StakeDecreasedEvent({
@@ -901,7 +891,7 @@ export async function workingGroups_StakeDecreased(db: DatabaseManager, event_:
 export async function workingGroups_WorkerStartedLeaving(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   const [workerId, optRationale] = new WorkingGroups.WorkerStartedLeavingEvent(event_).params
   const group = await getWorkingGroup(db, event_)
-  const worker = await getWorker(db, `${group.name}-${workerId.toString()}`)
+  const worker = await getWorker(db, group.name as WorkingGroupModuleName, workerId)
   const eventTime = new Date(event_.blockTimestamp)
 
   const workerStartedLeavingEvent = new WorkerStartedLeavingEvent({

+ 1 - 1
query-node/schemas/forumEvents.graphql

@@ -449,7 +449,7 @@ type CategoryStickyThreadUpdateEvent @entity {
   actor: Worker!
 }
 
-type CategoryMembershipOfModeratorUpdated @entity {
+type CategoryMembershipOfModeratorUpdatedEvent @entity {
   ### GENERIC DATA ###
 
   "(network}-{blockNumber}-{indexInBlock}"

+ 18 - 0
tests/integration-tests/src/QueryNodeApi.ts

@@ -227,6 +227,10 @@ import {
   GetCategoryStickyThreadUpdateEventsByEventIdsQuery,
   GetCategoryStickyThreadUpdateEventsByEventIdsQueryVariables,
   GetCategoryStickyThreadUpdateEventsByEventIds,
+  CategoryMembershipOfModeratorUpdatedEventFieldsFragment,
+  GetCategoryMembershipOfModeratorUpdatedEventsByEventIdsQuery,
+  GetCategoryMembershipOfModeratorUpdatedEventsByEventIdsQueryVariables,
+  GetCategoryMembershipOfModeratorUpdatedEventsByEventIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
@@ -839,4 +843,18 @@ export class QueryNodeApi {
       GetCategoryStickyThreadUpdateEventsByEventIdsQueryVariables
     >(GetCategoryStickyThreadUpdateEventsByEventIds, { eventIds }, 'categoryStickyThreadUpdateEvents')
   }
+
+  public async getCategoryMembershipOfModeratorUpdatedEvents(
+    events: EventDetails[]
+  ): Promise<CategoryMembershipOfModeratorUpdatedEventFieldsFragment[]> {
+    const eventIds = events.map((e) => this.getQueryNodeEventId(e.blockNumber, e.indexInBlock))
+    return this.multipleEntitiesQuery<
+      GetCategoryMembershipOfModeratorUpdatedEventsByEventIdsQuery,
+      GetCategoryMembershipOfModeratorUpdatedEventsByEventIdsQueryVariables
+    >(
+      GetCategoryMembershipOfModeratorUpdatedEventsByEventIds,
+      { eventIds },
+      'categoryMembershipOfModeratorUpdatedEvents'
+    )
+  }
 }

+ 90 - 0
tests/integration-tests/src/fixtures/forum/UpdateCategoryModeratorsFixture.ts

@@ -0,0 +1,90 @@
+import { Api } from '../../Api'
+import { QueryNodeApi } from '../../QueryNodeApi'
+import { EventDetails } from '../../types'
+import { SubmittableExtrinsic } from '@polkadot/api/types'
+import { Utils } from '../../utils'
+import { ISubmittableResult } from '@polkadot/types/types/'
+import {
+  CategoryMembershipOfModeratorUpdatedEventFieldsFragment,
+  ForumCategoryFieldsFragment,
+} from '../../graphql/generated/queries'
+import { assert } from 'chai'
+import { CategoryId } from '@joystream/types/forum'
+import { WorkerId } from '@joystream/types/working-group'
+import { WithForumWorkersFixture } from './WithForumWorkersFixture'
+import _ from 'lodash'
+
+export type CategoryModeratorStatusUpdate = {
+  categoryId: CategoryId
+  moderatorId: WorkerId
+  canModerate: boolean
+}
+
+export class UpdateCategoryModeratorsFixture extends WithForumWorkersFixture {
+  protected events: EventDetails[] = []
+
+  protected updates: CategoryModeratorStatusUpdate[]
+
+  public constructor(api: Api, query: QueryNodeApi, updates: CategoryModeratorStatusUpdate[]) {
+    super(api, query)
+    this.updates = updates
+  }
+
+  protected async getSignerAccountOrAccounts(): Promise<string> {
+    return await this.api.getLeadRoleKey('forumWorkingGroup')
+  }
+
+  protected async getExtrinsics(): Promise<SubmittableExtrinsic<'promise'>[]> {
+    return this.updates.map((u) =>
+      this.api.tx.forum.updateCategoryMembershipOfModerator(u.moderatorId, u.categoryId, u.canModerate)
+    )
+  }
+
+  protected async getEventFromResult(result: ISubmittableResult): Promise<EventDetails> {
+    return this.api.retrieveForumEventDetails(result, 'CategoryMembershipOfModeratorUpdated')
+  }
+
+  protected assertQueriedCategoriesAreValid(qCategories: ForumCategoryFieldsFragment[]): void {
+    for (const [categoryId, updates] of _.entries(_.groupBy(this.updates, (u) => u.categoryId.toString()))) {
+      const finalUpdates = _.uniqBy([...updates.reverse()], (u) => u.moderatorId)
+      const qCategory = qCategories.find((c) => c.id === categoryId.toString())
+      Utils.assert(qCategory, 'Query node: Category not found!')
+      finalUpdates.forEach((finalUpdate) => {
+        if (finalUpdate.canModerate) {
+          assert.include(
+            qCategory.moderators.map((m) => m.id),
+            `forumWorkingGroup-${finalUpdate.moderatorId.toString()}`
+          )
+        } else {
+          assert.notInclude(
+            qCategory.moderators.map((m) => m.id),
+            `forumWorkingGroup-${finalUpdate.moderatorId.toString()}`
+          )
+        }
+      })
+    }
+  }
+
+  protected assertQueryNodeEventIsValid(
+    qEvent: CategoryMembershipOfModeratorUpdatedEventFieldsFragment,
+    i: number
+  ): void {
+    const { categoryId, moderatorId, canModerate } = this.updates[i]
+    assert.equal(qEvent.category.id, categoryId.toString())
+    assert.equal(qEvent.moderator.id, `forumWorkingGroup-${moderatorId.toString()}`)
+    assert.equal(qEvent.newCanModerateValue, canModerate)
+  }
+
+  async runQueryNodeChecks(): Promise<void> {
+    await super.runQueryNodeChecks()
+    // Query the events
+    await this.query.tryQueryWithTimeout(
+      () => this.query.getCategoryMembershipOfModeratorUpdatedEvents(this.events),
+      (qEvents) => this.assertQueryNodeEventsAreValid(qEvents)
+    )
+
+    // Query the categories
+    const qCategories = await this.query.getCategoriesByIds(this.updates.map((u) => u.categoryId))
+    this.assertQueriedCategoriesAreValid(qCategories)
+  }
+}

+ 1 - 0
tests/integration-tests/src/fixtures/forum/index.ts

@@ -8,3 +8,4 @@ export { AddPostsFixture, PostParams } from './AddPostsFixture'
 export { UpdateThreadTitlesFixture, ThreadTitleUpdate } from './UpdateThreadTitlesFixture'
 export { MoveThreadsFixture, MoveThreadParams } from './MoveThreadsFixture'
 export { SetStickyThreadsFixture, StickyThreadsParams } from './SetStickyThreadsFixture'
+export { UpdateCategoryModeratorsFixture, CategoryModeratorStatusUpdate } from './UpdateCategoryModeratorsFixture'

+ 31 - 10
tests/integration-tests/src/flows/forum/categories.ts

@@ -7,7 +7,10 @@ import {
   CategoryStatusUpdate,
   UpdateCategoriesStatusFixture,
   RemoveCategoriesFixture,
+  CategoryModeratorStatusUpdate,
+  UpdateCategoryModeratorsFixture,
 } from '../../fixtures/forum'
+import { HireWorkersFixture } from '../../fixtures/workingGroups/HireWorkersFixture'
 
 export default async function categories({ api, query }: FlowProps): Promise<void> {
   const debug = Debugger(`flow:cateogries`)
@@ -51,19 +54,37 @@ export default async function categories({ api, query }: FlowProps): Promise<voi
 
   await Promise.all([createCategoriesRunner.runQueryNodeChecks(), createSubcategoriesRunner.runQueryNodeChecks()])
 
-  // Update archival status
-  const categoryUpdatesArchival: CategoryStatusUpdate[] = subcategoryIds.map((id) => ({
-    categoryId: id,
-    archived: true,
-  }))
+  // Create moderators and perform status updates
+  const createModeratorsFixture = new HireWorkersFixture(api, query, 'forumWorkingGroup', subcategoryIds.length + 1)
+  await new FixtureRunner(createModeratorsFixture).run()
+  const moderatorIds = createModeratorsFixture.getCreatedWorkerIds()
+
+  const moderatorUpdates: CategoryModeratorStatusUpdate[] = subcategoryIds.reduce(
+    (updates, categoryId, i) =>
+      updates.concat([
+        { categoryId, moderatorId: moderatorIds[i], canModerate: true },
+        { categoryId, moderatorId: moderatorIds[i + 1], canModerate: true },
+        { categoryId, moderatorId: moderatorIds[i + 1], canModerate: false },
+      ]),
+    [] as CategoryModeratorStatusUpdate[]
+  )
+  const updateCategoryModeratorsFixture = new UpdateCategoryModeratorsFixture(api, query, moderatorUpdates)
+  const updateCategoryModeratorsRunner = new FixtureRunner(updateCategoryModeratorsFixture)
+  await updateCategoryModeratorsRunner.run()
 
-  const categoryUpdatesArchivalFixture = new UpdateCategoriesStatusFixture(api, query, categoryUpdatesArchival)
-  await new FixtureRunner(categoryUpdatesArchivalFixture).runWithQueryNodeChecks()
+  // Update archival status
+  const categoryUpdates: CategoryStatusUpdate[] = [
+    { categoryId: subcategoryIds[0], archived: true },
+    { categoryId: subcategoryIds[1], archived: true },
+    { categoryId: subcategoryIds[1], archived: false },
+  ]
 
-  const categoryUpdatesActive: CategoryStatusUpdate[] = categoryUpdatesArchival.map((u) => ({ ...u, archived: false }))
+  const categoryUpdatesFixture = new UpdateCategoriesStatusFixture(api, query, categoryUpdates)
+  const categoryUpdatesRunner = new FixtureRunner(categoryUpdatesFixture)
+  await categoryUpdatesRunner.run()
 
-  const categoryUpdatesActiveFixture = new UpdateCategoriesStatusFixture(api, query, categoryUpdatesActive)
-  await new FixtureRunner(categoryUpdatesActiveFixture).runWithQueryNodeChecks()
+  // Run compound query node checks
+  await Promise.all([updateCategoryModeratorsFixture.runQueryNodeChecks(), categoryUpdatesRunner.runQueryNodeChecks()])
 
   // Remove categories (make sure subcategories are removed first)
   const removeSubcategoriesFixture = new RemoveCategoriesFixture(

+ 45 - 0
tests/integration-tests/src/graphql/generated/queries.ts

@@ -261,6 +261,26 @@ export type GetCategoryStickyThreadUpdateEventsByEventIdsQuery = {
   categoryStickyThreadUpdateEvents: Array<CategoryStickyThreadUpdateEventFieldsFragment>
 }
 
+export type CategoryMembershipOfModeratorUpdatedEventFieldsFragment = {
+  id: string
+  createdAt: any
+  inBlock: number
+  network: Types.Network
+  inExtrinsic?: Types.Maybe<string>
+  indexInBlock: number
+  newCanModerateValue: boolean
+  category: { id: string }
+  moderator: { id: string }
+}
+
+export type GetCategoryMembershipOfModeratorUpdatedEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetCategoryMembershipOfModeratorUpdatedEventsByEventIdsQuery = {
+  categoryMembershipOfModeratorUpdatedEvents: Array<CategoryMembershipOfModeratorUpdatedEventFieldsFragment>
+}
+
 export type MemberMetadataFieldsFragment = { name?: Types.Maybe<string>; about?: Types.Maybe<string> }
 
 export type MembershipFieldsFragment = {
@@ -1420,6 +1440,23 @@ export const CategoryStickyThreadUpdateEventFields = gql`
     }
   }
 `
+export const CategoryMembershipOfModeratorUpdatedEventFields = gql`
+  fragment CategoryMembershipOfModeratorUpdatedEventFields on CategoryMembershipOfModeratorUpdatedEvent {
+    id
+    createdAt
+    inBlock
+    network
+    inExtrinsic
+    indexInBlock
+    category {
+      id
+    }
+    moderator {
+      id
+    }
+    newCanModerateValue
+  }
+`
 export const MemberMetadataFields = gql`
   fragment MemberMetadataFields on MemberMetadata {
     name
@@ -2323,6 +2360,14 @@ export const GetCategoryStickyThreadUpdateEventsByEventIds = gql`
   }
   ${CategoryStickyThreadUpdateEventFields}
 `
+export const GetCategoryMembershipOfModeratorUpdatedEventsByEventIds = gql`
+  query getCategoryMembershipOfModeratorUpdatedEventsByEventIds($eventIds: [ID!]) {
+    categoryMembershipOfModeratorUpdatedEvents(where: { id_in: $eventIds }) {
+      ...CategoryMembershipOfModeratorUpdatedEventFields
+    }
+  }
+  ${CategoryMembershipOfModeratorUpdatedEventFields}
+`
 export const GetMemberById = gql`
   query getMemberById($id: ID!) {
     membershipByUniqueInput(where: { id: $id }) {

+ 31 - 31
tests/integration-tests/src/graphql/generated/schema.ts

@@ -1177,7 +1177,7 @@ export type CategoryDeletedEventWhereUniqueInput = {
   id: Scalars['ID']
 }
 
-export type CategoryMembershipOfModeratorUpdated = BaseGraphQlObject & {
+export type CategoryMembershipOfModeratorUpdatedEvent = BaseGraphQlObject & {
   id: Scalars['ID']
   createdAt: Scalars['DateTime']
   createdById: Scalars['String']
@@ -1202,13 +1202,13 @@ export type CategoryMembershipOfModeratorUpdated = BaseGraphQlObject & {
   newCanModerateValue: Scalars['Boolean']
 }
 
-export type CategoryMembershipOfModeratorUpdatedConnection = {
+export type CategoryMembershipOfModeratorUpdatedEventConnection = {
   totalCount: Scalars['Int']
-  edges: Array<CategoryMembershipOfModeratorUpdatedEdge>
+  edges: Array<CategoryMembershipOfModeratorUpdatedEventEdge>
   pageInfo: PageInfo
 }
 
-export type CategoryMembershipOfModeratorUpdatedCreateInput = {
+export type CategoryMembershipOfModeratorUpdatedEventCreateInput = {
   inExtrinsic?: Maybe<Scalars['String']>
   inBlock: Scalars['Float']
   network: Network
@@ -1218,12 +1218,12 @@ export type CategoryMembershipOfModeratorUpdatedCreateInput = {
   newCanModerateValue: Scalars['Boolean']
 }
 
-export type CategoryMembershipOfModeratorUpdatedEdge = {
-  node: CategoryMembershipOfModeratorUpdated
+export type CategoryMembershipOfModeratorUpdatedEventEdge = {
+  node: CategoryMembershipOfModeratorUpdatedEvent
   cursor: Scalars['String']
 }
 
-export enum CategoryMembershipOfModeratorUpdatedOrderByInput {
+export enum CategoryMembershipOfModeratorUpdatedEventOrderByInput {
   CreatedAtAsc = 'createdAt_ASC',
   CreatedAtDesc = 'createdAt_DESC',
   UpdatedAtAsc = 'updatedAt_ASC',
@@ -1246,7 +1246,7 @@ export enum CategoryMembershipOfModeratorUpdatedOrderByInput {
   NewCanModerateValueDesc = 'newCanModerateValue_DESC',
 }
 
-export type CategoryMembershipOfModeratorUpdatedUpdateInput = {
+export type CategoryMembershipOfModeratorUpdatedEventUpdateInput = {
   inExtrinsic?: Maybe<Scalars['String']>
   inBlock?: Maybe<Scalars['Float']>
   network?: Maybe<Network>
@@ -1256,7 +1256,7 @@ export type CategoryMembershipOfModeratorUpdatedUpdateInput = {
   newCanModerateValue?: Maybe<Scalars['Boolean']>
 }
 
-export type CategoryMembershipOfModeratorUpdatedWhereInput = {
+export type CategoryMembershipOfModeratorUpdatedEventWhereInput = {
   id_eq?: Maybe<Scalars['ID']>
   id_in?: Maybe<Array<Scalars['ID']>>
   createdAt_eq?: Maybe<Scalars['DateTime']>
@@ -1308,11 +1308,11 @@ export type CategoryMembershipOfModeratorUpdatedWhereInput = {
   newCanModerateValue_in?: Maybe<Array<Scalars['Boolean']>>
   moderator?: Maybe<WorkerWhereInput>
   category?: Maybe<ForumCategoryWhereInput>
-  AND?: Maybe<Array<CategoryMembershipOfModeratorUpdatedWhereInput>>
-  OR?: Maybe<Array<CategoryMembershipOfModeratorUpdatedWhereInput>>
+  AND?: Maybe<Array<CategoryMembershipOfModeratorUpdatedEventWhereInput>>
+  OR?: Maybe<Array<CategoryMembershipOfModeratorUpdatedEventWhereInput>>
 }
 
-export type CategoryMembershipOfModeratorUpdatedWhereUniqueInput = {
+export type CategoryMembershipOfModeratorUpdatedEventWhereUniqueInput = {
   id: Scalars['ID']
 }
 
@@ -2217,7 +2217,7 @@ export type ForumCategory = BaseGraphQlObject & {
   /** Current category status */
   status: CategoryStatus
   categorydeletedeventcategory?: Maybe<Array<CategoryDeletedEvent>>
-  categorymembershipofmoderatorupdatedcategory?: Maybe<Array<CategoryMembershipOfModeratorUpdated>>
+  categorymembershipofmoderatorupdatedeventcategory?: Maybe<Array<CategoryMembershipOfModeratorUpdatedEvent>>
   categorystickythreadupdateeventcategory?: Maybe<Array<CategoryStickyThreadUpdateEvent>>
   categoryupdatedeventcategory?: Maybe<Array<CategoryUpdatedEvent>>
   forumcategoryparent?: Maybe<Array<ForumCategory>>
@@ -2314,9 +2314,9 @@ export type ForumCategoryWhereInput = {
   categorydeletedeventcategory_none?: Maybe<CategoryDeletedEventWhereInput>
   categorydeletedeventcategory_some?: Maybe<CategoryDeletedEventWhereInput>
   categorydeletedeventcategory_every?: Maybe<CategoryDeletedEventWhereInput>
-  categorymembershipofmoderatorupdatedcategory_none?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
-  categorymembershipofmoderatorupdatedcategory_some?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
-  categorymembershipofmoderatorupdatedcategory_every?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
+  categorymembershipofmoderatorupdatedeventcategory_none?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
+  categorymembershipofmoderatorupdatedeventcategory_some?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
+  categorymembershipofmoderatorupdatedeventcategory_every?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
   categorystickythreadupdateeventcategory_none?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
   categorystickythreadupdateeventcategory_some?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
   categorystickythreadupdateeventcategory_every?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
@@ -6668,9 +6668,9 @@ export type Query = {
   categoryDeletedEvents: Array<CategoryDeletedEvent>
   categoryDeletedEventByUniqueInput?: Maybe<CategoryDeletedEvent>
   categoryDeletedEventsConnection: CategoryDeletedEventConnection
-  categoryMembershipOfModeratorUpdateds: Array<CategoryMembershipOfModeratorUpdated>
-  categoryMembershipOfModeratorUpdatedByUniqueInput?: Maybe<CategoryMembershipOfModeratorUpdated>
-  categoryMembershipOfModeratorUpdatedsConnection: CategoryMembershipOfModeratorUpdatedConnection
+  categoryMembershipOfModeratorUpdatedEvents: Array<CategoryMembershipOfModeratorUpdatedEvent>
+  categoryMembershipOfModeratorUpdatedEventByUniqueInput?: Maybe<CategoryMembershipOfModeratorUpdatedEvent>
+  categoryMembershipOfModeratorUpdatedEventsConnection: CategoryMembershipOfModeratorUpdatedEventConnection
   categoryStickyThreadUpdateEvents: Array<CategoryStickyThreadUpdateEvent>
   categoryStickyThreadUpdateEventByUniqueInput?: Maybe<CategoryStickyThreadUpdateEvent>
   categoryStickyThreadUpdateEventsConnection: CategoryStickyThreadUpdateEventConnection
@@ -7023,24 +7023,24 @@ export type QueryCategoryDeletedEventsConnectionArgs = {
   orderBy?: Maybe<Array<CategoryDeletedEventOrderByInput>>
 }
 
-export type QueryCategoryMembershipOfModeratorUpdatedsArgs = {
+export type QueryCategoryMembershipOfModeratorUpdatedEventsArgs = {
   offset?: Maybe<Scalars['Int']>
   limit?: Maybe<Scalars['Int']>
-  where?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
-  orderBy?: Maybe<Array<CategoryMembershipOfModeratorUpdatedOrderByInput>>
+  where?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
+  orderBy?: Maybe<Array<CategoryMembershipOfModeratorUpdatedEventOrderByInput>>
 }
 
-export type QueryCategoryMembershipOfModeratorUpdatedByUniqueInputArgs = {
-  where: CategoryMembershipOfModeratorUpdatedWhereUniqueInput
+export type QueryCategoryMembershipOfModeratorUpdatedEventByUniqueInputArgs = {
+  where: CategoryMembershipOfModeratorUpdatedEventWhereUniqueInput
 }
 
-export type QueryCategoryMembershipOfModeratorUpdatedsConnectionArgs = {
+export type QueryCategoryMembershipOfModeratorUpdatedEventsConnectionArgs = {
   first?: Maybe<Scalars['Int']>
   after?: Maybe<Scalars['String']>
   last?: Maybe<Scalars['Int']>
   before?: Maybe<Scalars['String']>
-  where?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
-  orderBy?: Maybe<Array<CategoryMembershipOfModeratorUpdatedOrderByInput>>
+  where?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
+  orderBy?: Maybe<Array<CategoryMembershipOfModeratorUpdatedEventOrderByInput>>
 }
 
 export type QueryCategoryStickyThreadUpdateEventsArgs = {
@@ -11087,7 +11087,7 @@ export type Worker = BaseGraphQlObject & {
   storage?: Maybe<Scalars['String']>
   managedForumCategories: Array<ForumCategory>
   categorydeletedeventactor?: Maybe<Array<CategoryDeletedEvent>>
-  categorymembershipofmoderatorupdatedmoderator?: Maybe<Array<CategoryMembershipOfModeratorUpdated>>
+  categorymembershipofmoderatorupdatedeventmoderator?: Maybe<Array<CategoryMembershipOfModeratorUpdatedEvent>>
   categorystickythreadupdateeventactor?: Maybe<Array<CategoryStickyThreadUpdateEvent>>
   categoryupdatedeventactor?: Maybe<Array<CategoryUpdatedEvent>>
   leaderseteventworker?: Maybe<Array<LeaderSetEvent>>
@@ -12050,9 +12050,9 @@ export type WorkerWhereInput = {
   categorydeletedeventactor_none?: Maybe<CategoryDeletedEventWhereInput>
   categorydeletedeventactor_some?: Maybe<CategoryDeletedEventWhereInput>
   categorydeletedeventactor_every?: Maybe<CategoryDeletedEventWhereInput>
-  categorymembershipofmoderatorupdatedmoderator_none?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
-  categorymembershipofmoderatorupdatedmoderator_some?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
-  categorymembershipofmoderatorupdatedmoderator_every?: Maybe<CategoryMembershipOfModeratorUpdatedWhereInput>
+  categorymembershipofmoderatorupdatedeventmoderator_none?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
+  categorymembershipofmoderatorupdatedeventmoderator_some?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
+  categorymembershipofmoderatorupdatedeventmoderator_every?: Maybe<CategoryMembershipOfModeratorUpdatedEventWhereInput>
   categorystickythreadupdateeventactor_none?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
   categorystickythreadupdateeventactor_some?: Maybe<CategoryStickyThreadUpdateEventWhereInput>
   categorystickythreadupdateeventactor_every?: Maybe<CategoryStickyThreadUpdateEventWhereInput>

+ 22 - 0
tests/integration-tests/src/graphql/queries/forumEvents.graphql

@@ -212,3 +212,25 @@ query getCategoryStickyThreadUpdateEventsByEventIds($eventIds: [ID!]) {
     ...CategoryStickyThreadUpdateEventFields
   }
 }
+
+fragment CategoryMembershipOfModeratorUpdatedEventFields on CategoryMembershipOfModeratorUpdatedEvent {
+  id
+  createdAt
+  inBlock
+  network
+  inExtrinsic
+  indexInBlock
+  category {
+    id
+  }
+  moderator {
+    id
+  }
+  newCanModerateValue
+}
+
+query getCategoryMembershipOfModeratorUpdatedEventsByEventIds($eventIds: [ID!]) {
+  categoryMembershipOfModeratorUpdatedEvents(where: { id_in: $eventIds }) {
+    ...CategoryMembershipOfModeratorUpdatedEventFields
+  }
+}