Skip to content

Refactors admin controller #12130

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

Merged
merged 3 commits into from
May 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
148 changes: 148 additions & 0 deletions libs/model/src/aggregates/super-admin/GetStats.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { Query } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { ALL_COMMUNITIES } from '@hicommonwealth/shared';
import { BindOrReplacements, QueryTypes } from 'sequelize';
import { z } from 'zod';
import { models } from '../../database';
import { isSuperAdmin } from '../../middleware';

export function GetStats(): Query<typeof schemas.GetStats> {
return {
...schemas.GetStats,
auth: [isSuperAdmin],
secure: true,
body: async ({ payload }) => {
const { community_id } = payload;

const oneMonthAgo = new Date();
oneMonthAgo.setDate(oneMonthAgo.getDate() - 30);

const replacements: BindOrReplacements = {
oneMonthAgo,
...(community_id ? { community_id } : {}),
};
const community_filter =
community_id && community_id !== ALL_COMMUNITIES
? `AND X.community_id = :community_id`
: '';

const [
lastMonthNewCommunities,
[{ monthlySummary }],
[{ result: averageAddressesPerCommunity }],
[{ result: populatedCommunities }],
] = await Promise.all([
models.sequelize.query<{ id: string; created_at: Date }>(
`SELECT id, created_at FROM "Communities"
WHERE created_at >= NOW() - INTERVAL '30 days'
ORDER BY created_at desc`,
{ type: QueryTypes.SELECT },
),
models.sequelize.query<{
monthlySummary: z.infer<typeof schemas.TotalStats>;
}>(
`
WITH MonthlyStats AS (
SELECT
'numCommentsLastMonth' as label,
COUNT(C.*) as count
FROM "Comments" C JOIN "Threads" X ON C.thread_id = X.id
WHERE C."created_at" >= :oneMonthAgo
${community_filter}
UNION ALL
SELECT
'numThreadsLastMonth' as label,
COUNT(*)
FROM "Threads" X
WHERE "created_at" >= :oneMonthAgo
${community_filter}
UNION ALL
SELECT
'numReactionsLastMonth' as label,
COUNT(R.*)
FROM "Reactions" R
LEFT JOIN "Threads" T ON R.thread_id = T.id
LEFT JOIN "Comments" C ON R.comment_id = C.id
LEFT JOIN "Threads" TC ON C.thread_id = TC.id
WHERE R."created_at" >= :oneMonthAgo
${community_filter ? `AND (T.community_id = :community_id OR TC.community_id = :community_id)` : ''}
UNION ALL
SELECT
'numProposalVotesLastMonth' as label,
COUNT(*)
FROM "Votes" X
WHERE "created_at" >= :oneMonthAgo
${community_filter}
UNION ALL
SELECT
'numPollsLastMonth' as label,
COUNT(*)
FROM "Polls" X
WHERE "created_at" >= :oneMonthAgo
${community_filter}
UNION ALL
SELECT
'numMembersLastMonth' as label,
COUNT(*)
FROM "Addresses" X
WHERE "created_at" >= :oneMonthAgo
${community_filter}
UNION ALL
SELECT
'numGroupsLastMonth' as label,
COUNT(*)
FROM "Groups" X
WHERE "created_at" >= :oneMonthAgo
${community_filter}
)
SELECT json_build_object(
'numCommentsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numCommentsLastMonth'),
'numThreadsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numThreadsLastMonth'),
'numReactionsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numReactionsLastMonth'),
'numProposalVotesLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numProposalVotesLastMonth'),
'numPollsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numPollsLastMonth'),
'numMembersLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numMembersLastMonth'),
'numGroupsLastMonth', (SELECT count FROM MonthlyStats WHERE label = 'numGroupsLastMonth')
) AS "monthlySummary";
`,
{ replacements, type: QueryTypes.SELECT },
),
models.sequelize.query<{ result: string }>(
`
SELECT AVG(profile_count) as result
FROM (
SELECT "Communities".id, COUNT("Addresses".id) as profile_count
FROM "Communities"
JOIN "Addresses" ON "Addresses".community_id = "Communities".id
GROUP BY "Communities".id
) as _;
`,
{ type: QueryTypes.SELECT },
),
models.sequelize.query<{ result: string }>(
`
SELECT COUNT(communities_count) as result FROM (
SELECT "Communities".id
FROM "Communities"
JOIN "Addresses" ON "Addresses".community_id = "Communities".id
GROUP BY "Communities".id
HAVING COUNT("Addresses".id) > 2
) as communities_count;
`,
{ type: QueryTypes.SELECT },
),
]);

return {
lastMonthNewCommunities: lastMonthNewCommunities.map(
({ id, created_at }) => ({ id, created_at }),
),
totalStats: {
...monthlySummary,
averageAddressesPerCommunity: +averageAddressesPerCommunity,
populatedCommunities: +populatedCommunities,
},
};
},
};
}
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
import { AppError } from '@hicommonwealth/core';
import { UserInstance } from '@hicommonwealth/model';
import { Query } from '@hicommonwealth/core';
import * as schemas from '@hicommonwealth/schemas';
import { QueryTypes } from 'sequelize';
import { ServerAdminController } from '../server_admin_controller';
import { models } from '../../database';
import { isSuperAdmin } from '../../middleware';

export const Errors = {
NotAdmin: 'Must be a site admin',
};

export type GetTopUsersOptions = {
user: UserInstance;
};

export type GetTopUsersResult = any[];

export async function __getTopUsers(
this: ServerAdminController,
{ user }: GetTopUsersOptions,
): Promise<GetTopUsersResult> {
if (!user.isAdmin) {
throw new AppError(Errors.NotAdmin);
}

const sql = `
export function GetTopUsers(): Query<typeof schemas.GetTopUsers> {
return {
...schemas.GetTopUsers,
auth: [isSuperAdmin],
secure: true,
body: async () => {
const sql = `
WITH Stats as (
SELECT
u.profile->>'name' AS profile_name,
p.user_id as user_id,
u.id as user_id,
COUNT(DISTINCT t.id) AS thread_count,
COUNT(DISTINCT c.id) AS comment_count,
COUNT(DISTINCT c.id) + COUNT(DISTINCT t.id) AS total_activity,
Expand All @@ -40,7 +29,7 @@ export async function __getTopUsers(
LEFT JOIN "Threads" AS t ON a.id = t.address_id
LEFT JOIN "Comments" AS c ON a.id = c.address_id
WHERE u."isAdmin" = FALSE
GROUP BY p.id
GROUP BY u.id
ORDER BY total_activity DESC
LIMIT 150
)
Expand All @@ -57,9 +46,9 @@ export async function __getTopUsers(
FROM Stats
`;

const result = await this.models.sequelize.query<GetTopUsersResult[0]>(sql, {
type: QueryTypes.SELECT,
});

return result;
return await models.sequelize.query(sql, {
type: QueryTypes.SELECT,
});
},
};
}
2 changes: 2 additions & 0 deletions libs/model/src/aggregates/super-admin/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from './EnableDigestEmail';
export * from './GetStats.query';
export * from './GetTopUsers.query';
export * from './SetCommunityTier.command';
export * from './SetUserTier.command';
export * from './TriggerNotificationsWorkflow.command';
Expand Down
1 change: 1 addition & 0 deletions libs/schemas/src/queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './pagination';
export * from './poll.schemas';
export * from './quest.schemas';
export * from './subscription.schemas';
export * from './super-admin.schemas';
export * from './tag.schemas';
export * from './thread.schemas';
export * from './token.schemas';
Expand Down
34 changes: 34 additions & 0 deletions libs/schemas/src/queries/super-admin.schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { z } from 'zod';

export const TotalStats = z.object({
numCommentsLastMonth: z.number(),
numThreadsLastMonth: z.number(),
numPollsLastMonth: z.number(),
numReactionsLastMonth: z.number(),
numProposalVotesLastMonth: z.number(),
numMembersLastMonth: z.number(),
numGroupsLastMonth: z.number(),
});

export const GetStats = {
input: z.object({
community_id: z.string().optional(),
}),
output: z.object({
lastMonthNewCommunities: z.array(
z.object({
id: z.string(),
created_at: z.date(),
}),
),
totalStats: TotalStats.extend({
averageAddressesPerCommunity: z.number(),
populatedCommunities: z.number(),
}),
}),
};

export const GetTopUsers = {
input: z.object({}),
output: z.array(z.any()),
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { notifyError } from 'client/scripts/controllers/app/notifications';
import { trpc } from 'utils/trpcClient';

const STATS_STALE_TIME = 60 * 60 * 1000;

const useGetStatsQuery = (community_id?: string) => {
return trpc.superAdmin.getStats.useQuery(
{
community_id,
},
{
staleTime: STATS_STALE_TIME,
enabled: !!community_id,
onError: (error) => {
notifyError(`Error fetching stats: ${error.message}`);
},
},
);
};

export default useGetStatsQuery;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { trpc } from 'utils/trpcClient';

const TOP_USERS_STALE_TIME = 6 * 60 * 60 * 1000;

const useGetTopUsersQuery = (enabled: boolean) => {
return trpc.superAdmin.getTopUsers.useQuery(
{},
{
staleTime: TOP_USERS_STALE_TIME,
enabled,
},
);
};

export default useGetTopUsersQuery;
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
.TaskRow {
display: flex;
flex-direction: row;
gap: 44px;
align-items: flex-end;
gap: 15px;
align-items: center;

.Downloading {
display: flex;
Expand Down
Loading
Loading