diff --git a/package-lock.json b/package-lock.json index 643962e7..c1edbf77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "helmet": "^6.2.0", "lodash": "^4.17.21", "morgan": "^1.10.0", + "qs": "^6.13.0", "semver": "^6.3.1", "source-map-support": "^0.5.21" }, @@ -10693,7 +10694,6 @@ "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, diff --git a/package.json b/package.json index 865204ee..5d74d5be 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "helmet": "^6.2.0", "lodash": "^4.17.21", "morgan": "^1.10.0", + "qs": "^6.13.0", "semver": "^6.3.1", "source-map-support": "^0.5.21" } diff --git a/src/domains/__tests__/stac.spec.ts b/src/domains/__tests__/stac.spec.ts index 741f224b..6ab5cf63 100644 --- a/src/domains/__tests__/stac.spec.ts +++ b/src/domains/__tests__/stac.spec.ts @@ -1,7 +1,8 @@ import chai from "chai"; const { expect } = chai; +import { stringify as stringifyQuery } from "qs"; -import { buildQuery, sortByToSortKeys, stringifyQuery, browseAssets } from "../stac"; +import { buildQuery, sortByToSortKeys, browseAssets } from "../stac"; import { RelatedUrlType, UrlContentType } from "../../models/GraphQLModels"; import { SortObject } from "../../models/StacModels"; @@ -451,7 +452,7 @@ describe("stringifyQuery", () => { { query: { limit: 2 }, queryString: "limit=2" }, { query: { bbox: [-180, -90, 180, 90] }, - queryString: "bbox=-180,-90,180,90", + queryString: "bbox[0]=-180&bbox[1]=-90&bbox[2]=180&bbox[3]=90", }, { query: { bbox: "-180,-90,180,90" }, @@ -463,11 +464,11 @@ describe("stringifyQuery", () => { }, { query: { collections: ["C1234-PROV1", "C5468-PROV1"] }, - queryString: "collections=C1234-PROV1,C5468-PROV1", + queryString: "collections[0]=C1234-PROV1&collections[1]=C5468-PROV1", }, { query: { ids: ["G1234-PROV1", "G5468-PROV1"] }, - queryString: "ids=G1234-PROV1,G5468-PROV1", + queryString: "ids[0]=G1234-PROV1&ids[1]=G5468-PROV1", }, { query: { query: { "eo:cloud_cover": { gt: 50 } } }, @@ -477,6 +478,29 @@ describe("stringifyQuery", () => { query: { query: { "eo:cloud_cover": { gt: 50, lt: 95 } } }, queryString: "query[eo:cloud_cover][gt]=50&query[eo:cloud_cover][lt]=95", }, + { + query: { intersects: { coordinates: ["100,0,101,1", "102,2,103,3"] } }, + queryString: + "intersects[coordinates][0]=100,0,101,1&intersects[coordinates][1]=102,2,103,3", + }, + { + query: { + intersects: { + geometries: [ + { type: "Point", coordinates: [100.0, 0.0] }, + { + type: "LineString", + coordinates: [ + [101.0, 0.0], + [102.0, 1.0], + ], + }, + ], + }, + }, + queryString: + "intersects[geometries][0][type]=Point&intersects[geometries][0][coordinates][0]=100&intersects[geometries][0][coordinates][1]=0&intersects[geometries][1][type]=LineString&intersects[geometries][1][coordinates][0][0]=101&intersects[geometries][1][coordinates][0][1]=0&intersects[geometries][1][coordinates][1][0]=102&intersects[geometries][1][coordinates][1][1]=1", + }, ].forEach(({ query, queryString }) => { describe(`given query of ${JSON.stringify(query)}`, () => { it(`should return ${queryString}`, () => { diff --git a/src/domains/stac.ts b/src/domains/stac.ts index e2358cb1..426baeb7 100644 --- a/src/domains/stac.ts +++ b/src/domains/stac.ts @@ -1,6 +1,6 @@ import { Request } from "express"; import { IncomingHttpHeaders } from "http"; -import { flattenDeep, isPlainObject } from "lodash"; +import { flattenDeep } from "lodash"; import { request } from "graphql-request"; import { @@ -23,7 +23,6 @@ import { import { SortObject, StacQuery } from "../models/StacModels"; import { getAllCollectionIds } from "./collections"; import { - flattenTree, mergeMaybe, buildClientId, extractAssetMapKey, @@ -518,31 +517,6 @@ export const buildQuery = async (req: Request) => { ); }; -export type SimpleMap = { [key: string]: unknown }; -/** - * Convert a JSON query structure to an array style query string. - * - * @example - * stringifyQuery({provider:"my_prov", query:{"eo:cloud_cover": {"gt": 60}}}) - * => "provider=my_prov&query[eo:cloud_cover][gt]=60" - */ -export const stringifyQuery = (input: { [key: string]: unknown }) => { - const queryParams = new URLSearchParams(); - - Object.keys(input).forEach((key) => { - if (isPlainObject(input[key])) { - flattenTree(input[key] as SimpleMap).forEach((leaf: { key: string[]; value: unknown }) => { - const deepKeys = leaf.key.map((k: string) => `[${k}]`).join(""); - queryParams.set(`${key}${deepKeys}`, leaf.value as string); - }); - } else { - queryParams.set(key, input[key] as string); - } - }); - - return queryParams.toString(); -}; - /** * Query GraphQL with built in pagination handling and return the results. * Provide a results handler to handle the results of the query. diff --git a/src/routes/browse.ts b/src/routes/browse.ts index 7cbe99df..93acc72c 100644 --- a/src/routes/browse.ts +++ b/src/routes/browse.ts @@ -1,9 +1,10 @@ import { Request, Response } from "express"; +import { stringify as stringifyQuery } from "qs"; import { Links } from "../@types/StacCatalog"; import { getCollections } from "../domains/collections"; -import { buildQuery, stringifyQuery } from "../domains/stac"; +import { buildQuery } from "../domains/stac"; import { ItemNotFound } from "../models/errors"; import { getBaseUrl, mergeMaybe, stacContext } from "../utils"; import { STACCollection } from "../@types/StacCollection"; diff --git a/src/routes/catalog.ts b/src/routes/catalog.ts index d378abc3..080e1675 100644 --- a/src/routes/catalog.ts +++ b/src/routes/catalog.ts @@ -1,4 +1,5 @@ import { Request, Response } from "express"; +import { stringify as stringifyQuery } from "qs"; import { Links, STACCatalog } from "../@types/StacCatalog"; @@ -6,7 +7,7 @@ import { getAllCollectionIds } from "../domains/collections"; import { conformance } from "../domains/providers"; import { ServiceUnavailableError } from "../models/errors"; import { getBaseUrl, mergeMaybe, stacContext } from "../utils"; -import { CMR_QUERY_MAX, stringifyQuery } from "../domains/stac"; +import { CMR_QUERY_MAX } from "../domains/stac"; const STAC_VERSION = process.env.STAC_VERSION ?? "1.0.0"; diff --git a/src/routes/items.ts b/src/routes/items.ts index 9a77fed5..e51beece 100644 --- a/src/routes/items.ts +++ b/src/routes/items.ts @@ -1,7 +1,8 @@ import { Request, Response } from "express"; +import { stringify as stringifyQuery } from "qs"; import { addProviderLinks, getItems } from "../domains/items"; -import { buildQuery, stringifyQuery } from "../domains/stac"; +import { buildQuery } from "../domains/stac"; import { ItemNotFound } from "../models/errors"; import { mergeMaybe, stacContext, WEEK_IN_MS } from "../utils/index"; diff --git a/src/routes/search.ts b/src/routes/search.ts index 7df5f345..bce211f3 100644 --- a/src/routes/search.ts +++ b/src/routes/search.ts @@ -1,9 +1,10 @@ import { Request, Response } from "express"; +import { stringify as stringifyQuery } from "qs"; import { Link, STACItem } from "../@types/StacItem"; import { addProviderLinks, getItems } from "../domains/items"; -import { buildQuery, stringifyQuery } from "../domains/stac"; +import { buildQuery } from "../domains/stac"; import { mergeMaybe, stacContext } from "../utils"; const searchLinks = (req: Request, nextCursor: string | null): Link[] => {