m3api-rest 0.1.0 → 0.1.1
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.
- package/CHANGES.md +10 -0
- package/index.js +129 -52
- package/package.json +3 -3
package/CHANGES.md
CHANGED
|
@@ -5,6 +5,16 @@ This file records the changes in each m3api-rest release.
|
|
|
5
5
|
The annotated tag (and GitLab release) for each version also lists the changes,
|
|
6
6
|
but this file may sometimes contain later improvements (e.g. typo fixes).
|
|
7
7
|
|
|
8
|
+
## v0.1.1 (2026-04-05)
|
|
9
|
+
|
|
10
|
+
- Updated the library for the new network interface of m3api v1.1.0,
|
|
11
|
+
so that it can be used together with that version.
|
|
12
|
+
(This also unlocks many new features in m3api-rest,
|
|
13
|
+
which will be implemented in a subsequent release.)
|
|
14
|
+
- Introduced a new error type, `IncompatibleResponseType`,
|
|
15
|
+
thrown if the server responds with an incompatible response type.
|
|
16
|
+
- Updated dependencies.
|
|
17
|
+
|
|
8
18
|
## v0.1.0 (2026-01-01)
|
|
9
19
|
|
|
10
20
|
Initial release, including:
|
package/index.js
CHANGED
|
@@ -114,6 +114,58 @@ export class UnexpectedResponseStatus extends Error {
|
|
|
114
114
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
/**
|
|
118
|
+
* An error representing a HTTP response with a content type incompatible with that requested.
|
|
119
|
+
* For example, if the API responded with HTML despite JSON having been requested.
|
|
120
|
+
* Note that, depending on server behavior, this condition may also be represented
|
|
121
|
+
* by a {@link RestApiClientError} with the status 406 Not Acceptable;
|
|
122
|
+
* {@link IncompatibleResponseType} is thrown by m3api-rest if the server responded
|
|
123
|
+
* with a successful status that nevertheless does not match the requested type.
|
|
124
|
+
*/
|
|
125
|
+
export class IncompatibleResponseType extends Error {
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {string} expectedType The expected MIME type.
|
|
129
|
+
* @param {string} actualType The actual Content-Type response header.
|
|
130
|
+
* @param {*} body The response body received from the API.
|
|
131
|
+
*/
|
|
132
|
+
constructor( expectedType, actualType, body ) {
|
|
133
|
+
super( `Incompatible REST API response type: expected ${ expectedType }, got ${ actualType }` );
|
|
134
|
+
|
|
135
|
+
if ( Error.captureStackTrace ) {
|
|
136
|
+
Error.captureStackTrace( this, IncompatibleResponseType );
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.name = 'IncompatibleResponseType';
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The MIME type expected by m3api-rest based on the method,
|
|
143
|
+
* e.g. 'application/json' for {@link getJson} and related functions.
|
|
144
|
+
*
|
|
145
|
+
* @member {string}
|
|
146
|
+
*/
|
|
147
|
+
this.expectedType = expectedType;
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The actual Content-Type header returned by the server,
|
|
151
|
+
* e.g. 'text/html; charset=utf-8; profile="https://www.mediawiki.org/wiki/Specs/HTML/2.8.0"'.
|
|
152
|
+
*
|
|
153
|
+
* @member {string}
|
|
154
|
+
*/
|
|
155
|
+
this.actualType = actualType;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* The response body.
|
|
159
|
+
* The type of this member will depend on the Content-Type returned by the server
|
|
160
|
+
* and on whether m3api-rest is able to decode that content type or not.
|
|
161
|
+
*
|
|
162
|
+
* @member {*}
|
|
163
|
+
*/
|
|
164
|
+
this.body = body;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
117
169
|
/**
|
|
118
170
|
* An Error representing an invalid body in the REST API response.
|
|
119
171
|
*/
|
|
@@ -241,25 +293,55 @@ export class InvalidPathParams extends Error {
|
|
|
241
293
|
}
|
|
242
294
|
|
|
243
295
|
/**
|
|
244
|
-
*
|
|
296
|
+
* Determine whether this response contains JSON
|
|
297
|
+
* according to its headers.
|
|
245
298
|
*
|
|
246
299
|
* @private
|
|
247
|
-
* @param {
|
|
300
|
+
* @param {Response} response
|
|
301
|
+
* @return {boolean}
|
|
248
302
|
*/
|
|
249
|
-
function
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
303
|
+
function isResponseJson( response ) {
|
|
304
|
+
const contentType = response.headers.get( 'Content-Type' );
|
|
305
|
+
const [ mimeType ] = contentType.split( ';', 1 ); // split off ;charset=utf-8 and suchlike
|
|
306
|
+
return mimeType === 'application/json' ||
|
|
307
|
+
( mimeType.startsWith( 'application/' ) && mimeType.endsWith( '+json' ) );
|
|
308
|
+
// this accepts some technically invalid Content-Type headers
|
|
309
|
+
// (we don’t check if the part before +json is a valid subtype / token,
|
|
310
|
+
// cf. https://httpwg.org/specs/rfc9110.html#media.type)
|
|
311
|
+
// because there’s no reason to reject them
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get a version of the body of the response to put in an error.
|
|
316
|
+
*
|
|
317
|
+
* @private
|
|
318
|
+
* @param {Response} response
|
|
319
|
+
* @return {string|Object}
|
|
320
|
+
*/
|
|
321
|
+
async function getResponseBodyForError( response ) {
|
|
322
|
+
if ( isResponseJson( response ) ) {
|
|
323
|
+
return await response.json();
|
|
324
|
+
} else {
|
|
325
|
+
return await response.text();
|
|
254
326
|
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Check the status code of the response and potentially throw an error based on it.
|
|
331
|
+
*
|
|
332
|
+
* @private
|
|
333
|
+
* @param {Response} response
|
|
334
|
+
*/
|
|
335
|
+
async function checkResponseStatus( response ) {
|
|
336
|
+
const { status } = response;
|
|
255
337
|
if ( status >= 500 ) {
|
|
256
|
-
throw new RestApiServerError( status,
|
|
338
|
+
throw new RestApiServerError( status, await getResponseBodyForError( response ) );
|
|
257
339
|
}
|
|
258
340
|
if ( status >= 400 ) {
|
|
259
|
-
throw new RestApiClientError( status,
|
|
341
|
+
throw new RestApiClientError( status, await getResponseBodyForError( response ) );
|
|
260
342
|
}
|
|
261
|
-
if ( status
|
|
262
|
-
throw new UnexpectedResponseStatus( status,
|
|
343
|
+
if ( status >= 300 ) {
|
|
344
|
+
throw new UnexpectedResponseStatus( status, await getResponseBodyForError( response ) );
|
|
263
345
|
}
|
|
264
346
|
}
|
|
265
347
|
|
|
@@ -271,7 +353,7 @@ const responseStatuses = new WeakMap();
|
|
|
271
353
|
* @param {Object|Array} response A response object returned by one of the request functions
|
|
272
354
|
* ({@link getJson} etc.). Note that it must be exactly the object returned by the function
|
|
273
355
|
* (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
|
|
356
|
+
* @return {number} The HTTP status code, i.e. an integer between 200 and 599.
|
|
275
357
|
* (And for a successful response, really only between 200 and 299.)
|
|
276
358
|
*/
|
|
277
359
|
export function getResponseStatus( response ) {
|
|
@@ -286,16 +368,24 @@ export function getResponseStatus( response ) {
|
|
|
286
368
|
* Get the body of the response and check that it’s valid JSON.
|
|
287
369
|
*
|
|
288
370
|
* @private
|
|
289
|
-
* @param {
|
|
371
|
+
* @param {Response} response
|
|
290
372
|
* @return {Object|Array}
|
|
291
373
|
*/
|
|
292
|
-
function getResponseJson(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
374
|
+
async function getResponseJson( response ) {
|
|
375
|
+
if ( isResponseJson( response ) ) {
|
|
376
|
+
const body = await response.json();
|
|
377
|
+
if ( typeof body === 'object' && body !== null ) {
|
|
378
|
+
responseStatuses.set( body, response.status );
|
|
379
|
+
return body;
|
|
380
|
+
} else {
|
|
381
|
+
throw new InvalidResponseBody( body );
|
|
382
|
+
}
|
|
297
383
|
} else {
|
|
298
|
-
throw new
|
|
384
|
+
throw new IncompatibleResponseType(
|
|
385
|
+
'application/json',
|
|
386
|
+
response.headers.get( 'Content-Type' ),
|
|
387
|
+
await response.text(),
|
|
388
|
+
);
|
|
299
389
|
}
|
|
300
390
|
}
|
|
301
391
|
|
|
@@ -347,6 +437,7 @@ export function path( strings, ...values ) {
|
|
|
347
437
|
* Substitute template expressions in the path,
|
|
348
438
|
* taking values from the given params.
|
|
349
439
|
*
|
|
440
|
+
* @private
|
|
350
441
|
* @param {string} path The path with optional `{param}` expressions.
|
|
351
442
|
* @param {Object|URLSearchParams} params The input params. Not modified.
|
|
352
443
|
* @return {Object} The potentially modified path and params.
|
|
@@ -393,22 +484,6 @@ function substitutePathParams( path, params ) {
|
|
|
393
484
|
return { path: substitutedPath, params: copiedParams || params };
|
|
394
485
|
}
|
|
395
486
|
|
|
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
487
|
/**
|
|
413
488
|
* Make a GET request to a REST API endpoint and return the JSON-decoded body.
|
|
414
489
|
*
|
|
@@ -424,22 +499,27 @@ function splitUrlForInternalInterface( url ) {
|
|
|
424
499
|
export async function getJson( session, path, params, options = {} ) {
|
|
425
500
|
( { path, params } = substitutePathParams( path, params ) );
|
|
426
501
|
const restUrl = session.apiUrl.replace( /api\.php$/, 'rest.php' );
|
|
427
|
-
const
|
|
428
|
-
|
|
502
|
+
const url = new URL( restUrl + path );
|
|
503
|
+
for ( const [ name, value ] of Object.entries( params || {} ) ) {
|
|
504
|
+
url.searchParams.append( name, value );
|
|
505
|
+
}
|
|
429
506
|
const headers = {
|
|
430
507
|
accept: 'application/json',
|
|
431
508
|
...session.getRequestHeaders( options ),
|
|
432
509
|
};
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
510
|
+
const response = await session.fetch( url, {
|
|
511
|
+
method: 'GET',
|
|
512
|
+
headers,
|
|
513
|
+
} );
|
|
514
|
+
await checkResponseStatus( response );
|
|
515
|
+
return await getResponseJson( response );
|
|
436
516
|
}
|
|
437
517
|
|
|
438
518
|
/**
|
|
439
519
|
* Make a POST request to a REST API endpoint and return the JSON-decoded body.
|
|
440
520
|
*
|
|
441
521
|
* @param {Session} session The m3api session to use for this request.
|
|
442
|
-
* @param {string} path The
|
|
522
|
+
* @param {string} path The resource path, e.g. `/v1/page`.
|
|
443
523
|
* Does not include the domain, script path, or `rest.php` endpoint.
|
|
444
524
|
* Use the {@link path} tag function to build the path.
|
|
445
525
|
* @param {URLSearchParams} params The request body.
|
|
@@ -453,19 +533,16 @@ export async function getJson( session, path, params, options = {} ) {
|
|
|
453
533
|
export async function postForJson( session, path, params, options = {} ) {
|
|
454
534
|
( { path, params } = substitutePathParams( path, params ) );
|
|
455
535
|
const restUrl = session.apiUrl.replace( /api\.php$/, 'rest.php' );
|
|
456
|
-
const
|
|
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
|
-
}
|
|
536
|
+
const url = new URL( restUrl + path );
|
|
464
537
|
const headers = {
|
|
465
538
|
// accept: 'application/json', // skip this for now due to T412610
|
|
466
539
|
...session.getRequestHeaders( options ),
|
|
467
540
|
};
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
|
|
541
|
+
const response = await session.fetch( url, {
|
|
542
|
+
method: 'POST',
|
|
543
|
+
headers,
|
|
544
|
+
body: params,
|
|
545
|
+
} );
|
|
546
|
+
await checkResponseStatus( response );
|
|
547
|
+
return await getResponseJson( response );
|
|
471
548
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "m3api-rest",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "m3api extension package to interact with the MediaWiki REST API.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"node": ">=18.2.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
|
-
"m3api": "~1.
|
|
29
|
+
"m3api": "~1.1.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"chai": "^6.0.1",
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"eslint-config-wikimedia": "^0.31.0",
|
|
37
37
|
"eslint-plugin-chai-friendly": "^1.0.1",
|
|
38
38
|
"jsdoc": "^4.0.3",
|
|
39
|
-
"m3api": "~1.
|
|
39
|
+
"m3api": "~1.1.0",
|
|
40
40
|
"mocha": "^11.1.0",
|
|
41
41
|
"npm-run-all": "^4.1.5"
|
|
42
42
|
},
|