From 22ac7f6b150a034a174de6864ae8cf61d2c98306 Mon Sep 17 00:00:00 2001 From: DecDuck Date: Fri, 4 Oct 2024 13:01:06 +1000 Subject: [PATCH] metadata engine --- README.md | 2 +- package.json | 3 + .../migration.sql | 16 ++ .../migration.sql | 12 + prisma/schema.prisma | 8 + server/api/v1/game/search.get.ts | 12 + server/h3.d.ts | 4 +- server/internal/metadata/giantbomb.ts | 207 ++++++++++++++++++ server/internal/metadata/index.ts | 122 ++++++++++- server/internal/metadata/types.d.ts | 6 +- server/internal/objects/index.ts | 0 server/internal/objects/transactional.ts | 38 ++++ server/internal/utils/prioritylist.ts | 89 ++++++++ server/internal/utils/typefilter.ts | 1 + server/plugins/metadata.ts | 22 ++ yarn.lock | 74 +++++++ 16 files changed, 604 insertions(+), 12 deletions(-) create mode 100644 prisma/migrations/20241004020835_unique_constraints/migration.sql create mode 100644 prisma/migrations/20241004025235_add_dev_pub_websites/migration.sql create mode 100644 server/api/v1/game/search.get.ts create mode 100644 server/internal/metadata/giantbomb.ts create mode 100644 server/internal/objects/index.ts create mode 100644 server/internal/objects/transactional.ts create mode 100644 server/internal/utils/prioritylist.ts create mode 100644 server/internal/utils/typefilter.ts create mode 100644 server/plugins/metadata.ts diff --git a/README.md b/README.md index a9dfe19..3383b2c 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,6 @@ ## To-do list - User authentication (done) - Sessions (done) - - Game database/API + - Game database/API (done, with GiantBomb provider, more to come!) - Metadata matching and import - Frontend beginnings \ No newline at end of file diff --git a/package.json b/package.json index 4a6b4fb..673d247 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,12 @@ }, "dependencies": { "@prisma/client": "5.20.0", + "axios": "^1.7.7", "bcrypt": "^5.1.1", "moment": "^2.30.1", "nuxt": "^3.13.0", "prisma": "^5.20.0", + "turndown": "^7.2.0", "uuid": "^10.0.0", "vue": "latest", "vue-router": "latest" @@ -22,6 +24,7 @@ "packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e", "devDependencies": { "@types/bcrypt": "^5.0.2", + "@types/turndown": "^5.0.5", "@types/uuid": "^10.0.0", "h3": "^1.12.0" } diff --git a/prisma/migrations/20241004020835_unique_constraints/migration.sql b/prisma/migrations/20241004020835_unique_constraints/migration.sql new file mode 100644 index 0000000..a475fd5 --- /dev/null +++ b/prisma/migrations/20241004020835_unique_constraints/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - A unique constraint covering the columns `[metadataSource,metadataId]` on the table `Developer` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[metadataSource,metadataId]` on the table `Game` will be added. If there are existing duplicate values, this will fail. + - A unique constraint covering the columns `[metadataSource,metadataId]` on the table `Publisher` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Developer_metadataSource_metadataId_key" ON "Developer"("metadataSource", "metadataId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Game_metadataSource_metadataId_key" ON "Game"("metadataSource", "metadataId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Publisher_metadataSource_metadataId_key" ON "Publisher"("metadataSource", "metadataId"); diff --git a/prisma/migrations/20241004025235_add_dev_pub_websites/migration.sql b/prisma/migrations/20241004025235_add_dev_pub_websites/migration.sql new file mode 100644 index 0000000..0f3e9bf --- /dev/null +++ b/prisma/migrations/20241004025235_add_dev_pub_websites/migration.sql @@ -0,0 +1,12 @@ +/* + Warnings: + + - Added the required column `mWebsite` to the `Developer` table without a default value. This is not possible if the table is not empty. + - Added the required column `mWebsite` to the `Publisher` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "Developer" ADD COLUMN "mWebsite" TEXT NOT NULL; + +-- AlterTable +ALTER TABLE "Publisher" ADD COLUMN "mWebsite" TEXT NOT NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f9c820..31f6a07 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -61,6 +61,8 @@ model Game { mBannerId String // linked to objects in s3 mArt String[] // linked to objects in s3 mScreenshots String[] // linked to objects in s3 + + @@unique([metadataSource, metadataId], name: "metadataKey") } model Developer { @@ -74,8 +76,11 @@ model Developer { mDescription String mLogo String mBanner String + mWebsite String games Game[] + + @@unique([metadataSource, metadataId], name: "metadataKey") } model Publisher { @@ -89,6 +94,9 @@ model Publisher { mDescription String mLogo String mBanner String + mWebsite String games Game[] + + @@unique([metadataSource, metadataId], name: "metadataKey") } diff --git a/server/api/v1/game/search.get.ts b/server/api/v1/game/search.get.ts new file mode 100644 index 0000000..d4c7b81 --- /dev/null +++ b/server/api/v1/game/search.get.ts @@ -0,0 +1,12 @@ +export default defineEventHandler(async (h3) => { + const query = getQuery(h3); + const search = query["q"]?.toString(); + if (!search) throw createError({ + statusCode: 400, + statusMessage: "Missing search param" + }); + + const results = await h3.context.metadataHandler.search(search); + + return results; +}); \ No newline at end of file diff --git a/server/h3.d.ts b/server/h3.d.ts index 46fba81..9a6c284 100644 --- a/server/h3.d.ts +++ b/server/h3.d.ts @@ -1,8 +1,10 @@ +import { MetadataHandler } from "./internal/metadata"; import { SessionHandler } from "./internal/session"; export * from "h3"; declare module "h3" { interface H3EventContext { - session: SessionHandler + session: SessionHandler; + metadataHandler: MetadataHandler; } } diff --git a/server/internal/metadata/giantbomb.ts b/server/internal/metadata/giantbomb.ts new file mode 100644 index 0000000..289ae9f --- /dev/null +++ b/server/internal/metadata/giantbomb.ts @@ -0,0 +1,207 @@ +import { Developer, MetadataSource, Publisher } from "@prisma/client"; +import { MetadataProvider } from "."; +import { GameMetadataSearchResult, _FetchGameMetadataParams, GameMetadata, _FetchPublisherMetadataParams, PublisherMetadata, _FetchDeveloperMetadataParams, DeveloperMetadata } from "./types"; +import axios, { AxiosRequestConfig } from "axios"; +import moment from "moment"; +import TurndownService from "turndown"; + +interface GiantBombResponseType { + error: "OK" | string; + limit: number, + offset: number, + number_of_page_results: number, + number_of_total_results: number, + status_code: number, + results: T, + version: string +} + +interface GameSearchResult { + guid: string, + name: string, + deck: string, + original_release_date?: string + expected_release_year?: number + image?: { + icon_url: string + } +} + +interface GameResult { + guid: string, + name: string, + deck: string, + description?: string, + + developers: Array<{ id: number, name: string }>, + publishers: Array<{ id: number, name: string }> + + number_of_user_reviews: number, // Doesn't provide an actual rating, so kinda useless + + image: { + icon_url: string, + screen_large_url: string, + }, + images: Array<{ + tags: string; // If it's "All Images", art, otherwise screenshot + original: string + }> +} + +interface CompanySearchResult { + guid: string, + deck: string, + description: string, + name: string, + + image: { + icon_url: string, + screen_large_url: string, + } +} + +export class GiantBombProvider implements MetadataProvider { + private apikey: string; + private turndown: TurndownService; + + constructor() { + const apikey = process.env.GIANT_BOMB_API_KEY; + if (!apikey) throw new Error("No GIANT_BOMB_API_KEY in environment"); + + this.apikey = apikey; + this.turndown = new TurndownService(); + } + + private async request(resource: string, url: string, query: { [key: string]: string | Array }, options?: AxiosRequestConfig) { + + const queryOptions = { ...query, api_key: this.apikey, format: 'json' }; + const queryString = Object.entries(queryOptions).map(([key, value]) => { + if (Array.isArray(value)) { + return `${key}=${value.map(encodeURIComponent).join(',')}` + } + return `${key}=${encodeURIComponent(value)}`; + }).join("&"); + + const finalURL = `https://www.giantbomb.com/api/${resource}/${url}?${queryString}`; + + const overlay: AxiosRequestConfig = { + url: finalURL, + baseURL: "", + } + const response = await axios.request>(Object.assign({}, options, overlay)); + return response; + } + + id() { + return "giantbomb"; + } + name() { + return "GiantBomb" + } + source() { + return MetadataSource.GiantBomb; + } + + + async search(query: string): Promise { + const results = await this.request>("search", "", { query: query, resources: ["game"] }); + const mapped = results.data.results.map((result) => { + const date = (result.original_release_date ? moment(result.original_release_date).year() : result.expected_release_year) ?? 0; + + const metadata: GameMetadataSearchResult = { + id: result.guid, + name: result.name, + icon: result.image?.icon_url ?? "", + description: result.deck, + year: date + } + + return metadata; + }) + + return mapped; + } + async fetchGame({ id, publisher, developer, createObject }: _FetchGameMetadataParams): Promise { + const result = await this.request("game", id, {}); + const gameData = result.data.results; + + + const longDescription = gameData.description ? + this.turndown.turndown(gameData.description) : + gameData.deck; + + const publishers: Publisher[] = []; + for (const pub of gameData.publishers) { + publishers.push(await publisher(pub.name)); + } + + const developers: Developer[] = []; + for (const dev of gameData.developers) { + developers.push(await developer(dev.name)); + } + + const icon = createObject(gameData.image.icon_url); + const banner = createObject(gameData.image.screen_large_url); + + const artUrls: string[] = []; + const screenshotUrls: string[] = []; + // If it's "All Images", art, otherwise screenshot + for (const image of gameData.images) { + if (image.tags == 'All Images') { + artUrls.push(image.original) + } else { + screenshotUrls.push(image.original) + } + } + + const art = artUrls.map(createObject); + const screenshots = screenshotUrls.map(createObject); + + const metadata: GameMetadata = { + id: gameData.guid, + name: gameData.name, + shortDescription: gameData.deck, + description: longDescription, + + reviewCount: 0, + reviewRating: 0, + + publishers, + developers, + + icon, + banner, + art, + screenshots + } + + return metadata; + } + async fetchPublisher({ query, createObject }: _FetchPublisherMetadataParams): Promise { + const results = await this.request>("search", "", { query, resources: "company" }); + + // Find the right entry + const company = results.data.results.find((e) => e.name == query) ?? results.data.results.at(0); + if (!company) throw new Error(`No results for "${query}"`); + + const longDescription = company.description ? + this.turndown.turndown(company.description) : + company.deck; + + const metadata: PublisherMetadata = { + id: company.guid, + name: company.name, + shortDescription: company.deck, + description: longDescription, + + logo: createObject(company.image.icon_url), + banner: createObject(company.image.screen_large_url), + } + + return metadata; + } + async fetchDeveloper(params: _FetchDeveloperMetadataParams): Promise { + return await this.fetchPublisher(params) + } + +} \ No newline at end of file diff --git a/server/internal/metadata/index.ts b/server/internal/metadata/index.ts index 7721879..7f6f0e3 100644 --- a/server/internal/metadata/index.ts +++ b/server/internal/metadata/index.ts @@ -1,9 +1,13 @@ +import { Developer, MetadataSource, PrismaClient, Publisher } from "@prisma/client"; +import prisma from "../db/database"; import { _FetchDeveloperMetadataParams, _FetchGameMetadataParams, _FetchPublisherMetadataParams, DeveloperMetadata, GameMetadata, GameMetadataSearchResult, InternalGameMetadataResult, PublisherMetadata } from "./types"; - +import { ObjectTransactionalHandler } from "../objects/transactional"; +import { PriorityList, PriorityListIndexed } from "../utils/prioritylist"; export abstract class MetadataProvider { abstract id(): string; abstract name(): string; + abstract source(): MetadataSource; abstract search(query: string): Promise; abstract fetchGame(params: _FetchGameMetadataParams): Promise; @@ -11,13 +15,13 @@ export abstract class MetadataProvider { abstract fetchDeveloper(params: _FetchDeveloperMetadataParams): Promise; } -class MetadataHandler { +export class MetadataHandler { // Ordered by priority - private providers: Map = new Map(); - private createObject: (url: string) => Promise; + private providers: PriorityListIndexed = new PriorityListIndexed("id"); + private objectHandler: ObjectTransactionalHandler = new ObjectTransactionalHandler(); - constructor() { - this.createObject = async () => ""; + addProvider(provider: MetadataProvider, priority: number = 0) { + this.providers.push(provider, priority); } async search(query: string) { @@ -44,12 +48,114 @@ class MetadataHandler { return successfulResults; } - async fetchGame(game: InternalGameMetadataResult) { + async fetchGame(result: InternalGameMetadataResult) { + const provider = this.providers.get(result.sourceId); + if (!provider) throw new Error(`Invalid metadata provider for ID "${result.sourceId}"`); + const existing = await prisma.game.findUnique({ + where: { + metadataKey: { + metadataSource: provider.source(), + metadataId: provider.id(), + } + } + }); + if (existing) return existing; + + const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(); + + let metadata; + try { + metadata = await provider.fetchGame({ + id: result.id, + publisher: this.fetchPublisher, + developer: this.fetchDeveloper, + createObject, + }) + } catch (e) { + dumpObjects(); + throw e; + } + + await pullObjects(); + const game = await prisma.game.create({ + data: { + metadataSource: provider.source(), + metadataId: metadata.id, + + mName: metadata.name, + mShortDescription: metadata.shortDescription, + mDescription: metadata.description, + mDevelopers: { + connect: metadata.developers + }, + mPublishers: { + connect: metadata.publishers, + }, + + mReviewCount: metadata.reviewCount, + mReviewRating: metadata.reviewRating, + + mIconId: metadata.icon, + mBannerId: metadata.banner, + mArt: metadata.art, + mScreenshots: metadata.screenshots, + }, + }); + + return game; } async fetchDeveloper(query: string) { - + return await this.fetchDeveloperPublisher(query, "fetchDeveloper", "developer") as Developer; + } + + async fetchPublisher(query: string) { + return await this.fetchDeveloperPublisher(query, "fetchPublisher", "publisher") as Publisher; + } + + // Careful with this function, it has no typechecking + // TODO: fix typechecking + private async fetchDeveloperPublisher(query: string, functionName: any, databaseName: any) { + const existing = await (prisma as any)[databaseName].findFirst({ + where: { + mName: query, + } + }); + if (existing) return existing; + + for (const provider of this.providers.values() as any) { + const [createObject, pullObjects, dumpObjects] = this.objectHandler.new(); + let result; + try { + result = await provider[functionName]({ query, createObject }); + } catch { + dumpObjects(); + continue; + } + + // If we're successful + await pullObjects(); + + const object = await (prisma as any)[databaseName].create({ + data: { + metadataSource: provider.source(), + metadataId: provider.id(), + + mName: result.name, + mShortDescription: result.shortDescription, + mDescription: result.description, + mLogo: result.logo, + mBanner: result.banner, + }, + }) + + return object; + + } + + throw new Error(`No metadata provider found a ${databaseName} for "${query}"`); + } } diff --git a/server/internal/metadata/types.d.ts b/server/internal/metadata/types.d.ts index 30ef3c8..9792ac0 100644 --- a/server/internal/metadata/types.d.ts +++ b/server/internal/metadata/types.d.ts @@ -17,6 +17,7 @@ export type InternalGameMetadataResult = GameMetadataSearchResult & GameMetadata export type RemoteObject = string; export interface GameMetadata { + id: string; name: string; shortDescription: string; description: string; @@ -37,6 +38,7 @@ export interface GameMetadata { } export interface PublisherMetadata { + id: string; name: string; shortDescription: string; description: string; @@ -53,12 +55,12 @@ export interface _FetchGameMetadataParams { publisher: (query: string) => Promise developer: (query: string) => Promise - createObject: (url: string) => Promise + createObject: (url: string) => RemoteObject } export interface _FetchPublisherMetadataParams { query: string; - createObject: (url: string) => Promise; + createObject: (url: string) => RemoteObject; } export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams; \ No newline at end of file diff --git a/server/internal/objects/index.ts b/server/internal/objects/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/internal/objects/transactional.ts b/server/internal/objects/transactional.ts new file mode 100644 index 0000000..767b81d --- /dev/null +++ b/server/internal/objects/transactional.ts @@ -0,0 +1,38 @@ +/* +The purpose of this class is to hold references to remote objects (like images) until they're actually needed +This is used as a utility in metadata handling, so we only fetch the objects if we're actually creating a database record. +*/ +import { v4 as uuidv4 } from 'uuid'; + +type TransactionTable = { [key: string]: string }; // ID to URL +type GlobalTransactionRecord = { [key: string]: TransactionTable }; // Transaction ID to table + +type Register = (url: string) => string; +type Pull = () => Promise; +type Dump = () => void; + +export class ObjectTransactionalHandler { + private record: GlobalTransactionRecord = {}; + + new(): [Register, Pull, Dump] { + const transactionId = uuidv4(); + + const register = (url: string) => { + const objectId = uuidv4(); + this.record[transactionId][objectId] = url; + + return objectId; + } + + const pull = async () => { + // Dummy function + dump(); + } + + const dump = () => { + delete this.record[transactionId]; + } + + return [register, pull, dump]; + } +} \ No newline at end of file diff --git a/server/internal/utils/prioritylist.ts b/server/internal/utils/prioritylist.ts new file mode 100644 index 0000000..8e3991b --- /dev/null +++ b/server/internal/utils/prioritylist.ts @@ -0,0 +1,89 @@ +import { FilterConditionally } from "./typefilter"; + +interface PriorityTagged { + object: T, + priority: number, // Higher takes priority + addedIndex: number, // Lower takes priority +} + +export class PriorityList { + private source: Array> = []; + private cachedSorted: Array | undefined; + + push(item: T, priority: number = 0) { + this.source.push({ + object: item, + priority, + addedIndex: this.source.length, + }); + this.cachedSorted = undefined; + } + + pop(index: number = 0) { + this.cachedSorted = undefined; + return this.source.splice(index, 1)[0]; + } + + values() { + if (this.cachedSorted !== undefined) { + return this.cachedSorted; + } + + const sorted = this.source.sort((a, b) => { + if (a.priority == a.priority) { + return a.addedIndex - b.addedIndex; + } + + return b.priority - a.priority; + }).map((e) => e.object); + this.cachedSorted = sorted; + + return this.cachedSorted; + } + + find(predicate: (value: T, index: number, obj: T[]) => boolean) { + return this.source.map((e) => e.object).find(predicate); + } +} + + +type IndexableProperty = keyof FilterConditionally string) | string>; +export class PriorityListIndexed extends PriorityList { + private indexName: IndexableProperty; + private indexMap: { [key: string]: T } = {}; + + constructor(indexName: IndexableProperty) { + super(); + this.indexName = indexName; + } + + private getIndex(object: T): string { + const index = object[this.indexName]; + + if (typeof index === 'function') { + return index(); + } + + return index as string; + } + + push(item: T, priority?: number): void { + const index = this.getIndex(item); + this.indexMap[index] = item; + + super.push(item, priority); + } + + pop(position?: number): PriorityTagged { + const value = super.pop(position); + + const index = this.getIndex(value.object); + delete this.indexMap[index]; + + return value; + } + + get(index: string) { + return this.indexMap[index]; + } +} \ No newline at end of file diff --git a/server/internal/utils/typefilter.ts b/server/internal/utils/typefilter.ts new file mode 100644 index 0000000..b720293 --- /dev/null +++ b/server/internal/utils/typefilter.ts @@ -0,0 +1 @@ +export type FilterConditionally = Pick; \ No newline at end of file diff --git a/server/plugins/metadata.ts b/server/plugins/metadata.ts new file mode 100644 index 0000000..4865a78 --- /dev/null +++ b/server/plugins/metadata.ts @@ -0,0 +1,22 @@ +import { MetadataHandler, MetadataProvider } from "../internal/metadata"; +import { GiantBombProvider } from "../internal/metadata/giantbomb"; + +export const GlobalMedataHandler = new MetadataHandler(); + +const providerCreators: Array<() => MetadataProvider> = [() => new GiantBombProvider()]; + +export default defineNitroPlugin(async (nitro) => { + for (const creator of providerCreators) { + try { + const instance = creator(); + GlobalMedataHandler.addProvider(instance); + } + catch (e) { + console.warn(e); + } + } + + nitro.hooks.hook('request', (h3) => { + h3.context.metadataHandler = GlobalMedataHandler; + }) +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 3514dd0..b92ab4a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -730,6 +730,11 @@ semver "^7.3.5" tar "^6.1.11" +"@mixmark-io/domino@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@mixmark-io/domino/-/domino-2.2.0.tgz#4e8ec69bf1afeb7a14f0628b7e2c0f35bdb336c3" + integrity sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw== + "@netlify/functions@^2.8.0": version "2.8.1" resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.8.1.tgz#67cd94f929551e156225fb50d2efba603b97e138" @@ -1293,6 +1298,11 @@ resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== +"@types/turndown@^5.0.5": + version "5.0.5" + resolved "https://registry.yarnpkg.com/@types/turndown/-/turndown-5.0.5.tgz#614de24fc9ace4d8c0d9483ba81dc8c1976dd26f" + integrity sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w== + "@types/uuid@^10.0.0": version "10.0.0" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" @@ -1694,6 +1704,11 @@ async@^3.2.4: resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + autoprefixer@^10.4.20: version "10.4.20" resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b" @@ -1706,6 +1721,15 @@ autoprefixer@^10.4.20: picocolors "^1.0.1" postcss-value-parser "^4.2.0" +axios@^1.7.7: + version "1.7.7" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f" + integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q== + dependencies: + follow-redirects "^1.15.6" + form-data "^4.0.0" + proxy-from-env "^1.1.0" + b4a@^1.6.4: version "1.6.7" resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4" @@ -1967,6 +1991,13 @@ colord@^2.9.3: resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + commander@^2.20.0: version "2.20.3" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" @@ -2242,6 +2273,11 @@ defu@^6.1.4: resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479" integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg== +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" @@ -2617,6 +2653,11 @@ flatted@^3.3.1: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +follow-redirects@^1.15.6: + version "1.15.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" + integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== + foreground-child@^3.1.0: version "3.3.0" resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77" @@ -2625,6 +2666,15 @@ foreground-child@^3.1.0: cross-spawn "^7.0.0" signal-exit "^4.0.1" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + fraction.js@^4.3.7: version "4.3.7" resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" @@ -3332,6 +3382,18 @@ micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: braces "^3.0.3" picomatch "^2.3.1" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + mime@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" @@ -4140,6 +4202,11 @@ protocols@^2.0.0, protocols@^2.0.1: resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + queue-microtask@^1.2.2: version "1.2.3" resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" @@ -4728,6 +4795,13 @@ tr46@~0.0.3: resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== +turndown@^7.2.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/turndown/-/turndown-7.2.0.tgz#67d614fe8371fb511079a93345abfd156c0ffcf4" + integrity sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A== + dependencies: + "@mixmark-io/domino" "^2.2.0" + type-fest@^0.21.3: version "0.21.3" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"