m3api-rest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,11 @@
1
+ Node 18.2.0
2
+ Chrome 63
3
+ ChromeAndroid 63
4
+ Firefox 60
5
+ FirefoxAndroid 60
6
+ Edge 79
7
+ Opera 50
8
+ # OperaAndroid 46 according to MDN but caniuse-lite situation confusing
9
+ Safari 12
10
+ ios_saf 12
11
+ Samsung 12 # actually 8 but caniuse-lite only knows 12
package/CHANGES.md ADDED
@@ -0,0 +1,20 @@
1
+ # Changelog
2
+
3
+ This file records the changes in each m3api-rest release.
4
+
5
+ The annotated tag (and GitLab release) for each version also lists the changes,
6
+ but this file may sometimes contain later improvements (e.g. typo fixes).
7
+
8
+ ## v0.1.0 (2026-01-01)
9
+
10
+ Initial release, including:
11
+
12
+ - `getJson()` and `postForJson()` functions,
13
+ for making GET and POST requests returning JSON data.
14
+ - ``` path`` ``` template literal tag function,
15
+ for encoding request paths.
16
+ - `getResponseStatus()` function,
17
+ for getting the HTTP status code of a response.
18
+ - `RestApiServerError`, `RestApiClientError`,
19
+ `UnexpectedResponseStatus`, `InvalidResponseBody`,
20
+ and `UnknownResponseError` error classes thrown by these functions.
@@ -0,0 +1 @@
1
+ The development of this software is covered by a [Code of Conduct](https://www.mediawiki.org/wiki/Special:MyLanguage/Code_of_Conduct).
package/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2025 Lucas Werkmeister
2
+
3
+ Permission to use, copy, modify, and/or distribute this software for any
4
+ purpose with or without fee is hereby granted, provided that the above
5
+ copyright notice and this permission notice appear in all copies.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
8
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
9
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
10
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
11
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
12
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR
13
+ IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,156 @@
1
+ # m3api-rest
2
+
3
+ m3api-rest is an extension package for [m3api],
4
+ allowing you to interact with the MediaWiki REST API.
5
+
6
+ ## Request functions
7
+
8
+ m3api-rest includes functions to make GET and POST requests that return JSON responses.
9
+ Other request methods and response content types will be added in future versions.
10
+ All functions start with the HTTP method name (in lowercase);
11
+ for GET functions, this is directly followed by the response type (e.g. `getJson`),
12
+ while other functions have the word “for” in between (e.g. `postForJson`)
13
+ to clarify that this is the response content type, not the request content type.
14
+ (The content type of the request body is specified by the type of the body passed into the function;
15
+ currently `postForJson` only supports `URLSearchParams` bodies,
16
+ but in future versions the same function will support multiple body types.)
17
+
18
+ ### `getJson`
19
+
20
+ Make a GET request for some JSON data.
21
+ Usage examples:
22
+
23
+ ```js
24
+ import Session from 'm3api';
25
+ import { getJson, path } from 'm3api-rest';
26
+
27
+ const session = new Session( 'en.wikipedia.org', {}, {
28
+ userAgent: 'm3api-rest-README-example',
29
+ } );
30
+
31
+ const title = 'Main page';
32
+ const page = await getJson( session, path`/v1/page/${ title }` );
33
+ console.log( page ); // { id: 217225, title: 'Main page', ... }
34
+ ```
35
+
36
+ ```js
37
+ const searchResults = await getJson( session, '/v1/search/page', {
38
+ q: 'search query',
39
+ limit: 10,
40
+ } );
41
+ console.log( searchResults ); // { pages: [ { title: 'Web query', ... }, ... ] }
42
+ ```
43
+
44
+ See also below for details on the different ways to specify parameters.
45
+
46
+ ### `postForJson`
47
+
48
+ Make a POST request that will return some JSON data.
49
+ Usage example:
50
+
51
+ ```js
52
+ const wikitext = '== ==\nThis is <span id=id>bad</span> <span id=id>wikitext</span>.';
53
+ const lints = await postForJson( session, '/v1/transform/wikitext/to/lint', new URLSearchParams( {
54
+ wikitext,
55
+ } ) );
56
+ console.log( lints ); // [ { type: 'empty-heading', ... }, { type: 'duplicate-ids', ... } ]
57
+ ```
58
+
59
+ (Note that some endpoints, including `POST /v1/page`, cannot be used yet,
60
+ because m3api-rest can currently only send `application/x-www-form-urlencoded` request bodies
61
+ but those endpoints require `application/json`.
62
+ This will be fixed in a future version.)
63
+
64
+ ## Specifying parameters
65
+
66
+ REST API endpoints have different kinds of parameters.
67
+ Many endpoints take parameters in the path, written like `/v1/page/{title}` (`title` is a parameter);
68
+ many endpoints take query parameters after the path, like `/v1/search/page?q=search` (`q` is a parameter);
69
+ and non-GET endpoints (POST etc.) take body parameters in several encodings.
70
+ m3api-rest supports several ways to specify these parameters.
71
+
72
+ ### Path parameters
73
+
74
+ Path parameters can be specified using a tagged template literal (string template)
75
+ using the `path` function,
76
+ which will encode the parameter value if necessary.
77
+
78
+ ```js
79
+ import { path } from 'm3api-rest';
80
+
81
+ const title = 'AC/DC';
82
+ const url = path`/v1/page/${ title }`; // /v1/page/AC%2FDC
83
+ ```
84
+
85
+ To use it, take the endpoint URL and change `{parameters}` to `${substitutions}`
86
+ (or `${ substitutions }` with spaces, depending on your code style).
87
+
88
+ Alternatively, you can pass the original endpoint URL into the request function unmodified,
89
+ and the request function will pull the parameters out of the params passed into it.
90
+ For example:
91
+
92
+ ```js
93
+ const page = await getJson( session, '/v1/page/{title}', {
94
+ // this object contains the params for the request
95
+ title: 'AC/DC', // this is a path parameter
96
+ redirect: true, // this is a query parameter, see below
97
+ } );
98
+ // makes a request to /v1/page/AC%2FDC?redirect=true
99
+ ```
100
+
101
+ ### Query parameters
102
+
103
+ The GET request functions, e.g. `getJson`, take an object with query parameters after the path:
104
+
105
+ ```js
106
+ const searchResults = await getJson( session, '/v1/search/page', {
107
+ q: 'search query',
108
+ limit: 10,
109
+ } );
110
+ // makes a request to /v1/search/page?q=search%20query&limit=10
111
+ ```
112
+
113
+ As mentioned above, you can also specify path parameters here.
114
+
115
+ Request functions for other methods don’t take an object with query parameters,
116
+ since they already take a request body.
117
+ Instead, if the endpoint still takes query parameters (which is uncommon, but possible),
118
+ you should pass them into the `path` as an object:
119
+
120
+ ```js
121
+ const params = { fakeQueryParam: 'abc' };
122
+ await postForJson( path`/v0/fake/endpoint?${ params }`, new URLSearchParams( {
123
+ fakeBodyParam: 'xyz',
124
+ } ) );
125
+ // makes a request to /v0/fake/endpoint?fakeQueryParam=abc with fakeBodyParam=xyz in the body
126
+ ```
127
+
128
+ This is also possible for GET requests, but you should probably prefer passing the query parameters separately there.
129
+
130
+ ### Body parameters
131
+
132
+ The non-GET request functions, e.g. `postForJson`, take a value for the body after the path.
133
+ The type of the value encodes the content type with which the body will be sent.
134
+ Currently, the only supported type is `URLSearchParams`,
135
+ which will send the body as `application/x-www-form-urlencoded`;
136
+ support for other body types
137
+ (`FormData` for `multipart/form-data`, plain object for `application/json`)
138
+ will be added in a future version.
139
+
140
+ ```js
141
+ const lints = await postForJson( session, '/v1/transform/wikitext/to/lint', new URLSearchParams( {
142
+ wikitext: 'some wikitext',
143
+ } ) );
144
+ /// makes a request to /v1/transform/wikitext/to/lint with wikitext=some%20wikitext
145
+ ```
146
+
147
+ As mentioned above, you can also specify path parameters here.
148
+
149
+ ## License
150
+
151
+ Published under the [ISC License][].
152
+ By contributing to this software,
153
+ you agree to publish your contribution under the same license.
154
+
155
+ [m3api]: https://www.npmjs.com/package/m3api
156
+ [ISC License]: https://spdx.org/licenses/ISC.html
package/index.js ADDED
@@ -0,0 +1,471 @@
1
+ /**
2
+ * An Error representing an HTTP 5xx response from the REST API.
3
+ */
4
+ export class RestApiServerError extends Error {
5
+
6
+ /**
7
+ * @param {number} status The invalid status code received from the API.
8
+ * @param {string|Object} body The response body received from the API.
9
+ */
10
+ constructor( status, body ) {
11
+ super( `REST API server error: ${ status }\n\n${ JSON.stringify( body ) }` );
12
+
13
+ if ( Error.captureStackTrace ) {
14
+ Error.captureStackTrace( this, RestApiServerError );
15
+ }
16
+
17
+ this.name = 'RestApiServerError';
18
+
19
+ /**
20
+ * The invalid status code received from the API.
21
+ *
22
+ * @member {number}
23
+ */
24
+ this.status = status;
25
+
26
+ /**
27
+ * The body of the response.
28
+ *
29
+ * Depending on the response’s content type,
30
+ * this may be a string or a JSON-decoded object.
31
+ *
32
+ * @member {string|Object}
33
+ */
34
+ this.body = body;
35
+ }
36
+
37
+ }
38
+
39
+ /**
40
+ * An Error representing an HTTP 4xx response from the REST API.
41
+ */
42
+ export class RestApiClientError extends Error {
43
+
44
+ /**
45
+ * @param {number} status The invalid status code received from the API.
46
+ * @param {string|Object} body The response body received from the API.
47
+ */
48
+ constructor( status, body ) {
49
+ super( `REST API client error: ${ status }\n\n${ JSON.stringify( body ) }` );
50
+
51
+ if ( Error.captureStackTrace ) {
52
+ Error.captureStackTrace( this, RestApiClientError );
53
+ }
54
+
55
+ this.name = 'RestApiClientError';
56
+
57
+ /**
58
+ * The invalid status code received from the API.
59
+ *
60
+ * @member {number}
61
+ */
62
+ this.status = status;
63
+
64
+ /**
65
+ * The body of the response.
66
+ *
67
+ * Depending on the response’s content type,
68
+ * this may be a string or a JSON-decoded object.
69
+ *
70
+ * @member {string|Object}
71
+ */
72
+ this.body = body;
73
+ }
74
+
75
+ }
76
+
77
+ /**
78
+ * An Error representing an unexpected HTTP response status (1xx or 3xx) from the REST API.
79
+ * 3xx responses are unexpected because the JavaScript runtime should have followed the redirect;
80
+ * 1xx responses are unexpected because it should have awaited the non-informational response.
81
+ */
82
+ export class UnexpectedResponseStatus extends Error {
83
+
84
+ /**
85
+ * @param {number} status The unexpected status code received from the API.
86
+ * @param {string|Object} body The response body received from the API.
87
+ */
88
+ constructor( status, body ) {
89
+ super( `Unexpected REST API response status: ${ status }\n\n${ JSON.stringify( body ) }` );
90
+
91
+ if ( Error.captureStackTrace ) {
92
+ Error.captureStackTrace( this, UnexpectedResponseStatus );
93
+ }
94
+
95
+ this.name = 'UnexpectedResponseStatus';
96
+
97
+ /**
98
+ * The unexpected status code received from the API.
99
+ *
100
+ * @member {number}
101
+ */
102
+ this.status = status;
103
+
104
+ /**
105
+ * The body of the response.
106
+ *
107
+ * Depending on the response’s content type,
108
+ * this may be a string or a JSON-decoded object.
109
+ *
110
+ * @member {string|Object}
111
+ */
112
+ this.body = body;
113
+ }
114
+
115
+ }
116
+
117
+ /**
118
+ * An Error representing an invalid body in the REST API response.
119
+ */
120
+ export class InvalidResponseBody extends Error {
121
+
122
+ /**
123
+ * @param {*} body The invalid response body received from the API.
124
+ */
125
+ constructor( body ) {
126
+ super( `Invalid REST API response body: ${ JSON.stringify( body ) }` );
127
+
128
+ if ( Error.captureStackTrace ) {
129
+ Error.captureStackTrace( this, InvalidResponseBody );
130
+ }
131
+
132
+ this.name = 'InvalidResponseBody';
133
+
134
+ /**
135
+ * The body of the response.
136
+ * Objects and arrays are expected response bodies,
137
+ * so an unexpected body is probably some kind of primitive value,
138
+ * such as a string, number or boolean.
139
+ *
140
+ * @member {*}
141
+ */
142
+ this.body = body;
143
+ }
144
+
145
+ }
146
+
147
+ /**
148
+ * An Error thrown if {@link getResponseStatus} is called with an unknown response.
149
+ *
150
+ * {@link getResponseStatus} must be called with a value that was previously returned
151
+ * by one of the request functions ({@link getJson} etc.).
152
+ * If this error is thrown, then {@link getResponseStatus} was called with some other value,
153
+ * and the response status cannot be determined.
154
+ * This may be the result of calling the function incorrectly.
155
+ *
156
+ * The following example shows how {@link getResponseStatus} can be used correctly:
157
+ * ```
158
+ * const title = 'Main Page';
159
+ * const page = await getJson( session, path`/v1/page/${ title }/bare` );
160
+ * const status = getResponseStatus( page );
161
+ * const { id, latest } = page;
162
+ * ```
163
+ *
164
+ * For comparison, the following usage of {@link getResponseStatus} is **incorrect**:
165
+ * ```
166
+ * const title = 'Main Page';
167
+ * const { id, latest } = await getJson( session, path`/v1/page/${ title }/bare` );
168
+ * const status = getResponseStatus( { id, latest } ); // this does not work
169
+ * ```
170
+ */
171
+ export class UnknownResponseError extends Error {
172
+
173
+ /**
174
+ * @param {*} response The unknown response passed into {@link getResponseStatus}.
175
+ */
176
+ constructor( response ) {
177
+ super( `Unknown REST API response: ${ JSON.stringify( response ) }` );
178
+
179
+ if ( Error.captureStackTrace ) {
180
+ Error.captureStackTrace( this, UnknownResponseError );
181
+ }
182
+
183
+ this.name = 'UnknownResponseError';
184
+
185
+ /**
186
+ * The unknown response passed into {@link getResponseStatus}.
187
+ *
188
+ * @member {*}
189
+ */
190
+ this.response = response;
191
+ }
192
+
193
+ }
194
+
195
+ /**
196
+ * An Error thrown if a request path contains an unsubstituted parameter.
197
+ *
198
+ * For example, if {@link getJson} is called with the path `/v1/page/{title}`,
199
+ * the `title` must be specified in the params, like this:
200
+ * ```
201
+ * const title = 'Main Page';
202
+ * const page = await getJson( session, '/v1/page/{title}', { title } );
203
+ * ```
204
+ *
205
+ * Alternatively, you can directly interpolate the title using {@link path}:
206
+ * ```
207
+ * const title = 'Main Page';
208
+ * const page = await getJson( session, path`/v1/page/${ title }` );
209
+ * ```
210
+ *
211
+ * But the following is invalid and will result in this error being thrown:
212
+ * ```
213
+ * const page = await getJson( session, '/v1/page/{title}' ); // which title?
214
+ * // ^ throws InvalidPathParams
215
+ * ```
216
+ *
217
+ * (Another, less common condition under which this error may be thrown
218
+ * is that a path parameter was assigned multiple values;
219
+ * this is possible with functions like {@link postForJson},
220
+ * where the params of type {@link URLSearchParams} can contain
221
+ * multiple values for the same name.)
222
+ */
223
+ export class InvalidPathParams extends Error {
224
+
225
+ constructor( message, path, paramName, params ) {
226
+ super( message );
227
+
228
+ if ( Error.captureStackTrace ) {
229
+ Error.captureStackTrace( this, InvalidPathParams );
230
+ }
231
+
232
+ this.name = 'InvalidPathParams';
233
+
234
+ this.path = path;
235
+
236
+ this.paramName = paramName;
237
+
238
+ this.params = params;
239
+ }
240
+
241
+ }
242
+
243
+ /**
244
+ * Check the status code of the response and potentially throw an error based on it.
245
+ *
246
+ * @private
247
+ * @param {Object} internalResponse
248
+ */
249
+ function checkResponseStatus( internalResponse ) {
250
+ const { status, body } = internalResponse;
251
+ if ( !Number.isInteger( status ) || status < 100 || status > 599 ) {
252
+ // invalid status: RFC 9110 section 15 says treat it like 5xx
253
+ throw new RestApiServerError( status, body );
254
+ }
255
+ if ( status >= 500 ) {
256
+ throw new RestApiServerError( status, body );
257
+ }
258
+ if ( status >= 400 ) {
259
+ throw new RestApiClientError( status, body );
260
+ }
261
+ if ( status < 200 || status >= 300 ) {
262
+ throw new UnexpectedResponseStatus( status, body );
263
+ }
264
+ }
265
+
266
+ const responseStatuses = new WeakMap();
267
+
268
+ /**
269
+ * Get the HTTP status code for this response.
270
+ *
271
+ * @param {Object|Array} response A response object returned by one of the request functions
272
+ * ({@link getJson} etc.). Note that it must be exactly the object returned by the function
273
+ * (compared by identity, i.e. `===`), not a serialization of it or anything similar.
274
+ * @return {number} The HTTP status code, i.e. an integer between 100 and 599.
275
+ * (And for a successful response, really only between 200 and 299.)
276
+ */
277
+ export function getResponseStatus( response ) {
278
+ const status = responseStatuses.get( response );
279
+ if ( status === undefined ) {
280
+ throw new UnknownResponseError( response );
281
+ }
282
+ return status;
283
+ }
284
+
285
+ /**
286
+ * Get the body of the response and check that it’s valid JSON.
287
+ *
288
+ * @private
289
+ * @param {Object} internalResponse
290
+ * @return {Object|Array}
291
+ */
292
+ function getResponseJson( internalResponse ) {
293
+ const { status, body } = internalResponse;
294
+ if ( typeof body === 'object' && body !== null ) {
295
+ responseStatuses.set( body, status );
296
+ return body;
297
+ } else {
298
+ throw new InvalidResponseBody( body );
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Encode a path for a REST API endpoint.
304
+ *
305
+ * This function should be used as a tag for a tagged template literal (template string).
306
+ * Usage example:
307
+ * ```
308
+ * const title = 'AC/DC';
309
+ * const json = await getJson( session, path`/v1/page/${ title }` );
310
+ * // makes a request to /v1/page/AC%2FDC
311
+ * ```
312
+ *
313
+ * Each parameter in the template can either be a string or an object of query params,
314
+ * for example:
315
+ * ```
316
+ * const id = 376020677;
317
+ * const params = { stash: false, flavor: 'view' };
318
+ * const json = await getJson( session, path`/v1/revision/${ id }/html?${ params }` );
319
+ * ```
320
+ *
321
+ * Alternatively, query params for GET requests can be specified separately:
322
+ * ```
323
+ * const id = 376020677;
324
+ * const json = await getJson( session, path`/v1/revision/${ id }/html`, {
325
+ * stash: false,
326
+ * flavor: 'view',
327
+ * } );
328
+ * ```
329
+ *
330
+ * (Calling this function like a regular function is not very useful,
331
+ * so you can ignore the parameters documented below.)
332
+ *
333
+ * @param {string|object[]} strings
334
+ * @param {Array} values
335
+ * @return {string}
336
+ */
337
+ export function path( strings, ...values ) {
338
+ return String.raw( { raw: strings }, ...values.map( ( value ) => {
339
+ if ( typeof value === 'object' ) {
340
+ return new URLSearchParams( value );
341
+ }
342
+ return encodeURIComponent( value );
343
+ } ) );
344
+ }
345
+
346
+ /**
347
+ * Substitute template expressions in the path,
348
+ * taking values from the given params.
349
+ *
350
+ * @param {string} path The path with optional `{param}` expressions.
351
+ * @param {Object|URLSearchParams} params The input params. Not modified.
352
+ * @return {Object} The potentially modified path and params.
353
+ */
354
+ function substitutePathParams( path, params ) {
355
+ let copiedParams = null;
356
+ const substitutedPath = path.replace( /\{[^{}]+\}/g, ( match ) => {
357
+ const paramName = match.slice( 1, -1 );
358
+ if ( params instanceof URLSearchParams ) {
359
+ if ( copiedParams === null ) {
360
+ copiedParams = new URLSearchParams( params );
361
+ }
362
+ const values = copiedParams.getAll( paramName );
363
+ if ( values.length !== 1 ) {
364
+ throw new InvalidPathParams(
365
+ values.length === 0 ?
366
+ `Unspecified path param ${ match }` :
367
+ `Ambiguous path param ${ match }`,
368
+ path,
369
+ paramName,
370
+ params,
371
+ );
372
+ }
373
+ copiedParams.delete( paramName );
374
+ return encodeURIComponent( values[ 0 ] );
375
+ } else {
376
+ if ( copiedParams === null ) {
377
+ copiedParams = { ...params };
378
+ }
379
+ const value = copiedParams[ paramName ];
380
+ if ( value === undefined ) {
381
+ throw new InvalidPathParams(
382
+ `Unspecified path param ${ match }`,
383
+ path,
384
+ paramName,
385
+ params,
386
+ );
387
+ }
388
+ delete copiedParams[ paramName ];
389
+ return encodeURIComponent( value );
390
+ }
391
+ } );
392
+
393
+ return { path: substitutedPath, params: copiedParams || params };
394
+ }
395
+
396
+ /**
397
+ * Split a given URL for the m3api internal interface.
398
+ *
399
+ * @param {string} url
400
+ * @return {Object}
401
+ */
402
+ function splitUrlForInternalInterface( url ) {
403
+ url = new URL( url );
404
+ const urlParams = {};
405
+ for ( const [ key, value ] of url.searchParams.entries() ) {
406
+ urlParams[ key ] = value;
407
+ }
408
+ url.search = '';
409
+ return { url: String( url ), urlParams };
410
+ }
411
+
412
+ /**
413
+ * Make a GET request to a REST API endpoint and return the JSON-decoded body.
414
+ *
415
+ * @param {Session} session The m3api session to use for this request.
416
+ * @param {string} path The resource path, e.g. `/v1/search`.
417
+ * Does not include the domain, script path, or `rest.php` endpoint.
418
+ * Use the {@link path} tag function to build the path.
419
+ * @param {Object} [params] Parameters for the request URL.
420
+ * This may include both parameters for the path and query parameters.
421
+ * @param {Options} [options] Request options.
422
+ * @return {Object|Array} The body of the API response, JSON-decoded.
423
+ */
424
+ export async function getJson( session, path, params, options = {} ) {
425
+ ( { path, params } = substitutePathParams( path, params ) );
426
+ const restUrl = session.apiUrl.replace( /api\.php$/, 'rest.php' );
427
+ const { url, urlParams } = splitUrlForInternalInterface( restUrl + path );
428
+ params = { ...urlParams, ...params };
429
+ const headers = {
430
+ accept: 'application/json',
431
+ ...session.getRequestHeaders( options ),
432
+ };
433
+ const internalResponse = await session.internalGet( url, params, headers );
434
+ checkResponseStatus( internalResponse );
435
+ return getResponseJson( internalResponse );
436
+ }
437
+
438
+ /**
439
+ * Make a POST request to a REST API endpoint and return the JSON-decoded body.
440
+ *
441
+ * @param {Session} session The m3api session to use for this request.
442
+ * @param {string} path The resourcee path, e.g. `/v1/page`.
443
+ * Does not include the domain, script path, or `rest.php` endpoint.
444
+ * Use the {@link path} tag function to build the path.
445
+ * @param {URLSearchParams} params The request body.
446
+ * Will be sent using the `application/x-www-form-urlencoded` content type.
447
+ * (Future versions of this library will support additional request body content types,
448
+ * but that requires changes to m3api first.)
449
+ * You may also include parameters for the path here.
450
+ * @param {Options} [options] Request options.
451
+ * @return {Object|Array} The body of the API response, JSON-decoded.
452
+ */
453
+ export async function postForJson( session, path, params, options = {} ) {
454
+ ( { path, params } = substitutePathParams( path, params ) );
455
+ const restUrl = session.apiUrl.replace( /api\.php$/, 'rest.php' );
456
+ const { url, urlParams } = splitUrlForInternalInterface( restUrl + path );
457
+ const bodyParams = {};
458
+ for ( const [ key, value ] of params ) {
459
+ if ( Object.prototype.hasOwnProperty.call( bodyParams, key ) ) {
460
+ throw new Error( `Duplicate param name not yet supported: ${ key }` );
461
+ }
462
+ bodyParams[ key ] = value;
463
+ }
464
+ const headers = {
465
+ // accept: 'application/json', // skip this for now due to T412610
466
+ ...session.getRequestHeaders( options ),
467
+ };
468
+ const internalResponse = await session.internalPost( url, urlParams, bodyParams, headers );
469
+ checkResponseStatus( internalResponse );
470
+ return getResponseJson( internalResponse );
471
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "source": {
3
+ "include": [
4
+ "."
5
+ ],
6
+ "exclude": [
7
+ "test"
8
+ ]
9
+ },
10
+ "templates": {
11
+ "default": {
12
+ "layoutFile": "jsdoc/layout.tmpl",
13
+ "staticFiles": {
14
+ "include": [
15
+ "jsdoc/version-warning.css"
16
+ ]
17
+ }
18
+ }
19
+ },
20
+ "opts": {
21
+ "destination": "doc",
22
+ "readme": "README.md"
23
+ },
24
+ "plugins": [
25
+ "plugins/markdown"
26
+ ]
27
+ }
@@ -0,0 +1,38 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>m3api-rest: <?js= title ?></title>
6
+
7
+ <script defer src="scripts/prettify/prettify.js"></script>
8
+ <script defer src="scripts/prettify/lang-css.js"></script>
9
+ <link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
10
+ <link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
11
+ <link type="text/css" rel="stylesheet" href="version-warning.css">
12
+ </head>
13
+
14
+ <body>
15
+
16
+ <div id="main">
17
+
18
+ <!-- VERSION-WARNING -->
19
+
20
+ <h1 class="page-title"><?js= title ?></h1>
21
+
22
+ <?js= content ?>
23
+ </div>
24
+
25
+ <nav>
26
+ <?js= this.nav ?>
27
+ </nav>
28
+
29
+ <br class="clear">
30
+
31
+ <footer>
32
+ Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc <?js= env.version.number ?></a>
33
+ </footer>
34
+
35
+ <script>document.addEventListener('DOMContentLoaded', function () { prettyPrint(); });</script>
36
+ <script async src="scripts/linenumber.js"></script>
37
+ </body>
38
+ </html>
@@ -0,0 +1,27 @@
1
+ body {
2
+ --body-margin: 8px; /* copied from Firefox */
3
+ margin: var( --body-margin );
4
+ }
5
+
6
+ .version-warning {
7
+ --version-warning-margin-y: 2px;
8
+ --version-warning-margin-x: 24px; /* matching other doc elements */
9
+ top: calc(var(--body-margin) + var(--version-warning-margin-y));
10
+ position: sticky;
11
+ padding: 0.5em 3em;
12
+ margin: var(--version-warning-margin-y) var(--version-warning-margin-x);
13
+ border-radius: 4px;
14
+ text-align: center;
15
+ font-size: 16px; /* absolute px to match other doc elements */
16
+ color: #fff; /* Base100 */
17
+ background: repeating-linear-gradient(
18
+ 135deg,
19
+ #36c 0px 56px, /* Accent50 */
20
+ #2a4b8d 56px 112px /* Accent30 */
21
+ );
22
+ }
23
+
24
+ .version-warning a {
25
+ color: #fff; /* Base100 */
26
+ text-decoration: underline;
27
+ }
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "m3api-rest",
3
+ "version": "0.1.0",
4
+ "description": "m3api extension package to interact with the MediaWiki REST API.",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "scripts": {
8
+ "test": "npm-run-all test:*",
9
+ "test:lint": "eslint .",
10
+ "test:unit": "mocha test/unit/",
11
+ "test:integration": "mocha test/integration/",
12
+ "doc": "jsdoc -c jsdoc/conf.json"
13
+ },
14
+ "homepage": "https://gitlab.wikimedia.org/repos/m3api/m3api-rest#m3api-rest",
15
+ "bugs": "https://phabricator.wikimedia.org/tag/m3api/",
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://gitlab.wikimedia.org/repos/m3api/m3api-rest.git"
19
+ },
20
+ "keywords": [
21
+ "mediawiki"
22
+ ],
23
+ "author": "Lucas Werkmeister <mail@lucaswerkmeister.de>",
24
+ "license": "ISC",
25
+ "engines": {
26
+ "node": ">=18.2.0"
27
+ },
28
+ "peerDependencies": {
29
+ "m3api": "~1.0.0"
30
+ },
31
+ "devDependencies": {
32
+ "chai": "^6.0.1",
33
+ "chai-as-promised": "^8.0.1",
34
+ "chai-string": "^2.0.0",
35
+ "eslint": "^8.44.0",
36
+ "eslint-config-wikimedia": "^0.31.0",
37
+ "eslint-plugin-chai-friendly": "^1.0.1",
38
+ "jsdoc": "^4.0.3",
39
+ "m3api": "~1.0.0",
40
+ "mocha": "^11.1.0",
41
+ "npm-run-all": "^4.1.5"
42
+ },
43
+ "mocha": {
44
+ "recursive": true
45
+ }
46
+ }