// @ts-check
import fetch from 'node-fetch'
import { Sema } from 'async-sema'
/**
* @private
* @ignore
*
* @typedef {Object} ParamParser
* @property {unknown} d
* @property {function(unknown): string} v
*/
/**
* @private
* @ignore
*
* @typedef {Object} Constraint
* @property {string} m
* @property {function(URLSearchParams): boolean} v
*/
/**
* Object wrapping a particular wiki's Wikia API V1.
*
* @tutorial getting-started
* @tutorial endpoints
* @tutorial migration-guide
*/
class WikiaAPI {
/**
* @param {string} subdomain Subdomain, for example "dev" for "dev.fandom.com"
* @param {string} [language] Optional language code, for example "pl" for "leagueoflegends.fandom.com/pl"
*/
constructor(subdomain, language) {
/**
* Subdomain, for example "dev" for "dev.fandom.com".
* @type {string}
*/
this.subdomain = subdomain
/**
* Language code, for example "pl" for "leagueoflegends.fandom.com/pl".
* @type {string | null}
*/
this.language = language ?? null
}
/**
* @private
* @ignore
* @static
* @type {Sema}
*/
static _semaphore = new Sema(2)
/**
* Get details about one or more articles.
*
* @param {ArticleDetailsReq} request Request parameters
* @returns {Promise<ArticleDetailsRes>}
*
* @see {@link ArticleDetailsReq}
* @see {@link ArticleDetailsRes}
*/
getArticleDetails(request) {
return /** @type {Promise<ArticleDetailsRes>} */ (
this._makeRequest(
'Articles/Details',
this._parseParams(
request,
{
ids: this._paramParser([], 'number', true),
titles: this._paramParser([], 'string', true),
abstract: this._paramParser(100, 'number'),
},
[{ m: 'Article ids, titles or both should be passed!', v: p => p.has('ids') || p.has('titles') }]
)
)
)
}
/**
* Get article list in alphabetical order.
*
* @param {ArticleListReq} [request] Optional request parameters
* @returns {Promise<ArticleListRes>}
*
* @see {@link ArticleListReq}
* @see {@link ArticleListRes}
*/
getArticleList(request = {}) {
return /** @type {Promise<ArticleListRes>} */ (
this._makeRequest(
'Articles/List',
this._parseParams(request, {
category: this._paramParser('', 'string'),
namespaces: this._paramParser([], 'number', true),
limit: this._paramParser(25, 'number'),
offset: this._paramParser('', 'string'),
})
)
)
}
/**
* Get the most viewed articles from this wiki.
*
* @param {TopArticlesReq} [request] Optional request parameters
* @returns {Promise<TopArticlesRes>}
*
* @see {@link TopArticlesReq}
* @see {@link TopArticlesRes}
*/
getTopArticles(request = {}) {
return /** @type {Promise<TopArticlesRes>} */ (
this._makeRequest(
'Articles/Top',
this._parseParams(request, {
namespaces: this._paramParser([], 'number', true),
category: this._paramParser('', 'string'),
limit: this._paramParser(10, 'number'),
})
)
)
}
/**
* Get wiki data, including key values, navigation data and more.
*
* @returns {Promise<WikiVariablesRes>}
*
* @see {@link WikiVariablesRes}
*/
getWikiVariables() {
return /** @type {Promise<WikiVariablesRes>} */ (this._makeRequest('Mercury/WikiVariables'))
}
/**
* Find suggested phrases for chosen query.
*
* @param {string} query Search query
* @returns {Promise<SearchSuggestionsRes>}
*
* @see {@link SearchSuggestionsRes}
*/
getSearchSuggestions(query) {
return /** @type {Promise<SearchSuggestionsRes>} */ (
this._makeRequest(
'SearchSuggestions/List',
this._parseParams({ query }, { query: this._paramParser(undefined, 'string') })
)
)
}
/**
* Get details about selected users.
*
* @param {UserDetailsReq} request Request parameters
* @returns {Promise<UserDetailsRes>}
*
* @see {@link UserDetailsReq}
* @see {@link UserDetailsRes}
*/
getUserDetails(request) {
return /** @type {Promise<UserDetailsRes>} */ (
this._makeRequest(
'User/Details',
this._parseParams(request, {
ids: this._paramParser([], 'number', true),
size: this._paramParser(100, 'number'),
})
)
)
}
/**
* Basepath of Wikia API V1 for the given subdomain and language, for example "http://dev.fandom.com/api/v1/".
* @type {string}
* @readonly
*/
get apiBasepath() {
return `https://${this.subdomain}.fandom.com/${this.language ? `${this.language}/` : ''}api/v1/`
}
/**
* @private
* @ignore
*
* @param {string} endpoint
* @param {URLSearchParams} [params]
* @returns {Promise<unknown>}
*/
async _makeRequest(endpoint, params) {
const query = params ? `?${params.toString()}` : ''
const url = `${this.apiBasepath}${endpoint}${query}`
await WikiaAPI._semaphore.acquire()
try {
const response = await fetch(url, { headers: new Headers({ 'User-Agent': 'nodewikiaapi' }) })
const data = await response.json()
return data
} catch {
throw new Error(`An error occured while requesting the resource (${url})!`)
} finally {
WikiaAPI._semaphore.release()
}
}
/**
* @private
* @ignore
*
* @param {unknown} defaultValue
* @param {"number" | "string"} requiredType
* @param {boolean} [canBeArray=false]
* @returns {ParamParser}
*/
_paramParser(defaultValue, requiredType, canBeArray = false) {
return {
d: defaultValue,
v: input => {
if (typeof input === requiredType) return /** @type {number | string} */ (input).toString()
if (canBeArray && Array.isArray(input) && input.every(x => typeof x === requiredType)) return input.join(',')
throw new Error(`Given input (${input}) has invalid type!`)
},
}
}
/**
* @private
* @ignore
*
* @param {Object<string, unknown>} options
* @param {Object<string, ParamParser>} optionParsers
* @param {Constraint[]} [constraints=[]]
* @returns {URLSearchParams}
*/
_parseParams(options, optionParsers, constraints = []) {
const defaultOptions = Object.fromEntries(Object.entries(optionParsers).map(([p, { d }]) => [p, d]))
const optionsWithDefaults = Object.assign({}, defaultOptions, options)
const parsedOptions = new URLSearchParams()
for (const paramName in optionsWithDefaults) {
if (optionsWithDefaults[paramName] === undefined) continue
if (!(paramName in optionParsers)) throw new Error(`Unexpected param (${paramName}) given!`)
const paramValue = optionParsers[paramName].v(optionsWithDefaults[paramName])
if (paramValue === '') continue
parsedOptions.append(paramName, paramValue)
}
for (const { m, v } of constraints) {
if (!v(parsedOptions)) throw new Error(m)
}
return parsedOptions
}
}
export default WikiaAPI
source