diff --git a/distributor-node/src/services/networking/query-node/schema.graphql b/distributor-node/src/services/networking/query-node/schema.graphql index 59bf0630c5..d311147aa7 100644 --- a/distributor-node/src/services/networking/query-node/schema.graphql +++ b/distributor-node/src/services/networking/query-node/schema.graphql @@ -2108,7 +2108,7 @@ type Proposal implements BaseGraphQLObject { proposalexecutedeventproposal: [ProposalExecutedEvent!] } -union ProposalDetails = SignalProposalDetails | RuntimeUpgradeProposalDetails | FundingRequestProposalDetails | SetMaxValidatorCountProposalDetails | CreateWorkingGroupLeadOpeningProposalDetails | FillWorkingGroupLeadOpeningProposalDetails | UpdateWorkingGroupBudgetProposalDetails | DecreaseWorkingGroupLeadStakeProposalDetails | SlashWorkingGroupLeadProposalDetails | SetWorkingGroupLeadRewardProposalDetails | TerminateWorkingGroupLeadProposalDetails | AmendConstitutionProposalDetails | CancelWorkingGroupLeadOpeningProposalDetails | SetMembershipPriceProposalDetails | SetCouncilBudgetIncrementProposalDetails | SetCouncilorRewardProposalDetails | SetInitialInvitationBalanceProposalDetails | SetInitialInvitationCountProposalDetails | SetMembershipLeadInvitationQuotaProposalDetails | SetReferralCutProposalDetails | VetoProposalDetails | UpdateChannelPayoutsProposalDetails +union ProposalDetails = SignalProposalDetails | RuntimeUpgradeProposalDetails | FundingRequestProposalDetails | SetMaxValidatorCountProposalDetails | CreateWorkingGroupLeadOpeningProposalDetails | FillWorkingGroupLeadOpeningProposalDetails | UpdateWorkingGroupBudgetProposalDetails | DecreaseWorkingGroupLeadStakeProposalDetails | SlashWorkingGroupLeadProposalDetails | SetWorkingGroupLeadRewardProposalDetails | TerminateWorkingGroupLeadProposalDetails | AmendConstitutionProposalDetails | CancelWorkingGroupLeadOpeningProposalDetails | SetMembershipPriceProposalDetails | SetCouncilBudgetIncrementProposalDetails | SetCouncilorRewardProposalDetails | SetInitialInvitationBalanceProposalDetails | SetInitialInvitationCountProposalDetails | SetMembershipLeadInvitationQuotaProposalDetails | SetReferralCutProposalDetails | VetoProposalDetails | UpdateChannelPayoutsProposalDetails | UpdateGlobalNftLimitProposalDetails union ProposalStatus = ProposalStatusDeciding | ProposalStatusGracing | ProposalStatusDormant | ProposalStatusVetoed | ProposalStatusExecuted | ProposalStatusExecutionFailed | ProposalStatusSlashed | ProposalStatusRejected | ProposalStatusExpired | ProposalStatusCancelled | ProposalStatusCanceledByRuntime @@ -2826,6 +2826,14 @@ type UpdateChannelPayoutsProposalDetails { payloadHash: String } +type UpdateGlobalNftLimitProposalDetails { + """New daily NFT limit set in the proposal (if any)""" + newDailyNftLimit: Int + + """New weekly NFT limit set in the proposal (if any)""" + newWeeklyNftLimit: Int +} + type UpdateWorkingGroupBudgetProposalDetails { """ Amount to increase / decrease the working group budget by (will be decudted from / appended to council budget accordingly) diff --git a/eslint-local-rules.js b/eslint-local-rules.js new file mode 100644 index 0000000000..f20f03a890 --- /dev/null +++ b/eslint-local-rules.js @@ -0,0 +1,24 @@ +module.exports = { + 'no-throw': { + meta: { + type: 'problem', + docs: { + description: 'disallow the use of throw keyword', + category: 'Possible Errors', + recommended: true, + }, + schema: [], + }, + + create: function (context) { + return { + ThrowStatement(node) { + context.report({ + node: node, + message: "The use of 'throw' keyword is not allowed.", + }) + }, + } + }, + }, +} diff --git a/query-node/.prettierignore b/query-node/.prettierignore new file mode 100644 index 0000000000..0ed04b87da --- /dev/null +++ b/query-node/.prettierignore @@ -0,0 +1 @@ +chain-metadata/ diff --git a/query-node/CHANGELOG.md b/query-node/CHANGELOG.md index eb5ec166fd..2444c11327 100644 --- a/query-node/CHANGELOG.md +++ b/query-node/CHANGELOG.md @@ -1,3 +1,9 @@ +### 1.7.0 + +- Refactor of mappings for more better handling of error cases. [#4856](https://github.com/Joystream/joystream/pull/4856) +- Bug fix [#4855](https://github.com/Joystream/joystream/issues/4855) +- Add support for UpdateGlobalNftLimit proposal. + ### 1.6.0 - Store membership handles both as utf-8 string and raw bytes - [#4950](https://github.com/Joystream/joystream/pull/4950) diff --git a/query-node/mappings/.eslintrc.js b/query-node/mappings/.eslintrc.js index 91881aea67..dbcdac64b6 100644 --- a/query-node/mappings/.eslintrc.js +++ b/query-node/mappings/.eslintrc.js @@ -3,7 +3,9 @@ module.exports = { env: { node: true, }, + plugins: ['eslint-plugin-local-rules'], rules: { + 'local-rules/no-throw': 'error', '@typescript-eslint/naming-convention': 'off', // TODO: Remove all the rules below, they seem quite useful '@typescript-eslint/no-explicit-any': 'off', diff --git a/query-node/mappings/package.json b/query-node/mappings/package.json index 19959cd928..8c96d93315 100644 --- a/query-node/mappings/package.json +++ b/query-node/mappings/package.json @@ -1,6 +1,6 @@ { "name": "query-node-mappings", - "version": "1.6.0", + "version": "1.7.0", "description": "Mappings for hydra-processor", "main": "lib/src/index.js", "license": "MIT", @@ -16,14 +16,15 @@ "bootstrap-data:fetch": "yarn bootstrap-data:fetch:members && yarn bootstrap-data:fetch:workingGroups && yarn bootstrap-data:fetch:categories" }, "dependencies": { - "@polkadot/types": "8.9.1", + "@apollo/client": "^3.2.5", "@joystream/hydra-common": "5.0.0-alpha.4", "@joystream/hydra-db-utils": "5.0.0-alpha.4", - "@joystream/warthog": "^2.41.9", "@joystream/js": "^1.5.0", - "@apollo/client": "^3.2.5" + "@joystream/warthog": "^2.41.9", + "@polkadot/types": "8.9.1" }, "devDependencies": { + "eslint-plugin-local-rules": "2.0.0", "prettier": "^2.2.1", "ts-node": "^10.2.1", "typescript": "^4.4.3" diff --git a/query-node/mappings/src/common.ts b/query-node/mappings/src/common.ts index 500d2f91a3..398aabbc8a 100644 --- a/query-node/mappings/src/common.ts +++ b/query-node/mappings/src/common.ts @@ -1,22 +1,29 @@ -import { DatabaseManager, SubstrateEvent, FindOneOptions } from '@joystream/hydra-common' -import { Bytes, Option } from '@polkadot/types' -import { Codec } from '@polkadot/types/types' +import { + DatabaseManager, + FindOneOptions, + FindOptionsOrderValue, + FindOptionsWhere, + SubstrateEvent, +} from '@joystream/hydra-common' +import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types' +import { metaToObject } from '@joystream/metadata-protobuf/utils' import { MemberId, WorkerId } from '@joystream/types/primitives' +import { BaseModel } from '@joystream/warthog' +import { Bytes, Option } from '@polkadot/types' import { PalletCommonWorkingGroupIterableEnumsWorkingGroup as WGType } from '@polkadot/types/lookup' +import { Codec } from '@polkadot/types/types' +import BN from 'bn.js' import { - Worker, Event, - Network, - WorkingGroup as WGEntity, - MetaprotocolTransactionStatusEvent, + Membership, MetaprotocolTransactionErrored, + MetaprotocolTransactionStatusEvent, MetaprotocolTransactionSuccessful, - Membership, + Network, + WorkingGroup as WGEntity, + Worker, } from 'query-node/dist/model' -import { BaseModel } from '@joystream/warthog' -import { metaToObject } from '@joystream/metadata-protobuf/utils' -import { AnyMetadataClass, DecodedMetadataObject } from '@joystream/metadata-protobuf/types' -import BN from 'bn.js' +import { In } from 'typeorm' export const CURRENT_NETWORK = Network.OLYMPIA @@ -88,19 +95,20 @@ export function inconsistentState(extraInfo: string, data?: unknown): never { // log error logger.error(errorMessage, data) + // eslint-disable-next-line local-rules/no-throw throw errorMessage } -/* - Reports that insurmountable unexpected data has been encountered and throws an exception. -*/ -export function unexpectedData(extraInfo: string, data?: unknown): never { - const errorMessage = 'Unexpected data: ' + extraInfo +/** + * Reports an unimplemented mapping/variant error for which a runtime implementation logic exists and is not filtered. + */ +export function unimplementedError(extraInfo: string, data?: unknown): never { + const errorMessage = 'unimplemented error: ' + extraInfo // log error logger.error(errorMessage, data) - throw errorMessage + process.exit(1) } /* @@ -124,7 +132,7 @@ export function deserializeMetadata( const message = metadataType.decode(metadataBytes.toU8a(true)) Object.keys(message).forEach((key) => { if (key in message && typeof message[key] === 'string') { - message[key] = perpareString(message[key]) + message[key] = prepareString(message[key]) } }) return metaToObject(metadataType, message) @@ -145,7 +153,7 @@ export function bytesToString(b: Bytes): string { ) } -export function perpareString(s: string): string { +export function prepareString(s: string): string { // eslint-disable-next-line no-control-regex return s.replace(/\u0000/g, '') } @@ -211,55 +219,33 @@ export function getWorkingGroupModuleName(group: WGType): WorkingGroupModuleName return 'operationsWorkingGroupGamma' } - unexpectedData('Unsupported working group encountered:', group.type) + unimplementedError('Unsupported working group encountered:', group.type) } -export async function getWorkingGroupByName( +export async function getWorkingGroupByNameOrFail( store: DatabaseManager, name: WorkingGroupModuleName, - relations: string[] = [] + relations: RelationsArr = [] ): Promise { - const group = await store.get(WGEntity, { where: { name }, relations }) - if (!group) { - return inconsistentState(`Working group ${name} not found!`) - } - return group + return getOneByOrFail(store, WGEntity, { name }, relations) } -export async function getMemberById( +export async function getMembershipById( store: DatabaseManager, id: MemberId, - relations: string[] = [] + relations: RelationsArr = [] ): Promise { - const member = await store.get(Membership, { where: { id: id.toString() }, relations }) - if (!member) { - throw new Error(`Member(${id}) not found`) - } - return member -} - -export async function getWorkingGroupLead(store: DatabaseManager, groupName: WorkingGroupModuleName) { - const lead = await store.get(Worker, { where: { groupId: groupName, isLead: true, isActive: true } }) - if (!lead) { - return inconsistentState(`Couldn't find an active lead for ${groupName}`) - } - - return lead + return getByIdOrFail(store, Membership, id.toString(), relations) } -export async function getWorker( +export async function getWorkerOrFail( store: DatabaseManager, groupName: WorkingGroupModuleName, runtimeId: WorkerId | number, - relations: string[] = [] + relations: RelationsArr = [] ): Promise { const workerDbId = `${groupName}-${runtimeId}` - const worker = await store.get(Worker, { where: { id: workerDbId }, relations }) - if (!worker) { - return inconsistentState(`Expected worker not found by id ${workerDbId}`) - } - - return worker + return getByIdOrFail(store, Worker, workerDbId, relations) } type EntityClass = { @@ -277,15 +263,93 @@ export async function getById( entityClass: EntityClass, id: string, relations?: RelationsArr +): Promise { + return store.get(entityClass, { where: { id }, relations } as FindOneOptions) +} + +export async function getByIdOrFail( + store: DatabaseManager, + entityClass: EntityClass, + id: string, + relations?: RelationsArr, + errMessage?: string +): Promise { + const result = await getById(store, entityClass, id, relations) + if (!result) { + // eslint-disable-next-line local-rules/no-throw + throw new Error(`Expected "${entityClass.name}" not found by ID: ${id} ${errMessage ? `- ${errMessage}` : ''}`) + } + + return result +} + +export async function getOneBy( + store: DatabaseManager, + entityClass: EntityClass, + where?: FindOptionsWhere, + relations?: RelationsArr, + order?: Partial<{ [K in keyof T]: FindOptionsOrderValue }> +): Promise { + return store.get(entityClass, { where, relations, order } as FindOneOptions) +} + +/** + * Retrieves a entity by any field(s) or throws an error if not found + */ +export async function getOneByOrFail( + store: DatabaseManager, + entityClass: EntityClass, + where?: FindOptionsWhere, + relations?: RelationsArr, + order?: Partial<{ [K in keyof T]: FindOptionsOrderValue }>, + errMessage?: string ): Promise { - const result = await store.get(entityClass, { where: { id }, relations } as FindOneOptions) + const result = await getOneBy(store, entityClass, where, relations, order) if (!result) { - throw new Error(`Expected ${entityClass.name} not found by ID: ${id}`) + // eslint-disable-next-line local-rules/no-throw + throw new Error( + `Expected "${entityClass.name}" not found by filter: ${JSON.stringify({ + where, + relations, + order, + })} ${errMessage ? `- ${errMessage}` : ''}` + ) } return result } +export async function getManyBy( + store: DatabaseManager, + entityClass: EntityClass, + entityIds: string[], + where?: FindOptionsWhere, + relations?: RelationsArr +): Promise { + return store.getMany(entityClass, { where: { id: In(entityIds), ...where }, relations } as FindOneOptions) +} + +export async function getManyByOrFail( + store: DatabaseManager, + entityClass: EntityClass, + entityIds: string[], + where?: FindOptionsWhere, + relations?: RelationsArr, + errMessage?: string +): Promise { + const entities = await getManyBy(store, entityClass, entityIds, where, relations) + const loadedEntityIds = entities.map((item) => item.id) + if (loadedEntityIds.length !== entityIds.length) { + const missingIds = entityIds.filter((item) => !loadedEntityIds.includes(item)) + + // eslint-disable-next-line local-rules/no-throw + throw new Error( + `"${entityClass.name}" missing records for following IDs: ${missingIds} ${errMessage ? `- ${errMessage}` : ''}` + ) + } + return entities +} + export function deterministicEntityId(createdInEvent: SubstrateEvent, additionalIdentifier?: string | number): string { return ( `${createdInEvent.blockNumber}-${createdInEvent.indexInBlock}` + @@ -332,10 +396,10 @@ export async function saveMetaprotocolTransactionSuccessful( export async function saveMetaprotocolTransactionErrored( store: DatabaseManager, event: SubstrateEvent, - message: string + error: MetaprotocolTxError ): Promise { const status = new MetaprotocolTransactionErrored() - status.message = message + status.message = error const metaprotocolTransaction = new MetaprotocolTransactionStatusEvent({ ...genericEventFields(event), @@ -344,3 +408,30 @@ export async function saveMetaprotocolTransactionErrored( await store.save(metaprotocolTransaction) } + +export enum MetaprotocolTxError { + InvalidMetadata = 'InvalidMetadata', + + // App errors + AppAlreadyExists = 'AppAlreadyExists', + AppNotFound = 'AppNotFound', + InvalidAppOwnerMember = 'InvalidAppOwnerMember', + + // Video errors + VideoNotFound = 'VideoNotFound', + VideoNotFoundInChannel = 'VideoNotFoundInChannel', + VideoReactionsDisabled = 'VideoReactionsDisabled', + + // Comment errors + CommentSectionDisabled = 'CommentSectionDisabled', + CommentNotFound = 'CommentNotFound', + ParentCommentNotFound = 'ParentCommentNotFound', + InvalidCommentAuthor = 'InvalidCommentAuthor', + + // Membership error + MemberNotFound = 'MemberNotFound', + MemberBannedFromChannel = 'MemberBannedFromChannel', + + // Channel errors + InvalidChannelRewardAccount = 'InvalidChannelRewardAccount', +} diff --git a/query-node/mappings/src/content/app.ts b/query-node/mappings/src/content/app.ts index 6fc4b2c064..38aff1e309 100644 --- a/query-node/mappings/src/content/app.ts +++ b/query-node/mappings/src/content/app.ts @@ -3,32 +3,28 @@ import { ICreateApp, IUpdateApp } from '@joystream/metadata-protobuf' import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' import { integrateMeta } from '@joystream/metadata-protobuf/utils' import { App, Membership } from 'query-node/dist/model' -import { logger, unexpectedData } from '../common' +import { MetaprotocolTxError, getById, getOneBy, logger } from '../common' export async function processCreateAppMessage( store: DatabaseManager, event: SubstrateEvent, metadata: DecodedMetadataObject, - memberId: string -): Promise { + member: Membership +): Promise { const { name, appMetadata } = metadata const appId = `${event.blockNumber}-${event.indexInBlock}` - const isAppExists = await store.get(App, { - where: { - name: metadata.name, - }, - }) + const isAppExists = await getOneBy(store, App, { name }) if (isAppExists) { - unexpectedData('App already exists', { name }) + return MetaprotocolTxError.AppAlreadyExists } const newApp = new App({ name, id: appId, - ownerMember: new Membership({ id: memberId }), + ownerMember: member, websiteUrl: appMetadata?.websiteUrl || undefined, useUri: appMetadata?.useUri || undefined, smallIcon: appMetadata?.smallIcon || undefined, @@ -43,23 +39,25 @@ export async function processCreateAppMessage( }) await store.save(newApp) logger.info('App has been created', { name }) + + return newApp } export async function processUpdateAppMessage( store: DatabaseManager, metadata: DecodedMetadataObject, - memberId: string -): Promise { + member: Membership +): Promise { const { appId, appMetadata } = metadata - const app = await getAppById(store, appId) + const app = await getById(store, App, appId, ['ownerMember']) if (!app) { - unexpectedData("App doesn't exists", { appId }) + return MetaprotocolTxError.AppNotFound } - if (app.ownerMember.id !== memberId) { - unexpectedData("App doesn't belong to the member", { appId, memberId }) + if (app.ownerMember.id !== member.id) { + return MetaprotocolTxError.InvalidAppOwnerMember } if (appMetadata) { @@ -80,15 +78,5 @@ export async function processUpdateAppMessage( await store.save(app) logger.info('App has been updated', { appId }) -} - -export async function getAppById(store: DatabaseManager, appId: string): Promise { - const app = await store.get(App, { - where: { - id: appId, - }, - relations: ['ownerMember'], - }) - return app } diff --git a/query-node/mappings/src/content/channel.ts b/query-node/mappings/src/content/channel.ts index 2f974f00a3..dd73c967e0 100644 --- a/query-node/mappings/src/content/channel.ts +++ b/query-node/mappings/src/content/channel.ts @@ -9,7 +9,7 @@ import { ChannelOwnerRemarked, IMakeChannelPayment, } from '@joystream/metadata-protobuf' -import { ChannelId, DataObjectId, MemberId } from '@joystream/types/primitives' +import { DataObjectId } from '@joystream/types/primitives' import { BTreeMap, BTreeSet, u64 } from '@polkadot/types' import { Channel, @@ -23,6 +23,7 @@ import { ChannelRewardClaimedEvent, ChannelVisibilitySetByModeratorEvent, Collaborator, + Comment, ContentActor, ContentActorCurator, ContentActorMember, @@ -33,22 +34,21 @@ import { MetaprotocolTransactionSuccessful, PaymentContextChannel, PaymentContextVideo, - StorageBag, StorageDataObject, Video, } from 'query-node/dist/model' import { FindOptionsWhere, In } from 'typeorm' import { + MetaprotocolTxError, bytesToString, deserializeMetadata, genericEventFields, - getMemberById, - inconsistentState, + getOneBy, invalidMetadata, logger, saveMetaprotocolTransactionErrored, saveMetaprotocolTransactionSuccessful, - unexpectedData, + unimplementedError, unwrap, } from '../common' import { @@ -60,6 +60,7 @@ import { import { convertChannelOwnerToMemberOrCuratorGroup, convertContentActor, + getChannelOrFail, mapAgentPermission, processAppActionMetadata, processChannelMetadata, @@ -114,12 +115,6 @@ export async function content_ChannelCreated(ctx: EventContext & StoreContext): // deserialize & process metadata if (channelCreationParameters.meta.isSome) { - const storageBag = await store.get(StorageBag, { where: { id: `dynamic:channel:${channelId.toString()}` } }) - - if (!storageBag) { - inconsistentState(`storageBag for channel ${channelId} does not exist`) - } - const appAction = deserializeMetadata(AppAction, channelCreationParameters.meta.unwrap(), { skipWarning: true }) if (appAction) { @@ -165,26 +160,13 @@ export async function content_ChannelUpdated(ctx: EventContext & StoreContext): const [, channelId, channelUpdateParameters, newDataObjects] = new ChannelUpdatedEvent_V1001(event).params // load channel - const channel = await store.get(Channel, { - where: { id: channelId.toString() }, - }) - - // ensure channel exists - if (!channel) { - return inconsistentState('Non-existing channel update requested', channelId) - } + const channel = await getChannelOrFail(store, channelId.toString()) // prepare changed metadata const newMetadataBytes = channelUpdateParameters.newMeta.unwrapOr(null) // update metadata if it was changed if (newMetadataBytes) { - const storageBag = await store.get(StorageBag, { where: { id: `dynamic:channel:${channelId.toString()}` } }) - - if (!storageBag) { - inconsistentState(`storageBag for channel ${channelId} does not exist`) - } - const newMetadata = deserializeMetadata(AppAction, newMetadataBytes, { skipWarning: true }) if (newMetadata) { @@ -280,14 +262,7 @@ export async function content_ChannelVisibilitySetByModerator({ const [actor, channelId, isCensored, rationale] = new ChannelVisibilitySetByModeratorEvent_V1001(event).params // load channel - const channel = await store.get(Channel, { - where: { id: channelId.toString() }, - }) - - // ensure channel exists - if (!channel) { - return inconsistentState('Non-existing channel censoring requested', channelId) - } + const channel = await getChannelOrFail(store, channelId.toString()) // update channel channel.isCensored = isCensored.isTrue @@ -317,15 +292,7 @@ export async function content_ChannelOwnerRemarked(ctx: EventContext & StoreCont const [channelId, message] = new ChannelOwnerRemarkedEvent_V1001(ctx.event).params // load channel - const channel = await store.get(Channel, { - where: { id: channelId.toString() }, - relations: ['ownerMember', 'ownerCuratorGroup'], - }) - - // ensure channel exists - if (!channel) { - return inconsistentState('Owner Remarked for Non-existing channel', channelId) - } + const channel = await getChannelOrFail(store, channelId.toString(), ['ownerMember', 'ownerCuratorGroup']) const getContentActor = (ownerMember?: Membership, ownerCuratorGroup?: CuratorGroup) => { if (ownerMember) { @@ -340,23 +307,18 @@ export async function content_ChannelOwnerRemarked(ctx: EventContext & StoreCont return actor } - return inconsistentState('Unknown content actor', { ownerMember, ownerCuratorGroup }) + return unimplementedError('Unsupported content actor type') } - try { - const decodedMessage = ChannelOwnerRemarked.decode(message.toU8a(true)) - const contentActor = getContentActor(channel.ownerMember, channel.ownerCuratorGroup) - const metaTransactionInfo = await processOwnerRemark(store, event, channelId, contentActor, decodedMessage) - - await saveMetaprotocolTransactionSuccessful(store, event, metaTransactionInfo) - // emit log event - logger.info('Channel owner remarked', { decodedMessage }) - } catch (e) { - // emit log event - logger.info(`Bad metadata for channel owner's remark`, { e }) + const metadata = ChannelOwnerRemarked.decode(message.toU8a(true)) + const contentActor = getContentActor(channel.ownerMember, channel.ownerCuratorGroup) + const metaprotocolTxResult = await processOwnerRemark(store, event, channel, contentActor, metadata) - // save metaprotocol info - await saveMetaprotocolTransactionErrored(store, event, `Bad metadata for channel's owner`) + if (metaprotocolTxResult instanceof MetaprotocolTransactionSuccessful) { + await saveMetaprotocolTransactionSuccessful(store, event, metaprotocolTxResult) + } else { + await saveMetaprotocolTransactionErrored(store, event, metaprotocolTxResult) + invalidMetadata(`Bad metadata for channel owner's remark:`, { metadata, metaprotocolTxResult }) } } @@ -364,21 +326,18 @@ export async function content_ChannelAgentRemarked(ctx: EventContext & StoreCont const { event, store } = ctx const [moderator, channelId, message] = new ChannelAgentRemarkedEvent_V1001(ctx.event).params - try { - const decodedMessage = ChannelModeratorRemarked.decode(message.toU8a(true)) - const contentActor = await convertContentActor(store, moderator) - - const metaTransactionInfo = await processModeratorRemark(store, event, channelId, contentActor, decodedMessage) + // load channel + const channel = await getChannelOrFail(store, channelId.toString()) - await saveMetaprotocolTransactionSuccessful(store, event, metaTransactionInfo) - // emit log event - logger.info('Channel moderator remarked', { decodedMessage }) - } catch (e) { - // emit log event - logger.info(`Bad metadata for channel moderator's remark`, { e }) + const metadata = ChannelModeratorRemarked.decode(message.toU8a(true)) + const contentActor = await convertContentActor(store, moderator) + const metaprotocolTxResult = await processModeratorRemark(store, event, channel, contentActor, metadata) - // save metaprotocol info - await saveMetaprotocolTransactionErrored(store, event, `Bad metadata for channel's remark`) + if (metaprotocolTxResult instanceof MetaprotocolTransactionSuccessful) { + await saveMetaprotocolTransactionSuccessful(store, event, metaprotocolTxResult) + } else { + await saveMetaprotocolTransactionErrored(store, event, metaprotocolTxResult) + invalidMetadata(`Bad metadata for channel_agent_remark:`, { metadata, metaprotocolTxResult }) } } @@ -412,71 +371,95 @@ async function updateChannelAgentsPermissions( async function processOwnerRemark( store: DatabaseManager, event: SubstrateEvent, - channelId: ChannelId, + channel: Channel, contentActor: typeof ContentActor, decodedMessage: ChannelOwnerRemarked -): Promise> { - const messageType = decodedMessage.channelOwnerRemarked - - if (messageType === 'pinOrUnpinComment') { - await processPinOrUnpinCommentMessage(store, event, channelId, decodedMessage.pinOrUnpinComment!) +): Promise { + const metaprotocolTransactionSuccessful = new MetaprotocolTransactionSuccessful() + if (decodedMessage.pinOrUnpinComment) { + const commentOrError = await processPinOrUnpinCommentMessage( + store, + event, + channel, + decodedMessage.pinOrUnpinComment! + ) - return {} + if (commentOrError instanceof Comment) { + return metaprotocolTransactionSuccessful + } + return commentOrError } - if (messageType === 'banOrUnbanMemberFromChannel') { - await processBanOrUnbanMemberFromChannelMessage( + if (decodedMessage.banOrUnbanMemberFromChannel) { + const memberOrError = await processBanOrUnbanMemberFromChannelMessage( store, event, - channelId, + channel, decodedMessage.banOrUnbanMemberFromChannel! ) - return {} + if (memberOrError instanceof Membership) { + return metaprotocolTransactionSuccessful + } + return memberOrError } - if (messageType === 'videoReactionsPreference') { - await processVideoReactionsPreferenceMessage(store, event, channelId, decodedMessage.videoReactionsPreference!) + if (decodedMessage.videoReactionsPreference) { + const channelOrError = await processVideoReactionsPreferenceMessage( + store, + event, + channel, + decodedMessage.videoReactionsPreference! + ) - return {} + if (channelOrError instanceof Channel) { + return metaprotocolTransactionSuccessful + } + return channelOrError } - if (messageType === 'moderateComment') { - const comment = await processModerateCommentMessage( + if (decodedMessage.moderateComment) { + const commentOrError = await processModerateCommentMessage( store, event, contentActor, - channelId, + channel, decodedMessage.moderateComment! ) - return { commentModeratedId: comment.id } + + if (commentOrError instanceof Comment) { + return metaprotocolTransactionSuccessful + } + return commentOrError } - return inconsistentState('Unsupported message type in channel owner remark action', messageType) + return MetaprotocolTxError.InvalidMetadata } async function processModeratorRemark( store: DatabaseManager, event: SubstrateEvent, - channelId: ChannelId, + channel: Channel, contentActor: typeof ContentActor, decodedMessage: ChannelModeratorRemarked -): Promise> { - const messageType = decodedMessage.channelModeratorRemarked - - if (messageType === 'moderateComment') { - const comment = await processModerateCommentMessage( +): Promise { + const metaprotocolTransactionSuccessful = new MetaprotocolTransactionSuccessful() + if (decodedMessage.moderateComment) { + const commentOrError = await processModerateCommentMessage( store, event, contentActor, - channelId, + channel, decodedMessage.moderateComment! ) - return { commentModeratedId: comment.id } + if (commentOrError instanceof Comment) { + return metaprotocolTransactionSuccessful + } + return commentOrError } - return inconsistentState('Unsupported message type in moderator remark action', messageType) + return MetaprotocolTxError.InvalidMetadata } export async function content_ChannelPayoutsUpdated({ store, event }: EventContext & StoreContext): Promise { @@ -517,12 +500,7 @@ export async function content_ChannelRewardUpdated({ store, event }: EventContex const [cumulativeRewardEarned, claimedAmount, channelId] = new ChannelRewardUpdatedEvent_V2001(event).params // load channel - const channel = await store.get(Channel, { where: { id: channelId.toString() } }) - - // ensure channel exists - if (!channel) { - return inconsistentState('Non-existing channel reward updated', channelId) - } + const channel = await getChannelOrFail(store, channelId.toString()) // common event processing - second @@ -549,12 +527,7 @@ export async function content_ChannelRewardClaimedAndWithdrawn({ const [owner, channelId, withdrawnAmount, destination] = new ChannelRewardClaimedAndWithdrawnEvent_V1001(event).params // load channel - const channel = await store.get(Channel, { where: { id: channelId.toString() } }) - - // ensure channel exists - if (!channel) { - return inconsistentState('Non-existing channel reward updated', channelId) - } + const channel = await getChannelOrFail(store, channelId.toString()) // common event processing - second @@ -576,20 +549,13 @@ export async function content_ChannelRewardClaimedAndWithdrawn({ } export async function content_ChannelFundsWithdrawn({ store, event }: EventContext & StoreContext): Promise { - // load event data // load event data const [owner, channelId, amount, destination] = new ChannelFundsWithdrawnEvent_V1001(event).params // load channel - const channel = await store.get(Channel, { where: { id: channelId.toString() } }) - - // ensure channel exists - if (!channel) { - return inconsistentState('Non-existing channel reward updated', channelId) - } + const channel = await getChannelOrFail(store, channelId.toString()) // common event processing - second - const rewardClaimedEvent = new ChannelFundsWithdrawnEvent({ ...genericEventFields(event), @@ -605,27 +571,22 @@ export async function content_ChannelFundsWithdrawn({ store, event }: EventConte export async function processChannelPaymentFromMember( store: DatabaseManager, event: SubstrateEvent, - memberId: MemberId, + member: Membership, message: DecodedMetadataObject, [payeeAccount, amount]: [AccountId32, Balance] -): Promise { - const member = await getMemberById(store, memberId) - +): Promise { // Only channel reward accounts are being checked right now as payment destination. // Transfers to any other destination will be ignored by the query node. - const channel = await store.get(Channel, { where: { rewardAccount: payeeAccount.toString() } }) + const channel = await getOneBy(store, Channel, { rewardAccount: payeeAccount.toString() }) if (!channel) { - unexpectedData('Payment made to unknown channel reward account') + return MetaprotocolTxError.InvalidChannelRewardAccount } // Get payment context from the metadata const getPaymentContext = async (msg: DecodedMetadataObject) => { if (msg.videoId) { const paymentContext = new PaymentContextVideo() - const video = await store.get(Video, { - where: { id: msg.videoId.toString(), channel: { id: channel.id } }, - relations: ['channel'], - }) + const video = await getOneBy(store, Video, { id: msg.videoId, channel: { id: channel.id } }, ['channel']) if (!video) { invalidMetadata( `payment context video not found in channel that was queried based on reward (or payee) account.` diff --git a/query-node/mappings/src/content/commentAndReaction.ts b/query-node/mappings/src/content/commentAndReaction.ts index 01fefcdba2..31ac3bed92 100644 --- a/query-node/mappings/src/content/commentAndReaction.ts +++ b/query-node/mappings/src/content/commentAndReaction.ts @@ -15,7 +15,6 @@ import { VideoReactionsPreference, } from '@joystream/metadata-protobuf' import { DecodedMetadataObject } from '@joystream/metadata-protobuf/types' -import { ChannelId, MemberId } from '@joystream/types/primitives' import { Channel, Comment, @@ -38,77 +37,30 @@ import { VideoReactionsCountByReactionType, VideoReactionsPreferenceEvent, } from 'query-node/dist/model' -import { genericEventFields, inconsistentState, newMetaprotocolEntityId, unexpectedData } from '../common' +import { MetaprotocolTxError, RelationsArr, genericEventFields, getById, newMetaprotocolEntityId } from '../common' -// TODO: Ensure video is actually a video -// TODO: make sure comment is fully removed (all of its reactions) -// TODO: make sure video is fully removed (all of its comments & reactions) - -async function getChannel(store: DatabaseManager, channelId: string, relations?: string[]): Promise { - const channel = await store.get(Channel, { where: { id: channelId }, relations }) - if (!channel) { - inconsistentState(`Channel not found by id: ${channelId}`) - } - return channel -} - -async function getVideo(store: DatabaseManager, videoId: string, relations?: string[]): Promise