Skip to content

Commit

Permalink
feat: 🎨 add graphql bounoty api
Browse files Browse the repository at this point in the history
  • Loading branch information
Ignazio Bovo committed Jun 22, 2024
1 parent 9def129 commit 0690d27
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 12 deletions.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ services:

volumes:
orion_db_data:
# networks:
# default:
# external:
# name: joystream_default

networks:
default:
external:
name: joystream_default
21 changes: 17 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@
"type-graphql": "^1.2.0-rc.1",
"typeorm": "^0.3.11",
"ua-parser-js": "^1.0.34",
"url-join": "^4"
"url-join": "^4",
"uuid": "^10.0.0"
},
"devDependencies": {
"@apollo/client": "^3.7.17",
Expand All @@ -105,8 +106,8 @@
"@subsquid/substrate-typegen": "^2.1.0",
"@subsquid/typeorm-codegen": "0.3.1",
"@types/async-lock": "^1.1.3",
"@types/chai": "^4.3.11",
"@types/big-json": "^3.2.4",
"@types/chai": "^4.3.11",
"@types/cookie-parser": "^1.4.3",
"@types/express-rate-limit": "^6.0.0",
"@types/mocha": "^10.0.1",
Expand Down
2 changes: 2 additions & 0 deletions schema/bounties.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ type Bounty @entity @schema(name: "admin") {
}

type ActivatedBounty @entity {
id: ID!
bounty: Bounty!
channelOwner: Channel!
key: String
completed: Boolean!
}
174 changes: 174 additions & 0 deletions src/server-extension/resolvers/BountyResolver/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import 'reflect-metadata'
import { Resolver, UseMiddleware, Ctx, Args, Arg, Mutation, Query } from 'type-graphql'
import { EntityManager } from 'typeorm'
import { AccountOnly, OperatorOnly } from '../middleware'
import { withHiddenEntities } from '../../../utils/sql'
import { ActivatedBounty, Bounty, Channel } from '../../../model'
import {
ActivatedBountyOperationResult,
AddActivatedBountyArgs,
AddBountyArgs,
BountyOperationResult,
GetActivatedBountiesForChannel,
} from './types'
import { v4 as uuidv4 } from 'uuid'
import { Context } from '../../check'

@Resolver()
export class BountyResolver {
constructor(private em: () => Promise<EntityManager>) {}

@Mutation(() => BountyOperationResult)
@UseMiddleware(OperatorOnly())
async addBounty(@Args() params: AddBountyArgs): Promise<BountyOperationResult> {
const em = await this.em()
return withHiddenEntities(em, async () => {
const id = uuidv4()
const bounty = new Bounty({
id,
createdAt: new Date(),
expirationDate: params.expirationDate,
maxPayoutUSD: params.maxPayoutUSD,
title: params.title,
description: params.description,
coverImageLink: params.coverImageLink,
talkingPointsText: params.talkingPointsText,
})
await em.save(bounty)

return { bountyId: id }
})
}

// add a new resolver in order to add a activated bounty entity to the database
@Mutation(() => ActivatedBountyOperationResult)
@UseMiddleware(AccountOnly)
async addActivatedBounty(
@Args() params: AddActivatedBountyArgs,
@Ctx() ctx: Context
): Promise<ActivatedBountyOperationResult> {
const em = await this.em()
return withHiddenEntities(em, async () => {
await this.ensureChannelBelongsToContextAccount(em, params.channelOwnerId, ctx)

const bounty = await em.getRepository(Bounty).findOne({ where: { id: params.bountyId } })
if (!bounty) {
throw new Error(`Bounty with id ${params.bountyId} not found`)
}
const id = uuidv4()
const activatedBounty = new ActivatedBounty({
id,
bountyId: bounty.id,
channelOwnerId: params.channelOwnerId,
completed: false,
})
bounty.activatedBounties.push(activatedBounty)

await em.save(activatedBounty)
await em.save(bounty)

return { bountyId: id }
})
}

// add a resolver in order to remove an activated bounty
@Mutation(() => ActivatedBountyOperationResult)
@UseMiddleware(AccountOnly)
async removeActivatedBounty(
@Arg('id') id: string,
@Ctx() ctx: Context
): Promise<ActivatedBountyOperationResult> {
const em = await this.em()
return withHiddenEntities(em, async () => {
const activatedBounty = await em.getRepository(ActivatedBounty).findOne({ where: { id } })
if (!activatedBounty) {
throw new Error(`Activated bounty with id ${id} not found`)
}
await this.ensureActivatedBountyBelongsToContextAccount(em, activatedBounty, ctx)

const bounty = await em
.getRepository(Bounty)
.findOne({ where: { id: activatedBounty.bountyId! } })
if (!bounty) {
throw new Error(`Bounty for activated bounty ${activatedBounty.bountyId} not found`)
}

bounty.activatedBounties = bounty.activatedBounties.filter((ab) => ab.id !== id)

await em.remove(activatedBounty)
await em.save(bounty)

return { bountyId: id }
})
}

// mark activated bounty as completed
@Mutation(() => ActivatedBountyOperationResult)
@UseMiddleware(OperatorOnly())
async markActivatedBountyAsCompleted(
@Arg('id') id: string
): Promise<ActivatedBountyOperationResult> {
const em = await this.em()
return withHiddenEntities(em, async () => {
const bounty = await em.getRepository(ActivatedBounty).findOne({ where: { id } })
if (!bounty) {
throw new Error(`Activated bounty with id ${id} not found`)
}

bounty.completed = true

await em.save(bounty)

return { bountyId: id }
})
}

// get all bounties for a specific channel
@Query(() => [GetActivatedBountiesForChannel])
@UseMiddleware(AccountOnly)
async getBountiesForChannel(
@Arg('channelId') channelId: string,
@Ctx() ctx: Context
): Promise<GetActivatedBountiesForChannel[]> {
const em = await this.em()
return withHiddenEntities(em, async () => {
await this.ensureChannelBelongsToContextAccount(em, channelId, ctx)

const bountiesResult: any = await em
.createQueryBuilder(Bounty, 'bounty')
.innerJoin(ActivatedBounty, 'activatedBounty', 'bounty.id = activatedBounty.bountyId')
.addSelect('activatedBounty.completed', 'completed') // Select ActivatedBounty.completed in addition to all Bounty properties
.getMany()

return bountiesResult.map((bounty: any) => ({
maxPayoutsUSD: bounty.maxPayoutUSD,
title: bounty.title,
description: bounty.description,
coverImageLink: bounty.coverImageLink,
expirationDate: bounty.expirationDate,
talkingPointsText: bounty.talkingPointsText,
completed: bounty.activatedBounties.completed,
}))
})
}

async ensureActivatedBountyBelongsToContextAccount(
em: EntityManager,
activatedBounty: ActivatedBounty,
ctx: Context
) {
await this.ensureChannelBelongsToContextAccount(em, activatedBounty.channelOwnerId!, ctx)
}

async ensureChannelBelongsToContextAccount(em: EntityManager, channelId: string, ctx: Context) {
const channel = await em.getRepository(Channel).findOne({ where: { id: channelId } })
if (!channel) {
throw new Error(`Channel with id ${channelId} not found`)
}
if (channel.ownerMemberId !== ctx.account?.membershipId) {
throw new Error(
`Channel with id ${channelId} does not belong to account with id ${ctx.accountId}`
)
}
}
}
71 changes: 71 additions & 0 deletions src/server-extension/resolvers/BountyResolver/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { ArgsType, Field, ObjectType, InputType, Int, registerEnumType } from 'type-graphql'
import { DateTime } from '@subsquid/graphql-server'

@ArgsType()
export class AddBountyArgs {
@Field(() => Int, { nullable: false })
maxPayoutUSD: number

@Field(() => String, { nullable: false })
title: string

@Field(() => String, { nullable: false })
description: string

@Field(() => String, { nullable: true })
coverImageLink: string

@Field(() => DateTime, { nullable: false })
expirationDate: Date

@Field(() => String, { nullable: true })
talkingPointsText: string
}

@ObjectType()
export class BountyOperationResult {
@Field(() => [String], { nullable: false })
bountyId!: string
}

@ArgsType()
export class AddActivatedBountyArgs {
@Field(() => String, { nullable: false })
bountyId: string

@Field(() => String, { nullable: false })
channelOwnerId: string

@Field(() => String, { nullable: false })
key: string
}

@ObjectType()
export class ActivatedBountyOperationResult {
@Field(() => [String], { nullable: false })
bountyId!: string
}

@ObjectType()
export class GetActivatedBountiesForChannel {
@Field(() => Int, { nullable: false })
maxPayoutUSD: number

@Field(() => String, { nullable: false })
title: string

@Field(() => String, { nullable: false })
description: string

@Field(() => String, { nullable: true })
coverImageLink: string

@Field(() => DateTime, { nullable: false })
expirationDate: Date

@Field(() => String, { nullable: true })
talkingPointsText: string

@Field(() => Boolean, { nullable: false })
completed: boolean
}

0 comments on commit 0690d27

Please sign in to comment.