Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ curl http://localhost:3000/
{"rel": "conformance", "href": "http://localhost:3000/conformance", "type": "application/json"},
{"rel": "data", "href": "http://localhost:3000/collections", "type": "application/json"},
{"rel": "health", "href": "http://localhost:3000/health", "type": "application/json"},
{"rel": "queryables", "href": "http://localhost:3000/collections-queryables", "type": "application/schema+json"},
{"rel": "queryables", "href": "http://localhost:3000/collection-queryables", "type": "application/schema+json"},
{"rel": "service-doc", "href": "http://localhost:3000/api-docs", "type": "text/html"},
{"rel": "service-desc", "href": "http://localhost:3000/openapi.yaml", "type": "application/vnd.oai.openapi+json;version=3.0"}
]
Expand Down Expand Up @@ -320,21 +320,21 @@ curl http://localhost:3000/collections/sentinel-2-l2a
### Queryables

```
GET /collections-queryables
GET /collection-queryables
```

Returns a JSON Schema describing properties that can be used in CQL2 filter expressions.

**Example Request:**
```bash
curl http://localhost:3000/collections-queryables
curl http://localhost:3000/collection-queryables
```

**Example Response (abbreviated):**
```json
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$id": "http://localhost:3000/collections-queryables",
"$id": "http://localhost:3000/collection-queryables",
"type": "object",
"title": "STAC Atlas Collections Queryables",
"properties": {
Expand Down Expand Up @@ -1021,7 +1021,7 @@ api/
│ ├── index.js # Landing page (/)
│ ├── conformance.js # Conformance (/conformance)
│ ├── collections.js # Collections (/collections)
│ ├── queryables.js # Queryables (/collections-queryables)
│ ├── queryables.js # Queryables (/collection-queryables)
│ └── health.js # Health check (/health)
├── utils/
│ ├── cql2.js # CQL2 parser interface
Expand Down
6 changes: 3 additions & 3 deletions api/__tests__/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,17 +90,17 @@ describe('STAC API Core Endpoints', () => {
});
});

describe('GET /collections-queryables', () => {
describe('GET /collection-queryables', () => {
it('should return queryables schema', async () => {
const response = await request(app).get('/collections-queryables').expect(200);
const response = await request(app).get('/collection-queryables').expect(200);

expect(response.body).toHaveProperty('$schema');
expect(response.body).toHaveProperty('type', 'object');
expect(response.body).toHaveProperty('properties');
});

it('should include standard STAC queryable fields', async () => {
const response = await request(app).get('/collections-queryables').expect(200);
const response = await request(app).get('/collection-queryables').expect(200);

const properties = response.body.properties;
expect(properties).toHaveProperty('id');
Expand Down
4 changes: 2 additions & 2 deletions api/__tests__/collections-queryables.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
const request = require('supertest');
const app = require('../app');

describe('GET /collections-queryables', () => {
describe('GET /collection-queryables', () => {
it('returns queryables as JSON Schema', async () => {
const res = await request(app).get('/collections-queryables');
const res = await request(app).get('/collection-queryables');

expect(res.status).toBe(200);

Expand Down
31 changes: 24 additions & 7 deletions api/__tests__/collections-sort.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@ describe('Collection Search - Sorting behavior (4.4 Implement Sorting)', () => {
*/
it('should sort ascending by title with +title', async () => {
const response = await request(app)
.get('/collections?sortby=%2Btitle&limit=100')
.get('/collections?sortby=%2Btitle&limit=100&token=10000')
.expect(200);

const titles = response.body.collections.map(c => c.title);
console.log(titles)

// PostgreSQL's collation may differ from JavaScript's localeCompare.
// Instead, verify that:
Expand All @@ -32,9 +33,15 @@ describe('Collection Search - Sorting behavior (4.4 Implement Sorting)', () => {
expect(titles.length).toBeGreaterThan(0);

// Filter out undefined/null values for comparison
const validTitles = titles.filter(t => t != null);
const validTitles = titles.filter(t => t != null && t !== '');
expect(validTitles.length).toBeGreaterThan(0);

// Skip detailed checks if we have less than 2 valid titles
if (validTitles.length < 2) {
console.warn('Only 1 valid title found, skipping order verification');
return;
}

// Check first vs last (should be alphabetically before or equal)
const firstTitle = validTitles[0].toLowerCase();
const lastTitle = validTitles[validTitles.length - 1].toLowerCase();
Expand Down Expand Up @@ -192,20 +199,30 @@ describe('Collection Search - Sorting behavior (4.4 Implement Sorting)', () => {

expect(titles.length).toBeGreaterThan(0);

// Filter out undefined/null/empty values
const validTitles = titles.filter(t => t != null && t !== '');
expect(validTitles.length).toBeGreaterThan(0);

// Skip detailed checks if we have less than 2 valid titles
if (validTitles.length < 2) {
console.warn('Only 1 valid title found, skipping order verification');
return;
}

// Verify ascending order (first <= last)
const firstTitle = titles[0].toLowerCase();
const lastTitle = titles[titles.length - 1].toLowerCase();
const firstTitle = validTitles[0].toLowerCase();
const lastTitle = validTitles[validTitles.length - 1].toLowerCase();
expect(firstTitle.localeCompare(lastTitle, 'en', { sensitivity: 'base' })).toBeLessThanOrEqual(0);

// At least 80% of pairs should be ascending
let correctPairs = 0;
for (let i = 0; i < titles.length - 1; i++) {
if (titles[i].toLowerCase().localeCompare(titles[i + 1].toLowerCase(), 'en', { sensitivity: 'base' }) <= 0) {
for (let i = 0; i < validTitles.length - 1; i++) {
if (validTitles[i].toLowerCase().localeCompare(validTitles[i + 1].toLowerCase(), 'en', { sensitivity: 'base' }) <= 0) {
correctPairs++;
}
}

const pairRatio = correctPairs / (titles.length - 1);
const pairRatio = correctPairs / (validTitles.length - 1);
expect(pairRatio).toBeGreaterThanOrEqual(0.8);
});
});
2 changes: 1 addition & 1 deletion api/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
const swaggerDocument = YAML.load(path.join(__dirname, 'docs', 'openapi.yaml'));
app.use('/api-docs', swaggerUi.serve);
app.get('/api-docs', swaggerUi.setup(swaggerDocument));
} catch (err) {

Check warning on line 66 in api/app.js

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

'err' is defined but never used

Check warning on line 66 in api/app.js

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

'err' is defined but never used
console.log('OpenAPI documentation not found. Create docs/openapi.yaml to enable Swagger UI.');
}

Expand All @@ -77,7 +77,7 @@
app.use('/', indexRouter);
app.use('/conformance', conformanceRouter);
app.use('/collections', collectionsRouter);
app.use('/collections-queryables', queryablesRouter);
app.use('/collection-queryables', queryablesRouter);
app.use('/health', healthRouter);

// 404 handler - must be after all routes
Expand Down
8 changes: 7 additions & 1 deletion api/config/conformanceURIS.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ const CONFORMANCE_URIS = [
'http://www.opengis.net/spec/cql2/1.0/conf/spatial-functions', // s_within, s_contains, etc.

// CQL2 Temporal conformance classes
'http://www.opengis.net/spec/cql2/1.0/conf/temporal-functions' // t_intersects, t_before, t_after
'http://www.opengis.net/spec/cql2/1.0/conf/temporal-functions', // t_intersects, t_before, t_after

'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/collections',
'http://www.opengis.net/spec/ogcapi-features-1/1.0/conf/core',
'https://api.stacspec.org/v1.1.0/collection-search#sortables',


];

module.exports = {
Expand Down
6 changes: 3 additions & 3 deletions api/config/queryablesSchema.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

function buildCollectionsQueryablesSchema(baseUrl) {
const cleanBase = String(baseUrl || '').replace(/\/+$/, '');
const schemaId = `${cleanBase}/collections-queryables`;
const schemaId = `${cleanBase}/collection-queryables`;

// Operator sets based on utils/cql2ToSql.js implementation
const OPS_COMPARISON = ['=', '<>', '<', '<=', '>', '>='];
Expand All @@ -28,10 +28,10 @@
const OPS_SET = ['in'];
const OPS_NULL = ['isNull'];
const OPS_LIKE = ['like'];
const OPS_LOGICAL = ['and', 'or', 'not']; // Applied to expressions, not properties

Check warning on line 31 in api/config/queryablesSchema.js

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

'OPS_LOGICAL' is assigned a value but never used

Check warning on line 31 in api/config/queryablesSchema.js

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

'OPS_LOGICAL' is assigned a value but never used

const OPS_STRING = [...OPS_COMPARISON, ...OPS_RANGE, ...OPS_SET, ...OPS_NULL, ...OPS_LIKE];
const OPS_NUMERIC = [...OPS_COMPARISON, ...OPS_RANGE, ...OPS_SET, ...OPS_NULL];

Check warning on line 34 in api/config/queryablesSchema.js

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

'OPS_NUMERIC' is assigned a value but never used

Check warning on line 34 in api/config/queryablesSchema.js

View workflow job for this annotation

GitHub Actions / Build & Test (22.x)

'OPS_NUMERIC' is assigned a value but never used
const OPS_BOOLEAN = ['=', '<>', ...OPS_NULL];
const OPS_TIMESTAMP = [...OPS_COMPARISON, ...OPS_RANGE, ...OPS_SET, ...OPS_NULL, 't_before', 't_after', 't_intersects'];
const OPS_GEOMETRY = ['s_intersects', 's_within', 's_contains', ...OPS_NULL];
Expand Down Expand Up @@ -77,7 +77,7 @@
id: {
title: 'Collection ID',
description: 'STAC Collection identifier (string or numeric). Maps to c.id.',
type: ['string', 'integer'],
type: ['string'],
'x-ogc-operators': OPS_STRING,
'x-ogc-property': 'c.id'
},
Expand Down Expand Up @@ -289,7 +289,7 @@
collection: {
title: 'Collection (Alias)',
description: 'Alias for id. Maps to c.id.',
type: ['string', 'integer'],
type: ['string'],
'x-ogc-operators': OPS_STRING,
'x-ogc-property': 'c.id',
'x-ogc-alias-of': 'id'
Expand Down
2 changes: 1 addition & 1 deletion api/docs/api-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ Lists all available fields (properties) that can be used for filtering and sorti
The response includes each field’s name, data type, and—where applicable—possible values or value ranges.
Use this endpoint to discover which attributes you can use in your queries and how to reference them in filter expressions.

"http://localhost:3000/collections-queryables"
"http://localhost:3000/collection-queryables"

---

Expand Down
6 changes: 3 additions & 3 deletions api/docs/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ info:
A centralized platform for managing, indexing, and providing STAC Collection metadata from distributed catalogs and APIs.

## Features
- **STAC API 1.1.0 Core** conformance
- **STAC API 1.0.0 Core** conformance
- **Collection Search** with advanced filtering
- **CQL2 Filtering** (Basic + Advanced operators including LIKE, BETWEEN, IN, Spatial, Temporal)
- **Full-text search** with PostgreSQL FTS
Expand Down Expand Up @@ -170,7 +170,7 @@ paths:

**Important:** String literals must be in single quotes: `license = 'MIT'`

See `/collections-queryables` for available properties.
See `/collection-queryables` for available properties.
required: false
schema:
type: string
Expand Down Expand Up @@ -311,7 +311,7 @@ paths:
requestId: "550e8400-e29b-41d4-a716-446655440000"
timestamp: "2026-01-31T12:00:00Z"

/collections-queryables:
/collection-queryables:
get:
summary: Collection Queryables
description: |
Expand Down
2 changes: 1 addition & 1 deletion api/load-test-simple.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,4 +79,4 @@ scenarios:

# Queryables endpoint
- get:
url: "/collections-queryables"
url: "/collection-queryables"
3 changes: 2 additions & 1 deletion api/routes/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,8 @@ router.get('/', validateCollectionSearchParams, async (req, res, next) => {
const links = [
{ rel: 'self', href: selfHref, type: 'application/json' },
{ rel: 'root', href: baseHost, type: 'application/json' },
{ rel: 'parent', href: baseHost, type: 'application/json' }
{ rel: 'parent', href: baseHost, type: 'application/json' },
{ rel: 'http://www.opengis.net/def/rel/ogc/1.0/queryables', href: `${baseHost}/collection-queryables`, type: 'application/schema+json', title: 'Queryables for collection search' }
];

// "next": only if returned === limit AND token + limit < matched
Expand Down
2 changes: 1 addition & 1 deletion api/routes/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ router.get('/', (req, res) => {
},
{
rel: 'queryables',
href: `${baseUrl}/collections-queryables`, //updated path
href: `${baseUrl}/collection-queryables`, //updated path
type: 'application/schema+json',
title: 'Queryables for Collections'
},
Expand Down
4 changes: 2 additions & 2 deletions api/routes/queryables.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ const router = express.Router();
const { buildCollectionsQueryablesSchema } = require('../config/queryablesSchema');

/**
* GET /collections-queryables
* GET /collection-queryables
* Returns the queryables schema for STAC Collections
* Conforms to OGC API Features Part 3 (Filtering) and STAC API Filter Extension
*/
router.get('/', (req, res) => {
const baseUrl = `${req.protocol}://${req.get('host')}`;
const selfUrl = `${baseUrl}/collections-queryables`;
const selfUrl = `${baseUrl}/collection-queryables`;
const schema = buildCollectionsQueryablesSchema(baseUrl);

// Add required links for STAC/OGC conformance
Expand Down
Loading