Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: projects view #1785

Merged
merged 41 commits into from
Jun 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
1e601e9
chore: project entities (#1783)
1emu May 7, 2024
dd5556a
chore: create projects per passed grants and bids proposals (#1784)
1emu May 7, 2024
acfa4e5
chore: add basic project info to proposal data on proposals endpoint …
1emu May 8, 2024
2a05f1c
chore: rename enums (#1788)
1emu May 9, 2024
bac0ebc
chore: add project endpoint (#1789)
1emu May 10, 2024
0cf9c87
feat: get projects query (#1795)
ncomerci May 16, 2024
36baca8
feat: add address to team member schema (#1799)
andyesp May 17, 2024
745f285
feat: validate milestones and add them to snapshot message (#1797)
andyesp May 20, 2024
ee59ada
refactor: rename description field to tasks in milestone (#1802)
andyesp May 20, 2024
3c69ab4
chore: removed console log
ncomerci May 21, 2024
b229e76
chore: create project personnel (#1796)
1emu May 21, 2024
d0ae508
fix: remove personnel status from projects query, (#1804)
1emu May 21, 2024
c40b813
feat: use abstract as about in project (#1805)
andyesp May 21, 2024
fdaf9a4
feat: validate milestones on bid request (#1803)
andyesp May 21, 2024
6ea384c
fix: team address not required in schema (#1807)
andyesp May 21, 2024
31b66e6
feat: create project milestones from proposal data (#1808)
andyesp May 27, 2024
68c74b0
feat: add milestones to project query (#1810)
andyesp May 27, 2024
1f6c118
chore: add personnel (#1814)
1emu May 28, 2024
5b8b309
chore: delete personnel (#1815)
1emu May 28, 2024
60172f9
chore: create project links (#1818)
1emu May 29, 2024
97c824e
feat: project id in response (#1816)
ncomerci May 29, 2024
fbd14cf
fix: migration error (#1820)
andyesp May 29, 2024
183dd60
fix: project query improvements (#1822)
1emu May 30, 2024
b88603e
chore: rename updates table (#1824)
ncomerci May 31, 2024
3ebadc6
chore: project links (#1823)
1emu Jun 3, 2024
9480f6d
feat: add and delete milestone endpoints (#1829)
andyesp Jun 5, 2024
78b89b0
feat: milestones submit limit in proposals request (#1831)
andyesp Jun 5, 2024
fad9378
chore: Projects migration (#1826)
ncomerci Jun 5, 2024
959be0f
chore: update project status on proposal enactment (#1828)
1emu Jun 5, 2024
27641c5
feat: add roadmap field validation (#1833)
andyesp Jun 5, 2024
ad05617
chore: update project status with vesting status on fetch/enactment (…
1emu Jun 6, 2024
41dadfc
refactor: Updates (#1835)
ncomerci Jun 6, 2024
1a09db6
fix: use updated_at as created_at for projects (#1841)
ncomerci Jun 7, 2024
50a7bbe
feat: add proposal link as project link (#1839)
andyesp Jun 7, 2024
05a1f16
Chore: add vesting to project (#1838)
1emu Jun 10, 2024
4b7c4f0
refactor: rename updates method (#1843)
1emu Jun 10, 2024
959f2dd
chore: merge master into projects view (#1847)
andyesp Jun 11, 2024
f59f06c
Merge branch 'master' into feat/projects-view
andyesp Jun 11, 2024
6131d0d
feat: Not editable project status (#1848)
ncomerci Jun 11, 2024
d383d19
fix: migrations and remove links creation on migration/project create…
andyesp Jun 12, 2024
788c19d
fix: revert text replace (#1856)
1emu Jun 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"build": "npm run build:server",
"build:server": "tsc -p .",
"format": "prettier --write \"**/*.{js,jsx,json,md,ts,tsx}\"",
"start": "concurrently -c blue,green -n SERVER,FRONT 'npm run serve' 'npm run develop'",
"start": "concurrently -c blue -n SERVER 'npm run serve'",
"serve": "DOTENV_CONFIG_PATH=.env.development nodemon --watch src/entities --watch src/back --watch src/clients --watch src/services --watch src/server.ts --watch static/api.yaml -e ts,json --exec 'ts-node -r dotenv/config' src/server",
"version": "tsc -p .",
"migrate": "DOTENV_CONFIG_PATH=.env.development ts-node -r dotenv/config.js ./node_modules/node-pg-migrate/bin/node-pg-migrate -j ts -m src/migrations -d CONNECTION_STRING",
Expand Down
10 changes: 10 additions & 0 deletions src/back/jobs/ProjectsMigration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ProjectService } from '../../services/ProjectService'

export async function migrateProjects() {
const result = await ProjectService.migrateProjects()
if (result.error || result.migrationErrors.length > 0) {
throw JSON.stringify(result)
}

return result
}
18 changes: 18 additions & 0 deletions src/back/models/Personnel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Model } from 'decentraland-gatsby/dist/entities/Database/model'

import { TeamMember } from '../../entities/Grant/types'

export type PersonnelAttributes = TeamMember & {
id: string
project_id: string
deleted: boolean
updated_by?: string
updated_at?: Date
created_at: Date
}

export default class PersonnelModel extends Model<PersonnelAttributes> {
static tableName = 'personnel'
static withTimestamps = false
static primaryKey = 'id'
}
115 changes: 115 additions & 0 deletions src/back/models/Project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import crypto from 'crypto'
import { Model } from 'decentraland-gatsby/dist/entities/Database/model'
import { SQL, join, table } from 'decentraland-gatsby/dist/entities/Database/utils'
import isEthereumAddress from 'validator/lib/isEthereumAddress'
import isUUID from 'validator/lib/isUUID'

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, ProposalProject } from '../../entities/Proposal/types'

import PersonnelModel, { PersonnelAttributes } from './Personnel'
import ProjectLinkModel, { ProjectLink } from './ProjectLink'
import ProjectMilestoneModel, { ProjectMilestone } from './ProjectMilestone'

export type ProjectAttributes = {
id: string
proposal_id: string
title: string
status: ProjectStatus
about?: string
about_updated_by?: string
about_updated_at?: Date
updated_at?: Date
created_at: Date
}

export type Project = ProjectAttributes & {
personnel: PersonnelAttributes[]
links: ProjectLink[]
milestones: ProjectMilestone[]
author: string
coauthors: string[] | null
vesting_addresses: string[]
funding?: ProjectFunding
}

export default class ProjectModel extends Model<ProjectAttributes> {
static tableName = 'projects'
static withTimestamps = false
static primaryKey = 'id'

static async getProject(id: string) {
if (!isUUID(id || '')) {
throw new Error(`Invalid project id: "${id}"`)
}

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(co.address) FILTER (WHERE co.address IS NOT NULL), '{}') AS coauthors
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}
WHERE pr.id = ${id}
GROUP BY pr.id, p.user, p.vesting_addresses;
`

const result = await this.namedQuery<Project>(`get_project`, query)
if (!result || result.length === 0) {
throw new Error(`Project not found: "${id}"`)
}

return result[0]
}

static async migrate(proposals: ProposalProject[]) {
const values = proposals.map(
({ id, title, about, status, updated_at }) =>
SQL`(${crypto.randomUUID()}, ${id}, ${title}, ${about}, ${status}, ${new Date(updated_at)})`
)

const query = SQL`
INSERT INTO ${table(ProjectModel)} (id, proposal_id, title, about, status, created_at)
VALUES ${join(values, SQL`, `)}
RETURNING *;
`

return this.namedQuery<ProjectAttributes>(`create_multiple_projects`, query)
}

static async isAuthorOrCoauthor(user: string, projectId: string): Promise<boolean> {
if (!isUUID(projectId || '')) {
throw new Error(`Invalid project id: "${projectId}"`)
}
if (!isEthereumAddress(user)) {
throw new Error(`Invalid user: "${user}"`)
}

const query = SQL`
SELECT EXISTS (
SELECT 1
FROM ${table(ProjectModel)} pr
JOIN ${table(ProposalModel)} p ON pr.proposal_id = p.id
LEFT JOIN ${table(CoauthorModel)} co ON pr.proposal_id = co.proposal_id
AND co.status = ${CoauthorStatus.APPROVED}
WHERE pr.id = ${projectId}
AND (p.user = ${user} OR co.address = ${user})
) AS "exists"
`

const result = await this.namedQuery<{ exists: boolean }>(`is_author_or_coauthor`, query)
return result[0]?.exists || false
}
}
18 changes: 18 additions & 0 deletions src/back/models/ProjectLink.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Model } from 'decentraland-gatsby/dist/entities/Database/model'

export type ProjectLink = {
id: string
project_id: string
label: string
url: string
updated_by?: string
updated_at?: Date
created_by: string
created_at: Date
}

export default class ProjectLinkModel extends Model<ProjectLink> {
static tableName = 'project_links'
static withTimestamps = false
static primaryKey = 'id'
}
26 changes: 26 additions & 0 deletions src/back/models/ProjectMilestone.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Model } from 'decentraland-gatsby/dist/entities/Database/model'

export type ProjectMilestone = {
id: string
project_id: string
title: string
description: string
delivery_date: Date
status: ProjectMilestoneStatus
updated_by?: string
updated_at?: Date
created_by: string
created_at: Date
}

export enum ProjectMilestoneStatus {
Pending = 'pending',
InProgress = 'in_progress',
Done = 'done',
}

export default class ProjectMilestoneModel extends Model<ProjectMilestone> {
static tableName = 'project_milestones'
static withTimestamps = false
static primaryKey = 'id'
}
16 changes: 16 additions & 0 deletions src/back/models/ProjectMilestoneUpdate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Model } from 'decentraland-gatsby/dist/entities/Database/model'

export type ProjectMilestoneUpdate = {
id: string
update_id: string
milestone_id: string
description: string
tasks: string[]
created_at: Date
}

export default class ProjectMilestoneUpdateModel extends Model<ProjectMilestoneUpdate> {
static tableName = 'project_milestone_updates'
static withTimestamps = false
static primaryKey = 'id'
}
4 changes: 2 additions & 2 deletions src/back/routes/budget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Budget, BudgetWithContestants, CategoryBudget } from '../../entities/Bu
import { QuarterBudgetAttributes } from '../../entities/QuarterBudget/types'
import { toNewGrantCategory } from '../../entities/QuarterCategoryBudget/utils'
import { BudgetService } from '../../services/BudgetService'
import { validateProposalId } from '../utils/validations'
import { validateId } from '../utils/validations'

export default routes((route) => {
const withAuth = auth()
Expand Down Expand Up @@ -48,6 +48,6 @@ async function getCurrentContestedBudget(): Promise<BudgetWithContestants> {
}

async function getBudgetWithContestants(req: Request<{ proposal: string }>): Promise<BudgetWithContestants> {
const id = validateProposalId(req.params.proposal)
const id = validateId(req.params.proposal)
return await BudgetService.getBudgetWithContestants(id)
}
2 changes: 2 additions & 0 deletions src/back/routes/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { DEBUG_ADDRESSES } from '../../constants'
import CacheService from '../../services/CacheService'
import { ErrorService } from '../../services/ErrorService'
import { giveAndRevokeLandOwnerBadges, giveTopVoterBadges, runQueuedAirdropJobs } from '../jobs/BadgeAirdrop'
import { migrateProjects } from '../jobs/ProjectsMigration'
import { validateDebugAddress } from '../utils/validations'

const FUNCTIONS_MAP: { [key: string]: () => Promise<unknown> } = {
runQueuedAirdropJobs,
giveAndRevokeLandOwnerBadges,
giveTopVoterBadges,
migrateProjects,
}

export default routes((router) => {
Expand Down
4 changes: 2 additions & 2 deletions src/back/routes/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { EventsService } from '../services/events'
import {
validateDebugAddress,
validateEventTypesFilters,
validateProposalId,
validateId,
validateRequiredString,
} from '../utils/validations'

Expand All @@ -26,7 +26,7 @@ async function getLatestEvents(req: Request) {
async function voted(req: WithAuth) {
const user = req.auth!

validateProposalId(req.body.proposalId)
validateId(req.body.proposalId)
validateRequiredString('proposalTitle', req.body.proposalTitle)
validateRequiredString('choice', req.body.choice)
return await EventsService.voted(req.body.proposalId, req.body.proposalTitle, req.body.choice, user)
Expand Down
Loading
Loading