Skip to content

Commit 13eba9f

Browse files
authored
filter ext adv comp ops in and between (#852)
* Add support for the Filter Extension operator 'IN' and 'BETWEEN'
1 parent d8bc6a9 commit 13eba9f

File tree

10 files changed

+295
-28
lines changed

10 files changed

+295
-28
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
77

8+
## [3.11.0] - TBD
9+
10+
### Added
11+
12+
- Added support for the "in" and "between" operators of the Filter Extension
13+
814
## [3.10.0] - 2025-03-21
915

1016
### Changed

src/lambdas/api/app.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import morgan from 'morgan'
55
import path from 'path'
66
import { fileURLToPath } from 'url'
77
import database from '../../lib/database.js'
8-
import api, { ValidationError } from '../../lib/api.js'
8+
import api from '../../lib/api.js'
9+
import { ValidationError } from '../../lib/errors.js'
910
import { readFile } from '../../lib/fs.js'
1011
import addEndpoint from './middleware/add-endpoint.js'
1112
import logger from '../../lib/logger.js'

src/lib/api.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import extent from '@mapbox/extent'
33
import { DateTime } from 'luxon'
44
import { getSignedUrl } from '@aws-sdk/s3-request-presigner'
55
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3'
6-
6+
import { ValidationError } from './errors.js'
77
import { isIndexNotFoundError } from './database.js'
88
import logger from './logger.js'
99

@@ -51,13 +51,6 @@ const ALL_AGGREGATION_NAMES = DEFAULT_AGGREGATIONS.map((x) => x.name).concat(
5151
]
5252
)
5353

54-
export class ValidationError extends Error {
55-
constructor(message) {
56-
super(message)
57-
this.name = 'ValidationError'
58-
}
59-
}
60-
6154
export const extractIntersects = function (params) {
6255
let intersectsGeometry
6356
const { intersects } = params

src/lib/database.js

Lines changed: 61 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import { isEmpty } from 'lodash-es'
22
import { dbClient as _client, createIndex } from './database-client.js'
33
import logger from './logger.js'
4+
import { ValidationError } from './errors.js'
45

56
const COLLECTIONS_INDEX = process.env['COLLECTIONS_INDEX'] || 'collections'
67
const DEFAULT_INDICES = ['*', '-.*', '-collections']
7-
const LOGICAL_OP = {
8+
const OP = {
89
AND: 'and',
910
OR: 'or',
10-
NOT: 'not'
11-
}
12-
const COMPARISON_OP = {
11+
NOT: 'not',
1312
EQ: '=',
1413
NEQ: '<>',
1514
LT: '<',
1615
LTE: '<=',
1716
GT: '>',
1817
GTE: '>=',
19-
IS_NULL: 'isNull'
18+
IS_NULL: 'isNull',
19+
IN: 'in',
20+
BETWEEN: 'between',
21+
LIKE: 'like',
2022
}
2123
const RANGE_TRANSLATION = {
2224
'<': 'lt',
@@ -213,34 +215,34 @@ function buildFilterExtQuery(filter) {
213215

214216
switch (filter.op) {
215217
// recursive cases
216-
case LOGICAL_OP.AND:
218+
case OP.AND:
217219
return {
218220
bool: {
219221
filter: filter.args.map(buildFilterExtQuery)
220222
}
221223
}
222-
case LOGICAL_OP.OR:
224+
case OP.OR:
223225
return {
224226
bool: {
225227
should: filter.args.map(buildFilterExtQuery),
226228
minimum_should_match: 1
227229
}
228230
}
229-
case LOGICAL_OP.NOT:
231+
case OP.NOT:
230232
return {
231233
bool: {
232234
must_not: filter.args.map(buildFilterExtQuery)
233235
}
234236
}
235237

236238
// direct cases
237-
case COMPARISON_OP.EQ:
239+
case OP.EQ:
238240
return {
239241
term: {
240242
[cql2Field]: cql2Value
241243
}
242244
}
243-
case COMPARISON_OP.NEQ:
245+
case OP.NEQ:
244246
return {
245247
bool: {
246248
must_not: [
@@ -252,7 +254,7 @@ function buildFilterExtQuery(filter) {
252254
]
253255
}
254256
}
255-
case COMPARISON_OP.IS_NULL:
257+
case OP.IS_NULL:
256258
return {
257259
bool: {
258260
must_not: [
@@ -266,17 +268,61 @@ function buildFilterExtQuery(filter) {
266268
}
267269

268270
// range cases
269-
case COMPARISON_OP.LT:
270-
case COMPARISON_OP.LTE:
271-
case COMPARISON_OP.GT:
272-
case COMPARISON_OP.GTE:
271+
case OP.LT:
272+
case OP.LTE:
273+
case OP.GT:
274+
case OP.GTE:
273275
return {
274276
range: {
275277
[cql2Field]: {
276278
[RANGE_TRANSLATION[filter.op]]: cql2Value
277279
}
278280
}
279281
}
282+
case OP.IN:
283+
if (!Array.isArray(cql2Value) || cql2Value.length === 0) {
284+
throw new ValidationError("Operand for 'in' must be a non-empty array")
285+
}
286+
if (!cql2Value.every((x) => x !== Object(x))) {
287+
throw new ValidationError(
288+
"Operand for 'in' must contain only string, number, or boolean types"
289+
)
290+
}
291+
292+
return {
293+
terms: {
294+
[cql2Field]: cql2Value
295+
}
296+
}
297+
case OP.BETWEEN:
298+
if (filter.args.length < 3) {
299+
throw new ValidationError("Two operands must be provided for the 'between' operator")
300+
}
301+
302+
// eslint-disable-next-line no-case-declarations
303+
const cql2Value2 = filter.args[2]
304+
if (!(typeof cql2Value === 'number' && typeof cql2Value2 === 'number')) {
305+
throw new ValidationError("Operands for 'between' must be numbers")
306+
}
307+
308+
if (cql2Value > cql2Value2) {
309+
throw new ValidationError(
310+
"For the 'between' operator, the first operand must be less than or equal "
311+
+ 'to the second operand'
312+
)
313+
}
314+
315+
return {
316+
range: {
317+
[cql2Field]: {
318+
gte: cql2Value,
319+
lte: cql2Value2
320+
}
321+
}
322+
}
323+
324+
case OP.LIKE:
325+
throw new ValidationError("The 'like' operator is not currently supported")
280326

281327
// should not get here
282328
default:

src/lib/errors.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
/* eslint-disable import/prefer-default-export */
2+
export class ValidationError extends Error {
3+
constructor(message) {
4+
super(message)
5+
this.name = 'ValidationError'
6+
}
7+
}

tests/fixtures/stac/collection2_item.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"landsat:processing_level": "L1T",
1616
"landsat:product_id": null,
1717
"landsat:wrs_row": "10",
18-
"landsat:scene_id": "collection2_item"
18+
"landsat:scene_id": "collection2_item",
19+
"boolean_property": true
1920
},
2021
"geometry": {
2122
"coordinates": [

0 commit comments

Comments
 (0)