Skip to content

Commit

Permalink
chore: updated projects endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
1emu committed Aug 8, 2024
1 parent 7db165f commit c930b31
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 10 deletions.
43 changes: 42 additions & 1 deletion src/models/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import CoauthorModel from '../entities/Coauthor/model'
import { CoauthorStatus } from '../entities/Coauthor/types'
import { ProjectStatus } from '../entities/Grant/types'
import ProposalModel from '../entities/Proposal/model'
import { ProjectFunding } from '../entities/Proposal/types'
import { ProjectFunding, ProposalAttributes } from '../entities/Proposal/types'
import UpdateModel from '../entities/Updates/model'
import { UpdateAttributes } from '../entities/Updates/types'

import PersonnelModel, { PersonnelAttributes } from './Personnel'
import ProjectLinkModel, { ProjectLink } from './ProjectLink'
Expand All @@ -33,8 +35,14 @@ export type Project = ProjectAttributes & {
coauthors: string[] | null
vesting_addresses: string[]
funding?: ProjectFunding
updates?: UpdateAttributes
}

export type ProjectQueryResult = Project &
Pick<ProposalAttributes, 'enacting_tx' | 'enacted_description'> & {
proposal_updated_at: string
}

export default class ProjectModel extends Model<ProjectAttributes> {
static tableName = 'projects'
static withTimestamps = false
Expand Down Expand Up @@ -96,4 +104,37 @@ export default class ProjectModel extends Model<ProjectAttributes> {
const result = await this.namedQuery<{ exists: boolean }>(`is_author_or_coauthor`, query)
return result[0]?.exists || false
}

static async getProjectsWithUpdates(): Promise<ProjectQueryResult[]> {
const query = SQL`
SELECT
pr.*,
p.user as author,
p.vesting_addresses as vesting_addresses,
COALESCE(json_agg(DISTINCT to_jsonb(pe.*)) FILTER (WHERE pe.id IS NOT NULL), '[]') as personnel,
COALESCE(json_agg(DISTINCT to_jsonb(mi.*)) FILTER (WHERE mi.id IS NOT NULL), '[]') as milestones,
COALESCE(json_agg(DISTINCT to_jsonb(li.*)) FILTER (WHERE li.id IS NOT NULL), '[]') as links,
COALESCE(array_agg(DISTINCT co.address) FILTER (WHERE co.address IS NOT NULL), '{}') AS coauthors,
COALESCE(json_agg(DISTINCT to_jsonb(ordered_updates.*)) FILTER
(WHERE ordered_updates.id IS NOT NULL), '[]') as updates,
p.enacting_tx,
p.enacted_description,
p.updated_at as proposal_updated_at
FROM ${table(ProjectModel)} pr
JOIN ${table(ProposalModel)} p ON pr.proposal_id = p.id
LEFT JOIN ${table(PersonnelModel)} pe ON pr.id = pe.project_id AND pe.deleted = false
LEFT JOIN ${table(ProjectMilestoneModel)} mi ON pr.id = mi.project_id
LEFT JOIN ${table(ProjectLinkModel)} li ON pr.id = li.project_id
LEFT JOIN ${table(CoauthorModel)} co ON pr.proposal_id = co.proposal_id
AND co.status = ${CoauthorStatus.APPROVED}
LEFT JOIN (SELECT * FROM ${table(
UpdateModel
)} up ORDER BY up.created_at DESC) ordered_updates ON pr.id = ordered_updates.project_id
GROUP BY pr.id, p.user, p.vesting_addresses, p.enacting_tx, p.enacted_description, proposal_updated_at;
`

const result = await this.namedQuery<ProjectQueryResult>(`get_projects`, query)
return result || []
}
}
13 changes: 13 additions & 0 deletions src/routes/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@ import {
} from '../entities/Project/types'
import { ProposalProjectWithUpdate } from '../entities/Proposal/types'
import PersonnelModel, { PersonnelAttributes } from '../models/Personnel'
import { Project } from '../models/Project'
import ProjectLinkModel, { ProjectLink } from '../models/ProjectLink'
import ProjectMilestoneModel, { ProjectMilestone } from '../models/ProjectMilestone'
import CacheService, { TTL_1_HS } from '../services/CacheService'
import { ErrorService } from '../services/ErrorService'
import { ProjectService } from '../services/ProjectService'
import { ErrorCategory } from '../utils/errorCategories'
import { isValidDate, validateCanEditProject, validateId } from '../utils/validations'

export default routes((route) => {
const withAuth = auth()
route.get('/projects', handleJSON(getProjects))
route.get('/projects/updated/', handleJSON(getUpdatedProjects))
route.post('/projects/personnel/', withAuth, handleAPI(addPersonnel))
route.delete('/projects/personnel/:personnel_id', withAuth, handleAPI(deletePersonnel))
route.post('/projects/links/', withAuth, handleAPI(addLink))
Expand Down Expand Up @@ -60,6 +64,15 @@ async function getProjects(req: Request) {
return { data: filterProjectsByDate(projects, from, to) }
}

async function getUpdatedProjects(): Promise<Project[]> {
try {
return await ProjectService.getUpdatedProjects()
} catch (error) {
ErrorService.report('Error fetching projets', { error, category: ErrorCategory.Project })
throw new RequestError(`Unable to load projects`, RequestError.InternalServerError)
}
}

async function getProject(req: Request<{ project: string }>) {
const id = validateId(req.params.project)
try {
Expand Down
46 changes: 40 additions & 6 deletions src/services/ProjectService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import crypto from 'crypto'

import { TransparencyVesting } from '../clients/Transparency'
import { VestingWithLogs } from '../clients/VestingData'
import UnpublishedBidModel from '../entities/Bid/model'
import { BidProposalConfiguration } from '../entities/Bid/types'
import { GrantTier } from '../entities/Grant/GrantTier'
Expand All @@ -22,14 +23,19 @@ import { IndexedUpdate, UpdateAttributes } from '../entities/Updates/types'
import { getPublicUpdates } from '../entities/Updates/utils'
import { formatError, inBackground } from '../helpers'
import PersonnelModel, { PersonnelAttributes } from '../models/Personnel'
import ProjectModel, { Project, ProjectAttributes } from '../models/Project'
import ProjectModel, { Project, ProjectAttributes, ProjectQueryResult } from '../models/Project'
import ProjectLinkModel, { ProjectLink } from '../models/ProjectLink'
import ProjectMilestoneModel, { ProjectMilestone, ProjectMilestoneStatus } from '../models/ProjectMilestone'
import Time from '../utils/date/Time'
import { ErrorCategory } from '../utils/errorCategories'
import { isProdEnv } from '../utils/governanceEnvs'
import logger from '../utils/logger'
import { createProposalProject, toGovernanceProjectStatus } from '../utils/projects'
import {
createProposalProject,
getProjectFunding,
getProjectStatus,
toGovernanceProjectStatus,
} from '../utils/projects'

import { BudgetService } from './BudgetService'
import { ErrorService } from './ErrorService'
Expand All @@ -47,7 +53,7 @@ export class ProjectService {
public static async getProposalProjects() {
const proposalWithProjects = await ProposalModel.getProjectList()
const vestings = await VestingService.getAllVestings()
const projects: ProposalProjectWithUpdate[] = []
const proposalProjects: ProposalProjectWithUpdate[] = []

await Promise.all(
proposalWithProjects.map(async (proposal) => {
Expand All @@ -57,7 +63,7 @@ export class ProjectService {
proposalVestings.find(
(vesting) =>
vesting.vesting_status === VestingStatus.InProgress || vesting.vesting_status === VestingStatus.Finished
) || proposalVestings[0] //TODO: replace transparency vestings for vestings subgraph
) || proposalVestings[0]
const project = createProposalProject(proposal, prioritizedVesting)

try {
Expand All @@ -67,7 +73,7 @@ export class ProjectService {
...this.getUpdateData(update),
}

return projects.push(projectWithUpdate)
return proposalProjects.push(projectWithUpdate)
} catch (error) {
logger.error(`Failed to fetch grant update data from proposal ${project.id}`, formatError(error as Error))
}
Expand All @@ -78,8 +84,36 @@ export class ProjectService {
}
})
)
return proposalProjects
}

public static async getUpdatedProjects() {
const projectsQueryResults = await ProjectModel.getProjectsWithUpdates()
const latestVestingAddresses = projectsQueryResults.map(
(project) => project.vesting_addresses[project.vesting_addresses.length - 1]
)
const latestVestings = await VestingService.getVestings(latestVestingAddresses)
const updatedProjects = this.mergeProjectsWithVestings(projectsQueryResults, latestVestings)
return updatedProjects
}

return projects
private static mergeProjectsWithVestings(
projects: ProjectQueryResult[],
latestVestings: VestingWithLogs[]
): Project[] {
return (
projects.map((project) => {
const latestVestingAddress = project.vesting_addresses[project.vesting_addresses.length - 1]
const vestingWithLogs = latestVestings.find((vesting) => vesting.address === latestVestingAddress)
const funding = getProjectFunding(project, vestingWithLogs)
const status = getProjectStatus(project, vestingWithLogs)
return {
...project,
status,
funding,
}
}) || []
)
}

private static getUpdateData(update: (UpdateAttributes & { index: number }) | null) {
Expand Down
43 changes: 40 additions & 3 deletions src/utils/projects.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TransparencyVesting } from '../clients/Transparency'
import { Vesting } from '../clients/VestingData'
import { Vesting, VestingWithLogs } from '../clients/VestingData'
import { ProjectStatus, VestingStatus } from '../entities/Grant/types'
import { ProjectFunding, ProposalAttributes, ProposalProject, ProposalWithProject } from '../entities/Proposal/types'
import { CLIFF_PERIOD_IN_DAYS } from '../entities/Proposal/utils'
import { ProjectQueryResult } from '../models/Project'

import Time from './date/Time'

Expand Down Expand Up @@ -46,7 +47,43 @@ function getFunding(proposal: ProposalAttributes, transparencyVesting?: Transpar
}
}

function getProjectStatus(proposal: ProposalAttributes, vesting?: TransparencyVesting) {
export function getProjectFunding(project: ProjectQueryResult, vesting: VestingWithLogs | undefined): ProjectFunding {
if (project.enacting_tx) {
// one time payment
return {
enacted_at: project.proposal_updated_at,
one_time_payment: {
enacting_tx: project.enacting_tx,
},
}
}

if (!vesting) {
return {}
}

return {
enacted_at: vesting.start_at,
vesting,
}
}

export function getProjectStatus(project: ProjectQueryResult, vesting: VestingWithLogs | undefined) {
const legacyCondition = !vesting && project.enacted_description
if (project.enacting_tx || legacyCondition) {
return ProjectStatus.Finished
}

if (!vesting) {
return ProjectStatus.Pending
}

const { status } = vesting

return toGovernanceProjectStatus(status)
}

function getProposalProjectStatus(proposal: ProposalAttributes, vesting?: TransparencyVesting) {
const legacyCondition = !vesting && proposal.enacted_description
if (proposal.enacting_tx || legacyCondition) {
return ProjectStatus.Finished
Expand All @@ -63,7 +100,7 @@ function getProjectStatus(proposal: ProposalAttributes, vesting?: TransparencyVe

export function createProposalProject(proposal: ProposalWithProject, vesting?: TransparencyVesting): ProposalProject {
const funding = getFunding(proposal, vesting)
const status = getProjectStatus(proposal, vesting)
const status = getProposalProjectStatus(proposal, vesting)

return {
id: proposal.id,
Expand Down

0 comments on commit c930b31

Please sign in to comment.