Skip to content

Commit

Permalink
Support file parameters
Browse files Browse the repository at this point in the history
When we’re using native fetch() everywhere, this is quite simple: we
just have to specify the body as FormData instead of URLSearchParams.

Implements #26.
  • Loading branch information
lucaswerkmeister committed Jul 3, 2023
1 parent 7121694 commit ae7f498
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ but this file may sometimes contain later improvements (e.g. typo fixes).
The internal interface has been rearranged,
with `fetch.js` now used for both backends,
augmented by `fetch-browser.js` and `fetch-node.js`.
- m3api now supports `Blob` and `File` parameters in POST requests.
This can be used to upload files.
- Improved `README.md` formatting for npmjs.com.
- Updated dependencies.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ Other features not demonstrated above:
pass an object with a `method` value as the second parameter:
e.g. `request( { ... }, { method: 'POST' } )`.
(`requestAndContinue` also supports this.)
In POST requests, `Blob` or `File` parameters are also supported.

- API requests will automatically be retried if necessary
(if the response contains a Retry-After header,
Expand Down
7 changes: 4 additions & 3 deletions core.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@

/**
* A request parameter value that cannot be put in a list:
* a boolean toggle, or null or undefined as a default fallback
* for not sending a parameter at all.
* a boolean toggle, a Blob or File (POST requests only),
* or null or undefined as a default fallback for not sending a parameter at all.
*
* @typedef UnlistableParam
* @type {boolean|null|undefined}
* @type {boolean|Blob|File|null|undefined}
*/

/**
Expand Down Expand Up @@ -48,6 +48,7 @@
* or an array or set of strings or numbers.
* Parameters with values false, null, or undefined are completely removed
* when the request is sent out.
* In POST requests, a parameter may also be a Blob or File.
*
* @typedef Params
* @type {Object<string, Param>}
Expand Down
6 changes: 5 additions & 1 deletion fetch.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,14 @@ class FetchSession extends Session {
async internalPost( apiUrl, urlParams, bodyParams, headers ) {
const url = new URL( apiUrl );
url.search = new URLSearchParams( urlParams );
const body = new FormData();
for ( const [ paramName, paramValue ] of Object.entries( bodyParams ) ) {
body.append( paramName, paramValue );
}
const response = await fetch( url, {
...this.getFetchOptions( headers ),
method: 'POST',
body: new URLSearchParams( bodyParams ),
body,
} );
return transformResponse( response );
}
Expand Down
62 changes: 62 additions & 0 deletions test/integration/node.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,66 @@ describe( 'NodeSession', function () {
expect( content ).to.equal( text );
} );

for ( const { name, getData } of [
{
name: 'Blob',
getData( content ) {
return new Blob( [ content ], { type: 'image/svg+xml' } );
},
},
{
name: 'File',
getData( content ) {
return new File( [ content ], 'blank.svg', { type: 'image/svg.xml' } );
},
},
] ) {
const contentA = "<svg xmlns='http://www.w3.org/2000/svg'/>\n";
const contentB = "<svg xmlns='http://www.w3.org/2000/svg' />\n";
// eslint-disable-next-line no-loop-func
it( `upload (${name})`, async function () {
if ( !mediawikiUsername || !mediawikiPassword ) {
return this.skip();
}
const session = new NodeSession( 'commons.wikimedia.beta.wmflabs.org', {
formatversion: 2,
}, {
userAgent,
} );
await session.request( {
action: 'login',
lgname: mediawikiUsername,
lgpassword: mediawikiPassword,
}, { method: 'POST', tokenType: 'login', tokenName: 'lgtoken' } );
session.tokens.clear();
const filename = `m3api test file ${new Date().getUTCFullYear()}.svg`;
const text = 'Minimal file for m3api integration tests. CC0.';

// find out if we’re using contentA or contentB
let content = contentA;
try {
const currentContent = ( await session.internalGet(
`https://commons.wikimedia.beta.wmflabs.org/wiki/Special:FilePath/${filename}`,
{},
{ 'user-agent': session.getUserAgent( {} ) },
) ).body;
if ( currentContent === contentA ) {
content = contentB;
}
} catch {
// ignore, file doesn’t exist yet
}

const { upload } = await session.request( {
action: 'upload',
filename,
comment: 'm3api integration test',
text,
watchlist: 'nochange',
file: getData( content ),
}, { method: 'POST', tokenType: 'csrf' } );
expect( upload.result ).to.equal( 'Success' );
} );
}

} );
36 changes: 36 additions & 0 deletions test/unit/combine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,42 @@ describe( 'CombiningSession', () => {
expect( responses[ 1 ] ).to.equal( response2 );
} );

it( 'different blobs', async () => {
const blob1 = new Blob( [ '1' ], { type: 'text/plain' } );
const params1 = { file: blob1 };
const response1 = { upload: { filename: '1' } };
const blob2 = new Blob( [ '2' ], { type: 'text/plain' } );
const params2 = { file: blob2 };
const response2 = { upload: { filename: '2' } };
const session = sequentialRequestSession( [
{ expectedParams: { file: blob1 }, response: response1 },
{ expectedParams: { file: blob2 }, response: response2 },
] );
const promise1 = session.request( params1 );
const promise2 = session.request( params2 );
const responses = await Promise.all( [ promise1, promise2 ] );
expect( responses[ 0 ] ).to.equal( response1 );
expect( responses[ 1 ] ).to.equal( response2 );
} );

it( 'different files', async () => {
const file1 = new File( [ '1' ], '1', { type: 'text/plain' } );
const params1 = { file: file1 };
const response1 = { upload: { filename: '1' } };
const file2 = new File( [ '2' ], '2', { type: 'text/plain' } );
const params2 = { file: file2 };
const response2 = { upload: { filename: '2' } };
const session = sequentialRequestSession( [
{ expectedParams: { file: file1 }, response: response1 },
{ expectedParams: { file: file2 }, response: response2 },
] );
const promise1 = session.request( params1 );
const promise2 = session.request( params2 );
const responses = await Promise.all( [ promise1, promise2 ] );
expect( responses[ 0 ] ).to.equal( response1 );
expect( responses[ 1 ] ).to.equal( response2 );
} );

for ( const first of [ 'generator', 'continue' ] ) {
for ( const second of [ 'titles', 'pageids', 'revids' ] ) {
it( `${first} + ${second}`, async () => {
Expand Down

0 comments on commit ae7f498

Please sign in to comment.