m3api-rest 0.1.0 → 0.2.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.
Files changed (4) hide show
  1. package/CHANGES.md +61 -0
  2. package/README.md +170 -34
  3. package/index.js +774 -74
  4. package/package.json +3 -3
package/CHANGES.md CHANGED
@@ -5,6 +5,67 @@ 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.2.0 (2026-04-05)
9
+
10
+ - Major functionality update:
11
+ beyond `getJson()` and `postForJson()`,
12
+ m3api-rest now supports the following:
13
+ - request methods:
14
+ - GET
15
+ - POST
16
+ - PUT
17
+ - DELETE
18
+ - PATCH
19
+ - request body content types:
20
+ - `application/json`
21
+ - `application/x-www-form-urlencoded`
22
+ - `multipart/form-data`
23
+ - response body content types:
24
+ - `application/json`
25
+ - `text/plain`
26
+ - `text/html`
27
+
28
+ Yielding the following full list of request functions:
29
+ - `getJson()`
30
+ - `getText()`
31
+ - `getHtml()`
32
+ - `postForJson()`
33
+ - `postForText()`
34
+ - `postForHtml()`
35
+ - `putForJson()`
36
+ - `putForText()`
37
+ - `putForHtml()`
38
+ - `deleteForJson()`
39
+ - `deleteForText()`
40
+ - `deleteForHtml()`
41
+ - `patchForJson()`
42
+ - `patchForText()`
43
+ - `patchForHtml()`
44
+ - BREAKING CHANGE:
45
+ `getJson()` and `postForJson()` can now return `String` instances
46
+ in addition to objects and arrays,
47
+ if the server returns JSON-encoded strings.
48
+ (This is unlikely for MediaWiki core’s REST API endpoints,
49
+ but common in the Wikibase REST API.)
50
+ - BREAKING CHANGE:
51
+ The `body` property of the `RestApiServerError`,
52
+ `RestApiClientError` and `UnexpectedResponseStatus` classes
53
+ is now documented with the type `*` (“any”) rather than `string|Object`.
54
+ The previous type was incorrect – if the server response is JSON,
55
+ m3api-rest just decodes it for the error without checking if it’s an object or not.
56
+ (You almost certainly don’t need to care about this change,
57
+ but it’s technically breaking.)
58
+
59
+ ## v0.1.1 (2026-04-05)
60
+
61
+ - Updated the library for the new network interface of m3api v1.1.0,
62
+ so that it can be used together with that version.
63
+ (This also unlocks many new features in m3api-rest,
64
+ which will be implemented in a subsequent release.)
65
+ - Introduced a new error type, `IncompatibleResponseType`,
66
+ thrown if the server responds with an incompatible response type.
67
+ - Updated dependencies.
68
+
8
69
  ## v0.1.0 (2026-01-01)
9
70
 
10
71
  Initial release, including:
package/README.md CHANGED
@@ -3,22 +3,14 @@
3
3
  m3api-rest is an extension package for [m3api],
4
4
  allowing you to interact with the MediaWiki REST API.
5
5
 
6
- ## Request functions
6
+ ## Usage examples
7
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.)
8
+ Before digging into all the available features in m3api-rest,
9
+ let’s start with some examples for commonly used functions.
17
10
 
18
11
  ### `getJson`
19
12
 
20
13
  Make a GET request for some JSON data.
21
- Usage examples:
22
14
 
23
15
  ```js
24
16
  import Session from 'm3api';
@@ -46,7 +38,23 @@ See also below for details on the different ways to specify parameters.
46
38
  ### `postForJson`
47
39
 
48
40
  Make a POST request that will return some JSON data.
49
- Usage example:
41
+
42
+ ```js
43
+ const session = new Session( 'test.wikipedia.org', {}, {
44
+ userAgent: 'm3api-rest-README-example',
45
+ accessToken: ...,
46
+ } );
47
+
48
+ const title = 'Test page',
49
+ source = 'Test page contents',
50
+ comment = 'test edit';
51
+ const response = await postForJson( session, '/v1/page', {
52
+ title,
53
+ source,
54
+ comment,
55
+ } );
56
+ console.log( `Created new page with page ID ${ response.id }` );
57
+ ```
50
58
 
51
59
  ```js
52
60
  const wikitext = '== ==\nThis is <span id=id>bad</span> <span id=id>wikitext</span>.';
@@ -56,12 +64,112 @@ const lints = await postForJson( session, '/v1/transform/wikitext/to/lint', new
56
64
  console.log( lints ); // [ { type: 'empty-heading', ... }, { type: 'duplicate-ids', ... } ]
57
65
  ```
58
66
 
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.)
67
+ ### `putForJson`
68
+
69
+ Make a PUT request that will return some JSON data.
70
+
71
+ ```js
72
+ // session should still have an accessToken, like in the previous example
73
+ const title = 'Test page';
74
+ const page = await getJson( session, path`/v1/page/${ title }` );
75
+ const updatedPage = await putForJson( session, path`/v1/page/${ title }`, {
76
+ source: page.source.replace( /privledge/g, 'privilege' ), // example typo fix
77
+ comment: 'test edit',
78
+ content_model: page.content_model,
79
+ latest: page.latest,
80
+ } );
81
+ console.log( `Edited page with revision ID ${ updatedPage.latest.id }` );
82
+ ```
83
+
84
+ ### `getHtml`
85
+
86
+ Make a GET request that will return HTML.
87
+ The HTML is returned as a `String`;
88
+ if you want to turn it into a DOM, you will need to parse it yourself,
89
+ e.g. using a [`DOMParser`][] in the browser or [jsdom][] on Node.js.
90
+
91
+ ```js
92
+ const title = 'Douglas Adams';
93
+ const html = await getHtml( session, path`/v1/page/${ title }/html` );
94
+ const dom = new DOMParser().parseFromString( html, 'text/html' );
95
+ const rudimentaryLeadParagraph = dom.querySelector( 'p:not( .mw-empty-elt )' );
96
+ ```
97
+
98
+ ### `postForText`, `postForHtml`
99
+
100
+ Make GET requests that will return text or HTML, respectively.
101
+ As with `getHtml`, the result is returned as a `String`.
102
+ You can use these functions to convert between wikitext and HTML, for instance.
103
+
104
+ ```js
105
+ const wikitext = "''Hello, world!''";
106
+ const html = await postForHtml( session, '/v1/transform/wikitext/to/html', {
107
+ wikitext,
108
+ } );
109
+ console.log( html.valueOf() );
110
+ // <!DOCTYPE html>...<i ...>Hello, world!</i>
111
+ const wikitext2 = await postForText( session, '/v1/transform/html/to/wikitext', {
112
+ html,
113
+ } );
114
+ console.log( wikitext2.valueOf() );
115
+ // ''Hello, world!''
116
+ ```
117
+
118
+ ## Request functions
63
119
 
64
- ## Specifying parameters
120
+ m3api-rest supports the following:
121
+
122
+ - request methods:
123
+ - GET
124
+ - POST
125
+ - PUT
126
+ - DELETE
127
+ - PATCH
128
+ - request body content types:
129
+ - `application/json`
130
+ - `application/x-www-form-urlencoded`
131
+ - `multipart/form-data`
132
+ - response body content types:
133
+ - `application/json`
134
+ - `text/plain`
135
+ - `text/html`
136
+
137
+ ### Request methods and response body content types
138
+
139
+ Each combination of request method and response body content type has a separate function,
140
+ whose name begins with the HTTP method name (in lowercase).
141
+ For GET functions, this is directly followed by the response type (e.g. `getJson`),
142
+ while other functions have the word “for” in between (e.g. `postForJson`)
143
+ to clarify that this is the response body content type, not the request body content type.
144
+ The response body content types are “json”, “text”, or “html”,
145
+ and included in the method name with an initial uppercase letter
146
+ (e.g. `getJson`, `postForText`, `postForHtml`).
147
+ Overall, the following functions are available:
148
+
149
+ - `getJson`
150
+ - `getText`
151
+ - `getHtml`
152
+ - `postForJson`
153
+ - `postForText`
154
+ - `postForHtml`
155
+ - `putForJson`
156
+ - `putForText`
157
+ - `putForHtml`
158
+ - `deleteForJson`
159
+ - `deleteForText`
160
+ - `deleteForHtml`
161
+ - `patchForJson`
162
+ - `patchForText`
163
+ - `patchForHtml`
164
+
165
+ ### Request body content types
166
+
167
+ The request body type for non-GET request functions is determined
168
+ by the type of the object passed into the function,
169
+ i.e. the same function (e.g. `postForJson`) can send request bodies encoded in different ways.
170
+ For details, see the section on “body parameters” below.
171
+
172
+ ### Specifying parameters
65
173
 
66
174
  REST API endpoints have different kinds of parameters.
67
175
  Many endpoints take parameters in the path, written like `/v1/page/{title}` (`title` is a parameter);
@@ -69,7 +177,7 @@ many endpoints take query parameters after the path, like `/v1/search/page?q=sea
69
177
  and non-GET endpoints (POST etc.) take body parameters in several encodings.
70
178
  m3api-rest supports several ways to specify these parameters.
71
179
 
72
- ### Path parameters
180
+ #### Path parameters
73
181
 
74
182
  Path parameters can be specified using a tagged template literal (string template)
75
183
  using the `path` function,
@@ -98,7 +206,7 @@ const page = await getJson( session, '/v1/page/{title}', {
98
206
  // makes a request to /v1/page/AC%2FDC?redirect=true
99
207
  ```
100
208
 
101
- ### Query parameters
209
+ #### Query parameters
102
210
 
103
211
  The GET request functions, e.g. `getJson`, take an object with query parameters after the path:
104
212
 
@@ -119,31 +227,57 @@ you should pass them into the `path` as an object:
119
227
 
120
228
  ```js
121
229
  const params = { fakeQueryParam: 'abc' };
122
- await postForJson( path`/v0/fake/endpoint?${ params }`, new URLSearchParams( {
230
+ await postForJson( path`/v0/fake/endpoint?${ params }`, {
123
231
  fakeBodyParam: 'xyz',
124
- } ) );
125
- // makes a request to /v0/fake/endpoint?fakeQueryParam=abc with fakeBodyParam=xyz in the body
232
+ } );
233
+ // makes a request to /v0/fake/endpoint?fakeQueryParam=abc with {"fakeBodyParam":"xyz"} in the body
126
234
  ```
127
235
 
128
236
  This is also possible for GET requests, but you should probably prefer passing the query parameters separately there.
129
237
 
130
- ### Body parameters
238
+ #### Body parameters
131
239
 
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.
240
+ The non-GET request functions, e.g. `postForJson`, take a value for the request body after the path.
241
+ The type of the value encodes the content type with which the request body will be sent:
242
+ a plain object is sent as `application/json`;
243
+ a `URLSearchParams` instance is sent as `application/x-www-form-urlencoded`;
244
+ and a `FormData` instance is sent as `multipart/form-data`.
139
245
 
140
246
  ```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
247
+ const wikitext = "''Hello, world!''";
248
+
249
+ // send as application/json
250
+ const html = await postForHtml(
251
+ session,
252
+ '/v1/transform/wikitext/to/html',
253
+ {
254
+ wikitext,
255
+ },
256
+ );
257
+
258
+ // send as application/x-www-form-urlencoded
259
+ const html = await postForHtml(
260
+ session,
261
+ '/v1/transform/wikitext/to/html',
262
+ new URLSearchParams( {
263
+ wikitext,
264
+ } ),
265
+ );
266
+
267
+ // send as multipart/form-data
268
+ const formData = new FormData();
269
+ formData.set( 'wikitext', wikitext );
270
+ const html = await postForHtml(
271
+ session,
272
+ '/v1/transform/wikitext/to/html',
273
+ formData,
274
+ );
145
275
  ```
146
276
 
277
+ Note that not all REST API endpoints accept all request body content types.
278
+ Generally speaking, JSON is your safest bet,
279
+ but you should consult the API endpoint’s documentation to see what request bodies it accepts.
280
+
147
281
  As mentioned above, you can also specify path parameters here.
148
282
 
149
283
  ## License
@@ -153,4 +287,6 @@ By contributing to this software,
153
287
  you agree to publish your contribution under the same license.
154
288
 
155
289
  [m3api]: https://www.npmjs.com/package/m3api
290
+ [`DOMParser`]: https://developer.mozilla.org/en-US/docs/Web/API/DOMParser
291
+ [jsdom]: https://www.npmjs.com/package/jsdom
156
292
  [ISC License]: https://spdx.org/licenses/ISC.html
package/index.js CHANGED
@@ -5,7 +5,7 @@ export class RestApiServerError extends Error {
5
5
 
6
6
  /**
7
7
  * @param {number} status The invalid status code received from the API.
8
- * @param {string|Object} body The response body received from the API.
8
+ * @param {*} body The response body received from the API.
9
9
  */
10
10
  constructor( status, body ) {
11
11
  super( `REST API server error: ${ status }\n\n${ JSON.stringify( body ) }` );
@@ -27,9 +27,9 @@ export class RestApiServerError extends Error {
27
27
  * The body of the response.
28
28
  *
29
29
  * Depending on the response’s content type,
30
- * this may be a string or a JSON-decoded object.
30
+ * this may be a string or a JSON-decoded value.
31
31
  *
32
- * @member {string|Object}
32
+ * @member {*}
33
33
  */
34
34
  this.body = body;
35
35
  }
@@ -43,7 +43,7 @@ export class RestApiClientError extends Error {
43
43
 
44
44
  /**
45
45
  * @param {number} status The invalid status code received from the API.
46
- * @param {string|Object} body The response body received from the API.
46
+ * @param {*} body The response body received from the API.
47
47
  */
48
48
  constructor( status, body ) {
49
49
  super( `REST API client error: ${ status }\n\n${ JSON.stringify( body ) }` );
@@ -65,9 +65,9 @@ export class RestApiClientError extends Error {
65
65
  * The body of the response.
66
66
  *
67
67
  * Depending on the response’s content type,
68
- * this may be a string or a JSON-decoded object.
68
+ * this may be a string or a JSON-decoded value.
69
69
  *
70
- * @member {string|Object}
70
+ * @member {*}
71
71
  */
72
72
  this.body = body;
73
73
  }
@@ -83,7 +83,7 @@ export class UnexpectedResponseStatus extends Error {
83
83
 
84
84
  /**
85
85
  * @param {number} status The unexpected status code received from the API.
86
- * @param {string|Object} body The response body received from the API.
86
+ * @param {*} body The response body received from the API.
87
87
  */
88
88
  constructor( status, body ) {
89
89
  super( `Unexpected REST API response status: ${ status }\n\n${ JSON.stringify( body ) }` );
@@ -105,9 +105,61 @@ export class UnexpectedResponseStatus extends Error {
105
105
  * The body of the response.
106
106
  *
107
107
  * Depending on the response’s content type,
108
- * this may be a string or a JSON-decoded object.
108
+ * this may be a string or a JSON-decoded value.
109
109
  *
110
- * @member {string|Object}
110
+ * @member {*}
111
+ */
112
+ this.body = body;
113
+ }
114
+
115
+ }
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 {*}
111
163
  */
112
164
  this.body = body;
113
165
  }
@@ -241,25 +293,97 @@ export class InvalidPathParams extends Error {
241
293
  }
242
294
 
243
295
  /**
244
- * Check the status code of the response and potentially throw an error based on it.
296
+ * Get the MIME type (e.g. application/json) of the given response.
297
+ *
298
+ * @private
299
+ * @param {Response} response
300
+ * @return {string} The mime type part of the Content-Type response header,
301
+ * not including parameters like ;charset=utf-8
302
+ */
303
+ function getResponseMimeType( response ) {
304
+ const contentType = response.headers.get( 'Content-Type' );
305
+ const [ mimeType ] = contentType.split( ';', 1 );
306
+ // this accepts some technically invalid Content-Type headers
307
+ // (we don’t check if the part before ; is a valid media-type,
308
+ // i.e. two tokens separated by a slash,
309
+ // cf. https://httpwg.org/specs/rfc9110.html#media.type)
310
+ // because there’s no reason to reject them
311
+ return mimeType;
312
+ }
313
+
314
+ /**
315
+ * Determine whether this response contains JSON
316
+ * according to its headers.
245
317
  *
246
318
  * @private
247
- * @param {Object} internalResponse
319
+ * @param {Response} response
320
+ * @return {boolean}
248
321
  */
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 );
322
+ function isResponseJson( response ) {
323
+ const mimeType = getResponseMimeType( response );
324
+ return mimeType === 'application/json' ||
325
+ ( mimeType.startsWith( 'application/' ) && mimeType.endsWith( '+json' ) );
326
+ }
327
+
328
+ /**
329
+ * Determine whether this response contains text
330
+ * (“plain text”, but in practice often wikitext) according to its headers.
331
+ *
332
+ * @private
333
+ * @param {Response} response
334
+ * @return {boolean}
335
+ */
336
+ function isResponseText( response ) {
337
+ const mimeType = getResponseMimeType( response );
338
+ return mimeType === 'text/plain' ||
339
+ ( mimeType.startsWith( 'text/' ) && mimeType.endsWith( '+plain' ) );
340
+ }
341
+
342
+ /**
343
+ * Determine whether this response contains HTML
344
+ * according to its headers.
345
+ *
346
+ * @private
347
+ * @param {Response} response
348
+ * @return {boolean}
349
+ */
350
+ function isResponseHtml( response ) {
351
+ const mimeType = getResponseMimeType( response );
352
+ return mimeType === 'text/html' ||
353
+ ( mimeType.startsWith( 'text/' ) && mimeType.endsWith( '+html' ) );
354
+ }
355
+
356
+ /**
357
+ * Get a version of the body of the response to put in an error.
358
+ *
359
+ * @private
360
+ * @param {Response} response
361
+ * @return {*}
362
+ */
363
+ async function getResponseBodyForError( response ) {
364
+ if ( isResponseJson( response ) ) {
365
+ return await response.json();
366
+ } else {
367
+ return await response.text();
254
368
  }
369
+ }
370
+
371
+ /**
372
+ * Check the status code of the response and potentially throw an error based on it.
373
+ *
374
+ * @private
375
+ * @param {Response} response
376
+ */
377
+ async function checkResponseStatus( response ) {
378
+ const { status } = response;
255
379
  if ( status >= 500 ) {
256
- throw new RestApiServerError( status, body );
380
+ throw new RestApiServerError( status, await getResponseBodyForError( response ) );
257
381
  }
258
382
  if ( status >= 400 ) {
259
- throw new RestApiClientError( status, body );
383
+ throw new RestApiClientError( status, await getResponseBodyForError( response ) );
260
384
  }
261
- if ( status < 200 || status >= 300 ) {
262
- throw new UnexpectedResponseStatus( status, body );
385
+ if ( status >= 300 ) {
386
+ throw new UnexpectedResponseStatus( status, await getResponseBodyForError( response ) );
263
387
  }
264
388
  }
265
389
 
@@ -268,10 +392,10 @@ const responseStatuses = new WeakMap();
268
392
  /**
269
393
  * Get the HTTP status code for this response.
270
394
  *
271
- * @param {Object|Array} response A response object returned by one of the request functions
395
+ * @param {Object|Array|String} response A response object returned by one of the request functions
272
396
  * ({@link getJson} etc.). Note that it must be exactly the object returned by the function
273
397
  * (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.
398
+ * @return {number} The HTTP status code, i.e. an integer between 200 and 599.
275
399
  * (And for a successful response, really only between 200 and 299.)
276
400
  */
277
401
  export function getResponseStatus( response ) {
@@ -286,16 +410,71 @@ export function getResponseStatus( response ) {
286
410
  * Get the body of the response and check that it’s valid JSON.
287
411
  *
288
412
  * @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;
413
+ * @param {Response} response
414
+ * @return {Object|Array|String}
415
+ */
416
+ async function getResponseJson( response ) {
417
+ if ( isResponseJson( response ) ) {
418
+ const body = await response.json();
419
+ let bodyWithIdentity;
420
+ if ( typeof body === 'object' && body !== null ) {
421
+ bodyWithIdentity = body;
422
+ } else if ( typeof body === 'string' ) {
423
+ bodyWithIdentity = new String( body );
424
+ } else {
425
+ throw new InvalidResponseBody( body );
426
+ }
427
+ responseStatuses.set( bodyWithIdentity, response.status );
428
+ return bodyWithIdentity;
297
429
  } else {
298
- throw new InvalidResponseBody( body );
430
+ throw new IncompatibleResponseType(
431
+ 'application/json',
432
+ response.headers.get( 'Content-Type' ),
433
+ await response.text(),
434
+ );
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Get the body of the response and check that it’s valid text
440
+ * (“plain text”, but in practice often wikitext.)
441
+ *
442
+ * @private
443
+ * @param {Response} response
444
+ * @return {String}
445
+ */
446
+ async function getResponseText( response ) {
447
+ if ( isResponseText( response ) ) {
448
+ const text = new String( await response.text() );
449
+ responseStatuses.set( text, response.status );
450
+ return text;
451
+ } else {
452
+ throw new IncompatibleResponseType(
453
+ 'text/plain',
454
+ response.headers.get( 'Content-Type' ),
455
+ await getResponseBodyForError( response ),
456
+ );
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Get the body of the response and check that it’s valid HTML.
462
+ *
463
+ * @private
464
+ * @param {Response} response
465
+ * @return {String}
466
+ */
467
+ async function getResponseHtml( response ) {
468
+ if ( isResponseHtml( response ) ) {
469
+ const html = new String( await response.text() );
470
+ responseStatuses.set( html, response.status );
471
+ return html;
472
+ } else {
473
+ throw new IncompatibleResponseType(
474
+ 'text/html',
475
+ response.headers.get( 'Content-Type' ),
476
+ await getResponseBodyForError( response ),
477
+ );
299
478
  }
300
479
  }
301
480
 
@@ -347,8 +526,9 @@ export function path( strings, ...values ) {
347
526
  * Substitute template expressions in the path,
348
527
  * taking values from the given params.
349
528
  *
529
+ * @private
350
530
  * @param {string} path The path with optional `{param}` expressions.
351
- * @param {Object|URLSearchParams} params The input params. Not modified.
531
+ * @param {Object|URLSearchParams|FormData} params The input params. Not modified.
352
532
  * @return {Object} The potentially modified path and params.
353
533
  */
354
534
  function substitutePathParams( path, params ) {
@@ -372,6 +552,38 @@ function substitutePathParams( path, params ) {
372
552
  }
373
553
  copiedParams.delete( paramName );
374
554
  return encodeURIComponent( values[ 0 ] );
555
+ } else if ( params instanceof FormData ) {
556
+ if ( copiedParams === null ) {
557
+ copiedParams = new FormData();
558
+ for ( const [ name, value ] of params.entries() ) {
559
+ copiedParams.append( name, value );
560
+ }
561
+ }
562
+ const values = copiedParams.getAll( paramName );
563
+ if ( values.length !== 1 ) {
564
+ throw new InvalidPathParams(
565
+ values.length === 0 ?
566
+ `Unspecified path param ${ match }` :
567
+ `Ambiguous path param ${ match }`,
568
+ path,
569
+ paramName,
570
+ params,
571
+ );
572
+ }
573
+ copiedParams.delete( paramName );
574
+ const value = values[ 0 ];
575
+ if ( value instanceof Blob ) {
576
+ // in practice, value will almost always be a File here,
577
+ // as FormData will turn any Blob into a File (except on old Node versions),
578
+ // hence the unspecific “Blob or File” in the message
579
+ throw new InvalidPathParams(
580
+ `Path param ${ match } cannot be a Blob or File`,
581
+ path,
582
+ paramName,
583
+ params,
584
+ );
585
+ }
586
+ return encodeURIComponent( value );
375
587
  } else {
376
588
  if ( copiedParams === null ) {
377
589
  copiedParams = { ...params };
@@ -394,19 +606,41 @@ function substitutePathParams( path, params ) {
394
606
  }
395
607
 
396
608
  /**
397
- * Split a given URL for the m3api internal interface.
609
+ * Convenience function to add a request header to the given `fetch()` options.
610
+ *
611
+ * @private
612
+ * @param {RequestInit} fetchOptions
613
+ * @param {string} name
614
+ * @param {string} value
615
+ */
616
+ function addHeaderToOptions( fetchOptions, name, value ) {
617
+ fetchOptions.headers = new Headers( fetchOptions.headers );
618
+ fetchOptions.headers.set( name, value );
619
+ }
620
+
621
+ /**
622
+ * Prepare a GET request for the given parameters.
623
+ * Encodes the params into the URL and sets up request headers.
398
624
  *
399
- * @param {string} url
400
- * @return {Object}
625
+ * @private
626
+ * @param {Session} session
627
+ * @param {string} path
628
+ * @param {Object} params
629
+ * @param {Options} options
630
+ * @return {Array} url and fetchOptions
401
631
  */
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;
632
+ function prepareGetRequest( session, path, params, options ) {
633
+ ( { path, params } = substitutePathParams( path, params ) );
634
+ const restUrl = session.apiUrl.replace( /api\.php$/, 'rest.php' );
635
+ const url = new URL( restUrl + path );
636
+ for ( const [ name, value ] of Object.entries( params || {} ) ) {
637
+ url.searchParams.append( name, value );
407
638
  }
408
- url.search = '';
409
- return { url: String( url ), urlParams };
639
+ const fetchOptions = {
640
+ method: 'GET',
641
+ headers: session.getRequestHeaders( options ),
642
+ };
643
+ return [ url, fetchOptions ];
410
644
  }
411
645
 
412
646
  /**
@@ -419,53 +653,519 @@ function splitUrlForInternalInterface( url ) {
419
653
  * @param {Object} [params] Parameters for the request URL.
420
654
  * This may include both parameters for the path and query parameters.
421
655
  * @param {Options} [options] Request options.
422
- * @return {Object|Array} The body of the API response, JSON-decoded.
656
+ * @return {Object|Array|String} The body of the API response, JSON-decoded.
657
+ * If the API returned a string, then for technical reasons it will be returned
658
+ * as a `String` instance, not a primitive string value;
659
+ * you can mostly use it interchangeably with an ordinary string,
660
+ * or turn it into one by calling its `.valueOf()` method.
423
661
  */
424
662
  export async function getJson( session, path, params, options = {} ) {
663
+ const [ url, fetchOptions ] = prepareGetRequest( session, path, params, options );
664
+ addHeaderToOptions( fetchOptions, 'accept', 'application/json' );
665
+ const response = await session.fetch( url, fetchOptions );
666
+ await checkResponseStatus( response );
667
+ return await getResponseJson( response );
668
+ }
669
+
670
+ /**
671
+ * Make a GET request to a REST API endpoint and return the text body.
672
+ *
673
+ * @param {Session} session The m3api session to use for this request.
674
+ * @param {string} path The resource path.
675
+ * Does not include the domain, script path, or `rest.php` endpoint.
676
+ * Use the {@link path} tag function to build the path.
677
+ * @param {Object} [params] Parameters for the request URL.
678
+ * This may include both parameters for the path and query parameters.
679
+ * @param {Options} [options] Request options.
680
+ * @return {String} The body of the API response.
681
+ * For technical reasons, this is a `String` instance, not a primitive string value;
682
+ * you can mostly use it interchangeably with an ordinary string,
683
+ * or turn it into one by calling its `.valueOf()` method.
684
+ */
685
+ export async function getText( session, path, params, options = {} ) {
686
+ const [ url, fetchOptions ] = prepareGetRequest( session, path, params, options );
687
+ addHeaderToOptions( fetchOptions, 'accept', 'text/plain' );
688
+ const response = await session.fetch( url, fetchOptions );
689
+ await checkResponseStatus( response );
690
+ return await getResponseText( response );
691
+ }
692
+
693
+ /**
694
+ * Make a GET request to a REST API endpoint and return the HTML body.
695
+ *
696
+ * @param {Session} session The m3api session to use for this request.
697
+ * @param {string} path The resource path, e.g. `/v1/page/{title}/html`.
698
+ * Does not include the domain, script path, or `rest.php` endpoint.
699
+ * Use the {@link path} tag function to build the path.
700
+ * @param {Object} [params] Parameters for the request URL.
701
+ * This may include both parameters for the path and query parameters.
702
+ * @param {Options} [options] Request options.
703
+ * @return {String} The body of the API response.
704
+ * For technical reasons, this is a `String` instance, not a primitive string value;
705
+ * you can mostly use it interchangeably with an ordinary string,
706
+ * or turn it into one by calling its `.valueOf()` method.
707
+ */
708
+ export async function getHtml( session, path, params, options = {} ) {
709
+ const [ url, fetchOptions ] = prepareGetRequest( session, path, params, options );
710
+ addHeaderToOptions( fetchOptions, 'accept', 'text/html' );
711
+ const response = await session.fetch( url, fetchOptions );
712
+ await checkResponseStatus( response );
713
+ return await getResponseHtml( response );
714
+ }
715
+
716
+ /**
717
+ * Encode a body for the request to the server.
718
+ *
719
+ * @param {Object|URLSearchParams|FormData} body
720
+ * @return {RequestInit} To be mixed into the `fetch()` options.
721
+ */
722
+ function encodeBody( body ) {
723
+ if ( body instanceof URLSearchParams || body instanceof FormData ) {
724
+ return { body };
725
+ } else {
726
+ return {
727
+ body: JSON.stringify( body ),
728
+ headers: {
729
+ 'Content-Type': 'application/json',
730
+ },
731
+ };
732
+ }
733
+ }
734
+
735
+ /**
736
+ * Prepare a non-GET request for the given parameters.
737
+ * Encodes the params into the URL and body and sets up request headers.
738
+ * The caller must set the specific method afterwards.
739
+ *
740
+ * @private
741
+ * @param {Session} session
742
+ * @param {string} path
743
+ * @param {Object} params
744
+ * @param {Options} options
745
+ * @return {Array} url and fetchOptions
746
+ */
747
+ function prepareRequestWithBody( session, path, params, options ) {
425
748
  ( { path, params } = substitutePathParams( path, params ) );
426
749
  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 ),
750
+ const url = new URL( restUrl + path );
751
+ const fetchOptions = {
752
+ method: '', // trigger a TypeError in fetch() if caller does not override method
753
+ ...encodeBody( params ),
432
754
  };
433
- const internalResponse = await session.internalGet( url, params, headers );
434
- checkResponseStatus( internalResponse );
435
- return getResponseJson( internalResponse );
755
+ fetchOptions.headers = new Headers( fetchOptions.headers );
756
+ for ( const [ name, value ] of Object.entries( session.getRequestHeaders( options ) ) ) {
757
+ fetchOptions.headers.set( name, value );
758
+ }
759
+ return [ url, fetchOptions ];
760
+ }
761
+
762
+ /**
763
+ * Prepare a POST request for the given parameters.
764
+ * Encodes the params into the URL and body and sets up request headers.
765
+ *
766
+ * @private
767
+ * @param {Session} session
768
+ * @param {string} path
769
+ * @param {Object} params
770
+ * @param {Options} options
771
+ * @return {Array} url and fetchOptions
772
+ */
773
+ function preparePostRequest( session, path, params, options ) {
774
+ const [ url, fetchOptions ] = prepareRequestWithBody( session, path, params, options );
775
+ return [ url, {
776
+ ...fetchOptions,
777
+ method: 'POST',
778
+ } ];
436
779
  }
437
780
 
438
781
  /**
439
782
  * Make a POST request to a REST API endpoint and return the JSON-decoded body.
440
783
  *
441
784
  * @param {Session} session The m3api session to use for this request.
442
- * @param {string} path The resourcee path, e.g. `/v1/page`.
785
+ * @param {string} path The resource path, e.g. `/v1/page`.
443
786
  * Does not include the domain, script path, or `rest.php` endpoint.
444
787
  * 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.)
788
+ * @param {Object|URLSearchParams|FormData} params The request body.
789
+ * An Object will be sent using the `application/json` content type;
790
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
791
+ * FormData will be sent using the `multipart/form-data` content type.
449
792
  * You may also include parameters for the path here.
450
793
  * @param {Options} [options] Request options.
451
- * @return {Object|Array} The body of the API response, JSON-decoded.
794
+ * @return {Object|Array|String} The body of the API response, JSON-decoded.
795
+ * If the API returned a string, then for technical reasons it will be returned
796
+ * as a `String` instance, not a primitive string value;
797
+ * you can mostly use it interchangeably with an ordinary string,
798
+ * or turn it into one by calling its `.valueOf()` method.
452
799
  */
453
800
  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 );
801
+ const [ url, fetchOptions ] = preparePostRequest( session, path, params, options );
802
+ // skip this for now due to T412610
803
+ // addHeaderToOptions( fetchOptions, 'accept', 'application/json' );
804
+ const response = await session.fetch( url, fetchOptions );
805
+ await checkResponseStatus( response );
806
+ return await getResponseJson( response );
807
+ }
808
+
809
+ /**
810
+ * Make a POST request to a REST API endpoint and return the text body.
811
+ *
812
+ * @param {Session} session The m3api session to use for this request.
813
+ * @param {string} path The resource path, e.g. `/v1/transform/html/to/wikitext`.
814
+ * Does not include the domain, script path, or `rest.php` endpoint.
815
+ * Use the {@link path} tag function to build the path.
816
+ * @param {Object|URLSearchParams|FormData} params The request body.
817
+ * An Object will be sent using the `application/json` content type;
818
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
819
+ * FormData will be sent using the `multipart/form-data` content type.
820
+ * You may also include parameters for the path here.
821
+ * @param {Options} [options] Request options.
822
+ * @return {String} The body of the API response.
823
+ * For technical reasons, this is a `String` instance, not a primitive string value;
824
+ * you can mostly use it interchangeably with an ordinary string,
825
+ * or turn it into one by calling its `.valueOf()` method.
826
+ */
827
+ export async function postForText( session, path, params, options = {} ) {
828
+ const [ url, fetchOptions ] = preparePostRequest( session, path, params, options );
829
+ addHeaderToOptions( fetchOptions, 'accept', 'text/plain' );
830
+ const response = await session.fetch( url, fetchOptions );
831
+ await checkResponseStatus( response );
832
+ return await getResponseText( response );
833
+ }
834
+
835
+ /**
836
+ * Make a POST request to a REST API endpoint and return the HTML body.
837
+ *
838
+ * @param {Session} session The m3api session to use for this request.
839
+ * @param {string} path The resource path, e.g. `/v1/transform/wikitext/to/html`.
840
+ * Does not include the domain, script path, or `rest.php` endpoint.
841
+ * Use the {@link path} tag function to build the path.
842
+ * @param {Object|URLSearchParams|FormData} params The request body.
843
+ * An Object will be sent using the `application/json` content type;
844
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
845
+ * FormData will be sent using the `multipart/form-data` content type.
846
+ * You may also include parameters for the path here.
847
+ * @param {Options} [options] Request options.
848
+ * @return {String} The body of the API response.
849
+ * For technical reasons, this is a `String` instance, not a primitive string value;
850
+ * you can mostly use it interchangeably with an ordinary string,
851
+ * or turn it into one by calling its `.valueOf()` method.
852
+ */
853
+ export async function postForHtml( session, path, params, options = {} ) {
854
+ const [ url, fetchOptions ] = preparePostRequest( session, path, params, options );
855
+ addHeaderToOptions( fetchOptions, 'accept', 'text/html' );
856
+ const response = await session.fetch( url, fetchOptions );
857
+ await checkResponseStatus( response );
858
+ return await getResponseHtml( response );
859
+ }
860
+
861
+ /**
862
+ * Prepare a PUT request for the given parameters.
863
+ * Encodes the params into the URL and body and sets up request headers.
864
+ *
865
+ * @private
866
+ * @param {Session} session
867
+ * @param {string} path
868
+ * @param {Object} params
869
+ * @param {Options} options
870
+ * @return {Array} url and fetchOptions
871
+ */
872
+ function preparePutRequest( session, path, params, options ) {
873
+ const [ url, fetchOptions ] = prepareRequestWithBody( session, path, params, options );
874
+ return [ url, {
875
+ ...fetchOptions,
876
+ method: 'PUT',
877
+ } ];
878
+ }
879
+
880
+ /**
881
+ * Make a PUT request to a REST API endpoint and return the JSON-decoded body.
882
+ *
883
+ * @param {Session} session The m3api session to use for this request.
884
+ * @param {string} path The resource path, e.g. `/v1/page/{title}`.
885
+ * Does not include the domain, script path, or `rest.php` endpoint.
886
+ * Use the {@link path} tag function to build the path.
887
+ * @param {Object|URLSearchParams|FormData} params The request body.
888
+ * An Object will be sent using the `application/json` content type;
889
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
890
+ * FormData will be sent using the `multipart/form-data` content type.
891
+ * You may also include parameters for the path here.
892
+ * Note that all known PUT endpoints only accept JSON bodies,
893
+ * so URLSearchParams or FormData params are unlikely to be useful.
894
+ * @param {Options} [options] Request options.
895
+ * @return {Object|Array|String} The body of the API response, JSON-decoded.
896
+ * If the API returned a string, then for technical reasons it will be returned
897
+ * as a `String` instance, not a primitive string value;
898
+ * you can mostly use it interchangeably with an ordinary string,
899
+ * or turn it into one by calling its `.valueOf()` method.
900
+ */
901
+ export async function putForJson( session, path, params, options = {} ) {
902
+ const [ url, fetchOptions ] = preparePutRequest( session, path, params, options );
903
+ addHeaderToOptions( fetchOptions, 'accept', 'application/json' );
904
+ const response = await session.fetch( url, fetchOptions );
905
+ await checkResponseStatus( response );
906
+ return await getResponseJson( response );
907
+ }
908
+
909
+ /**
910
+ * Make a PUT request to a REST API endpoint and return the text body.
911
+ *
912
+ * @param {Session} session The m3api session to use for this request.
913
+ * @param {string} path The resource path.
914
+ * Does not include the domain, script path, or `rest.php` endpoint.
915
+ * Use the {@link path} tag function to build the path.
916
+ * @param {Object|URLSearchParams|FormData} params The request body.
917
+ * An Object will be sent using the `application/json` content type;
918
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
919
+ * FormData will be sent using the `multipart/form-data` content type.
920
+ * You may also include parameters for the path here.
921
+ * Note that all known PUT endpoints only accept JSON bodies,
922
+ * so URLSearchParams or FormData params are unlikely to be useful.
923
+ * @param {Options} [options] Request options.
924
+ * @return {String} The body of the API response.
925
+ * For technical reasons, this is a `String` instance, not a primitive string value;
926
+ * you can mostly use it interchangeably with an ordinary string,
927
+ * or turn it into one by calling its `.valueOf()` method.
928
+ */
929
+ export async function putForText( session, path, params, options = {} ) {
930
+ const [ url, fetchOptions ] = preparePutRequest( session, path, params, options );
931
+ addHeaderToOptions( fetchOptions, 'accept', 'text/plain' );
932
+ const response = await session.fetch( url, fetchOptions );
933
+ await checkResponseStatus( response );
934
+ return await getResponseText( response );
935
+ }
936
+
937
+ /**
938
+ * Make a PUT request to a REST API endpoint and return the HTML body.
939
+ *
940
+ * @param {Session} session The m3api session to use for this request.
941
+ * @param {string} path The resource path.
942
+ * Does not include the domain, script path, or `rest.php` endpoint.
943
+ * Use the {@link path} tag function to build the path.
944
+ * @param {Object|URLSearchParams|FormData} params The request body.
945
+ * An Object will be sent using the `application/json` content type;
946
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
947
+ * FormData will be sent using the `multipart/form-data` content type.
948
+ * You may also include parameters for the path here.
949
+ * Note that all known PUT endpoints only accept JSON bodies,
950
+ * so URLSearchParams or FormData params are unlikely to be useful.
951
+ * @param {Options} [options] Request options.
952
+ * @return {String} The body of the API response.
953
+ * For technical reasons, this is a `String` instance, not a primitive string value;
954
+ * you can mostly use it interchangeably with an ordinary string,
955
+ * or turn it into one by calling its `.valueOf()` method.
956
+ */
957
+ export async function putForHtml( session, path, params, options = {} ) {
958
+ const [ url, fetchOptions ] = preparePutRequest( session, path, params, options );
959
+ addHeaderToOptions( fetchOptions, 'accept', 'text/html' );
960
+ const response = await session.fetch( url, fetchOptions );
961
+ await checkResponseStatus( response );
962
+ return await getResponseHtml( response );
963
+ }
964
+
965
+ /**
966
+ * Prepare a DELETE request for the given parameters.
967
+ * Encodes the params into the URL and body and sets up request headers.
968
+ *
969
+ * @private
970
+ * @param {Session} session
971
+ * @param {string} path
972
+ * @param {Object} params
973
+ * @param {Options} options
974
+ * @return {Array} url and fetchOptions
975
+ */
976
+ function prepareDeleteRequest( session, path, params, options ) {
977
+ const [ url, fetchOptions ] = prepareRequestWithBody( session, path, params, options );
978
+ return [ url, {
979
+ ...fetchOptions,
980
+ method: 'DELETE',
981
+ } ];
982
+ }
983
+
984
+ /**
985
+ * Make a DELETE request to a REST API endpoint and return the JSON-decoded body.
986
+ *
987
+ * @param {Session} session The m3api session to use for this request.
988
+ * @param {string} path The resource path, e.g. `/v1/page/{title}`.
989
+ * Does not include the domain, script path, or `rest.php` endpoint.
990
+ * Use the {@link path} tag function to build the path.
991
+ * @param {Object|URLSearchParams|FormData} params The request body.
992
+ * An Object will be sent using the `application/json` content type;
993
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
994
+ * FormData will be sent using the `multipart/form-data` content type.
995
+ * You may also include parameters for the path here.
996
+ * Note that all known DELETE endpoints only accept JSON bodies,
997
+ * so URLSearchParams or FormData params are unlikely to be useful.
998
+ * @param {Options} [options] Request options.
999
+ * @return {Object|Array|String} The body of the API response, JSON-decoded.
1000
+ * If the API returned a string, then for technical reasons it will be returned
1001
+ * as a `String` instance, not a primitive string value;
1002
+ * you can mostly use it interchangeably with an ordinary string,
1003
+ * or turn it into one by calling its `.valueOf()` method.
1004
+ */
1005
+ export async function deleteForJson( session, path, params, options = {} ) {
1006
+ const [ url, fetchOptions ] = prepareDeleteRequest( session, path, params, options );
1007
+ addHeaderToOptions( fetchOptions, 'accept', 'application/json' );
1008
+ const response = await session.fetch( url, fetchOptions );
1009
+ await checkResponseStatus( response );
1010
+ return await getResponseJson( response );
1011
+ }
1012
+
1013
+ /**
1014
+ * Make a DELETE request to a REST API endpoint and return the text body.
1015
+ *
1016
+ * @param {Session} session The m3api session to use for this request.
1017
+ * @param {string} path The resource path.
1018
+ * Does not include the domain, script path, or `rest.php` endpoint.
1019
+ * Use the {@link path} tag function to build the path.
1020
+ * @param {Object|URLSearchParams|FormData} params The request body.
1021
+ * An Object will be sent using the `application/json` content type;
1022
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
1023
+ * FormData will be sent using the `multipart/form-data` content type.
1024
+ * You may also include parameters for the path here.
1025
+ * Note that all known DELETE endpoints only accept JSON bodies,
1026
+ * so URLSearchParams or FormData params are unlikely to be useful.
1027
+ * @param {Options} [options] Request options.
1028
+ * @return {String} The body of the API response.
1029
+ * For technical reasons, this is a `String` instance, not a primitive string value;
1030
+ * you can mostly use it interchangeably with an ordinary string,
1031
+ * or turn it into one by calling its `.valueOf()` method.
1032
+ */
1033
+ export async function deleteForText( session, path, params, options = {} ) {
1034
+ const [ url, fetchOptions ] = prepareDeleteRequest( session, path, params, options );
1035
+ addHeaderToOptions( fetchOptions, 'accept', 'text/plain' );
1036
+ const response = await session.fetch( url, fetchOptions );
1037
+ await checkResponseStatus( response );
1038
+ return await getResponseText( response );
1039
+ }
1040
+
1041
+ /**
1042
+ * Make a DELETE request to a REST API endpoint and return the HTML body.
1043
+ *
1044
+ * @param {Session} session The m3api session to use for this request.
1045
+ * @param {string} path The resource path.
1046
+ * Does not include the domain, script path, or `rest.php` endpoint.
1047
+ * Use the {@link path} tag function to build the path.
1048
+ * @param {Object|URLSearchParams|FormData} params The request body.
1049
+ * An Object will be sent using the `application/json` content type;
1050
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
1051
+ * FormData will be sent using the `multipart/form-data` content type.
1052
+ * You may also include parameters for the path here.
1053
+ * Note that all known DELETE endpoints only accept JSON bodies,
1054
+ * so URLSearchParams or FormData params are unlikely to be useful.
1055
+ * @param {Options} [options] Request options.
1056
+ * @return {String} The body of the API response.
1057
+ * For technical reasons, this is a `String` instance, not a primitive string value;
1058
+ * you can mostly use it interchangeably with an ordinary string,
1059
+ * or turn it into one by calling its `.valueOf()` method.
1060
+ */
1061
+ export async function deleteForHtml( session, path, params, options = {} ) {
1062
+ const [ url, fetchOptions ] = prepareDeleteRequest( session, path, params, options );
1063
+ addHeaderToOptions( fetchOptions, 'accept', 'text/html' );
1064
+ const response = await session.fetch( url, fetchOptions );
1065
+ await checkResponseStatus( response );
1066
+ return await getResponseHtml( response );
1067
+ }
1068
+
1069
+ /**
1070
+ * Prepare a PATCH request for the given parameters.
1071
+ * Encodes the params into the URL and body and sets up request headers.
1072
+ *
1073
+ * @private
1074
+ * @param {Session} session
1075
+ * @param {string} path
1076
+ * @param {Object} params
1077
+ * @param {Options} options
1078
+ * @return {Array} url and fetchOptions
1079
+ */
1080
+ function preparePatchRequest( session, path, params, options ) {
1081
+ const [ url, fetchOptions ] = prepareRequestWithBody( session, path, params, options );
1082
+ return [ url, {
1083
+ ...fetchOptions,
1084
+ method: 'PATCH',
1085
+ } ];
1086
+ }
1087
+
1088
+ /**
1089
+ * Make a PATCH request to a REST API endpoint and return the JSON-decoded body.
1090
+ *
1091
+ * @param {Session} session The m3api session to use for this request.
1092
+ * @param {string} path The resource path, e.g. `/v1/page/{title}`.
1093
+ * Does not include the domain, script path, or `rest.php` endpoint.
1094
+ * Use the {@link path} tag function to build the path.
1095
+ * @param {Object|URLSearchParams|FormData} params The request body.
1096
+ * An Object will be sent using the `application/json` content type;
1097
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
1098
+ * FormData will be sent using the `multipart/form-data` content type.
1099
+ * You may also include parameters for the path here.
1100
+ * Note that all known PATCH endpoints only accept JSON bodies,
1101
+ * so URLSearchParams or FormData params are unlikely to be useful.
1102
+ * @param {Options} [options] Request options.
1103
+ * @return {Object|Array|String} The body of the API response, JSON-decoded.
1104
+ * If the API returned a string, then for technical reasons it will be returned
1105
+ * as a `String` instance, not a primitive string value;
1106
+ * you can mostly use it interchangeably with an ordinary string,
1107
+ * or turn it into one by calling its `.valueOf()` method.
1108
+ */
1109
+ export async function patchForJson( session, path, params, options = {} ) {
1110
+ const [ url, fetchOptions ] = preparePatchRequest( session, path, params, options );
1111
+ addHeaderToOptions( fetchOptions, 'accept', 'application/json' );
1112
+ const response = await session.fetch( url, fetchOptions );
1113
+ await checkResponseStatus( response );
1114
+ return await getResponseJson( response );
1115
+ }
1116
+
1117
+ /**
1118
+ * Make a PATCH request to a REST API endpoint and return the text body.
1119
+ *
1120
+ * @param {Session} session The m3api session to use for this request.
1121
+ * @param {string} path The resource path.
1122
+ * Does not include the domain, script path, or `rest.php` endpoint.
1123
+ * Use the {@link path} tag function to build the path.
1124
+ * @param {Object|URLSearchParams|FormData} params The request body.
1125
+ * An Object will be sent using the `application/json` content type;
1126
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
1127
+ * FormData will be sent using the `multipart/form-data` content type.
1128
+ * You may also include parameters for the path here.
1129
+ * Note that all known PATCH endpoints only accept JSON bodies,
1130
+ * so URLSearchParams or FormData params are unlikely to be useful.
1131
+ * @param {Options} [options] Request options.
1132
+ * @return {String} The body of the API response.
1133
+ * For technical reasons, this is a `String` instance, not a primitive string value;
1134
+ * you can mostly use it interchangeably with an ordinary string,
1135
+ * or turn it into one by calling its `.valueOf()` method.
1136
+ */
1137
+ export async function patchForText( session, path, params, options = {} ) {
1138
+ const [ url, fetchOptions ] = preparePatchRequest( session, path, params, options );
1139
+ addHeaderToOptions( fetchOptions, 'accept', 'text/plain' );
1140
+ const response = await session.fetch( url, fetchOptions );
1141
+ await checkResponseStatus( response );
1142
+ return await getResponseText( response );
1143
+ }
1144
+
1145
+ /**
1146
+ * Make a PATCH request to a REST API endpoint and return the HTML body.
1147
+ *
1148
+ * @param {Session} session The m3api session to use for this request.
1149
+ * @param {string} path The resource path.
1150
+ * Does not include the domain, script path, or `rest.php` endpoint.
1151
+ * Use the {@link path} tag function to build the path.
1152
+ * @param {Object|URLSearchParams|FormData} params The request body.
1153
+ * An Object will be sent using the `application/json` content type;
1154
+ * URLSearchParams will be sent using the `application/x-www-form-urlencoded` content type;
1155
+ * FormData will be sent using the `multipart/form-data` content type.
1156
+ * You may also include parameters for the path here.
1157
+ * Note that all known PATCH endpoints only accept JSON bodies,
1158
+ * so URLSearchParams or FormData params are unlikely to be useful.
1159
+ * @param {Options} [options] Request options.
1160
+ * @return {String} The body of the API response.
1161
+ * For technical reasons, this is a `String` instance, not a primitive string value;
1162
+ * you can mostly use it interchangeably with an ordinary string,
1163
+ * or turn it into one by calling its `.valueOf()` method.
1164
+ */
1165
+ export async function patchForHtml( session, path, params, options = {} ) {
1166
+ const [ url, fetchOptions ] = preparePatchRequest( session, path, params, options );
1167
+ addHeaderToOptions( fetchOptions, 'accept', 'text/html' );
1168
+ const response = await session.fetch( url, fetchOptions );
1169
+ await checkResponseStatus( response );
1170
+ return await getResponseHtml( response );
471
1171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m3api-rest",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
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.0.0"
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.0.0",
39
+ "m3api": "~1.1.0",
40
40
  "mocha": "^11.1.0",
41
41
  "npm-run-all": "^4.1.5"
42
42
  },