wiki-saikou
Version:
The library provides the out of box accessing to MediaWiki API in both browsers & Node.js, and the syntax is very similar to vanilla `new mw.Api()`. TypeScript definition included~
530 lines (521 loc) • 16.2 kB
JavaScript
'use strict';
const fexios = require('fexios');
exports.MwParamNormalizer = void 0;
((MwParamNormalizer2) => {
function normalizeParamValue(item) {
if (Array.isArray(item)) {
return item.join("|");
} else if (typeof item === "boolean" || item === null) {
return item ? "1" : void 0;
} else if (typeof item === "number") {
return "" + item;
} else {
return item;
}
}
MwParamNormalizer2.normalizeParamValue = normalizeParamValue;
function normalizeBody(body) {
const isFormLike = (body2) => body2 && (body2 instanceof URLSearchParams || body2 instanceof FormData);
if (body === void 0 || body === null) {
return void 0;
}
const formData = new FormData();
if (isFormLike(body)) {
body.forEach((value, key) => {
const data = normalizeParamValue(value);
if (data !== null && data !== void 0) {
formData.append(key, data);
}
});
return formData;
}
if (fexios.isPlainObject(body)) {
Object.entries(body).forEach(([key, value]) => {
const data = normalizeParamValue(value);
if (data !== null && data !== void 0) {
formData.append(key, data);
}
});
return formData;
}
return void 0;
}
MwParamNormalizer2.normalizeBody = normalizeBody;
})(exports.MwParamNormalizer || (exports.MwParamNormalizer = {}));
var WikiSaikouErrorCode = /* @__PURE__ */ ((WikiSaikouErrorCode2) => {
WikiSaikouErrorCode2["HTTP_ERROR"] = "HTTP_ERROR";
WikiSaikouErrorCode2["LOGIN_FAILED"] = "LOGIN_FAILED";
WikiSaikouErrorCode2["LOGIN_RETRY_LIMIT_EXCEEDED"] = "LOGIN_RETRY_LIMIT_EXCEEDED";
WikiSaikouErrorCode2["TOKEN_RETRY_LIMIT_EXCEEDED"] = "TOKEN_RETRY_LIMIT_EXCEEDED";
return WikiSaikouErrorCode2;
})(WikiSaikouErrorCode || {});
class WikiSaikouError extends Error {
constructor(code, message = "", cause) {
super();
this.code = code;
this.message = message;
this.cause = cause;
}
name = "WikiSaikouError";
static is(data, code) {
return data instanceof this && (code === void 0 || data.code === code);
}
}
class MediaWikiApiError extends Error {
constructor(errors, cause) {
super();
this.errors = errors;
this.cause = cause;
this.errors = MediaWikiApiError.normalizeErrors(errors);
this.message = errors.map((error) => error.text).filter(Boolean).join("\n");
this.code = this.isBadTokenError() ? "badtoken" : this.errors[0]?.code || "Unknown Error";
}
name = "MediaWikiApiError";
code;
get firstError() {
return this.errors[0];
}
isBadTokenError() {
return this.errors.some((error) => error.code === "badtoken") || ["NeedToken", "WrongToken"].includes(this.cause?.data?.login?.result);
}
toString() {
return `[${this.name} ${this.code}]`;
}
static is(data) {
return data instanceof this;
}
static normalizeErrors(errors) {
if (Array.isArray(errors) === false) {
return [];
}
return errors.filter((e) => typeof e === "object" && !!e?.code).map((error) => {
if (!error.text) {
if (error.info) {
return { ...error, text: error.info };
} else if (error["*"]) {
return { ...error, text: error["*"] };
} else {
return { ...error, text: "" };
}
}
return error;
});
}
}
((WikiSaikouError2) => {
function extractResponseDataFromAny(data) {
if (data == null) return void 0;
if (data?.response?.data !== void 0) return data.response.data;
if (data?.data !== void 0) return data.data;
const cause = data instanceof Error ? data.cause : void 0;
if (cause?.response?.data !== void 0) return cause.response.data;
if (cause?.data !== void 0) return cause.data;
return data || void 0;
}
function includesMediaWikiApiError(data) {
return extractMediaWikiApiErrors(data).length > 0;
}
WikiSaikouError2.includesMediaWikiApiError = includesMediaWikiApiError;
WikiSaikouError2.normalizeMwApiErrors = MediaWikiApiError.normalizeErrors;
function extractMediaWikiApiErrors(data) {
let r = extractResponseDataFromAny(data);
if (typeof r !== "object" || r === null) {
return [];
}
const error = r?.error;
const errors = r?.errors;
const result = [];
if (error) {
result.push(error);
}
if (Array.isArray(errors)) {
result.push(...errors);
}
return (0, WikiSaikouError2.normalizeMwApiErrors)(result);
}
WikiSaikouError2.extractMediaWikiApiErrors = extractMediaWikiApiErrors;
function isBadTokenError(data) {
if (MediaWikiApiError.is(data)) {
return data.isBadTokenError();
} else {
const errors = extractMediaWikiApiErrors(data);
return new MediaWikiApiError(errors).isBadTokenError();
}
}
WikiSaikouError2.isBadTokenError = isBadTokenError;
})(WikiSaikouError || (WikiSaikouError = {}));
const FEXIOS_SAIKOU_MARKER = Symbol.for("__FEXIOS_SAIKOU__");
/**
* FexiosSaikou
*
* A pre-configured Fexios instance with MediaWiki-friendly defaults:
* - MediaWiki-specific request/response params normalization
* - built-in token management
* - error handling
*
* @author dragon-fish <dragon-fish@qq.com>
* @license MIT
*
* @param {string|URL} payload create a new FexiosSaikou instance with the given baseURL
* @param {Fexios} payload or make the given Fexios instance a FexiosSaikou instance
*/
function createFexiosSaikou(payload) {
const instance = payload instanceof fexios.Fexios ? payload : new fexios.Fexios({
baseURL: payload instanceof URL ? payload.toString() : String(payload),
responseType: "json"
});
if (instance[FEXIOS_SAIKOU_MARKER]) {
return instance;
} else {
Reflect.defineProperty(instance, FEXIOS_SAIKOU_MARKER, {
get: () => true,
enumerable: false,
configurable: false
});
}
instance.on("beforeInit", (ctx) => {
if (ctx.request.method?.toLowerCase() !== "post") {
return ctx;
}
if (ctx.request.body === void 0 || ctx.request.body === null) {
ctx.request.body = void 0;
return ctx;
}
const body = ctx.request.body = exports.MwParamNormalizer.normalizeBody(
ctx.request.body
);
const query = new URLSearchParams(ctx.request.query);
if (body.has("format")) {
query.delete("format");
}
if (body.has("formatversion")) {
query.delete("formatversion");
}
if (body.has("action")) {
query.delete("action");
}
if (body.has("origin")) {
query.set("origin", "" + body.get("origin"));
body.delete("origin");
}
ctx.request.query = Object.fromEntries(query.entries());
return ctx;
});
instance.on("beforeInit", (ctx) => {
ctx.request.query = fexios.FexiosQueryBuilder.mergeQueries(
{},
exports.MwParamNormalizer.normalizeBody(ctx.request.query)
);
return ctx;
});
instance.on("beforeRequest", (ctx) => {
const url = new URL(ctx.request.url);
const query = fexios.FexiosQueryBuilder.mergeQueries(url.searchParams, ctx.query);
if (globalThis.location) {
if (!query.origin && location.origin !== url.origin) {
query.origin = location.origin;
instance.baseConfigs.credentials = "include";
instance.baseConfigs.mode = "cors";
} else if (location.origin === url.origin) {
delete query.origin;
instance.baseConfigs.credentials = void 0;
instance.baseConfigs.mode = void 0;
}
}
if (query.origin) {
const origin = encodeURIComponent(query.origin).replace(/\./g, "%2E");
delete query.origin;
ctx.request.query = query;
ctx.request.url = `${url.origin}${url.pathname}?origin=${origin}`;
} else {
ctx.request.query = query;
}
return ctx;
});
instance._tokens = /* @__PURE__ */ new Map();
instance.on("afterResponse", (ctx) => {
const { data } = ctx;
const tokenName = ctx.runtime.customEnv?.mwTokenName;
if (tokenName && WikiSaikouError.isBadTokenError(data)) {
instance._tokens.delete(tokenName);
}
const tokens = data?.query?.tokens;
if (tokens && typeof tokens === "object") {
Object.entries(tokens).forEach(([type, token]) => {
typeof token === "string" && instance._tokens.set(type.replace(/token$/i, "").toLowerCase(), token);
});
}
const loginToken = data?.login?.token;
if (typeof loginToken === "string") {
instance._tokens.set("login", loginToken);
}
return ctx;
});
return instance;
}
const resolveLegacyCtor = (configOrBaseURL, defaultOptions, defaultParams) => {
let config = { ...WikiSaikouCore.DEFAULT_CONFIGS };
if (typeof configOrBaseURL === "string") {
config = fexios.deepMerge(config, {
baseURL: configOrBaseURL,
fexiosConfigs: defaultOptions || {},
defaultParams: defaultParams || {},
throwOnApiError: false
// Set a default value for throwOnApiError
});
} else if (typeof configOrBaseURL === "object" && configOrBaseURL !== null) {
config = fexios.deepMerge(config, configOrBaseURL);
}
if (!config.baseURL && typeof window === "object" && window.mediaWiki) {
const { wgServer, wgScriptPath } = window.mediaWiki?.config?.get(["wgServer", "wgScriptPath"]) || {};
if (typeof wgServer === "string" && typeof wgScriptPath === "string") {
config.baseURL = `${wgServer}${wgScriptPath}/api.php`;
}
}
if (typeof config.baseURL !== "string") {
throw new Error("baseURL is required");
}
return config;
};
const noop = () => {
};
const useRetry = async (fn, options) => {
let retryCount = 0;
const { retry = 3, onRetry = noop, shouldRetry = () => true } = options;
let lastError;
do {
try {
return await fn();
} catch (error) {
lastError = error;
if (shouldRetry(error, retryCount)) {
onRetry(error, retryCount);
retryCount++;
} else {
throw error;
}
}
} while (retryCount < retry);
throw lastError || new Error("Retry failed");
};
/**
* WikiSaikou Core class
* @internal You **SHOULD NOT** use this class directly, instead, use the `MediaWikiApi` class.
* @author dragon-fish <dragon-fish@qq.com>
* @license MIT
*/
class WikiSaikouCore {
config;
version = "7.6.0";
request;
static INIT_DEFAULT_PARAMS = {
action: "query",
errorformat: "plaintext",
format: "json",
formatversion: 2
};
static DEFAULT_CONFIGS = {
baseURL: void 0,
fexiosConfigs: {
responseType: "json"
},
defaultParams: this.INIT_DEFAULT_PARAMS,
throwOnApiError: false
};
constructor(configOrBaseURL, defaultOptions, defaultParams) {
const config = this.config = resolveLegacyCtor(
configOrBaseURL,
defaultOptions,
defaultParams
);
this.request = createFexiosSaikou(config.baseURL);
}
setBaseURL(baseURL) {
this.config.baseURL = baseURL;
this.request.baseConfigs.baseURL = baseURL;
return this;
}
/** Base methods encapsulation */
async get(query, options) {
const payload = fexios.deepMerge(
{},
this.config.fexiosConfigs,
{
query: fexios.FexiosQueryBuilder.mergeQueries(
this.config.defaultParams,
query
)
},
options
);
return this.runRequestWithApiErrorMapping(
() => this.request.get("", payload)
);
}
async post(data, options) {
return this.runRequestWithApiErrorMapping(
() => this.request.post(
"",
data,
fexios.deepMerge(
{},
this.config.fexiosConfigs,
{
query: this.config.defaultParams
},
options
)
)
);
}
/**
* Wrap a request to map non-2xx responses containing MediaWiki API error bodies
* into MediaWikiApiError when throwOnApiError=true, and then pass 2xx responses
* through handleApiResponse for unified processing.
*/
async runRequestWithApiErrorMapping(doRequest) {
try {
const res = await doRequest();
return this.handleApiResponse(res);
} catch (err) {
if (this.config.throwOnApiError && WikiSaikouError.includesMediaWikiApiError(err)) {
throw new MediaWikiApiError(
WikiSaikouError.extractMediaWikiApiErrors(err),
err
);
}
throw err;
}
}
throwIfApiError(data) {
const errors = WikiSaikouError.extractMediaWikiApiErrors(data);
if (errors.length > 0) {
throw new MediaWikiApiError(errors, data);
}
}
handleApiResponse(res) {
if (this.config.throwOnApiError) {
this.throwIfApiError(res.data);
}
return res;
}
/** Token Handler */
get tokens() {
return this.request._tokens;
}
async fetchTokens(types = ["csrf"]) {
this.config.fexiosConfigs.credentials = "include";
await this.get({
action: "query",
meta: "tokens",
type: types
});
return this.tokens;
}
async getToken(type = "csrf", noCache = false) {
if (!this.tokens.get(type) || noCache) {
this.tokens.delete(type);
await this.fetchTokens([type]);
}
return this.tokens.get(type);
}
badToken(type) {
this.tokens.delete(type);
return this.tokens;
}
async postWithToken(tokenType, body, options) {
const {
tokenName = "token",
retry = 3,
noCache = false,
fexiosOptions
} = options || {};
if (retry < 1) {
throw new WikiSaikouError(
WikiSaikouErrorCode.TOKEN_RETRY_LIMIT_EXCEEDED,
"The limit of the number of times to automatically re-acquire the token has been exceeded"
);
}
let attemptIndex = 0;
return useRetry(
async () => {
const token = await this.getToken(
tokenType,
noCache || attemptIndex > 0
);
try {
const ctx = await this.post(
{
[tokenName]: token,
...body
},
fexios.deepMerge(fexiosOptions || {}, {
customEnv: {
mwTokenName: tokenType
}
})
);
if (WikiSaikouError.isBadTokenError(ctx.data)) {
throw ctx;
}
return ctx;
} catch (err) {
if (WikiSaikouError.isBadTokenError(err) || err?.ok === false) {
throw err;
} else if (MediaWikiApiError.is(err)) {
throw err;
} else {
throw new WikiSaikouError(
WikiSaikouErrorCode.HTTP_ERROR,
"Network/transport or SDK-internal error (not a MediaWiki API error)",
err
);
}
}
},
{
retry,
onRetry: (_error, count) => {
attemptIndex = count + 1;
},
shouldRetry: (error) => WikiSaikouError.isBadTokenError(error) || error?.ok === false
}
).catch((err) => {
if (WikiSaikouError.isBadTokenError(err) || err?.ok === false) {
throw new WikiSaikouError(
WikiSaikouErrorCode.TOKEN_RETRY_LIMIT_EXCEEDED,
"Retry attempts for acquiring/using token exhausted",
err
);
}
throw err;
});
}
postWithEditToken(body) {
return this.postWithToken("csrf", body);
}
// for backward compatibility
/** @deprecated Use `this.config.baseURL` instead */
get baseURL() {
return this.config.baseURL;
}
/** @deprecated Use `this.config.defaultParams` instead */
get defaultParams() {
return this.config.defaultParams;
}
/** @deprecated Use `this.config.fexiosConfigs` instead */
get defaultOptions() {
return this.config.fexiosConfigs;
}
/** @deprecated Use `createFexiosSaikou` instead */
static createRequestHandler = createFexiosSaikou;
/** @deprecated Use `this.getToken` instead */
token = this.getToken;
}
exports.MediaWikiApiError = MediaWikiApiError;
exports.WikiSaikouCore = WikiSaikouCore;
exports.WikiSaikouError = WikiSaikouError;
exports.WikiSaikouErrorCode = WikiSaikouErrorCode;
exports.createFexiosSaikou = createFexiosSaikou;
exports.resolveLegacyCtor = resolveLegacyCtor;
//# sourceMappingURL=wiki-saikou.1DPaxo4l.cjs.map