From cbe545f9660a38d880cdcb3d8d9dd3501f66cd2f Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Tue, 24 Sep 2024 14:23:25 -0400 Subject: [PATCH 1/7] HARMONY-1889: Add support for requesting time or area averaging in preparation for Giovanni services. --- .../ogc-coverages/get-coverage-rangeset.ts | 3 +- .../app/frontends/ogc-edr/get-data-common.ts | 3 +- services/harmony/app/models/data-operation.ts | 2 + .../app/models/services/base-service.ts | 4 + services/harmony/app/models/services/index.ts | 98 +++++++++++++++++++ .../1.0.0/ogc-api-coverages-v1.0.0.yml | 11 +++ .../ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml | 26 +++++ .../harmony/app/util/parameter-parsers.ts | 19 ++++ 8 files changed, 164 insertions(+), 2 deletions(-) diff --git a/services/harmony/app/frontends/ogc-coverages/get-coverage-rangeset.ts b/services/harmony/app/frontends/ogc-coverages/get-coverage-rangeset.ts index f8ebe6cbe..5b5ab8434 100644 --- a/services/harmony/app/frontends/ogc-coverages/get-coverage-rangeset.ts +++ b/services/harmony/app/frontends/ogc-coverages/get-coverage-rangeset.ts @@ -2,7 +2,7 @@ import { NextFunction, Response } from 'express'; import DataOperation from '../../models/data-operation'; import HarmonyRequest from '../../models/harmony-request'; import wrap from '../../util/array'; -import { handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleHeight, handleScaleExtent, handleScaleSize, handleWidth } from '../../util/parameter-parsers'; +import { handleAveragingType, handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleHeight, handleScaleExtent, handleScaleSize, handleWidth } from '../../util/parameter-parsers'; import { createDecrypter, createEncrypter } from '../../util/crypto'; import env from '../../util/env'; import { RequestValidationError } from '../../util/errors'; @@ -41,6 +41,7 @@ export default function getCoverageRangeset( handleScaleSize(operation, query); handleHeight(operation, query); handleWidth(operation, query); + handleAveragingType(operation, query); operation.interpolationMethod = query.interpolation; if (query.forceasync) { diff --git a/services/harmony/app/frontends/ogc-edr/get-data-common.ts b/services/harmony/app/frontends/ogc-edr/get-data-common.ts index 538683ecd..4eb3cca3a 100644 --- a/services/harmony/app/frontends/ogc-edr/get-data-common.ts +++ b/services/harmony/app/frontends/ogc-edr/get-data-common.ts @@ -1,7 +1,7 @@ import DataOperation from '../../models/data-operation'; import HarmonyRequest from '../../models/harmony-request'; import wrap from '../../util/array'; -import { handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleScaleExtent, handleScaleSize } from '../../util/parameter-parsers'; +import { handleAveragingType, handleCrs, handleExtend, handleFormat, handleGranuleIds, handleGranuleNames, handleScaleExtent, handleScaleSize } from '../../util/parameter-parsers'; import { createDecrypter, createEncrypter } from '../../util/crypto'; import env from '../../util/env'; import { RequestValidationError } from '../../util/errors'; @@ -35,6 +35,7 @@ export function getDataCommon( handleCrs(operation, query.crs); handleScaleExtent(operation, query); handleScaleSize(operation, query); + handleAveragingType(operation, query); operation.interpolationMethod = query.interpolation; operation.outputWidth = query.width; diff --git a/services/harmony/app/models/data-operation.ts b/services/harmony/app/models/data-operation.ts index db115f625..9d62c1ae5 100644 --- a/services/harmony/app/models/data-operation.ts +++ b/services/harmony/app/models/data-operation.ts @@ -310,6 +310,8 @@ export default class DataOperation { ignoreErrors?: boolean; + averagingType: string; + destinationUrl: string; ummCollections: CmrUmmCollection[]; diff --git a/services/harmony/app/models/services/base-service.ts b/services/harmony/app/models/services/base-service.ts index 4b9945c16..d3c4b0fae 100644 --- a/services/harmony/app/models/services/base-service.ts +++ b/services/harmony/app/models/services/base-service.ts @@ -28,6 +28,10 @@ export interface ServiceCapabilities { variable?: boolean; multiple_variable?: true; }; + averaging?: { + time?: boolean; + area?: boolean; + }; output_formats?: string[]; reprojection?: boolean; extend?: boolean; diff --git a/services/harmony/app/models/services/index.ts b/services/harmony/app/models/services/index.ts index 106207f9f..5fb498803 100644 --- a/services/harmony/app/models/services/index.ts +++ b/services/harmony/app/models/services/index.ts @@ -448,6 +448,42 @@ function supportsDimensionSubsetting(configs: ServiceConfig[]): Service return configs.filter((config) => getIn(config, 'capabilities.subsetting.dimension', false)); } +/** + * Returns true if the operation requires time averaging + * @param operation - The operation to perform. + * @returns true if the provided operation requires time averaging and false otherwise + */ +function requiresTimeAveraging(operation: DataOperation): boolean { + return operation.averagingType === 'time'; +} + +/** + * Returns any services that support time averaging from the list of configs + * @param configs - The potential matching service configurations + * @returns Any configurations that support time averaging + */ +function supportsTimeAveraging(configs: ServiceConfig[]): ServiceConfig[] { + return configs.filter((config) => getIn(config, 'capabilities.averaging.time', false)); +} + +/** + * Returns true if the operation requires area averaging + * @param operation - The operation to perform. + * @returns true if the provided operation requires area averaging and false otherwise + */ +function requiresAreaAveraging(operation: DataOperation): boolean { + return operation.averagingType === 'area'; +} + +/** + * Returns any services that support area averaging from the list of configs + * @param configs - The potential matching service configurations + * @returns Any configurations that support area averaging + */ +function supportsAreaAveraging(configs: ServiceConfig[]): ServiceConfig[] { + return configs.filter((config) => getIn(config, 'capabilities.averaging.area', false)); +} + export class UnsupportedOperation extends HttpError { operation: DataOperation; @@ -755,6 +791,64 @@ function filterDimensionSubsettingMatches( return services; } +/** + * Returns any services that support time averaging from the list of configs + * if the operation requires time averaging. + * @param operation - The operation to perform. + * @param context - Additional context that's not part of the operation, but influences the + * choice regarding the service to use + * @param configs - All service configurations that have matched up to this call + * @param requestedOperations - Operations that have been considered in filtering out services up to + * this call + * @returns Any service configurations that could still support the request + */ +function filterTimeAveragingMatches( + operation: DataOperation, + context: RequestContext, + configs: ServiceConfig[], + requestedOperations: string[], +): ServiceConfig[] { + let services = configs; + if (requiresTimeAveraging(operation)) { + requestedOperations.push('time averaging'); + services = supportsTimeAveraging(configs); + } + + if (services.length === 0) { + throw new UnsupportedOperation(operation, requestedOperations); + } + return services; +} + +/** + * Returns any services that support area averaging from the list of configs + * if the operation requires area averaging. + * @param operation - The operation to perform. + * @param context - Additional context that's not part of the operation, but influences the + * choice regarding the service to use + * @param configs - All service configurations that have matched up to this call + * @param requestedOperations - Operations that have been considered in filtering out services up to + * this call + * @returns Any service configurations that could still support the request + */ +function filterAreaAveragingMatches( + operation: DataOperation, + context: RequestContext, + configs: ServiceConfig[], + requestedOperations: string[], +): ServiceConfig[] { + let services = configs; + if (requiresAreaAveraging(operation)) { + requestedOperations.push('area averaging'); + services = supportsAreaAveraging(configs); + } + + if (services.length === 0) { + throw new UnsupportedOperation(operation, requestedOperations); + } + return services; +} + type FilterFunction = ( // The operation to perform operation: DataOperation, @@ -781,6 +875,8 @@ const allFilterFns = [ filterDimensionSubsettingMatches, filterReprojectionMatches, filterExtendMatches, + filterAreaAveragingMatches, + filterTimeAveragingMatches, // This filter must be last because it chooses a format based on the accepted MimeTypes and // the remaining services that could support the operation. If it ran earlier we could // potentially eliminate services that a different accepted MimeType would have allowed. We @@ -798,6 +894,8 @@ const requiredFilterFns = [ filterDimensionSubsettingMatches, filterReprojectionMatches, filterExtendMatches, + filterAreaAveragingMatches, + filterTimeAveragingMatches, // See caveat above in allFilterFns about why this filter must be applied last filterOutputFormatMatches, ]; diff --git a/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml b/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml index 38d330958..00cc4b06f 100644 --- a/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml +++ b/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml @@ -343,6 +343,7 @@ paths: - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - $ref: "#/components/parameters/variable" + - $ref: "#/components/parameters/averagingType" responses: "200": description: A coverage's range set. @@ -384,6 +385,7 @@ paths: - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - $ref: "#/components/parameters/variable" + - $ref: "#/components/parameters/averagingType" requestBody: content: multipart/form-data: @@ -984,3 +986,12 @@ components: items: type: string minLength: 1 + averagingType: + name: averagingType + in: query + description: | + requests the data to be averaged over time or area + required: false + schema: + type: string + diff --git a/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml b/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml index 60e94f282..c9fcb9c46 100644 --- a/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml +++ b/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml @@ -172,6 +172,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" + - $ref: "#/components/parameters/averagingType" responses: "200": description: A collection's EDR. @@ -258,6 +259,8 @@ paths: type: string extend: type: string + averagingType: + type: string responses: "200": description: A collection's EDR. @@ -304,6 +307,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" + - $ref: "#/components/parameters/averagingType" responses: "200": description: A collection's EDR. @@ -396,6 +400,8 @@ paths: type: string extend: type: string + averagingType: + type: string responses: "200": description: A collection's EDR. @@ -442,6 +448,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" + - $ref: "#/components/parameters/averagingType" responses: "200": description: A collection's EDR. @@ -532,6 +539,8 @@ paths: type: string extend: type: string + averagingType: + type: string responses: "200": description: A edr description. @@ -576,6 +585,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" + - $ref: "#/components/parameters/averagingType" responses: "200": description: A collection's EDR. @@ -661,6 +671,8 @@ paths: type: string extend: type: string + averagingType: + type: string responses: "200": description: A collection's EDR. @@ -705,6 +717,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" + - $ref: "#/components/parameters/averagingType" responses: "200": description: A collection's EDR. @@ -791,6 +804,8 @@ paths: type: string extend: type: string + averagingType: + type: string responses: "200": description: A collection's EDR. @@ -841,6 +856,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" + - $ref: "#/components/parameters/averagingType" responses: "200": description: A collection's EDR. @@ -943,6 +959,8 @@ paths: type: string extend: type: string + averagingType: + type: string responses: "200": description: A collection's EDR. @@ -1644,3 +1662,11 @@ components: required: false schema: type: string + averagingType: + name: averagingType + in: query + description: | + requests the data to be averaged over time or area + required: false + schema: + type: string diff --git a/services/harmony/app/util/parameter-parsers.ts b/services/harmony/app/util/parameter-parsers.ts index 67b9fc09d..181a2e477 100644 --- a/services/harmony/app/util/parameter-parsers.ts +++ b/services/harmony/app/util/parameter-parsers.ts @@ -201,3 +201,22 @@ export function handleWidth( } } } + +/** + * Handle the averaging parameter in a Harmony query, adding it to the DataOperation + * if necessary. + * + * @param operation - the DataOperation for the request + * @param query - the query for the request + */ +export function handleAveragingType( + operation: DataOperation, + query: Record): void { + if (query.averagingtype) { + const value = query.averagingtype.toLowerCase(); + if (value !== 'time' && value !== 'area') { + throw new RequestValidationError('query parameter "averagingType" must be either "time" or "area"'); + } + operation.averagingType = value; + } +} From 5d26983978cb59548fd60d2969ff87fd938649cd Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Tue, 24 Sep 2024 15:06:37 -0400 Subject: [PATCH 2/7] HARMONY-1889: Add tests that requests for averaging are correctly matched to services that provide area and time averaging. --- services/harmony/test/models/services.ts | 44 ++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/services/harmony/test/models/services.ts b/services/harmony/test/models/services.ts index fed36d29f..c6f084a4e 100644 --- a/services/harmony/test/models/services.ts +++ b/services/harmony/test/models/services.ts @@ -107,6 +107,28 @@ describe('services.chooseServiceConfig and services.buildService', function () { }, }, }, + { + name: 'time-averaging-service', + type: { name: 'turbo' }, + collections: [{ id: collectionId }], + capabilities: { + averaging: { + time: true, + }, + output_formats: ['application/x-netcdf4'], + }, + }, + { + name: 'area-averaging-service', + type: { name: 'turbo' }, + collections: [{ id: collectionId }], + capabilities: { + averaging: { + area: true, + }, + output_formats: ['application/x-netcdf4'], + }, + }, ]; }); @@ -177,6 +199,28 @@ describe('services.chooseServiceConfig and services.buildService', function () { }); }); + describe('and the request needs area averaging', function () { + beforeEach(function () { + this.operation.averagingType = 'area'; + }); + + it('chooses the service that supports area averaging', function () { + const serviceConfig = chooseServiceConfig(this.operation, {}, this.config); + expect(serviceConfig.name).to.equal('area-averaging-service'); + }); + }); + + describe('and the request needs time averaging', function () { + beforeEach(function () { + this.operation.averagingType = 'time'; + }); + + it('chooses the service that supports time averaging', function () { + const serviceConfig = chooseServiceConfig(this.operation, {}, this.config); + expect(serviceConfig.name).to.equal('time-averaging-service'); + }); + }); + describe('and the request needs both spatial subsetting and netcdf output, but no service supports that combination', function () { beforeEach(function () { this.operation.boundingRectangle = [0, 0, 10, 10]; From 51554196dc89e089207639e065ed619f50215e61 Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Tue, 24 Sep 2024 15:54:40 -0400 Subject: [PATCH 3/7] HARMONY-1889: Add tests for making coverages requests with the averagingType parameter set to time or area. --- .../harmony/test/parameters/averaging_type.ts | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 services/harmony/test/parameters/averaging_type.ts diff --git a/services/harmony/test/parameters/averaging_type.ts b/services/harmony/test/parameters/averaging_type.ts new file mode 100644 index 000000000..c63d871b4 --- /dev/null +++ b/services/harmony/test/parameters/averaging_type.ts @@ -0,0 +1,88 @@ +import { expect } from 'chai'; +// import { hookRedirect } from '../helpers/hooks'; +import { hookRangesetRequest } from '../helpers/ogc-api-coverages'; +import hookServersStartStop from '../helpers/servers'; +import StubService from '../helpers/stub-service'; + +// Note that there are currently no time or area averaging services in services.yml +// which is why several of the assertions are using `xit`. Once there are services to +// support it we should enable the assertions and uncomment out the code to follow +// the redirects. +describe('averagingType', function () { + const collection = 'C1233800302-EEDTEST'; + hookServersStartStop(); + + describe('when making a request with averagingType time', function () { + const averagingTimeQuery = { + averagingType: 'time', + }; + + describe('for a collection that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingTimeQuery } }); + // hookRedirect('anonymous'); + + xit('returns a 200 status code for the request', async function () { + expect(this.res.status).to.equal(200); + }); + + xit('specifies to perform time averaging in the operation', async function () { + expect(this.service.operation.averagingType).to.equal('time'); + }); + }); + + describe('for a collection that has no service that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingTimeQuery } }); + + xit('returns a 400 status code for the request', async function () { + expect(this.res.status).to.equal(400); + }); + + xit('returns a message indicating that no service supports time averaging', async function () { + const errorMessage = { + 'code': 'harmony.UnsupportedOperation', + 'description': 'Error: the requested combination of operations: time averaging on C1233800302-EEDTEST is unsupported', + }; + expect(this.res.body).to.eql(errorMessage); + }); + }); + }); + + describe('when making a request with averagingType area', function () { + const averagingAreaQuery = { + averagingType: 'area', + }; + + describe('for a collection that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingAreaQuery } }); + // hookRedirect('anonymous'); + + xit('returns a 200 status code for the request', async function () { + expect(this.res.status).to.equal(200); + }); + + xit('specifies to perform area averaging in the operation', async function () { + expect(this.service.operation.averagingType).to.equal('area'); + }); + }); + + describe('for a collection that has no service that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingAreaQuery } }); + + it('returns a 422 status code for the request', async function () { + expect(this.res.status).to.equal(422); + }); + + it('returns a message indicating that no service supports area averaging', async function () { + const errorMessage = { + 'code': 'harmony.UnsupportedOperation', + 'description': 'Error: the requested combination of operations: area averaging on C1233800302-EEDTEST is unsupported', + }; + expect(this.res.body).to.eql(errorMessage); + }); + }); + }); +}); \ No newline at end of file From f402eaf364c856893ec1251435be1d7e238e4ecf Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Tue, 24 Sep 2024 16:05:59 -0400 Subject: [PATCH 4/7] HARMONY-1889: Add one more averagingType parameter validation test. --- .../harmony/test/parameters/averaging_type.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/services/harmony/test/parameters/averaging_type.ts b/services/harmony/test/parameters/averaging_type.ts index c63d871b4..de206169a 100644 --- a/services/harmony/test/parameters/averaging_type.ts +++ b/services/harmony/test/parameters/averaging_type.ts @@ -85,4 +85,23 @@ describe('averagingType', function () { }); }); }); + + describe('when making a request with an invalid averagingType', function () { + const badAveragingQuery = { + averagingType: 'no not that', + }; + hookRangesetRequest('1.0.0', collection, 'all', { query: { ...badAveragingQuery } }); + + it('returns a 400 status code for the request', async function () { + expect(this.res.status).to.equal(400); + }); + + it('returns a message indicating that the averagingType value is invalid', async function () { + const errorMessage = { + 'code': 'harmony.RequestValidationError', + 'description': 'Error: query parameter "averagingType" must be either "time" or "area"', + }; + expect(this.res.body).to.eql(errorMessage); + }); + }); }); \ No newline at end of file From 8dc569bd55971a671090a396f8b859d484c8fec4 Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Tue, 24 Sep 2024 16:17:07 -0400 Subject: [PATCH 5/7] HARMONY-1889: Document the averagingType parameter and the new averaging capabilities in services.yml --- docs/guides/adapting-new-services.md | 3 +++ services/harmony/app/markdown/apis.md | 2 ++ 2 files changed, 5 insertions(+) diff --git a/docs/guides/adapting-new-services.md b/docs/guides/adapting-new-services.md index 31726838e..0f4683884 100644 --- a/docs/guides/adapting-new-services.md +++ b/docs/guides/adapting-new-services.md @@ -179,6 +179,9 @@ The structure of an entry in the [services.yml](../../config/services.yml) file temporal: true # Can subset by a time range variable: true # Can subset by UMM-Var variable multiple_variable: true # Can subset multiple variables at once + averaging: + time: true # Can perform averaging over time + area: true # Can perform averaging over area output_formats: # A list of output mime types the service can produce - image/tiff - image/png diff --git a/services/harmony/app/markdown/apis.md b/services/harmony/app/markdown/apis.md index ef07c7009..50fefaccb 100644 --- a/services/harmony/app/markdown/apis.md +++ b/services/harmony/app/markdown/apis.md @@ -71,6 +71,7 @@ As such it accepts parameters in the URL path as well as query parameters. | ignoreErrors | if "true", continue processing a request to completion even if some items fail. If "false" immediately fail the request. Defaults to true | | destinationUrl | destination url specified by the client; currently only s3 link urls are supported (e.g. s3://my-bucket-name/mypath) and will result in the job being run asynchronously | | variable | the variable(s) to be used for variable subsetting. Multiple variables can be specified as a comma-separated list. This parameter is only used if the url `variable` path element is "parameter_vars" | +| averagingType | requests the data to be averaged over either time or area | --- **Table {{tableCounter}}** - Harmony OGC Coverages API query parameters @@ -131,6 +132,7 @@ Currently only the `/position`, `/cube` and `/area` routes are supported for spa | subset | get a subset of the coverage by slicing or trimming along one axis. Harmony supports arbitrary dimension names for subsetting on numeric ranges for that dimension. | | height | number of rows to return in the output coverage | | width | number of columns to return in the output coverage | +| averagingType | requests the data to be averaged over either time or area | --- **Table {{tableCounter}}** - Harmony extended parameters for all OGC EDR API routes From 3446b9d005a3c3f048d3004a0c3d5ce89064d00c Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Wed, 25 Sep 2024 10:07:10 -0400 Subject: [PATCH 6/7] HARMONY-1889: Add EDR endpoint testing of the averagingType parameter. --- services/harmony/app/models/services/index.ts | 2 +- services/harmony/test/helpers/util.ts | 13 ++ .../harmony/test/parameters/averaging_type.ts | 183 ++++++++++-------- 3 files changed, 119 insertions(+), 79 deletions(-) create mode 100644 services/harmony/test/helpers/util.ts diff --git a/services/harmony/app/models/services/index.ts b/services/harmony/app/models/services/index.ts index 5fb498803..b1319f7be 100644 --- a/services/harmony/app/models/services/index.ts +++ b/services/harmony/app/models/services/index.ts @@ -870,13 +870,13 @@ const allFilterFns = [ filterConcatenationMatches, filterVariableSubsettingMatches, filterSpatialSubsettingMatches, - filterShapefileSubsettingMatches, filterTemporalSubsettingMatches, filterDimensionSubsettingMatches, filterReprojectionMatches, filterExtendMatches, filterAreaAveragingMatches, filterTimeAveragingMatches, + filterShapefileSubsettingMatches, // This filter must be last because it chooses a format based on the accepted MimeTypes and // the remaining services that could support the operation. If it ran earlier we could // potentially eliminate services that a different accepted MimeType would have allowed. We diff --git a/services/harmony/test/helpers/util.ts b/services/harmony/test/helpers/util.ts new file mode 100644 index 000000000..ebea35dab --- /dev/null +++ b/services/harmony/test/helpers/util.ts @@ -0,0 +1,13 @@ +/** + * Partially applies a function by pre-filling some arguments. + * + * @param fn - The original function to partially apply. + * @param presetArgs - Arguments to pre-fill when calling the function. + * + * @returns A new function that takes the remaining arguments and calls the original function + * with both the preset and remaining arguments. + * + */ +export function partialApply(fn: (...args: unknown[]) => void, ...presetArgs: unknown[]) { + return (...laterArgs: unknown[]): void => fn(...presetArgs, ...laterArgs); +} \ No newline at end of file diff --git a/services/harmony/test/parameters/averaging_type.ts b/services/harmony/test/parameters/averaging_type.ts index de206169a..7d0ca004d 100644 --- a/services/harmony/test/parameters/averaging_type.ts +++ b/services/harmony/test/parameters/averaging_type.ts @@ -1,107 +1,134 @@ +// Note that there are currently no time or area averaging services in services.yml +// which is why several of the assertions are using `xit`. Once there are services to +// support it we should enable the assertions and uncomment out the code to follow +// the redirects. + import { expect } from 'chai'; -// import { hookRedirect } from '../helpers/hooks'; import { hookRangesetRequest } from '../helpers/ogc-api-coverages'; import hookServersStartStop from '../helpers/servers'; import StubService from '../helpers/stub-service'; +import { hookEdrRequest } from '../helpers/ogc-api-edr'; +import { partialApply } from '../helpers/util'; +// import { hookRedirect } from '../helpers/hooks'; -// Note that there are currently no time or area averaging services in services.yml -// which is why several of the assertions are using `xit`. Once there are services to -// support it we should enable the assertions and uncomment out the code to follow -// the redirects. -describe('averagingType', function () { - const collection = 'C1233800302-EEDTEST'; - hookServersStartStop(); - - describe('when making a request with averagingType time', function () { - const averagingTimeQuery = { - averagingType: 'time', - }; - - describe('for a collection that can support it', function () { - StubService.hook({ params: { redirect: 'http://example.com' } }); - hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingTimeQuery } }); - // hookRedirect('anonymous'); - - xit('returns a 200 status code for the request', async function () { - expect(this.res.status).to.equal(200); - }); +const collection = 'C1233800302-EEDTEST'; +const edrVersion = '1.1.0'; + +// We want to test the averagingType parameter on each of the following APIs, so we'll +// run the same tests against each in a loop +const endpointFunctions = [{ + label: 'OGC Coverages', + endpointFn: partialApply(hookRangesetRequest, '1.0.0', collection, 'all'), + extraArgs: {}, +}, { + label: 'EDR area', + endpointFn: partialApply(hookEdrRequest, 'area', edrVersion, collection), + extraArgs: { coords: 'POLYGON ((-65.3 -13.2, -29.8 -50.9, 17.9 30.1, -65.3 -13.2))' }, +}, { + label: 'EDR position', + endpointFn: partialApply(hookEdrRequest, 'position', edrVersion, collection), + extraArgs: { coords: 'POINT (-40 10)' }, +}, { + label: 'EDR trajectory', + endpointFn: partialApply(hookEdrRequest, 'trajectory', edrVersion, collection), + extraArgs: { coords: 'LINESTRING (-40 10, 30 10)' }, +}, { + label: 'EDR cube', + endpointFn: partialApply(hookEdrRequest, 'cube', edrVersion, collection), + extraArgs: { bbox: '-20.1,0,20,10' }, +}]; + +for (const { label, endpointFn, extraArgs } of endpointFunctions) { + describe(`averagingType for ${label} API`, function () { + hookServersStartStop(); + + describe('when making a request with averagingType time', function () { + const averagingTimeQuery = { + averagingType: 'time', + }; - xit('specifies to perform time averaging in the operation', async function () { - expect(this.service.operation.averagingType).to.equal('time'); - }); - }); + describe('for a collection that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + endpointFn({ query: { ...averagingTimeQuery, ...extraArgs } }); + // hookRedirect('anonymous'); - describe('for a collection that has no service that can support it', function () { - StubService.hook({ params: { redirect: 'http://example.com' } }); - hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingTimeQuery } }); + xit('returns a 200 status code for the request', async function () { + expect(this.res.status).to.equal(200); + }); - xit('returns a 400 status code for the request', async function () { - expect(this.res.status).to.equal(400); + xit('specifies to perform time averaging in the operation', async function () { + expect(this.service.operation.averagingType).to.equal('time'); + }); }); - xit('returns a message indicating that no service supports time averaging', async function () { - const errorMessage = { - 'code': 'harmony.UnsupportedOperation', - 'description': 'Error: the requested combination of operations: time averaging on C1233800302-EEDTEST is unsupported', - }; - expect(this.res.body).to.eql(errorMessage); + describe('for a collection that has no service that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + endpointFn({ query: { ...averagingTimeQuery, ...extraArgs } }); + + it('returns a 422 status code for the request', async function () { + expect(this.res.status).to.equal(422); + }); + + it('returns a message indicating that no service supports time averaging', async function () { + const error = this.res.body; + expect(error.code).to.equal('harmony.UnsupportedOperation'); + expect(error.description).to.include('time averaging'); + }); }); }); - }); - describe('when making a request with averagingType area', function () { - const averagingAreaQuery = { - averagingType: 'area', - }; + describe('when making a request with averagingType area', function () { + const averagingAreaQuery = { + averagingType: 'area', + }; - describe('for a collection that can support it', function () { - StubService.hook({ params: { redirect: 'http://example.com' } }); - hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingAreaQuery } }); - // hookRedirect('anonymous'); + describe('for a collection that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + endpointFn({ query: { ...averagingAreaQuery, ...extraArgs } }); + // hookRedirect('anonymous'); - xit('returns a 200 status code for the request', async function () { - expect(this.res.status).to.equal(200); + xit('returns a 200 status code for the request', async function () { + expect(this.res.status).to.equal(200); + }); + + xit('specifies to perform area averaging in the operation', async function () { + expect(this.service.operation.averagingType).to.equal('area'); + }); }); - xit('specifies to perform area averaging in the operation', async function () { - expect(this.service.operation.averagingType).to.equal('area'); + describe('for a collection that has no service that can support it', function () { + StubService.hook({ params: { redirect: 'http://example.com' } }); + endpointFn({ query: { ...averagingAreaQuery, ...extraArgs } }); + + it('returns a 422 status code for the request', async function () { + expect(this.res.status).to.equal(422); + }); + + it('returns a message indicating that no service supports area averaging', async function () { + const error = this.res.body; + expect(error.code).to.equal('harmony.UnsupportedOperation'); + expect(error.description).to.include('area averaging'); + }); }); }); - describe('for a collection that has no service that can support it', function () { - StubService.hook({ params: { redirect: 'http://example.com' } }); - hookRangesetRequest('1.0.0', collection, 'all', { query: { ...averagingAreaQuery } }); + describe('when making a request with an invalid averagingType', function () { + const badAveragingQuery = { + averagingType: 'no not that', + }; + endpointFn({ query: { ...badAveragingQuery, ...extraArgs } }); - it('returns a 422 status code for the request', async function () { - expect(this.res.status).to.equal(422); + it('returns a 400 status code for the request', async function () { + expect(this.res.status).to.equal(400); }); - it('returns a message indicating that no service supports area averaging', async function () { + it('returns a message indicating that the averagingType value is invalid', async function () { const errorMessage = { - 'code': 'harmony.UnsupportedOperation', - 'description': 'Error: the requested combination of operations: area averaging on C1233800302-EEDTEST is unsupported', + 'code': 'harmony.RequestValidationError', + 'description': 'Error: query parameter "averagingType" must be either "time" or "area"', }; expect(this.res.body).to.eql(errorMessage); }); }); }); - - describe('when making a request with an invalid averagingType', function () { - const badAveragingQuery = { - averagingType: 'no not that', - }; - hookRangesetRequest('1.0.0', collection, 'all', { query: { ...badAveragingQuery } }); - - it('returns a 400 status code for the request', async function () { - expect(this.res.status).to.equal(400); - }); - - it('returns a message indicating that the averagingType value is invalid', async function () { - const errorMessage = { - 'code': 'harmony.RequestValidationError', - 'description': 'Error: query parameter "averagingType" must be either "time" or "area"', - }; - expect(this.res.body).to.eql(errorMessage); - }); - }); -}); \ No newline at end of file +} \ No newline at end of file From cf4aed4b4f33ec82c917a81403c074e1c536e5dc Mon Sep 17 00:00:00 2001 From: Chris Durbin Date: Wed, 25 Sep 2024 14:02:50 -0400 Subject: [PATCH 7/7] HARMONY-1889: Replace averagingType with average to be consistent with our API parameters. --- services/harmony/app/markdown/apis.md | 4 +-- services/harmony/app/models/data-operation.ts | 2 +- services/harmony/app/models/services/index.ts | 4 +-- .../1.0.0/ogc-api-coverages-v1.0.0.yml | 8 +++--- .../ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml | 28 +++++++++---------- .../harmony/app/util/parameter-parsers.ts | 8 +++--- services/harmony/test/models/services.ts | 4 +-- .../harmony/test/parameters/averaging_type.ts | 24 ++++++++-------- 8 files changed, 41 insertions(+), 41 deletions(-) diff --git a/services/harmony/app/markdown/apis.md b/services/harmony/app/markdown/apis.md index 66b998b72..cd3b48763 100644 --- a/services/harmony/app/markdown/apis.md +++ b/services/harmony/app/markdown/apis.md @@ -71,7 +71,7 @@ As such it accepts parameters in the URL path as well as query parameters. | ignoreErrors | if "true", continue processing a request to completion even if some items fail. If "false" immediately fail the request. Defaults to true | | destinationUrl | destination url specified by the client; currently only s3 link urls are supported (e.g. s3://my-bucket-name/mypath) and will result in the job being run asynchronously | | variable | the variable(s) to be used for variable subsetting. Multiple variables can be specified as a comma-separated list. This parameter is only used if the url `variable` path element is "parameter_vars" | -| averagingType | requests the data to be averaged over either time or area | +| average | requests the data to be averaged over either time or area | --- **Table {{tableCounter}}** - Harmony OGC Coverages API query parameters @@ -132,7 +132,7 @@ Currently only the `/position`, `/cube`, `/trajectory` and `/area` routes are su | subset | get a subset of the coverage by slicing or trimming along one axis. Harmony supports arbitrary dimension names for subsetting on numeric ranges for that dimension. | | height | number of rows to return in the output coverage | | width | number of columns to return in the output coverage | -| averagingType | requests the data to be averaged over either time or area | +| average | requests the data to be averaged over either time or area | --- **Table {{tableCounter}}** - Harmony extended parameters for all OGC EDR API routes diff --git a/services/harmony/app/models/data-operation.ts b/services/harmony/app/models/data-operation.ts index 9d62c1ae5..195a64160 100644 --- a/services/harmony/app/models/data-operation.ts +++ b/services/harmony/app/models/data-operation.ts @@ -310,7 +310,7 @@ export default class DataOperation { ignoreErrors?: boolean; - averagingType: string; + average: string; destinationUrl: string; diff --git a/services/harmony/app/models/services/index.ts b/services/harmony/app/models/services/index.ts index b1319f7be..562b9e4bd 100644 --- a/services/harmony/app/models/services/index.ts +++ b/services/harmony/app/models/services/index.ts @@ -454,7 +454,7 @@ function supportsDimensionSubsetting(configs: ServiceConfig[]): Service * @returns true if the provided operation requires time averaging and false otherwise */ function requiresTimeAveraging(operation: DataOperation): boolean { - return operation.averagingType === 'time'; + return operation.average === 'time'; } /** @@ -472,7 +472,7 @@ function supportsTimeAveraging(configs: ServiceConfig[]): ServiceConfig * @returns true if the provided operation requires area averaging and false otherwise */ function requiresAreaAveraging(operation: DataOperation): boolean { - return operation.averagingType === 'area'; + return operation.average === 'area'; } /** diff --git a/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml b/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml index 00cc4b06f..2795b2869 100644 --- a/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml +++ b/services/harmony/app/schemas/ogc-api-coverages/1.0.0/ogc-api-coverages-v1.0.0.yml @@ -343,7 +343,7 @@ paths: - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - $ref: "#/components/parameters/variable" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" responses: "200": description: A coverage's range set. @@ -385,7 +385,7 @@ paths: - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - $ref: "#/components/parameters/variable" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" requestBody: content: multipart/form-data: @@ -986,8 +986,8 @@ components: items: type: string minLength: 1 - averagingType: - name: averagingType + average: + name: average in: query description: | requests the data to be averaged over time or area diff --git a/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml b/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml index c9fcb9c46..c6cf0c89b 100644 --- a/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml +++ b/services/harmony/app/schemas/ogc-api-edr/1.1.0/ogc-api-edr-v1.1.0.yml @@ -172,7 +172,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" responses: "200": description: A collection's EDR. @@ -259,7 +259,7 @@ paths: type: string extend: type: string - averagingType: + average: type: string responses: "200": @@ -307,7 +307,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" responses: "200": description: A collection's EDR. @@ -400,7 +400,7 @@ paths: type: string extend: type: string - averagingType: + average: type: string responses: "200": @@ -448,7 +448,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" responses: "200": description: A collection's EDR. @@ -539,7 +539,7 @@ paths: type: string extend: type: string - averagingType: + average: type: string responses: "200": @@ -585,7 +585,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" responses: "200": description: A collection's EDR. @@ -671,7 +671,7 @@ paths: type: string extend: type: string - averagingType: + average: type: string responses: "200": @@ -717,7 +717,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" responses: "200": description: A collection's EDR. @@ -804,7 +804,7 @@ paths: type: string extend: type: string - averagingType: + average: type: string responses: "200": @@ -856,7 +856,7 @@ paths: - $ref: "#/components/parameters/granuleName" - $ref: "#/components/parameters/grid" - $ref: "#/components/parameters/extend" - - $ref: "#/components/parameters/averagingType" + - $ref: "#/components/parameters/average" responses: "200": description: A collection's EDR. @@ -959,7 +959,7 @@ paths: type: string extend: type: string - averagingType: + average: type: string responses: "200": @@ -1662,8 +1662,8 @@ components: required: false schema: type: string - averagingType: - name: averagingType + average: + name: average in: query description: | requests the data to be averaged over time or area diff --git a/services/harmony/app/util/parameter-parsers.ts b/services/harmony/app/util/parameter-parsers.ts index 181a2e477..b6aa0fa75 100644 --- a/services/harmony/app/util/parameter-parsers.ts +++ b/services/harmony/app/util/parameter-parsers.ts @@ -212,11 +212,11 @@ export function handleWidth( export function handleAveragingType( operation: DataOperation, query: Record): void { - if (query.averagingtype) { - const value = query.averagingtype.toLowerCase(); + if (query.average) { + const value = query.average.toLowerCase(); if (value !== 'time' && value !== 'area') { - throw new RequestValidationError('query parameter "averagingType" must be either "time" or "area"'); + throw new RequestValidationError('query parameter "average" must be either "time" or "area"'); } - operation.averagingType = value; + operation.average = value; } } diff --git a/services/harmony/test/models/services.ts b/services/harmony/test/models/services.ts index c6f084a4e..da6de6f76 100644 --- a/services/harmony/test/models/services.ts +++ b/services/harmony/test/models/services.ts @@ -201,7 +201,7 @@ describe('services.chooseServiceConfig and services.buildService', function () { describe('and the request needs area averaging', function () { beforeEach(function () { - this.operation.averagingType = 'area'; + this.operation.average = 'area'; }); it('chooses the service that supports area averaging', function () { @@ -212,7 +212,7 @@ describe('services.chooseServiceConfig and services.buildService', function () { describe('and the request needs time averaging', function () { beforeEach(function () { - this.operation.averagingType = 'time'; + this.operation.average = 'time'; }); it('chooses the service that supports time averaging', function () { diff --git a/services/harmony/test/parameters/averaging_type.ts b/services/harmony/test/parameters/averaging_type.ts index 7d0ca004d..6a2e7fe4a 100644 --- a/services/harmony/test/parameters/averaging_type.ts +++ b/services/harmony/test/parameters/averaging_type.ts @@ -14,7 +14,7 @@ import { partialApply } from '../helpers/util'; const collection = 'C1233800302-EEDTEST'; const edrVersion = '1.1.0'; -// We want to test the averagingType parameter on each of the following APIs, so we'll +// We want to test the average parameter on each of the following APIs, so we'll // run the same tests against each in a loop const endpointFunctions = [{ label: 'OGC Coverages', @@ -39,12 +39,12 @@ const endpointFunctions = [{ }]; for (const { label, endpointFn, extraArgs } of endpointFunctions) { - describe(`averagingType for ${label} API`, function () { + describe(`average for ${label} API`, function () { hookServersStartStop(); - describe('when making a request with averagingType time', function () { + describe('when making a request with average time', function () { const averagingTimeQuery = { - averagingType: 'time', + average: 'time', }; describe('for a collection that can support it', function () { @@ -57,7 +57,7 @@ for (const { label, endpointFn, extraArgs } of endpointFunctions) { }); xit('specifies to perform time averaging in the operation', async function () { - expect(this.service.operation.averagingType).to.equal('time'); + expect(this.service.operation.average).to.equal('time'); }); }); @@ -77,9 +77,9 @@ for (const { label, endpointFn, extraArgs } of endpointFunctions) { }); }); - describe('when making a request with averagingType area', function () { + describe('when making a request with average area', function () { const averagingAreaQuery = { - averagingType: 'area', + average: 'area', }; describe('for a collection that can support it', function () { @@ -92,7 +92,7 @@ for (const { label, endpointFn, extraArgs } of endpointFunctions) { }); xit('specifies to perform area averaging in the operation', async function () { - expect(this.service.operation.averagingType).to.equal('area'); + expect(this.service.operation.average).to.equal('area'); }); }); @@ -112,9 +112,9 @@ for (const { label, endpointFn, extraArgs } of endpointFunctions) { }); }); - describe('when making a request with an invalid averagingType', function () { + describe('when making a request with an invalid average', function () { const badAveragingQuery = { - averagingType: 'no not that', + average: 'no not that', }; endpointFn({ query: { ...badAveragingQuery, ...extraArgs } }); @@ -122,10 +122,10 @@ for (const { label, endpointFn, extraArgs } of endpointFunctions) { expect(this.res.status).to.equal(400); }); - it('returns a message indicating that the averagingType value is invalid', async function () { + it('returns a message indicating that the average value is invalid', async function () { const errorMessage = { 'code': 'harmony.RequestValidationError', - 'description': 'Error: query parameter "averagingType" must be either "time" or "area"', + 'description': 'Error: query parameter "average" must be either "time" or "area"', }; expect(this.res.body).to.eql(errorMessage); });