mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-20 01:54:01 +00:00
Upgrade grats
This commit is contained in:
parent
0f431d9e05
commit
1c8b4e0478
18 changed files with 1997 additions and 1210 deletions
1
packages/skin-database/.prettierignore
Normal file
1
packages/skin-database/.prettierignore
Normal file
|
|
@ -0,0 +1 @@
|
|||
api/graphql/schema.ts
|
||||
|
|
@ -231,7 +231,7 @@ describe("Query.skins", () => {
|
|||
});
|
||||
|
||||
// TODO: Upgrade Grats
|
||||
test.skip("Query.fetch_skin_by_md5 (debug data)", async () => {
|
||||
test("Query.fetch_skin_by_md5 (debug data)", async () => {
|
||||
const { data } = await graphQLRequest(
|
||||
gql`
|
||||
query MyQuery($md5: String!) {
|
||||
|
|
|
|||
|
|
@ -21,7 +21,12 @@ export type ApiAction =
|
|||
| { type: "CLASSIC_SKIN_UPLOADED"; md5: string }
|
||||
| { type: "MODERN_SKIN_UPLOADED"; md5: string }
|
||||
| { type: "SKIN_UPLOAD_ERROR"; uploadId: string; message: string }
|
||||
| { type: "GOT_FEEDBACK"; message: string; email?: string; url?: string }
|
||||
| {
|
||||
type: "GOT_FEEDBACK";
|
||||
message: string;
|
||||
email?: string | null;
|
||||
url?: string | null;
|
||||
}
|
||||
| { type: "SYNCED_TO_ARCHIVE"; successes: number; errors: number }
|
||||
| { type: "STARTED_SYNC_TO_ARCHIVE"; count: number }
|
||||
| {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Int } from "grats";
|
|||
import SkinModel from "../../data/SkinModel";
|
||||
import { knex } from "../../db";
|
||||
import ModernSkinResolver from "./resolvers/ModernSkinResolver";
|
||||
import { Ctx } from ".";
|
||||
|
||||
/**
|
||||
* A collection of "modern" Winamp skins
|
||||
|
|
@ -29,7 +30,10 @@ export default class ModernSkinsConnection {
|
|||
/**
|
||||
* The list of skins
|
||||
* @gqlField */
|
||||
async nodes(_args: never, ctx): Promise<Array<ModernSkinResolver | null>> {
|
||||
async nodes(
|
||||
_args: unknown,
|
||||
{ ctx }: Ctx
|
||||
): Promise<Array<ModernSkinResolver | null>> {
|
||||
const skins = await this._getQuery()
|
||||
.select()
|
||||
.limit(this._first)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import SkinResolver from "./resolvers/SkinResolver";
|
|||
import LRU from "lru-cache";
|
||||
import { Int } from "grats";
|
||||
import { ISkin } from "./resolvers/CommonSkinResolver";
|
||||
import { Ctx } from ".";
|
||||
|
||||
const options = {
|
||||
max: 100,
|
||||
|
|
@ -43,9 +44,14 @@ async function getSkinMuseumPageFromCache(first: number, offset: number) {
|
|||
export default class SkinsConnection {
|
||||
_first: number;
|
||||
_offset: number;
|
||||
_sort?: string;
|
||||
_filter?: string;
|
||||
constructor(first: number, offset: number, sort?: string, filter?: string) {
|
||||
_sort?: string | null;
|
||||
_filter?: string | null;
|
||||
constructor(
|
||||
first: number,
|
||||
offset: number,
|
||||
sort?: string | null,
|
||||
filter?: string | null
|
||||
) {
|
||||
this._first = first;
|
||||
this._offset = offset;
|
||||
this._filter = filter;
|
||||
|
|
@ -96,7 +102,7 @@ export default class SkinsConnection {
|
|||
* The list of skins
|
||||
* @gqlField
|
||||
*/
|
||||
async nodes(args: never, ctx): Promise<Array<ISkin | null>> {
|
||||
async nodes(args: unknown, { ctx }: Ctx): Promise<Array<ISkin | null>> {
|
||||
if (this._sort === "MUSEUM") {
|
||||
if (this._filter) {
|
||||
throw new Error(
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ export type TweetsSortOption = "LIKES" | "RETWEETS";
|
|||
export default class TweetsConnection {
|
||||
_first: number;
|
||||
_offset: number;
|
||||
_sort?: TweetsSortOption;
|
||||
constructor(first: number, offset: number, sort?: TweetsSortOption) {
|
||||
_sort?: TweetsSortOption | null;
|
||||
constructor(first: number, offset: number, sort?: TweetsSortOption | null) {
|
||||
this._first = first;
|
||||
this._offset = offset;
|
||||
this._sort = sort;
|
||||
|
|
@ -43,7 +43,7 @@ export default class TweetsConnection {
|
|||
* The list of tweets
|
||||
* @gqlField
|
||||
*/
|
||||
async nodes(args: never, ctx): Promise<Array<TweetModel | null>> {
|
||||
async nodes(): Promise<Array<TweetModel | null>> {
|
||||
return this._getQuery().select().limit(this._first).offset(this._offset);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,497 +0,0 @@
|
|||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION
|
||||
|
||||
directive @methodName(name: String!) on FIELD_DEFINITION
|
||||
|
||||
"""A file found within a Winamp Skin's .wsz archive"""
|
||||
type ArchiveFile {
|
||||
"""
|
||||
The date on the file inside the archive. Given in simplified extended ISO
|
||||
format (ISO 8601).
|
||||
"""
|
||||
date: String
|
||||
"""The md5 hash of the file within the archive"""
|
||||
file_md5: String
|
||||
"""Filename of the file within the archive"""
|
||||
filename: String
|
||||
"""Is the file a directory?"""
|
||||
is_directory: Boolean
|
||||
"""
|
||||
The uncompressed size of the file in bytes.
|
||||
|
||||
**Note:** Will be `null` for directories
|
||||
"""
|
||||
size: Int
|
||||
"""The skin in which this file was found"""
|
||||
skin: Skin
|
||||
"""The content of the file, if it's a text file"""
|
||||
text_content: String
|
||||
"""
|
||||
A URL to download the file. **Note:** This is powered by a little
|
||||
serverless Cloudflare function which tries to exctact the file on the fly.
|
||||
It may not work for all files.
|
||||
"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A classic Winamp skin"""
|
||||
type ClassicSkin implements Node & Skin {
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
"""String representation (rgb usually) of the skin's average color"""
|
||||
average_color: String
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
"""
|
||||
Filename of skin when uploaded to the Museum. Note: In some cases a skin
|
||||
has been uploaded under multiple names. Here we just pick one.
|
||||
"""
|
||||
filename(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""Does the skin include sprite sheets for the media library?"""
|
||||
has_media_library: Boolean
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""
|
||||
The date on which this skin was last updated in the Algolia search index.
|
||||
Given in simplified extended ISO format (ISO 8601).
|
||||
"""
|
||||
last_algolia_index_update_date: String
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
"""URL of the skin on the Winamp Skin Museum"""
|
||||
museum_url: String
|
||||
"""Has the skin been flagged as "not safe for wrok"?"""
|
||||
nsfw: Boolean
|
||||
"""Text of the readme file extracted from the skin"""
|
||||
readme_text: String
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
"""URL of a screenshot of the skin"""
|
||||
screenshot_url: String
|
||||
"""The number of transparent pixels rendered by the skin."""
|
||||
transparent_pixels: Int
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet]
|
||||
"""URL of webamp.org with the skin loaded"""
|
||||
webamp_url: String
|
||||
}
|
||||
|
||||
"""Statistics about the contents of the Museum's database."""
|
||||
type DatabaseStatistics {
|
||||
"""
|
||||
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 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 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 approved for tweeting, but not yet tweeted.
|
||||
"""
|
||||
tweetable_skins_count: Int
|
||||
"""
|
||||
The number of skins in the Museum that have been tweeted by @winampskins
|
||||
"""
|
||||
tweeted_skins_count: Int
|
||||
"""The total number of classic skins in the Museum's database"""
|
||||
unique_classic_skins_count: Int
|
||||
"""The number of skins that have never been reviewed."""
|
||||
unreviewed_skins_count: Int
|
||||
"""Skins uploads that have errored during processing."""
|
||||
uploads_in_error_state_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
|
||||
"""
|
||||
Number of skins that have been uploaded to the Museum via the web interface.
|
||||
"""
|
||||
web_uploads_count: Int
|
||||
}
|
||||
|
||||
type InternetArchiveItem {
|
||||
"""The Internet Archive's unique identifier for this item"""
|
||||
identifier: String
|
||||
"""
|
||||
The date and time that we last scraped this item's metadata.
|
||||
**Note:** This field is temporary and will be removed in the future.
|
||||
The date format is just what we get from the database, and it's ambiguous.
|
||||
"""
|
||||
last_metadata_scrape_date_UNSTABLE: String
|
||||
"""URL to get the Internet Archive's metadata for this item in JSON form."""
|
||||
metadata_url: String
|
||||
"""
|
||||
Our cached version of the metadata avaliable at \`metadata_url\` (above)
|
||||
"""
|
||||
raw_metadata_json: String
|
||||
"""The skin that this item contains"""
|
||||
skin: Skin
|
||||
"""The URL where this item can be viewed on the Internet Archive"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""
|
||||
A "modern" Winamp skin. These skins use the `.wal` file extension and are free-form.
|
||||
|
||||
Most functionality in the Winamp Skin Museum is centered around "classic" skins,
|
||||
which are currently called just `Skin` in this schema.
|
||||
"""
|
||||
type ModernSkin implements Node & Skin {
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
average_color: String @deprecated(reason: "Needed for migration")
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
"""
|
||||
Filename of skin when uploaded to the Museum. Note: In some cases a skin
|
||||
has been uploaded under multiple names. Here we just pick one.
|
||||
"""
|
||||
filename(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
museum_url: String @deprecated(reason: "Needed for migration")
|
||||
nsfw: Boolean @deprecated(reason: "Needed for migration")
|
||||
readme_text: String @deprecated(reason: "Needed for migration")
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
screenshot_url: String @deprecated(reason: "Needed for migration")
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet]
|
||||
webamp_url: String @deprecated(reason: "Needed for migration")
|
||||
}
|
||||
|
||||
"""A collection of "modern" Winamp skins"""
|
||||
type ModernSkinsConnection {
|
||||
"""The total number of skins matching the filter"""
|
||||
count: Int
|
||||
"""The list of skins"""
|
||||
nodes: [ModernSkin]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
"""
|
||||
Approve skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
approve_skin(md5: String!): Boolean
|
||||
"""
|
||||
Mark a skin as NSFW
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
mark_skin_nsfw(md5: String!): Boolean
|
||||
"""
|
||||
Reject skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
reject_skin(md5: String!): Boolean
|
||||
"""
|
||||
Request that an admin check if this skin is NSFW.
|
||||
Unlike other review mutaiton endpoints, this one does not require being logged
|
||||
in.
|
||||
"""
|
||||
request_nsfw_review_for_skin(md5: String!): Boolean
|
||||
"""
|
||||
Send a message to the admin of the site. Currently this appears in Discord.
|
||||
"""
|
||||
send_feedback(email: String, message: String!, url: String): Boolean
|
||||
"""Mutations for the upload flow"""
|
||||
upload: UploadMutations
|
||||
}
|
||||
|
||||
"""
|
||||
A globally unique object. The `id` here is intended only for use within
|
||||
GraphQL.
|
||||
https://graphql.org/learn/global-object-identification/
|
||||
"""
|
||||
interface Node {
|
||||
id: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
"""
|
||||
Fetch archive file by it's MD5 hash
|
||||
|
||||
Get information about a file found within a skin's wsz/wal/zip archive.
|
||||
"""
|
||||
fetch_archive_file_by_md5(md5: String!): ArchiveFile
|
||||
"""
|
||||
Get an archive.org item by its identifier. You can find this in the URL:
|
||||
|
||||
https://archive.org/details/<identifier>/
|
||||
"""
|
||||
fetch_internet_archive_item_by_identifier(identifier: String!): InternetArchiveItem
|
||||
"""Get a skin by its MD5 hash"""
|
||||
fetch_skin_by_md5(md5: String!): Skin
|
||||
"""Get a tweet by its URL"""
|
||||
fetch_tweet_by_url(url: String!): Tweet
|
||||
"""The currently authenticated user, if any."""
|
||||
me: User
|
||||
"""All modern skins in the database"""
|
||||
modern_skins(first: Int = 10, offset: Int = 0): ModernSkinsConnection
|
||||
"""
|
||||
Get a globally unique object by its ID.
|
||||
|
||||
https://graphql.org/learn/global-object-identification/
|
||||
"""
|
||||
node(id: ID!): Node
|
||||
"""
|
||||
Search the database using the Algolia search index used by the Museum.
|
||||
|
||||
Useful for locating a particular skin.
|
||||
"""
|
||||
search_skins(first: Int = 10, offset: Int = 0, query: String!): [Skin]
|
||||
"""A random skin that needs to be reviewed"""
|
||||
skin_to_review: Skin
|
||||
"""
|
||||
All classic skins in the database
|
||||
|
||||
**Note:** We don't currently support combining sorting and filtering.
|
||||
"""
|
||||
skins(filter: SkinsFilterOption, first: Int = 10, offset: Int = 0, sort: SkinsSortOption): SkinsConnection
|
||||
"""A namespace for statistics about the database"""
|
||||
statistics: DatabaseStatistics
|
||||
"""Tweets tweeted by @winampskins"""
|
||||
tweets(first: Int = 10, offset: Int = 0, sort: TweetsSortOption): TweetsConnection
|
||||
"""Get the status of a batch of uploads by ids"""
|
||||
upload_statuses(ids: [String!]!): [SkinUpload]
|
||||
"""Get the status of a batch of uploads by md5s"""
|
||||
upload_statuses_by_md5(md5s: [String!]!): [SkinUpload] @deprecated(reason: "Prefer `upload_statuses` instead, were we operate on ids.")
|
||||
}
|
||||
|
||||
"""The judgement made about a skin by a moderator"""
|
||||
enum Rating {
|
||||
APPROVED
|
||||
NSFW
|
||||
REJECTED
|
||||
}
|
||||
|
||||
"""
|
||||
A review of a skin. Done either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
type Review {
|
||||
"""The rating that the user gave the skin"""
|
||||
rating: Rating
|
||||
"""
|
||||
The user who made the review (if known). **Note:** In the early days we didn't
|
||||
track this, so many will be null.
|
||||
"""
|
||||
reviewer: String
|
||||
"""The skin that was reviewed"""
|
||||
skin: Skin
|
||||
}
|
||||
|
||||
"""
|
||||
A Winamp skin. Could be modern or classic.
|
||||
|
||||
**Note**: At some point in the future, this might be renamed to `Skin`.
|
||||
"""
|
||||
interface Skin {
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
average_color: String @deprecated(reason: "Needed for migration")
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
"""
|
||||
Filename of skin when uploaded to the Museum. Note: In some cases a skin
|
||||
has been uploaded under multiple names. Here we just pick one.
|
||||
"""
|
||||
filename(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
museum_url: String @deprecated(reason: "Needed for migration")
|
||||
nsfw: Boolean @deprecated(reason: "Needed for migration")
|
||||
readme_text: String @deprecated(reason: "Needed for migration")
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
screenshot_url: String @deprecated(reason: "Needed for migration")
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet]
|
||||
webamp_url: String @deprecated(reason: "Needed for migration")
|
||||
}
|
||||
|
||||
"""Information about an attempt to upload a skin to the Museum."""
|
||||
type SkinUpload {
|
||||
id: String
|
||||
"""
|
||||
Skin that was uploaded. **Note:** This is null if the skin has not yet been
|
||||
fully processed. (status == ARCHIVED)
|
||||
"""
|
||||
skin: Skin
|
||||
status: SkinUploadStatus
|
||||
"""Md5 hash given when requesting the upload URL."""
|
||||
upload_md5: String
|
||||
}
|
||||
|
||||
"""
|
||||
The current status of a pending upload.
|
||||
|
||||
**Note:** Expect more values here as we try to be more transparent about
|
||||
the status of a pending uploads.
|
||||
"""
|
||||
enum SkinUploadStatus {
|
||||
ARCHIVED
|
||||
DELAYED
|
||||
ERRORED
|
||||
UPLOAD_REPORTED
|
||||
URL_REQUESTED
|
||||
}
|
||||
|
||||
"""A collection of classic Winamp skins"""
|
||||
type SkinsConnection {
|
||||
"""The total number of skins matching the filter"""
|
||||
count: Int
|
||||
"""The list of skins"""
|
||||
nodes: [Skin]
|
||||
}
|
||||
|
||||
enum SkinsFilterOption {
|
||||
APPROVED
|
||||
NSFW
|
||||
REJECTED
|
||||
TWEETED
|
||||
}
|
||||
|
||||
enum SkinsSortOption {
|
||||
MUSEUM
|
||||
}
|
||||
|
||||
"""A tweet made by @winampskins mentioning a Winamp skin"""
|
||||
type Tweet {
|
||||
"""
|
||||
Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here)
|
||||
"""
|
||||
likes: Int
|
||||
"""
|
||||
Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here)
|
||||
"""
|
||||
retweets: Int
|
||||
"""The skin featured in this Tweet"""
|
||||
skin: Skin
|
||||
"""
|
||||
URL of the tweet. **Note:** Early on in the bot's life we just recorded
|
||||
_which_ skins were tweeted, not any info about the actual tweet. This means we
|
||||
don't always know the URL of the tweet.
|
||||
"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A collection of tweets made by the @winampskins bot"""
|
||||
type TweetsConnection {
|
||||
"""The total number of tweets"""
|
||||
count: Int
|
||||
"""The list of tweets"""
|
||||
nodes: [Tweet]
|
||||
}
|
||||
|
||||
enum TweetsSortOption {
|
||||
LIKES
|
||||
RETWEETS
|
||||
}
|
||||
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
type UploadMutations {
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
get_upload_urls(files: [UploadUrlRequest!]!): [UploadUrl]
|
||||
"""Notify the server that the user is done uploading."""
|
||||
report_skin_uploaded(id: String!, md5: String!): Boolean
|
||||
}
|
||||
|
||||
"""
|
||||
A URL that the client can use to upload a skin to S3, and then notify the server
|
||||
when they're done.
|
||||
"""
|
||||
type UploadUrl {
|
||||
id: String
|
||||
md5: String
|
||||
url: String
|
||||
}
|
||||
|
||||
"""Input object used for a user to request an UploadUrl"""
|
||||
input UploadUrlRequest {
|
||||
filename: String!
|
||||
md5: String!
|
||||
}
|
||||
|
||||
type User {
|
||||
username: String
|
||||
}
|
||||
|
|
@ -1,14 +1,10 @@
|
|||
import { RequestHandler, 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";
|
||||
import { getSchema } from "./schema";
|
||||
|
||||
const schemaPath = path.join(__dirname, "./schema.graphql");
|
||||
const schema = buildSchema(fs.readFileSync(schemaPath, "utf8"));
|
||||
export type Ctx = Express.Request;
|
||||
|
||||
const router = Router();
|
||||
|
||||
|
|
@ -44,8 +40,7 @@ const extensions = ({
|
|||
router.use(
|
||||
"/",
|
||||
graphqlHTTP({
|
||||
schema: schema,
|
||||
rootValue: new RootResolver(),
|
||||
schema: getSchema(),
|
||||
graphiql: {
|
||||
defaultQuery: DEFAULT_QUERY,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import SkinModel from "../../../data/SkinModel";
|
|||
import * as S3 from "../../../s3";
|
||||
import * as Skins from "../../../data/skins";
|
||||
import { processUserUploads } from "../../processUserUploads";
|
||||
import { Ctx } from "..";
|
||||
|
||||
// We don't use a resolver here, just return the value directly.
|
||||
/**
|
||||
|
|
@ -45,7 +46,7 @@ class UploadMutationResolver {
|
|||
*/
|
||||
async get_upload_urls(
|
||||
{ files }: { files: UploadUrlRequest[] },
|
||||
{ ctx }
|
||||
{ ctx }: Ctx
|
||||
): Promise<Array<UploadUrl | null>> {
|
||||
const missing: UploadUrl[] = [];
|
||||
await Parallel.each(
|
||||
|
|
@ -69,7 +70,7 @@ class UploadMutationResolver {
|
|||
*/
|
||||
async report_skin_uploaded(
|
||||
{ id, md5 }: { id: string; md5: string },
|
||||
req
|
||||
req: Ctx
|
||||
): Promise<boolean> {
|
||||
// TODO: Validate md5 and id;
|
||||
await Skins.recordUserUploadComplete(md5, id);
|
||||
|
|
@ -80,7 +81,7 @@ class UploadMutationResolver {
|
|||
}
|
||||
|
||||
function requireAuthed(handler) {
|
||||
return (args, req) => {
|
||||
return (args, req: Ctx) => {
|
||||
if (!req.ctx.authed()) {
|
||||
throw new Error("You must be logged in to read this field.");
|
||||
} else {
|
||||
|
|
@ -92,102 +93,120 @@ 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;
|
||||
}
|
||||
type Mutation = unknown;
|
||||
|
||||
/**
|
||||
* Reject skin for tweeting
|
||||
*
|
||||
* **Note:** Requires being logged in
|
||||
* @gqlField */
|
||||
reject_skin(args: { md5: string }, req): Promise<boolean> {
|
||||
return this._reject_skin(args, req);
|
||||
}
|
||||
|
||||
_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;
|
||||
});
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
/**
|
||||
* Mutations for the upload flow
|
||||
* @gqlField */
|
||||
export async function upload(_: Mutation): Promise<UploadMutationResolver> {
|
||||
return new UploadMutationResolver();
|
||||
}
|
||||
/**
|
||||
* Send a message to the admin of the site. Currently this appears in Discord.
|
||||
* @gqlField */
|
||||
export async function send_feedback(
|
||||
_: Mutation,
|
||||
{
|
||||
message,
|
||||
email,
|
||||
url,
|
||||
}: { message: string; email?: string | null; url?: string | null },
|
||||
req: Ctx
|
||||
): Promise<boolean> {
|
||||
req.notify({
|
||||
type: "GOT_FEEDBACK",
|
||||
url,
|
||||
message,
|
||||
email,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reject skin for tweeting
|
||||
*
|
||||
* **Note:** Requires being logged in
|
||||
* @gqlField */
|
||||
export function reject_skin(
|
||||
_: Mutation,
|
||||
args: { md5: string },
|
||||
req: Ctx
|
||||
): Promise<boolean> {
|
||||
return _reject_skin(args, req);
|
||||
}
|
||||
|
||||
const _reject_skin = requireAuthed(async ({ md5 }, req: Ctx) => {
|
||||
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 */
|
||||
export function approve_skin(
|
||||
_: Mutation,
|
||||
args: { md5: string },
|
||||
req: Ctx
|
||||
): Promise<boolean> {
|
||||
return _approve_skin(args, req);
|
||||
}
|
||||
|
||||
const _approve_skin = requireAuthed(async ({ md5 }, req: Ctx) => {
|
||||
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 */
|
||||
export function mark_skin_nsfw(
|
||||
_: Mutation,
|
||||
args: { md5: string },
|
||||
req: Ctx
|
||||
): Promise<boolean> {
|
||||
return _mark_skin_nsfw(args, req);
|
||||
}
|
||||
|
||||
const _mark_skin_nsfw = requireAuthed(async ({ md5 }, req: Ctx) => {
|
||||
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;
|
||||
});
|
||||
|
||||
/**
|
||||
* Request that an admin check if this skin is NSFW.
|
||||
* Unlike other review mutaiton endpoints, this one does not require being logged
|
||||
* in.
|
||||
* @gqlField */
|
||||
export async function request_nsfw_review_for_skin(
|
||||
_: Mutation,
|
||||
{ md5 }: { md5: string },
|
||||
req: Ctx
|
||||
): 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { Ctx } from "..";
|
||||
import { Rating, ReviewRow } from "../../../types";
|
||||
import { ISkin } from "./CommonSkinResolver";
|
||||
import SkinResolver from "./SkinResolver";
|
||||
|
|
@ -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 }: Ctx): Promise<ISkin | null> {
|
||||
return SkinResolver.fromMd5(ctx, this._model.skin_md5);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,132 +9,144 @@ import * as Skins from "../../../data/skins";
|
|||
import { ID, Int } from "grats";
|
||||
|
||||
import algoliasearch from "algoliasearch";
|
||||
import MutationResolver from "./MutationResolver";
|
||||
import { knex } from "../../../db";
|
||||
import ArchiveFileModel from "../../../data/ArchiveFileModel";
|
||||
import DatabaseStatisticsResolver from "./DatabaseStatisticsResolver";
|
||||
import { fromId, NodeResolver } from "./NodeResolver";
|
||||
import ModernSkinsConnection from "../ModernSkinsConnection";
|
||||
import { ISkin } from "./CommonSkinResolver";
|
||||
import { Ctx } from "..";
|
||||
|
||||
// These keys are already in the web client, so they are not secret at all.
|
||||
const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51");
|
||||
const index = client.initIndex("Skins");
|
||||
|
||||
/** @gqlType Query */
|
||||
class RootResolver extends MutationResolver {
|
||||
/**
|
||||
* Get a globally unique object by its ID.
|
||||
*
|
||||
* https://graphql.org/learn/global-object-identification/
|
||||
* @gqlField
|
||||
*/
|
||||
async node({ id }: { id: ID }, { ctx }): Promise<NodeResolver | null> {
|
||||
const { graphqlType, id: localId } = fromId(id);
|
||||
// TODO Use typeResolver
|
||||
switch (graphqlType) {
|
||||
case "ClassicSkin":
|
||||
case "ModernSkin": {
|
||||
const skin = await SkinModel.fromMd5(ctx, localId);
|
||||
if (skin == null) {
|
||||
return null;
|
||||
}
|
||||
return SkinResolver.fromModel(skin);
|
||||
type Query = unknown;
|
||||
|
||||
/**
|
||||
* Get a globally unique object by its ID.
|
||||
*
|
||||
* https://graphql.org/learn/global-object-identification/
|
||||
* @gqlField
|
||||
*/
|
||||
export async function node(
|
||||
_: Query,
|
||||
{ id }: { id: ID },
|
||||
{ ctx }: Ctx
|
||||
): Promise<NodeResolver | null> {
|
||||
const { graphqlType, id: localId } = fromId(id);
|
||||
// TODO Use typeResolver
|
||||
switch (graphqlType) {
|
||||
case "ClassicSkin":
|
||||
case "ModernSkin": {
|
||||
const skin = await SkinModel.fromMd5(ctx, localId);
|
||||
if (skin == null) {
|
||||
return null;
|
||||
}
|
||||
return SkinResolver.fromModel(skin);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a skin by its MD5 hash
|
||||
* @gqlField
|
||||
*/
|
||||
export async function fetch_skin_by_md5(
|
||||
_: Query,
|
||||
{ md5 }: { md5: string },
|
||||
{ ctx }: Ctx
|
||||
): Promise<ISkin | null> {
|
||||
const skin = await SkinModel.fromMd5(ctx, md5);
|
||||
if (skin == null) {
|
||||
return null;
|
||||
}
|
||||
return SkinResolver.fromModel(skin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a skin by its MD5 hash
|
||||
* @gqlField
|
||||
*/
|
||||
async fetch_skin_by_md5(
|
||||
{ md5 }: { md5: string },
|
||||
{ ctx }
|
||||
): Promise<ISkin | null> {
|
||||
const skin = await SkinModel.fromMd5(ctx, md5);
|
||||
if (skin == null) {
|
||||
return null;
|
||||
}
|
||||
return SkinResolver.fromModel(skin);
|
||||
/**
|
||||
* Get a tweet by its URL
|
||||
* @gqlField
|
||||
*/
|
||||
export async function fetch_tweet_by_url(
|
||||
_: Query,
|
||||
{ url }: { url: string },
|
||||
{ ctx }: Ctx
|
||||
): Promise<TweetModel | null> {
|
||||
return TweetModel.fromAnything(ctx, url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an archive.org item by its identifier. You can find this in the URL:
|
||||
*
|
||||
* https://archive.org/details/<identifier>/
|
||||
* @gqlField
|
||||
*/
|
||||
export async function fetch_internet_archive_item_by_identifier(
|
||||
_: Query,
|
||||
{ identifier }: { identifier: string },
|
||||
{ ctx }: Ctx
|
||||
): Promise<IaItemModel | null> {
|
||||
return IaItemModel.fromIdentifier(ctx, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch archive file by it's MD5 hash
|
||||
*
|
||||
* Get information about a file found within a skin's wsz/wal/zip archive.
|
||||
* @gqlField
|
||||
*/
|
||||
export async function fetch_archive_file_by_md5(
|
||||
_: Query,
|
||||
{ md5 }: { md5: string },
|
||||
{ ctx }: Ctx
|
||||
): Promise<ArchiveFileModel | null> {
|
||||
return ArchiveFileModel.fromFileMd5(ctx, md5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the database using the Algolia search index used by the Museum.
|
||||
*
|
||||
* Useful for locating a particular skin.
|
||||
* @gqlField
|
||||
*/
|
||||
export async function search_skins(
|
||||
_: Query,
|
||||
{
|
||||
query,
|
||||
first = 10,
|
||||
offset = 0,
|
||||
}: { query: string; first?: Int; offset?: Int },
|
||||
{ ctx }: Ctx
|
||||
): Promise<Array<ISkin | null>> {
|
||||
if (first > 1000) {
|
||||
throw new Error("Can only query 1000 records via search.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a tweet by its URL
|
||||
* @gqlField
|
||||
*/
|
||||
async fetch_tweet_by_url(
|
||||
{ url }: { url: string },
|
||||
{ ctx }
|
||||
): Promise<TweetModel | null> {
|
||||
return TweetModel.fromAnything(ctx, url);
|
||||
}
|
||||
const results: { hits: { md5: string }[] } = await index.search(query, {
|
||||
attributesToRetrieve: ["md5"],
|
||||
length: first,
|
||||
offset,
|
||||
});
|
||||
|
||||
/**
|
||||
* Get an archive.org item by its identifier. You can find this in the URL:
|
||||
*
|
||||
* https://archive.org/details/<identifier>/
|
||||
* @gqlField
|
||||
*/
|
||||
async fetch_internet_archive_item_by_identifier(
|
||||
{ identifier }: { identifier: string },
|
||||
{ ctx }
|
||||
): Promise<IaItemModel | null> {
|
||||
return IaItemModel.fromIdentifier(ctx, identifier);
|
||||
}
|
||||
return Promise.all(
|
||||
results.hits.map(async (hit) => {
|
||||
const model = await SkinModel.fromMd5Assert(ctx, hit.md5);
|
||||
return SkinResolver.fromModel(model);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch archive file by it's MD5 hash
|
||||
*
|
||||
* Get information about a file found within a skin's wsz/wal/zip archive.
|
||||
* @gqlField
|
||||
*/
|
||||
async fetch_archive_file_by_md5(
|
||||
{ md5 }: { md5: string },
|
||||
{ ctx }
|
||||
): Promise<ArchiveFileModel | null> {
|
||||
return ArchiveFileModel.fromFileMd5(ctx, md5);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the database using the Algolia search index used by the Museum.
|
||||
*
|
||||
* Useful for locating a particular skin.
|
||||
* @gqlField
|
||||
*/
|
||||
async search_skins(
|
||||
{
|
||||
query,
|
||||
first = 10,
|
||||
offset = 0,
|
||||
}: { query: string; first?: Int; offset?: Int },
|
||||
{ ctx }
|
||||
): Promise<Array<ISkin | null>> {
|
||||
if (first > 1000) {
|
||||
throw new Error("Can only query 1000 records via search.");
|
||||
}
|
||||
|
||||
const results: { hits: { md5: string }[] } = await index.search(query, {
|
||||
attributesToRetrieve: ["md5"],
|
||||
length: first,
|
||||
offset,
|
||||
});
|
||||
|
||||
return Promise.all(
|
||||
results.hits.map(async (hit) => {
|
||||
const model = await SkinModel.fromMd5Assert(ctx, hit.md5);
|
||||
return SkinResolver.fromModel(model);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* All classic skins in the database
|
||||
*
|
||||
* **Note:** We don't currently support combining sorting and filtering.
|
||||
* @gqlField */
|
||||
skins({
|
||||
/**
|
||||
* All classic skins in the database
|
||||
*
|
||||
* **Note:** We don't currently support combining sorting and filtering.
|
||||
* @gqlField */
|
||||
export function skins(
|
||||
_: Query,
|
||||
{
|
||||
first = 10,
|
||||
offset = 0,
|
||||
sort,
|
||||
|
|
@ -142,120 +154,131 @@ class RootResolver extends MutationResolver {
|
|||
}: {
|
||||
first?: Int;
|
||||
offset?: Int;
|
||||
sort?: SkinsSortOption;
|
||||
filter?: SkinsFilterOption;
|
||||
}): SkinsConnection {
|
||||
if (first > 1000) {
|
||||
throw new Error("Maximum limit is 1000");
|
||||
}
|
||||
return new SkinsConnection(first, offset, sort, filter);
|
||||
sort?: SkinsSortOption | null;
|
||||
filter?: SkinsFilterOption | null;
|
||||
}
|
||||
): SkinsConnection {
|
||||
if (first > 1000) {
|
||||
throw new Error("Maximum limit is 1000");
|
||||
}
|
||||
return new SkinsConnection(first, offset, sort, filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* All modern skins in the database
|
||||
* @gqlField */
|
||||
async modern_skins({
|
||||
/**
|
||||
* All modern skins in the database
|
||||
* @gqlField */
|
||||
export async function modern_skins(
|
||||
_: Query,
|
||||
{
|
||||
first = 10,
|
||||
offset = 0,
|
||||
}: {
|
||||
first?: Int;
|
||||
offset?: Int;
|
||||
}): Promise<ModernSkinsConnection> {
|
||||
if (first > 1000) {
|
||||
throw new Error("Maximum limit is 1000");
|
||||
}
|
||||
return new ModernSkinsConnection(first, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* A random skin that needs to be reviewed
|
||||
* @gqlField */
|
||||
async skin_to_review(_args: never, { ctx }): Promise<ISkin | null> {
|
||||
if (!ctx.authed()) {
|
||||
return null;
|
||||
}
|
||||
const { md5 } = await Skins.getSkinToReview();
|
||||
const model = await SkinModel.fromMd5Assert(ctx, md5);
|
||||
return SkinResolver.fromModel(model);
|
||||
): Promise<ModernSkinsConnection> {
|
||||
if (first > 1000) {
|
||||
throw new Error("Maximum limit is 1000");
|
||||
}
|
||||
return new ModernSkinsConnection(first, offset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweets tweeted by @winampskins
|
||||
* @gqlField
|
||||
*/
|
||||
async tweets({
|
||||
/**
|
||||
* A random skin that needs to be reviewed
|
||||
* @gqlField */
|
||||
export async function skin_to_review(
|
||||
_: Query,
|
||||
_args: unknown,
|
||||
{ ctx }: Ctx
|
||||
): Promise<ISkin | null> {
|
||||
if (!ctx.authed()) {
|
||||
return null;
|
||||
}
|
||||
const { md5 } = await Skins.getSkinToReview();
|
||||
const model = await SkinModel.fromMd5Assert(ctx, md5);
|
||||
return SkinResolver.fromModel(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweets tweeted by @winampskins
|
||||
* @gqlField
|
||||
*/
|
||||
export async function tweets(
|
||||
_: Query,
|
||||
{
|
||||
first = 10,
|
||||
offset = 0,
|
||||
sort,
|
||||
}: {
|
||||
first?: Int;
|
||||
offset?: Int;
|
||||
sort?: TweetsSortOption;
|
||||
}): Promise<TweetsConnection> {
|
||||
if (first > 1000) {
|
||||
throw new Error("Maximum limit is 1000");
|
||||
}
|
||||
return new TweetsConnection(first, offset, sort);
|
||||
sort?: TweetsSortOption | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The currently authenticated user, if any.
|
||||
* @gqlField
|
||||
*/
|
||||
me(): UserResolver | null {
|
||||
return new UserResolver();
|
||||
): Promise<TweetsConnection> {
|
||||
if (first > 1000) {
|
||||
throw new Error("Maximum limit is 1000");
|
||||
}
|
||||
return new TweetsConnection(first, offset, sort);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of a batch of uploads by md5s
|
||||
* @gqlField
|
||||
* @deprecated Prefer `upload_statuses` instead, were we operate on ids.
|
||||
*/
|
||||
async upload_statuses_by_md5(
|
||||
{ md5s }: { md5s: string[] },
|
||||
{ ctx }
|
||||
): Promise<Array<SkinUpload | null>> {
|
||||
return this._upload_statuses({ keyName: "skin_md5", keys: md5s }, ctx);
|
||||
}
|
||||
/**
|
||||
* The currently authenticated user, if any.
|
||||
* @gqlField
|
||||
*/
|
||||
export function me(_: Query): UserResolver | null {
|
||||
return new UserResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of a batch of uploads by ids
|
||||
* @gqlField */
|
||||
async upload_statuses(
|
||||
{ ids }: { ids: string[] },
|
||||
{ ctx }
|
||||
): Promise<Array<SkinUpload | null>> {
|
||||
return this._upload_statuses({ keyName: "id", keys: ids }, ctx);
|
||||
}
|
||||
/**
|
||||
* Get the status of a batch of uploads by md5s
|
||||
* @gqlField
|
||||
* @deprecated Prefer `upload_statuses` instead, were we operate on ids.
|
||||
*/
|
||||
export async function upload_statuses_by_md5(
|
||||
_: Query,
|
||||
{ md5s }: { md5s: string[] },
|
||||
{ ctx }: Ctx
|
||||
): Promise<Array<SkinUpload | null>> {
|
||||
return _upload_statuses({ keyName: "skin_md5", keys: md5s }, ctx);
|
||||
}
|
||||
|
||||
// Shared implementation for upload_statuses and upload_statuses_by_md5
|
||||
async _upload_statuses({ keyName, keys }, ctx) {
|
||||
const skins = await knex("skin_uploads")
|
||||
.whereIn(keyName, keys)
|
||||
.orderBy("id", "desc")
|
||||
.select("id", "skin_md5", "status");
|
||||
/**
|
||||
* Get the status of a batch of uploads by ids
|
||||
* @gqlField */
|
||||
export async function upload_statuses(
|
||||
_: Query,
|
||||
{ ids }: { ids: string[] },
|
||||
{ ctx }: Ctx
|
||||
): Promise<Array<SkinUpload | null>> {
|
||||
return _upload_statuses({ keyName: "id", keys: ids }, ctx);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
skins.map(async ({ id, skin_md5, status }) => {
|
||||
// TODO: Could we avoid fetching the skin if it's not read?
|
||||
const skinModel = await SkinModel.fromMd5(ctx, skin_md5);
|
||||
const skin =
|
||||
skinModel == null ? null : SkinResolver.fromModel(skinModel);
|
||||
// Most of the time when a skin fails to process, it's due to some infa
|
||||
// issue on our side, and we can recover. For now, we'll always tell the user
|
||||
// That processing is just delayed.
|
||||
status = status === "ERRORED" ? "DELAYED" : status;
|
||||
return { id, skin, status, upload_md5: skin_md5 };
|
||||
})
|
||||
);
|
||||
}
|
||||
// Shared implementation for upload_statuses and upload_statuses_by_md5
|
||||
async function _upload_statuses({ keyName, keys }, ctx) {
|
||||
const skins = await knex("skin_uploads")
|
||||
.whereIn(keyName, keys)
|
||||
.orderBy("id", "desc")
|
||||
.select("id", "skin_md5", "status");
|
||||
|
||||
/**
|
||||
* A namespace for statistics about the database
|
||||
* @gqlField */
|
||||
statistics(): DatabaseStatisticsResolver {
|
||||
return new DatabaseStatisticsResolver();
|
||||
}
|
||||
return Promise.all(
|
||||
skins.map(async ({ id, skin_md5, status }) => {
|
||||
// TODO: Could we avoid fetching the skin if it's not read?
|
||||
const skinModel = await SkinModel.fromMd5(ctx, skin_md5);
|
||||
const skin = skinModel == null ? null : SkinResolver.fromModel(skinModel);
|
||||
// Most of the time when a skin fails to process, it's due to some infa
|
||||
// issue on our side, and we can recover. For now, we'll always tell the user
|
||||
// That processing is just delayed.
|
||||
status = status === "ERRORED" ? "DELAYED" : status;
|
||||
return { id, skin, status, upload_md5: skin_md5 };
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A namespace for statistics about the database
|
||||
* @gqlField */
|
||||
export function statistics(_: Query): DatabaseStatisticsResolver {
|
||||
return new DatabaseStatisticsResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -341,5 +364,3 @@ type SkinUploadStatus =
|
|||
* @deprecated
|
||||
*/
|
||||
| "ARCHIVED";
|
||||
|
||||
export default RootResolver;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import { Ctx } from "..";
|
||||
|
||||
/** @gqlType User */
|
||||
export default class UserResolver {
|
||||
/** @gqlField */
|
||||
username(_args: never, { ctx }): string {
|
||||
username(_args: unknown, { ctx }: Ctx): string | null {
|
||||
return ctx.username;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,252 +1,40 @@
|
|||
schema {
|
||||
query: Query
|
||||
mutation: Mutation
|
||||
}
|
||||
|
||||
directive @exported(filename: String!, functionName: String!) on FIELD_DEFINITION
|
||||
|
||||
directive @methodName(name: String!) on FIELD_DEFINITION
|
||||
|
||||
"""A file found within a Winamp Skin's .wsz archive"""
|
||||
type ArchiveFile {
|
||||
"""
|
||||
The date on the file inside the archive. Given in simplified extended ISO
|
||||
format (ISO 8601).
|
||||
"""
|
||||
date: String @methodName(name: "getIsoDate")
|
||||
"""The md5 hash of the file within the archive"""
|
||||
file_md5: String @methodName(name: "getFileMd5")
|
||||
"""Filename of the file within the archive"""
|
||||
filename: String @methodName(name: "getFileName")
|
||||
"""Is the file a directory?"""
|
||||
is_directory: Boolean @methodName(name: "getIsDirectory")
|
||||
"""
|
||||
The uncompressed size of the file in bytes.
|
||||
|
||||
**Note:** Will be `null` for directories
|
||||
"""
|
||||
size: Int @methodName(name: "getFileSize")
|
||||
"""The skin in which this file was found"""
|
||||
skin: Skin
|
||||
"""The content of the file, if it's a text file"""
|
||||
text_content: String @methodName(name: "getTextContent")
|
||||
"""
|
||||
A URL to download the file. **Note:** This is powered by a little
|
||||
serverless Cloudflare function which tries to exctact the file on the fly.
|
||||
It may not work for all files.
|
||||
"""
|
||||
url: String @methodName(name: "getUrl")
|
||||
}
|
||||
|
||||
"""A classic Winamp skin"""
|
||||
type ClassicSkin implements Node & Skin {
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile] @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "archive_files")
|
||||
"""String representation (rgb usually) of the skin's average color"""
|
||||
average_color: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "average_color")
|
||||
"""URL to download the skin"""
|
||||
download_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "download_url")
|
||||
"""
|
||||
Filename of skin when uploaded to the Museum. Note: In some cases a skin
|
||||
has been uploaded under multiple names. Here we just pick one.
|
||||
"""
|
||||
filename(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "filename")
|
||||
"""Does the skin include sprite sheets for the media library?"""
|
||||
has_media_library: Boolean
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID! @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "id")
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "internet_archive_item")
|
||||
"""
|
||||
The date on which this skin was last updated in the Algolia search index.
|
||||
Given in simplified extended ISO format (ISO 8601).
|
||||
"""
|
||||
last_algolia_index_update_date: String
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "md5")
|
||||
"""URL of the skin on the Winamp Skin Museum"""
|
||||
museum_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "museum_url")
|
||||
"""
|
||||
Has the skin been flagged as "not safe for work"?"
|
||||
"""
|
||||
nsfw: Boolean @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "nsfw")
|
||||
"""Text of the readme file extracted from the skin"""
|
||||
readme_text: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "readme_text")
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
review page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review] @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "reviews")
|
||||
"""URL of a screenshot of the skin"""
|
||||
screenshot_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "screenshot_url")
|
||||
"""The number of transparent pixels rendered by the skin."""
|
||||
transparent_pixels: Int
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "tweeted")
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet] @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "tweets")
|
||||
"""URL of webamp.org with the skin loaded"""
|
||||
webamp_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "webamp_url")
|
||||
}
|
||||
|
||||
"""Statistics about the contents of the Museum's database."""
|
||||
type DatabaseStatistics {
|
||||
"""
|
||||
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 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 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 approved for tweeting, but not yet tweeted."""
|
||||
tweetable_skins_count: Int
|
||||
"""The number of skins in the Museum that have been tweeted by @winampskins"""
|
||||
tweeted_skins_count: Int
|
||||
"""The total number of classic skins in the Museum's database"""
|
||||
unique_classic_skins_count: Int
|
||||
"""The number of skins that have never been reviewed."""
|
||||
unreviewed_skins_count: Int
|
||||
"""Skins uploads that have errored during processing."""
|
||||
uploads_in_error_state_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
|
||||
"""Number of skins that have been uploaded to the Museum via the web interface."""
|
||||
web_uploads_count: Int
|
||||
}
|
||||
|
||||
type InternetArchiveItem {
|
||||
"""The Internet Archive's unique identifier for this item"""
|
||||
identifier: String @methodName(name: "getIdentifier")
|
||||
"""
|
||||
The date and time that we last scraped this item's metadata.
|
||||
**Note:** This field is temporary and will be removed in the future.
|
||||
The date format is just what we get from the database, and it's ambiguous.
|
||||
"""
|
||||
last_metadata_scrape_date_UNSTABLE: String @methodName(name: "getMetadataTimestamp")
|
||||
"""URL to get the Internet Archive's metadata for this item in JSON form."""
|
||||
metadata_url: String @methodName(name: "getMetadataUrl")
|
||||
"""Our cached version of the metadata available at \`metadata_url\` (above)"""
|
||||
raw_metadata_json: String @methodName(name: "getMetadataJSON")
|
||||
"""The skin that this item contains"""
|
||||
skin: Skin
|
||||
"""The URL where this item can be viewed on the Internet Archive"""
|
||||
url: String @methodName(name: "getUrl")
|
||||
# Schema generated by Grats (https://grats.capt.dev)
|
||||
# Do not manually edit. Regenerate by running `npx grats`.
|
||||
"""The judgement made about a skin by a moderator"""
|
||||
enum Rating {
|
||||
APPROVED
|
||||
NSFW
|
||||
REJECTED
|
||||
}
|
||||
|
||||
"""
|
||||
A "modern" Winamp skin. These skins use the `.wal` file extension and are free-form.
|
||||
The current status of a pending upload.
|
||||
|
||||
Most functionality in the Winamp Skin Museum is centered around "classic" skins,
|
||||
which are currently called just `Skin` in this schema.
|
||||
**Note:** Expect more values here as we try to be more transparent about
|
||||
the status of a pending uploads.
|
||||
"""
|
||||
type ModernSkin implements Node & Skin {
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile] @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "archive_files")
|
||||
"""String representation (rgb usually) of the skin's average color"""
|
||||
average_color: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "average_color")
|
||||
"""URL to download the skin"""
|
||||
download_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "download_url")
|
||||
"""
|
||||
Filename of skin when uploaded to the Museum. Note: In some cases a skin
|
||||
has been uploaded under multiple names. Here we just pick one.
|
||||
"""
|
||||
filename(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "filename")
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID! @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "id")
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "internet_archive_item")
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "md5")
|
||||
"""URL of the skin on the Winamp Skin Museum"""
|
||||
museum_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "museum_url")
|
||||
"""
|
||||
Has the skin been flagged as "not safe for work"?"
|
||||
"""
|
||||
nsfw: Boolean @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "nsfw")
|
||||
"""Text of the readme file extracted from the skin"""
|
||||
readme_text: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "readme_text")
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
review page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review] @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "reviews")
|
||||
"""URL of a screenshot of the skin"""
|
||||
screenshot_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "screenshot_url")
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "tweeted")
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
tweets: [Tweet] @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "tweets")
|
||||
"""URL of webamp.org with the skin loaded"""
|
||||
webamp_url: String @exported(filename: "../../../../packages/skin-database/dist/api/graphql/resolvers/CommonSkinResolver.js", functionName: "webamp_url")
|
||||
enum SkinUploadStatus {
|
||||
ARCHIVED
|
||||
DELAYED
|
||||
ERRORED
|
||||
UPLOAD_REPORTED
|
||||
URL_REQUESTED
|
||||
}
|
||||
|
||||
"""A collection of "modern" Winamp skins"""
|
||||
type ModernSkinsConnection {
|
||||
"""The total number of skins matching the filter"""
|
||||
count: Int
|
||||
"""The list of skins"""
|
||||
nodes: [ModernSkin]
|
||||
enum SkinsFilterOption {
|
||||
APPROVED
|
||||
NSFW
|
||||
REJECTED
|
||||
TWEETED
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
"""
|
||||
Approve skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
approve_skin(md5: String!): Boolean
|
||||
"""
|
||||
Mark a skin as NSFW
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
mark_skin_nsfw(md5: String!): Boolean
|
||||
"""
|
||||
Reject skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
reject_skin(md5: String!): Boolean
|
||||
"""
|
||||
Request that an admin check if this skin is NSFW.
|
||||
Unlike other review mutaiton endpoints, this one does not require being logged
|
||||
in.
|
||||
"""
|
||||
request_nsfw_review_for_skin(md5: String!): Boolean
|
||||
"""Send a message to the admin of the site. Currently this appears in Discord."""
|
||||
send_feedback(email: String, message: String!, url: String): Boolean
|
||||
"""Mutations for the upload flow"""
|
||||
upload: UploadMutations
|
||||
enum SkinsSortOption {
|
||||
MUSEUM
|
||||
}
|
||||
|
||||
enum TweetsSortOption {
|
||||
LIKES
|
||||
RETWEETS
|
||||
}
|
||||
|
||||
"""
|
||||
|
|
@ -258,80 +46,6 @@ interface Node {
|
|||
id: ID!
|
||||
}
|
||||
|
||||
type Query {
|
||||
"""
|
||||
Fetch archive file by it's MD5 hash
|
||||
|
||||
Get information about a file found within a skin's wsz/wal/zip archive.
|
||||
"""
|
||||
fetch_archive_file_by_md5(md5: String!): ArchiveFile
|
||||
"""
|
||||
Get an archive.org item by its identifier. You can find this in the URL:
|
||||
|
||||
https://archive.org/details/<identifier>/
|
||||
"""
|
||||
fetch_internet_archive_item_by_identifier(identifier: String!): InternetArchiveItem
|
||||
"""Get a skin by its MD5 hash"""
|
||||
fetch_skin_by_md5(md5: String!): Skin
|
||||
"""Get a tweet by its URL"""
|
||||
fetch_tweet_by_url(url: String!): Tweet
|
||||
"""The currently authenticated user, if any."""
|
||||
me: User
|
||||
"""All modern skins in the database"""
|
||||
modern_skins(first: Int = 10, offset: Int = 0): ModernSkinsConnection
|
||||
"""
|
||||
Get a globally unique object by its ID.
|
||||
|
||||
https://graphql.org/learn/global-object-identification/
|
||||
"""
|
||||
node(id: ID!): Node
|
||||
"""
|
||||
Search the database using the Algolia search index used by the Museum.
|
||||
|
||||
Useful for locating a particular skin.
|
||||
"""
|
||||
search_skins(first: Int = 10, offset: Int = 0, query: String!): [Skin]
|
||||
"""A random skin that needs to be reviewed"""
|
||||
skin_to_review: Skin
|
||||
"""
|
||||
All classic skins in the database
|
||||
|
||||
**Note:** We don't currently support combining sorting and filtering.
|
||||
"""
|
||||
skins(filter: SkinsFilterOption, first: Int = 10, offset: Int = 0, sort: SkinsSortOption): SkinsConnection
|
||||
"""A namespace for statistics about the database"""
|
||||
statistics: DatabaseStatistics
|
||||
"""Tweets tweeted by @winampskins"""
|
||||
tweets(first: Int = 10, offset: Int = 0, sort: TweetsSortOption): TweetsConnection
|
||||
"""Get the status of a batch of uploads by ids"""
|
||||
upload_statuses(ids: [String!]!): [SkinUpload]
|
||||
"""Get the status of a batch of uploads by md5s"""
|
||||
upload_statuses_by_md5(md5s: [String!]!): [SkinUpload] @deprecated(reason: "Prefer `upload_statuses` instead, were we operate on ids.")
|
||||
}
|
||||
|
||||
"""The judgement made about a skin by a moderator"""
|
||||
enum Rating {
|
||||
APPROVED
|
||||
NSFW
|
||||
REJECTED
|
||||
}
|
||||
|
||||
"""
|
||||
A review of a skin. Done either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
type Review {
|
||||
"""The rating that the user gave the skin"""
|
||||
rating: Rating
|
||||
"""
|
||||
The user who made the review (if known). **Note:** In the early days we didn't
|
||||
track this, so many will be null.
|
||||
"""
|
||||
reviewer: String
|
||||
"""The skin that was reviewed"""
|
||||
skin: Skin
|
||||
}
|
||||
|
||||
"""
|
||||
A Winamp skin. Could be modern or classic.
|
||||
|
||||
|
|
@ -378,29 +92,333 @@ interface Skin {
|
|||
screenshot_url: String
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
"""List of @winampskins tweets that mentioned the skin."""
|
||||
"""List of"""
|
||||
tweets: [Tweet]
|
||||
"""URL of webamp.org with the skin loaded"""
|
||||
webamp_url: String
|
||||
}
|
||||
|
||||
"""A collection of classic Winamp skins"""
|
||||
type SkinsConnection {
|
||||
"""Input object used for a user to request an UploadUrl"""
|
||||
input UploadUrlRequest {
|
||||
filename: String!
|
||||
md5: String!
|
||||
}
|
||||
|
||||
"""A file found within a Winamp Skin's .wsz archive"""
|
||||
type ArchiveFile {
|
||||
"""
|
||||
The date on the file inside the archive. Given in simplified extended ISO
|
||||
format (ISO 8601).
|
||||
"""
|
||||
date: String
|
||||
"""The md5 hash of the file within the archive"""
|
||||
file_md5: String
|
||||
"""Filename of the file within the archive"""
|
||||
filename: String
|
||||
"""Is the file a directory?"""
|
||||
is_directory: Boolean
|
||||
"""
|
||||
The uncompressed size of the file in bytes.
|
||||
|
||||
**Note:** Will be `null` for directories
|
||||
"""
|
||||
size: Int
|
||||
"""The skin in which this file was found"""
|
||||
skin: Skin
|
||||
"""The content of the file, if it's a text file"""
|
||||
text_content: String
|
||||
"""
|
||||
A URL to download the file. **Note:** This is powered by a little
|
||||
serverless Cloudflare function which tries to exctact the file on the fly.
|
||||
It may not work for all files.
|
||||
"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A classic Winamp skin"""
|
||||
type ClassicSkin implements Node & Skin {
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
"""String representation (rgb usually) of the skin's average color"""
|
||||
average_color: String
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
"""
|
||||
Filename of skin when uploaded to the Museum. Note: In some cases a skin
|
||||
has been uploaded under multiple names. Here we just pick one.
|
||||
"""
|
||||
filename(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""Does the skin include sprite sheets for the media library?"""
|
||||
has_media_library: Boolean
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""
|
||||
The date on which this skin was last updated in the Algolia search index.
|
||||
Given in simplified extended ISO format (ISO 8601).
|
||||
"""
|
||||
last_algolia_index_update_date: String
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
"""URL of the skin on the Winamp Skin Museum"""
|
||||
museum_url: String
|
||||
"""
|
||||
Has the skin been flagged as "not safe for work"?"
|
||||
"""
|
||||
nsfw: Boolean
|
||||
"""Text of the readme file extracted from the skin"""
|
||||
readme_text: String
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
review page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
"""URL of a screenshot of the skin"""
|
||||
screenshot_url: String
|
||||
"""The number of transparent pixels rendered by the skin."""
|
||||
transparent_pixels: Int
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
"""List of"""
|
||||
tweets: [Tweet]
|
||||
"""URL of webamp.org with the skin loaded"""
|
||||
webamp_url: String
|
||||
}
|
||||
|
||||
"""Statistics about the contents of the Museum's database."""
|
||||
type DatabaseStatistics {
|
||||
"""
|
||||
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 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 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 approved for tweeting, but not yet tweeted.
|
||||
"""
|
||||
tweetable_skins_count: Int
|
||||
"""The number of skins in the Museum that have been tweeted by"""
|
||||
tweeted_skins_count: Int
|
||||
"""The total number of classic skins in the Museum's database"""
|
||||
unique_classic_skins_count: Int
|
||||
"""The number of skins that have never been reviewed."""
|
||||
unreviewed_skins_count: Int
|
||||
"""Skins uploads that have errored during processing."""
|
||||
uploads_in_error_state_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
|
||||
"""
|
||||
Number of skins that have been uploaded to the Museum via the web interface.
|
||||
"""
|
||||
web_uploads_count: Int
|
||||
}
|
||||
|
||||
type InternetArchiveItem {
|
||||
"""The Internet Archive's unique identifier for this item"""
|
||||
identifier: String
|
||||
"""
|
||||
The date and time that we last scraped this item's metadata.
|
||||
**Note:** This field is temporary and will be removed in the future.
|
||||
The date format is just what we get from the database, and it's ambiguous.
|
||||
"""
|
||||
last_metadata_scrape_date_UNSTABLE: String
|
||||
"""URL to get the Internet Archive's metadata for this item in JSON form."""
|
||||
metadata_url: String
|
||||
"""
|
||||
Our cached version of the metadata available at \`metadata_url\` (above)
|
||||
"""
|
||||
raw_metadata_json: String
|
||||
"""The skin that this item contains"""
|
||||
skin: Skin
|
||||
"""The URL where this item can be viewed on the Internet Archive"""
|
||||
url: String
|
||||
}
|
||||
|
||||
"""
|
||||
A "modern" Winamp skin. These skins use the `.wal` file extension and are free-form.
|
||||
|
||||
Most functionality in the Winamp Skin Museum is centered around "classic" skins,
|
||||
which are currently called just `Skin` in this schema.
|
||||
"""
|
||||
type ModernSkin implements Node & Skin {
|
||||
"""List of files contained within the skin's .wsz archive"""
|
||||
archive_files: [ArchiveFile]
|
||||
"""String representation (rgb usually) of the skin's average color"""
|
||||
average_color: String
|
||||
"""URL to download the skin"""
|
||||
download_url: String
|
||||
"""
|
||||
Filename of skin when uploaded to the Museum. Note: In some cases a skin
|
||||
has been uploaded under multiple names. Here we just pick one.
|
||||
"""
|
||||
filename(
|
||||
"""
|
||||
If true, the the correct file extension (.wsz or .wal) will be .
|
||||
Otherwise, the original user-uploaded file extension will be used.
|
||||
"""
|
||||
normalize_extension: Boolean = false
|
||||
): String
|
||||
"""GraphQL ID of the skin"""
|
||||
id: ID!
|
||||
"""The skin's "item" at archive.org"""
|
||||
internet_archive_item: InternetArchiveItem
|
||||
"""MD5 hash of the skin's file"""
|
||||
md5: String
|
||||
"""URL of the skin on the Winamp Skin Museum"""
|
||||
museum_url: String
|
||||
"""
|
||||
Has the skin been flagged as "not safe for work"?"
|
||||
"""
|
||||
nsfw: Boolean
|
||||
"""Text of the readme file extracted from the skin"""
|
||||
readme_text: String
|
||||
"""
|
||||
Times that the skin has been reviewed either on the Museum's Tinder-style
|
||||
review page, or via the Discord bot.
|
||||
"""
|
||||
reviews: [Review]
|
||||
"""URL of a screenshot of the skin"""
|
||||
screenshot_url: String
|
||||
"""Has the skin been tweeted?"""
|
||||
tweeted: Boolean
|
||||
"""List of"""
|
||||
tweets: [Tweet]
|
||||
"""URL of webamp.org with the skin loaded"""
|
||||
webamp_url: String
|
||||
}
|
||||
|
||||
"""A collection of "modern" Winamp skins"""
|
||||
type ModernSkinsConnection {
|
||||
"""The total number of skins matching the filter"""
|
||||
count: Int
|
||||
"""The list of skins"""
|
||||
nodes: [Skin]
|
||||
nodes: [ModernSkin]
|
||||
}
|
||||
|
||||
enum SkinsFilterOption {
|
||||
APPROVED
|
||||
NSFW
|
||||
REJECTED
|
||||
TWEETED
|
||||
type Mutation {
|
||||
"""
|
||||
Approve skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
approve_skin(md5: String!): Boolean
|
||||
"""
|
||||
Mark a skin as NSFW
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
mark_skin_nsfw(md5: String!): Boolean
|
||||
"""
|
||||
Reject skin for tweeting
|
||||
|
||||
**Note:** Requires being logged in
|
||||
"""
|
||||
reject_skin(md5: String!): Boolean
|
||||
"""
|
||||
Request that an admin check if this skin is NSFW.
|
||||
Unlike other review mutaiton endpoints, this one does not require being logged
|
||||
in.
|
||||
"""
|
||||
request_nsfw_review_for_skin(md5: String!): Boolean
|
||||
"""
|
||||
Send a message to the admin of the site. Currently this appears in Discord.
|
||||
"""
|
||||
send_feedback(email: String, message: String!, url: String): Boolean
|
||||
"""Mutations for the upload flow"""
|
||||
upload: UploadMutations
|
||||
}
|
||||
|
||||
enum SkinsSortOption {
|
||||
MUSEUM
|
||||
type Query {
|
||||
"""
|
||||
Fetch archive file by it's MD5 hash
|
||||
|
||||
Get information about a file found within a skin's wsz/wal/zip archive.
|
||||
"""
|
||||
fetch_archive_file_by_md5(md5: String!): ArchiveFile
|
||||
"""
|
||||
Get an archive.org item by its identifier. You can find this in the URL:
|
||||
|
||||
https://archive.org/details/<identifier>/
|
||||
"""
|
||||
fetch_internet_archive_item_by_identifier(identifier: String!): InternetArchiveItem
|
||||
"""Get a skin by its MD5 hash"""
|
||||
fetch_skin_by_md5(md5: String!): Skin
|
||||
"""Get a tweet by its URL"""
|
||||
fetch_tweet_by_url(url: String!): Tweet
|
||||
"""The currently authenticated user, if any."""
|
||||
me: User
|
||||
"""All modern skins in the database"""
|
||||
modern_skins(first: Int! = 10, offset: Int! = 0): ModernSkinsConnection
|
||||
"""
|
||||
Get a globally unique object by its ID.
|
||||
|
||||
https://graphql.org/learn/global-object-identification/
|
||||
"""
|
||||
node(id: ID!): Node
|
||||
"""
|
||||
Search the database using the Algolia search index used by the Museum.
|
||||
|
||||
Useful for locating a particular skin.
|
||||
"""
|
||||
search_skins(first: Int! = 10, offset: Int! = 0, query: String!): [Skin]
|
||||
"""A random skin that needs to be reviewed"""
|
||||
skin_to_review: Skin
|
||||
"""
|
||||
All classic skins in the database
|
||||
|
||||
**Note:** We don't currently support combining sorting and filtering.
|
||||
"""
|
||||
skins(filter: SkinsFilterOption, first: Int! = 10, offset: Int! = 0, sort: SkinsSortOption): SkinsConnection
|
||||
"""A namespace for statistics about the database"""
|
||||
statistics: DatabaseStatistics
|
||||
"""Tweets tweeted by"""
|
||||
tweets(first: Int! = 10, offset: Int! = 0, sort: TweetsSortOption): TweetsConnection
|
||||
"""Get the status of a batch of uploads by ids"""
|
||||
upload_statuses(ids: [String!]!): [SkinUpload]
|
||||
"""Get the status of a batch of uploads by md5s"""
|
||||
upload_statuses_by_md5(md5s: [String!]!): [SkinUpload] @deprecated(reason: "Prefer `upload_statuses` instead, were we operate on ids.")
|
||||
}
|
||||
|
||||
"""
|
||||
A review of a skin. Done either on the Museum's Tinder-style
|
||||
reivew page, or via the Discord bot.
|
||||
"""
|
||||
type Review {
|
||||
"""The rating that the user gave the skin"""
|
||||
rating: Rating
|
||||
"""
|
||||
The user who made the review (if known). **Note:** In the early days we didn't
|
||||
track this, so many will be null.
|
||||
"""
|
||||
reviewer: String
|
||||
"""The skin that was reviewed"""
|
||||
skin: Skin
|
||||
}
|
||||
|
||||
"""Information about an attempt to upload a skin to the Museum."""
|
||||
|
|
@ -416,26 +434,24 @@ type SkinUpload {
|
|||
upload_md5: String
|
||||
}
|
||||
|
||||
"""
|
||||
The current status of a pending upload.
|
||||
|
||||
**Note:** Expect more values here as we try to be more transparent about
|
||||
the status of a pending uploads.
|
||||
"""
|
||||
enum SkinUploadStatus {
|
||||
ARCHIVED
|
||||
DELAYED
|
||||
ERRORED
|
||||
UPLOAD_REPORTED
|
||||
URL_REQUESTED
|
||||
"""A collection of classic Winamp skins"""
|
||||
type SkinsConnection {
|
||||
"""The total number of skins matching the filter"""
|
||||
count: Int
|
||||
"""The list of skins"""
|
||||
nodes: [Skin]
|
||||
}
|
||||
|
||||
"""A tweet made by @winampskins mentioning a Winamp skin"""
|
||||
"""A tweet made by"""
|
||||
type Tweet {
|
||||
"""Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here)"""
|
||||
likes: Int @methodName(name: "getLikes")
|
||||
"""Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here)"""
|
||||
retweets: Int @methodName(name: "getRetweets")
|
||||
"""
|
||||
Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here)
|
||||
"""
|
||||
likes: Int
|
||||
"""
|
||||
Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here)
|
||||
"""
|
||||
retweets: Int
|
||||
"""The skin featured in this Tweet"""
|
||||
skin: Skin
|
||||
"""
|
||||
|
|
@ -443,10 +459,10 @@ type Tweet {
|
|||
_which_ skins were tweeted, not any info about the actual tweet. This means we
|
||||
don't always know the URL of the tweet.
|
||||
"""
|
||||
url: String @methodName(name: "getUrl")
|
||||
url: String
|
||||
}
|
||||
|
||||
"""A collection of tweets made by the @winampskins bot"""
|
||||
"""A collection of tweets made by the"""
|
||||
type TweetsConnection {
|
||||
"""The total number of tweets"""
|
||||
count: Int
|
||||
|
|
@ -454,11 +470,6 @@ type TweetsConnection {
|
|||
nodes: [Tweet]
|
||||
}
|
||||
|
||||
enum TweetsSortOption {
|
||||
LIKES
|
||||
RETWEETS
|
||||
}
|
||||
|
||||
"""
|
||||
Mutations for the upload flow
|
||||
|
||||
|
|
@ -490,12 +501,6 @@ type UploadUrl {
|
|||
url: String
|
||||
}
|
||||
|
||||
"""Input object used for a user to request an UploadUrl"""
|
||||
input UploadUrlRequest {
|
||||
filename: String!
|
||||
md5: String!
|
||||
}
|
||||
|
||||
type User {
|
||||
username: String
|
||||
}
|
||||
|
|
|
|||
1249
packages/skin-database/api/graphql/schema.ts
Normal file
1249
packages/skin-database/api/graphql/schema.ts
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -21,8 +21,7 @@
|
|||
"express-graphql": "^0.12.0",
|
||||
"express-sitemap-xml": "^2.0.0",
|
||||
"fast-xml-parser": "^4.2.2",
|
||||
"graphql": "14.7.0",
|
||||
"grats": "0.0.0-main-b47472ff",
|
||||
"graphql": "^16.8.1",
|
||||
"imagemin": "^7.0.0",
|
||||
"imagemin-optipng": "^7.0.0",
|
||||
"knex": "^0.21.1",
|
||||
|
|
@ -50,13 +49,13 @@
|
|||
"cli": "ts-node --transpile-only ./cli.ts",
|
||||
"sync": "ts-node --transpile-only ./tasks/syncWithArchive.ts",
|
||||
"migrate": "knex migrate:latest",
|
||||
"grats": "grats --output=./api/graphql/schema.graphql"
|
||||
"grats": "grats"
|
||||
},
|
||||
"prettier": {},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.10.1",
|
||||
"@types/cookie-session": "^2.0.48",
|
||||
"@types/body-parser": "^1.19.5",
|
||||
"@types/cookie-session": "^2.0.48",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "4.17.9",
|
||||
"@types/express-fileupload": "^1.4.4",
|
||||
|
|
@ -66,10 +65,11 @@
|
|||
"@types/supertest": "^2.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"grats": "^0.0.23",
|
||||
"supertest": "^6.0.1",
|
||||
"typescript": "^5.3.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"graphql": "15.3.0"
|
||||
"graphql": "16.8.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -172,7 +172,6 @@ function putTemp(fileName, buffer) {
|
|||
...acl,
|
||||
},
|
||||
(err) => {
|
||||
console.log("Hello...");
|
||||
if (err) {
|
||||
rejectPromise(err);
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
{
|
||||
"grats": {
|
||||
"nullableByDefault": true
|
||||
"nullableByDefault": true,
|
||||
"graphqlSchema": "./api/graphql/schema.graphql",
|
||||
"tsSchema": "./api/graphql/schema.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"plugins": [{ "name": "grats-ts-plugin" }],
|
||||
"module": "commonjs",
|
||||
"esModuleInterop": true,
|
||||
"target": "es6",
|
||||
|
|
|
|||
40
yarn.lock
40
yarn.lock
|
|
@ -3140,14 +3140,6 @@
|
|||
dependencies:
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@graphql-tools/utils@^9.2.1":
|
||||
version "9.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-9.2.1.tgz#1b3df0ef166cfa3eae706e3518b17d5922721c57"
|
||||
integrity sha512-WUw506Ql6xzmOORlriNrD6Ugx+HjVgYxt9KCXD9mHAak+eaXSwuGGPyE60hy9xaDEoXKBsG7SkG69ybitaVl6A==
|
||||
dependencies:
|
||||
"@graphql-typed-document-node/core" "^3.1.1"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@graphql-tools/wrap@9.2.1":
|
||||
version "9.2.1"
|
||||
resolved "https://registry.npmjs.org/@graphql-tools/wrap/-/wrap-9.2.1.tgz"
|
||||
|
|
@ -3159,11 +3151,6 @@
|
|||
tslib "^2.4.0"
|
||||
value-or-promise "1.0.11"
|
||||
|
||||
"@graphql-typed-document-node/core@^3.1.1":
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.2.0.tgz#5f3d96ec6b2354ad6d8a28bf216a1d97b5426861"
|
||||
integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==
|
||||
|
||||
"@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0":
|
||||
version "9.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@hapi/hoek/-/hoek-9.3.0.tgz#8368869dcb735be2e7f5cb7647de78e167a251fb"
|
||||
|
|
@ -15662,27 +15649,19 @@ graphql-ws@^5.4.1:
|
|||
resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.11.1.tgz"
|
||||
integrity sha512-AlOO/Gt0fXuSHXe/Weo6o3rIQVnH5MW7ophzeYzL+vYNlkf0NbWRJ6IIFgtSLcv9JpTlQdxSpB3t0SnM47/BHA==
|
||||
|
||||
graphql@14.7.0:
|
||||
version "14.7.0"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72"
|
||||
integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==
|
||||
dependencies:
|
||||
iterall "^1.2.2"
|
||||
|
||||
graphql@^16.6.0:
|
||||
graphql@16.8.1, graphql@^16.6.0, graphql@^16.8.1:
|
||||
version "16.8.1"
|
||||
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07"
|
||||
integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw==
|
||||
|
||||
grats@0.0.0-main-b47472ff:
|
||||
version "0.0.0-main-b47472ff"
|
||||
resolved "https://registry.yarnpkg.com/grats/-/grats-0.0.0-main-b47472ff.tgz#ca8d7f9bad66fcf7011090c681207105c4542f13"
|
||||
integrity sha512-k+O8Jh3rqhCp80lZcYrXY5oWEf4FzwAtvLuTQtFskKPU8y6V/mlzL6per643uFSnisv7lmFrxY+8aBIbUDEFfg==
|
||||
grats@^0.0.23:
|
||||
version "0.0.23"
|
||||
resolved "https://registry.yarnpkg.com/grats/-/grats-0.0.23.tgz#5fee4448c1b26b8f9ee0dfa3df49dba6f1353fd4"
|
||||
integrity sha512-LqkHsw7OXXWkDAumMZ2AhV/SnDkrYgIEMhYm29ndSGmgFOP3PBfAZXgWcKrwKEU4FkWQ3JfN9PtpddwPN4t/Ig==
|
||||
dependencies:
|
||||
"@graphql-tools/utils" "^9.2.1"
|
||||
commander "^10.0.0"
|
||||
graphql "^16.6.0"
|
||||
typescript "^4.9.5"
|
||||
typescript "^5.0.2"
|
||||
|
||||
gulp-eslint@^3.0.1:
|
||||
version "3.0.1"
|
||||
|
|
@ -17446,11 +17425,6 @@ isurl@^1.0.0-alpha5:
|
|||
has-to-string-tag-x "^1.2.0"
|
||||
is-object "^1.0.1"
|
||||
|
||||
iterall@^1.2.2:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea"
|
||||
integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg==
|
||||
|
||||
jake@^10.8.5:
|
||||
version "10.8.5"
|
||||
resolved "https://registry.npmjs.org/jake/-/jake-10.8.5.tgz"
|
||||
|
|
@ -27598,7 +27572,7 @@ typescript@^4.9.5:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
typescript@^5.3.3:
|
||||
typescript@^5.0.2, typescript@^5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue