From 0497f2e9034ed0c176c2f8739b8d5bcba525b2b2 Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Fri, 23 Feb 2024 15:49:20 -0800 Subject: [PATCH] Upgrade grats --- .eslintignore | 1 + .prettierignore | 3 +- packages/skin-database/.prettierignore | 2 +- packages/skin-database/api/graphql/index.ts | 4 +- .../skin-database/api/graphql/schema.graphql | 873 ++++--- packages/skin-database/api/graphql/schema.ts | 2067 +++++++++-------- packages/skin-database/package.json | 5 +- yarn.lock | 15 +- 8 files changed, 1443 insertions(+), 1527 deletions(-) diff --git a/.eslintignore b/.eslintignore index d8e6b698..696eb62a 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ **/build/ **/dist/ *.min.js +packages/skin-database/api/graphql/schema.ts \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 514a00e6..6f51fd7a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,4 +4,5 @@ packages/webamp/demo/built/ packages/webamp/built/ packages/webamp/dist/ packages/webamp-modern/build/ -packages/webamp-modern/tools/eslint-rules/dist/ \ No newline at end of file +packages/webamp-modern/tools/eslint-rules/dist/ +packages/skin-database/api/graphql/schema.ts \ No newline at end of file diff --git a/packages/skin-database/.prettierignore b/packages/skin-database/.prettierignore index 804359d8..56a471ba 100644 --- a/packages/skin-database/.prettierignore +++ b/packages/skin-database/.prettierignore @@ -1 +1 @@ -api/graphql/schema.ts \ No newline at end of file +./api/graphql/schema.ts diff --git a/packages/skin-database/api/graphql/index.ts b/packages/skin-database/api/graphql/index.ts index cc0644ce..525b4494 100644 --- a/packages/skin-database/api/graphql/index.ts +++ b/packages/skin-database/api/graphql/index.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import { graphqlHTTP } from "express-graphql"; import DEFAULT_QUERY from "./defaultQuery"; -import { schema } from "./schema"; +import { getSchema } from "./schema"; const router = Router(); @@ -38,7 +38,7 @@ const extensions = ({ router.use( "/", graphqlHTTP({ - schema, + schema: getSchema(), graphiql: { defaultQuery: DEFAULT_QUERY, }, diff --git a/packages/skin-database/api/graphql/schema.graphql b/packages/skin-database/api/graphql/schema.graphql index da8b2333..379273b0 100644 --- a/packages/skin-database/api/graphql/schema.graphql +++ b/packages/skin-database/api/graphql/schema.graphql @@ -1,372 +1,5 @@ # Schema generated by Grats (https://grats.capt.dev) # Do not manually edit. Regenerate by running `npx grats`. -"""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 - - """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 mutation 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// - """ - 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 @@ -374,91 +7,6 @@ enum Rating { REJECTED } -""" -A review of a skin. Done either on the Museum's Tinder-style -review 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 - - """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. @@ -473,15 +21,6 @@ enum SkinUploadStatus { 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 @@ -493,21 +32,412 @@ enum SkinsSortOption { MUSEUM } +enum TweetsSortOption { + LIKES + RETWEETS +} + +""" +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! +} + +""" +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 + """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") +} + +"""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 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 + """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 mutation 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 +} + +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// + """ + 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.") +} + +""" +A review of a skin. Done either on the Museum's Tinder-style +review 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.""" +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 +} + +"""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""" 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 @@ -520,16 +450,10 @@ type Tweet { type TweetsConnection { """The total number of tweets""" count: Int - """The list of tweets""" nodes: [Tweet] } -enum TweetsSortOption { - LIKES - RETWEETS -} - """ Mutations for the upload flow @@ -547,7 +471,6 @@ type UploadMutations { 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 } @@ -562,12 +485,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 -} \ No newline at end of file +} diff --git a/packages/skin-database/api/graphql/schema.ts b/packages/skin-database/api/graphql/schema.ts index e192151c..5a6142ee 100644 --- a/packages/skin-database/api/graphql/schema.ts +++ b/packages/skin-database/api/graphql/schema.ts @@ -23,1059 +23,1060 @@ import { request_nsfw_review_for_skin as mutationRequest_nsfw_review_for_skinRes import { send_feedback as mutationSend_feedbackResolver } from "./resolvers/MutationResolver"; import { upload as mutationUploadResolver } from "./resolvers/UploadMutationResolver"; import { GraphQLSchema, GraphQLObjectType, GraphQLString, GraphQLBoolean, GraphQLInt, GraphQLInterfaceType, GraphQLList, GraphQLNonNull, GraphQLID, GraphQLEnumType, GraphQLInputObjectType } from "graphql"; -const InternetArchiveItemType: GraphQLObjectType = new GraphQLObjectType({ - name: "InternetArchiveItem", - fields() { - return { - identifier: { - description: "The Internet Archive's unique identifier for this item", - name: "identifier", - type: GraphQLString - }, - last_metadata_scrape_date_UNSTABLE: { - description: "The date and time that we last scraped this item's metadata.\n**Note:** This field is temporary and will be removed in the future.\nThe date format is just what we get from the database, and it's ambiguous.", - name: "last_metadata_scrape_date_UNSTABLE", - type: GraphQLString - }, - metadata_url: { - description: "URL to get the Internet Archive's metadata for this item in JSON form.", - name: "metadata_url", - type: GraphQLString - }, - raw_metadata_json: { - description: "Our cached version of the metadata avaliable at \\`metadata_url\\` (above)", - name: "raw_metadata_json", - type: GraphQLString - }, - skin: { - description: "The skin that this item contains", - name: "skin", - type: SkinType - }, - url: { - description: "The URL where this item can be viewed on the Internet Archive", - name: "url", - type: GraphQLString - } - }; - } -}); -const RatingType: GraphQLEnumType = new GraphQLEnumType({ - description: "The judgement made about a skin by a moderator", - name: "Rating", - values: { - APPROVED: { - value: "APPROVED" - }, - NSFW: { - value: "NSFW" - }, - REJECTED: { - value: "REJECTED" +export function getSchema(): GraphQLSchema { + const InternetArchiveItemType: GraphQLObjectType = new GraphQLObjectType({ + name: "InternetArchiveItem", + fields() { + return { + identifier: { + description: "The Internet Archive's unique identifier for this item", + name: "identifier", + type: GraphQLString + }, + last_metadata_scrape_date_UNSTABLE: { + description: "The date and time that we last scraped this item's metadata.\n**Note:** This field is temporary and will be removed in the future.\nThe date format is just what we get from the database, and it's ambiguous.", + name: "last_metadata_scrape_date_UNSTABLE", + type: GraphQLString + }, + metadata_url: { + description: "URL to get the Internet Archive's metadata for this item in JSON form.", + name: "metadata_url", + type: GraphQLString + }, + raw_metadata_json: { + description: "Our cached version of the metadata avaliable at \\`metadata_url\\` (above)", + name: "raw_metadata_json", + type: GraphQLString + }, + skin: { + description: "The skin that this item contains", + name: "skin", + type: SkinType + }, + url: { + description: "The URL where this item can be viewed on the Internet Archive", + name: "url", + type: GraphQLString + } + }; } - } -}); -const ReviewType: GraphQLObjectType = new GraphQLObjectType({ - name: "Review", - description: "A review of a skin. Done either on the Museum's Tinder-style\nreview page, or via the Discord bot.", - fields() { - return { - rating: { - description: "The rating that the user gave the skin", - name: "rating", - type: RatingType + }); + const RatingType: GraphQLEnumType = new GraphQLEnumType({ + description: "The judgement made about a skin by a moderator", + name: "Rating", + values: { + APPROVED: { + value: "APPROVED" }, - reviewer: { - description: "The user who made the review (if known). **Note:** In the early days we didn't\ntrack this, so many will be null.", - name: "reviewer", - type: GraphQLString + NSFW: { + value: "NSFW" }, - skin: { - description: "The skin that was reviewed", - name: "skin", - type: SkinType + REJECTED: { + value: "REJECTED" } - }; - } -}); -const TweetType: GraphQLObjectType = new GraphQLObjectType({ - name: "Tweet", - description: "A tweet made by `@winampskins` mentioning a Winamp skin", - fields() { - return { - likes: { - description: "Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here)", - name: "likes", - type: GraphQLInt - }, - retweets: { - description: "Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here)", - name: "retweets", - type: GraphQLInt - }, - skin: { - description: "The skin featured in this Tweet", - name: "skin", - type: SkinType - }, - url: { - description: "URL of the tweet. **Note:** Early on in the bot's life we just recorded\n_which_ skins were tweeted, not any info about the actual tweet. This means we\ndon't always know the URL of the tweet.", - name: "url", - type: GraphQLString - } - }; - } -}); -const SkinType: GraphQLInterfaceType = new GraphQLInterfaceType({ - description: "A Winamp skin. Could be modern or classic.\n\n**Note**: At some point in the future, this might be renamed to `Skin`.", - name: "Skin", - fields() { - return { - archive_files: { - description: "List of files contained within the skin's .wsz archive", - name: "archive_files", - type: new GraphQLList(ArchiveFileType) - }, - average_color: { - deprecationReason: "Needed for migration", - name: "average_color", - type: GraphQLString - }, - download_url: { - description: "URL to download the skin", - name: "download_url", - type: GraphQLString - }, - filename: { - description: "Filename of skin when uploaded to the Museum. Note: In some cases a skin\nhas been uploaded under multiple names. Here we just pick one.", - name: "filename", - type: GraphQLString, - args: { - normalize_extension: { - description: "If true, the the correct file extension (.wsz or .wal) will be .\nOtherwise, the original user-uploaded file extension will be used.", - name: "normalize_extension", - type: GraphQLBoolean, - defaultValue: false - } - } - }, - id: { - description: "GraphQL ID of the skin", - name: "id", - type: new GraphQLNonNull(GraphQLID) - }, - internet_archive_item: { - description: "The skin's \"item\" at archive.org", - name: "internet_archive_item", - type: InternetArchiveItemType - }, - md5: { - description: "MD5 hash of the skin's file", - name: "md5", - type: GraphQLString - }, - museum_url: { - deprecationReason: "Needed for migration", - name: "museum_url", - type: GraphQLString - }, - nsfw: { - deprecationReason: "Needed for migration", - name: "nsfw", - type: GraphQLBoolean - }, - readme_text: { - deprecationReason: "Needed for migration", - name: "readme_text", - type: GraphQLString - }, - reviews: { - description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreivew page, or via the Discord bot.", - name: "reviews", - type: new GraphQLList(ReviewType) - }, - screenshot_url: { - name: "screenshot_url", - type: GraphQLString - }, - tweeted: { - description: "Has the skin been tweeted?", - name: "tweeted", - type: GraphQLBoolean - }, - tweets: { - description: "List of `@winampskins` tweets that mentioned the skin.", - name: "tweets", - type: new GraphQLList(TweetType) - }, - webamp_url: { - deprecationReason: "Needed for migration", - name: "webamp_url", - type: GraphQLString - } - }; - } -}); -const ArchiveFileType: GraphQLObjectType = new GraphQLObjectType({ - name: "ArchiveFile", - description: "A file found within a Winamp Skin's .wsz archive", - fields() { - return { - date: { - description: "The date on the file inside the archive. Given in simplified extended ISO\nformat (ISO 8601).", - name: "date", - type: GraphQLString - }, - file_md5: { - description: "The md5 hash of the file within the archive", - name: "file_md5", - type: GraphQLString - }, - filename: { - description: "Filename of the file within the archive", - name: "filename", - type: GraphQLString - }, - is_directory: { - description: "Is the file a directory?", - name: "is_directory", - type: GraphQLBoolean - }, - size: { - description: "The uncompressed size of the file in bytes.\n\n**Note:** Will be `null` for directories", - name: "size", - type: GraphQLInt - }, - skin: { - description: "The skin in which this file was found", - name: "skin", - type: SkinType - }, - text_content: { - description: "The content of the file, if it's a text file", - name: "text_content", - type: GraphQLString - }, - url: { - description: "A URL to download the file. **Note:** This is powered by a little\nserverless Cloudflare function which tries to exctact the file on the fly.\nIt may not work for all files.", - name: "url", - type: GraphQLString - } - }; - } -}); -const UserType: GraphQLObjectType = new GraphQLObjectType({ - name: "User", - fields() { - return { - username: { - name: "username", - type: GraphQLString - } - }; - } -}); -const NodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ - description: "A globally unique object. The `id` here is intended only for use within\nGraphQL.\nhttps://graphql.org/learn/global-object-identification/", - name: "Node", - fields() { - return { - id: { - name: "id", - type: new GraphQLNonNull(GraphQLID) - } - }; - } -}); -const ModernSkinType: GraphQLObjectType = new GraphQLObjectType({ - name: "ModernSkin", - description: "A \"modern\" Winamp skin. These skins use the `.wal` file extension and are free-form.\n\nMost functionality in the Winamp Skin Museum is centered around \"classic\" skins,\nwhich are currently called just `Skin` in this schema.", - fields() { - return { - archive_files: { - description: "List of files contained within the skin's .wsz archive", - name: "archive_files", - type: new GraphQLList(ArchiveFileType) - }, - average_color: { - deprecationReason: "Needed for migration", - name: "average_color", - type: GraphQLString - }, - download_url: { - description: "URL to download the skin", - name: "download_url", - type: GraphQLString - }, - filename: { - description: "Filename of skin when uploaded to the Museum. Note: In some cases a skin\nhas been uploaded under multiple names. Here we just pick one.", - name: "filename", - type: GraphQLString, - args: { - normalize_extension: { - description: "If true, the the correct file extension (.wsz or .wal) will be .\nOtherwise, the original user-uploaded file extension will be used.", - name: "normalize_extension", - type: GraphQLBoolean, - defaultValue: false - } - } - }, - id: { - description: "GraphQL ID of the skin", - name: "id", - type: new GraphQLNonNull(GraphQLID) - }, - internet_archive_item: { - description: "The skin's \"item\" at archive.org", - name: "internet_archive_item", - type: InternetArchiveItemType - }, - md5: { - description: "MD5 hash of the skin's file", - name: "md5", - type: GraphQLString - }, - museum_url: { - deprecationReason: "Needed for migration", - name: "museum_url", - type: GraphQLString - }, - nsfw: { - deprecationReason: "Needed for migration", - name: "nsfw", - type: GraphQLBoolean - }, - readme_text: { - deprecationReason: "Needed for migration", - name: "readme_text", - type: GraphQLString - }, - reviews: { - description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreivew page, or via the Discord bot.", - name: "reviews", - type: new GraphQLList(ReviewType) - }, - screenshot_url: { - name: "screenshot_url", - type: GraphQLString - }, - tweeted: { - description: "Has the skin been tweeted?", - name: "tweeted", - type: GraphQLBoolean - }, - tweets: { - description: "List of `@winampskins` tweets that mentioned the skin.", - name: "tweets", - type: new GraphQLList(TweetType) - }, - webamp_url: { - deprecationReason: "Needed for migration", - name: "webamp_url", - type: GraphQLString - } - }; - }, - interfaces() { - return [NodeType, SkinType]; - } -}); -const ModernSkinsConnectionType: GraphQLObjectType = new GraphQLObjectType({ - name: "ModernSkinsConnection", - description: "A collection of \"modern\" Winamp skins", - fields() { - return { - count: { - description: "The total number of skins matching the filter", - name: "count", - type: GraphQLInt - }, - nodes: { - description: "The list of skins", - name: "nodes", - type: new GraphQLList(ModernSkinType) - } - }; - } -}); -const SkinsConnectionType: GraphQLObjectType = new GraphQLObjectType({ - name: "SkinsConnection", - description: "A collection of classic Winamp skins", - fields() { - return { - count: { - description: "The total number of skins matching the filter", - name: "count", - type: GraphQLInt - }, - nodes: { - description: "The list of skins", - name: "nodes", - type: new GraphQLList(SkinType) - } - }; - } -}); -const SkinsFilterOptionType: GraphQLEnumType = new GraphQLEnumType({ - name: "SkinsFilterOption", - values: { - APPROVED: { - value: "APPROVED" - }, - NSFW: { - value: "NSFW" - }, - REJECTED: { - value: "REJECTED" - }, - TWEETED: { - value: "TWEETED" } - } -}); -const SkinsSortOptionType: GraphQLEnumType = new GraphQLEnumType({ - name: "SkinsSortOption", - values: { - MUSEUM: { - value: "MUSEUM" + }); + const ReviewType: GraphQLObjectType = new GraphQLObjectType({ + name: "Review", + description: "A review of a skin. Done either on the Museum's Tinder-style\nreview page, or via the Discord bot.", + fields() { + return { + rating: { + description: "The rating that the user gave the skin", + name: "rating", + type: RatingType + }, + reviewer: { + description: "The user who made the review (if known). **Note:** In the early days we didn't\ntrack this, so many will be null.", + name: "reviewer", + type: GraphQLString + }, + skin: { + description: "The skin that was reviewed", + name: "skin", + type: SkinType + } + }; } - } -}); -const DatabaseStatisticsType: GraphQLObjectType = new GraphQLObjectType({ - name: "DatabaseStatistics", - description: "Statistics about the contents of the Museum's database.", - fields() { - return { - approved_skins_count: { - description: "The number of skins that have been approved for tweeting. This includes both\ntweeted and untweeted skins.\n\n**Note:** Skins can be both approved and rejected by different users.", - name: "approved_skins_count", - type: GraphQLInt - }, - nsfw_skins_count: { - description: "The number of skins that have been marked as NSFW.\n\n**Note:** Skins can be approved and rejected by different users.\n**Note:** Generally skins that have been marked NSFW are also marked as rejected.", - name: "nsfw_skins_count", - type: GraphQLInt - }, - rejected_skins_count: { - description: "The number of skins that have been rejected for tweeting.\n\n**Note:** Skins can be both approved and rejected by different users.\n**Note:** Generally skins that have been marked NSFW are also marked as rejected.", - name: "rejected_skins_count", - type: GraphQLInt - }, - tweetable_skins_count: { - description: "The number of skins that have been approved for tweeting, but not yet tweeted.", - name: "tweetable_skins_count", - type: GraphQLInt - }, - tweeted_skins_count: { - description: "The number of skins in the Museum that have been tweeted by `@winampskins`", - name: "tweeted_skins_count", - type: GraphQLInt - }, - unique_classic_skins_count: { - description: "The total number of classic skins in the Museum's database", - name: "unique_classic_skins_count", - type: GraphQLInt - }, - unreviewed_skins_count: { - description: "The number of skins that have never been reviewed.", - name: "unreviewed_skins_count", - type: GraphQLInt - }, - uploads_in_error_state_count: { - description: "Skins uploads that have errored during processing.", - name: "uploads_in_error_state_count", - type: GraphQLInt - }, - uploads_pending_processing_count: { - description: "Skins uplaods awaiting processing. This can happen when there are a large\nnumber of skin uplaods at the same time, or when the skin uploading processing\npipeline gets stuck.", - name: "uploads_pending_processing_count", - type: GraphQLInt - }, - web_uploads_count: { - description: "Number of skins that have been uploaded to the Museum via the web interface.", - name: "web_uploads_count", - type: GraphQLInt - } - }; - } -}); -const TweetsConnectionType: GraphQLObjectType = new GraphQLObjectType({ - name: "TweetsConnection", - description: "A collection of tweets made by the `@winampskins` bot", - fields() { - return { - count: { - description: "The total number of tweets", - name: "count", - type: GraphQLInt - }, - nodes: { - description: "The list of tweets", - name: "nodes", - type: new GraphQLList(TweetType) - } - }; - } -}); -const TweetsSortOptionType: GraphQLEnumType = new GraphQLEnumType({ - name: "TweetsSortOption", - values: { - LIKES: { - value: "LIKES" - }, - RETWEETS: { - value: "RETWEETS" + }); + const TweetType: GraphQLObjectType = new GraphQLObjectType({ + name: "Tweet", + description: "A tweet made by `@winampskins` mentioning a Winamp skin", + fields() { + return { + likes: { + description: "Number of likes the tweet has received. Updated nightly. (Note: Recent likes on older tweets may not be reflected here)", + name: "likes", + type: GraphQLInt + }, + retweets: { + description: "Number of retweets the tweet has received. Updated nightly. (Note: Recent retweets on older tweets may not be reflected here)", + name: "retweets", + type: GraphQLInt + }, + skin: { + description: "The skin featured in this Tweet", + name: "skin", + type: SkinType + }, + url: { + description: "URL of the tweet. **Note:** Early on in the bot's life we just recorded\n_which_ skins were tweeted, not any info about the actual tweet. This means we\ndon't always know the URL of the tweet.", + name: "url", + type: GraphQLString + } + }; } - } -}); -const SkinUploadStatusType: GraphQLEnumType = new GraphQLEnumType({ - description: "The current status of a pending upload.\n\n**Note:** Expect more values here as we try to be more transparent about\nthe status of a pending uploads.", - name: "SkinUploadStatus", - values: { - ARCHIVED: { - value: "ARCHIVED" - }, - DELAYED: { - value: "DELAYED" - }, - ERRORED: { - value: "ERRORED" - }, - UPLOAD_REPORTED: { - value: "UPLOAD_REPORTED" - }, - URL_REQUESTED: { - value: "URL_REQUESTED" + }); + const SkinType: GraphQLInterfaceType = new GraphQLInterfaceType({ + description: "A Winamp skin. Could be modern or classic.\n\n**Note**: At some point in the future, this might be renamed to `Skin`.", + name: "Skin", + fields() { + return { + archive_files: { + description: "List of files contained within the skin's .wsz archive", + name: "archive_files", + type: new GraphQLList(ArchiveFileType) + }, + average_color: { + deprecationReason: "Needed for migration", + name: "average_color", + type: GraphQLString + }, + download_url: { + description: "URL to download the skin", + name: "download_url", + type: GraphQLString + }, + filename: { + description: "Filename of skin when uploaded to the Museum. Note: In some cases a skin\nhas been uploaded under multiple names. Here we just pick one.", + name: "filename", + type: GraphQLString, + args: { + normalize_extension: { + description: "If true, the the correct file extension (.wsz or .wal) will be .\nOtherwise, the original user-uploaded file extension will be used.", + name: "normalize_extension", + type: GraphQLBoolean, + defaultValue: false + } + } + }, + id: { + description: "GraphQL ID of the skin", + name: "id", + type: new GraphQLNonNull(GraphQLID) + }, + internet_archive_item: { + description: "The skin's \"item\" at archive.org", + name: "internet_archive_item", + type: InternetArchiveItemType + }, + md5: { + description: "MD5 hash of the skin's file", + name: "md5", + type: GraphQLString + }, + museum_url: { + deprecationReason: "Needed for migration", + name: "museum_url", + type: GraphQLString + }, + nsfw: { + deprecationReason: "Needed for migration", + name: "nsfw", + type: GraphQLBoolean + }, + readme_text: { + deprecationReason: "Needed for migration", + name: "readme_text", + type: GraphQLString + }, + reviews: { + description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreivew page, or via the Discord bot.", + name: "reviews", + type: new GraphQLList(ReviewType) + }, + screenshot_url: { + name: "screenshot_url", + type: GraphQLString + }, + tweeted: { + description: "Has the skin been tweeted?", + name: "tweeted", + type: GraphQLBoolean + }, + tweets: { + description: "List of `@winampskins` tweets that mentioned the skin.", + name: "tweets", + type: new GraphQLList(TweetType) + }, + webamp_url: { + deprecationReason: "Needed for migration", + name: "webamp_url", + type: GraphQLString + } + }; } - } -}); -const SkinUploadType: GraphQLObjectType = new GraphQLObjectType({ - name: "SkinUpload", - description: "Information about an attempt to upload a skin to the Museum.", - fields() { - return { - id: { - name: "id", - type: GraphQLString + }); + const ArchiveFileType: GraphQLObjectType = new GraphQLObjectType({ + name: "ArchiveFile", + description: "A file found within a Winamp Skin's .wsz archive", + fields() { + return { + date: { + description: "The date on the file inside the archive. Given in simplified extended ISO\nformat (ISO 8601).", + name: "date", + type: GraphQLString + }, + file_md5: { + description: "The md5 hash of the file within the archive", + name: "file_md5", + type: GraphQLString + }, + filename: { + description: "Filename of the file within the archive", + name: "filename", + type: GraphQLString + }, + is_directory: { + description: "Is the file a directory?", + name: "is_directory", + type: GraphQLBoolean + }, + size: { + description: "The uncompressed size of the file in bytes.\n\n**Note:** Will be `null` for directories", + name: "size", + type: GraphQLInt + }, + skin: { + description: "The skin in which this file was found", + name: "skin", + type: SkinType + }, + text_content: { + description: "The content of the file, if it's a text file", + name: "text_content", + type: GraphQLString + }, + url: { + description: "A URL to download the file. **Note:** This is powered by a little\nserverless Cloudflare function which tries to exctact the file on the fly.\nIt may not work for all files.", + name: "url", + type: GraphQLString + } + }; + } + }); + const UserType: GraphQLObjectType = new GraphQLObjectType({ + name: "User", + fields() { + return { + username: { + name: "username", + type: GraphQLString + } + }; + } + }); + const NodeType: GraphQLInterfaceType = new GraphQLInterfaceType({ + description: "A globally unique object. The `id` here is intended only for use within\nGraphQL.\nhttps://graphql.org/learn/global-object-identification/", + name: "Node", + fields() { + return { + id: { + name: "id", + type: new GraphQLNonNull(GraphQLID) + } + }; + } + }); + const ModernSkinType: GraphQLObjectType = new GraphQLObjectType({ + name: "ModernSkin", + description: "A \"modern\" Winamp skin. These skins use the `.wal` file extension and are free-form.\n\nMost functionality in the Winamp Skin Museum is centered around \"classic\" skins,\nwhich are currently called just `Skin` in this schema.", + fields() { + return { + archive_files: { + description: "List of files contained within the skin's .wsz archive", + name: "archive_files", + type: new GraphQLList(ArchiveFileType) + }, + average_color: { + deprecationReason: "Needed for migration", + name: "average_color", + type: GraphQLString + }, + download_url: { + description: "URL to download the skin", + name: "download_url", + type: GraphQLString + }, + filename: { + description: "Filename of skin when uploaded to the Museum. Note: In some cases a skin\nhas been uploaded under multiple names. Here we just pick one.", + name: "filename", + type: GraphQLString, + args: { + normalize_extension: { + description: "If true, the the correct file extension (.wsz or .wal) will be .\nOtherwise, the original user-uploaded file extension will be used.", + name: "normalize_extension", + type: GraphQLBoolean, + defaultValue: false + } + } + }, + id: { + description: "GraphQL ID of the skin", + name: "id", + type: new GraphQLNonNull(GraphQLID) + }, + internet_archive_item: { + description: "The skin's \"item\" at archive.org", + name: "internet_archive_item", + type: InternetArchiveItemType + }, + md5: { + description: "MD5 hash of the skin's file", + name: "md5", + type: GraphQLString + }, + museum_url: { + deprecationReason: "Needed for migration", + name: "museum_url", + type: GraphQLString + }, + nsfw: { + deprecationReason: "Needed for migration", + name: "nsfw", + type: GraphQLBoolean + }, + readme_text: { + deprecationReason: "Needed for migration", + name: "readme_text", + type: GraphQLString + }, + reviews: { + description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreivew page, or via the Discord bot.", + name: "reviews", + type: new GraphQLList(ReviewType) + }, + screenshot_url: { + name: "screenshot_url", + type: GraphQLString + }, + tweeted: { + description: "Has the skin been tweeted?", + name: "tweeted", + type: GraphQLBoolean + }, + tweets: { + description: "List of `@winampskins` tweets that mentioned the skin.", + name: "tweets", + type: new GraphQLList(TweetType) + }, + webamp_url: { + deprecationReason: "Needed for migration", + name: "webamp_url", + type: GraphQLString + } + }; + }, + interfaces() { + return [NodeType, SkinType]; + } + }); + const ModernSkinsConnectionType: GraphQLObjectType = new GraphQLObjectType({ + name: "ModernSkinsConnection", + description: "A collection of \"modern\" Winamp skins", + fields() { + return { + count: { + description: "The total number of skins matching the filter", + name: "count", + type: GraphQLInt + }, + nodes: { + description: "The list of skins", + name: "nodes", + type: new GraphQLList(ModernSkinType) + } + }; + } + }); + const SkinsConnectionType: GraphQLObjectType = new GraphQLObjectType({ + name: "SkinsConnection", + description: "A collection of classic Winamp skins", + fields() { + return { + count: { + description: "The total number of skins matching the filter", + name: "count", + type: GraphQLInt + }, + nodes: { + description: "The list of skins", + name: "nodes", + type: new GraphQLList(SkinType) + } + }; + } + }); + const SkinsFilterOptionType: GraphQLEnumType = new GraphQLEnumType({ + name: "SkinsFilterOption", + values: { + APPROVED: { + value: "APPROVED" }, - skin: { - description: "Skin that was uploaded. **Note:** This is null if the skin has not yet been\nfully processed. (status == ARCHIVED)", - name: "skin", - type: SkinType + NSFW: { + value: "NSFW" }, - status: { - name: "status", - type: SkinUploadStatusType + REJECTED: { + value: "REJECTED" }, - upload_md5: { - description: "Md5 hash given when requesting the upload URL.", - name: "upload_md5", - type: GraphQLString + TWEETED: { + value: "TWEETED" } - }; - } -}); -const QueryType: GraphQLObjectType = new GraphQLObjectType({ - name: "Query", - fields() { - return { - fetch_archive_file_by_md5: { - description: "Fetch archive file by it's MD5 hash\n\nGet information about a file found within a skin's wsz/wal/zip archive.", - name: "fetch_archive_file_by_md5", - type: ArchiveFileType, - args: { - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return queryFetch_archive_file_by_md5Resolver(source, args, context); - } - }, - fetch_internet_archive_item_by_identifier: { - description: "Get an archive.org item by its identifier. You can find this in the URL:\n\nhttps://archive.org/details//", - name: "fetch_internet_archive_item_by_identifier", - type: InternetArchiveItemType, - args: { - identifier: { - name: "identifier", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return queryFetch_internet_archive_item_by_identifierResolver(source, args, context); - } - }, - fetch_skin_by_md5: { - description: "Get a skin by its MD5 hash", - name: "fetch_skin_by_md5", - type: SkinType, - args: { - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return queryFetch_skin_by_md5Resolver(source, args, context); - } - }, - fetch_tweet_by_url: { - description: "Get a tweet by its URL", - name: "fetch_tweet_by_url", - type: TweetType, - args: { - url: { - name: "url", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return queryFetch_tweet_by_urlResolver(source, args, context); - } - }, - me: { - description: "The currently authenticated user, if any.", - name: "me", - type: UserType, - resolve(source) { - return queryMeResolver(source); - } - }, - modern_skins: { - description: "All modern skins in the database", - name: "modern_skins", - type: ModernSkinsConnectionType, - args: { - first: { - name: "first", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 10 - }, - offset: { - name: "offset", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 0 - } - }, - resolve(source, args) { - return queryModern_skinsResolver(source, args); - } - }, - node: { - description: "Get a globally unique object by its ID.\n\nhttps://graphql.org/learn/global-object-identification/", - name: "node", - type: NodeType, - args: { - id: { - name: "id", - type: new GraphQLNonNull(GraphQLID) - } - }, - resolve(source, args, context) { - return queryNodeResolver(source, args, context); - } - }, - search_skins: { - description: "Search the database using the Algolia search index used by the Museum.\n\nUseful for locating a particular skin.", - name: "search_skins", - type: new GraphQLList(SkinType), - args: { - first: { - name: "first", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 10 - }, - offset: { - name: "offset", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 0 - }, - query: { - name: "query", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return querySearch_skinsResolver(source, args, context); - } - }, - skin_to_review: { - description: "A random skin that needs to be reviewed", - name: "skin_to_review", - type: SkinType, - resolve(source, args, context) { - return querySkin_to_reviewResolver(source, args, context); - } - }, - skins: { - description: "All classic skins in the database\n\n**Note:** We don't currently support combining sorting and filtering.", - name: "skins", - type: SkinsConnectionType, - args: { - filter: { - name: "filter", - type: SkinsFilterOptionType - }, - first: { - name: "first", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 10 - }, - offset: { - name: "offset", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 0 - }, - sort: { - name: "sort", - type: SkinsSortOptionType - } - }, - resolve(source, args) { - return querySkinsResolver(source, args); - } - }, - statistics: { - description: "A namespace for statistics about the database", - name: "statistics", - type: DatabaseStatisticsType, - resolve(source) { - return queryStatisticsResolver(source); - } - }, - tweets: { - description: "Tweets tweeted by `@winampskins`", - name: "tweets", - type: TweetsConnectionType, - args: { - first: { - name: "first", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 10 - }, - offset: { - name: "offset", - type: new GraphQLNonNull(GraphQLInt), - defaultValue: 0 - }, - sort: { - name: "sort", - type: TweetsSortOptionType - } - }, - resolve(source, args) { - return queryTweetsResolver(source, args); - } - }, - upload_statuses: { - description: "Get the status of a batch of uploads by ids", - name: "upload_statuses", - type: new GraphQLList(SkinUploadType), - args: { - ids: { - name: "ids", - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))) - } - }, - resolve(source, args, context) { - return queryUpload_statusesResolver(source, args, context); - } - }, - upload_statuses_by_md5: { - description: "Get the status of a batch of uploads by md5s", - deprecationReason: "Prefer `upload_statuses` instead, were we operate on ids.", - name: "upload_statuses_by_md5", - type: new GraphQLList(SkinUploadType), - args: { - md5s: { - name: "md5s", - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))) - } - }, - resolve(source, args, context) { - return queryUpload_statuses_by_md5Resolver(source, args, context); - } + } + }); + const SkinsSortOptionType: GraphQLEnumType = new GraphQLEnumType({ + name: "SkinsSortOption", + values: { + MUSEUM: { + value: "MUSEUM" } - }; - } -}); -const UploadUrlType: GraphQLObjectType = new GraphQLObjectType({ - name: "UploadUrl", - description: "A URL that the client can use to upload a skin to S3, and then notify the server\nwhen they're done.", - fields() { - return { - id: { - name: "id", - type: GraphQLString - }, - md5: { - name: "md5", - type: GraphQLString - }, - url: { - name: "url", - type: GraphQLString - } - }; - } -}); -const UploadUrlRequestType: GraphQLInputObjectType = new GraphQLInputObjectType({ - description: "Input object used for a user to request an UploadUrl", - name: "UploadUrlRequest", - fields() { - return { - filename: { - name: "filename", - type: new GraphQLNonNull(GraphQLString) - }, - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) - } - }; - } -}); -const UploadMutationsType: GraphQLObjectType = new GraphQLObjectType({ - name: "UploadMutations", - description: "Mutations for the upload flow\n\n1. The user finds the md5 hash of their local files.\n2. (`get_upload_urls`) The user requests upload URLs for each of their files.\n3. The server returns upload URLs for each of their files which are not already in the collection.\n4. The user uploads each of their files to the URLs returned in step 3.\n5. (`report_skin_uploaded`) The user notifies the server that they're done uploading.\n6. (TODO) The user polls for the status of their uploads.", - fields() { - return { - get_upload_urls: { - description: "Get a (possibly incomplete) list of UploadUrls for each of the files. If an\nUploadUrl is not returned for a given hash, it means the file is already in\nthe collection.", - name: "get_upload_urls", - type: new GraphQLList(UploadUrlType), - args: { - files: { - name: "files", - type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(UploadUrlRequestType))) - } + } + }); + const DatabaseStatisticsType: GraphQLObjectType = new GraphQLObjectType({ + name: "DatabaseStatistics", + description: "Statistics about the contents of the Museum's database.", + fields() { + return { + approved_skins_count: { + description: "The number of skins that have been approved for tweeting. This includes both\ntweeted and untweeted skins.\n\n**Note:** Skins can be both approved and rejected by different users.", + name: "approved_skins_count", + type: GraphQLInt + }, + nsfw_skins_count: { + description: "The number of skins that have been marked as NSFW.\n\n**Note:** Skins can be approved and rejected by different users.\n**Note:** Generally skins that have been marked NSFW are also marked as rejected.", + name: "nsfw_skins_count", + type: GraphQLInt + }, + rejected_skins_count: { + description: "The number of skins that have been rejected for tweeting.\n\n**Note:** Skins can be both approved and rejected by different users.\n**Note:** Generally skins that have been marked NSFW are also marked as rejected.", + name: "rejected_skins_count", + type: GraphQLInt + }, + tweetable_skins_count: { + description: "The number of skins that have been approved for tweeting, but not yet tweeted.", + name: "tweetable_skins_count", + type: GraphQLInt + }, + tweeted_skins_count: { + description: "The number of skins in the Museum that have been tweeted by `@winampskins`", + name: "tweeted_skins_count", + type: GraphQLInt + }, + unique_classic_skins_count: { + description: "The total number of classic skins in the Museum's database", + name: "unique_classic_skins_count", + type: GraphQLInt + }, + unreviewed_skins_count: { + description: "The number of skins that have never been reviewed.", + name: "unreviewed_skins_count", + type: GraphQLInt + }, + uploads_in_error_state_count: { + description: "Skins uploads that have errored during processing.", + name: "uploads_in_error_state_count", + type: GraphQLInt + }, + uploads_pending_processing_count: { + description: "Skins uplaods awaiting processing. This can happen when there are a large\nnumber of skin uplaods at the same time, or when the skin uploading processing\npipeline gets stuck.", + name: "uploads_pending_processing_count", + type: GraphQLInt + }, + web_uploads_count: { + description: "Number of skins that have been uploaded to the Museum via the web interface.", + name: "web_uploads_count", + type: GraphQLInt } + }; + } + }); + const TweetsConnectionType: GraphQLObjectType = new GraphQLObjectType({ + name: "TweetsConnection", + description: "A collection of tweets made by the `@winampskins` bot", + fields() { + return { + count: { + description: "The total number of tweets", + name: "count", + type: GraphQLInt + }, + nodes: { + description: "The list of tweets", + name: "nodes", + type: new GraphQLList(TweetType) + } + }; + } + }); + const TweetsSortOptionType: GraphQLEnumType = new GraphQLEnumType({ + name: "TweetsSortOption", + values: { + LIKES: { + value: "LIKES" }, - report_skin_uploaded: { - description: "Notify the server that the user is done uploading.", - name: "report_skin_uploaded", - type: GraphQLBoolean, - args: { - id: { - name: "id", - type: new GraphQLNonNull(GraphQLString) + RETWEETS: { + value: "RETWEETS" + } + } + }); + const SkinUploadStatusType: GraphQLEnumType = new GraphQLEnumType({ + description: "The current status of a pending upload.\n\n**Note:** Expect more values here as we try to be more transparent about\nthe status of a pending uploads.", + name: "SkinUploadStatus", + values: { + ARCHIVED: { + value: "ARCHIVED" + }, + DELAYED: { + value: "DELAYED" + }, + ERRORED: { + value: "ERRORED" + }, + UPLOAD_REPORTED: { + value: "UPLOAD_REPORTED" + }, + URL_REQUESTED: { + value: "URL_REQUESTED" + } + } + }); + const SkinUploadType: GraphQLObjectType = new GraphQLObjectType({ + name: "SkinUpload", + description: "Information about an attempt to upload a skin to the Museum.", + fields() { + return { + id: { + name: "id", + type: GraphQLString + }, + skin: { + description: "Skin that was uploaded. **Note:** This is null if the skin has not yet been\nfully processed. (status == ARCHIVED)", + name: "skin", + type: SkinType + }, + status: { + name: "status", + type: SkinUploadStatusType + }, + upload_md5: { + description: "Md5 hash given when requesting the upload URL.", + name: "upload_md5", + type: GraphQLString + } + }; + } + }); + const QueryType: GraphQLObjectType = new GraphQLObjectType({ + name: "Query", + fields() { + return { + fetch_archive_file_by_md5: { + description: "Fetch archive file by it's MD5 hash\n\nGet information about a file found within a skin's wsz/wal/zip archive.", + name: "fetch_archive_file_by_md5", + type: ArchiveFileType, + args: { + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } }, - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) - } - } - } - }; - } -}); -const MutationType: GraphQLObjectType = new GraphQLObjectType({ - name: "Mutation", - fields() { - return { - approve_skin: { - description: "Approve skin for tweeting\n\n**Note:** Requires being logged in", - name: "approve_skin", - type: GraphQLBoolean, - args: { - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) + resolve(source, args, context) { + return queryFetch_archive_file_by_md5Resolver(source, args, context); } }, - resolve(source, args, context) { - return mutationApprove_skinResolver(source, args, context); - } - }, - mark_skin_nsfw: { - description: "Mark a skin as NSFW\n\n**Note:** Requires being logged in", - name: "mark_skin_nsfw", - type: GraphQLBoolean, - args: { - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return mutationMark_skin_nsfwResolver(source, args, context); - } - }, - reject_skin: { - description: "Reject skin for tweeting\n\n**Note:** Requires being logged in", - name: "reject_skin", - type: GraphQLBoolean, - args: { - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return mutationReject_skinResolver(source, args, context); - } - }, - request_nsfw_review_for_skin: { - description: "Request that an admin check if this skin is NSFW.\nUnlike other review mutation endpoints, this one does not require being logged\nin.", - name: "request_nsfw_review_for_skin", - type: GraphQLBoolean, - args: { - md5: { - name: "md5", - type: new GraphQLNonNull(GraphQLString) - } - }, - resolve(source, args, context) { - return mutationRequest_nsfw_review_for_skinResolver(source, args, context); - } - }, - send_feedback: { - description: "Send a message to the admin of the site. Currently this appears in Discord.", - name: "send_feedback", - type: GraphQLBoolean, - args: { - email: { - name: "email", - type: GraphQLString + fetch_internet_archive_item_by_identifier: { + description: "Get an archive.org item by its identifier. You can find this in the URL:\n\nhttps://archive.org/details//", + name: "fetch_internet_archive_item_by_identifier", + type: InternetArchiveItemType, + args: { + identifier: { + name: "identifier", + type: new GraphQLNonNull(GraphQLString) + } }, - message: { - name: "message", - type: new GraphQLNonNull(GraphQLString) - }, - url: { - name: "url", - type: GraphQLString + resolve(source, args, context) { + return queryFetch_internet_archive_item_by_identifierResolver(source, args, context); } }, - resolve(source, args, context) { - return mutationSend_feedbackResolver(source, args, context); - } - }, - upload: { - description: "Mutations for the upload flow", - name: "upload", - type: UploadMutationsType, - resolve(source) { - return mutationUploadResolver(source); - } - } - }; - } -}); -const ClassicSkinType: GraphQLObjectType = new GraphQLObjectType({ - name: "ClassicSkin", - description: "A classic Winamp skin", - fields() { - return { - archive_files: { - description: "List of files contained within the skin's .wsz archive", - name: "archive_files", - type: new GraphQLList(ArchiveFileType) - }, - average_color: { - description: "String representation (rgb usually) of the skin's average color", - name: "average_color", - type: GraphQLString - }, - download_url: { - description: "URL to download the skin", - name: "download_url", - type: GraphQLString - }, - filename: { - description: "Filename of skin when uploaded to the Museum. Note: In some cases a skin\nhas been uploaded under multiple names. Here we just pick one.", - name: "filename", - type: GraphQLString, - args: { - normalize_extension: { - description: "If true, the the correct file extension (.wsz or .wal) will be .\nOtherwise, the original user-uploaded file extension will be used.", - name: "normalize_extension", - type: GraphQLBoolean, - defaultValue: false + fetch_skin_by_md5: { + description: "Get a skin by its MD5 hash", + name: "fetch_skin_by_md5", + type: SkinType, + args: { + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return queryFetch_skin_by_md5Resolver(source, args, context); + } + }, + fetch_tweet_by_url: { + description: "Get a tweet by its URL", + name: "fetch_tweet_by_url", + type: TweetType, + args: { + url: { + name: "url", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return queryFetch_tweet_by_urlResolver(source, args, context); + } + }, + me: { + description: "The currently authenticated user, if any.", + name: "me", + type: UserType, + resolve(source) { + return queryMeResolver(source); + } + }, + modern_skins: { + description: "All modern skins in the database", + name: "modern_skins", + type: ModernSkinsConnectionType, + args: { + first: { + name: "first", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 10 + }, + offset: { + name: "offset", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 0 + } + }, + resolve(source, args) { + return queryModern_skinsResolver(source, args); + } + }, + node: { + description: "Get a globally unique object by its ID.\n\nhttps://graphql.org/learn/global-object-identification/", + name: "node", + type: NodeType, + args: { + id: { + name: "id", + type: new GraphQLNonNull(GraphQLID) + } + }, + resolve(source, args, context) { + return queryNodeResolver(source, args, context); + } + }, + search_skins: { + description: "Search the database using the Algolia search index used by the Museum.\n\nUseful for locating a particular skin.", + name: "search_skins", + type: new GraphQLList(SkinType), + args: { + first: { + name: "first", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 10 + }, + offset: { + name: "offset", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 0 + }, + query: { + name: "query", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return querySearch_skinsResolver(source, args, context); + } + }, + skin_to_review: { + description: "A random skin that needs to be reviewed", + name: "skin_to_review", + type: SkinType, + resolve(source, args, context) { + return querySkin_to_reviewResolver(source, args, context); + } + }, + skins: { + description: "All classic skins in the database\n\n**Note:** We don't currently support combining sorting and filtering.", + name: "skins", + type: SkinsConnectionType, + args: { + filter: { + name: "filter", + type: SkinsFilterOptionType + }, + first: { + name: "first", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 10 + }, + offset: { + name: "offset", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 0 + }, + sort: { + name: "sort", + type: SkinsSortOptionType + } + }, + resolve(source, args) { + return querySkinsResolver(source, args); + } + }, + statistics: { + description: "A namespace for statistics about the database", + name: "statistics", + type: DatabaseStatisticsType, + resolve(source) { + return queryStatisticsResolver(source); + } + }, + tweets: { + description: "Tweets tweeted by `@winampskins`", + name: "tweets", + type: TweetsConnectionType, + args: { + first: { + name: "first", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 10 + }, + offset: { + name: "offset", + type: new GraphQLNonNull(GraphQLInt), + defaultValue: 0 + }, + sort: { + name: "sort", + type: TweetsSortOptionType + } + }, + resolve(source, args) { + return queryTweetsResolver(source, args); + } + }, + upload_statuses: { + description: "Get the status of a batch of uploads by ids", + name: "upload_statuses", + type: new GraphQLList(SkinUploadType), + args: { + ids: { + name: "ids", + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))) + } + }, + resolve(source, args, context) { + return queryUpload_statusesResolver(source, args, context); + } + }, + upload_statuses_by_md5: { + description: "Get the status of a batch of uploads by md5s", + deprecationReason: "Prefer `upload_statuses` instead, were we operate on ids.", + name: "upload_statuses_by_md5", + type: new GraphQLList(SkinUploadType), + args: { + md5s: { + name: "md5s", + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(GraphQLString))) + } + }, + resolve(source, args, context) { + return queryUpload_statuses_by_md5Resolver(source, args, context); } } - }, - has_media_library: { - description: "Does the skin include sprite sheets for the media library?", - name: "has_media_library", - type: GraphQLBoolean - }, - id: { - description: "GraphQL ID of the skin", - name: "id", - type: new GraphQLNonNull(GraphQLID) - }, - internet_archive_item: { - description: "The skin's \"item\" at archive.org", - name: "internet_archive_item", - type: InternetArchiveItemType - }, - last_algolia_index_update_date: { - description: "The date on which this skin was last updated in the Algolia search index.\nGiven in simplified extended ISO format (ISO 8601).", - name: "last_algolia_index_update_date", - type: GraphQLString - }, - md5: { - description: "MD5 hash of the skin's file", - name: "md5", - type: GraphQLString - }, - museum_url: { - description: "URL of the skin on the Winamp Skin Museum", - name: "museum_url", - type: GraphQLString - }, - nsfw: { - description: "Has the skin been flagged as \"not safe for wrok\"?", - name: "nsfw", - type: GraphQLBoolean - }, - readme_text: { - description: "Text of the readme file extracted from the skin", - name: "readme_text", - type: GraphQLString - }, - reviews: { - description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreivew page, or via the Discord bot.", - name: "reviews", - type: new GraphQLList(ReviewType) - }, - screenshot_url: { - description: "URL of a screenshot of the skin", - name: "screenshot_url", - type: GraphQLString - }, - transparent_pixels: { - description: "The number of transparent pixels rendered by the skin.", - name: "transparent_pixels", - type: GraphQLInt - }, - tweeted: { - description: "Has the skin been tweeted?", - name: "tweeted", - type: GraphQLBoolean - }, - tweets: { - description: "List of `@winampskins` tweets that mentioned the skin.", - name: "tweets", - type: new GraphQLList(TweetType) - }, - webamp_url: { - description: "URL of webamp.org with the skin loaded", - name: "webamp_url", - type: GraphQLString - } - }; - }, - interfaces() { - return [NodeType, SkinType]; - } -}); -const schema = new GraphQLSchema({ - query: QueryType, - mutation: MutationType, - types: [ArchiveFileType, ClassicSkinType, DatabaseStatisticsType, InternetArchiveItemType, ModernSkinType, ModernSkinsConnectionType, MutationType, NodeType, QueryType, RatingType, ReviewType, SkinType, SkinUploadType, SkinUploadStatusType, SkinsConnectionType, SkinsFilterOptionType, SkinsSortOptionType, TweetType, TweetsConnectionType, TweetsSortOptionType, UploadMutationsType, UploadUrlType, UploadUrlRequestType, UserType] -}); -export { schema }; + }; + } + }); + const UploadUrlType: GraphQLObjectType = new GraphQLObjectType({ + name: "UploadUrl", + description: "A URL that the client can use to upload a skin to S3, and then notify the server\nwhen they're done.", + fields() { + return { + id: { + name: "id", + type: GraphQLString + }, + md5: { + name: "md5", + type: GraphQLString + }, + url: { + name: "url", + type: GraphQLString + } + }; + } + }); + const UploadUrlRequestType: GraphQLInputObjectType = new GraphQLInputObjectType({ + description: "Input object used for a user to request an UploadUrl", + name: "UploadUrlRequest", + fields() { + return { + filename: { + name: "filename", + type: new GraphQLNonNull(GraphQLString) + }, + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } + }; + } + }); + const UploadMutationsType: GraphQLObjectType = new GraphQLObjectType({ + name: "UploadMutations", + description: "Mutations for the upload flow\n\n1. The user finds the md5 hash of their local files.\n2. (`get_upload_urls`) The user requests upload URLs for each of their files.\n3. The server returns upload URLs for each of their files which are not already in the collection.\n4. The user uploads each of their files to the URLs returned in step 3.\n5. (`report_skin_uploaded`) The user notifies the server that they're done uploading.\n6. (TODO) The user polls for the status of their uploads.", + fields() { + return { + get_upload_urls: { + description: "Get a (possibly incomplete) list of UploadUrls for each of the files. If an\nUploadUrl is not returned for a given hash, it means the file is already in\nthe collection.", + name: "get_upload_urls", + type: new GraphQLList(UploadUrlType), + args: { + files: { + name: "files", + type: new GraphQLNonNull(new GraphQLList(new GraphQLNonNull(UploadUrlRequestType))) + } + } + }, + report_skin_uploaded: { + description: "Notify the server that the user is done uploading.", + name: "report_skin_uploaded", + type: GraphQLBoolean, + args: { + id: { + name: "id", + type: new GraphQLNonNull(GraphQLString) + }, + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } + } + } + }; + } + }); + const MutationType: GraphQLObjectType = new GraphQLObjectType({ + name: "Mutation", + fields() { + return { + approve_skin: { + description: "Approve skin for tweeting\n\n**Note:** Requires being logged in", + name: "approve_skin", + type: GraphQLBoolean, + args: { + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return mutationApprove_skinResolver(source, args, context); + } + }, + mark_skin_nsfw: { + description: "Mark a skin as NSFW\n\n**Note:** Requires being logged in", + name: "mark_skin_nsfw", + type: GraphQLBoolean, + args: { + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return mutationMark_skin_nsfwResolver(source, args, context); + } + }, + reject_skin: { + description: "Reject skin for tweeting\n\n**Note:** Requires being logged in", + name: "reject_skin", + type: GraphQLBoolean, + args: { + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return mutationReject_skinResolver(source, args, context); + } + }, + request_nsfw_review_for_skin: { + description: "Request that an admin check if this skin is NSFW.\nUnlike other review mutation endpoints, this one does not require being logged\nin.", + name: "request_nsfw_review_for_skin", + type: GraphQLBoolean, + args: { + md5: { + name: "md5", + type: new GraphQLNonNull(GraphQLString) + } + }, + resolve(source, args, context) { + return mutationRequest_nsfw_review_for_skinResolver(source, args, context); + } + }, + send_feedback: { + description: "Send a message to the admin of the site. Currently this appears in Discord.", + name: "send_feedback", + type: GraphQLBoolean, + args: { + email: { + name: "email", + type: GraphQLString + }, + message: { + name: "message", + type: new GraphQLNonNull(GraphQLString) + }, + url: { + name: "url", + type: GraphQLString + } + }, + resolve(source, args, context) { + return mutationSend_feedbackResolver(source, args, context); + } + }, + upload: { + description: "Mutations for the upload flow", + name: "upload", + type: UploadMutationsType, + resolve(source) { + return mutationUploadResolver(source); + } + } + }; + } + }); + const ClassicSkinType: GraphQLObjectType = new GraphQLObjectType({ + name: "ClassicSkin", + description: "A classic Winamp skin", + fields() { + return { + archive_files: { + description: "List of files contained within the skin's .wsz archive", + name: "archive_files", + type: new GraphQLList(ArchiveFileType) + }, + average_color: { + description: "String representation (rgb usually) of the skin's average color", + name: "average_color", + type: GraphQLString + }, + download_url: { + description: "URL to download the skin", + name: "download_url", + type: GraphQLString + }, + filename: { + description: "Filename of skin when uploaded to the Museum. Note: In some cases a skin\nhas been uploaded under multiple names. Here we just pick one.", + name: "filename", + type: GraphQLString, + args: { + normalize_extension: { + description: "If true, the the correct file extension (.wsz or .wal) will be .\nOtherwise, the original user-uploaded file extension will be used.", + name: "normalize_extension", + type: GraphQLBoolean, + defaultValue: false + } + } + }, + has_media_library: { + description: "Does the skin include sprite sheets for the media library?", + name: "has_media_library", + type: GraphQLBoolean + }, + id: { + description: "GraphQL ID of the skin", + name: "id", + type: new GraphQLNonNull(GraphQLID) + }, + internet_archive_item: { + description: "The skin's \"item\" at archive.org", + name: "internet_archive_item", + type: InternetArchiveItemType + }, + last_algolia_index_update_date: { + description: "The date on which this skin was last updated in the Algolia search index.\nGiven in simplified extended ISO format (ISO 8601).", + name: "last_algolia_index_update_date", + type: GraphQLString + }, + md5: { + description: "MD5 hash of the skin's file", + name: "md5", + type: GraphQLString + }, + museum_url: { + description: "URL of the skin on the Winamp Skin Museum", + name: "museum_url", + type: GraphQLString + }, + nsfw: { + description: "Has the skin been flagged as \"not safe for wrok\"?", + name: "nsfw", + type: GraphQLBoolean + }, + readme_text: { + description: "Text of the readme file extracted from the skin", + name: "readme_text", + type: GraphQLString + }, + reviews: { + description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreivew page, or via the Discord bot.", + name: "reviews", + type: new GraphQLList(ReviewType) + }, + screenshot_url: { + description: "URL of a screenshot of the skin", + name: "screenshot_url", + type: GraphQLString + }, + transparent_pixels: { + description: "The number of transparent pixels rendered by the skin.", + name: "transparent_pixels", + type: GraphQLInt + }, + tweeted: { + description: "Has the skin been tweeted?", + name: "tweeted", + type: GraphQLBoolean + }, + tweets: { + description: "List of `@winampskins` tweets that mentioned the skin.", + name: "tweets", + type: new GraphQLList(TweetType) + }, + webamp_url: { + description: "URL of webamp.org with the skin loaded", + name: "webamp_url", + type: GraphQLString + } + }; + }, + interfaces() { + return [NodeType, SkinType]; + } + }); + return new GraphQLSchema({ + query: QueryType, + mutation: MutationType, + types: [RatingType, SkinUploadStatusType, SkinsFilterOptionType, SkinsSortOptionType, TweetsSortOptionType, NodeType, SkinType, UploadUrlRequestType, ArchiveFileType, ClassicSkinType, DatabaseStatisticsType, InternetArchiveItemType, ModernSkinType, ModernSkinsConnectionType, MutationType, QueryType, ReviewType, SkinUploadType, SkinsConnectionType, TweetType, TweetsConnectionType, UploadMutationsType, UploadUrlType, UserType] + }); +} diff --git a/packages/skin-database/package.json b/packages/skin-database/package.json index 504aaaab..76034962 100644 --- a/packages/skin-database/package.json +++ b/packages/skin-database/package.json @@ -29,6 +29,7 @@ "express-sitemap-xml": "^2.0.0", "fast-xml-parser": "^4.2.2", "graphql": "16.6.0", + "grats": "^0.0.11", "imagemin": "^7.0.0", "imagemin-optipng": "^7.0.0", "knex": "^0.21.1", @@ -64,9 +65,9 @@ "@types/supertest": "^2.0.10", "@typescript-eslint/eslint-plugin": "^5.15.0", "@typescript-eslint/parser": "^5.15.0", + "grats": "^0.0.22", "supertest": "^6.0.1", - "typescript": "^3.8.3", - "grats": "0.0.0-main-d4d43d4b" + "typescript": "^3.8.3" }, "resolutions": { "graphql": "16.6.0" diff --git a/yarn.lock b/yarn.lock index 9ef84aff..b3c71d96 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14765,20 +14765,15 @@ 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@16.6.0: +graphql@16.6.0, graphql@^16.6.0: version "16.6.0" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.6.0.tgz#c2dcffa4649db149f6282af726c8c83f1c7c5fdb" integrity sha512-KPIBPDlW7NxrbT/eh4qPXz5FiFdL5UbaA0XUNz2Rp3Z3hqBSkbj0GVjwFDztsWVauZUWsbKHgMg++sk8UX0bkw== -graphql@^16.6.0: - version "16.8.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" - integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== - -grats@0.0.0-main-d4d43d4b: - version "0.0.0-main-d4d43d4b" - resolved "https://registry.yarnpkg.com/grats/-/grats-0.0.0-main-d4d43d4b.tgz#5e81c5558568251b9e026aeb77782198a8647a6d" - integrity sha512-SvxKQ5VV5W4tcxPHWutt5JSt2MUFbeWydKuiloPIIhzibiMMwNdCoPIO1E8YnQN//tCEsRdFqeV29zu/tENQeQ== +grats@^0.0.22: + version "0.0.22" + resolved "https://registry.yarnpkg.com/grats/-/grats-0.0.22.tgz#a391122dbd0c0de84ee3f2cadca4bbbe11fa129c" + integrity sha512-CqP0/M1GIa9l24o+fkazIzbAQrtDGaLWFV731M9/q0j7ehL5/hjF6PqvxkNYMefA20R73ygtxyr7XW3ebSH0Zw== dependencies: commander "^10.0.0" graphql "^16.6.0"