Skip to content

Commit

Permalink
CMR-10184: Add link to collection if a STAC API is present in the rel…
Browse files Browse the repository at this point in the history
…ated urls of collection metadata. (#356)

* Added documentation for running tests, added implementation and tests for inserting STAC API as rel=items link at the collection level if an appropriate RelatedURL exista

* Added browse implementation and tests to provide the appropriate STAC API if a CMR collection has defined one.

* Remove console logging

* Added comments and used type definitions

* Fixed typos

* Prettified

* Better testing

* Update src/routes/browse.ts

Co-authored-by: Matthew Crouch <matthew.a.crouch@gmail.com>

* Update src/domains/collections.ts

Co-authored-by: Matthew Crouch <matthew.a.crouch@gmail.com>

* Added linting instructions

* Suggested modifications applied

* Linting

---------

Co-authored-by: doug-newman-nasa <douglas.j.newamn@nasa.gov>
Co-authored-by: Matthew Crouch <matthew.a.crouch@gmail.com>
  • Loading branch information
3 people authored Oct 9, 2024
1 parent 0590a70 commit 2449123
Show file tree
Hide file tree
Showing 6 changed files with 222 additions and 6 deletions.
12 changes: 12 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ Then install dependencies with npm:
npm install
```

To run the unit test suite associated with CMR-STAC:

```bash
npm test
```

To lint your developed code:

```bash
npm run prettier:fix
```

To run the CMR-STAC server locally:

```bash
Expand Down
38 changes: 38 additions & 0 deletions src/__tests__/providerCollection.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { createApp } from "../app";
const app = createApp();

import { generateSTACCollections } from "../utils/testUtils";
import { Link } from "../@types/StacCatalog";

const emptyCollections = { facets: null, count: 0, cursor: "", items: [] };

Expand Down Expand Up @@ -94,6 +95,43 @@ describe("GET /:provider/collections", () => {
});
});

describe("given a provider with two collections, one of which is a collection containing a link to a STAC item API", () => {
it("returns collections with links of rel=items to the appropriate endpoints", async () => {
sandbox
.stub(Providers, "getProviders")
.resolves([null, [{ "provider-id": "TEST", "short-name": "TEST" }]]);

const mockCollections = generateSTACCollections(2);

const link = {
rel: "items",
href: "https://brazildatacube.dpi.inpe.br/stac/collections/MOSAIC-S2-YANOMAMI-6M-1",
type: "application/json",
};
mockCollections[0].links.push(link);

sandbox.stub(Collections, "getCollections").resolves({
count: 2,
cursor: null,
items: mockCollections,
});

const { statusCode, body } = await request(app).get("/stac/TEST/collections");

expect(statusCode).to.equal(200);
expect(body.collections).to.have.lengthOf(2);

// Get the links of rel=item for the first collection
const link0: Link = body.collections[0].links.find((l: Link) => l.rel === "items");
expect(link0.href).to.equal(
"https://brazildatacube.dpi.inpe.br/stac/collections/MOSAIC-S2-YANOMAMI-6M-1"
);
// Get the links of rel=item for the second collection
const link1: Link = body.collections[1].links.find((l: Link) => l.rel === "items");
expect(link1.href).to.contain("/stac/TEST/collections/");
});
});

describe("datetime parameter", () => {
it("should return collections within the specified datetime range", async () => {
sandbox
Expand Down
113 changes: 113 additions & 0 deletions src/domains/__tests__/collections.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const { expect } = chai;
import * as gql from "graphql-request";
import { getCollections, collectionToStac } from "../collections";
import { generateCollections } from "../../utils/testUtils";
import { UrlContentType } from "../../models/GraphQLModels";

const sandbox = sinon.createSandbox();

Expand All @@ -29,6 +30,118 @@ describe("getCollections", () => {
});

describe("collectionsToStac", () => {
describe("given a collection with a related url describing a STAC items endpoint", () => {
it("should return a STAC collection with a link of relation 'items' pointing to that endpoint", () => {
const [base] = generateCollections(1);
// Add in a related url as above
const relatedUrl = {
description: "foo",
urlContentType: UrlContentType.DISTRIBUTION_URL,
type: "GET CAPABILITIES",
subtype: "STAC",
url: "https://data.inpe.br/bdc/stac/v1/collections/AMZ1-WFI-L4-SR-1",
getData: {
format: "Not provided",
mimeType: "application/json",
size: 0.0,
unit: "KB",
},
};
base.relatedUrls?.push(relatedUrl);

const stacCollection: any = collectionToStac(base);

expect(stacCollection).to.have.deep.property("links", [
{
rel: "license",
href: "https://science.nasa.gov/earth-science/earth-science-data/data-information-policy",
title: "EOSDIS Data Use Policy",
type: "text/html",
},
{
rel: "about",
href: "undefined/search/concepts/C00000000-TEST_PROV.html",
title: "HTML metadata for collection",
type: "text/html",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.native",
title: "Native metadata for collection",
type: "application/xml",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.echo10",
title: "ECHO10 metadata for collection",
type: "application/echo10+xml",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.json",
title: "CMR JSON metadata for collection",
type: "application/json",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.umm_json",
title: "CMR UMM_JSON metadata for collection",
type: "application/vnd.nasa.cmr.umm+json",
},
{
rel: "items",
href: "https://data.inpe.br/bdc/stac/v1/collections/AMZ1-WFI-L4-SR-1",
type: "application/json",
},
]);
});
});
describe("given a collection without a related url describing a STAC items endpoint", () => {
it("should return a STAC collection without a link of relation 'items' pointing to that endpoint", () => {
const [base] = generateCollections(1);

const stacCollection: any = collectionToStac(base);

expect(stacCollection).to.have.deep.property("links", [
{
rel: "license",
href: "https://science.nasa.gov/earth-science/earth-science-data/data-information-policy",
title: "EOSDIS Data Use Policy",
type: "text/html",
},
{
rel: "about",
href: "undefined/search/concepts/C00000000-TEST_PROV.html",
title: "HTML metadata for collection",
type: "text/html",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.native",
title: "Native metadata for collection",
type: "application/xml",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.echo10",
title: "ECHO10 metadata for collection",
type: "application/echo10+xml",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.json",
title: "CMR JSON metadata for collection",
type: "application/json",
},
{
rel: "via",
href: "undefined/search/concepts/C00000000-TEST_PROV.umm_json",
title: "CMR UMM_JSON metadata for collection",
type: "application/vnd.nasa.cmr.umm+json",
},
]);
});
});
describe("given a collection with S3 Links", () => {
describe("given the S3 links are badly formatted with commas", () => {
it("should return a STAC collection with the s3 links as assets", () => {
Expand Down
41 changes: 40 additions & 1 deletion src/domains/collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import {
CollectionsInput,
GraphQLHandler,
GraphQLResults,
RelatedUrlType,
RelatedUrlSubType,
} from "../models/GraphQLModels";

import { cmrSpatialToExtent } from "./bounding-box";
Expand Down Expand Up @@ -95,8 +97,29 @@ const extractLicense = (_collection: Collection) => {
return { license, licenseLink };
};

/**
* Examing a collections related URLs to see if it contains a reference to a STAC catalog.
* If the collection has a RelatedURL of type: "GET CAPABILITIES",
* and subtype: "STAC" then that URL should be placed in the href for the items link.
*
* @param collection the collection object from a CMR GraphQL result
*
* @returns a string representing the URL of the item Catalog described by the collection
* or NULL if the related URL does not exist.
*/
const itemCatalogUrl = (collection: Collection) => {
const { relatedUrls } = collection;

const relatedUrl = relatedUrls?.find(
(relatedUrl) =>
relatedUrl.type == RelatedUrlType.GET_CAPABILITIES &&
relatedUrl.subtype == RelatedUrlSubType.STAC
);
return relatedUrl?.url;
};

const generateCollectionLinks = (collection: Collection, links: Links) => {
return [
const collectionLinks = [
...links,
{
rel: "about",
Expand Down Expand Up @@ -129,6 +152,22 @@ const generateCollectionLinks = (collection: Collection, links: Links) => {
type: "application/vnd.nasa.cmr.umm+json",
},
];
/* A CMR collection can now indicate to consumers that it has a STAC API.
* If that is the case then we use that link instead of a generic CMR one.
* This is useful for collections that do not index their granule
* metadata in CMR, like CWIC collection. If there is one present,
* it needs to be added as an 'item' link. If not, let browse.ts add a
* generic one in CMR STAC
*/
const catalogUrl = itemCatalogUrl(collection);
if (catalogUrl != null) {
collectionLinks.push({
rel: "items",
href: catalogUrl,
type: "application/json",
});
}
return collectionLinks;
};

const createKeywords = (collection: Collection): Keywords => {
Expand Down
1 change: 1 addition & 0 deletions src/models/GraphQLModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ export enum RelatedUrlSubType {
DATA_TREE = "DATA TREE",
EARTHDATA_SEARCH = "Earthdata Search",
GIOVANNI = "GIOVANNI",
STAC = "STAC",
}

export type UseConstraints =
Expand Down
23 changes: 18 additions & 5 deletions src/routes/browse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,24 @@ export const collectionsHandler = async (req: Request, res: Response): Promise<v
href: encodeURI(stacRoot),
type: "application/json",
});
collection.links.push({
rel: "items",
href: `${baseUrl}/${encodeURIComponent(collection.id)}/items`,
type: "application/json",
});
/* A CMR collection can now indicate to consumers that it has a STAC API.
* If that is the case then we use that link instead of a generic CMR one.
* This is useful of collections that do not index their granule
* metadata in CMR, like CWIC collection.
* If the list of links of does not contain a link of type 'items' then
* add the default items element
*/
const { links } = collection;

const itemsLink = links.find((link) => link.rel === "items");

if (!itemsLink) {
collection.links.push({
rel: "items",
href: `${baseUrl}/${encodeURIComponent(collection.id)}/items`,
type: "application/json",
});
}
});

const links = collectionLinks(req, cursor);
Expand Down

0 comments on commit 2449123

Please sign in to comment.