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 : add badges #9

Open
wants to merge 39 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a4862e4
feat: add API endpoint to return a cached file
wa0x6e Apr 19, 2023
fff3ae7
feat: compress response
wa0x6e Apr 19, 2023
f1dedbd
feat: define precise error codes
wa0x6e Apr 19, 2023
e7174fa
chore: rename file to reflect its purpose
wa0x6e Apr 19, 2023
a757ee6
fix: fix missing new line in CSV file
wa0x6e Apr 19, 2023
ed221dc
fix: fix choices columns for votes with multiple choices
wa0x6e Apr 19, 2023
1a8150b
refactor: avoid passing `proposal` object
wa0x6e Apr 19, 2023
2c0fef4
fix: use different name for incomplete file
wa0x6e Apr 19, 2023
474b250
fix: prevent race condition on file generation
wa0x6e Apr 19, 2023
fd8a3c9
Merge branch 'main' into add-api
wa0x6e Apr 19, 2023
07fe282
feat: use AWS as cached file storage
wa0x6e Apr 20, 2023
399c8d4
feat: separate cache fetcher and generator in API endpoint
wa0x6e Apr 20, 2023
f7e8809
fix: do not return an error code on cache generation success
wa0x6e Apr 21, 2023
dbe874b
feat: add additional file storage engine
wa0x6e Apr 21, 2023
8d78400
chore: update README
wa0x6e Apr 21, 2023
a523f2a
fix: use inferred env variables for S3 client setup
wa0x6e Apr 21, 2023
57cb712
feat: custimize location of cached files
wa0x6e Apr 21, 2023
c7bd2cd
feat: protect generate endpoint behind authentication
wa0x6e Apr 21, 2023
cc31a4d
chore: add LICENCE file
wa0x6e Apr 22, 2023
d83e00a
feat: add webhook support for `generate` endpoint
wa0x6e Apr 22, 2023
8d0a8ed
feat: add basic queue system to handle cache file generation
wa0x6e Apr 22, 2023
e18cc3b
chore: update README
wa0x6e Apr 22, 2023
3eae769
chore: add tests
wa0x6e Apr 22, 2023
d0c7733
chore: fix github worflow
wa0x6e Apr 22, 2023
dee6171
fix: fix folder creation
wa0x6e Apr 22, 2023
9821586
chore: rename github workflow
wa0x6e Apr 22, 2023
fbdefa3
chore: update README
wa0x6e Apr 22, 2023
519a499
chorel; add test for weighted votes
wa0x6e Apr 22, 2023
9a36731
feat: add reason to votes report
wa0x6e Apr 22, 2023
9c91d29
chore: fix false positives in linter
wa0x6e Apr 24, 2023
3a391de
chore: dependencies upgrade
wa0x6e Apr 24, 2023
7b5404c
chore: fix CI job name
wa0x6e Apr 24, 2023
57e4400
chore: typecheck task should not also build
wa0x6e Apr 24, 2023
d2343d0
refactor: always throw Error objects instead of literal
wa0x6e Apr 25, 2023
0c4f55b
feat: add badges
wa0x6e May 2, 2023
1e0c899
fix: use extension instead of query params to specify the content type
wa0x6e May 2, 2023
d842ca7
fix: remove badge PNG format support
wa0x6e May 2, 2023
c0fe646
Merge branch 'main' into fix-7
wa0x6e May 5, 2023
d2b8af8
chore: fix tests
wa0x6e May 5, 2023
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
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@
"test": "jest"
},
"eslintConfig": {
"extends": "@snapshot-labs"
"extends": "@snapshot-labs",
"rules": {
"no-throw-literal": "error"
}
},
"engines": {
"node": ">= 18"
Expand All @@ -23,6 +26,7 @@
"dependencies": {
"@apollo/client": "^3.7.12",
"@aws-sdk/client-s3": "^3.316.0",
"badge-maker": "^3.3.1",
"compression": "^1.7.4",
"cors": "^2.8.5",
"express": "^4.18.2",
Expand Down
14 changes: 14 additions & 0 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import express from 'express';
import { rpcError, rpcSuccess, storageEngine } from './helpers/utils';
import log from './helpers/log';
import { queues } from './lib/queue';
import { BadgeType, getBadge } from './lib/badge';
import { name, version } from '../package.json';
import VotesReport from './lib/votesReport';

Expand Down Expand Up @@ -74,4 +75,17 @@ router.post('/votes/:id', async (req, res) => {
}
});

router.get('/badges/:type(space|proposal)/:id', async (req, res) => {
const { id, type } = req.params;

try {
res.setHeader('Content-Type', 'image/svg+xml');
res.setHeader('Cache-Control', 'public, max-age=18000, must-revalidate');
return res.end(await getBadge(type as BadgeType, id, req.query));
} catch (e) {
log.error(e);
return rpcError(res, 'INTERNAL_ERROR', id);
}
});

export default router;
55 changes: 55 additions & 0 deletions src/helpers/snapshot.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { gql, ApolloClient, InMemoryCache, HttpLink } from '@apollo/client/core';

export type Space = {
id: string;
name: string;
about?: string;
followersCount?: number;
};
export type Proposal = {
id: string;
state: string;
votes: number;
choices: string[];
};
export type Vote = {
Expand Down Expand Up @@ -32,6 +39,27 @@ const PROPOSAL_QUERY = gql`
id
state
choices

votes
}
}
`;

const SPACE_PROPOSALS_QUERY = gql`
query Proposals($id: String!, $state: String) {
proposals(where: { space: $id, state: $state }) {
id
}
}
`;

const SPACE_QUERY = gql`
query Space($id: String) {
space(id: $id) {
id
name
about
followersCount
}
}
`;
Expand Down Expand Up @@ -75,6 +103,20 @@ export async function fetchProposal(id: string) {
return proposal;
}

export async function fetchProposals(id: string, state?: string) {
const {
data: { proposals }
}: { data: { proposals: Proposal[] } } = await client.query({
query: SPACE_PROPOSALS_QUERY,
variables: {
id,
state
}
});

return proposals;
}

export async function fetchVotes(
id: string,
{ first = 1000, skip = 0, orderBy = 'created_gte', orderDirection = 'asc', created_gte = 0 } = {}
Expand All @@ -95,3 +137,16 @@ export async function fetchVotes(

return votes;
}

export async function fetchSpace(id: string) {
const {
data: { space }
}: { data: { space: Space | null } } = await client.query({
query: SPACE_QUERY,
variables: {
id
}
});

return space;
}
80 changes: 80 additions & 0 deletions src/lib/badge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { makeBadge } from 'badge-maker';
import { fetchSpace, fetchProposal, fetchProposals, Space, Proposal } from '../helpers/snapshot';
import type { Request } from 'express';

type State = 'pending' | 'active' | 'closed';
export type BadgeType = 'space' | 'proposal';
const STATE_COLORS: Record<State, string> = {
pending: 'rgb(107, 114, 128)',
active: 'yellowgreen',
closed: '#384aff'
};

export async function getBadge(type: BadgeType, id: string, query: Request['query']) {
switch (type) {
case 'space':
return await getSpaceBadge(id, query);
break;
case 'proposal':
return await getProposalBadge(id);
break;
default:
throw new Error('Invalid badge type');
}
}

async function getSpaceBadge(id: string, query: Request['query']) {
const space = await fetchSpace(id);

if (!space) {
throw new Error('Space not found');
}

if (query.field === 'followersCount') {
return getSpaceFollowersCountBadge(space);
} else {
return await getSpaceProposalsCountBadge(space, (query.state || 'active') as State);
}
}

async function getProposalBadge(id: string) {
const proposal = await fetchProposal(id);

if (!proposal) {
throw new Error('PROPOSAL_NOT_FOUND');
}

return getProposalVotesCountBadge(proposal);
}

function getSpaceFollowersCountBadge(space: Space) {
const format = {
label: 'members',
message: space.followersCount?.toLocaleString('en-US').toString() || '0',
color: STATE_COLORS.active
};

return makeBadge(format);
}

async function getSpaceProposalsCountBadge(space: Space, state: State) {
const proposals = await fetchProposals(space.id, state);

const format = {
label: 'proposals',
message: proposals?.length.toLocaleString('en-US').toString() || '0',
color: state ? STATE_COLORS[state] : STATE_COLORS.active
};

return makeBadge(format);
}

function getProposalVotesCountBadge(proposal: Proposal) {
const format = {
label: 'votes',
message: proposal.votes.toLocaleString('en-US').toString() || '0',
color: STATE_COLORS.active
};

return makeBadge(format);
}
6 changes: 4 additions & 2 deletions test/unit/lib/votesReport.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ describe('VotesReport', () => {
});

describe('canBeCached()', () => {
const mockedBaseProposal = { id: '', votes: 0, choices: [] };

it('raises an error when the proposal does not exist', () => {
const report = new VotesReport('test', storageEngine);
const votesReportSpy = jest.spyOn(report, 'fetchProposal').mockResolvedValueOnce(null);
Expand All @@ -57,7 +59,7 @@ describe('VotesReport', () => {
const report = new VotesReport(id, storageEngine);
const votesReportSpy = jest
.spyOn(report, 'fetchProposal')
.mockResolvedValueOnce({ state: 'pending', id: '', choices: [] });
.mockResolvedValueOnce({ state: 'pending', ...mockedBaseProposal });

expect(report.canBeCached()).rejects.toBe('PROPOSAL_NOT_CLOSED');
expect(votesReportSpy).toHaveBeenCalled();
Expand All @@ -67,7 +69,7 @@ describe('VotesReport', () => {
const report = new VotesReport(id, storageEngine);
const votesReportSpy = jest
.spyOn(report, 'fetchProposal')
.mockResolvedValueOnce({ state: 'closed', id: '', choices: [] });
.mockResolvedValueOnce({ state: 'closed', ...mockedBaseProposal });

expect(await report.canBeCached()).toBe(true);
expect(votesReportSpy).toHaveBeenCalled();
Expand Down
48 changes: 47 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1999,6 +1999,13 @@ ajv@^6.10.0, ajv@^6.12.4:
json-schema-traverse "^0.4.1"
uri-js "^4.2.2"

[email protected]:
version "2.0.0"
resolved "https://registry.yarnpkg.com/anafanafo/-/anafanafo-2.0.0.tgz#43f56274680bc553dd67a9625a920f88d0057b5c"
integrity sha512-Nlfq7NC4AOkTJerWRIZcOAiMNtIDVIGWGvQ98O7Jl6Kr2Dk0dX5u4MqN778kSRTy5KRqchpLdF2RtLFEz9FVkQ==
dependencies:
char-width-table-consumer "^1.0.0"

ansi-escapes@^4.2.1:
version "4.3.2"
resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e"
Expand Down Expand Up @@ -2130,6 +2137,14 @@ babel-preset-jest@^29.5.0:
babel-plugin-jest-hoist "^29.5.0"
babel-preset-current-node-syntax "^1.0.0"

badge-maker@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/badge-maker/-/badge-maker-3.3.1.tgz#df1cb2d5943f25740672f37a95598d9ba2b109c9"
integrity sha512-OO/PS7Zg2E6qaUWzHEHt21Q5VjcFBAJVA8ztgT/fIdSZFBUwoyeo0ZhA6V5tUM8Vcjq8DJl6jfGhpjESssyqMQ==
dependencies:
anafanafo "2.0.0"
css-color-converter "^2.0.0"

balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
Expand All @@ -2140,6 +2155,11 @@ binary-extensions@^2.0.0:
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==

binary-search@^1.3.5:
version "1.3.6"
resolved "https://registry.yarnpkg.com/binary-search/-/binary-search-1.3.6.tgz#e32426016a0c5092f0f3598836a1c7da3560565c"
integrity sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==

[email protected]:
version "1.20.1"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
Expand Down Expand Up @@ -2267,6 +2287,13 @@ char-regex@^1.0.2:
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==

char-width-table-consumer@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/char-width-table-consumer/-/char-width-table-consumer-1.0.0.tgz#bb44ccd1ba3ed4fcdb062e22876721858a7697a8"
integrity sha512-Fz4UD0LBpxPgL9i29CJ5O4KANwaMnX/OhhbxzvNa332h+9+nRKyeuLw4wA51lt/ex67+/AdsoBQJF3kgX2feYQ==
dependencies:
binary-search "^1.3.5"

chokidar@^3.5.2:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
Expand Down Expand Up @@ -2311,6 +2338,11 @@ collect-v8-coverage@^1.0.0:
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==

color-convert@^0.5.2:
version "0.5.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-0.5.3.tgz#bdb6c69ce660fadffe0b0007cc447e1b9f7282bd"
integrity sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==

color-convert@^1.9.0, color-convert@^1.9.3:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
Expand All @@ -2330,7 +2362,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==

color-name@^1.0.0, color-name@~1.1.4:
color-name@^1.0.0, color-name@^1.1.4, color-name@~1.1.4:
version "1.1.4"
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
Expand Down Expand Up @@ -2438,6 +2470,20 @@ cross-spawn@^7.0.2, cross-spawn@^7.0.3:
shebang-command "^2.0.0"
which "^2.0.1"

css-color-converter@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/css-color-converter/-/css-color-converter-2.0.0.tgz#70c00fa451a19675e2808f28de9be360c84db5fb"
integrity sha512-oLIG2soZz3wcC3aAl/7Us5RS8Hvvc6I8G8LniF/qfMmrm7fIKQ8RIDDRZeKyGL2SrWfNqYspuLShbnjBMVWm8g==
dependencies:
color-convert "^0.5.2"
color-name "^1.1.4"
css-unit-converter "^1.1.2"

css-unit-converter@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/css-unit-converter/-/css-unit-converter-1.1.2.tgz#4c77f5a1954e6dbff60695ecb214e3270436ab21"
integrity sha512-IiJwMC8rdZE0+xiEZHeru6YoONC4rfPMqGm2W85jMIbkFvv5nFTwJVFHam2eFrN6txmoUYFAFXiv8ICVeTO0MA==

[email protected]:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
Expand Down