Преглед изворни кода

LeaderSet event workaround

Leszek Wiesner пре 3 година
родитељ
комит
8d2ff2c53d

+ 8 - 8
query-node/manifest.yml

@@ -103,8 +103,8 @@ mappings:
       handler: workingGroups_AppliedOnOpening(DatabaseManager, SubstrateEvent)
     - event: storageWorkingGroup.OpeningFilled
       handler: workingGroups_OpeningFilled(DatabaseManager, SubstrateEvent)
-    # - event: storageWorkingGroup.LeaderSet
-    #   handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
+    - event: storageWorkingGroup.LeaderSet
+      handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
     - event: storageWorkingGroup.WorkerRoleAccountUpdated
       handler: workingGroups_WorkerRoleAccountUpdated(DatabaseManager, SubstrateEvent)
     - event: storageWorkingGroup.LeaderUnset
@@ -148,8 +148,8 @@ mappings:
       handler: workingGroups_AppliedOnOpening(DatabaseManager, SubstrateEvent)
     - event: forumWorkingGroup.OpeningFilled
       handler: workingGroups_OpeningFilled(DatabaseManager, SubstrateEvent)
-    # - event: forumWorkingGroup.LeaderSet
-    #   handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
+    - event: forumWorkingGroup.LeaderSet
+      handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
     - event: forumWorkingGroup.WorkerRoleAccountUpdated
       handler: workingGroups_WorkerRoleAccountUpdated(DatabaseManager, SubstrateEvent)
     - event: forumWorkingGroup.LeaderUnset
@@ -193,8 +193,8 @@ mappings:
       handler: workingGroups_AppliedOnOpening(DatabaseManager, SubstrateEvent)
     - event: membershipWorkingGroup.OpeningFilled
       handler: workingGroups_OpeningFilled(DatabaseManager, SubstrateEvent)
-    # - event: membershipWorkingGroup.LeaderSet
-    #   handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
+    - event: membershipWorkingGroup.LeaderSet
+      handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
     - event: membershipWorkingGroup.WorkerRoleAccountUpdated
       handler: workingGroups_WorkerRoleAccountUpdated(DatabaseManager, SubstrateEvent)
     - event: membershipWorkingGroup.LeaderUnset
@@ -238,8 +238,8 @@ mappings:
       handler: workingGroups_AppliedOnOpening(DatabaseManager, SubstrateEvent)
     - event: contentDirectoryWorkingGroup.OpeningFilled
       handler: workingGroups_OpeningFilled(DatabaseManager, SubstrateEvent)
-    # - event: contentDirectoryWorkingGroup.LeaderSet
-    #   handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
+    - event: contentDirectoryWorkingGroup.LeaderSet
+      handler: workingGroups_LeaderSet(DatabaseManager, SubstrateEvent)
     - event: contentDirectoryWorkingGroup.WorkerRoleAccountUpdated
       handler: workingGroups_WorkerRoleAccountUpdated(DatabaseManager, SubstrateEvent)
     - event: contentDirectoryWorkingGroup.LeaderUnset

+ 84 - 73
query-node/mappings/workingGroups.ts

@@ -71,6 +71,8 @@ import {
   WorkerStartedLeavingEvent,
   BudgetSetEvent,
   BudgetSpendingEvent,
+  LeaderSetEvent,
+  Event,
 } from 'query-node/dist/model'
 import { createType } from '@joystream/types'
 import _ from 'lodash'
@@ -370,6 +372,17 @@ async function handleTerminatedWorker(db: DatabaseManager, event_: SubstrateEven
   await db.save<Worker>(worker)
 }
 
+export async function findLeaderSetEventByTxHash(db: DatabaseManager, txHash?: string): Promise<LeaderSetEvent> {
+  const event = await db.get(Event, { where: { inExtrinsic: txHash } })
+  const leaderSetEvent = await db.get(LeaderSetEvent, { where: { event }, relations: ['event'] })
+
+  if (!leaderSetEvent) {
+    throw new Error(`LeaderSet event not found by tx hash: ${txHash}`)
+  }
+
+  return leaderSetEvent
+}
+
 // Mapping functions
 export async function workingGroups_OpeningAdded(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
@@ -465,6 +478,22 @@ export async function workingGroups_AppliedOnOpening(db: DatabaseManager, event_
   await db.save<AppliedOnOpeningEvent>(appliedOnOpeningEvent)
 }
 
+export async function workingGroups_LeaderSet(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
+  event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
+  const eventTime = new Date(event_.blockTimestamp.toNumber())
+  const group = await getWorkingGroup(db, event_)
+
+  const event = await createEvent(db, event_, EventType.LeaderSet)
+  const leaderSetEvent = new LeaderSetEvent({
+    createdAt: eventTime,
+    updatedAt: eventTime,
+    event,
+    group,
+  })
+
+  await db.save<LeaderSetEvent>(leaderSetEvent)
+}
+
 export async function workingGroups_OpeningFilled(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   const eventTime = new Date(event_.blockTimestamp.toNumber())
@@ -494,56 +523,57 @@ export async function workingGroups_OpeningFilled(db: DatabaseManager, event_: S
 
   await db.save<OpeningFilledEvent>(openingFilledEvent)
 
-  const hiredWorkers: Worker[] = []
   // Update applications and create new workers
-  await Promise.all(
-    (opening.applications || [])
-      // Skip withdrawn applications
-      .filter((application) => application.status.isTypeOf !== 'ApplicationStatusWithdrawn')
-      .map(async (application) => {
-        const isAccepted = acceptedApplicationIds.some((runtimeId) => runtimeId.toNumber() === application.runtimeId)
-        const applicationStatus = isAccepted ? new ApplicationStatusAccepted() : new ApplicationStatusRejected()
-        applicationStatus.openingFilledEventId = openingFilledEvent.id
-        application.status = applicationStatus
-        application.updatedAt = eventTime
-        if (isAccepted) {
-          // Cannot use "applicationIdToWorkerIdMap.get" here,
-          // it only works if the passed instance is identical to BTreeMap key instance (=== instead of .eq)
-          const [, workerRuntimeId] =
-            Array.from(applicationIdToWorkerIdMap.entries()).find(
-              ([applicationRuntimeId]) => applicationRuntimeId.toNumber() === application.runtimeId
-            ) || []
-          if (!workerRuntimeId) {
-            throw new Error(
-              `Fatal: No worker id found by accepted application id ${application.id} when handling OpeningFilled event!`
-            )
+  const hiredWorkers = (
+    await Promise.all(
+      (opening.applications || [])
+        // Skip withdrawn applications
+        .filter((application) => application.status.isTypeOf !== 'ApplicationStatusWithdrawn')
+        .map(async (application) => {
+          const isAccepted = acceptedApplicationIds.some((runtimeId) => runtimeId.toNumber() === application.runtimeId)
+          const applicationStatus = isAccepted ? new ApplicationStatusAccepted() : new ApplicationStatusRejected()
+          applicationStatus.openingFilledEventId = openingFilledEvent.id
+          application.status = applicationStatus
+          application.updatedAt = eventTime
+          await db.save<WorkingGroupApplication>(application)
+          if (isAccepted) {
+            // Cannot use "applicationIdToWorkerIdMap.get" here,
+            // it only works if the passed instance is identical to BTreeMap key instance (=== instead of .eq)
+            const [, workerRuntimeId] =
+              Array.from(applicationIdToWorkerIdMap.entries()).find(
+                ([applicationRuntimeId]) => applicationRuntimeId.toNumber() === application.runtimeId
+              ) || []
+            if (!workerRuntimeId) {
+              throw new Error(
+                `Fatal: No worker id found by accepted application id ${application.id} when handling OpeningFilled event!`
+              )
+            }
+            const worker = new Worker({
+              createdAt: eventTime,
+              updatedAt: eventTime,
+              id: `${group.name}-${workerRuntimeId.toString()}`,
+              runtimeId: workerRuntimeId.toNumber(),
+              hiredAtBlock: await getOrCreateBlock(db, event_),
+              hiredAtTime: new Date(event_.blockTimestamp.toNumber()),
+              application,
+              group,
+              isLead: opening.type === WorkingGroupOpeningType.LEADER,
+              membership: application.applicant,
+              stake: application.stake,
+              roleAccount: application.roleAccount,
+              rewardAccount: application.rewardAccount,
+              stakeAccount: application.stakingAccount,
+              payouts: [],
+              status: new WorkerStatusActive(),
+              entry: openingFilledEvent,
+              rewardPerBlock: opening.rewardPerBlock,
+            })
+            await db.save<Worker>(worker)
+            return worker
           }
-          const worker = new Worker({
-            createdAt: eventTime,
-            updatedAt: eventTime,
-            id: `${group.name}-${workerRuntimeId.toString()}`,
-            runtimeId: workerRuntimeId.toNumber(),
-            hiredAtBlock: await getOrCreateBlock(db, event_),
-            hiredAtTime: new Date(event_.blockTimestamp.toNumber()),
-            application,
-            group,
-            isLead: opening.type === WorkingGroupOpeningType.LEADER,
-            membership: application.applicant,
-            stake: application.stake,
-            roleAccount: application.roleAccount,
-            rewardAccount: application.rewardAccount,
-            stakeAccount: application.stakingAccount,
-            payouts: [],
-            status: new WorkerStatusActive(),
-            entry: openingFilledEvent,
-            rewardPerBlock: opening.rewardPerBlock,
-          })
-          await db.save<Worker>(worker)
-          hiredWorkers.push(worker)
-        }
-        await db.save<WorkingGroupApplication>(application)
-      })
-  )
+        })
+    )
+  ).filter((w) => w !== undefined) as Worker[]
 
   // Set opening status
   const openingFilled = new OpeningStatusFilled()
@@ -552,38 +582,19 @@ export async function workingGroups_OpeningFilled(db: DatabaseManager, event_: S
   opening.updatedAt = eventTime
   await db.save<WorkingGroupOpening>(opening)
 
-  // Update working group if necessary
+  // Update working group and LeaderSetEvent if necessary
   if (opening.type === WorkingGroupOpeningType.LEADER && hiredWorkers.length) {
     group.leader = hiredWorkers[0]
     group.updatedAt = eventTime
     await db.save<WorkingGroup>(group)
+
+    const leaderSetEvent = await findLeaderSetEventByTxHash(db, openingFilledEvent.event.inExtrinsic)
+    leaderSetEvent.worker = hiredWorkers[0]
+    leaderSetEvent.updatedAt = eventTime
+    await db.save<LeaderSetEvent>(leaderSetEvent)
   }
 }
 
-// FIXME: Currently this event cannot be handled directly, because the worker does not yet exist at the time when it is emitted
-// export async function workingGroups_LeaderSet(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
-//   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
-//   const { workerId: workerRuntimeId } = new WorkingGroups.LeaderSetEvent(event_).data
-
-//   const group = await getWorkingGroup(db, event_)
-//   const workerDbId = `${group.name}-${workerRuntimeId.toString()}`
-//   const worker = new Worker({ id: workerDbId })
-//   const eventTime = new Date(event_.blockTimestamp.toNumber())
-
-//   // Create and save event
-//   const event = createEvent(event_, EventType.LeaderSet)
-//   const leaderSetEvent = new LeaderSetEvent({
-//     createdAt: eventTime,
-//     updatedAt: eventTime,
-//     event,
-//     group,
-//     worker,
-//   })
-
-//   await db.save<Event>(event)
-//   await db.save<LeaderSetEvent>(leaderSetEvent)
-// }
-
 export async function workingGroups_OpeningCanceled(db: DatabaseManager, event_: SubstrateEvent): Promise<void> {
   event_.blockTimestamp = new BN(event_.blockTimestamp) // FIXME: Temporary fix for wrong blockTimestamp type
   const { openingId: openingRuntimeId } = new WorkingGroups.OpeningCanceledEvent(event_).data

+ 2 - 1
query-node/schemas/workingGroupsEvents.graphql

@@ -48,8 +48,9 @@ type LeaderSetEvent @entity {
   "Related group"
   group: WorkingGroup!
 
+  # The field must be optional, because at the time the event is emitted the worker does not yet exist
   "Related Lead worker"
-  worker: Worker!
+  worker: Worker
 }
 
 type WorkerRoleAccountUpdatedEvent @entity {

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

@@ -168,6 +168,10 @@ import {
   GetLeaderUnsetEventsByEventIdsQuery,
   GetLeaderUnsetEventsByEventIdsQueryVariables,
   GetLeaderUnsetEventsByEventIds,
+  LeaderSetEventFieldsFragment,
+  GetLeaderSetEventsByEventIdsQuery,
+  GetLeaderSetEventsByEventIdsQueryVariables,
+  GetLeaderSetEventsByEventIds,
 } from './graphql/generated/queries'
 import { Maybe } from './graphql/generated/schema'
 import { OperationDefinitionNode } from 'graphql'
@@ -642,6 +646,15 @@ export class QueryNodeApi {
     >(GetBudgetSpendingEventsByEventIds, { eventIds }, 'budgetSpendingEvents')
   }
 
+  public async getLeaderSetEvent(event: EventDetails): Promise<LeaderSetEventFieldsFragment | null> {
+    const eventId = this.getQueryNodeEventId(event.blockNumber, event.indexInBlock)
+    return this.firstEntityQuery<GetLeaderSetEventsByEventIdsQuery, GetLeaderSetEventsByEventIdsQueryVariables>(
+      GetLeaderSetEventsByEventIds,
+      { eventIds: [eventId] },
+      'leaderSetEvents'
+    )
+  }
+
   public async getLeaderUnsetEvent(event: EventDetails): Promise<LeaderUnsetEventFieldsFragment | null> {
     const eventId = this.getQueryNodeEventId(event.blockNumber, event.indexInBlock)
     return this.firstEntityQuery<GetLeaderUnsetEventsByEventIdsQuery, GetLeaderUnsetEventsByEventIdsQueryVariables>(

+ 24 - 2
tests/integration-tests/src/fixtures/workingGroups/FillOpeningsFixture.ts

@@ -2,7 +2,7 @@ import BN from 'bn.js'
 import { assert } from 'chai'
 import { Api } from '../../Api'
 import { QueryNodeApi } from '../../QueryNodeApi'
-import { OpeningFilledEventDetails, WorkingGroupModuleName } from '../../types'
+import { EventDetails, OpeningFilledEventDetails, WorkingGroupModuleName } from '../../types'
 import { BaseWorkingGroupFixture } from './BaseWorkingGroupFixture'
 import { Application, ApplicationId, Opening, OpeningId, WorkerId } from '@joystream/types/working-group'
 import { SubmittableExtrinsic } from '@polkadot/api/types'
@@ -13,6 +13,7 @@ import { JoyBTreeSet } from '@joystream/types/common'
 import { registry } from '@joystream/types'
 import { lockIdByWorkingGroup } from '../../consts'
 import {
+  LeaderSetEventFieldsFragment,
   OpeningFieldsFragment,
   OpeningFilledEventFieldsFragment,
   WorkerFieldsFragment,
@@ -188,6 +189,20 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
     })
   }
 
+  protected assertQueryNodeLeaderSetEventIsValid(
+    eventDetails: EventDetails,
+    qEvent: LeaderSetEventFieldsFragment | null,
+    workerRuntimeId: number
+  ): void {
+    Utils.assert(qEvent, 'Query node: LeaderSet not found!')
+    assert.equal(qEvent.event.inBlock.timestamp, eventDetails.blockTimestamp)
+    assert.equal(qEvent.event.inExtrinsic, this.extrinsics[0].hash.toString())
+    assert.equal(qEvent.event.type, EventType.LeaderSet)
+    assert.equal(qEvent.group.name, this.group)
+    Utils.assert(qEvent.worker, 'LeaderSet: Worker is empty')
+    assert.equal(qEvent.worker.runtimeId, workerRuntimeId)
+  }
+
   async runQueryNodeChecks(): Promise<void> {
     await super.runQueryNodeChecks()
     // Query the event and check event + hiredWorkers
@@ -204,10 +219,17 @@ export class FillOpeningsFixture extends BaseWorkingGroupFixture {
     this.assertApplicationStatusesAreValid(qEvents, qOpenings)
 
     if (this.asSudo) {
+      const leaderId = qEvents[0].workersHired[0].runtimeId
+      assert.isNumber(leaderId)
+
       const qGroup = await this.query.getWorkingGroup(this.group)
       Utils.assert(qGroup, 'Query node: Working group not found!')
       Utils.assert(qGroup.leader, 'Query node: Working group leader not set!')
-      assert.equal(qGroup.leader.runtimeId, qEvents[0].workersHired[0].runtimeId)
+      assert.equal(qGroup.leader.runtimeId, leaderId)
+
+      const leaderSetEvent = await this.api.retrieveWorkingGroupsEventDetails(this.results[0], this.group, 'LeaderSet')
+      const qEvent = await this.query.getLeaderSetEvent(leaderSetEvent)
+      this.assertQueryNodeLeaderSetEventIsValid(leaderSetEvent, qEvent, leaderId)
     }
   }
 }

+ 39 - 2
tests/integration-tests/src/graphql/generated/queries.ts

@@ -266,7 +266,7 @@ export type OpeningStatusFieldsFragment =
   | OpeningStatusFields_OpeningStatusCancelled_Fragment
 
 export type ApplicationFormQuestionFieldsFragment = {
-  question: string
+  question?: Types.Maybe<string>
   type: Types.ApplicationFormQuestionType
   index: number
 }
@@ -353,7 +353,7 @@ export type ApplicationFieldsFragment = {
   createdAtBlock: BlockFieldsFragment
   opening: { id: string; runtimeId: number }
   applicant: { id: string }
-  answers: Array<{ answer: string; question: { question: string } }>
+  answers: Array<{ answer: string; question: { question?: Types.Maybe<string> } }>
 } & ApplicationBasicFieldsFragment
 
 export type GetApplicationByIdQueryVariables = Types.Exact<{
@@ -454,6 +454,19 @@ export type GetOpeningAddedEventsByEventIdsQueryVariables = Types.Exact<{
 
 export type GetOpeningAddedEventsByEventIdsQuery = { openingAddedEvents: Array<OpeningAddedEventFieldsFragment> }
 
+export type LeaderSetEventFieldsFragment = {
+  id: string
+  event: EventFieldsFragment
+  group: { name: string }
+  worker?: Types.Maybe<{ id: string; runtimeId: number }>
+}
+
+export type GetLeaderSetEventsByEventIdsQueryVariables = Types.Exact<{
+  eventIds?: Types.Maybe<Array<Types.Scalars['ID']> | Types.Scalars['ID']>
+}>
+
+export type GetLeaderSetEventsByEventIdsQuery = { leaderSetEvents: Array<LeaderSetEventFieldsFragment> }
+
 export type OpeningFilledEventFieldsFragment = {
   id: string
   event: EventFieldsFragment
@@ -1139,6 +1152,22 @@ export const OpeningAddedEventFields = gql`
   }
   ${EventFields}
 `
+export const LeaderSetEventFields = gql`
+  fragment LeaderSetEventFields on LeaderSetEvent {
+    id
+    event {
+      ...EventFields
+    }
+    group {
+      name
+    }
+    worker {
+      id
+      runtimeId
+    }
+  }
+  ${EventFields}
+`
 export const WorkerFields = gql`
   fragment WorkerFields on Worker {
     id
@@ -1673,6 +1702,14 @@ export const GetOpeningAddedEventsByEventIds = gql`
   }
   ${OpeningAddedEventFields}
 `
+export const GetLeaderSetEventsByEventIds = gql`
+  query getLeaderSetEventsByEventIds($eventIds: [ID!]) {
+    leaderSetEvents(where: { eventId_in: $eventIds }) {
+      ...LeaderSetEventFields
+    }
+  }
+  ${LeaderSetEventFields}
+`
 export const GetOpeningFilledEventsByEventIds = gql`
   query getOpeningFilledEventsByEventIds($eventIds: [ID!]) {
     openingFilledEvents(where: { eventId_in: $eventIds }) {

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

@@ -29,7 +29,7 @@ export type ApplicationFormQuestion = BaseGraphQlObject & {
   openingMetadata: WorkingGroupOpeningMetadata
   openingMetadataId: Scalars['String']
   /** The question itself */
-  question: Scalars['String']
+  question?: Maybe<Scalars['String']>
   /** Type of the question (UI answer input type) */
   type: ApplicationFormQuestionType
   /** Index of the question */
@@ -140,7 +140,7 @@ export type ApplicationFormQuestionConnection = {
 
 export type ApplicationFormQuestionCreateInput = {
   openingMetadataId: Scalars['ID']
-  question: Scalars['String']
+  question?: Maybe<Scalars['String']>
   type: ApplicationFormQuestionType
   index: Scalars['Float']
 }
@@ -2083,8 +2083,8 @@ export type LeaderSetEvent = BaseGraphQlObject & {
   eventId: Scalars['String']
   group: WorkingGroup
   groupId: Scalars['String']
-  worker: Worker
-  workerId: Scalars['String']
+  worker?: Maybe<Worker>
+  workerId?: Maybe<Scalars['String']>
 }
 
 export type LeaderSetEventConnection = {
@@ -2096,7 +2096,7 @@ export type LeaderSetEventConnection = {
 export type LeaderSetEventCreateInput = {
   eventId: Scalars['ID']
   groupId: Scalars['ID']
-  workerId: Scalars['ID']
+  workerId?: Maybe<Scalars['ID']>
 }
 
 export type LeaderSetEventEdge = {

+ 20 - 0
tests/integration-tests/src/graphql/queries/workingGroupsEvents.graphql

@@ -42,6 +42,26 @@ query getOpeningAddedEventsByEventIds($eventIds: [ID!]) {
   }
 }
 
+fragment LeaderSetEventFields on LeaderSetEvent {
+  id
+  event {
+    ...EventFields
+  }
+  group {
+    name
+  }
+  worker {
+    id
+    runtimeId
+  }
+}
+
+query getLeaderSetEventsByEventIds($eventIds: [ID!]) {
+  leaderSetEvents(where: { eventId_in: $eventIds }) {
+    ...LeaderSetEventFields
+  }
+}
+
 fragment OpeningFilledEventFields on OpeningFilledEvent {
   id
   event {