Add support for SQLite based search

This commit is contained in:
Jordan Eldredge 2025-01-25 13:24:11 -08:00
parent 68b8b22a49
commit faee5410ac
7 changed files with 498 additions and 213 deletions

View file

@ -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<Ctx>({
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;

View file

@ -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<Array<ISkin | null>> {
if (first > 1000) {
throw new Error("Can only query 1000 records via search.");
}
const results: { hits: { md5: string }[] } = await index.search(query, {
attributesToRetrieve: ["md5"],
length: first,
offset,
});
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<Array<ClassicSkinResolver | null>> {
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 */

View file

@ -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

View file

@ -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,

View file

@ -0,0 +1,88 @@
import * as Knex from "knex";
export async function up(knex: Knex): Promise<void> {
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<void> {
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;
`);
}

View file

@ -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",

170
yarn.lock
View file

@ -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==