From a64b8c0ebb90dbb7a34b9c1fdb26de38442affb8 Mon Sep 17 00:00:00 2001 From: less Date: Sat, 24 Feb 2024 23:30:45 +0700 Subject: [PATCH 1/9] feat: store proposal, vote and follower count on space --- src/writer/delete-proposal.ts | 11 +++++++---- src/writer/follow.ts | 13 +++++++++++-- src/writer/proposal.ts | 8 ++++++-- src/writer/unfollow.ts | 15 ++++++++++++--- src/writer/vote.ts | 8 +++++++- 5 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/writer/delete-proposal.ts b/src/writer/delete-proposal.ts index 02639ba4..45437a34 100644 --- a/src/writer/delete-proposal.ts +++ b/src/writer/delete-proposal.ts @@ -20,11 +20,14 @@ export async function verify(body): Promise { export async function action(body): Promise { const msg = jsonParse(body.msg); - const id = msg.payload.proposal; + const proposal = await getProposal(msg.space, msg.payload.proposal); + const id = msg.payload.proposal; const query = ` - DELETE FROM proposals WHERE id = ? LIMIT 1; - DELETE FROM votes WHERE proposal = ?; + DELETE FROM proposals WHERE id = ? LIMIT 1; + DELETE FROM votes WHERE proposal = ?; + UPDATE spaces SET proposal_count = proposal_count - 1, vote_count = vote_count - ? WHERE id = ?; `; - await db.queryAsync(query, [id, id]); + + await db.queryAsync(query, [id, id, proposal.votes, msg.space]); } diff --git a/src/writer/follow.ts b/src/writer/follow.ts index 260f0d9e..6f32cd7d 100644 --- a/src/writer/follow.ts +++ b/src/writer/follow.ts @@ -10,6 +10,11 @@ export const getFollowsCount = async (follower: string): Promise => { }; export async function verify(message): Promise { + const query = `SELECT * FROM follows WHERE follower = ? AND space = ? LIMIT 1`; + const follows = await db.queryAsync(query, [message.from, message.space]); + + if (follows.length !== 0) return Promise.reject('you are already following this space'); + const count = await getFollowsCount(message.from); if (count >= FOLLOWS_LIMIT_PER_USER) { @@ -19,7 +24,11 @@ export async function verify(message): Promise { return true; } -export async function action(message, ipfs, receipt, id): Promise { +export async function action(message, ipfs, _receipt, id): Promise { + const query = ` + INSERT IGNORE INTO follows SET ?; + UPDATE spaces SET follower_count = follower_count + 1 WHERE id = ?; + `; const params = { id, ipfs, @@ -28,5 +37,5 @@ export async function action(message, ipfs, receipt, id): Promise { created: message.timestamp }; - await db.queryAsync('INSERT IGNORE INTO follows SET ?', params); + await db.queryAsync(query, [params, message.space]); } diff --git a/src/writer/proposal.ts b/src/writer/proposal.ts index 87d49244..24636b49 100644 --- a/src/writer/proposal.ts +++ b/src/writer/proposal.ts @@ -235,8 +235,12 @@ export async function action(body, ipfs, receipt, id): Promise { votes: 0, validation }; - const query = 'INSERT IGNORE INTO proposals SET ?; '; - const params: any[] = [proposal]; + + const query = ` + INSERT IGNORE INTO proposals SET ?; + UPDATE spaces SET proposal_count = proposal_count + 1 WHERE id = ?; + `; + const params: any[] = [proposal, space]; await db.queryAsync(query, params); } diff --git a/src/writer/unfollow.ts b/src/writer/unfollow.ts index 001df9a3..07bf683f 100644 --- a/src/writer/unfollow.ts +++ b/src/writer/unfollow.ts @@ -1,10 +1,19 @@ import db from '../helpers/mysql'; -export async function verify(): Promise { +export async function verify(message): Promise { + const query = `SELECT * FROM follows WHERE follower = ? AND space = ? LIMIT 1`; + const follows = await db.queryAsync(query, [message.from, message.space]); + + if (follows.length === 0) return Promise.reject('you can only unfollow a space you follow'); + return true; } export async function action(message): Promise { - const query = 'DELETE FROM follows WHERE follower = ? AND space = ? LIMIT 1'; - await db.queryAsync(query, [message.from, message.space]); + const query = ` + DELETE FROM follows WHERE follower = ? AND space = ? LIMIT 1; + UPDATE spaces SET follower_count = follower_count - 1 WHERE id = ?; + `; + + await db.queryAsync(query, [message.from, message.space, message.space]); } diff --git a/src/writer/vote.ts b/src/writer/vote.ts index b054dd70..430f7bb8 100644 --- a/src/writer/vote.ts +++ b/src/writer/vote.ts @@ -175,7 +175,13 @@ export async function action(body, ipfs, receipt, id, context): Promise { ); } else { // Store vote in dedicated table - await db.queryAsync('INSERT IGNORE INTO votes SET ?', params); + await db.queryAsync( + ` + INSERT IGNORE INTO votes SET ?; + UPDATE spaces SET vote_count = vote_count + 1 WHERE id = ?; + `, + [params, msg.space] + ); } // Update proposal scores and voters vp From b894b8a0e7dfd2a2d68acaa112253e00bfab4e9f Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sun, 26 May 2024 03:44:44 +0400 Subject: [PATCH 2/9] fix: ensure positive counters --- src/writer/delete-proposal.ts | 4 +++- src/writer/proposal.ts | 4 ++-- src/writer/unfollow.ts | 10 +++++++--- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/writer/delete-proposal.ts b/src/writer/delete-proposal.ts index b809abf5..4ee1b53a 100644 --- a/src/writer/delete-proposal.ts +++ b/src/writer/delete-proposal.ts @@ -34,7 +34,9 @@ export async function action(body): Promise { SET proposal_count = GREATEST(proposal_count - 1, 0) WHERE user = ? AND space = ? LIMIT 1; - UPDATE spaces SET proposal_count = proposal_count - 1, vote_count = vote_count - ? WHERE id = ?; + UPDATE spaces + SET proposal_count = GREATEST(proposal_count - 1, 0), vote_count = GREATEST(vote_count - ?, 0) + WHERE id = ?; `; const parameters = [id, id, proposal.author, msg.space, proposal.votes, msg.space]; diff --git a/src/writer/proposal.ts b/src/writer/proposal.ts index 6ed46dec..f85ebd49 100644 --- a/src/writer/proposal.ts +++ b/src/writer/proposal.ts @@ -253,8 +253,8 @@ export async function action(body, ipfs, receipt, id): Promise { const query = ` INSERT INTO proposals SET ?; INSERT INTO leaderboard (space, user, proposal_count) - VALUES(?, ?, 1) - ON DUPLICATE KEY UPDATE proposal_count = proposal_count + 1; + VALUES(?, ?, 1) + ON DUPLICATE KEY UPDATE proposal_count = proposal_count + 1; UPDATE spaces SET proposal_count = proposal_count + 1 WHERE id = ?; `; diff --git a/src/writer/unfollow.ts b/src/writer/unfollow.ts index cfafc8d7..cd2ec000 100644 --- a/src/writer/unfollow.ts +++ b/src/writer/unfollow.ts @@ -2,8 +2,12 @@ import db from '../helpers/mysql'; import { defaultNetwork } from './follow'; export async function verify(message): Promise { - const query = `SELECT * FROM follows WHERE follower = ? AND space = ? LIMIT 1`; - const follows = await db.queryAsync(query, [message.from, message.space]); + const query = `SELECT * FROM follows WHERE follower = ? AND space = ? AND network = ? LIMIT 1`; + const follows = await db.queryAsync(query, [ + message.from, + message.space, + message.network || defaultNetwork + ]); if (follows.length === 0) return Promise.reject('you can only unfollow a space you follow'); @@ -13,7 +17,7 @@ export async function verify(message): Promise { export async function action(message): Promise { const query = ` DELETE FROM follows WHERE follower = ? AND space = ? AND network = ? LIMIT 1; - UPDATE spaces SET follower_count = follower_count - 1 WHERE id = ?; + UPDATE spaces SET follower_count = GREATEST(follower_count - 1) WHERE id = ?; `; await db.queryAsync(query, [ message.from, From 1e0b9e7317a0775f315db9c77043200658632755 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sun, 26 May 2024 03:46:57 +0400 Subject: [PATCH 3/9] fix: fix missing arg --- src/writer/unfollow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writer/unfollow.ts b/src/writer/unfollow.ts index cd2ec000..f8eabda2 100644 --- a/src/writer/unfollow.ts +++ b/src/writer/unfollow.ts @@ -17,7 +17,7 @@ export async function verify(message): Promise { export async function action(message): Promise { const query = ` DELETE FROM follows WHERE follower = ? AND space = ? AND network = ? LIMIT 1; - UPDATE spaces SET follower_count = GREATEST(follower_count - 1) WHERE id = ?; + UPDATE spaces SET follower_count = GREATEST(follower_count - 1, 0) WHERE id = ?; `; await db.queryAsync(query, [ message.from, From f090eef3928dfe27055913bee3d1e54f21a774a8 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sun, 26 May 2024 03:50:33 +0400 Subject: [PATCH 4/9] fix: use total number of votes, instead of number of valid votes --- src/writer/delete-proposal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/writer/delete-proposal.ts b/src/writer/delete-proposal.ts index 4ee1b53a..d26a29bc 100644 --- a/src/writer/delete-proposal.ts +++ b/src/writer/delete-proposal.ts @@ -39,7 +39,7 @@ export async function action(body): Promise { WHERE id = ?; `; - const parameters = [id, id, proposal.author, msg.space, proposal.votes, msg.space]; + const parameters = [id, id, proposal.author, msg.space, voters.length, msg.space]; if (voters.length > 0) { queries += ` From 84d2851eebfb34b72e5058483426e3dadd1a387f Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sun, 26 May 2024 04:03:10 +0400 Subject: [PATCH 5/9] fix: add init script to populate spaces counters --- scripts/refresh_spaces_counters.ts | 35 ++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 scripts/refresh_spaces_counters.ts diff --git a/scripts/refresh_spaces_counters.ts b/scripts/refresh_spaces_counters.ts new file mode 100644 index 00000000..2bbdbe05 --- /dev/null +++ b/scripts/refresh_spaces_counters.ts @@ -0,0 +1,35 @@ +import 'dotenv/config'; +import db from '../src/helpers/mysql'; + +// Usage: yarn ts-node scripts/refresh_spaces_counters.ts +async function main() { + const spaces = await db.queryAsync(`SELECT id FROM spaces`); + + console.log(`Found ${spaces.length} spaces`); + + for (const i in spaces) { + const stats = await db.queryAsync( + `SELECT COUNT(voter) as vote_count, COUNT(DISTINCT(proposal)) as proposal_count FROM votes WHERE space = ? GROUP BY space`, + [spaces[i].id] + ); + + for (const stat of stats) { + await db.queryAsync(`UPDATE spaces SET vote_count = ?, proposal_count = ? WHERE id = ?`, [ + stat.vote_count, + stat.proposal_count, + spaces[i].id + ]); + console.log(`${i} / ${spaces.length}`); + } + } +} + +(async () => { + try { + await main(); + process.exit(0); + } catch (e) { + console.error(e); + process.exit(1); + } +})(); From 50797977831c83eaf4ef2c923729d3986e829e36 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Sun, 26 May 2024 04:31:56 +0400 Subject: [PATCH 6/9] chore: add tests for follow and unfollow --- test/integration/writer/follows.test.ts | 27 ++++++++ test/integration/writer/unfollows.test.ts | 84 +++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 test/integration/writer/unfollows.test.ts diff --git a/test/integration/writer/follows.test.ts b/test/integration/writer/follows.test.ts index 52c7f13a..4fecccdd 100644 --- a/test/integration/writer/follows.test.ts +++ b/test/integration/writer/follows.test.ts @@ -1,10 +1,15 @@ import { verify, action } from '../../../src/writer/follow'; import { FOLLOWS_LIMIT_PER_USER } from '../../../src/helpers/limits'; import db, { sequencerDB } from '../../../src/helpers/mysql'; +import { spacesSqlFixtures } from '../../fixtures/space'; describe('writer/follow', () => { + const TEST_PREFIX = 'test-follow-'; + const space = spacesSqlFixtures[1]; + afterAll(async () => { await db.queryAsync('DELETE FROM follows'); + await db.queryAsync('DELETE FROM spaces WHERE id = ?', [`${TEST_PREFIX}-${space.id}`]); await db.endAsync(); await sequencerDB.endAsync(); }); @@ -102,5 +107,27 @@ describe('writer/follow', () => { ]); }); }); + + it('should increment the follower count of the space', async () => { + await db.queryAsync('INSERT INTO spaces SET ?', { + ...space, + id: `${TEST_PREFIX}-${space.id}`, + settings: JSON.stringify(space.settings) + }); + + const id = '3'; + const ipfs = '4'; + const message = { + from: '0x4', + space: `${TEST_PREFIX}-${space.id}`, + timestamp: 1 + }; + + await action(message, ipfs, 1, id); + + return expect( + db.queryAsync('SELECT follower_count FROM spaces WHERE id = ?', [message.space]) + ).resolves.toEqual([{ follower_count: 1 }]); + }); }); }); diff --git a/test/integration/writer/unfollows.test.ts b/test/integration/writer/unfollows.test.ts new file mode 100644 index 00000000..9defc658 --- /dev/null +++ b/test/integration/writer/unfollows.test.ts @@ -0,0 +1,84 @@ +import { verify, action } from '../../../src/writer/unfollow'; +import db, { sequencerDB } from '../../../src/helpers/mysql'; +import { spacesSqlFixtures } from '../../fixtures/space'; + +describe('writer/unfollow', () => { + const TEST_PREFIX = 'test-unfollow-'; + const space = spacesSqlFixtures[0]; + const followerId = '0x0'; + + afterAll(async () => { + await db.queryAsync('DELETE FROM follows'); + await db.queryAsync('DELETE FROM spaces WHERE id LIKE ?', [`${TEST_PREFIX}-%`]); + await db.endAsync(); + await sequencerDB.endAsync(); + }); + + describe('verify()', () => { + beforeAll(async () => { + let i = 0; + const promises: Promise[] = []; + + while (i <= 2) { + promises.push( + db.queryAsync( + 'INSERT INTO follows SET id = ?, ipfs = ?, follower = ?, space = ?, network = ?, created = ?', + [i, i, followerId, `${TEST_PREFIX}-test-${i}.eth`, 's', i] + ) + ); + + i++; + } + + await Promise.all(promises); + }); + + it('rejects when the user has not followed the space', () => { + return expect(verify({ from: '0x1', space: `${TEST_PREFIX}-test-2.eth` })).rejects.toEqual( + 'you can only unfollow a space you follow' + ); + }); + + it('returns true when the user has followed the space', () => { + return expect( + verify({ from: followerId, space: `${TEST_PREFIX}-test-0.eth`, network: 's' }) + ).resolves.toEqual(true); + }); + }); + + describe('action()', () => { + it('should unfollow and decrement the follower count of the space', async () => { + await db.queryAsync('INSERT INTO spaces SET ?', { + ...space, + id: `${TEST_PREFIX}-test-0.eth`, + settings: JSON.stringify(space.settings), + follower_count: 2 + }); + + const message = { + from: followerId, + space: `${TEST_PREFIX}-test-0.eth`, + network: 's' + }; + + expect( + db.queryAsync('SELECT id FROM follows WHERE follower = ? AND space = ?', [ + message.from, + message.space + ]) + ).resolves.not.toEqual([]); + + await action(message); + + expect( + db.queryAsync('SELECT * FROM follows WHERE follower = ? AND space = ?', [ + message.from, + message.space + ]) + ).resolves.toEqual([]); + return expect( + db.queryAsync('SELECT follower_count FROM spaces WHERE id = ?', [message.space]) + ).resolves.toEqual([{ follower_count: 1 }]); + }); + }); +}); From 51646188b82fe2800c98187b9b3d3494f019b040 Mon Sep 17 00:00:00 2001 From: Wan <495709+wa0x6e@users.noreply.github.com> Date: Sun, 2 Jun 2024 23:39:43 +0400 Subject: [PATCH 7/9] Update scripts/refresh_spaces_counters.ts Co-authored-by: Chaitanya --- scripts/refresh_spaces_counters.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/scripts/refresh_spaces_counters.ts b/scripts/refresh_spaces_counters.ts index 2bbdbe05..31f5ccc7 100644 --- a/scripts/refresh_spaces_counters.ts +++ b/scripts/refresh_spaces_counters.ts @@ -9,18 +9,16 @@ async function main() { for (const i in spaces) { const stats = await db.queryAsync( - `SELECT COUNT(voter) as vote_count, COUNT(DISTINCT(proposal)) as proposal_count FROM votes WHERE space = ? GROUP BY space`, + `SELECT COUNT(voter) as vote_count, COUNT(DISTINCT(proposal)) as proposal_count FROM votes WHERE space = ?`, [spaces[i].id] ); - - for (const stat of stats) { - await db.queryAsync(`UPDATE spaces SET vote_count = ?, proposal_count = ? WHERE id = ?`, [ - stat.vote_count, - stat.proposal_count, - spaces[i].id - ]); - console.log(`${i} / ${spaces.length}`); - } + const stat = stats[0]; + await db.queryAsync(`UPDATE spaces SET vote_count = ?, proposal_count = ? WHERE id = ?`, [ + stat.vote_count, + stat.proposal_count, + spaces[i].id + ]); + console.log(`${i} / ${spaces.length}`); } } From f9c436644ee04333de4d164d80d12e30eb56341d Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Mon, 3 Jun 2024 00:20:49 +0400 Subject: [PATCH 8/9] fix: fix proposals counter being skipped when no votes --- scripts/refresh_spaces_counters.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/scripts/refresh_spaces_counters.ts b/scripts/refresh_spaces_counters.ts index 31f5ccc7..d7cfc85b 100644 --- a/scripts/refresh_spaces_counters.ts +++ b/scripts/refresh_spaces_counters.ts @@ -9,17 +9,20 @@ async function main() { for (const i in spaces) { const stats = await db.queryAsync( - `SELECT COUNT(voter) as vote_count, COUNT(DISTINCT(proposal)) as proposal_count FROM votes WHERE space = ?`, + `SELECT COUNT(voter) as vote_count FROM votes WHERE space = ?`, [spaces[i].id] ); const stat = stats[0]; - await db.queryAsync(`UPDATE spaces SET vote_count = ?, proposal_count = ? WHERE id = ?`, [ + await db.queryAsync(`UPDATE spaces SET vote_count = ? WHERE id = ?`, [ stat.vote_count, - stat.proposal_count, spaces[i].id ]); console.log(`${i} / ${spaces.length}`); } + + await db.queryAsync( + 'UPDATE spaces SET proposal_count = (SELECT count(id) from proposals WHERE space = spaces.id)' + ); } (async () => { From 9d3f89551652691f312de1359bc04fb710d69d04 Mon Sep 17 00:00:00 2001 From: Wan Qi Chen <495709+wa0x6e@users.noreply.github.com> Date: Mon, 3 Jun 2024 00:22:50 +0400 Subject: [PATCH 9/9] fix: fix missing follows_count initializer --- scripts/refresh_spaces_counters.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/refresh_spaces_counters.ts b/scripts/refresh_spaces_counters.ts index d7cfc85b..7f7c46c8 100644 --- a/scripts/refresh_spaces_counters.ts +++ b/scripts/refresh_spaces_counters.ts @@ -23,6 +23,10 @@ async function main() { await db.queryAsync( 'UPDATE spaces SET proposal_count = (SELECT count(id) from proposals WHERE space = spaces.id)' ); + + await db.queryAsync( + 'UPDATE spaces SET follower_count = (SELECT count(id) from follows WHERE space = spaces.id)' + ); } (async () => {