mirror of
https://github.com/Drop-OSS/drop.git
synced 2026-07-25 09:14:54 +10:00
metadata engine
This commit is contained in:
+1
-1
@@ -3,6 +3,6 @@
|
|||||||
## To-do list
|
## To-do list
|
||||||
- User authentication (done)
|
- User authentication (done)
|
||||||
- Sessions (done)
|
- Sessions (done)
|
||||||
- Game database/API
|
- Game database/API (done, with GiantBomb provider, more to come!)
|
||||||
- Metadata matching and import
|
- Metadata matching and import
|
||||||
- Frontend beginnings
|
- Frontend beginnings
|
||||||
@@ -11,10 +11,12 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "5.20.0",
|
"@prisma/client": "5.20.0",
|
||||||
|
"axios": "^1.7.7",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"nuxt": "^3.13.0",
|
"nuxt": "^3.13.0",
|
||||||
"prisma": "^5.20.0",
|
"prisma": "^5.20.0",
|
||||||
|
"turndown": "^7.2.0",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest"
|
"vue-router": "latest"
|
||||||
@@ -22,6 +24,7 @@
|
|||||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
|
"@types/turndown": "^5.0.5",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"h3": "^1.12.0"
|
"h3": "^1.12.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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");
|
||||||
@@ -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;
|
||||||
@@ -61,6 +61,8 @@ model Game {
|
|||||||
mBannerId String // linked to objects in s3
|
mBannerId String // linked to objects in s3
|
||||||
mArt String[] // linked to objects in s3
|
mArt String[] // linked to objects in s3
|
||||||
mScreenshots String[] // linked to objects in s3
|
mScreenshots String[] // linked to objects in s3
|
||||||
|
|
||||||
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Developer {
|
model Developer {
|
||||||
@@ -74,8 +76,11 @@ model Developer {
|
|||||||
mDescription String
|
mDescription String
|
||||||
mLogo String
|
mLogo String
|
||||||
mBanner String
|
mBanner String
|
||||||
|
mWebsite String
|
||||||
|
|
||||||
games Game[]
|
games Game[]
|
||||||
|
|
||||||
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Publisher {
|
model Publisher {
|
||||||
@@ -89,6 +94,9 @@ model Publisher {
|
|||||||
mDescription String
|
mDescription String
|
||||||
mLogo String
|
mLogo String
|
||||||
mBanner String
|
mBanner String
|
||||||
|
mWebsite String
|
||||||
|
|
||||||
games Game[]
|
games Game[]
|
||||||
|
|
||||||
|
@@unique([metadataSource, metadataId], name: "metadataKey")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
});
|
||||||
Vendored
+3
-1
@@ -1,8 +1,10 @@
|
|||||||
|
import { MetadataHandler } from "./internal/metadata";
|
||||||
import { SessionHandler } from "./internal/session";
|
import { SessionHandler } from "./internal/session";
|
||||||
|
|
||||||
export * from "h3";
|
export * from "h3";
|
||||||
declare module "h3" {
|
declare module "h3" {
|
||||||
interface H3EventContext {
|
interface H3EventContext {
|
||||||
session: SessionHandler
|
session: SessionHandler;
|
||||||
|
metadataHandler: MetadataHandler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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<T> {
|
||||||
|
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<T>(resource: string, url: string, query: { [key: string]: string | Array<string> }, 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<GiantBombResponseType<T>>(Object.assign({}, options, overlay));
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
id() {
|
||||||
|
return "giantbomb";
|
||||||
|
}
|
||||||
|
name() {
|
||||||
|
return "GiantBomb"
|
||||||
|
}
|
||||||
|
source() {
|
||||||
|
return MetadataSource.GiantBomb;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async search(query: string): Promise<GameMetadataSearchResult[]> {
|
||||||
|
const results = await this.request<Array<GameSearchResult>>("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<GameMetadata> {
|
||||||
|
const result = await this.request<GameResult>("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<PublisherMetadata> {
|
||||||
|
const results = await this.request<Array<CompanySearchResult>>("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<DeveloperMetadata> {
|
||||||
|
return await this.fetchPublisher(params)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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 { _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 {
|
export abstract class MetadataProvider {
|
||||||
abstract id(): string;
|
abstract id(): string;
|
||||||
abstract name(): string;
|
abstract name(): string;
|
||||||
|
abstract source(): MetadataSource;
|
||||||
|
|
||||||
abstract search(query: string): Promise<GameMetadataSearchResult[]>;
|
abstract search(query: string): Promise<GameMetadataSearchResult[]>;
|
||||||
abstract fetchGame(params: _FetchGameMetadataParams): Promise<GameMetadata>;
|
abstract fetchGame(params: _FetchGameMetadataParams): Promise<GameMetadata>;
|
||||||
@@ -11,13 +15,13 @@ export abstract class MetadataProvider {
|
|||||||
abstract fetchDeveloper(params: _FetchDeveloperMetadataParams): Promise<DeveloperMetadata>;
|
abstract fetchDeveloper(params: _FetchDeveloperMetadataParams): Promise<DeveloperMetadata>;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MetadataHandler {
|
export class MetadataHandler {
|
||||||
// Ordered by priority
|
// Ordered by priority
|
||||||
private providers: Map<string, MetadataProvider> = new Map();
|
private providers: PriorityListIndexed<MetadataProvider> = new PriorityListIndexed("id");
|
||||||
private createObject: (url: string) => Promise<string>;
|
private objectHandler: ObjectTransactionalHandler = new ObjectTransactionalHandler();
|
||||||
|
|
||||||
constructor() {
|
addProvider(provider: MetadataProvider, priority: number = 0) {
|
||||||
this.createObject = async () => "";
|
this.providers.push(provider, priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string) {
|
async search(query: string) {
|
||||||
@@ -44,12 +48,114 @@ class MetadataHandler {
|
|||||||
return successfulResults;
|
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) {
|
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}"`);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -17,6 +17,7 @@ export type InternalGameMetadataResult = GameMetadataSearchResult & GameMetadata
|
|||||||
export type RemoteObject = string;
|
export type RemoteObject = string;
|
||||||
|
|
||||||
export interface GameMetadata {
|
export interface GameMetadata {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
shortDescription: string;
|
shortDescription: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -37,6 +38,7 @@ export interface GameMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface PublisherMetadata {
|
export interface PublisherMetadata {
|
||||||
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
shortDescription: string;
|
shortDescription: string;
|
||||||
description: string;
|
description: string;
|
||||||
@@ -53,12 +55,12 @@ export interface _FetchGameMetadataParams {
|
|||||||
publisher: (query: string) => Promise<Publisher>
|
publisher: (query: string) => Promise<Publisher>
|
||||||
developer: (query: string) => Promise<Developer>
|
developer: (query: string) => Promise<Developer>
|
||||||
|
|
||||||
createObject: (url: string) => Promise<RemoteObject>
|
createObject: (url: string) => RemoteObject
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface _FetchPublisherMetadataParams {
|
export interface _FetchPublisherMetadataParams {
|
||||||
query: string;
|
query: string;
|
||||||
createObject: (url: string) => Promise<RemoteObject>;
|
createObject: (url: string) => RemoteObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams;
|
export type _FetchDeveloperMetadataParams = _FetchPublisherMetadataParams;
|
||||||
@@ -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<void>;
|
||||||
|
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];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { FilterConditionally } from "./typefilter";
|
||||||
|
|
||||||
|
interface PriorityTagged<T> {
|
||||||
|
object: T,
|
||||||
|
priority: number, // Higher takes priority
|
||||||
|
addedIndex: number, // Lower takes priority
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PriorityList<T> {
|
||||||
|
private source: Array<PriorityTagged<T>> = [];
|
||||||
|
private cachedSorted: Array<T> | 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<T> = keyof FilterConditionally<T, (() => string) | string>;
|
||||||
|
export class PriorityListIndexed<T> extends PriorityList<T> {
|
||||||
|
private indexName: IndexableProperty<T>;
|
||||||
|
private indexMap: { [key: string]: T } = {};
|
||||||
|
|
||||||
|
constructor(indexName: IndexableProperty<T>) {
|
||||||
|
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<T> {
|
||||||
|
const value = super.pop(position);
|
||||||
|
|
||||||
|
const index = this.getIndex(value.object);
|
||||||
|
delete this.indexMap[index];
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(index: string) {
|
||||||
|
return this.indexMap[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export type FilterConditionally<Source, Condition> = Pick<Source, { [K in keyof Source]: Source[K] extends Condition ? K : never }[keyof Source]>;
|
||||||
@@ -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;
|
||||||
|
})
|
||||||
|
});
|
||||||
@@ -730,6 +730,11 @@
|
|||||||
semver "^7.3.5"
|
semver "^7.3.5"
|
||||||
tar "^6.1.11"
|
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":
|
"@netlify/functions@^2.8.0":
|
||||||
version "2.8.1"
|
version "2.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-2.8.1.tgz#67cd94f929551e156225fb50d2efba603b97e138"
|
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"
|
resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975"
|
||||||
integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==
|
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":
|
"@types/uuid@^10.0.0":
|
||||||
version "10.0.0"
|
version "10.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d"
|
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"
|
resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce"
|
||||||
integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==
|
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:
|
autoprefixer@^10.4.20:
|
||||||
version "10.4.20"
|
version "10.4.20"
|
||||||
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b"
|
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.20.tgz#5caec14d43976ef42e32dcb4bd62878e96be5b3b"
|
||||||
@@ -1706,6 +1721,15 @@ autoprefixer@^10.4.20:
|
|||||||
picocolors "^1.0.1"
|
picocolors "^1.0.1"
|
||||||
postcss-value-parser "^4.2.0"
|
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:
|
b4a@^1.6.4:
|
||||||
version "1.6.7"
|
version "1.6.7"
|
||||||
resolved "https://registry.yarnpkg.com/b4a/-/b4a-1.6.7.tgz#a99587d4ebbfbd5a6e3b21bdb5d5fa385767abe4"
|
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"
|
resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43"
|
||||||
integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==
|
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:
|
commander@^2.20.0:
|
||||||
version "2.20.3"
|
version "2.20.3"
|
||||||
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
|
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"
|
resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.4.tgz#4e0c9cf9ff68fe5f3d7f2765cc1a012dfdcb0479"
|
||||||
integrity sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==
|
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:
|
delegates@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a"
|
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"
|
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a"
|
||||||
integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==
|
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:
|
foreground-child@^3.1.0:
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.0.tgz#0ac8644c06e431439f8561db8ecf29a7b5519c77"
|
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"
|
cross-spawn "^7.0.0"
|
||||||
signal-exit "^4.0.1"
|
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:
|
fraction.js@^4.3.7:
|
||||||
version "4.3.7"
|
version "4.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7"
|
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"
|
braces "^3.0.3"
|
||||||
picomatch "^2.3.1"
|
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:
|
mime@1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
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"
|
resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86"
|
||||||
integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==
|
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:
|
queue-microtask@^1.2.2:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
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"
|
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
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:
|
type-fest@^0.21.3:
|
||||||
version "0.21.3"
|
version "0.21.3"
|
||||||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
|
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37"
|
||||||
|
|||||||
Reference in New Issue
Block a user