Skip to content

Commit

Permalink
endpoints using upload without file parameters will post JSON; fix fo…
Browse files Browse the repository at this point in the history
…r fields parameter when value string in multipart requests
  • Loading branch information
pleary committed Nov 19, 2024
1 parent fdcc473 commit cf649ca
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 51 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ jobs:

- run: npm run coverage

- run: npm run eslint

notify:
name: Notify Slack
needs: build
Expand Down
62 changes: 41 additions & 21 deletions build/inaturalistjs.js
Original file line number Diff line number Diff line change
Expand Up @@ -242,29 +242,13 @@ var iNaturalistAPI = /*#__PURE__*/function () {
}
// get the right host to send requests
var host = iNaturalistAPI.methodHostPrefix(options);
// make the request
// prepare the request
var body;
if (options.upload) {
body = new LocalFormData();
// Before params get "flattened" extract the fields and encode them as a
// single JSON string, which the server can handle
var fields = interpolated.remainingParams.fields;
if (fields) {
delete interpolated.remainingParams.fields;
body.append("fields", JSON.stringify(fields));
}
// multipart requests reference all nested parameter names as strings
// so flatten arrays into "arr[0]" and objects into "obj[prop]"
params = iNaturalistAPI.flattenMultipartParams(interpolated.remainingParams);
Object.keys(params).forEach(function (k) {
// FormData params can include options like file upload sizes
if (params[k] && params[k].type === "custom" && params[k].value) {
body.append(k, params[k].value, params[k].options);
} else {
body.append(k, typeof params[k] === "boolean" ? params[k].toString() : params[k]);
}
});
} else {
body = iNaturalistAPI.multipartBodyForResuest(interpolated.remainingParams);
}
// if there is no multipart request body, prepare it as a JSON request body
if (body === null || _typeof(body) !== "object") {
headers["Content-Type"] = "application/json";
body = JSON.stringify(interpolated.remainingParams);
}
Expand All @@ -286,6 +270,42 @@ var iNaturalistAPI = /*#__PURE__*/function () {
var url = "".concat(host, "/").concat(thisRoute).concat(query);
return localFetch(url, fetchOpts).then(iNaturalistAPI.thenText).then(iNaturalistAPI.thenJson);
}
}, {
key: "multipartBodyForResuest",
value: function multipartBodyForResuest(parameters) {
var body = new LocalFormData();
var bodyContainsObjects = false;
// Before params get "flattened" extract the fields and encode them as a
// single JSON string, which the server can handle
var fields = parameters.fields;
if (fields) {
body.append("fields", _typeof(fields) === "object" ? JSON.stringify(fields) : fields);
}
// multipart requests reference all nested parameter names as strings
// so flatten arrays into "arr[0]" and objects into "obj[prop]"
var params = iNaturalistAPI.flattenMultipartParams(parameters);
Object.keys(params).forEach(function (k) {
if (k.match(/^fields\[/)) {
return;
}
// FormData params can include options like file upload sizes
if (params[k] && params[k].type === "custom" && params[k].value) {
body.append(k, params[k].value, params[k].options);
bodyContainsObjects = true;
} else {
if (params[k] !== null && _typeof(params[k]) === "object") {
bodyContainsObjects = true;
}
body.append(k, typeof params[k] === "boolean" ? params[k].toString() : params[k]);
}
});
// there are no parameters with type object, so there are no files in this
// request. Return null as this request does not need to be multipart
if (!bodyContainsObjects) {
return null;
}
return body;
}

// a variant of post using the http PUT method
}, {
Expand Down
67 changes: 44 additions & 23 deletions lib/inaturalist_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const iNaturalistAPI = class iNaturalistAPI {

static post( route, p, opts ) {
const options = { ...( opts || { } ) };
let params = { ...( p || { } ) };
const params = { ...( p || { } ) };
// interpolate path params, e.g. /:id => /1
const interpolated = iNaturalistAPI.interpolateRouteParams( route, params );
if ( interpolated.err ) { return interpolated.err; }
Expand Down Expand Up @@ -160,29 +160,13 @@ const iNaturalistAPI = class iNaturalistAPI {
}
// get the right host to send requests
const host = iNaturalistAPI.methodHostPrefix( options );
// make the request
// prepare the request
let body;
if ( options.upload ) {
body = new LocalFormData( );
// Before params get "flattened" extract the fields and encode them as a
// single JSON string, which the server can handle
const { fields } = interpolated.remainingParams;
if ( fields ) {
delete interpolated.remainingParams.fields;
body.append( "fields", JSON.stringify( fields ) );
}
// multipart requests reference all nested parameter names as strings
// so flatten arrays into "arr[0]" and objects into "obj[prop]"
params = iNaturalistAPI.flattenMultipartParams( interpolated.remainingParams );
Object.keys( params ).forEach( k => {
// FormData params can include options like file upload sizes
if ( params[k] && params[k].type === "custom" && params[k].value ) {
body.append( k, params[k].value, params[k].options );
} else {
body.append( k, ( typeof params[k] === "boolean" ) ? params[k].toString( ) : params[k] );
}
} );
} else {
body = iNaturalistAPI.multipartBodyForResuest( interpolated.remainingParams );
}
// if there is no multipart request body, prepare it as a JSON request body
if ( body === null || typeof ( body ) !== "object" ) {
headers["Content-Type"] = "application/json";
body = JSON.stringify( interpolated.remainingParams );
}
Expand All @@ -207,6 +191,41 @@ const iNaturalistAPI = class iNaturalistAPI {
.then( iNaturalistAPI.thenJson );
}

static multipartBodyForResuest( parameters ) {
const body = new LocalFormData( );
let bodyContainsObjects = false;
// Before params get "flattened" extract the fields and encode them as a
// single JSON string, which the server can handle
const { fields } = parameters;
if ( fields ) {
body.append( "fields", typeof ( fields ) === "object" ? JSON.stringify( fields ) : fields );
}
// multipart requests reference all nested parameter names as strings
// so flatten arrays into "arr[0]" and objects into "obj[prop]"
const params = iNaturalistAPI.flattenMultipartParams( parameters );
Object.keys( params ).forEach( k => {
if ( k.match( /^fields\[/ ) ) {
return;
}
// FormData params can include options like file upload sizes
if ( params[k] && params[k].type === "custom" && params[k].value ) {
body.append( k, params[k].value, params[k].options );
bodyContainsObjects = true;
} else {
if ( params[k] !== null && typeof ( params[k] ) === "object" ) {
bodyContainsObjects = true;
}
body.append( k, ( typeof params[k] === "boolean" ) ? params[k].toString( ) : params[k] );
}
} );
// there are no parameters with type object, so there are no files in this
// request. Return null as this request does not need to be multipart
if ( !bodyContainsObjects ) {
return null;
}
return body;
}

// a variant of post using the http PUT method
static head( route, params, opts = { } ) {
const options = { ...opts, method: "head" };
Expand Down Expand Up @@ -278,7 +297,9 @@ const iNaturalistAPI = class iNaturalistAPI {
if ( params === null ) { return params; }
if ( typeof params === "object" ) {
if ( !params.constructor || params.constructor.name === "Object" ) {
if ( params.type === "custom" ) { return { [keyPrefix]: params }; }
if ( params.type === "custom" ) {
return { [keyPrefix]: params };
}
const flattenedParams = { };
Object.keys( params ).forEach( k => {
const newPrefix = keyPrefix ? `${keyPrefix}[${k}]` : k;
Expand Down
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "inaturalistjs",
"version": "2.13.0",
"version": "2.14.0",
"description": "inaturalistjs",
"author": "iNaturalist",
"license": "MIT",
Expand Down
63 changes: 63 additions & 0 deletions test/inaturalist_api.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,69 @@ describe( "iNaturalistAPI", ( ) => {
} );
} );

describe( "multipartBodyForResuest", ( ) => {
it( "returns FormData if there are custom blob fields", ( ) => {
const requestParameters = {
customField: {
type: "custom",
value: new Blob( )
}
};
const body = iNaturalistAPI.multipartBodyForResuest( requestParameters );
expect( body.constructor.name ).to.eq( "FormData" );
} );

it( "returns FormData if there are blob fields", ( ) => {
const requestParameters = {
blobField: new Blob( )
};
const body = iNaturalistAPI.multipartBodyForResuest( requestParameters );
expect( body.constructor.name ).to.eq( "FormData" );
} );

it( "returns fields as a JSON string", ( ) => {
const fields = {
field1: true,
field2: true
};
const requestParameters = {
fields,
stringField: "string",
blobField: new Blob( )
};
const body = iNaturalistAPI.multipartBodyForResuest( requestParameters );
expect( body.constructor.name ).to.eq( "FormData" );
expect( body.get( "fields" ) ).to.eq( JSON.stringify( fields ) );
} );

it( "returns null if there are no fields that need multipart requests", ( ) => {
const fields = {
field1: true,
field2: true
};
const requestParameters = {
fields,
stringField: "string",
someObject: {
nestedField: 101
}
};
const body = iNaturalistAPI.multipartBodyForResuest( requestParameters );
expect( body ).to.be.null;
} );

it( "strings fields remain strings", ( ) => {
const fields = "all";
const requestParameters = {
fields,
blobField: new Blob( )
};
const body = iNaturalistAPI.multipartBodyForResuest( requestParameters );
expect( body.constructor.name ).to.eq( "FormData" );
expect( body.get( "fields" ) ).to.eq( fields );
} );
} );

describe( "headers", ( ) => {
it( "should include Content-Type for post", done => {
nock( "http://localhost:3000", { reqheaders: { "Content-Type": "application/json" } } )
Expand Down

0 comments on commit cf649ca

Please sign in to comment.