From aba65fa87ab4d22eed75e1e8521f9830056599c4 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Fri, 14 Apr 2023 19:31:27 +0400 Subject: [PATCH 1/4] chore: improve typescript coverage --- package.json | 10 +- src/graphql/helpers.ts | 58 +++++----- src/graphql/index.ts | 1 + src/graphql/operations/aliases.ts | 3 +- src/graphql/operations/follows.ts | 3 +- src/graphql/operations/messages.ts | 3 +- src/graphql/operations/networks.ts | 4 +- src/graphql/operations/plugins.ts | 2 +- src/graphql/operations/proposal.ts | 4 +- src/graphql/operations/proposals.ts | 5 +- src/graphql/operations/skins.ts | 2 +- src/graphql/operations/space.ts | 4 +- src/graphql/operations/spaces.ts | 3 +- src/graphql/operations/strategy.ts | 2 +- src/graphql/operations/subscriptions.ts | 3 +- src/graphql/operations/user.ts | 3 +- src/graphql/operations/users.ts | 3 +- src/graphql/operations/validations.ts | 2 +- src/graphql/operations/vote.ts | 4 +- src/graphql/operations/votes.ts | 9 +- src/graphql/operations/vp.ts | 5 +- src/helpers/alias.ts | 2 +- src/helpers/ee.ts | 2 +- src/helpers/mysql.ts | 8 +- src/helpers/rateLimit.ts | 7 +- src/helpers/spaces.ts | 34 ++++-- src/helpers/strategies.ts | 9 +- src/helpers/utils.ts | 9 +- src/types.ts | 46 ++++++++ tsconfig.json | 8 +- yarn.lock | 134 +++++++++++++++++++++++- 31 files changed, 307 insertions(+), 85 deletions(-) create mode 100644 src/types.ts diff --git a/package.json b/package.json index 89373be8..2e7b90bc 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,15 @@ "devDependencies": { "@snapshot-labs/eslint-config": "^0.1.0-beta.7", "@snapshot-labs/prettier-config": "^0.1.0-beta.7", - "@types/node": "^14.0.13", + "@types/bluebird": "^3.5.38", + "@types/cors": "^2.8.13", + "@types/express": "^4.17.17", + "@types/express-rate-limit": "^6.0.0", + "@types/graphql-depth-limit": "^1.1.3", + "@types/graphql-fields": "^1.3.5", + "@types/lodash": "^4.14.194", + "@types/mysql": "^2.15.21", + "@types/node": "^18.15.11", "eslint": "^8.28.0", "nodemon": "^2.0.19", "prettier": "^2.8.0" diff --git a/src/graphql/helpers.ts b/src/graphql/helpers.ts index 7ed6291c..1c9f918e 100644 --- a/src/graphql/helpers.ts +++ b/src/graphql/helpers.ts @@ -3,11 +3,15 @@ import { jsonParse } from '../helpers/utils'; import { spaceProposals, spaceFollowers } from '../helpers/spaces'; import db from '../helpers/mysql'; +import type { Strategy, QueryArgs } from '../types'; + +type QueryFields = { [key: string]: string }; + const network = process.env.NETWORK || 'testnet'; export class PublicError extends Error {} -const ARG_LIMITS = { +const ARG_LIMITS: { [key: string]: { [key: string]: number } } = { default: { first: 1000, skip: 5000 @@ -20,7 +24,7 @@ const ARG_LIMITS = { } }; -export function checkLimits(args: any = {}, type) { +export function checkLimits(args: QueryArgs, type: string) { const { where = {} } = args; const typeLimits = { ...ARG_LIMITS.default, ...(ARG_LIMITS[type] || {}) }; @@ -35,7 +39,7 @@ export function checkLimits(args: any = {}, type) { return true; } -export function formatSpace(id, settings) { +export function formatSpace(id: string, settings: string) { const space = jsonParse(settings, {}); space.id = id; space.private = space.private || false; @@ -61,7 +65,7 @@ export function formatSpace(id, settings) { space.proposalsCount = spaceProposals[id]?.count || 0; space.voting.hideAbstain = space.voting.hideAbstain || false; space.voteValidation = space.voteValidation || { name: 'any', params: {} }; - space.strategies = space.strategies?.map(strategy => ({ + space.strategies = space.strategies?.map((strategy: Strategy) => ({ ...strategy, // By default return space network if strategy network is not defined network: strategy.network || space.network @@ -78,12 +82,12 @@ export function formatSpace(id, settings) { // always return parent and children in child node format // will be overwritten if other fields than id are requested space.parent = space.parent ? { id: space.parent } : null; - space.children = space.children?.map(child => ({ id: child })) || []; + space.children = space.children?.map((child: string) => ({ id: child })) || []; return space; } -export function buildWhereQuery(fields, alias, where) { +export function buildWhereQuery(fields: QueryFields, alias: string, where: QueryArgs['where']) { let query: any = ''; const params: any[] = []; Object.entries(fields).forEach(([field, type]) => { @@ -136,10 +140,10 @@ export function buildWhereQuery(fields, alias, where) { return { query, params }; } -export async function fetchSpaces(args) { +export async function fetchSpaces(args: QueryArgs) { const { first = 20, skip = 0, where = {} } = args; - const fields = { id: 'string' }; + const fields: QueryFields = { id: 'string' }; const whereQuery = buildWhereQuery(fields, 's', where); const queryStr = whereQuery.query; const params: any[] = whereQuery.params; @@ -159,10 +163,10 @@ export async function fetchSpaces(args) { params.push(skip, first); const spaces = await db.queryAsync(query, params); - return spaces.map(space => Object.assign(space, formatSpace(space.id, space.settings))); + return spaces.map((space: any) => Object.assign(space, formatSpace(space.id, space.settings))); } -function checkRelatedSpacesNesting(requestedFields): void { +function checkRelatedSpacesNesting(requestedFields: any): void { // for a children's parent or a parent's children, you can ONLY query id // (for the purpose of easier cross-checking of relations in frontend) // other than that, deeper nesting is not supported @@ -184,7 +188,7 @@ function checkRelatedSpacesNesting(requestedFields): void { } } -function needsRelatedSpacesData(requestedFields): boolean { +function needsRelatedSpacesData(requestedFields: any): boolean { // id's of parent/children are already included in the result from fetchSpaces // an additional query is only needed if other fields are requested return !( @@ -193,32 +197,34 @@ function needsRelatedSpacesData(requestedFields): boolean { ); } -function mapRelatedSpacesToSpaces(spaces, relatedSpaces) { +function mapRelatedSpacesToSpaces(spaces: any, relatedSpaces: any) { if (!relatedSpaces.length) return spaces; - return spaces.map(space => { + return spaces.map((space: any) => { if (space.children) { space.children = space.children - .map(c => relatedSpaces.find(s => s.id === c.id) || c) - .filter(s => s); + .map((c: any) => relatedSpaces.find((s: any) => s.id === c.id) || c) + .filter((s: any) => s); } if (space.parent) { - space.parent = relatedSpaces.find(s => s.id === space.parent.id) || space.parent; + space.parent = relatedSpaces.find((s: any) => s.id === space.parent.id) || space.parent; } return space; }); } -async function fetchRelatedSpaces(spaces) { +async function fetchRelatedSpaces(spaces: any) { // collect all parent and child ids of all spaces - const relatedSpaceIDs = spaces.reduce((ids, space) => { - if (space.children) ids.push(...space.children.map(c => c.id)); + const relatedSpaceIDs = spaces.reduce((ids: string[], space: any) => { + if (space.children) ids.push(...space.children.map((c: any) => c.id)); if (space.parent) ids.push(space.parent.id); return ids; }, []); return await fetchSpaces({ - where: { id_in: relatedSpaceIDs } + where: { id_in: relatedSpaceIDs }, + first: 20, + skip: 0 }); } @@ -232,7 +238,7 @@ export async function handleRelatedSpaces(info: any, spaces: any[]) { return spaces; } -export function formatUser(user) { +export function formatUser(user: any) { const profile = jsonParse(user.profile, {}); delete user.profile; return { @@ -241,7 +247,7 @@ export function formatUser(user) { }; } -export function formatProposal(proposal) { +export function formatProposal(proposal: any) { proposal.choices = jsonParse(proposal.choices, []); proposal.strategies = jsonParse(proposal.strategies, []); proposal.validation = jsonParse(proposal.validation, { name: 'any', params: {} }) || { @@ -259,7 +265,7 @@ export function formatProposal(proposal) { proposal.space = formatSpace(proposal.space, proposal.settings); const networkStr = network === 'testnet' ? 'demo.' : ''; proposal.link = `https://${networkStr}snapshot.org/#/${proposal.space.id}/proposal/${proposal.id}`; - proposal.strategies = proposal.strategies.map(strategy => ({ + proposal.strategies = proposal.strategies.map((strategy: Strategy) => ({ ...strategy, // By default return proposal network if strategy network is not defined network: strategy.network || proposal.network @@ -268,7 +274,7 @@ export function formatProposal(proposal) { return proposal; } -export function formatVote(vote) { +export function formatVote(vote: any) { vote.choice = jsonParse(vote.choice); vote.metadata = jsonParse(vote.metadata, {}); vote.vp_by_strategy = jsonParse(vote.vp_by_strategy, []); @@ -276,12 +282,12 @@ export function formatVote(vote) { return vote; } -export function formatFollow(follow) { +export function formatFollow(follow: any) { follow.space = formatSpace(follow.space, follow.settings); return follow; } -export function formatSubscription(subscription) { +export function formatSubscription(subscription: any) { subscription.space = formatSpace(subscription.space, subscription.settings); return subscription; } diff --git a/src/graphql/index.ts b/src/graphql/index.ts index 1f412271..b34dbf8c 100644 --- a/src/graphql/index.ts +++ b/src/graphql/index.ts @@ -2,6 +2,7 @@ import path from 'path'; import fs from 'fs'; import { graphqlHTTP } from 'express-graphql'; import { makeExecutableSchema } from 'graphql-tools'; +// @ts-ignore import queryCountLimit from 'graphql-query-count-limit'; import depthLimit from 'graphql-depth-limit'; import Query from './operations'; diff --git a/src/graphql/operations/aliases.ts b/src/graphql/operations/aliases.ts index f6a36ad6..32a9a733 100644 --- a/src/graphql/operations/aliases.ts +++ b/src/graphql/operations/aliases.ts @@ -1,8 +1,9 @@ import db from '../../helpers/mysql'; import { buildWhereQuery, checkLimits } from '../helpers'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -export default async function (parent, args) { +export default async function (parent: any, args: QueryArgs) { const { first = 20, skip = 0, where = {} } = args; checkLimits(args, 'aliases'); diff --git a/src/graphql/operations/follows.ts b/src/graphql/operations/follows.ts index b42cb607..c3917081 100644 --- a/src/graphql/operations/follows.ts +++ b/src/graphql/operations/follows.ts @@ -1,8 +1,9 @@ import db from '../../helpers/mysql'; import { buildWhereQuery, checkLimits, formatFollow } from '../helpers'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -export default async function (parent, args) { +export default async function (parent: any, args: QueryArgs) { const { first = 20, skip = 0, where = {} } = args; checkLimits(args, 'follows'); diff --git a/src/graphql/operations/messages.ts b/src/graphql/operations/messages.ts index be81f8c2..4183bf48 100644 --- a/src/graphql/operations/messages.ts +++ b/src/graphql/operations/messages.ts @@ -1,8 +1,9 @@ import db from '../../helpers/mysql'; import { buildWhereQuery, checkLimits } from '../helpers'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -export default async function (parent, args) { +export default async function (parent: any, args: QueryArgs) { const { first = 20, skip = 0, where = {} } = args; checkLimits(args, 'messages'); diff --git a/src/graphql/operations/networks.ts b/src/graphql/operations/networks.ts index 872f5a6f..c7b8b54b 100644 --- a/src/graphql/operations/networks.ts +++ b/src/graphql/operations/networks.ts @@ -1,8 +1,8 @@ import { spaces } from '../../helpers/spaces'; export default function () { - const networks = {}; - Object.values(spaces).forEach((space: any) => { + const networks: { [key: string]: number } = {}; + Object.values(spaces).forEach(space => { networks[space.network] = networks[space.network] ? networks[space.network] + 1 : 1; }); return Object.entries(networks).map(network => ({ diff --git a/src/graphql/operations/plugins.ts b/src/graphql/operations/plugins.ts index 3710a70c..a90e8296 100644 --- a/src/graphql/operations/plugins.ts +++ b/src/graphql/operations/plugins.ts @@ -1,7 +1,7 @@ import { spaces } from '../../helpers/spaces'; export default function () { - const plugins = {}; + const plugins: { [key: string]: number } = {}; Object.values(spaces).forEach((space: any) => { Object.keys(space.plugins || {}).forEach(plugin => { plugins[plugin] = plugins[plugin] ? plugins[plugin] + 1 : 1; diff --git a/src/graphql/operations/proposal.ts b/src/graphql/operations/proposal.ts index dc436a7b..1ac92669 100644 --- a/src/graphql/operations/proposal.ts +++ b/src/graphql/operations/proposal.ts @@ -2,7 +2,7 @@ import db from '../../helpers/mysql'; import { formatProposal } from '../helpers'; import log from '../../helpers/log'; -export default async function (parent, { id }) { +export default async function (parent: any, { id }: { id: string }) { const query = ` SELECT p.*, spaces.settings FROM proposals p INNER JOIN spaces ON spaces.id = p.space @@ -11,7 +11,7 @@ export default async function (parent, { id }) { `; try { const proposals = await db.queryAsync(query, [id]); - return proposals.map(proposal => formatProposal(proposal))[0] || null; + return proposals.map((proposal: any) => formatProposal(proposal))[0] || null; } catch (e) { log.error(`[graphql] proposal, ${JSON.stringify(e)}`); return Promise.reject('request failed'); diff --git a/src/graphql/operations/proposals.ts b/src/graphql/operations/proposals.ts index 9fe3e95d..72509b67 100644 --- a/src/graphql/operations/proposals.ts +++ b/src/graphql/operations/proposals.ts @@ -1,8 +1,9 @@ import db from '../../helpers/mysql'; import { buildWhereQuery, formatProposal, checkLimits } from '../helpers'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -export default async function (parent, args) { +export default async function (parent: any, args: QueryArgs) { const { first = 20, skip = 0, where = {} } = args; checkLimits(args, 'proposals'); @@ -74,7 +75,7 @@ export default async function (parent, args) { params.push(skip, first); try { const proposals = await db.queryAsync(query, params); - return proposals.map(proposal => formatProposal(proposal)); + return proposals.map((proposal: any) => formatProposal(proposal)); } catch (e) { log.error(`[graphql] proposals, ${JSON.stringify(e)}`); return Promise.reject('request failed'); diff --git a/src/graphql/operations/skins.ts b/src/graphql/operations/skins.ts index ddc33a02..e5164918 100644 --- a/src/graphql/operations/skins.ts +++ b/src/graphql/operations/skins.ts @@ -1,7 +1,7 @@ import { spaces } from '../../helpers/spaces'; export default function () { - const skins = {}; + const skins: { [key: string]: number } = {}; Object.values(spaces).forEach((space: any) => { if (space.skin) skins[space.skin] = skins[space.skin] ? skins[space.skin] + 1 : 1; }); diff --git a/src/graphql/operations/space.ts b/src/graphql/operations/space.ts index d3bdc1c6..209722fa 100644 --- a/src/graphql/operations/space.ts +++ b/src/graphql/operations/space.ts @@ -1,10 +1,10 @@ import { fetchSpaces, handleRelatedSpaces, PublicError } from '../helpers'; import log from '../../helpers/log'; -export default async function (_parent, { id }, _context, info) { +export default async function (parent: any, { id }: { id: string }, context: any, info: any) { if (!id) return new PublicError('Missing id'); try { - let spaces = await fetchSpaces({ first: 1, where: { id } }); + let spaces = await fetchSpaces({ first: 1, skip: 0, where: { id } }); if (spaces.length !== 1) return null; spaces = await handleRelatedSpaces(info, spaces); diff --git a/src/graphql/operations/spaces.ts b/src/graphql/operations/spaces.ts index 4fbfcbf6..d755e313 100644 --- a/src/graphql/operations/spaces.ts +++ b/src/graphql/operations/spaces.ts @@ -1,7 +1,8 @@ import { checkLimits, fetchSpaces, handleRelatedSpaces, PublicError } from '../helpers'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -export default async function (_parent, args, _context, info) { +export default async function (parent: any, args: QueryArgs, context: any, info: any) { checkLimits(args, 'spaces'); try { let spaces = await fetchSpaces(args); diff --git a/src/graphql/operations/strategy.ts b/src/graphql/operations/strategy.ts index 80833609..e157a8ec 100644 --- a/src/graphql/operations/strategy.ts +++ b/src/graphql/operations/strategy.ts @@ -1,5 +1,5 @@ import { strategiesObj } from '../../helpers/strategies'; -export default async function (parent, { id }) { +export default async function (parent: any, { id }: { id: string }) { return strategiesObj[id]; } diff --git a/src/graphql/operations/subscriptions.ts b/src/graphql/operations/subscriptions.ts index 33d07a7a..ba0266a7 100644 --- a/src/graphql/operations/subscriptions.ts +++ b/src/graphql/operations/subscriptions.ts @@ -1,8 +1,9 @@ import db from '../../helpers/mysql'; import { buildWhereQuery, checkLimits, formatSubscription } from '../helpers'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -export default async function (parent, args) { +export default async function (parent: any, args: QueryArgs) { const { first = 20, skip = 0, where = {} } = args; checkLimits(args, 'subscriptions'); diff --git a/src/graphql/operations/user.ts b/src/graphql/operations/user.ts index de25b0b2..bfaddeb5 100644 --- a/src/graphql/operations/user.ts +++ b/src/graphql/operations/user.ts @@ -2,8 +2,7 @@ import db from '../../helpers/mysql'; import { formatUser } from '../helpers'; import log from '../../helpers/log'; -export default async function (parent, args) { - const id = args.id; +export default async function (parent: any, { id }: { id: string }) { const query = `SELECT u.* FROM users u WHERE id = ? LIMIT 1`; try { const users = await db.queryAsync(query, id); diff --git a/src/graphql/operations/users.ts b/src/graphql/operations/users.ts index b7d4f77a..89569084 100644 --- a/src/graphql/operations/users.ts +++ b/src/graphql/operations/users.ts @@ -1,8 +1,9 @@ import db from '../../helpers/mysql'; import { buildWhereQuery, checkLimits, formatUser } from '../helpers'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -export default async function (parent, args) { +export default async function (parent: any, args: QueryArgs) { const { first = 20, skip = 0, where = {} } = args; checkLimits(args, 'users'); diff --git a/src/graphql/operations/validations.ts b/src/graphql/operations/validations.ts index 52d0f498..77148692 100644 --- a/src/graphql/operations/validations.ts +++ b/src/graphql/operations/validations.ts @@ -1,7 +1,7 @@ import { spaces } from '../../helpers/spaces'; export default function () { - const validations = {}; + const validations: { [key: string]: number } = {}; Object.values(spaces).forEach((space: any) => { if (space.validation) validations[space.validation.name] = validations[space.validation.name] diff --git a/src/graphql/operations/vote.ts b/src/graphql/operations/vote.ts index cfb3c65d..4b0d4b18 100644 --- a/src/graphql/operations/vote.ts +++ b/src/graphql/operations/vote.ts @@ -3,7 +3,7 @@ import db from '../../helpers/mysql'; import { formatProposal, formatVote } from '../helpers'; import log from '../../helpers/log'; -export default async function (parent, { id }, context, info) { +export default async function (parent: any, { id }: { id: string }, context: any, info: any) { const requestedFields = info ? graphqlFields(info) : {}; const query = ` SELECT v.*, spaces.settings FROM votes v @@ -13,7 +13,7 @@ export default async function (parent, { id }, context, info) { `; try { const votes = await db.queryAsync(query, [id]); - const result = votes.map(vote => formatVote(vote))[0] || null; + const result = votes.map((vote: any) => formatVote(vote))[0] || null; if (requestedFields.proposal && result?.proposal) { const proposalId = result.proposal; const query = ` diff --git a/src/graphql/operations/votes.ts b/src/graphql/operations/votes.ts index ee706f1c..c9a2eedc 100644 --- a/src/graphql/operations/votes.ts +++ b/src/graphql/operations/votes.ts @@ -3,8 +3,9 @@ import db from '../../helpers/mysql'; import { buildWhereQuery, checkLimits, formatProposal, formatSpace, formatVote } from '../helpers'; import serve from '../../helpers/ee'; import log from '../../helpers/log'; +import type { QueryArgs } from '../../types'; -async function query(parent, args, context?, info?) { +async function query(parent: any, args: QueryArgs, context?: any, info?: any) { const requestedFields = info ? graphqlFields(info) : {}; const { where = {}, first = 20, skip = 0 } = args; @@ -60,7 +61,7 @@ async function query(parent, args, context?, info?) { let spaces = await db.queryAsync(query, [spaceIds]); spaces = Object.fromEntries( - spaces.map(space => [space.id, formatSpace(space.id, space.settings)]) + spaces.map((space: any) => [space.id, formatSpace(space.id, space.settings)]) ); votes = votes.map(vote => { if (spaces[vote.space.id]) return { ...vote, space: spaces[vote.space.id] }; @@ -82,7 +83,7 @@ async function query(parent, args, context?, info?) { try { let proposals = await db.queryAsync(query, [proposalIds]); proposals = Object.fromEntries( - proposals.map(proposal => [proposal.id, formatProposal(proposal)]) + proposals.map((proposal: any) => [proposal.id, formatProposal(proposal)]) ); votes = votes.map(vote => { vote.proposal = proposals[vote.proposal]; @@ -97,7 +98,7 @@ async function query(parent, args, context?, info?) { return votes; } -export default async function (parent, args, context?, info?) { +export default async function (parent: any, args: QueryArgs, context?: any, info?: any) { const requestedFields = info ? graphqlFields(info) : {}; return await serve(JSON.stringify({ args, requestedFields }), query, [ parent, diff --git a/src/graphql/operations/vp.ts b/src/graphql/operations/vp.ts index 5efefbfd..6b48a584 100644 --- a/src/graphql/operations/vp.ts +++ b/src/graphql/operations/vp.ts @@ -1,7 +1,10 @@ import snapshot from '@snapshot-labs/snapshot.js'; import db from '../../helpers/mysql'; -export default async function (_parent, { voter, space, proposal }) { +export default async function ( + parent: any, + { voter, space, proposal }: { voter: any; space: string; proposal: string } +) { if (proposal) { const query = `SELECT * FROM proposals WHERE id = ?`; const [p] = await db.queryAsync(query, [proposal]); diff --git a/src/helpers/alias.ts b/src/helpers/alias.ts index 29b8cd04..be53fae7 100644 --- a/src/helpers/alias.ts +++ b/src/helpers/alias.ts @@ -1,6 +1,6 @@ import db from './mysql'; -export async function isValidAlias(from, alias): Promise { +export async function isValidAlias(from: string, alias: string): Promise { const query = 'SELECT * FROM aliases WHERE address = ? AND alias = ? LIMIT 1'; const results = await db.queryAsync(query, [from, alias]); return !!results[0]; diff --git a/src/helpers/ee.ts b/src/helpers/ee.ts index fc227a0f..2360d4ba 100644 --- a/src/helpers/ee.ts +++ b/src/helpers/ee.ts @@ -4,7 +4,7 @@ import { sha256 } from './utils'; const eventEmitter = new events.EventEmitter(); eventEmitter.setMaxListeners(1000); // https://stackoverflow.com/a/26176922 -export default async function serve(id, action, args) { +export default async function serve(id: string, action: any, args: any) { const key = sha256(id); // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { diff --git a/src/helpers/mysql.ts b/src/helpers/mysql.ts index 8fee8968..4ca70598 100644 --- a/src/helpers/mysql.ts +++ b/src/helpers/mysql.ts @@ -1,10 +1,16 @@ import mysql from 'mysql'; +// @ts-ignore import Pool from 'mysql/lib/Pool'; +// @ts-ignore import Connection from 'mysql/lib/Connection'; import bluebird from 'bluebird'; import parse from 'connection-string'; import log from './log'; +interface PromisedPool { + queryAsync: (query: string, args?: any) => Promise; +} + const connectionLimit = parseInt(process.env.CONNECTION_LIMIT || '25'); log.info(`[mysql] connection limit ${connectionLimit}`); @@ -20,6 +26,6 @@ config.acquireTimeout = 60e3; config.timeout = 60e3; config.charset = 'utf8mb4'; bluebird.promisifyAll([Pool, Connection]); -const db = mysql.createPool(config); +const db: PromisedPool = mysql.createPool(config) as mysql.Pool & PromisedPool; export default db; diff --git a/src/helpers/rateLimit.ts b/src/helpers/rateLimit.ts index 626743c8..77b06701 100644 --- a/src/helpers/rateLimit.ts +++ b/src/helpers/rateLimit.ts @@ -1,13 +1,16 @@ +// @ts-ignore import rateLimit from 'express-rate-limit'; import { getIp, sendError } from './utils'; import log from './log'; +import type { Response, Request } from 'express'; + export default rateLimit({ windowMs: 20 * 1e3, max: 60, - keyGenerator: req => getIp(req), + keyGenerator: (req: Request) => getIp(req), standardHeaders: true, - handler: (req, res) => { + handler: (req: Request, res: Response) => { // const id = sha256(getIp(req)); log.info(`too many requests ${getIp(req).slice(0, 7)}`); sendError(res, 'too many requests', 429); diff --git a/src/helpers/spaces.ts b/src/helpers/spaces.ts index db2f6d1f..8c9a0783 100644 --- a/src/helpers/spaces.ts +++ b/src/helpers/spaces.ts @@ -2,15 +2,29 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { uniq } from 'lodash'; import db from './mysql'; import log from './log'; +import type { Space, SpaceMetadata, SpaceSetting } from '../types'; -export let spaces = {}; -export const spacesMetadata = {}; -export const spaceProposals = {}; -export const spaceVotes = {}; -export const spaceFollowers = {}; +type SpaceVotes = { space: Space['id']; count: number; count_7d: number }; +type ProposalsMetrics = { + space: Space['id']; + count: number; + active: 0 | 1; + count_7d: number | null; +}; +type FollowersMetrics = { + space: Space['id']; + count: number; + count_7d: number; +}; + +export let spaces: { [key: Space['id']]: SpaceSetting } = {}; +export const spacesMetadata: { [key: Space['id']]: SpaceMetadata } = {}; +export const spaceProposals: { [key: Space['id']]: ProposalsMetrics } = {}; +export const spaceVotes: { [key: Space['id']]: SpaceVotes } = {}; +export const spaceFollowers: { [key: Space['id']]: FollowersMetrics } = {}; function mapSpaces() { - Object.entries(spaces).forEach(([id, space]: any) => { + Object.entries(spaces).forEach(([id, space]) => { spacesMetadata[id] = { name: space.name, private: space.private || undefined, @@ -32,14 +46,14 @@ function mapSpaces() { async function loadSpaces() { const query = 'SELECT id, settings FROM spaces WHERE deleted = 0 ORDER BY id ASC'; - const s = await db.queryAsync(query); + const s: { id: Space['id']; settings: string }[] = await db.queryAsync(query); spaces = Object.fromEntries(s.map(ensSpace => [ensSpace.id, JSON.parse(ensSpace.settings)])); const totalSpaces = Object.keys(spaces).length; log.info(`[spaces] total spaces ${totalSpaces}`); mapSpaces(); } -async function getProposals() { +async function getProposals(): Promise { const ts = parseInt((Date.now() / 1e3).toFixed()); const query = ` SELECT space, COUNT(id) AS count, @@ -50,7 +64,7 @@ async function getProposals() { return await db.queryAsync(query, [ts, ts]); } -async function getVotes() { +async function getVotes(): Promise { const query = ` SELECT space, COUNT(id) as count, count(IF(created > (UNIX_TIMESTAMP() - 604800), 1, NULL)) as count_7d @@ -59,7 +73,7 @@ async function getVotes() { return await db.queryAsync(query); } -async function getFollowers() { +async function getFollowers(): Promise { const query = ` SELECT space, COUNT(id) as count, count(IF(created > (UNIX_TIMESTAMP() - 604800), 1, NULL)) as count_7d diff --git a/src/helpers/strategies.ts b/src/helpers/strategies.ts index bd344c46..d65aee3d 100644 --- a/src/helpers/strategies.ts +++ b/src/helpers/strategies.ts @@ -1,17 +1,18 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { spaces } from './spaces'; import log from './log'; +import type { Strategy } from '../types'; -export let strategies: any[] = []; -export let strategiesObj: any = {}; +export let strategies: Strategy[] = []; +export let strategiesObj: { [id: Strategy['id']]: Strategy } = {}; const uri = 'https://score.snapshot.org/api/strategies'; async function loadStrategies() { const res = await snapshot.utils.getJSON(uri); - Object.values(spaces).forEach((space: any) => { - const ids = new Set(space.strategies.map(strategy => strategy.name)); + Object.values(spaces).forEach(space => { + const ids = new Set((space.strategies || []).map(strategy => strategy.name)); ids.forEach(id => { if (res[id]) { res[id].spacesCount = res[id].spacesCount || 0; diff --git a/src/helpers/utils.ts b/src/helpers/utils.ts index c07fb9fe..caca5724 100644 --- a/src/helpers/utils.ts +++ b/src/helpers/utils.ts @@ -1,6 +1,7 @@ import { createHash } from 'crypto'; +import type { Response, Request } from 'express'; -export function jsonParse(input, fallback?) { +export function jsonParse(input: string, fallback?: unknown) { try { return JSON.parse(input); } catch (err) { @@ -8,17 +9,17 @@ export function jsonParse(input, fallback?) { } } -export function sendError(res, description, status = 500) { +export function sendError(res: Response, description: string, status = 500) { return res.status(status).json({ error: 'unauthorized', error_description: description }); } -export function sha256(str) { +export function sha256(str: string) { return createHash('sha256').update(str).digest('hex'); } -export function getIp(req) { +export function getIp(req: Request) { return req.headers['cf-connecting-ip'] || req.ip; } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..f656a156 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,46 @@ +export type Strategy = { + id: string; + name: string; + spacesCount: number; + network?: string; +}; + +export type Space = { + id: string; +}; + +export type SpaceMetadata = { + name?: string; + strategies?: Strategy[]; + private?: boolean; + terms?: string; + network?: string; + networks: string[]; + categories?: string[]; + activeProposals?: number; + proposals?: number; + proposals_active?: number; + proposals_7d?: number; + votes?: number; + votes_7d?: number; + followers?: number; + followers_7d?: number; +}; + +export type SpaceSetting = { + name: string; + private?: boolean; + terms?: string; + network: string; + networks?: string[]; + categories: string[]; + strategies?: Strategy[]; +}; + +export type QueryArgs = { + where: { [key: string]: any }; + first: number; + skip: number; + orderBy?: string; + orderDirection?: string; +}; diff --git a/tsconfig.json b/tsconfig.json index 53f9c7a3..f166c93a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,9 @@ "outDir": "./build", "esModuleInterop": true, "strict": true, - "noImplicitAny": false, - "resolveJsonModule": true - } + "noImplicitAny": true, + "resolveJsonModule": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] } diff --git a/yarn.lock b/yarn.lock index 6eb8e70e..f88f9e79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1275,21 +1275,128 @@ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e" integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ== +"@types/bluebird@^3.5.38": + version "3.5.38" + resolved "https://registry.yarnpkg.com/@types/bluebird/-/bluebird-3.5.38.tgz#7a671e66750ccd21c9fc9d264d0e1e5330bc9908" + integrity sha512-yR/Kxc0dd4FfwtEoLZMoqJbM/VE/W7hXn/MIjb+axcwag0iFmSPK7OBUZq1YWLynJUoWQkfUrI7T0HDqGApNSg== + +"@types/body-parser@*": + version "1.19.2" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.2.tgz#aea2059e28b7658639081347ac4fab3de166e6f0" + integrity sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" + integrity sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ== + dependencies: + "@types/node" "*" + +"@types/cors@^2.8.13": + version "2.8.13" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.13.tgz#b8ade22ba455a1b8cb3b5d3f35910fd204f84f94" + integrity sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA== + dependencies: + "@types/node" "*" + +"@types/express-rate-limit@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@types/express-rate-limit/-/express-rate-limit-6.0.0.tgz#11a314477895a8a888958f27650ed0d1ddad01b0" + integrity sha512-nZxo3nwU20EkTl/f2eGdndQkDIJYwkXIX4S3Vrp2jMdSdFJ6AWtIda8gOz0wiMuOFoeH/UUlCAiacz3x3eWNFA== + dependencies: + express-rate-limit "*" + +"@types/express-serve-static-core@^4.17.33": + version "4.17.33" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.33.tgz#de35d30a9d637dc1450ad18dd583d75d5733d543" + integrity sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + +"@types/express@^4.17.17": + version "4.17.17" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4" + integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/graphql-depth-limit@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@types/graphql-depth-limit/-/graphql-depth-limit-1.1.3.tgz#2e4a1a85cd2dfaacaeaf7b4bf1a158f450687875" + integrity sha512-fvK0qXNvwKD5bSnMEkidi51EloYsz/E8JG/8Kzq1peoLRQAEGgLVauE1xGeT4W/nbSpecgG+34dcKPdwfGzFHQ== + dependencies: + graphql "^14.5.3" + +"@types/graphql-fields@^1.3.5": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/graphql-fields/-/graphql-fields-1.3.5.tgz#e1c78bb200a3d20d5a5a4ed4c34d3a09e8b696b6" + integrity sha512-F6Nkra4p4MeBRFhg4zfkrnl/2gL4HZdt5lkFgLKZaA+3U/5+eA1dMqSHuSHX7aFUbCFE48ch8qCBXB/udcRhMg== + dependencies: + graphql "*" + "@types/json-schema@^7.0.9": version "7.0.11" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== -"@types/node@*", "@types/node@^14.0.13": +"@types/lodash@^4.14.194": + version "4.14.194" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76" + integrity sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g== + +"@types/mime@*": + version "3.0.1" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" + integrity sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA== + +"@types/mysql@^2.15.21": + version "2.15.21" + resolved "https://registry.yarnpkg.com/@types/mysql/-/mysql-2.15.21.tgz#7516cba7f9d077f980100c85fd500c8210bd5e45" + integrity sha512-NPotx5CVful7yB+qZbWtXL2fA4e7aEHkihHLjklc6ID8aq7bhguHgeIoC1EmSNTAuCgI6ZXrjt2ZSaXnYX0EUg== + dependencies: + "@types/node" "*" + +"@types/node@*": version "14.14.44" resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.44.tgz#df7503e6002847b834371c004b372529f3f85215" integrity sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA== +"@types/node@^18.15.11": + version "18.15.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.15.11.tgz#b3b790f09cb1696cffcec605de025b088fa4225f" + integrity sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q== + +"@types/qs@*": + version "6.9.7" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" + integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== + +"@types/range-parser@*": + version "1.2.4" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc" + integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw== + "@types/semver@^7.3.12": version "7.3.13" resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.13.tgz#da4bfd73f49bd541d28920ab0e2bf0ee80f71c91" integrity sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw== +"@types/serve-static@*": + version "1.15.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.1.tgz#86b1753f0be4f9a1bee68d459fcda5be4ea52b5d" + integrity sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ== + dependencies: + "@types/mime" "*" + "@types/node" "*" + "@types/websocket@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.2.tgz#d2855c6a312b7da73ed16ba6781815bf30c6187a" @@ -2264,6 +2371,11 @@ express-graphql@^0.12.0: http-errors "1.8.0" raw-body "^2.4.1" +express-rate-limit@*: + version "6.7.0" + resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-6.7.0.tgz#6aa8a1bd63dfe79702267b3af1161a93afc1d3c2" + integrity sha512-vhwIdRoqcYB/72TK3tRZI+0ttS8Ytrk24GfmsxDXK9o9IhHNO5bXRiXQSExPQ4GbaE5tvIS7j1SGrxsuWs+sGA== + express-rate-limit@^5.2.6: version "5.2.6" resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-5.2.6.tgz#b454e1be8a252081bda58460e0a25bf43ee0f7b0" @@ -2634,6 +2746,18 @@ graphql-ws@^4.4.1: resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.9.0.tgz#5cfd8bb490b35e86583d8322f5d5d099c26e365c" integrity sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag== +graphql@*: + version "16.6.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" + integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== + +graphql@^14.5.3: + version "14.7.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72" + integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA== + dependencies: + iterall "^1.2.2" + graphql@^15.8.0: version "15.8.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-15.8.0.tgz#33410e96b012fa3bdb1091cc99a94769db212b38" @@ -2859,7 +2983,7 @@ isomorphic-ws@4.0.1: resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" integrity sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w== -iterall@^1.2.1: +iterall@^1.2.1, iterall@^1.2.2: version "1.3.0" resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== @@ -3955,9 +4079,9 @@ type-is@~1.6.18: mime-types "~2.1.24" typescript@^4.7.4: - version "4.7.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.7.4.tgz#1a88596d1cf47d59507a1bcdfb5b9dfe4d488235" - integrity sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ== + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== ua-parser-js@^0.7.18: version "0.7.28" From 74cb9dfa9a4603047d85a71fd73428835b09cda0 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sat, 15 Apr 2023 17:14:03 +0400 Subject: [PATCH 2/4] chore: improve typescript coverage --- src/graphql/helpers.ts | 133 +++++++++------- src/graphql/operations/aliases.ts | 2 +- src/graphql/operations/follows.ts | 5 +- src/graphql/operations/messages.ts | 2 +- src/graphql/operations/networks.ts | 7 +- src/graphql/operations/plugins.ts | 5 +- src/graphql/operations/proposal.ts | 2 +- src/graphql/operations/proposals.ts | 4 +- src/graphql/operations/skins.ts | 5 +- src/graphql/operations/spaces.ts | 6 +- src/graphql/operations/subscriptions.ts | 6 +- src/graphql/operations/users.ts | 6 +- src/graphql/operations/validations.ts | 5 +- src/graphql/operations/vote.ts | 19 ++- src/graphql/operations/votes.ts | 8 +- src/graphql/operations/vp.ts | 17 +- src/helpers/mysql.ts | 4 +- src/helpers/spaces.ts | 47 +++--- src/helpers/strategies.ts | 6 +- src/types.ts | 202 ++++++++++++++++++++++-- 20 files changed, 356 insertions(+), 135 deletions(-) diff --git a/src/graphql/helpers.ts b/src/graphql/helpers.ts index 1c9f918e..f12f9c10 100644 --- a/src/graphql/helpers.ts +++ b/src/graphql/helpers.ts @@ -3,7 +3,18 @@ import { jsonParse } from '../helpers/utils'; import { spaceProposals, spaceFollowers } from '../helpers/spaces'; import db from '../helpers/mysql'; -import type { Strategy, QueryArgs } from '../types'; +import type { + Strategy, + QueryArgs, + Countable, + SqlRow, + User, + Subscription, + Follow, + Vote, + Proposal, + Space +} from '../types'; type QueryFields = { [key: string]: string }; @@ -11,7 +22,7 @@ const network = process.env.NETWORK || 'testnet'; export class PublicError extends Error {} -const ARG_LIMITS: { [key: string]: { [key: string]: number } } = { +const ARG_LIMITS: { [key: string]: Countable } = { default: { first: 1000, skip: 5000 @@ -84,11 +95,11 @@ export function formatSpace(id: string, settings: string) { space.parent = space.parent ? { id: space.parent } : null; space.children = space.children?.map((child: string) => ({ id: child })) || []; - return space; + return space as Space; } export function buildWhereQuery(fields: QueryFields, alias: string, where: QueryArgs['where']) { - let query: any = ''; + let query = ''; const params: any[] = []; Object.entries(fields).forEach(([field, type]) => { if (where[field] !== undefined) { @@ -140,13 +151,13 @@ export function buildWhereQuery(fields: QueryFields, alias: string, where: Query return { query, params }; } -export async function fetchSpaces(args: QueryArgs) { +export async function fetchSpaces(args: QueryArgs): Promise { const { first = 20, skip = 0, where = {} } = args; const fields: QueryFields = { id: 'string' }; const whereQuery = buildWhereQuery(fields, 's', where); const queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; let orderBy = args.orderBy || 'created_at'; let orderDirection = args.orderDirection || 'desc'; @@ -163,7 +174,9 @@ export async function fetchSpaces(args: QueryArgs) { params.push(skip, first); const spaces = await db.queryAsync(query, params); - return spaces.map((space: any) => Object.assign(space, formatSpace(space.id, space.settings))); + return spaces.map(space => + Object.assign(space, formatSpace(space.id as string, space.settings as string)) + ); } function checkRelatedSpacesNesting(requestedFields: any): void { @@ -197,26 +210,27 @@ function needsRelatedSpacesData(requestedFields: any): boolean { ); } -function mapRelatedSpacesToSpaces(spaces: any, relatedSpaces: any) { +function mapRelatedSpacesToSpaces(spaces: Space[], relatedSpaces: Space[]) { if (!relatedSpaces.length) return spaces; - return spaces.map((space: any) => { + return spaces.map(space => { if (space.children) { space.children = space.children - .map((c: any) => relatedSpaces.find((s: any) => s.id === c.id) || c) - .filter((s: any) => s); + .map(c => relatedSpaces.find(s => s.id === c.id) || c) + .filter(s => s); } if (space.parent) { - space.parent = relatedSpaces.find((s: any) => s.id === space.parent.id) || space.parent; + space.parent = + relatedSpaces.find(s => space.parent && s.id === space.parent.id) || space.parent; } return space; }); } -async function fetchRelatedSpaces(spaces: any) { +async function fetchRelatedSpaces(spaces: Space[]) { // collect all parent and child ids of all spaces - const relatedSpaceIDs = spaces.reduce((ids: string[], space: any) => { - if (space.children) ids.push(...space.children.map((c: any) => c.id)); + const relatedSpaceIDs = spaces.reduce((ids: string[], space: Space) => { + if (space.children) ids.push(...space.children.map(c => c.id)); if (space.parent) ids.push(space.parent.id); return ids; }, []); @@ -228,7 +242,7 @@ async function fetchRelatedSpaces(spaces: any) { }); } -export async function handleRelatedSpaces(info: any, spaces: any[]) { +export async function handleRelatedSpaces(info: any, spaces: Space[]) { const requestedFields = info ? graphqlFields(info) : {}; if (needsRelatedSpacesData(requestedFields)) { checkRelatedSpacesNesting(requestedFields); @@ -238,56 +252,67 @@ export async function handleRelatedSpaces(info: any, spaces: any[]) { return spaces; } -export function formatUser(user: any) { - const profile = jsonParse(user.profile, {}); +export function formatUser(user: SqlRow) { + const profile = jsonParse(user.profile as string, {}); delete user.profile; + return { ...user, ...profile - }; + } as User; } -export function formatProposal(proposal: any) { - proposal.choices = jsonParse(proposal.choices, []); - proposal.strategies = jsonParse(proposal.strategies, []); - proposal.validation = jsonParse(proposal.validation, { name: 'any', params: {} }) || { - name: 'any', - params: {} - }; - proposal.plugins = jsonParse(proposal.plugins, {}); - proposal.scores = jsonParse(proposal.scores, []); - proposal.scores_by_strategy = jsonParse(proposal.scores_by_strategy, []); +export function formatProposal(proposal: SqlRow) { + const networkStr = network === 'testnet' ? 'demo.' : ''; let proposalState = 'pending'; const ts = parseInt((Date.now() / 1e3).toFixed()); - if (ts > proposal.start) proposalState = 'active'; - if (ts > proposal.end) proposalState = 'closed'; - proposal.state = proposalState; - proposal.space = formatSpace(proposal.space, proposal.settings); - const networkStr = network === 'testnet' ? 'demo.' : ''; - proposal.link = `https://${networkStr}snapshot.org/#/${proposal.space.id}/proposal/${proposal.id}`; - proposal.strategies = proposal.strategies.map((strategy: Strategy) => ({ - ...strategy, - // By default return proposal network if strategy network is not defined - network: strategy.network || proposal.network - })); - proposal.privacy = proposal.privacy || ''; - return proposal; + if (ts > (proposal.start as number)) proposalState = 'active'; + if (ts > (proposal.end as number)) proposalState = 'closed'; + const space = formatSpace(proposal.space as string, proposal.settings as string); + + return { + ...proposal, + choices: jsonParse(proposal.choices as string, []), + validation: jsonParse(proposal.validation as string, { name: 'any', params: {} }) || { + name: 'any', + params: {} + }, + plugins: jsonParse(proposal.plugins as string, {}), + scores: jsonParse(proposal.scores as string, []), + scores_by_strategy: jsonParse(proposal.scores_by_strategy as string, []), + state: proposalState, + space, + link: `https://${networkStr}snapshot.org/#/${space.id}/proposal/${proposal.id}`, + strategies: (jsonParse(proposal.strategies as string, []) as Strategy[]).map( + (strategy: Strategy) => ({ + ...strategy, + // By default return proposal network if strategy network is not defined + network: strategy.network || proposal.network + }) + ), + privacy: proposal.privacy || '' + } as Proposal; } -export function formatVote(vote: any) { - vote.choice = jsonParse(vote.choice); - vote.metadata = jsonParse(vote.metadata, {}); - vote.vp_by_strategy = jsonParse(vote.vp_by_strategy, []); - vote.space = formatSpace(vote.space, vote.settings); - return vote; +export function formatVote(vote: SqlRow) { + return { + ...vote, + metadata: jsonParse(vote.metadata as string, {}), + vp_by_strategy: jsonParse(vote.vp_by_strategy as string, []), + space: formatSpace(vote.space as string, vote.settings as string) + } as Vote; } -export function formatFollow(follow: any) { - follow.space = formatSpace(follow.space, follow.settings); - return follow; +export function formatFollow(follow: SqlRow) { + return { + ...follow, + space: formatSpace(follow.space as string, follow.settings as string) + } as Follow; } -export function formatSubscription(subscription: any) { - subscription.space = formatSpace(subscription.space, subscription.settings); - return subscription; +export function formatSubscription(subscription: SqlRow) { + return { + ...subscription, + space: formatSpace(subscription.space as string, subscription.settings as string) + } as Subscription; } diff --git a/src/graphql/operations/aliases.ts b/src/graphql/operations/aliases.ts index 32a9a733..293318ee 100644 --- a/src/graphql/operations/aliases.ts +++ b/src/graphql/operations/aliases.ts @@ -17,7 +17,7 @@ export default async function (parent: any, args: QueryArgs) { }; const whereQuery = buildWhereQuery(fields, 'a', where); const queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; let orderBy = args.orderBy || 'created'; let orderDirection = args.orderDirection || 'desc'; diff --git a/src/graphql/operations/follows.ts b/src/graphql/operations/follows.ts index c3917081..aaab7452 100644 --- a/src/graphql/operations/follows.ts +++ b/src/graphql/operations/follows.ts @@ -17,7 +17,7 @@ export default async function (parent: any, args: QueryArgs) { }; const whereQuery = buildWhereQuery(fields, 'f', where); const queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; let orderBy = args.orderBy || 'created'; let orderDirection = args.orderDirection || 'desc'; @@ -34,9 +34,8 @@ export default async function (parent: any, args: QueryArgs) { `; params.push(skip, first); - let follows: any[] = []; try { - follows = await db.queryAsync(query, params); + const follows = await db.queryAsync(query, params); return follows.map(follow => formatFollow(follow)); } catch (e) { log.error(`[graphql] follows, ${JSON.stringify(e)}`); diff --git a/src/graphql/operations/messages.ts b/src/graphql/operations/messages.ts index 4183bf48..a43f625e 100644 --- a/src/graphql/operations/messages.ts +++ b/src/graphql/operations/messages.ts @@ -17,7 +17,7 @@ export default async function (parent: any, args: QueryArgs) { }; const whereQuery = buildWhereQuery(fields, 'm', where); const queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; let orderBy = args.orderBy || 'timestamp'; let orderDirection = args.orderDirection || 'desc'; diff --git a/src/graphql/operations/networks.ts b/src/graphql/operations/networks.ts index c7b8b54b..38c71613 100644 --- a/src/graphql/operations/networks.ts +++ b/src/graphql/operations/networks.ts @@ -1,9 +1,12 @@ import { spaces } from '../../helpers/spaces'; +import { Countable } from '../../types'; export default function () { - const networks: { [key: string]: number } = {}; + const networks: Countable = {}; Object.values(spaces).forEach(space => { - networks[space.network] = networks[space.network] ? networks[space.network] + 1 : 1; + if (space.network) { + networks[space.network] = networks[space.network] ? networks[space.network] + 1 : 1; + } }); return Object.entries(networks).map(network => ({ id: network[0], diff --git a/src/graphql/operations/plugins.ts b/src/graphql/operations/plugins.ts index a90e8296..212fdf8d 100644 --- a/src/graphql/operations/plugins.ts +++ b/src/graphql/operations/plugins.ts @@ -1,8 +1,9 @@ import { spaces } from '../../helpers/spaces'; +import { Countable } from '../../types'; export default function () { - const plugins: { [key: string]: number } = {}; - Object.values(spaces).forEach((space: any) => { + const plugins: Countable = {}; + Object.values(spaces).forEach(space => { Object.keys(space.plugins || {}).forEach(plugin => { plugins[plugin] = plugins[plugin] ? plugins[plugin] + 1 : 1; }); diff --git a/src/graphql/operations/proposal.ts b/src/graphql/operations/proposal.ts index 1ac92669..106595a8 100644 --- a/src/graphql/operations/proposal.ts +++ b/src/graphql/operations/proposal.ts @@ -11,7 +11,7 @@ export default async function (parent: any, { id }: { id: string }) { `; try { const proposals = await db.queryAsync(query, [id]); - return proposals.map((proposal: any) => formatProposal(proposal))[0] || null; + return proposals.map(proposal => formatProposal(proposal))[0] || null; } catch (e) { log.error(`[graphql] proposal, ${JSON.stringify(e)}`); return Promise.reject('request failed'); diff --git a/src/graphql/operations/proposals.ts b/src/graphql/operations/proposals.ts index 72509b67..298b825a 100644 --- a/src/graphql/operations/proposals.ts +++ b/src/graphql/operations/proposals.ts @@ -23,7 +23,7 @@ export default async function (parent: any, args: QueryArgs) { }; const whereQuery = buildWhereQuery(fields, 'p', where); let queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; const ts = parseInt((Date.now() / 1e3).toFixed()); const state = where.state || null; @@ -75,7 +75,7 @@ export default async function (parent: any, args: QueryArgs) { params.push(skip, first); try { const proposals = await db.queryAsync(query, params); - return proposals.map((proposal: any) => formatProposal(proposal)); + return proposals.map(proposal => formatProposal(proposal)); } catch (e) { log.error(`[graphql] proposals, ${JSON.stringify(e)}`); return Promise.reject('request failed'); diff --git a/src/graphql/operations/skins.ts b/src/graphql/operations/skins.ts index e5164918..c61a0645 100644 --- a/src/graphql/operations/skins.ts +++ b/src/graphql/operations/skins.ts @@ -1,8 +1,9 @@ import { spaces } from '../../helpers/spaces'; +import { Countable } from '../../types'; export default function () { - const skins: { [key: string]: number } = {}; - Object.values(spaces).forEach((space: any) => { + const skins: Countable = {}; + Object.values(spaces).forEach(space => { if (space.skin) skins[space.skin] = skins[space.skin] ? skins[space.skin] + 1 : 1; }); return Object.entries(skins).map(skin => ({ diff --git a/src/graphql/operations/spaces.ts b/src/graphql/operations/spaces.ts index d755e313..06c61342 100644 --- a/src/graphql/operations/spaces.ts +++ b/src/graphql/operations/spaces.ts @@ -5,10 +5,8 @@ import type { QueryArgs } from '../../types'; export default async function (parent: any, args: QueryArgs, context: any, info: any) { checkLimits(args, 'spaces'); try { - let spaces = await fetchSpaces(args); - spaces = await handleRelatedSpaces(info, spaces); - - return spaces; + const spaces = await fetchSpaces(args); + return await handleRelatedSpaces(info, spaces); } catch (e) { log.error(`[graphql] spaces, ${JSON.stringify(e)}`); if (e instanceof PublicError) return e; diff --git a/src/graphql/operations/subscriptions.ts b/src/graphql/operations/subscriptions.ts index ba0266a7..37ffae52 100644 --- a/src/graphql/operations/subscriptions.ts +++ b/src/graphql/operations/subscriptions.ts @@ -17,7 +17,7 @@ export default async function (parent: any, args: QueryArgs) { }; const whereQuery = buildWhereQuery(fields, 's', where); const queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; let orderBy = args.orderBy || 'created'; let orderDirection = args.orderDirection || 'desc'; @@ -26,8 +26,6 @@ export default async function (parent: any, args: QueryArgs) { orderDirection = orderDirection.toUpperCase(); if (!['ASC', 'DESC'].includes(orderDirection)) orderDirection = 'DESC'; - let subscriptions: any[] = []; - const query = ` SELECT s.*, spaces.settings FROM subscriptions s INNER JOIN spaces ON spaces.id = s.space @@ -37,7 +35,7 @@ export default async function (parent: any, args: QueryArgs) { params.push(skip, first); try { - subscriptions = await db.queryAsync(query, params); + const subscriptions = await db.queryAsync(query, params); return subscriptions.map(subscription => formatSubscription(subscription)); } catch (e) { log.error(`[graphql] subscriptions, ${JSON.stringify(e)}`); diff --git a/src/graphql/operations/users.ts b/src/graphql/operations/users.ts index 89569084..d322b9ff 100644 --- a/src/graphql/operations/users.ts +++ b/src/graphql/operations/users.ts @@ -15,7 +15,7 @@ export default async function (parent: any, args: QueryArgs) { }; const whereQuery = buildWhereQuery(fields, 'u', where); const queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; let orderBy = args.orderBy || 'created'; let orderDirection = args.orderDirection || 'desc'; @@ -24,8 +24,6 @@ export default async function (parent: any, args: QueryArgs) { orderDirection = orderDirection.toUpperCase(); if (!['ASC', 'DESC'].includes(orderDirection)) orderDirection = 'DESC'; - let users: any[] = []; - const query = ` SELECT u.* FROM users u WHERE 1=1 ${queryStr} @@ -33,7 +31,7 @@ export default async function (parent: any, args: QueryArgs) { `; params.push(skip, first); try { - users = await db.queryAsync(query, params); + const users = await db.queryAsync(query, params); return users.map(user => formatUser(user)); } catch (e) { log.error(`[graphql] users, ${JSON.stringify(e)}`); diff --git a/src/graphql/operations/validations.ts b/src/graphql/operations/validations.ts index 77148692..f4b037c7 100644 --- a/src/graphql/operations/validations.ts +++ b/src/graphql/operations/validations.ts @@ -1,8 +1,9 @@ import { spaces } from '../../helpers/spaces'; +import type { Countable } from '../../types'; export default function () { - const validations: { [key: string]: number } = {}; - Object.values(spaces).forEach((space: any) => { + const validations: Countable = {}; + Object.values(spaces).forEach(space => { if (space.validation) validations[space.validation.name] = validations[space.validation.name] ? validations[space.validation.name] + 1 diff --git a/src/graphql/operations/vote.ts b/src/graphql/operations/vote.ts index 4b0d4b18..0a7ab9af 100644 --- a/src/graphql/operations/vote.ts +++ b/src/graphql/operations/vote.ts @@ -2,6 +2,9 @@ import graphqlFields from 'graphql-fields'; import db from '../../helpers/mysql'; import { formatProposal, formatVote } from '../helpers'; import log from '../../helpers/log'; +import type { Vote } from '../../types'; + +type VoteWithProposalId = Omit & { proposal: string }; export default async function (parent: any, { id }: { id: string }, context: any, info: any) { const requestedFields = info ? graphqlFields(info) : {}; @@ -11,11 +14,14 @@ export default async function (parent: any, { id }: { id: string }, context: any WHERE v.id = ? AND spaces.settings IS NOT NULL LIMIT 1 `; + let proposal; + try { const votes = await db.queryAsync(query, [id]); - const result = votes.map((vote: any) => formatVote(vote))[0] || null; - if (requestedFields.proposal && result?.proposal) { - const proposalId = result.proposal; + const vote = + votes.map((vote: any) => formatVote(vote) as unknown as VoteWithProposalId)[0] || null; + if (requestedFields.proposal && vote?.proposal) { + const proposalId = vote.proposal; const query = ` SELECT p.*, spaces.settings FROM proposals p INNER JOIN spaces ON spaces.id = p.space @@ -23,13 +29,16 @@ export default async function (parent: any, { id }: { id: string }, context: any `; try { const proposals = await db.queryAsync(query, [proposalId]); - result.proposal = formatProposal(proposals[0]); + proposal = formatProposal(proposals[0]); } catch (e) { log.error(`[graphql] vote, ${JSON.stringify(e)}`); return Promise.reject('request failed'); } } - return result; + return { + ...vote, + proposal + } as Vote; } catch (e) { log.error(`[graphql] vote, ${JSON.stringify(e)}`); return Promise.reject('request failed'); diff --git a/src/graphql/operations/votes.ts b/src/graphql/operations/votes.ts index c9a2eedc..47e9aa62 100644 --- a/src/graphql/operations/votes.ts +++ b/src/graphql/operations/votes.ts @@ -25,7 +25,7 @@ async function query(parent: any, args: QueryArgs, context?: any, info?: any) { }; const whereQuery = buildWhereQuery(fields, 'v', where); const queryStr = whereQuery.query; - const params: any[] = whereQuery.params; + const params = whereQuery.params; let orderBy = args.orderBy || 'created'; let orderDirection = args.orderDirection || 'desc'; @@ -61,7 +61,7 @@ async function query(parent: any, args: QueryArgs, context?: any, info?: any) { let spaces = await db.queryAsync(query, [spaceIds]); spaces = Object.fromEntries( - spaces.map((space: any) => [space.id, formatSpace(space.id, space.settings)]) + spaces.map(space => [space.id, formatSpace(space.id as string, space.settings as string)]) ); votes = votes.map(vote => { if (spaces[vote.space.id]) return { ...vote, space: spaces[vote.space.id] }; @@ -74,7 +74,7 @@ async function query(parent: any, args: QueryArgs, context?: any, info?: any) { } if (requestedFields.proposal && votes.length > 0) { - const proposalIds = votes.map(vote => vote.proposal); + const proposalIds = votes.map(vote => vote.proposal as string); const query = ` SELECT p.*, spaces.settings FROM proposals p INNER JOIN spaces ON spaces.id = p.space @@ -83,7 +83,7 @@ async function query(parent: any, args: QueryArgs, context?: any, info?: any) { try { let proposals = await db.queryAsync(query, [proposalIds]); proposals = Object.fromEntries( - proposals.map((proposal: any) => [proposal.id, formatProposal(proposal)]) + proposals.map(proposal => [proposal.id, formatProposal(proposal)]) ); votes = votes.map(vote => { vote.proposal = proposals[vote.proposal]; diff --git a/src/graphql/operations/vp.ts b/src/graphql/operations/vp.ts index 6b48a584..662ea15c 100644 --- a/src/graphql/operations/vp.ts +++ b/src/graphql/operations/vp.ts @@ -1,5 +1,6 @@ import snapshot from '@snapshot-labs/snapshot.js'; import db from '../../helpers/mysql'; +import { Space } from '../../types'; export default async function ( parent: any, @@ -11,21 +12,23 @@ export default async function ( return await snapshot.utils.getVp( voter, - p.network, - JSON.parse(p.strategies), - p.snapshot, + p.network as string, + JSON.parse(p.strategies as string), + p.snapshot as number, space, p.delegation === 1 ); } else if (space) { const query = `SELECT settings FROM spaces WHERE id = ? AND deleted = 0 LIMIT 1`; - let [s] = await db.queryAsync(query, [space]); - s = JSON.parse(s.settings); + const [s] = await db.queryAsync(query, [space]); + const settings: Required> = JSON.parse( + s.settings as string + ); return await snapshot.utils.getVp( voter, - s.network, - s.strategies, + settings.network as string, + settings.strategies, 'latest', space, s.delegation === 1 diff --git a/src/helpers/mysql.ts b/src/helpers/mysql.ts index 4ca70598..b284fcb3 100644 --- a/src/helpers/mysql.ts +++ b/src/helpers/mysql.ts @@ -6,9 +6,11 @@ import Connection from 'mysql/lib/Connection'; import bluebird from 'bluebird'; import parse from 'connection-string'; import log from './log'; +import { SqlRow } from '../types'; +type SqlQueryArgs = string | number | boolean | (string | number | boolean)[]; interface PromisedPool { - queryAsync: (query: string, args?: any) => Promise; + queryAsync: (query: string, args?: SqlQueryArgs | SqlQueryArgs[]) => Promise; } const connectionLimit = parseInt(process.env.CONNECTION_LIMIT || '25'); diff --git a/src/helpers/spaces.ts b/src/helpers/spaces.ts index 8c9a0783..0156875e 100644 --- a/src/helpers/spaces.ts +++ b/src/helpers/spaces.ts @@ -2,22 +2,21 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { uniq } from 'lodash'; import db from './mysql'; import log from './log'; -import type { Space, SpaceMetadata, SpaceSetting } from '../types'; +import type { Space, SpaceMetadata, SqlRow } from '../types'; -type SpaceVotes = { space: Space['id']; count: number; count_7d: number }; -type ProposalsMetrics = { - space: Space['id']; - count: number; - active: 0 | 1; - count_7d: number | null; -}; -type FollowersMetrics = { - space: Space['id']; - count: number; - count_7d: number; +type SpaceVotes = MetricLike & SqlRow; + +type MetricLike = { + count?: number; + count_7d?: number; }; +type ProposalsMetrics = { + active?: 0 | 1; +} & MetricLike & + SqlRow; +type FollowersMetrics = MetricLike & SqlRow; -export let spaces: { [key: Space['id']]: SpaceSetting } = {}; +export let spaces: { [key: Space['id']]: Space } = {}; export const spacesMetadata: { [key: Space['id']]: SpaceMetadata } = {}; export const spaceProposals: { [key: Space['id']]: ProposalsMetrics } = {}; export const spaceVotes: { [key: Space['id']]: SpaceVotes } = {}; @@ -30,7 +29,9 @@ function mapSpaces() { private: space.private || undefined, terms: space.terms || undefined, network: space.network || undefined, - networks: uniq((space.strategies || []).map(strategy => strategy.network || space.network)), + networks: uniq( + (space.strategies || []).map(strategy => (strategy.network || space.network) as string) + ), categories: space.categories || undefined, activeProposals: (spaceProposals[id] && spaceProposals[id].active) || undefined, proposals: (spaceProposals[id] && spaceProposals[id].count) || undefined, @@ -46,14 +47,16 @@ function mapSpaces() { async function loadSpaces() { const query = 'SELECT id, settings FROM spaces WHERE deleted = 0 ORDER BY id ASC'; - const s: { id: Space['id']; settings: string }[] = await db.queryAsync(query); - spaces = Object.fromEntries(s.map(ensSpace => [ensSpace.id, JSON.parse(ensSpace.settings)])); + const s = await db.queryAsync(query); + spaces = Object.fromEntries( + s.map(ensSpace => [ensSpace.id, JSON.parse(ensSpace.settings as string)]) + ); const totalSpaces = Object.keys(spaces).length; log.info(`[spaces] total spaces ${totalSpaces}`); mapSpaces(); } -async function getProposals(): Promise { +async function getProposals() { const ts = parseInt((Date.now() / 1e3).toFixed()); const query = ` SELECT space, COUNT(id) AS count, @@ -64,7 +67,7 @@ async function getProposals(): Promise { return await db.queryAsync(query, [ts, ts]); } -async function getVotes(): Promise { +async function getVotes() { const query = ` SELECT space, COUNT(id) as count, count(IF(created > (UNIX_TIMESTAMP() - 604800), 1, NULL)) as count_7d @@ -73,7 +76,7 @@ async function getVotes(): Promise { return await db.queryAsync(query); } -async function getFollowers(): Promise { +async function getFollowers() { const query = ` SELECT space, COUNT(id) as count, count(IF(created > (UNIX_TIMESTAMP() - 604800), 1, NULL)) as count_7d @@ -85,21 +88,21 @@ async function getFollowers(): Promise { async function loadSpacesMetrics() { const followersMetrics = await getFollowers(); followersMetrics.forEach(followers => { - if (spaces[followers.space]) spaceFollowers[followers.space] = followers; + if (followers.space && spaces[followers.space]) spaceFollowers[followers.space] = followers; }); log.info('[spaces] Followers metrics loaded'); mapSpaces(); const proposalsMetrics = await getProposals(); proposalsMetrics.forEach(proposals => { - if (spaces[proposals.space]) spaceProposals[proposals.space] = proposals; + if (proposals.space && spaces[proposals.space]) spaceProposals[proposals.space] = proposals; }); log.info('[spaces] Proposals metrics loaded'); mapSpaces(); const votesMetrics = await getVotes(); votesMetrics.forEach(votes => { - if (spaces[votes.space]) spaceVotes[votes.space] = votes; + if (votes.space && spaces[votes.space]) spaceVotes[votes.space] = votes; }); log.info('[spaces] Votes metrics loaded'); mapSpaces(); diff --git a/src/helpers/strategies.ts b/src/helpers/strategies.ts index d65aee3d..d547e97b 100644 --- a/src/helpers/strategies.ts +++ b/src/helpers/strategies.ts @@ -1,10 +1,10 @@ import snapshot from '@snapshot-labs/snapshot.js'; import { spaces } from './spaces'; import log from './log'; -import type { Strategy } from '../types'; +import type { StrategyItem } from '../types'; -export let strategies: Strategy[] = []; -export let strategiesObj: { [id: Strategy['id']]: Strategy } = {}; +export let strategies: StrategyItem[] = []; +export let strategiesObj: { [id: StrategyItem['id']]: StrategyItem } = {}; const uri = 'https://score.snapshot.org/api/strategies'; diff --git a/src/types.ts b/src/types.ts index f656a156..44c15427 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,14 +1,3 @@ -export type Strategy = { - id: string; - name: string; - spacesCount: number; - network?: string; -}; - -export type Space = { - id: string; -}; - export type SpaceMetadata = { name?: string; strategies?: Strategy[]; @@ -44,3 +33,194 @@ export type QueryArgs = { orderBy?: string; orderDirection?: string; }; + +export type Countable = { [key: string]: number }; + +export type SqlRow = { + [key: string]: string | number | boolean | null; +} & { space?: Space['id'] }; + +// Types + +export type Space = { + id: string; + name?: string; + private?: boolean; + about?: string; + avatar?: string; + terms?: string; + location?: string; + website?: string; + twitter?: string; + github?: string; + coingecko?: string; + email?: string; + network?: string; + symbol?: string; + skin?: string; + domain?: string; + strategies?: Strategy[]; + admins?: string[]; + members?: string[]; + moderators?: string[]; + filters?: SpaceFilters; + plugins?: any; + voting?: SpaceVoting; + categories?: string[]; + validation?: Validation; + voteValidation?: Validation; + treasuries?: Treasury[]; + followersCount?: number; + proposalsCount?: number; + parent?: Space; + children?: Space[]; + guidelines?: string; + template?: string; +}; + +export type SpaceFilters = { + minScore?: number; + onlyMembers?: boolean; +}; + +export type SpaceVoting = { + delay?: number; + period?: number; + type?: string; + quorum?: number; + blind?: boolean; + hideAbstain?: boolean; + privacy?: string; + aliased?: boolean; +}; + +export type Proposal = { + id: string; + ipfs?: string; + author: string; + created: number; + space?: Space; + network: string; + symbol: string; + type?: string; + strategies: Strategy[]; + validation?: Validation; + plugins: any; + title: string; + body?: string; + discussion: string; + choices: string[]; + start: number; + end: number; + quorum: number; + privacy?: string; + snapshot?: string; + state?: string; + link?: string; + app?: string; + scores?: number[]; + scores_by_strategy?: any; + scores_state?: string; + scores_total?: number; + scores_updated?: number; + votes?: number; +}; + +export type Strategy = { + name: string; + network?: string; + params: any; +}; + +export type Validation = { + name: string; + params?: any; +}; + +export type Vote = { + id: string; + ipfs?: string; + voter: string; + created: number; + space: Space; + proposal?: Proposal; + choice: any; + metadata?: any; + reason?: string; + app?: string; + vp?: number; + vp_by_strategy?: number[]; + vp_state?: string; +}; + +export type Alias = { + id: string; + ipfs?: string; + address: string; + alias: string; + created: number; +}; + +export type Follow = { + id: string; + ipfs?: string; + follower: string; + space: Space; + created: number; +}; +export type Subscription = { + id: string; + ipfs?: string; + address: string; + space: Space; + created: number; +}; + +export type User = { + id: string; + created: number; + ipfs?: string; + name?: string; + about?: string; + avatar?: string; +}; + +export type Item = { + id: string; + spacesCount?: number; +}; + +export type StrategyItem = { + id: string; + author?: string; + version?: string; + schema?: any; + examples?: any[]; + about?: string; + spacesCount?: number; +}; + +export type Treasury = { + name?: string; + address?: string; + network?: string; +}; + +export type Vp = { + vp?: number; + vp_by_strategy?: number[]; + vp_state?: string; +}; + +export type Message = { + mci?: number; + id?: string; + ipfs?: string; + address?: string; + version?: string; + timestamp?: number; + space?: string; + type?: string; + sig?: string; + receipt?: string; +}; From 9bdcb5aaf3487586fd9d9880b2035dcb6ec7fe68 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sat, 15 Apr 2023 17:14:13 +0400 Subject: [PATCH 3/4] chore: add task to check ts --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 2e7b90bc..6eda6477 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "lint": "eslint . --ext .ts --fix", "dev": "nodemon src/index.ts", - "start": "ts-node src/index.ts" + "start": "ts-node src/index.ts", + "typecheck": "tsc" }, "eslintConfig": { "extends": "@snapshot-labs" From 687a179ca7497efe27bfad5b2e08677ab51161bb Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sun, 23 Apr 2023 14:40:19 +0400 Subject: [PATCH 4/4] fix: fix choices formatted as string instead of JSON object --- src/graphql/helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/graphql/helpers.ts b/src/graphql/helpers.ts index f12f9c10..5919e904 100644 --- a/src/graphql/helpers.ts +++ b/src/graphql/helpers.ts @@ -297,6 +297,7 @@ export function formatProposal(proposal: SqlRow) { export function formatVote(vote: SqlRow) { return { ...vote, + choice: jsonParse(vote.choice as string, {}), metadata: jsonParse(vote.metadata as string, {}), vp_by_strategy: jsonParse(vote.vp_by_strategy as string, []), space: formatSpace(vote.space as string, vote.settings as string)