lucaswerkmeister / m3api

minimal modern MediaWiki API client
https://www.npmjs.com/package/m3api
ISC License
11 stars 1 forks source link
javascript m3api mediawiki mediawiki-api

m3api

npm documentation

m3api is a minimal, modern MediaWiki API client, a library for interacting with the MediaWiki Action API from JavaScript.

It supports both Node.js and browsers. The browser version has no external dependencies.

Usage

Here’s an example demonstrating some ways to use m3api:

// you may need to change the import path,
// e.g. ./node_modules/m3api/node.js if installed via npm
import Session, { set } from './node.js';

// note: this example uses top-level await for simplicity,
// you may need an async wrapper function

// create a session from a wiki domain (or full api.php URL)
const session = new Session( 'en.wikipedia.org', {
    // these default parameters will be added to all requests
    formatversion: 2,
    errorformat: 'plaintext',
}, {
    // these default options will apply to all requests
    userAgent: 'm3api-README-example',
} );

// a sample request to the siteinfo API
const siteinfoResponse = await session.request( {
    action: 'query',
    meta: 'siteinfo',
    // array parameters are automatically converted
    siprop: [ 'general', 'statistics' ],
} );
// one way to handle the response: destructure it
const { query: {
    general: { sitename },
    statistics: { edits },
} } = siteinfoResponse;
console.log( `Welcome to ${sitename}, home to ${edits} edits!` );

// another way to get the same result
async function getSiteName( session ) {
    const response = await session.request( {
        action: 'query',
        // set parameters may be combined with other requests
        meta: set( 'siteinfo' ),
        siprop: set( 'general' ),
    } );
    return response.query.general.sitename;
}
async function getSiteEdits( session ) {
    const response = await session.request( {
        action: 'query',
        meta: set( 'siteinfo' ),
        siprop: set( 'statistics' ),
    } );
    return response.query.statistics.edits;
}
// the following two concurrent API requests will be automatically combined,
// sending a single request with siprop=general|statistics,
// because they are compatible (set parameters get merged, others are equal)
const [ sitename_, edits_ ] = await Promise.all( [
    getSiteName( session ),
    getSiteEdits( session ),
] );
console.log( `Welcome back to ${sitename_}, home to ${edits_} edits!` );

// a slightly contrived example for continuation
console.log( 'Here are ten local file pages linking to web.archive.org:' );
// due to miser mode, each request may only return few results,
// so we need continuation in order to get ten results in total
let n = 0;
outer: for await ( const urlResponse of session.requestAndContinue( {
    // requestAndContinue returns an async iterable of responses
    action: 'query',
    list: set( 'exturlusage' ),
    euprotocol: 'https',
    euquery: 'web.archive.org',
    eunamespace: [ 6 ], // File:
    eulimit: 'max',
    euprop: set( 'title' ),
} ) ) {
    for ( const page of urlResponse.query.exturlusage ) {
        console.log( page.title );
        if ( ++n >= 10 ) {
            break outer;
            // once we stop iterating, no more requests are made
        }
    }
}

This code works in Node.js, but also in the browser with only two changes:

Other features not demonstrated above:

For more details, see also the code-level documentation (JSdoc comments).

Automatically combining requests

One m3api feature deserves a more detailed discussion: how it automatically combines concurrent, compatible API requests.

To take advantage of this feature, it’s recommended to use set( ... ) instead of [ ... ] for most “list-like” API paremeters, even if you’re only specifying a single set element, as long as that parameter is safe to merge with other requests. For example, consider this request from the usage example above:

session.requestAndContinue( {
    action: 'query',
    list: set( 'exturlusage' ),
    euprotocol: 'https',
    euquery: 'web.archive.org',
    eunamespace: [ 6 ], // File:
    eulimit: 'max',
    euprop: set( 'title' ),
} )

Let’s go through those parameters in turn:

The last point is worth elaborating on: don’t just rely on default parameter values if your requests may be combined with others. This is most important when you’re writing library code (similar to the getSiteName and getSiteEdits functions in the usage example above), where you don’t know which other requests may be made at any time; if you’re directly making API requests from an application, you may know that no other concurrent requests will be made at a certain time, and could get away with relying on default parameters.

To avoid just relying on default parameter values, you have several options:

  1. Explicitly specify a value for the parameter, either the default or (as with title vs. ids|title|url above) a part of it.

  2. Explicitly specify the parameter as null or undefined. This means that the parameter won’t be sent with the request (i.e. the server-side default will be used), but makes the request incompatible with any other request that has a different value for the parameter. (This is similar to using an array instead of a set, as we saw for eunamespace above: both strategies inhibit merging with some other requests.)

  3. Process the response in a way that works regardless of parameter value. This is not always possible, but as an example, with a bit of extra code, you may be able to process both formatversion=1 and formatversion=2 responses (see also the responseBoolean helper function).

Extension packages

While m3api itself aims to be a minimal library, its functionality can be extended by other packages, which make it easier to use certain APIs correctly. Available extension packages include:

If you create an additional extension package, feel free to submit a pull request to add it to this list. (Also, have a look at the guidelines below.)

Using extension packages

For the most part, m3api extension packages can be used like other packages: you install them using npm, import functions from them, etc.

However, they require some setup to be used in the browser. As they can’t import m3api using a relative path, and bare m3api imports only work out of the box in Node.js, something needs to resolve the imports for the browser. The most convenient way is to use a bundler or build system: for example, Vite has been tested and works out of the box.

Alternatively, you can specify an import map, like in this example:

<script type="importmap">
{
    "imports": {
        "m3api/": "./node_modules/m3api/",
        "m3api-query/": "./node_modules/m3api-query/"
    }
}
</script>
<script type="module">
    import Session, { set } from 'm3api/browser.js';
    import { queryFullPageByTitle } from 'm3api-query/index.js';
    // ...
</script>

Note that import maps are not as widely supported as ES6 modules in general.

Creating extension packages

Here are some guidelines or recommendations for creating m3api extension packages:

Compatibility

In Node.js, m3api is compatible with Node 18.2.0 or later. Among major browsers, m3api is compatible with Chrome 63, Firefox 60, Edge 79, Opera 50 (46 on Android), Safari 12, and Samsung Internet 8.0. The relevant browser requirements of m3api are:

The Node.js version requirement is based on fetch() being available and supported by the http-cookie-agent package. If you need support for earlier Node.js versions, try using m3api v0.7.3.

Other modern features used by m3api – destructuring assignment, spread syntax, default arguments, classes, etc. – are less recent than ES6 modules and async generators, and therefore aren’t expected to affect compatibility.

Using a combination of transpiling and polyfilling, it should be possible to use m3api on older platforms as well. If you try this, feel free to send a pull request updating this paragraph with your experience.

Stability

m3api follows a slightly modified version of semantic versioning. The public interface, which most users will use, is stable between minor versions (only changing incompatibly between major versions); however, the internal interface, which some extension packages may use, is only stable between patch versions, and may change incompatibly between minor versions. Most users are encouraged to use the “caret” operator in their m3api dependency (e.g. ^1), but extension packages depending on the internal interface should use the “tilde” operator (e.g. ~1.0), and list all m3api versions they’re compatible with (e.g. ~1.0||~1.1).

The stable, public interface comprises the following items:

The internal interface additionally comprises the following items:

That is, public code only changes incompatibly between major versions, @protected code only changes incompatibly between minor versions, and @private code may change incompatibly at any time.

For methods, the stable interface only includes calling them; overriding them is part of the internal interface. (That is, changes that are compatible for callers but will require overriders to adjust may take place between minor versions.)

Incompatible changes to the stable interface will be mentioned in the changelog, always at the beginning of the entry for an release (before compatible changes in the same release), using the words “BREAKING CHANGE” (in all caps). Incompatible changes to the internal interface will be mentioned using the words “Internal Breaking Change”, not necessarily at the beginning of the entry.

The usual semver interpretation of pre-1.0 versions applies, i.e. in 0.x.y, x is the “major” version and y the “minor” one.

License

Published under the ISC License. By contributing to this software, you agree to publish your contribution under the same license.