Skip to content

Commit

Permalink
CMR-10132: Fix stac-browser Stac item and collection search unmapped …
Browse files Browse the repository at this point in the history
…CMR sort keys (#370)

* CMR-10232: Fixing sort errors on radian-earth; passing searchType field and normalizing datetime param to cmr startDate field

* CMR-10232: Fix prettier and add pull request template

* CMR-10232: Update README
  • Loading branch information
eudoroolivares2016 authored Nov 7, 2024
1 parent 7a0fcb8 commit 5d86a25
Show file tree
Hide file tree
Showing 11 changed files with 120 additions and 27 deletions.
37 changes: 37 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Overview

### What is the feature?

Please summarize the feature or fix.

### What is the Solution?

Summarize what you changed.

### What areas of the application does this impact?

List impacted areas.

# Testing

### Reproduction steps

- **Environment for testing:**
- **Collection to test with:**

1. Step 1
2. Step 2...

### Attachments

Please include relevant screenshots or files that would be helpful in reviewing and verifying this change.

# Checklist

- [ ] I have added automated tests that prove my fix is effective or that my feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have performed a self-review of my own code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings

5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ CMR-STAC follows the STAC API 1.0.0-beta.1 specification, see the

### Navigating
CMR-STAC can be navigated manually using the endpoints provided above, or you can utilize available STAC software to browse and use the API.


A common STAC utility is Radiant Earth's `stac-browser` to use this tool against your development server navigate to
```radiantearth.github.io/stac-browser/#/external/http:/localhost:3000/stac?.language=en```

See the [Usage Documentation](docs/usage/usage.md) for examples of how to interact with the API and search for data.

### Limitations
Expand Down
37 changes: 27 additions & 10 deletions src/domains/__tests__/stac.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ describe("sortByToSortKeys", () => {
{ input: "id", output: ["entryId"] },
{ input: "-id", output: ["-entryId"] },
{ input: "title", output: ["entryTitle"] },
{ input: "datetime", output: ["startDate"] },
{ input: "-datetime", output: ["-startDate"] },
{ input: "-title", output: ["-entryTitle"] },
{ input: "someOtherField", output: ["someOtherField"] },
{ input: "-someOtherField", output: ["-someOtherField"] },
Expand All @@ -431,19 +433,34 @@ describe("sortByToSortKeys", () => {
].forEach(({ input, output }) => {
describe(`given sortby=${input}`, () => {
it("should return the corresponding sortKey", () => {
expect(sortByToSortKeys(input)).to.deep.equal(output);
});

it("should handle object-based sort specifications", () => {
const input: SortObject[] = [
{ field: "properties.eo:cloud_cover", direction: "desc" },
{ field: "id", direction: "asc" },
{ field: "title", direction: "desc" },
];
expect(sortByToSortKeys(input)).to.deep.equal(["-cloudCover", "entryId", "-entryTitle"]);
expect(sortByToSortKeys(input, "collection")).to.deep.equal(output);
});
});
});

it("should handle object-based sort specifications", () => {
const input: SortObject[] = [
{ field: "properties.eo:cloud_cover", direction: "desc" },
{ field: "id", direction: "asc" },
{ field: "title", direction: "desc" },
];
expect(sortByToSortKeys(input, "collection")).to.deep.equal([
"-cloudCover",
"entryId",
"-entryTitle",
]);
});

it("should handle item searching differences", () => {
const input: SortObject[] = [
{ field: "id", direction: "asc" },
{ field: "id", direction: "desc" },
];
expect(sortByToSortKeys(input, "item")).to.deep.equal([
"readableGranuleName",
"-readableGranuleName",
]);
});
});

describe("stringifyQuery", () => {
Expand Down
29 changes: 19 additions & 10 deletions src/domains/stac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { dateTimeToRange } from "../utils/datetime";

import { AssetLinks } from "../@types/StacCollection";
import { Collection, Granule, RelatedUrlType } from "../models/GraphQLModels";
import { parseSortFields } from "../utils/sort";
import { parseSortFields, mapIdSortKey } from "../utils/sort";

const CMR_ROOT = process.env.CMR_URL;

Expand Down Expand Up @@ -340,7 +340,10 @@ const bboxQuery = (_req: Request, query: StacQuery) => ({
/**
* Returns a list of sortKeys from the sortBy property
*/
export const sortByToSortKeys = (sortBys?: string | SortObject[] | string[]): string[] => {
export const sortByToSortKeys = (
sortBys?: string | SortObject[] | string[],
searchType = ""
): string[] => {
const baseSortKeys: string[] = parseSortFields(sortBys);

return baseSortKeys.reduce((sortKeys, sortBy) => {
Expand All @@ -350,15 +353,16 @@ export const sortByToSortKeys = (sortBys?: string | SortObject[] | string[]): st
const cleanSortBy = isDescending ? sortBy.slice(1) : sortBy;
// Allow for `properties` prefix
const fieldName = cleanSortBy.replace(/^properties\./, "");

let mappedField;

if (fieldName.match(/^eo:cloud_cover$/i)) {
mappedField = "cloudCover";
} else if (fieldName.match(/^id$/i)) {
mappedField = "entryId";
mappedField = mapIdSortKey(searchType);
} else if (fieldName.match(/^title$/i)) {
mappedField = "entryTitle";
} else if (fieldName.match(/^datetime$/i)) {
// If descending `-start_date` will sort by newest first
mappedField = "startDate";
} else {
mappedField = fieldName;
}
Expand All @@ -367,10 +371,16 @@ export const sortByToSortKeys = (sortBys?: string | SortObject[] | string[]): st
}, [] as string[]);
};

const sortKeyQuery = (_req: Request, query: StacQuery) => ({
// Use the sortByToSortKeys function to convert STAC sortby to CMR sortKey
sortKey: sortByToSortKeys(query.sortby),
});
const sortKeyQuery = (req: Request, query: StacQuery) => {
const {
params: { searchType },
} = req;

return {
// Use the sortByToSortKeys function to convert STAC sortby to CMR sortKey
sortKey: sortByToSortKeys(query.sortby, searchType),
};
};

const idsQuery = (req: Request, query: StacQuery) => {
const {
Expand Down Expand Up @@ -494,7 +504,6 @@ export const buildQuery = async (req: Request) => {
const {
params: { providerId: provider },
} = req;

const query = mergeMaybe(req.query, req.body);

const queryBuilders = [
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ const validFreeText = (freeText: string) => {
return false;
};

const VALID_SORT_FIELDS = ["startDate", "endDate", "id", "title", "eo:cloud_cover"];
const VALID_SORT_FIELDS = ["startDate", "endDate", "id", "title", "eo:cloud_cover", "datetime"];

const validSortBy = (sortBy: string | string[] | SortObject[]) => {
const fields: string[] = parseSortFields(sortBy);
Expand Down
2 changes: 1 addition & 1 deletion src/routes/__tests__/browse.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ describe("addItemLinkIfPresent", () => {
const numberoOfLinks = stacCollection.links.length;
// Invoke method
addItemLinkIfPresent(stacCollection, "https://foo.com/items");
// Observe no addiitonal link in the STAC Collection and that the item link remains a CMR link
// Observe no additional link in the STAC Collection and that the item link remains a CMR link
expect(stacCollection.links.length).to.equal(numberoOfLinks);
expect(stacCollection).to.have.deep.property("links", [
{
Expand Down
2 changes: 1 addition & 1 deletion src/routes/browse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const collectionLinks = (req: Request, nextCursor?: string | null): Links => {

export const collectionsHandler = async (req: Request, res: Response): Promise<void> => {
const { headers } = req;

req.params.searchType = "collection";
const query = await buildQuery(req);

const { cursor, items: collections } = await getCollections(query, {
Expand Down
4 changes: 2 additions & 2 deletions src/routes/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ export const singleItemHandler = async (req: Request, res: Response) => {
params: { collectionId, itemId },
} = req;

req.params.searchType = "item";
const itemQuery = await buildQuery(req);

const {
items: [item],
} = await getItems(itemQuery, { headers });
Expand Down Expand Up @@ -74,8 +74,8 @@ export const multiItemHandler = async (req: Request, res: Response) => {
);
}

req.params.searchType = "item";
const itemQuery = await buildQuery(req);

const links = generateLinks(req);

const {
Expand Down
1 change: 1 addition & 0 deletions src/routes/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ const searchLinks = (req: Request, nextCursor: string | null): Link[] => {

export const searchHandler = async (req: Request, res: Response): Promise<void> => {
const { headers } = req;
req.params.searchType = "item";
const gqlQuery = await buildQuery(req);

const itemsResponse = await getItems(gqlQuery, { headers });
Expand Down
18 changes: 17 additions & 1 deletion src/utils/__tests__/sort.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from "chai";
import { parseSortFields } from "../sort";
import { parseSortFields, mapIdSortKey } from "../sort";
import { SortObject } from "../../models/StacModels";

describe("parseSortFields", () => {
Expand Down Expand Up @@ -46,3 +46,19 @@ describe("parseSortFields", () => {
expect(parseSortFields(input)).to.deep.equal(["field1", "", "-field3"]);
});
});

describe("mapIdSortKey", () => {
it("should return a valid cmr sort value based on searchType", () => {
const collectionMappedKey = mapIdSortKey("collection");
expect(collectionMappedKey).to.equal("entryId");

const itemMappedKey = mapIdSortKey("item");
expect(itemMappedKey).to.equal("readableGranuleName");

const unmappedKey = mapIdSortKey("anything");
expect(unmappedKey).to.equal("");

const emptyKey = mapIdSortKey();
expect(emptyKey).to.equal("");
});
});
10 changes: 10 additions & 0 deletions src/utils/sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ export const parseSortFields = (sortBys?: string | string[] | SortObject[]): str

return [];
};

export const mapIdSortKey = (searchType = ""): string => {
if (searchType === "collection") {
return "entryId";
} else if (searchType === "item") {
return "readableGranuleName";
}

return "";
};

0 comments on commit 5d86a25

Please sign in to comment.