Skip to content

Commit

Permalink
refactor: Updates endpoints (#1713)
Browse files Browse the repository at this point in the history
* refactor: update routes

* Merge branch 'master' into refactor/update-endpoints

* merge fix
  • Loading branch information
ncomerci authored Mar 18, 2024
1 parent 242b7fd commit fffb1dc
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 115 deletions.
100 changes: 100 additions & 0 deletions src/back/routes/proposal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,35 @@ import { SNAPSHOT_DURATION } from '../../entities/Snapshot/constants'
import { isSameAddress } from '../../entities/Snapshot/utils'
import { validateUniqueAddresses } from '../../entities/Transparency/utils'
import UpdateModel from '../../entities/Updates/model'
import {
FinancialRecord,
FinancialUpdateSectionSchema,
GeneralUpdateSectionSchema,
UpdateGeneralSection,
} from '../../entities/Updates/types'
import {
getCurrentUpdate,
getFundsReleasedSinceLatestUpdate,
getLatestUpdate,
getNextPendingUpdate,
getPendingUpdates,
getPublicUpdates,
getReleases,
} from '../../entities/Updates/utils'
import BidService from '../../services/BidService'
import { DiscourseService } from '../../services/DiscourseService'
import { ErrorService } from '../../services/ErrorService'
import { ProjectService } from '../../services/ProjectService'
import { ProposalInCreation, ProposalService } from '../../services/ProposalService'
import { VestingService } from '../../services/VestingService'
import { getProfile } from '../../utils/Catalyst'
import Time from '../../utils/date/Time'
import { ErrorCategory } from '../../utils/errorCategories'
import { isProdEnv } from '../../utils/governanceEnvs'
import logger from '../../utils/logger'
import { DclNotificationService } from '../services/dcl-notification'
import { NotificationService } from '../services/notification'
import { UpdateService } from '../services/update'
import { validateAddress, validateProposalId } from '../utils/validations'

export default routes((route) => {
Expand All @@ -119,6 +136,8 @@ export default routes((route) => {
route.patch('/proposals/:proposal', withAuth, handleAPI(updateProposalStatus))
route.delete('/proposals/:proposal', withAuth, handleAPI(removeProposal))
route.get('/proposals/:proposal/comments', handleAPI(getProposalComments))
route.get('/proposals/:proposal/updates', handleAPI(getProposalUpdates))
route.post('/proposals/:proposal/update', withAuth, handleAPI(createProposalUpdate))
route.get('/proposals/linked-wearables/image', handleAPI(checkImage))
})

Expand Down Expand Up @@ -683,3 +702,84 @@ async function checkImage(req: Request) {
})
})
}

async function getProposalUpdates(req: Request<{ proposal_id: string }>) {
const proposal_id = req.params.proposal_id

if (!proposal_id) {
throw new RequestError(`Proposal not found: "${proposal_id}"`, RequestError.NotFound)
}

const updates = await UpdateService.getAllByProposalId(proposal_id)
const publicUpdates = getPublicUpdates(updates)
const nextUpdate = getNextPendingUpdate(updates)
const currentUpdate = getCurrentUpdate(updates)
const pendingUpdates = getPendingUpdates(updates)

return {
publicUpdates,
pendingUpdates,
nextUpdate,
currentUpdate,
}
}

function parseFinancialRecords(financial_records: unknown) {
const parsedResult = FinancialUpdateSectionSchema.safeParse({ financial_records })
if (!parsedResult.success) {
ErrorService.report('Submission of invalid financial records', {
error: parsedResult.error,
category: ErrorCategory.Financial,
})
throw new RequestError(`Invalid financial records`, RequestError.BadRequest)
}
return parsedResult.data.financial_records
}

async function validateFinancialRecords(
proposal: ProposalAttributes,
financial_records: unknown
): Promise<FinancialRecord[] | null> {
const [vestingData, updates] = await Promise.all([
VestingService.getVestingInfo(proposal.vesting_addresses),
UpdateService.getAllByProposalId(proposal.id),
])

const releases = vestingData ? getReleases(vestingData) : undefined
const publicUpdates = getPublicUpdates(updates)
const latestUpdate = getLatestUpdate(publicUpdates || [])
const { releasedFunds } = getFundsReleasedSinceLatestUpdate(latestUpdate, releases)
return releasedFunds > 0 ? parseFinancialRecords(financial_records) : null
}

async function createProposalUpdate(req: WithAuth<Request<{ proposal: string }>>) {
const { author, financial_records, ...body } = req.body
const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate<UpdateGeneralSection>(
schema.compile(GeneralUpdateSectionSchema),
body
)
try {
const proposal = await getProposal(req)
const financialRecords = await validateFinancialRecords(proposal, financial_records)
return await UpdateService.create(
{
proposal_id: req.params.proposal,
author,
health,
introduction,
highlights,
blockers,
next_steps,
additional_notes,
financial_records: financialRecords,
},
req.auth!
)
} catch (error) {
ErrorService.report('Error creating update', {
error,
category: ErrorCategory.Update,
})
throw new RequestError(`Something went wrong: ${error}`, RequestError.InternalServerError)
}
}
119 changes: 11 additions & 108 deletions src/back/routes/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,44 +9,29 @@ import { Request } from 'express'
import ProposalModel from '../../entities/Proposal/model'
import { ProposalAttributes } from '../../entities/Proposal/types'
import {
FinancialRecord,
FinancialUpdateSectionSchema,
GeneralUpdateSectionSchema,
UpdateGeneralSection,
} from '../../entities/Updates/types'
import {
getCurrentUpdate,
getFundsReleasedSinceLatestUpdate,
getLatestUpdate,
getNextPendingUpdate,
getPendingUpdates,
getPublicUpdates,
getReleases,
isBetweenLateThresholdDate,
} from '../../entities/Updates/utils'
import { isBetweenLateThresholdDate } from '../../entities/Updates/utils'
import { DiscourseService } from '../../services/DiscourseService'
import { ErrorService } from '../../services/ErrorService'
import { FinancialService } from '../../services/FinancialService'
import { VestingService } from '../../services/VestingService'
import Time from '../../utils/date/Time'
import { ErrorCategory } from '../../utils/errorCategories'
import { CoauthorService } from '../services/coauthor'
import { UpdateService } from '../services/update'

import { getProposal } from './proposal'

export default routes((route) => {
const withAuth = auth()
route.get('/proposals/:proposal/updates', handleAPI(getProposalUpdates))
route.get('/proposals/:update/update', handleAPI(getProposalUpdate))
route.get('/proposals/:update_id/update/comments', handleAPI(getProposalUpdateComments))
route.post('/proposals/:proposal/update', withAuth, handleAPI(createProposalUpdate))
route.patch('/proposals/:proposal/update', withAuth, handleAPI(updateProposalUpdate))
route.delete('/proposals/:update_id/update', withAuth, handleAPI(deleteProposalUpdate))
route.get('/updates/:update_id', handleAPI(getProposalUpdate))
route.patch('/updates/:update_id', withAuth, handleAPI(updateProposalUpdate))
route.delete('/updates/:update_id', withAuth, handleAPI(deleteProposalUpdate))
route.get('/updates/:update_id/comments', handleAPI(getProposalUpdateComments))
})

async function getProposalUpdate(req: Request<{ update: string }>) {
const id = req.params.update
async function getProposalUpdate(req: Request<{ update_id: string }>) {
const id = req.params.update_id

if (!id) {
throw new RequestError(`Missing id`, RequestError.NotFound)
Expand All @@ -61,27 +46,6 @@ async function getProposalUpdate(req: Request<{ update: string }>) {
return update
}

async function getProposalUpdates(req: Request<{ proposal: string }>) {
const proposal_id = req.params.proposal

if (!proposal_id) {
throw new RequestError(`Proposal not found: "${proposal_id}"`, RequestError.NotFound)
}

const updates = await UpdateService.getAllByProposalId(proposal_id)
const publicUpdates = getPublicUpdates(updates)
const nextUpdate = getNextPendingUpdate(updates)
const currentUpdate = getCurrentUpdate(updates)
const pendingUpdates = getPendingUpdates(updates)

return {
publicUpdates,
pendingUpdates,
nextUpdate,
currentUpdate,
}
}

async function getProposalUpdateComments(req: Request<{ update_id: string }>) {
const update = await UpdateService.getById(req.params.update_id)
if (!update) {
Expand Down Expand Up @@ -110,90 +74,29 @@ async function getProposalUpdateComments(req: Request<{ update_id: string }>) {
}

const generalSectionValidator = schema.compile(GeneralUpdateSectionSchema)

function parseFinancialRecords(financial_records: unknown) {
const parsedResult = FinancialUpdateSectionSchema.safeParse({ financial_records })
if (!parsedResult.success) {
ErrorService.report('Submission of invalid financial records', {
error: parsedResult.error,
category: ErrorCategory.Financial,
})
throw new RequestError(`Invalid financial records`, RequestError.BadRequest)
}
return parsedResult.data.financial_records
}

async function validateFinancialRecords(
proposal: ProposalAttributes,
financial_records: unknown
): Promise<FinancialRecord[] | null> {
const [vestingData, updates] = await Promise.all([
VestingService.getVestingInfo(proposal.vesting_addresses),
UpdateService.getAllByProposalId(proposal.id),
])

const releases = vestingData ? getReleases(vestingData) : undefined
const publicUpdates = getPublicUpdates(updates)
const latestUpdate = getLatestUpdate(publicUpdates || [])
const { releasedFunds } = getFundsReleasedSinceLatestUpdate(latestUpdate, releases)
return releasedFunds > 0 ? parseFinancialRecords(financial_records) : null
}

async function createProposalUpdate(req: WithAuth<Request<{ proposal: string }>>) {
async function updateProposalUpdate(req: WithAuth<Request<{ update_id: string }>>) {
const id = req.params.update_id
const { author, financial_records, ...body } = req.body
const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate<UpdateGeneralSection>(
generalSectionValidator,
body
)
try {
const proposal = await getProposal(req)
const financialRecords = await validateFinancialRecords(proposal, financial_records)
return await UpdateService.create(
{
proposal_id: req.params.proposal,
author,
health,
introduction,
highlights,
blockers,
next_steps,
additional_notes,
financial_records: financialRecords,
},
req.auth!
)
} catch (error) {
ErrorService.report('Error creating update', {
error,
category: ErrorCategory.Update,
})
throw new RequestError(`Something wnt wrong: ${error}`, RequestError.InternalServerError)
}
}

async function updateProposalUpdate(req: WithAuth<Request<{ proposal: string }>>) {
const { id, author, financial_records, ...body } = req.body
const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate<UpdateGeneralSection>(
generalSectionValidator,
body
)
const parsedResult = FinancialUpdateSectionSchema.safeParse({ financial_records })
if (!parsedResult.success) {
throw new RequestError(`Invalid financial records`, RequestError.BadRequest, parsedResult.error)
}
const parsedRecords = parsedResult.data.financial_records
const update = await UpdateService.getById(id)
const proposalId = req.params.proposal

if (!update) {
throw new RequestError(`Update not found: "${id}"`, RequestError.NotFound)
}

const user = req.auth

const proposal = await ProposalModel.findOne<ProposalAttributes>({ id: req.params.proposal })
const proposal = await ProposalModel.findOne<ProposalAttributes>({ id: update.proposal_id })
const isAuthorOrCoauthor =
user && (proposal?.user === user || (await CoauthorService.isCoauthor(proposalId, user))) && author === user
user && (proposal?.user === user || (await CoauthorService.isCoauthor(update.proposal_id, user))) && author === user

if (!proposal || !isAuthorOrCoauthor) {
throw new RequestError(`Unauthorized`, RequestError.Forbidden)
Expand Down
10 changes: 5 additions & 5 deletions src/clients/Governance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ export class Governance extends API {
}

async getProposalUpdate(update_id: string) {
return await this.fetchApiResponse<UpdateAttributes>(`/proposals/${update_id}/update`)
return await this.fetchApiResponse<UpdateAttributes>(`/updates/${update_id}`)
}

async getProposalUpdates(proposal_id: string) {
Expand All @@ -269,18 +269,18 @@ export class Governance extends API {
}

async updateProposalUpdate(
proposal_id: string,
update_id: string,
update: UpdateSubmissionDetails & UpdateGeneralSection & UpdateFinancialSection
) {
return await this.fetchApiResponse<UpdateAttributes>(`/proposals/${proposal_id}/update`, {
return await this.fetchApiResponse<UpdateAttributes>(`/updates/${update_id}`, {
method: 'PATCH',
sign: true,
json: update,
})
}

async deleteProposalUpdate(update_id: UpdateAttributes['id']) {
return await this.fetchApiResponse<UpdateAttributes>(`/proposals/${update_id}/update`, {
return await this.fetchApiResponse<UpdateAttributes>(`/updates/${update_id}`, {
method: 'DELETE',
sign: true,
})
Expand Down Expand Up @@ -559,7 +559,7 @@ export class Governance extends API {
}

async getUpdateComments(update_id: string) {
return await this.fetchApiResponse<ProposalCommentsInDiscourse>(`/proposals/${update_id}/update/comments`)
return await this.fetchApiResponse<ProposalCommentsInDiscourse>(`/updates/${update_id}/comments`)
}

async airdropBadge(badgeSpecCid: string, recipients: string[]) {
Expand Down
1 change: 1 addition & 0 deletions src/components/Proposal/Update/ProposalUpdate.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
}

.ProposalUpdate__Description--expanded {
word-break: break-word;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box !important;
Expand Down
2 changes: 1 addition & 1 deletion src/pages/submit/update.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export default function Update({ isEdit }: Props) {

try {
if (updateId) {
await Governance.get().updateProposalUpdate(proposalId, { id: updateId, ...newUpdate })
await Governance.get().updateProposalUpdate(updateId, newUpdate)
if (isEdit) {
setIsEditModalOpen(false)
}
Expand Down
13 changes: 12 additions & 1 deletion static/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ paths:
responses:
'200':
$ref: '#/components/responses/200'
/proposals/{update}/update:
/updates/{update}:
get:
tags:
- Grant Updates
Expand All @@ -224,6 +224,17 @@ paths:
responses:
'200':
$ref: '#/components/responses/200'
/updates/{update}/comments:
get:
tags:
- Grant Updates
summary: Get the update comments from the given update ID
parameters:
- name: update
$ref: '#/components/parameters/updateId'
responses:
'200':
$ref: '#/components/responses/200'

/committee:
get:
Expand Down

0 comments on commit fffb1dc

Please sign in to comment.