From a7173e8da50b70652b0546afc2a8f767475facb8 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Thu, 24 Feb 2022 23:50:49 -0800 Subject: [PATCH] Expose statistics in GraphQL --- .../resolvers/DatabaseStatisticsResolver.ts | 34 ++++++ .../api/graphql/resolvers/RootResolver.ts | 7 +- .../skin-database/api/graphql/schema.graphql | 71 ++++++++++++ packages/skin-database/data/skins.ts | 106 +++++++++++------- 4 files changed, 174 insertions(+), 44 deletions(-) create mode 100644 packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts diff --git a/packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts b/packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts new file mode 100644 index 00000000..c93901f8 --- /dev/null +++ b/packages/skin-database/api/graphql/resolvers/DatabaseStatisticsResolver.ts @@ -0,0 +1,34 @@ +import * as Skins from "../../../data/Skins"; + +export default class DatabaseStatisticsResolver { + unique_classic_skins_count(): Promise { + return Skins.getClassicSkinCount(); + } + tweeted_skins_count(): Promise { + return Skins.getTweetedSkinCount(); + } + approved_skins_count(): Promise { + return Skins.getApprovedSkinCount(); + } + rejected_skins_count(): Promise { + return Skins.getRejectedSkinCount(); + } + nsfw_skins_count(): Promise { + return Skins.getNsfwSkinCount(); + } + unreviewed_skins_count(): Promise { + return Skins.getUnreviewedSkinCount(); + } + tweetable_skins_count(): Promise { + return Skins.getTweetableSkinCount(); + } + uploads_pending_processing_count(): Promise { + return Skins.getUploadsAwaitingProcessingCount(); + } + uploads_in_error_state_count(): Promise { + return Skins.getUploadsErroredCount(); + } + web_uploads_count(): Promise { + return Skins.getWebUploadsCount(); + } +} diff --git a/packages/skin-database/api/graphql/resolvers/RootResolver.ts b/packages/skin-database/api/graphql/resolvers/RootResolver.ts index 9d1fa593..976c7e77 100644 --- a/packages/skin-database/api/graphql/resolvers/RootResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/RootResolver.ts @@ -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; diff --git a/packages/skin-database/api/graphql/schema.graphql b/packages/skin-database/api/graphql/schema.graphql index db74f17f..18f14ac7 100644 --- a/packages/skin-database/api/graphql/schema.graphql +++ b/packages/skin-database/api/graphql/schema.graphql @@ -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 } diff --git a/packages/skin-database/data/skins.ts b/packages/skin-database/data/skins.ts index f8074e0c..400b28d0 100644 --- a/packages/skin-database/data/skins.ts +++ b/packages/skin-database/data/skins.ts @@ -427,6 +427,61 @@ export async function getSkinToPostToInstagram(): Promise { return skin.md5; } +export async function getUnreviewedSkinCount(): Promise { + 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 { + 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 { + 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 { + 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 { + const rows = await knex("tweets").count("*", { as: "tweeted" }); + return Number(rows[0].tweeted); +} + +export async function getWebUploadsCount(): Promise { + const rows = await knex("files") + .where("source_attribution", "Web API") + .count("*", { as: "uploads" }); + return Number(rows[0].uploads); +} + +export async function getUploadsAwaitingProcessingCount(): Promise { + const rows = await knex("skin_uploads") + .where("status", "UPLOAD_REPORTED") + .count("*", { as: "uploads" }); + return Number(rows[0].uploads); +} + +export async function getUploadsErroredCount(): Promise { + 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(), }; }