Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Topic Archival #9871

Merged
merged 22 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 0 additions & 36 deletions libs/model/src/community/DeleteTopic.command.ts

This file was deleted.

150 changes: 71 additions & 79 deletions libs/model/src/community/GetTopics.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,96 +5,88 @@ import { QueryTypes } from 'sequelize';
import { z } from 'zod';
import { models } from '../database';

const includeContestManagersQuery = `
SELECT td.*,
coalesce((SELECT jsonb_agg(jsonb_set(to_jsonb(cm), -- Convert the contest manager (cm) row to JSONB
'{content}', -- Set the 'content' key in the resulting JSONB
coalesce(
-- Aggregates the filtered actions into content
(SELECT jsonb_agg(ca)
FROM "ContestActions" ca
WHERE ca.contest_address = cm.contest_address
AND ca.action = 'added'
AND ca.created_at > co.start_time
AND ca.created_at < co.end_time),
-- Use an empty array as fallback if no actions are found
'[]'::jsonb)
))
FROM "Topics" t
LEFT JOIN "ContestManagers" cm ON cm.topic_id = t.id
JOIN (
-- Subquery to get the max contest_id, start_time, and end_time for each contest address
SELECT contest_address,
max(contest_id) AS max_contest_id,
max(start_time) AS start_time,
max(end_time) AS end_time
FROM "Contests"
GROUP BY contest_address) co ON cm.contest_address = co.contest_address
WHERE t.id = td.id
AND cm.community_id = :community_id
AND COALESCE(cm.cancelled, FALSE) = FALSE -- Exclude cancelled managers
AND (cm.interval = 0
AND now() < co.end_time -- Check if the interval is 0 and the contest is ongoing
OR cm.interval > 0 -- Or if there is a valid interval
)), '[]'::jsonb) AS active_contest_managers
FROM topic_data td
`;

export function GetTopics(): Query<typeof schemas.GetTopics> {
return {
...schemas.GetTopics,
auth: [],
secure: false,
body: async ({ payload }) => {
const { community_id, with_contest_managers } = payload;
const { community_id, with_contest_managers, with_archived_topics } =
payload;

const contest_managers = with_contest_managers
? `
SELECT
td.*,
coalesce((
SELECT
jsonb_agg(jsonb_set(to_jsonb (cm), -- Convert the contest manager (cm) row to JSONB
'{content}', -- Set the 'content' key in the resulting JSONB
coalesce((
SELECT
jsonb_agg(ca) -- Aggregates the filtered actions into content
FROM "ContestActions" ca
WHERE
ca.contest_address = cm.contest_address
AND ca.action = 'added'
AND ca.created_at > co.start_time
AND ca.created_at < co.end_time), '[]'::jsonb) -- Use an empty array as fallback if no actions are found
))
FROM "Topics" t
LEFT JOIN "ContestManagers" cm ON cm.topic_id = t.id
JOIN (
-- Subquery to get the max contest_id, start_time, and end_time for each contest address
SELECT
contest_address,
max(contest_id) AS max_contest_id,
max(start_time) AS start_time,
max(end_time) AS end_time
FROM
"Contests"
GROUP BY
contest_address) co ON cm.contest_address = co.contest_address
WHERE
t.id = td.id
AND cm.community_id = :community_id
AND COALESCE(cm.cancelled, FALSE) = FALSE -- Exclude cancelled managers
AND (cm.interval = 0
AND now() < co.end_time -- Check if the interval is 0 and the contest is ongoing
OR cm.interval > 0 -- Or if there is a valid interval
)), '[]'::jsonb) AS active_contest_managers
FROM
topic_data td
`
? includeContestManagersQuery
: `SELECT *, '[]'::json as active_contest_managers FROM topic_data`;

const archivedTopicsQuery = with_archived_topics
? ''
: 'AND archived_at IS NULL';

const sql = `
WITH topic_data AS (
SELECT
id,
name,
community_id,
description,
telegram,
featured_in_sidebar,
featured_in_new_post,
default_offchain_template,
"order",
channel_id,
group_ids,
weighted_voting,
token_symbol,
vote_weight_multiplier,
token_address,
created_at::text AS created_at,
updated_at::text AS updated_at,
deleted_at::text AS deleted_at,
(
SELECT
count(*)::int
FROM
"Threads"
WHERE
community_id = :community_id
AND topic_id = t.id
AND deleted_at IS NULL) AS total_threads
FROM
"Topics" t
WHERE
t.community_id = :community_id
AND t.deleted_at IS NULL
)
${contest_managers}
`;
WITH topic_data AS (SELECT id,
name,
community_id,
description,
telegram,
featured_in_sidebar,
featured_in_new_post,
default_offchain_template,
"order",
channel_id,
group_ids,
weighted_voting,
token_symbol,
vote_weight_multiplier,
token_address,
created_at::text AS created_at,
updated_at::text AS updated_at,
deleted_at::text AS deleted_at,
archived_at::text AS archived_at,
(SELECT count(*)::int
FROM "Threads"
WHERE community_id = :community_id
AND topic_id = t.id
AND deleted_at IS NULL) AS total_threads
FROM "Topics" t
WHERE t.community_id = :community_id
AND t.deleted_at IS NULL ${archivedTopicsQuery})
${contest_managers}
`;

const results = await models.sequelize.query<
z.infer<typeof schemas.TopicView>
Expand Down
51 changes: 51 additions & 0 deletions libs/model/src/community/ToggleArchiveTopic.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { type Command } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { models } from '../database';
import { authTopic } from '../middleware';
import { mustExist } from '../middleware/guards';

export function ToggleArchiveTopic(): Command<
typeof schemas.ToggleArchiveTopic
> {
return {
...schemas.ToggleArchiveTopic,
auth: [authTopic({ roles: ['admin', 'moderator'] })],
body: async ({ payload }) => {
const { community_id, topic_id } = payload;
const { archive } = payload;

const topic = await models.Topic.findOne({
where: { community_id, id: topic_id! },
});
mustExist('Topic', topic);

if ((archive && topic.archived_at) || (!archive && !topic.archived_at)) {
return { community_id, topic_id: topic.id! };
}

// WARN: threads and topic must have the same archival date
// so that unarchival can avoid unarchiving threads that were manually
// archived separately
const archivalDate = archive ? new Date() : null;
await models.sequelize.transaction(async (transaction) => {
await models.Thread.update(
{ archived_at: archivalDate },
{
where: {
community_id: topic.community_id,
topic_id: topic_id!,
// don't update archival date for already archived threads
// see warning above
archived_at: archive ? null : topic.archived_at,
},
transaction,
},
);
topic.archived_at = archivalDate;
await topic.save({ transaction });
});

return { community_id, topic_id: topic.id! };
},
};
}
4 changes: 4 additions & 0 deletions libs/model/src/community/UpdateTopic.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ export function UpdateTopic(): Command<typeof schemas.UpdateTopic> {
const topic = await models.Topic.findByPk(topic_id!);
mustExist('Topic', topic);

if (topic.archived_at) {
throw new InvalidState('Cannot update archived topic');
}

const {
name,
description,
Expand Down
2 changes: 1 addition & 1 deletion libs/model/src/community/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ export * from './CreateStakeTransaction.command';
export * from './CreateTopic.command';
export * from './DeleteCommunity.command';
export * from './DeleteGroup.command';
export * from './DeleteTopic.command';
export * from './GenerateStakeholderGroups.command';
export * from './GetCommunities.query';
export * from './GetCommunity.query';
Expand All @@ -18,6 +17,7 @@ export * from './JoinCommunity.command';
export * from './RefreshCommunityMemberships.command';
export * from './RefreshCustomDomain.query';
export * from './SetCommunityStake.command';
export * from './ToggleArchiveTopic.command';
export * from './UpdateCommunity.command';
export * from './UpdateCustomDomain.command';
export * from './UpdateGroup.command';
Expand Down
2 changes: 1 addition & 1 deletion libs/model/src/models/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default (
defaultValue: 'discussion',
},
url: { type: Sequelize.TEXT, allowNull: true },
topic_id: { type: Sequelize.INTEGER, allowNull: true },
topic_id: { type: Sequelize.INTEGER, allowNull: false },
pinned: {
type: Sequelize.BOOLEAN,
defaultValue: false,
Expand Down
1 change: 1 addition & 0 deletions libs/model/src/models/topic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default (
created_at: { type: Sequelize.DATE, allowNull: false },
updated_at: { type: Sequelize.DATE, allowNull: false },
deleted_at: { type: Sequelize.DATE, allowNull: true },
archived_at: { type: Sequelize.DATE, allowNull: true },
order: { type: Sequelize.INTEGER, allowNull: true },
default_offchain_template: {
type: Sequelize.TEXT,
Expand Down
16 changes: 11 additions & 5 deletions libs/model/src/thread/CreateThread.command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Actor,
AppError,
InvalidInput,
InvalidState,
type Command,
} from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
Expand Down Expand Up @@ -34,6 +35,7 @@ export const CreateThreadErrors = {
DiscussionMissingTitle: 'Discussion posts must include a title',
NoBody: 'Thread body cannot be blank',
PostLimitReached: 'Post limit reached',
ArchivedTopic: 'Cannot post in archived topic',
};

const getActiveContestManagersQuery = GetActiveContestManagers();
Expand Down Expand Up @@ -96,6 +98,10 @@ export function CreateThread(): Command<typeof schemas.CreateThread> {
if (kind === 'link' && !url?.trim())
throw new InvalidInput(CreateThreadErrors.LinkMissingTitleOrUrl);

const topic = await models.Topic.findOne({ where: { id: topic_id } });
if (topic?.archived_at)
throw new InvalidState(CreateThreadErrors.ArchivedTopic);

// check contest invariants
const activeContestManagers = await getActiveContestManagersQuery.body({
actor: {} as Actor,
Expand Down Expand Up @@ -175,12 +181,12 @@ export function CreateThread(): Command<typeof schemas.CreateThread> {

const thread = await models.Thread.findOne({
where: { id: new_thread_id },
include: [
{ model: models.Address, as: 'Address' },
{ model: models.Topic, as: 'topic' },
],
include: [{ model: models.Address, as: 'Address' }],
});
return thread!.toJSON();
return {
...thread!.toJSON(),
topic: topic!.toJSON(),
};
},
};
}
1 change: 1 addition & 0 deletions libs/model/src/thread/GetThreadsByIds.query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function GetThreadsByIds(): Query<typeof schemas.GetThreadsByIds> {
{
model: models.Topic,
as: 'topic',
required: true,
},
{
model: models.Reaction,
Expand Down
Loading
Loading