Skip to content

Commit

Permalink
Upgrade grats
Browse files Browse the repository at this point in the history
  • Loading branch information
captbaritone committed Dec 7, 2023
1 parent 4dbc326 commit c53ad16
Show file tree
Hide file tree
Showing 23 changed files with 1,425 additions and 254 deletions.
1 change: 1 addition & 0 deletions packages/skin-database/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
api/graphql/schema.ts
1 change: 1 addition & 0 deletions packages/skin-database/api/graphql/GqlCtx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type GqlCtx = Express.Request;
6 changes: 5 additions & 1 deletion packages/skin-database/api/graphql/ModernSkinsConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { knex } from "../../db";
import ModernSkinResolver from "./resolvers/ModernSkinResolver";
import { Root } from "aws-sdk/clients/organizations";
import RootResolver from "./resolvers/RootResolver";
import { GqlCtx } from "./GqlCtx";

/**
* A collection of "modern" Winamp skins
Expand Down Expand Up @@ -31,7 +32,10 @@ export default class ModernSkinsConnection {
/**
* The list of skins
* @gqlField */
async nodes(_args: never, ctx): Promise<Array<ModernSkinResolver | null>> {
async nodes(
_args: unknown,
ctx: GqlCtx
): Promise<Array<ModernSkinResolver | null>> {
const skins = await this._getQuery()
.select()
.limit(this._first)
Expand Down
3 changes: 2 additions & 1 deletion packages/skin-database/api/graphql/SkinsConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import LRU from "lru-cache";
import { Int } from "grats";
import { ISkin } from "./resolvers/CommonSkinResolver";
import RootResolver from "./resolvers/RootResolver";
import { GqlCtx } from "./GqlCtx";

const options = {
max: 100,
Expand Down Expand Up @@ -97,7 +98,7 @@ export default class SkinsConnection {
* The list of skins
* @gqlField
*/
async nodes(args: never, ctx): Promise<Array<ISkin | null>> {
async nodes(args: unknown, ctx: GqlCtx): Promise<Array<ISkin | null>> {
if (this._sort === "MUSEUM") {
if (this._filter) {
throw new Error(
Expand Down
6 changes: 5 additions & 1 deletion packages/skin-database/api/graphql/TweetsConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import TweetModel from "../../data/TweetModel";
import { knex } from "../../db";
import TweetResolver from "./resolvers/TweetResolver";
import RootResolver from "./resolvers/RootResolver";
import { GqlCtx } from "./GqlCtx";

/** @gqlEnum */
export type TweetsSortOption = "LIKES" | "RETWEETS";
Expand Down Expand Up @@ -45,7 +46,10 @@ export default class TweetsConnection {
* The list of tweets
* @gqlField
*/
async nodes(args: never, ctx): Promise<Array<TweetResolver | null>> {
async nodes(
args: unknown,
ctx: GqlCtx
): Promise<Array<TweetResolver | null>> {
const tweets = await this._getQuery()
.select()
.limit(this._first)
Expand Down
11 changes: 2 additions & 9 deletions packages/skin-database/api/graphql/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import { Router } from "express";
import { graphqlHTTP } from "express-graphql";

import RootResolver from "./resolvers/RootResolver";
import DEFAULT_QUERY from "./defaultQuery";
import { buildSchema } from "graphql";
import fs from "fs";
import path from "path";

const schemaPath = path.join(__dirname, "./schema.graphql");
const schema = buildSchema(fs.readFileSync(schemaPath, "utf8"));
import { schema } from "./schema";

const router = Router();

Expand Down Expand Up @@ -44,8 +38,7 @@ const extensions = ({
router.use(
"/",
graphqlHTTP({
schema: schema,
rootValue: new RootResolver(),
schema,
graphiql: {
defaultQuery: DEFAULT_QUERY,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import SkinModel from "../../../data/SkinModel";
import { ISkin } from "./CommonSkinResolver";
import SkinResolver from "./SkinResolver";
import RootResolver from "./RootResolver";
import { GqlCtx } from "../GqlCtx";

/**
* A file found within a Winamp Skin's .wsz archive
Expand Down Expand Up @@ -66,7 +67,7 @@ export default class ArchiveFileResolver {
* The skin in which this file was found
* @gqlField
*/
async skin(_: never, { ctx }): Promise<ISkin | null> {
async skin(_: unknown, { ctx }: GqlCtx): Promise<ISkin | null> {
const model = await SkinModel.fromMd5Assert(ctx, this._model.getMd5());
return SkinResolver.fromModel(model);
}
Expand All @@ -90,7 +91,7 @@ export default class ArchiveFileResolver {
export async function fetch_archive_file_by_md5(
_: RootResolver,
{ md5 }: { md5: string },
{ ctx }
{ ctx }: GqlCtx
): Promise<ArchiveFileResolver | null> {
const archiveFile = await ArchiveFileModel.fromFileMd5(ctx, md5);
if (archiveFile == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import IaItemModel from "../../../data/IaItemModel";
import { GqlCtx } from "../GqlCtx";
import { ISkin } from "./CommonSkinResolver";
import RootResolver from "./RootResolver";
import SkinResolver from "./SkinResolver";
Expand Down Expand Up @@ -70,7 +71,7 @@ export default class InternetArchiveItemResolver {
export async function fetch_internet_archive_item_by_identifier(
_: RootResolver,
{ identifier }: { identifier: string },
{ ctx }
{ ctx }: GqlCtx
): Promise<InternetArchiveItemResolver | null> {
const iaItem = await IaItemModel.fromIdentifier(ctx, identifier);
if (iaItem == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ArchiveFileResolver from "./ArchiveFileResolver";
import TweetResolver from "./TweetResolver";
import { XMLParser } from "fast-xml-parser";
import RootResolver from "./RootResolver";
import { GqlCtx } from "../GqlCtx";

/**
* A "modern" Winamp skin. These skins use the `.wal` file extension and are free-form.
Expand Down Expand Up @@ -179,7 +180,7 @@ export default class ModernSkinResolver
export async function fetch_skin_by_md5(
_: RootResolver,
{ md5 }: { md5: string },
{ ctx }
{ ctx }: GqlCtx
): Promise<ISkin | null> {
const skin = await SkinModel.fromMd5(ctx, md5);
if (skin == null) {
Expand Down
198 changes: 18 additions & 180 deletions packages/skin-database/api/graphql/resolvers/MutationResolver.ts
Original file line number Diff line number Diff line change
@@ -1,85 +1,6 @@
import * as Parallel from "async-parallel";
import SkinModel from "../../../data/SkinModel";
import * as S3 from "../../../s3";
import * as Skins from "../../../data/skins";
import { processUserUploads } from "../../processUserUploads";
import { GqlCtx } from "../GqlCtx";

// We don't use a resolver here, just return the value directly.
/**
* A URL that the client can use to upload a skin to S3, and then notify the server
* when they're done.
* @gqlType
*/
type UploadUrl = {
/** @gqlField */
id: string;
/** @gqlField */
url: string;
/** @gqlField */
md5: string;
};

/**
* Input object used for a user to request an UploadUrl
* @gqlInput
*/
type UploadUrlRequest = { filename: string; md5: string };

/**
* Mutations for the upload flow
*
* 1. The user finds the md5 hash of their local files.
* 2. (`get_upload_urls`) The user requests upload URLs for each of their files.
* 3. The server returns upload URLs for each of their files which are not already in the collection.
* 4. The user uploads each of their files to the URLs returned in step 3.
* 5. (`report_skin_uploaded`) The user notifies the server that they're done uploading.
* 6. (TODO) The user polls for the status of their uploads.
*
* @gqlType UploadMutations */
class UploadMutationResolver {
/**
* Get a (possibly incompelte) list of UploadUrls for each of the files. If an
* UploadUrl is not returned for a given hash, it means the file is already in
* the collection.
* @gqlField
*/
async get_upload_urls(
{ files }: { files: UploadUrlRequest[] },
{ ctx }
): Promise<Array<UploadUrl | null>> {
const missing: UploadUrl[] = [];
await Parallel.each(
files,
async ({ md5, filename }) => {
if (!(await SkinModel.exists(ctx, md5))) {
const id = await Skins.recordUserUploadRequest(md5, filename);
const url = S3.getSkinUploadUrl(md5, id);
missing.push({ id, url, md5 });
}
},
5
);

return missing;
}

/**
* Notify the server that the user is done uploading.
* @gqlField
*/
async report_skin_uploaded(
{ id, md5 }: { id: string; md5: string },
req
): Promise<boolean> {
// TODO: Validate md5 and id;
await Skins.recordUserUploadComplete(md5, id);
// Don't await, just kick off the task.
processUserUploads(req.notify);
return true;
}
}

function requireAuthed(handler) {
export function requireAuthed(handler) {
return (args, req) => {
if (!req.ctx.authed()) {
throw new Error("You must be logged in to read this field.");
Expand All @@ -89,105 +10,22 @@ function requireAuthed(handler) {
};
}

/**
*
* @gqlType Mutation */
export default class MutationResolver {
/**
* Mutations for the upload flow
* @gqlField */
async upload(): Promise<UploadMutationResolver> {
return new UploadMutationResolver();
}
/**
* Send a message to the admin of the site. Currently this appears in Discord.
* @gqlField */
async send_feedback(
{ message, email, url }: { message: string; email?: string; url?: string },
req
): Promise<boolean> {
req.notify({
type: "GOT_FEEDBACK",
url,
message,
email,
});
return true;
}

/**
* Reject skin for tweeting
*
* **Note:** Requires being logged in
* @gqlField */
reject_skin(args: { md5: string }, req): Promise<boolean> {
return this._reject_skin(args, req);
}
/** @gqlType Mutation */
export type MutationResolver = unknown;

_reject_skin = requireAuthed(async ({ md5 }, req) => {
req.log(`Rejecting skin with hash "${md5}"`);
const skin = await SkinModel.fromMd5Assert(req.ctx, md5);
if (skin == null) {
return false;
}
await Skins.reject(req.ctx, md5);
req.notify({ type: "REJECTED_SKIN", md5 });
return true;
});

/**
* Approve skin for tweeting
*
* **Note:** Requires being logged in
* @gqlField */
approve_skin(args: { md5: string }, req): Promise<boolean> {
return this._approve_skin(args, req);
}

_approve_skin = requireAuthed(async ({ md5 }, req) => {
req.log(`Approving skin with hash "${md5}"`);
const skin = await SkinModel.fromMd5(req.ctx, md5);
if (skin == null) {
return false;
}
await Skins.approve(req.ctx, md5);
req.notify({ type: "APPROVED_SKIN", md5 });
return true;
});

/**
* Mark a skin as NSFW
*
* **Note:** Requires being logged in
* @gqlField */
mark_skin_nsfw(args: { md5: string }, req): Promise<boolean> {
return this._mark_skin_nsfw(args, req);
}

_mark_skin_nsfw = requireAuthed(async ({ md5 }, req) => {
req.log(`Approving skin with hash "${md5}"`);
const skin = await SkinModel.fromMd5(req.ctx, md5);
if (skin == null) {
return false;
}
await Skins.markAsNSFW(req.ctx, md5);
req.notify({ type: "MARKED_SKIN_NSFW", md5 });
return true;
/**
* Send a message to the admin of the site. Currently this appears in Discord.
* @gqlField */
export async function send_feedback(
_: MutationResolver,
{ message, email, url }: { message: string; email?: string; url?: string },
req: GqlCtx
): Promise<boolean> {
req.notify({
type: "GOT_FEEDBACK",
url,
message,
email,
});

/**
* Request that an admin check if this skin is NSFW.
* Unlike other review mutaiton endpoints, this one does not require being logged
* in.
* @gqlField */
async request_nsfw_review_for_skin(
{ md5 }: { md5: string },
req
): Promise<boolean> {
req.log(`Reporting skin with hash "${md5}"`);
// Blow up if there is no skin with this hash
await SkinModel.fromMd5Assert(req.ctx, md5);
req.notify({ type: "REVIEW_REQUESTED", md5 });
return true;
}
return true;
}
3 changes: 2 additions & 1 deletion packages/skin-database/api/graphql/resolvers/NodeResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ID } from "grats";
import SkinModel from "../../../data/SkinModel";
import SkinResolver from "../resolvers/SkinResolver";
import RootResolver from "./RootResolver";
import { GqlCtx } from "../GqlCtx";

/**
* A globally unique object. The `id` here is intended only for use within
Expand All @@ -29,7 +30,7 @@ export interface NodeResolver {
export async function node(
_: RootResolver,
{ id }: { id: ID },
{ ctx }
{ ctx }: GqlCtx
): Promise<NodeResolver | null> {
const { graphqlType, id: localId } = fromId(id);
// TODO Use typeResolver
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { Rating, ReviewRow } from "../../../types";
import { GqlCtx } from "../GqlCtx";
import { ISkin } from "./CommonSkinResolver";
import SkinResolver from "./SkinResolver";

/**
* A review of a skin. Done either on the Museum's Tinder-style
* reivew page, or via the Discord bot.
* review page, or via the Discord bot.
* @gqlType Review */
export default class ReviewResolver {
_model: ReviewRow;
Expand All @@ -16,7 +17,7 @@ export default class ReviewResolver {
* The skin that was reviewed
* @gqlField
*/
skin(args: never, { ctx }): Promise<ISkin | null> {
skin(args: unknown, { ctx }: GqlCtx): Promise<ISkin | null> {
return SkinResolver.fromMd5(ctx, this._model.skin_md5);
}

Expand Down
Loading

0 comments on commit c53ad16

Please sign in to comment.