Skip to content

Commit

Permalink
Updated processParams to support #18 handling of empty keys and array…
Browse files Browse the repository at this point in the history
…s of non-objects

Fixed addItemData input type and mix-in ordering #254

Switched to using determineOwner call #254

doc response, add another test

method and interface rename

whoops

Changed Date constructor to see if Travis behaves better #254

Added test for FormData path #254

one more interface name change
  • Loading branch information
MikeTschudi authored and jgravois committed Jul 27, 2018
1 parent 257edb6 commit 39a0710
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
genericInvalidResponse
} from "./mocks/feature";

function attachmentFile() {
export function attachmentFile() {
if (typeof File !== "undefined" && File) {
return new File(["foo"], "foo.txt", { type: "text/plain" });
} else {
Expand Down
43 changes: 28 additions & 15 deletions packages/arcgis-rest-items/src/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export interface IItemIdRequestOptions extends IUserRequestOptions {
owner?: string;
}

export interface IItemJsonRequestOptions extends IItemIdRequestOptions {
export interface IItemDataAddRequestOptions extends IItemIdRequestOptions {
/**
* Object to store
*/
Expand Down Expand Up @@ -97,6 +97,15 @@ export interface ISearchResult {
results: IItem[];
}

export interface IItemUpdateResponse {
success: boolean;
id: string;
}

export interface IItemAddResponse extends IItemUpdateResponse {
folder: string;
}

/**
* Search for items via the portal api
*
Expand Down Expand Up @@ -145,7 +154,7 @@ export function searchItems(
*/
export function createItemInFolder(
requestOptions: IItemAddRequestOptions
): Promise<any> {
): Promise<IItemAddResponse> {
const owner = determineOwner(requestOptions);

const baseUrl = `${getPortalUrl(requestOptions)}/content/users/${owner}`;
Expand Down Expand Up @@ -183,7 +192,7 @@ export function createItemInFolder(
*/
export function createItem(
requestOptions: IItemAddRequestOptions
): Promise<any> {
): Promise<IItemAddResponse> {
// delegate to createItemInFolder placing in the root of the filestore
const options = {
folder: null,
Expand All @@ -196,10 +205,12 @@ export function createItem(
* Send json to an item to be stored as the `/data` resource
*
* @param requestOptions - Options for the request
* @returns A Promise that will resolve with an object reporting
* success/failure and echoing the item id.
*/
export function addItemJsonData(
requestOptions: IItemJsonRequestOptions
): Promise<any> {
requestOptions: IItemDataAddRequestOptions
): Promise<IItemUpdateResponse> {
const owner = determineOwner(requestOptions);
const url = `${getPortalUrl(requestOptions)}/content/users/${owner}/items/${
requestOptions.id
Expand All @@ -209,30 +220,32 @@ export function addItemJsonData(
// a `text` form field. It can also be sent with the `.create` call by sending
// a `.data` property.
requestOptions.params = {
...requestOptions.params,
text: JSON.stringify(requestOptions.data)
text: JSON.stringify(requestOptions.data),
...requestOptions.params
};

return request(url, requestOptions);
}
/**
* Send blob to an item to be stored as the `/data` resource
* Send a file or blob to an item to be stored as the `/data` resource
*
* @param requestOptions - Options for the request
* @returns A Promise that will resolve with an object reporting
* success/failure and echoing the item id.
*/
export function addItemBinaryData(
requestOptions: IItemJsonRequestOptions
): Promise<any> {
const owner = requestOptions.owner || requestOptions.authentication.username;
export function addItemData(
requestOptions: IItemDataAddRequestOptions
): Promise<IItemUpdateResponse> {
const owner = determineOwner(requestOptions);

const url = `${getPortalUrl(requestOptions)}/content/users/${owner}/items/${
requestOptions.id
}/update`;

// Portal API requires that the 'data' be POSTed in a `file` form field.
requestOptions.params = {
...requestOptions.params,
file: requestOptions.data
file: requestOptions.data,
...requestOptions.params
};

return request(url, requestOptions);
Expand Down Expand Up @@ -448,7 +461,7 @@ export function removeItemResource(
* Serialize an item into a json format accepted by the Portal API
* for create and update operations
*
* @param item IItem to be serialized
* @param item Item to be serialized
* @returns a formatted json object to be sent to Portal
*/
function serializeItem(item: IItemAdd | IItemUpdate | IItem): any {
Expand Down
34 changes: 34 additions & 0 deletions packages/arcgis-rest-items/test/items.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import * as fetchMock from "fetch-mock";

import { attachmentFile } from "../../arcgis-rest-feature-service/test/attachments.test";

import {
searchItems,
getItem,
Expand All @@ -9,6 +11,7 @@ import {
createItemInFolder,
updateItem,
addItemJsonData,
addItemData,
protectItem,
unprotectItem,
getItemResources,
Expand Down Expand Up @@ -664,6 +667,37 @@ describe("search", () => {
});
});

it("should add binary item data by id", done => {
fetchMock.once("*", {
success: true
});

addItemData({
id: "3ef",
// File() is only available in the browser
data: attachmentFile(),
...MOCK_USER_REQOPTS
})
.then(() => {
expect(fetchMock.called()).toEqual(true);
const [url, options]: [string, RequestInit] = fetchMock.lastCall("*");
expect(url).toEqual(
"https://myorg.maps.arcgis.com/sharing/rest/content/users/casey/items/3ef/update"
);
expect(options.method).toBe("POST");
expect(options.body instanceof FormData).toBeTruthy();
const params = options.body as FormData;
// need to figure out how to introspect FormData from Node.js
// expect(params.get("token")).toEqual("fake-token");
// expect(params.get("f")).toEqual("json");
// expect(params.get("filename")).toEqual("foo.txt");
done();
})
.catch(e => {
fail(e);
});
});

it("should update an item, including data", done => {
fetchMock.once("*", ItemSuccessResponse);
const fakeItem = {
Expand Down
16 changes: 13 additions & 3 deletions packages/arcgis-rest-request/src/utils/process-params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,12 @@ export function processParams(params: any): any {

Object.keys(params).forEach(key => {
const param = params[key];
if (!param && param !== 0 && typeof param !== "boolean") {
if (
!param &&
param !== 0 &&
typeof param !== "boolean" &&
typeof param !== "string"
) {
return;
}
const type = param.constructor.name;
Expand All @@ -56,10 +61,15 @@ export function processParams(params: any): any {

// properly encodes objects, arrays and dates for arcgis.com and other services.
// ported from https://github.com/Esri/esri-leaflet/blob/master/src/Request.js#L22-L30
// also see https://github.com/Esri/arcgis-rest-js/issues/18
// also see https://github.com/Esri/arcgis-rest-js/issues/18:
// null, undefined, function are excluded. If you want to send an empty key you need to send an empty string "".
switch (type) {
case "Array":
// Based on the first element of the array, classify array as an array of objects to be stringified
// or an array of non-objects to be comma-separated
value =
param[0] &&
param[0].constructor &&
param[0].constructor.name === "Object"
? JSON.stringify(param)
: param.join(",");
Expand All @@ -80,7 +90,7 @@ export function processParams(params: any): any {
value = param;
break;
}
if (value || value === 0) {
if (value || value === 0 || typeof value === "string") {
newParams[key] = value;
}
});
Expand Down
96 changes: 96 additions & 0 deletions packages/arcgis-rest-request/test/utils/encode-form-data.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { encodeFormData } from "../../src/utils/encode-form-data";
import {
requiresFormData,
processParams
} from "../../src/utils/process-params";
import { attachmentFile } from "../../../arcgis-rest-feature-service/test/attachments.test";

describe("encodeFormData", () => {
it("should encode in form data for multipart requests", () => {
Expand All @@ -13,4 +18,95 @@ describe("encodeFormData", () => {

expect(formData instanceof FormData).toBeTruthy();
});

it("should encode as query string for basic types", () => {
const dateValue = 1471417200000;

// null, undefined, function are excluded. If you want to send an empty key you need to send an empty string "".
// See https://github.com/Esri/arcgis-rest-js/issues/18
const params = {
myArray1: new Array(8),
myArray2: [1, 2, 4, 16],
myArray3: [{ a: 1, b: 2 }, { c: "abc" }],
myDate: new Date(dateValue),
myFunction: () => {
return 3.1415;
},
myBoolean: true,
myString: "Hello, world!",
myEmptyString: "",
myNumber: 380
};

expect(requiresFormData(params)).toBeFalsy();

const formData = processParams(params);
expect(typeof formData).toBe("object");
expect(formData.myArray1).toBe(",,,,,,,");
expect(formData.myArray2).toBe("1,2,4,16");
expect(formData.myArray3).toBe('[{"a":1,"b":2},{"c":"abc"}]');
expect(formData.myDate).toBe(dateValue);
expect(formData.myBoolean).toBeTruthy();
expect(formData.myString).toBe("Hello, world!");
expect(formData.myEmptyString).toBe("");
expect(formData.myNumber).toBe(380);

const encodedFormData = encodeFormData(params);
expect(typeof encodedFormData).toBe("string");
expect(encodedFormData).toBe(
"myArray1=%2C%2C%2C%2C%2C%2C%2C&" +
"myArray2=1%2C2%2C4%2C16&" +
"myArray3=%5B%7B%22a%22%3A1%2C%22b%22%3A2%7D%2C%7B%22c%22%3A%22abc%22%7D%5D&" +
"myDate=1471417200000&" +
"myBoolean=true&" +
"myString=Hello%2C%20world!&" +
"myEmptyString=&" +
"myNumber=380"
);
});

it("should switch to form data if any item is not a basic type", () => {
const dateValue = 1471417200000;
const file = attachmentFile();
if (!file.name) {
// The file's name is used for adding files to a form, so supply a name when we're in a testing
// environment that doesn't support File (attachmentFile creates a File with the name "foo.txt"
// if File is supported and a file stream otherwise)
file.name = "foo.txt";
}

// null, undefined, function are excluded. If you want to send an empty key you need to send an empty string "".
// See https://github.com/Esri/arcgis-rest-js/issues/18
const params = {
myArray1: new Array(8),
myArray2: [1, 2, 4, 16],
myArray3: [{ a: 1, b: 2 }, { c: "abc" }],
myDate: new Date(dateValue),
myFunction: () => {
return 3.1415;
},
myBoolean: true,
myString: "Hello, world!",
myEmptyString: "",
myNumber: 380,
file
};

expect(requiresFormData(params)).toBeTruthy();

const formData = processParams(params);
expect(typeof formData).toBe("object");
expect(formData.myArray1).toBe(",,,,,,,");
expect(formData.myArray2).toBe("1,2,4,16");
expect(formData.myArray3).toBe('[{"a":1,"b":2},{"c":"abc"}]');
expect(formData.myDate).toBe(dateValue);
expect(formData.myBoolean).toBeTruthy();
expect(formData.myString).toBe("Hello, world!");
expect(formData.myEmptyString).toBe("");
expect(formData.myNumber).toBe(380);
expect(typeof formData.file).toBe("object");

const encodedFormData = encodeFormData(params);
expect(encodedFormData instanceof FormData).toBeTruthy();
});
});

0 comments on commit 39a0710

Please sign in to comment.