Skip to content

Commit

Permalink
feat(hub-common): add itemToContent and related functions
Browse files Browse the repository at this point in the history
affects: @esri/hub-common, @esri/hub-content
  • Loading branch information
tomwayson committed Sep 17, 2021
1 parent 7aa7ade commit 962a313
Show file tree
Hide file tree
Showing 9 changed files with 295 additions and 286 deletions.
163 changes: 162 additions & 1 deletion packages/common/src/content.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,37 @@
/* Copyright (c) 2019 Environmental Systems Research Institute, Inc.
* Apache-2.0 */
import { IItem } from "@esri/arcgis-rest-portal";
import { HubType, HubFamily, IBBox, IHubGeography } from "./types";
import { collections } from "./collections";
import { categories } from "./categories";
import { categories, isDownloadable } from "./categories";
import { createExtent } from "./extent";
import { includes, isGuid } from "./utils";
import { IHubContent, IModel } from "./types";
import { getProp } from "./objects";
import { getStructuredLicense } from "./items/get-structured-license";

function collectionToFamily(collection: string): string {
const overrides: any = {
other: "content",
solution: "template",
};
return overrides[collection] || collection;
}

function itemExtentToBoundary(extent: IBBox): IHubGeography {
return (
extent &&
extent.length && {
// TODO: center?
geometry: createExtent(
extent[0][0],
extent[0][1],
extent[1][0],
extent[1][1]
),
}
);
}

const cache: { [key: string]: string } = {};

Expand Down Expand Up @@ -324,3 +350,138 @@ export function removeContextFromSlug(slug: string, context: string): string {
return slug;
}
}

/**
* Splits item category strings at slashes and discards the "Categories" keyword
*
* ```
* ["/Categories/Boundaries", "/Categories/Planning and cadastre/Property records", "/Categories/Structure"]
* ```
* Should end up being
* ```
* ["Boundaries", "Planning and cadastre", "Property records", "Structure"]
* ```
*
* @param _categories - an array of strings
* @private
*/
export function parseItemCategories(_categories: string[]) {
if (!_categories) return _categories;

const exclude = ["categories", ""];
const parsed = _categories.map((cat) => cat.split("/"));
const flattened = parsed.reduce((acc, arr, _) => [...acc, ...arr], []);
return flattened.filter((cat) => !includes(exclude, cat.toLowerCase()));
}

/**
* return the Hub family given an item's type
* @param type item type
* @returns Hub family
*/
export function getFamily(type: string) {
let family;
// override default behavior for the rows that are highlighted in yellow here:
// https://esriis.sharepoint.com/:x:/r/sites/ArcGISHub/_layouts/15/Doc.aspx?sourcedoc=%7BADA1C9DC-4F6C-4DE4-92C6-693EF9571CFA%7D&file=Hub%20Routes.xlsx&nav=MTBfe0VENEREQzI4LUZFMDctNEI0Ri04NjcyLThCQUE2MTA0MEZGRn1fezIwMTIwMEJFLTA4MEQtNEExRC05QzA4LTE5MTAzOUQwMEE1RH0&action=default&mobileredirect=true&cid=df1c874b-c367-4cea-bc13-7bebfad3f2ac
switch ((type || "").toLowerCase()) {
case "image service":
family = "dataset";
break;
case "feature service":
case "raster layer":
// TODO: check if feature service has > 1 layer first?
family = "map";
break;
case "microsoft excel":
family = "document";
break;
case "cad drawing":
case "feature collection template":
case "report template":
family = "content";
break;
default:
// by default derive from collection
family = collectionToFamily(getCollection(type));
}
return family as HubFamily;
}

/**
* DEPRECATED: Use getFamily() instead.
*
* get the HubType for a given item or item type
*
* @param itemOrType an item or item.type
*/
export function getItemHubType(itemOrType: IItem | string): HubType {
/* tslint:disable no-console */
console.warn(
"DEPRECATED: Use getFamily() instead. getItemHubType() will be removed at v9.0.0"
);
/* tslint:enable no-console */
if (typeof itemOrType === "string") {
itemOrType = { type: itemOrType } as IItem;
}
const itemType = normalizeItemType(itemOrType);
// TODO: not all categories are Hub types, may need to validate
return getCollection(itemType) as HubType;
}

/**
* Convert a Portal item to Hub content
*
* @param item Portal Item
* @returns Hub content
* @export
*/
export function itemToContent(item: IItem): IHubContent {
const createdDate = new Date(item.created);
const createdDateSource = "item.created";
const properties = item.properties;
const normalizedType = normalizeItemType(item);
const content = Object.assign({}, item, {
// no server errors when fetching the item directly
errors: [],
// store a reference to the item
item,
// NOTE: this will overwrite any existing item.name, which is
// The file name of the item for file types. Read-only.
// presumably there to use as the default file name when downloading
// we don't store item.name in the Hub API and we use name for title
name: item.title,
family: getFamily(normalizedType),
// TODO: hubType is no longer used, remove it at next breaking change
hubType: getItemHubType(item),
normalizedType,
categories: parseItemCategories(item.categories),
itemCategories: item.categories,
// can we strip HTML from description, and do we need to trim it to a X chars?
summary: item.snippet || item.description,
publisher: {
name: item.owner,
username: item.owner,
},
permissions: {
visibility: item.access,
control: item.itemControl || "view",
},
// Hub app configuration metadata from item properties
actionLinks: properties && properties.links,
hubActions: properties && properties.actions,
metrics: properties && properties.metrics,
isDownloadable: isDownloadable(item),
// default boundary from item.extent
boundary: itemExtentToBoundary(item.extent),
license: { name: "Custom License", description: item.accessInformation },
// dates and sources we will enrich these later...
createdDate,
createdDateSource,
publishedDate: createdDate,
publishedDateSource: createdDateSource,
updatedDate: new Date(item.modified),
updatedDateSource: "item.modified",
structuredLicense: getStructuredLicense(item.licenseInfo),
});
return content;
}
112 changes: 112 additions & 0 deletions packages/common/test/content.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { IItem } from "@esri/arcgis-rest-portal";
import { IEnvelope } from "@esri/arcgis-rest-types";
import {
getCategory,
getCollection,
Expand All @@ -12,8 +14,15 @@ import {
isSlug,
addContextToSlug,
removeContextFromSlug,
itemToContent,
parseItemCategories,
getItemHubType,
getFamily,
} from "../src/content";
import { IHubContent } from "../src/types";
import { cloneObject } from "../src/util";
import * as documentItem from "./mocks/items/document.json";
import * as mapServiceItem from "./mocks/items/map-service.json";

describe("getCollection", () => {
it("can abort", () => {
Expand Down Expand Up @@ -417,3 +426,106 @@ describe("Slug Helpers", () => {
});
});
});

describe("get item family", () => {
it("returns dataset for image service", () => {
expect(getFamily("Image Service")).toBe("dataset");
});
it("returns map for feature service and raster layer", () => {
expect(getFamily("Feature Service")).toBe("map");
expect(getFamily("Raster Layer")).toBe("map");
});
it("returns document for excel", () => {
expect(getFamily("Microsoft Excel")).toBe("document");
});
it("returns template for solution", () => {
expect(getFamily("Solution")).toBe("template");
});
it("returns content for other specific types", () => {
expect(getFamily("CAD Drawing")).toBe("content");
expect(getFamily("Feature Collection Template")).toBe("content");
expect(getFamily("Report Template")).toBe("content");
});
it("returns content for collection other", () => {
expect(getFamily("360 VR Experience")).toBe("content");
});
});

describe("get item hub type", () => {
it("normalizes item", () => {
expect(
getItemHubType({
type: "Hub Initiative",
typeKeywords: ["hubInitiativeTemplate"],
} as IItem)
).toBe("template");
});
it("works with just type", () => {
expect(getItemHubType("Form")).toBe("feedback");
});
});

describe("parse item categories", () => {
it("parses the categories", () => {
const categories = [
"/Categories/Boundaries",
"/Categories/Planning and cadastre/Property records",
"/Categories/Structure",
];
expect(parseItemCategories(categories)).toEqual([
"Boundaries",
"Planning and cadastre",
"Property records",
"Structure",
]);
});
it("doesn't blow up with undefined", () => {
expect(() => parseItemCategories(undefined)).not.toThrow();
});
});

describe("item to content", () => {
let item: IItem;
beforeEach(() => {
item = cloneObject(documentItem) as IItem;
});
it("gets summary from description when no snippet", () => {
item.snippet = null;
const content = itemToContent(item);
expect(content.summary).toBe(item.description);
});
it("gets permissions.control from itemControl when it exists", () => {
item.itemControl = "update";
const content = itemToContent(item);
expect(content.permissions.control).toBe(item.itemControl);
});
describe("when item has properties", () => {
it("should set actionLinks to links", () => {
item.properties = {
links: [{ url: "https://foo.com" }],
};
const content = itemToContent(item);
expect(content.actionLinks).toEqual(item.properties.links);
});
});
it("has a reference to the item", () => {
const content = itemToContent(item);
expect(content.item).toBe(item);
});
it("has a boundary when the item has a valid extent", () => {
item = cloneObject(mapServiceItem) as IItem;
const content = itemToContent(item);
const geometry: IEnvelope = {
xmin: -2.732,
ymin: 53.4452,
xmax: -2.4139,
ymax: 53.6093,
spatialReference: {
wkid: 4326,
},
};
expect(content.boundary).toEqual({ geometry });
});
// NOTE: other use cases (including when a portal is passed)
// are covered by getContentFromPortal() tests
});
11 changes: 8 additions & 3 deletions packages/content/src/content.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
/* Copyright (c) 2018 Environmental Systems Research Institute, Inc.
* Apache-2.0 */

import { getItemHubId, IHubContent, IModel } from "@esri/hub-common";
import {
getItemHubId,
itemToContent,
IHubContent,
IModel,
} from "@esri/hub-common";
import { IGetContentOptions, getContentFromHub } from "./hub";
import { getContentFromPortal, itemToContent } from "./portal";
import { getContentFromPortal } from "./portal";
import { enrichContent } from "./enrichments";
import { isSlug, parseDatasetId } from "./slugs";

Expand All @@ -23,7 +28,7 @@ function getContentById(
const { itemId } = parseDatasetId(identifier);
getContentPromise = getContentFromPortal(itemId, options);
} else {
getContentPromise = getContentFromHub(identifier, options).catch(e => {
getContentPromise = getContentFromHub(identifier, options).catch((e) => {
// dataset is not in index (i.e. might be a private item)
if (!isSlug(identifier)) {
// try fetching from portal instead
Expand Down
3 changes: 2 additions & 1 deletion packages/content/src/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import {
IHubContent,
cloneObject,
getServiceTypeFromUrl,
getFamily,
hubApiRequest,
itemToContent,
mergeObjects,
} from "@esri/hub-common";
import { getFamily, itemToContent } from "./portal";
import { isSlug, addContextToSlug, parseDatasetId } from "./slugs";
import { enrichContent, IEnrichContentOptions } from "./enrichments";
import { isExtentCoordinateArray } from "@esri/hub-common";
Expand Down
Loading

0 comments on commit 962a313

Please sign in to comment.