Expose statistics in GraphQL

This commit is contained in:
Jordan Eldredge 2022-02-24 23:50:49 -08:00
parent bfa7e87bd4
commit a7173e8da5
4 changed files with 174 additions and 44 deletions

View file

@ -0,0 +1,34 @@
import * as Skins from "../../../data/Skins";
export default class DatabaseStatisticsResolver {
unique_classic_skins_count(): Promise<number> {
return Skins.getClassicSkinCount();
}
tweeted_skins_count(): Promise<number> {
return Skins.getTweetedSkinCount();
}
approved_skins_count(): Promise<number> {
return Skins.getApprovedSkinCount();
}
rejected_skins_count(): Promise<number> {
return Skins.getRejectedSkinCount();
}
nsfw_skins_count(): Promise<number> {
return Skins.getNsfwSkinCount();
}
unreviewed_skins_count(): Promise<number> {
return Skins.getUnreviewedSkinCount();
}
tweetable_skins_count(): Promise<number> {
return Skins.getTweetableSkinCount();
}
uploads_pending_processing_count(): Promise<number> {
return Skins.getUploadsAwaitingProcessingCount();
}
uploads_in_error_state_count(): Promise<number> {
return Skins.getUploadsErroredCount();
}
web_uploads_count(): Promise<number> {
return Skins.getWebUploadsCount();
}
}

View file

@ -14,6 +14,7 @@ import MutationResolver from "./MutationResolver";
import { knex } from "../../../db";
import ArchiveFileModel from "../../../data/ArchiveFileModel";
import ArchiveFileResolver from "./ArchiveFileResolver";
import DatabaseStatisticsResolver from "./DatabaseStatisticsResolver";
// These keys are already in the web client, so they are not secret at all.
const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51");
@ -54,7 +55,7 @@ class RootResolver extends MutationResolver {
throw new Error("Can only query 1000 records via search.");
}
const results = await index.search(query, {
const results: { hits: { md5: string }[] } = await index.search(query, {
attributesToRetrieve: ["md5"],
length: first,
offset,
@ -112,6 +113,10 @@ class RootResolver extends MutationResolver {
})
);
}
statistics() {
return new DatabaseStatisticsResolver();
}
}
export default RootResolver;

View file

@ -426,6 +426,72 @@ type Mutation {
request_nsfw_review_for_skin(md5: String): Boolean
}
"""
Statistics about the contents of the Museum's database.
"""
type DatabaseStatistics {
"""
The total number of classic skins in the Museum's database
"""
unique_classic_skins_count: Int
"""
The number of skins in the Museum that have been tweeted by @winampskins
"""
tweeted_skins_count: Int
"""
The number of skins that have been approved for tweeting. This includes both
tweeted and untweeted skins.
**Note:** Skins can be both approved and rejected by different users.
"""
approved_skins_count: Int
"""
The number of skins that have been rejected for tweeting.
**Note:** Skins can be both approved and rejected by different users.
**Note:** Generally skins that have been marked NSFW are also marked as rejected.
"""
rejected_skins_count: Int
"""
The number of skins that have been marked as NSFW.
**Note:** Skins can be approved and rejected by different users.
**Note:** Generally skins that have been marked NSFW are also marked as rejected.
"""
nsfw_skins_count: Int
"""
The number of skins that have never been reviewed.
"""
unreviewed_skins_count: Int
"""
The number of skins that have been approved for tweeting, but not yet tweeted.
"""
tweetable_skins_count: Int
"""
Skins uplaods awaiting processing. This can happen when there are a large
number of skin uplaods at the same time, or when the skin uploading processing
pipeline gets stuck.
"""
uploads_pending_processing_count: Int
"""
Skins uploads that have errored during processing.
"""
uploads_in_error_state_count: Int
"""
Number of skins that have been uploaded to the Museum via the web interface.
"""
web_uploads_count: Int
}
type Query {
"""
The currently authenticated user, if any.
@ -503,4 +569,9 @@ type Query {
Get the status of a batch of uploads by ids
"""
upload_statuses(ids: [String!]!): [SkinUpload]
"""
A namespace for statistics about the database
"""
statistics: DatabaseStatistics
}

View file

@ -427,6 +427,61 @@ export async function getSkinToPostToInstagram(): Promise<string | null> {
return skin.md5;
}
export async function getUnreviewedSkinCount(): Promise<number> {
const rows = await knex("skins")
.where({ skin_type: 1 })
.whereNotIn("md5", knex("skin_reviews").select("skin_md5"))
.count("*", { as: "unreviewed" });
return Number(rows[0].unreviewed);
}
export async function getApprovedSkinCount(): Promise<number> {
const row = await knex("skin_reviews")
.first(knex.raw(`COUNT(DISTINCT skin_md5) AS "approved_count"`))
.where({ review: "APPROVED" });
return Number(row.approved_count);
}
export async function getRejectedSkinCount(): Promise<number> {
const row = await knex("skin_reviews")
.first(knex.raw(`COUNT(DISTINCT skin_md5) AS "rejected_count"`))
.where({ review: "REJECTED" });
return Number(row.rejected_count);
}
export async function getNsfwSkinCount(): Promise<number> {
const row = await knex("skin_reviews")
.first(knex.raw(`COUNT(DISTINCT skin_md5) AS "nsfw_count"`))
.where({ review: "NSFW" });
return Number(row.nsfw_count);
}
export async function getTweetedSkinCount(): Promise<number> {
const rows = await knex("tweets").count("*", { as: "tweeted" });
return Number(rows[0].tweeted);
}
export async function getWebUploadsCount(): Promise<number> {
const rows = await knex("files")
.where("source_attribution", "Web API")
.count("*", { as: "uploads" });
return Number(rows[0].uploads);
}
export async function getUploadsAwaitingProcessingCount(): Promise<number> {
const rows = await knex("skin_uploads")
.where("status", "UPLOAD_REPORTED")
.count("*", { as: "uploads" });
return Number(rows[0].uploads);
}
export async function getUploadsErroredCount(): Promise<number> {
const rows = await knex("skin_uploads")
.where("status", "ERRORED")
.count("*", { as: "uploads" });
return Number(rows[0].uploads);
}
export async function getStats(): Promise<{
approved: number;
rejected: number;
@ -437,50 +492,15 @@ export async function getStats(): Promise<{
uploadsAwaitingProcessing: number;
uploadsErrored: number;
}> {
const approved = (
await knex("skin_reviews")
.first(knex.raw(`COUNT(DISTINCT skin_md5) AS "approved_count"`))
.where({ review: "APPROVED" })
).approved_count;
const rejected = (
await knex("skin_reviews")
.first(knex.raw(`COUNT(DISTINCT skin_md5) AS "rejected_count"`))
.where({ review: "REJECTED" })
).rejected_count;
const nsfw = (
await knex("skin_reviews")
.first(knex.raw(`COUNT(DISTINCT skin_md5) AS "nsfw_count"`))
.where({ review: "NSFW" })
).nsfw_count;
const tweeted = (await knex("tweets").count("*", { as: "tweeted" }))[0]
.tweeted;
const webUploads = (
await knex("files")
.where("source_attribution", "Web API")
.count("*", { as: "uploads" })
)[0].uploads;
const uploadsAwaitingProcessing = (
await knex("skin_uploads")
.where("status", "UPLOAD_REPORTED")
.count("*", { as: "uploads" })
)[0].uploads;
const uploadsErrored = (
await knex("skin_uploads")
.where("status", "ERRORED")
.count("*", { as: "uploads" })
)[0].uploads;
const tweetable = await getTweetableSkinCount();
return {
approved: Number(approved),
rejected: Number(rejected),
nsfw: Number(nsfw),
tweeted: Number(tweeted),
tweetable,
webUploads: Number(webUploads),
uploadsAwaitingProcessing: Number(uploadsAwaitingProcessing),
uploadsErrored: Number(uploadsErrored),
approved: await getApprovedSkinCount(),
rejected: await getRejectedSkinCount(),
nsfw: await getNsfwSkinCount(),
tweeted: await getTweetedSkinCount(),
tweetable: await getTweetableSkinCount(),
webUploads: await getWebUploadsCount(),
uploadsAwaitingProcessing: await getUploadsAwaitingProcessingCount(),
uploadsErrored: await getUploadsErroredCount(),
};
}