0.5.0 • Published 8 months ago

yugipedia-gql v0.5.0

Weekly downloads
-
License
ISC
Repository
github
Last release
8 months ago

Yugipedia GQL Wrapper

A GraphQL wrapper around the MediaWiki API for Yugipedia. Designed to make the queries and results more intuitive and simple.

⚠️ This wrapper is in its early stages. As new requirements are set and the full design/structure is realized, be prepared for drastic, breaking changes on a regular basis. See the change log for info on recent updates.

⚠️ The codebase on github isn't always synced up with the version hosted on NPM. It is likely the github version of the code is ahead in features and bug fixes, but might also be less stable. Clone it with optimistic caution for the latest updates.

Installation

You'll need to have NodeJS installed on your computer. It's best to have the most up-to-date LTS version installed. Installing NodeJS should also install NPM as a command line interface by default.

You can verify both NodeJS and NPM are installed properly by running the following command in your terminal/emulator of choice:

node -v
npm -v

Finally, cd into (open) your directory of choice through your terminal/emulator and create a new npm project:

cd "REPLACE\\WITH\\YOUR\\PATH"
touch index.js
npm init -y
npm pkg set type="module"
npm i yugipedia-gql

API

class Yugipedia

Creates a Yugipedia API entity capable of performing basic operations.

new Yugipedia(options: {
    userAgent: {
        name: string,
        contact: string,
        reason?: string
    },
    hydratePrototype?: boolean,
    cache?: {
        path?: string,
        ttl?: {
            years?: number,
            months?: number,
            days?: number,
            hours?: number,
            minutes?: number,
            seconds?: number,
        }
    }
}): Yugipedia
Argument NameTypeOptionalDefault ValueDescription
options.userAgent.namestringnoThe best thing to refer to you as
options.userAgent.contactstringnoThe contact details to get a hold of you in case the devs have a question or need to reach out
[options.userAgent.reason]stringyes"Data Collection for Personal Use [Yugipedia-GQL]"The reason you're using the API
[options.hydratePrototype]booleanyestrueThe returned data's prototype is rehydrated as the GraphQL library nullifies it. This is mostly aesthetic, so if it causes issues, set this to false
[options.cache]objectyesSee belowThe cache settings object. Set to false (not recommended) if you don't want caching
[options.cache.path]stringyes"{cwd}/yugipedia-gql-cache"The path to the cache file
[options.cache.ttl]objectyes{ days: 30 }The amount of time after data has been retrieved before it should expire

Yugipedia.prototype.query

Yugipedia.prototype.query(
    gqlQueryString: string,
    variables?: {[key]: unknown}
): {
    data: {[key]: unknown},
    errors: null | [{
        culprit: string,
        code: number,
        log: {
            message: string,
            payload: unknown
        }
    }],
    warnings: null | [{
        culprit: string,
        code: number,
        log: {
            message: string,
            payload: unknown
        }
    }]
}
Argument NameTypeOptionalDefault ValueDescription
gqlQueryStringstringnoThe GraphQL query string
variablesobjectyes{}The variables to use with the query

Usage

A general understanding of the GraphQL language is highly recommended before attempting to use this API wrapper.

💡 There is no need to roll your own rate limiter as all queries are rate limited to one page per second maximum to align with the wishes of the API devs. (See: Request limit).

Example:

import Yugipedia from "yugipedia-gql"

const api = new Yugipedia({
    userAgent: {
        name: "John Smith", // replace with your name
        contact: "jsmith@example.com", // replace with your contact method
        reason: "Testing the GraphQL wrapper for Yugipedia (https://www.npmjs.com/package/yugipedia-gql)"
    }
})

const queryString = `#graphql
    query {
        card(searchTerm: "Dark Magician") {
            name {
                english
                korean {
                    html
                }
            }
            stats {
                attribute
                attack
                defense
            }
            types
        }
    }
`
const result = await api.query(queryString)

console.dir(result, { depth: null })

The above result should be:

{
  data: {
    query: {
      card: {
        name: { english: 'Dark Magician', korean: { html: '블랙 매지션' } },
        stats: { attribute: 'Dark', attack: '2500', defense: '2100' },
        types: [ 'Spellcaster', 'Normal' ]
      }
    }
  },
  errors: null,
  warnings: null
}

Explanation

Currently, there are only two root queries

card(searchTerm: String!): Card
set(searchTerm: String!): Set

The searchTerm for each root query can be anything you think will match a page of that type on Yugipedia. So, that means if Yugipedia recognizes it and can redirect to it, you're probably in business. For example, let's say we wanted info on the Legend of Blue Eyes White Dragon set. We could use the set root query with any of the following searchTerm's:

  • "LOB"
  • "LDD"
  • "LOB-EN"
  • "Legend of Blue Eyes White Dragon"

All of these should end up querying the correct set we were looking for. And for the Blue-Eyes White Dragon card, for example, we could query the card root query with the following searchTerm's and we could expect accurate results:

  • "BEWD"
  • "Blue-Eyes White Dragon"
  • "Blue Eyes White Dragon"
  • "LOB-EN001"
  • "89631139" (the card's password)
  • "...etc."

The wrapper will do its best to take the searchTerm you provide and resolve it to the page it thinks you want, but be assured it will not make any assumptions should it find ambiguous results. It strictly relies on the underlying API's ability to resolve page name redirects, but will make some case and symbol adjustments to help find things should there be a slight discrepancy. More on how redirects are handled can be found in this section.


The basic structure of a query result will have the following shape:

{
    data: {
        [key: queryTitle]: {
            [key: rootQueryName]: {
                ...unknown // whatever your requested data shape looks like
            }
        }
    },
    errors: null | [{
        culprit: string,
        code: number,
        log: {
            message: string,
            payload?: unknown
        }
    }],
    warnings: null | [{
        culprit: string,
        code: number,
        log: {
            message: string,
            payload?: unknown
        }
    }], // same signature as the errors key
}

Stay tuned, more details and explanations to come...

Redirects

This wrapper will make an attempt to resolve redirects as best as possible. It does so by coercing lowercase, uppercase, propercase, titlecase, and sentencecase variations of your provided searchTermss and querying them all against the API efficiently. To test this, you can try a set query with each of the following set names for the Legend of Blue Eyes White Dragon set; they should all succeed.

  • "lob"
  • "Lob"
  • "LOB"
  • "Legend of Blue Eyes White Dragon"
  • "legend of blue eyes white dragon"
  • "legend_of_blue_eyes_white_dragon"
  • "leGEND Of Blue eyeS whitE DRaGON"

It's not infallible, unfortunately. Currently it can't handle spelling mistakes. Perhaps I add some AI to it in the future 😁🤖

Caching

Data caching is handled for you. Every individual property you look up will be saved, and subsequent queries asking for that data will search the cache first before querying the API. There is a very important caveat with the way caching is handled that you should take into account. The code controlling the API requests is designed to batch all requests. As such, all of the data of the request must exist in the cache already if the cache is to be used at all, otherwise it will be ignored and refreshed. Let's visualize this:

Example:

query {
    card(searchTerm: "Dark Magician") {
        name {
            english
        }
        stats {
            attack
        }
    }
}

Given the query above, I'd be getting the English name and attack properties. Now, let's say I realize I also want to get the defense stat:

query {
    card(searchTerm: "Dark Magician") {
        name {
            english
        }
        stats {
            attack
            defense
        }
    }
}

Because the defense stat doesn't exist in the cache based on our previous request the code will discard the cached data and request all of the properties from scratch.

The reasoning behind this behavior lies in the program trying to be as efficient as possible. While it may sound counter-intuitive to discard pre-existing data in the name of efficiency, not doing so would actually trigger a potential cascade of granular requests trying to account for different combinations of missing data, thus increasing the number of requests to the API and your wait time. By instead performing a fresh request, we're able to batch everything into a single request (sorta, this API gets weird).

So, why is this useful to you? For two reasons. The first being that if you understand how the caching works, you'll be more likely to understand why sometimes your queries seem near-instantaneous and why sometimes they'll be delayed by a couple seconds. The other is if you want to tinker with the code yourself. This isn't thoroughly documented in the codebase, and I don't plan to either, so laying it out here gives you what you need.

Errors/Warnings

The errors and warnings keys you'll receive are nullable, meaning they will be null if nothing populated them. If something exists, they will be an array of errors or warnings respectively.

Basic Categories:

  • 3xx - These are reserved for warnings
  • 4xx - These are reserved for errors originating from data collection, such as scraping and/or API errors
  • 5xx - These are reserved for internal errors, specifically GQL query syntax errors

Breakdown - 3xx:

  • 300 <Missing Data> - Describes data that is missing when it was expected by a parser or formatter.
  • 301 <Missing/Corrupted Data> - Describes data that is missing or corrupted as a resource on the fetched endpoint. (to be clear, it's data that should exist, but for some reason doesn't)

Breakdown - 4xx:

  • 400 <Bad Request> - The data that was requested doesn't match the data that was found. For instance, if a name you provided for the card query matches a set instead, etc.
  • 402 <Scrape Failed> - A scraping request failed. Most likely this will be caused by Yugipedia not being in good health for one reason or another.
  • 403 <API Error> - The underlying SMW/MW API produced an error.
  • 404 <Data Not Found> - The data you requested doesn't exist as a resource on the providing server. This will most likely occur due to spelling errors.

Breakdown - 5xx:

  • 500 <Internal Error> - These errors are produced by the underlying GraphQL interpreter, usually denoting syntax errors or issues. If you find this error causes consistent, repeatable issues for you, please create an issue and I'll take a look at it.
  • 501 <Unknown Error> - The error that was raised is hasn't been classified or foreseen. These will be random, uncaught and untested errors that are thrown by anyone from anywhere down the chain. These shouldn't be common, but still might occur.

FatalError:

You may sometimes be presented with a FatalError. These errors are internal to the scraping and data collection code and are 9.9x out of 10 caused by my missing a bug, or even potentially a breaking change to a website or API the code is using. If you see this kind of error, please create an issue immediately as it's likely not a fluke.

Schema

Below are some helpful descriptions of various fields found on root types. The entire schema definition can be found here. More detailed descriptions can be found here.

card(searchTerm: String!): Card <RootQuery>

  • Card.actions <Actions> - specific actions this card takes
  • Card.anti <AntiOrPro> - cards that are targeted by this card
  • Card.appearsIn <[String!]> - titles of media in which this card has appeared
  • Card.cardType <String> - this card's type (monster, spell, trap, etc.)
  • Card.charactersDepicted <[String!]> - what characters are seen in the art of this card
  • Card.debutDate <DebutDate> - the dates this card debuted for specific formats
  • Card.deckType <String> - which deck type this card belongs to (main, side, etc.)
  • Card.description <CardText> - 'lore' or description box text of this card
  • Card.effectTypes <[String!]> - what types of effects this card performs
  • Card.image <CardImage> - details on images, including names and links
  • Card.isReal <Boolean> - denotes if the card exists in the physical ocg/tcg
  • Card.konamiID <String> - the database ID Konami uses for this card
  • Card.limitation <String> - limitation text provided by this card
  • Card.materials <Materials> - materials required or used for this card in its lifetime
  • Card.mediums <[String!]> - the formats in which this card exists (ogc, tcg, games, etc.) (different with releases in that this is more general)
  • Card.mentions <[Card!]> - the cards mentioned by this card
  • Card.miscTags <[String!]> - tags/search properties that don't have their own specific category
  • Card.name <CardText> - the name of this card
  • Card.page <WikiPage> - meta details on the wiki page for this card
  • Card.password <String> - the password of this card
  • Card.pendulum <Pendulum> - pendulum details on this card
  • Card.print <PrintDetails> - print details on the card, such as notes and type (new, reprint, etc.) (only available when queried through a set)
  • Card.pro <AntiOrPro> - cards that are supported by this card
  • Card.rarity <String> - the rarity of this card (only available when queried through a set)
  • Card.related <Related> - page names representing pages that are related to this card
  • Card.releases <[String!]> - the specific release titles this card is associated with (different with mediums in that this is more specific)
  • Card.setCategory <String> - the category of this card in relation to its set (Variant card, Booster pack, etc.) (only available when queried through a set)
  • Card.setCode <String> - the set code of this card (only available when queried through a set)
  • Card.stats <Stats> - stats on this card, such as attack, defense, level, etc.
  • Card.status <Status> - the status given a card in official formats (limited, forbidden, etc.)
  • Card.summonedBy <[Card!]> - the cards that summon this card, typically used on token cards
  • Card.types <[String!]> - the types (warrior/effect/etc.) or spell/trap properties (continuous/equip/etc.) this card has
  • Card.usedBy <[String!]> - characters and their decks in which this card were used

💡 The card API is mainly focused on physical cards. It shouldn't throw an error (untested) when retrieving non-physical cards, such as video game cards, anime cards, etc., but the properties specific to those pages have been neglected for now. There is currently no plan to implement these properties as it would require many, many hours to scrape the "All cards" category members' properties to compile a complete and accurate list.


set(searchTerm: String!): Set <RootQuery>

  • Set.cards <CardList> - the cards that are part of this set's setlist (previous versions of this field were quite slow but have now been heavily optimized. what took 2.5 minutes before for a query to LOB now takes less than 20 seconds. have fun!)
  • Set.code <ProductCode> - the product codes for this set (ISBN, etc.)
  • Set.coverCards <[Card!]> - the cards that appear on the packaging for this set
  • Set.format <String> - the format in regards to forbidden and limited lists (not common on sets, but does exist here and there)
  • Set.image <String> - the main image used in the wiki for this set
  • Set.konamiID <SetKonamiDatabaseID> - the database ID used by Konami for this set
  • Set.mediums <[String!]> - the formats in which this set exists (ogc, tcg, games, etc.)
  • Set.name <LocaleText> - the name of this set
  • Set.page <WikiPage> - meta details on the wiki page for this set
  • Set.parent <Set> - the parent set to this set
  • Set.prefix <Prefix> - the prefixes for this set
  • Set.promotionalSeries <String> - the promotional series this set belongs to (core boosters, etc.)
  • Set.regionalPrefix <Prefix> - the region-specific prefixes for this set
  • Set.releaseDate <SetReleaseDate> - this set's release date
  • Set.type <String> - the type of set this set is (booster, tin, etc.)