From faee5410ac636e827f4a19f043824c26deba063e Mon Sep 17 00:00:00 2001 From: Jordan Eldredge Date: Sat, 25 Jan 2025 13:24:11 -0800 Subject: [PATCH] Add support for SQLite based search --- packages/skin-database/api/graphql/index.ts | 40 +- .../api/graphql/resolvers/SkinResolver.ts | 58 ++- .../skin-database/api/graphql/schema.graphql | 6 + packages/skin-database/api/graphql/schema.ts | 348 ++++++++++-------- .../migrations/20250123223740_search_index.ts | 88 +++++ packages/skin-database/package.json | 1 + yarn.lock | 170 ++++++++- 7 files changed, 498 insertions(+), 213 deletions(-) create mode 100644 packages/skin-database/migrations/20250123223740_search_index.ts diff --git a/packages/skin-database/api/graphql/index.ts b/packages/skin-database/api/graphql/index.ts index 3af1ba56..efd96775 100644 --- a/packages/skin-database/api/graphql/index.ts +++ b/packages/skin-database/api/graphql/index.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { createHandler } from "graphql-http/lib/use/express"; +import { createYoga, YogaInitialContext } from "graphql-yoga"; // import DEFAULT_QUERY from "./defaultQuery"; import { getSchema } from "./schema"; @@ -15,34 +15,14 @@ export function getUserContext(ctx: Ctx): UserContext { const router = Router(); -router.use( - "/", - createHandler({ - schema: getSchema(), - context: (req) => { - return req.raw; - }, - /* - graphiql: { - defaultQuery: DEFAULT_QUERY, - },*/ - // graphqlHTTP({ - // schema: getSchema(), - // graphiql: { - // defaultQuery: DEFAULT_QUERY, - // }, - // customFormatErrorFn: (error) => { - // console.error(error); - // return { - // message: error.message, - // locations: error.locations, - // stack: error.stack ? error.stack.split("\n") : [], - // path: error.path, - // }; - // }, - // extensions, - // }) as RequestHandler - }) -); +const yoga = createYoga({ + schema: getSchema(), + context: (ctx: YogaInitialContext) => { + return ctx.req; + }, +}); + +// Bind GraphQL Yoga to the graphql endpoint to avoid rendering the playground on any path +router.use("", yoga); export default router; diff --git a/packages/skin-database/api/graphql/resolvers/SkinResolver.ts b/packages/skin-database/api/graphql/resolvers/SkinResolver.ts index 46171ab2..d538209f 100644 --- a/packages/skin-database/api/graphql/resolvers/SkinResolver.ts +++ b/packages/skin-database/api/graphql/resolvers/SkinResolver.ts @@ -1,5 +1,4 @@ import { Int } from "grats"; -import { Ctx } from ".."; import SkinModel from "../../../data/SkinModel"; import UserContext from "../../../data/UserContext"; import ClassicSkinResolver from "./ClassicSkinResolver"; @@ -7,6 +6,7 @@ import { ISkin } from "./CommonSkinResolver"; import ModernSkinResolver from "./ModernSkinResolver"; import algoliasearch from "algoliasearch"; import * as Skins from "../../../data/skins"; +import { knex } from "../../../db"; // These keys are already in the web client, so they are not secret at all. const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51"); @@ -54,30 +54,62 @@ export async function fetch_skin_by_md5( * @gqlQueryField */ export async function search_skins( - { - query, - first = 10, - offset = 0, - }: { query: string; first?: Int; offset?: Int }, + query: string, + first: Int = 10, + offset: Int = 0, ctx: UserContext ): Promise> { 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, - }); + const skins = await knex("skin_search") + .select("skin_md5") + .leftJoin("skins", "skin_search.skin_md5", "skins.md5") + .where("skins.skin_type", "in", [1, 2]) + .limit(first) + .offset(offset) + .whereRaw("skin_search MATCH ?", query); return Promise.all( - results.hits.map(async (hit) => { - const model = await SkinModel.fromMd5Assert(ctx, hit.md5); + skins.map(async (hit) => { + const model = await SkinModel.fromMd5Assert(ctx, hit.skin_md5); return SkinResolver.fromModel(model); }) ); } + +/** + * Search the database using the Algolia search index used by the Museum. + * + * Useful for locating a particular skin. + * @gqlQueryField + */ +export async function search_classic_skins( + query: string, + first: Int = 10, + offset: Int = 0, + ctx: UserContext +): Promise> { + if (first > 1000) { + throw new Error("Can only query 1000 records via search."); + } + + const skins = await knex("skin_search") + .select("skin_md5") + .leftJoin("skins", "skin_search.skin_md5", "skins.md5") + .where("skins.skin_type", "=", 1) + .limit(first) + .offset(offset) + .whereRaw("skin_search MATCH ?", query); + + return Promise.all( + skins.map(async (hit) => { + const model = await SkinModel.fromMd5Assert(ctx, hit.skin_md5); + return new ClassicSkinResolver(model); + }) + ); +} /** * A random skin that needs to be reviewed * @gqlQueryField */ diff --git a/packages/skin-database/api/graphql/schema.graphql b/packages/skin-database/api/graphql/schema.graphql index 195d199d..737009b4 100644 --- a/packages/skin-database/api/graphql/schema.graphql +++ b/packages/skin-database/api/graphql/schema.graphql @@ -413,6 +413,12 @@ type Query { """ Search the database using the Algolia search index used by the Museum. + Useful for locating a particular skin. + """ + search_classic_skins(first: Int! = 10, offset: Int! = 0, query: String!): [ClassicSkin] @semanticNonNull + """ + 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] @semanticNonNull diff --git a/packages/skin-database/api/graphql/schema.ts b/packages/skin-database/api/graphql/schema.ts index 32c53e8e..4e49b632 100644 --- a/packages/skin-database/api/graphql/schema.ts +++ b/packages/skin-database/api/graphql/schema.ts @@ -6,7 +6,7 @@ import { defaultFieldResolver, GraphQLSchema, GraphQLObjectType, GraphQLString, import { getUserContext as getUserContext } from "./index"; import { fetch_archive_file_by_md5 as queryFetch_archive_file_by_md5Resolver } from "./../../data/ArchiveFileModel"; import { fetch_internet_archive_item_by_identifier as queryFetch_internet_archive_item_by_identifierResolver } from "./../../data/IaItemModel"; -import { fetch_skin_by_md5 as queryFetch_skin_by_md5Resolver, search_skins as querySearch_skinsResolver, skin_to_review as querySkin_to_reviewResolver } from "./resolvers/SkinResolver"; +import { fetch_skin_by_md5 as queryFetch_skin_by_md5Resolver, search_classic_skins as querySearch_classic_skinsResolver, search_skins as querySearch_skinsResolver, skin_to_review as querySkin_to_reviewResolver } from "./resolvers/SkinResolver"; import { fetch_tweet_by_url as queryFetch_tweet_by_urlResolver } from "./../../data/TweetModel"; import { me as queryMeResolver } from "./resolvers/UserResolver"; import { archive_files as modernSkinArchive_filesResolver, average_color as modernSkinAverage_colorResolver, download_url as modernSkinDownload_urlResolver, filename as modernSkinFilenameResolver, id as modernSkinIdResolver, internet_archive_item as modernSkinInternet_archive_itemResolver, md5 as modernSkinMd5Resolver, museum_url as modernSkinMuseum_urlResolver, nsfw as modernSkinNsfwResolver, readme_text as modernSkinReadme_textResolver, reviews as modernSkinReviewsResolver, screenshot_url as modernSkinScreenshot_urlResolver, tweeted as modernSkinTweetedResolver, tweets as modernSkinTweetsResolver, webamp_url as modernSkinWebamp_urlResolver, archive_files as classicSkinArchive_filesResolver, average_color as classicSkinAverage_colorResolver, download_url as classicSkinDownload_urlResolver, filename as classicSkinFilenameResolver, id as classicSkinIdResolver, internet_archive_item as classicSkinInternet_archive_itemResolver, md5 as classicSkinMd5Resolver, museum_url as classicSkinMuseum_urlResolver, nsfw as classicSkinNsfwResolver, readme_text as classicSkinReadme_textResolver, reviews as classicSkinReviewsResolver, screenshot_url as classicSkinScreenshot_urlResolver, tweeted as classicSkinTweetedResolver, tweets as classicSkinTweetsResolver, webamp_url as classicSkinWebamp_urlResolver } from "./resolvers/CommonSkinResolver"; @@ -511,6 +511,166 @@ export function getSchema(): GraphQLSchema { }; } }); + 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), + resolve(source) { + return assertNonNull(classicSkinArchive_filesResolver(source)); + } + }, + average_color: { + description: "String representation (rgb usually) of the skin's average color", + name: "average_color", + type: GraphQLString, + resolve(source) { + return classicSkinAverage_colorResolver(source); + } + }, + download_url: { + description: "URL to download the skin", + name: "download_url", + type: GraphQLString, + resolve(source) { + return assertNonNull(classicSkinDownload_urlResolver(source)); + } + }, + 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: new GraphQLNonNull(GraphQLBoolean), + defaultValue: false + } + }, + resolve(source, args) { + return assertNonNull(classicSkinFilenameResolver(source, args)); + } + }, + has_media_library: { + description: "Does the skin include sprite sheets for the media library?", + name: "has_media_library", + type: GraphQLBoolean, + resolve(source, args, context, info) { + return assertNonNull(defaultFieldResolver(source, args, context, info)); + } + }, + id: { + description: "GraphQL ID of the skin", + name: "id", + type: new GraphQLNonNull(GraphQLID), + resolve(source) { + return classicSkinIdResolver(source); + } + }, + internet_archive_item: { + description: "The skin's \"item\" at archive.org", + name: "internet_archive_item", + type: InternetArchiveItemType, + resolve(source) { + return classicSkinInternet_archive_itemResolver(source); + } + }, + 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, + resolve(source) { + return assertNonNull(classicSkinMd5Resolver(source)); + } + }, + museum_url: { + description: "URL of the skin on the Winamp Skin Museum", + name: "museum_url", + type: GraphQLString, + resolve(source) { + return classicSkinMuseum_urlResolver(source); + } + }, + nsfw: { + description: "Has the skin been flagged as \"not safe for work\"?\"", + name: "nsfw", + type: GraphQLBoolean, + resolve(source) { + return classicSkinNsfwResolver(source); + } + }, + readme_text: { + description: "Text of the readme file extracted from the skin", + name: "readme_text", + type: GraphQLString, + resolve(source) { + return classicSkinReadme_textResolver(source); + } + }, + reviews: { + description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreview page, or via the Discord bot.", + name: "reviews", + type: new GraphQLList(ReviewType), + resolve(source) { + return assertNonNull(classicSkinReviewsResolver(source)); + } + }, + screenshot_url: { + description: "URL of a screenshot of the skin", + name: "screenshot_url", + type: GraphQLString, + resolve(source) { + return classicSkinScreenshot_urlResolver(source); + } + }, + transparent_pixels: { + description: "The number of transparent pixels rendered by the skin.", + name: "transparent_pixels", + type: GraphQLInt, + resolve(source, args, context, info) { + return assertNonNull(defaultFieldResolver(source, args, context, info)); + } + }, + tweeted: { + description: "Has the skin been tweeted?", + name: "tweeted", + type: GraphQLBoolean, + resolve(source) { + return assertNonNull(classicSkinTweetedResolver(source)); + } + }, + tweets: { + description: "List of", + name: "tweets", + type: new GraphQLList(TweetType), + resolve(source) { + return assertNonNull(classicSkinTweetsResolver(source)); + } + }, + webamp_url: { + description: "URL of webamp.org with the skin loaded", + name: "webamp_url", + type: GraphQLString, + resolve(source) { + return classicSkinWebamp_urlResolver(source); + } + } + }; + }, + interfaces() { + return [NodeType, SkinType]; + } + }); const SkinsConnectionType: GraphQLObjectType = new GraphQLObjectType({ name: "SkinsConnection", description: "A collection of classic Winamp skins", @@ -841,6 +1001,30 @@ export function getSchema(): GraphQLSchema { return queryNodeResolver(args.id, getUserContext(context)); } }, + search_classic_skins: { + description: "Search the database using the Algolia search index used by the Museum.\n\nUseful for locating a particular skin.", + name: "search_classic_skins", + type: new GraphQLList(ClassicSkinType), + 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 assertNonNull(querySearch_classic_skinsResolver(args.query, args.first, args.offset, getUserContext(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", @@ -862,7 +1046,7 @@ export function getSchema(): GraphQLSchema { } }, resolve(_source, args, context) { - return assertNonNull(querySearch_skinsResolver(args, getUserContext(context))); + return assertNonNull(querySearch_skinsResolver(args.query, args.first, args.offset, getUserContext(context))); } }, skin_to_review: { @@ -1143,166 +1327,6 @@ export function getSchema(): GraphQLSchema { }; } }); - 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), - resolve(source) { - return assertNonNull(classicSkinArchive_filesResolver(source)); - } - }, - average_color: { - description: "String representation (rgb usually) of the skin's average color", - name: "average_color", - type: GraphQLString, - resolve(source) { - return classicSkinAverage_colorResolver(source); - } - }, - download_url: { - description: "URL to download the skin", - name: "download_url", - type: GraphQLString, - resolve(source) { - return assertNonNull(classicSkinDownload_urlResolver(source)); - } - }, - 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: new GraphQLNonNull(GraphQLBoolean), - defaultValue: false - } - }, - resolve(source, args) { - return assertNonNull(classicSkinFilenameResolver(source, args)); - } - }, - has_media_library: { - description: "Does the skin include sprite sheets for the media library?", - name: "has_media_library", - type: GraphQLBoolean, - resolve(source, args, context, info) { - return assertNonNull(defaultFieldResolver(source, args, context, info)); - } - }, - id: { - description: "GraphQL ID of the skin", - name: "id", - type: new GraphQLNonNull(GraphQLID), - resolve(source) { - return classicSkinIdResolver(source); - } - }, - internet_archive_item: { - description: "The skin's \"item\" at archive.org", - name: "internet_archive_item", - type: InternetArchiveItemType, - resolve(source) { - return classicSkinInternet_archive_itemResolver(source); - } - }, - 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, - resolve(source) { - return assertNonNull(classicSkinMd5Resolver(source)); - } - }, - museum_url: { - description: "URL of the skin on the Winamp Skin Museum", - name: "museum_url", - type: GraphQLString, - resolve(source) { - return classicSkinMuseum_urlResolver(source); - } - }, - nsfw: { - description: "Has the skin been flagged as \"not safe for work\"?\"", - name: "nsfw", - type: GraphQLBoolean, - resolve(source) { - return classicSkinNsfwResolver(source); - } - }, - readme_text: { - description: "Text of the readme file extracted from the skin", - name: "readme_text", - type: GraphQLString, - resolve(source) { - return classicSkinReadme_textResolver(source); - } - }, - reviews: { - description: "Times that the skin has been reviewed either on the Museum's Tinder-style\nreview page, or via the Discord bot.", - name: "reviews", - type: new GraphQLList(ReviewType), - resolve(source) { - return assertNonNull(classicSkinReviewsResolver(source)); - } - }, - screenshot_url: { - description: "URL of a screenshot of the skin", - name: "screenshot_url", - type: GraphQLString, - resolve(source) { - return classicSkinScreenshot_urlResolver(source); - } - }, - transparent_pixels: { - description: "The number of transparent pixels rendered by the skin.", - name: "transparent_pixels", - type: GraphQLInt, - resolve(source, args, context, info) { - return assertNonNull(defaultFieldResolver(source, args, context, info)); - } - }, - tweeted: { - description: "Has the skin been tweeted?", - name: "tweeted", - type: GraphQLBoolean, - resolve(source) { - return assertNonNull(classicSkinTweetedResolver(source)); - } - }, - tweets: { - description: "List of", - name: "tweets", - type: new GraphQLList(TweetType), - resolve(source) { - return assertNonNull(classicSkinTweetsResolver(source)); - } - }, - webamp_url: { - description: "URL of webamp.org with the skin loaded", - name: "webamp_url", - type: GraphQLString, - resolve(source) { - return classicSkinWebamp_urlResolver(source); - } - } - }; - }, - interfaces() { - return [NodeType, SkinType]; - } - }); return new GraphQLSchema({ query: QueryType, mutation: MutationType, diff --git a/packages/skin-database/migrations/20250123223740_search_index.ts b/packages/skin-database/migrations/20250123223740_search_index.ts new file mode 100644 index 00000000..6d4b8e26 --- /dev/null +++ b/packages/skin-database/migrations/20250123223740_search_index.ts @@ -0,0 +1,88 @@ +import * as Knex from "knex"; + +export async function up(knex: Knex): Promise { + await knex.raw(` + -- Step 1: Create the FTS table +CREATE VIRTUAL TABLE skin_search USING fts5( + skin_md5, + readme_text, + file_names +); + +-- Step 2: Populate the FTS table with existing data +INSERT INTO skin_search(skin_md5, readme_text, file_names) +SELECT + s.md5, + s.readme_text, + GROUP_CONCAT(f.file_path, ' ') +FROM + skins s +LEFT JOIN + files f ON s.md5 = f.skin_md5 +GROUP BY + s.md5; + +-- Step 3: Create triggers to keep the FTS index updated + +-- Trigger for inserting into the skins table +CREATE TRIGGER after_skin_insert AFTER INSERT ON skins +BEGIN + INSERT INTO skin_search(skin_md5, readme_text) + VALUES (NEW.md5, NEW.readme_text); +END; + +-- Trigger for updating skins readme_text +CREATE TRIGGER after_skin_update AFTER UPDATE OF readme_text ON skins +BEGIN + UPDATE skin_search + SET readme_text = NEW.readme_text + WHERE skin_md5 = NEW.md5; +END; + +-- Trigger for inserting into the files table +CREATE TRIGGER after_file_insert AFTER INSERT ON files +BEGIN + UPDATE skin_search + SET file_names = ( + SELECT GROUP_CONCAT(file_path, ' ') + FROM files + WHERE skin_md5 = NEW.skin_md5 + ) + WHERE skin_md5 = NEW.skin_md5; +END; + +-- Trigger for updating file_path in files +CREATE TRIGGER after_file_update AFTER UPDATE OF file_path ON files +BEGIN + UPDATE skin_search + SET file_names = ( + SELECT GROUP_CONCAT(file_path, ' ') + FROM files + WHERE skin_md5 = NEW.skin_md5 + ) + WHERE skin_md5 = NEW.skin_md5; +END; + +-- Trigger for deleting files +CREATE TRIGGER after_file_delete AFTER DELETE ON files +BEGIN + UPDATE skin_search + SET file_names = ( + SELECT GROUP_CONCAT(file_path, ' ') + FROM files + WHERE skin_md5 = OLD.skin_md5 + ) + WHERE skin_md5 = OLD.skin_md5; +END;`); +} + +export async function down(knex: Knex): Promise { + await knex.raw(` + DROP TRIGGER after_skin_insert; + DROP TRIGGER after_skin_update; + DROP TRIGGER after_file_insert; + DROP TRIGGER after_file_update; + DROP TRIGGER after_file_delete; + DROP TABLE skin_search; + `); +} diff --git a/packages/skin-database/package.json b/packages/skin-database/package.json index d03c58f0..95f471df 100644 --- a/packages/skin-database/package.json +++ b/packages/skin-database/package.json @@ -23,6 +23,7 @@ "fast-xml-parser": "^4.2.2", "graphql": "^16.8.1", "graphql-http": "^1.22.1", + "graphql-yoga": "^5.10.10", "imagemin": "^7.0.0", "imagemin-optipng": "^7.0.0", "knex": "^0.21.1", diff --git a/yarn.lock b/yarn.lock index ce5a669f..d61d6272 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1568,6 +1568,21 @@ resolved "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz" integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ== +"@envelop/core@^5.0.2": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@envelop/core/-/core-5.0.3.tgz#d359dd45853a4d3ab23e9d12187e544cde9ffa9b" + integrity sha512-SE3JxL7odst8igN6x77QWyPpXKXz/Hs5o5Y27r+9Br6WHIhkW90lYYVITWIJQ/qYgn5PkpbaVgeFY9rgqQaZ/A== + dependencies: + "@envelop/types" "5.0.0" + tslib "^2.5.0" + +"@envelop/types@5.0.0": + version "5.0.0" + resolved "https://registry.yarnpkg.com/@envelop/types/-/types-5.0.0.tgz#3ae59b50ec31d4bdcc7bd0b47e9c8cf2ac44b0ff" + integrity sha512-IPjmgSc4KpQRlO4qbEDnBEixvtb06WDmjKfi/7fkZaryh5HuOmTtixe1EupQI5XfXO8joc3d27uUZ0QdC++euA== + dependencies: + tslib "^2.5.0" + "@esbuild-plugins/node-modules-polyfill@^0.1.4": version "0.1.4" resolved "https://registry.npmjs.org/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.1.4.tgz" @@ -2068,6 +2083,18 @@ tslib "^2.4.0" value-or-promise "^1.0.12" +"@graphql-tools/executor@^1.3.7": + version "1.3.12" + resolved "https://registry.yarnpkg.com/@graphql-tools/executor/-/executor-1.3.12.tgz#d1019279cad02c099850fedd2be2b563a815de66" + integrity sha512-FzLXZQJOZHB75SecYFOIEEHw/qcxkRFViw0lVqHpaL07c+GqDxv6VOto0FZCIiV9RgGdyRj3O8lXDCp9Cw1MbA== + dependencies: + "@graphql-tools/utils" "^10.7.2" + "@graphql-typed-document-node/core" "^3.2.0" + "@repeaterjs/repeater" "^3.0.4" + "@whatwg-node/disposablestack" "^0.0.5" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-tools/graphql-file-loader@^7.3.7": version "7.5.17" resolved "https://registry.npmjs.org/@graphql-tools/graphql-file-loader/-/graphql-file-loader-7.5.17.tgz" @@ -2128,6 +2155,24 @@ "@graphql-tools/utils" "^9.2.1" tslib "^2.4.0" +"@graphql-tools/merge@^9.0.17": + version "9.0.17" + resolved "https://registry.yarnpkg.com/@graphql-tools/merge/-/merge-9.0.17.tgz#57c98b74d6553fb0c053d4a121405d6088e119b6" + integrity sha512-3K4g8KKbIqfdmK0L5+VtZsqwAeElPkvT5ejiH+KEhn2wyKNCi4HYHxpQk8xbu+dSwLlm9Lhet1hylpo/mWCkuQ== + dependencies: + "@graphql-tools/utils" "^10.7.2" + tslib "^2.4.0" + +"@graphql-tools/schema@^10.0.11": + version "10.0.16" + resolved "https://registry.yarnpkg.com/@graphql-tools/schema/-/schema-10.0.16.tgz#82b82f9193e708fc0d34f9f3ba80ccc7a057aefa" + integrity sha512-G2zgb8hNg9Sx6Z2FSXm57ToNcwMls9A9cUm+EsCrnGGDsryzN5cONYePUpSGj5NCFivVp3o1FT5dg19P/1qeqQ== + dependencies: + "@graphql-tools/merge" "^9.0.17" + "@graphql-tools/utils" "^10.7.2" + tslib "^2.4.0" + value-or-promise "^1.0.12" + "@graphql-tools/schema@^9.0.18", "@graphql-tools/schema@^9.0.19": version "9.0.19" resolved "https://registry.npmjs.org/@graphql-tools/schema/-/schema-9.0.19.tgz" @@ -2157,6 +2202,16 @@ value-or-promise "^1.0.11" ws "^8.12.0" +"@graphql-tools/utils@^10.6.2", "@graphql-tools/utils@^10.7.2": + version "10.7.2" + resolved "https://registry.yarnpkg.com/@graphql-tools/utils/-/utils-10.7.2.tgz#feafb7be9211570037288f5a3cadab76de41a097" + integrity sha512-Wn85S+hfkzfVFpXVrQ0hjnePa3p28aB6IdAGCiD1SqBCSMDRzL+OFEtyAyb30nV9Mqflqs9lCqjqlR2puG857Q== + dependencies: + "@graphql-typed-document-node/core" "^3.1.1" + cross-inspect "1.0.1" + dset "^3.1.4" + tslib "^2.4.0" + "@graphql-tools/utils@^9.0.0", "@graphql-tools/utils@^9.2.1": version "9.2.1" resolved "https://registry.npmjs.org/@graphql-tools/utils/-/utils-9.2.1.tgz" @@ -2176,11 +2231,36 @@ tslib "^2.4.0" value-or-promise "^1.0.12" -"@graphql-typed-document-node/core@3.2.0", "@graphql-typed-document-node/core@^3.1.1": +"@graphql-typed-document-node/core@3.2.0", "@graphql-typed-document-node/core@^3.1.1", "@graphql-typed-document-node/core@^3.2.0": version "3.2.0" resolved "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz" integrity sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ== +"@graphql-yoga/logger@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@graphql-yoga/logger/-/logger-2.0.1.tgz#b3d18371c07bb2fe03417e3920ddcaebb2ee0262" + integrity sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA== + dependencies: + tslib "^2.8.1" + +"@graphql-yoga/subscription@^5.0.3": + version "5.0.3" + resolved "https://registry.yarnpkg.com/@graphql-yoga/subscription/-/subscription-5.0.3.tgz#d36bc16d4f9c4ba3b9024133ba959ae2185b2d41" + integrity sha512-xLGEataxCULjL9rlTCVFL1IW2E90TnDIL+mE4lZBi9X92jc6s+Q+jk6Ax3I98kryCJh0UMiwjit7CaIIczTiJg== + dependencies: + "@graphql-yoga/typed-event-target" "^3.0.2" + "@repeaterjs/repeater" "^3.0.4" + "@whatwg-node/events" "^0.1.0" + tslib "^2.8.1" + +"@graphql-yoga/typed-event-target@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@graphql-yoga/typed-event-target/-/typed-event-target-3.0.2.tgz#af29ed2a5a84062ffab8e404b335ec4d4c37ceb4" + integrity sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA== + dependencies: + "@repeaterjs/repeater" "^3.0.4" + tslib "^2.8.1" + "@hapi/hoek@^9.0.0", "@hapi/hoek@^9.3.0": version "9.3.0" resolved "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz" @@ -7253,11 +7333,33 @@ resolved "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz" integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ== +"@whatwg-node/disposablestack@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@whatwg-node/disposablestack/-/disposablestack-0.0.5.tgz#cd646b1ef60a36972e018ab21f412a3539c6deec" + integrity sha512-9lXugdknoIequO4OYvIjhygvfSEgnO8oASLqLelnDhkRjgBZhc39shC3QSlZuyDO9bgYSIVa2cHAiN+St3ty4w== + dependencies: + tslib "^2.6.3" + "@whatwg-node/events@^0.0.3": version "0.0.3" resolved "https://registry.npmjs.org/@whatwg-node/events/-/events-0.0.3.tgz" integrity sha512-IqnKIDWfXBJkvy/k6tzskWTc2NK3LcqHlb+KHGCrjOCH4jfQckRX0NAiIcC/vIqQkzLYw2r2CTSwAxcrtcD6lA== +"@whatwg-node/events@^0.1.0": + version "0.1.2" + resolved "https://registry.yarnpkg.com/@whatwg-node/events/-/events-0.1.2.tgz#23f7c7ad887d7fd448e9ce3261eac9ef319ddd7c" + integrity sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ== + dependencies: + tslib "^2.6.3" + +"@whatwg-node/fetch@^0.10.0", "@whatwg-node/fetch@^0.10.1": + version "0.10.3" + resolved "https://registry.yarnpkg.com/@whatwg-node/fetch/-/fetch-0.10.3.tgz#b19883b8a1568c5ae09abd550589ec597e685f4b" + integrity sha512-jCTL/qYcIW2GihbBRHypQ/Us7saWMNZ5fsumsta+qPY0Pmi1ccba/KRQvgctmQsbP69FWemJSs8zVcFaNwdL0w== + dependencies: + "@whatwg-node/node-fetch" "^0.7.7" + urlpattern-polyfill "^10.0.0" + "@whatwg-node/fetch@^0.8.0", "@whatwg-node/fetch@^0.8.1": version "0.8.8" resolved "https://registry.npmjs.org/@whatwg-node/fetch/-/fetch-0.8.8.tgz" @@ -7280,6 +7382,24 @@ fast-url-parser "^1.1.3" tslib "^2.3.1" +"@whatwg-node/node-fetch@^0.7.7": + version "0.7.7" + resolved "https://registry.yarnpkg.com/@whatwg-node/node-fetch/-/node-fetch-0.7.7.tgz#6c1752e2e16cfac93fdef2ef4f852b7a50dd15ed" + integrity sha512-BDbIMOenThOTFDBLh1WscgBNAxfDAdAdd9sMG8Ff83hYxApJVbqEct38bUAj+zn8bTsfBx/lyfnVOTyq5xUlvg== + dependencies: + "@whatwg-node/disposablestack" "^0.0.5" + busboy "^1.6.0" + tslib "^2.6.3" + +"@whatwg-node/server@^0.9.64": + version "0.9.65" + resolved "https://registry.yarnpkg.com/@whatwg-node/server/-/server-0.9.65.tgz#abba6a790bfe7cf9a7ea1301c1e12743504f8c83" + integrity sha512-CnYTFEUJkbbAcuBXnXirVIgKBfs2YA6sSGjxeq07AUiyXuoQ0fbvTIQoteMglmn09QeGzcH/l0B7nIml83xvVw== + dependencies: + "@whatwg-node/disposablestack" "^0.0.5" + "@whatwg-node/fetch" "^0.10.0" + tslib "^2.6.3" + "@xtuc/ieee754@^1.2.0": version "1.2.0" resolved "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz" @@ -10617,6 +10737,13 @@ cross-fetch@3.1.5: dependencies: node-fetch "2.6.7" +cross-inspect@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/cross-inspect/-/cross-inspect-1.0.1.tgz#15f6f65e4ca963cf4cc1a2b5fef18f6ca328712b" + integrity sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A== + dependencies: + tslib "^2.4.0" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz" @@ -11835,6 +11962,11 @@ dset@^3.1.2: resolved "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz" integrity sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ== +dset@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/dset/-/dset-3.1.4.tgz#f8eaf5f023f068a036d08cd07dc9ffb7d0065248" + integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA== + duplexer2@0.0.2: version "0.0.2" resolved "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz" @@ -15134,16 +15266,28 @@ graphql-ws@5.12.1: resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.12.1.tgz" integrity sha512-umt4f5NnMK46ChM2coO36PTFhHouBrK9stWWBczERguwYrGnPNxJ9dimU6IyOBfOkC6Izhkg4H8+F51W/8CYDg== -graphql@^16.8.1: +graphql-yoga@^5.10.10: + version "5.10.10" + resolved "https://registry.yarnpkg.com/graphql-yoga/-/graphql-yoga-5.10.10.tgz#5384d7e391d4310a6eb8d03fb9dff18795badd90" + integrity sha512-0KF0mxKeedMBYOSVLbJh7GJJwrObhBktr77SuDdZPmVA+OtdC9Xef+gYHsk7EQDeBPodgsA99pmd/tL9j0d4zg== + dependencies: + "@envelop/core" "^5.0.2" + "@graphql-tools/executor" "^1.3.7" + "@graphql-tools/schema" "^10.0.11" + "@graphql-tools/utils" "^10.6.2" + "@graphql-yoga/logger" "^2.0.1" + "@graphql-yoga/subscription" "^5.0.3" + "@whatwg-node/fetch" "^0.10.1" + "@whatwg-node/server" "^0.9.64" + dset "^3.1.4" + lru-cache "^10.0.0" + tslib "^2.8.1" + +graphql@16.8.1, graphql@^16.8.1, graphql@^16.9.0: version "16.8.1" resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.8.1.tgz#1930a965bef1170603702acdb68aedd3f3cf6f07" integrity sha512-59LZHPdGZVh695Ud9lRzPBVTtlX9ZCV150Er2W43ro37wVof0ctenSaskPPjN7lVTIN8mSZt8PHUNKZuNQUuxw== -graphql@^16.9.0: - version "16.10.0" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.10.0.tgz#24c01ae0af6b11ea87bf55694429198aaa8e220c" - integrity sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ== - grats@^0.0.31: version "0.0.31" resolved "https://registry.yarnpkg.com/grats/-/grats-0.0.31.tgz#f094786584a0f2fb345b38e6c37330d3f4f391cd" @@ -19413,6 +19557,11 @@ lpad-align@^1.0.1: longest "^1.0.0" meow "^3.3.0" +lru-cache@^10.0.0: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + lru-cache@^10.2.0: version "10.2.0" resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.0.tgz" @@ -27653,6 +27802,11 @@ tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2.0, tslib@^2.3 resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== +tslib@^2.6.3, tslib@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tsscmp@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz" @@ -28278,7 +28432,7 @@ url@^0.11.0: punycode "^1.4.1" qs "^6.11.2" -urlpattern-polyfill@10.0.0: +urlpattern-polyfill@10.0.0, urlpattern-polyfill@^10.0.0: version "10.0.0" resolved "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz" integrity sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==