Skip to content

Commit acaa3cc

Browse files
authored
Merge pull request #279 from SpatioCore/dev-api
Almost final STAC-API-Compliant plus nicer Logging, CORS-Settings and a Favicon
2 parents d0b9550 + 4c1b526 commit acaa3cc

35 files changed

Lines changed: 1725 additions & 719 deletions

.github/workflows/api-ci.yml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ jobs:
7373
# API Configuration
7474
API_TITLE=STAC Atlas
7575
API_DESCRIPTION=A centralized platform for managing, indexing, and providing STAC Collection metadata
76-
API_VERSION=1.1.0
76+
API_VERSION=1.0.0
7777
EOF
7878
7979
# Step 4: Install dependencies
@@ -166,7 +166,7 @@ jobs:
166166
# API Configuration
167167
API_TITLE=STAC Atlas
168168
API_DESCRIPTION=A centralized platform for managing, indexing, and providing STAC Collection metadata
169-
API_VERSION=1.1.0
169+
API_VERSION=1.0.0
170170
EOF
171171
172172
- name: Install dependencies
@@ -230,7 +230,7 @@ jobs:
230230
# API Configuration
231231
API_TITLE=STAC Atlas
232232
API_DESCRIPTION=A centralized platform for managing, indexing, and providing STAC Collection metadata
233-
API_VERSION=1.1.0
233+
API_VERSION=1.0.0
234234
EOF
235235
236236
- name: Install Node dependencies
@@ -269,8 +269,7 @@ jobs:
269269
--root-url "http://localhost:3000/" \
270270
--conformance core \
271271
--conformance collections \
272-
--collection 1 \
273-
--verbose | tee stac-validator-output.txt
272+
--collection africa-agriculture-adaptation-atlas_extreme_hazard_risk_annual \
274273
275274
- name: Upload validator output
276275
uses: actions/upload-artifact@v4

api/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,12 @@ DB_CONNECTION_TIMEOUT=10000
2424

2525
# CORS Configuration
2626
CORS_ORIGIN=*
27+
CORS_CREDENTIALS=false
28+
29+
# Logging Configuration
30+
LOG_LEVEL=debug
2731

2832
# API Configuration
2933
API_TITLE=STAC Atlas
3034
API_DESCRIPTION=A centralized platform for managing, indexing, and providing STAC Collection metadata
31-
API_VERSION=1.1.0
35+
API_VERSION=1.0.0

api/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ CORS_ORIGIN=*
198198

199199
This API implements:
200200

201-
- ✅ STAC API Core (v1.1.0)
201+
- ✅ STAC API Core (v1.0.0)
202202
- ✅ OGC API Features Core
203203
- ✅ STAC Collections
204204
- ✅ Collection Search Extension

api/__tests__/DBconnection.test.js

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const { testConnection, queryByBBox, queryByGeometry, queryByDistance, closePool } = require('../db/db_APIconnection');
2-
2+
const { query } = require('../db/db_APIconnection');
33
/**
44
* Jest Test Suite: Database Connection & PostGIS Tests
55
*/
@@ -23,20 +23,22 @@ describe('Database Connection', () => {
2323

2424
describe('PostGIS - BBox Query', () => {
2525
test('should execute BBox query', async () => {
26-
const result = await queryByBBox('collection', [-180, -90, 180, 90]);
27-
28-
expect(result).toBeDefined();
29-
expect(result.rows).toBeDefined();
30-
});
26+
const result = await queryByBBox('collection', [-80, -60, 80, 60]);
27+
expect(result.rowCount).toBeGreaterThanOrEqual(0);
28+
}, 45000);
3129

3230
test('should return collections within bbox', async () => {
33-
const result = await queryByBBox('collection', [-180, -90, 180, 90]);
34-
31+
const result = await queryByBBox('collection', [-80, -60, 80, 60]);
32+
33+
// query worked and returned structure
34+
expect(Array.isArray(result.rows)).toBe(true);
35+
36+
// if there are results, they should have spatial_extent property
3537
if (result.rowCount > 0) {
36-
expect(result.rows[0]).toHaveProperty('spatial_extend');
37-
expect(result.rowCount).toBeGreaterThan(0);
38+
expect(result.rows[0]).toHaveProperty('spatial_extent');
3839
}
39-
});
40+
}, 45000);
41+
});
4042

4143
test('should reject invalid longitude', async () => {
4244
await expect(
@@ -76,7 +78,7 @@ describe('Database Connection', () => {
7678
expect(result.rows).toBeDefined();
7779
});
7880

79-
test('should return spatial_extend column', async () => {
81+
test('should return spatial_extent column', async () => {
8082
const point = {
8183
type: 'Point',
8284
coordinates: [0, 0]
@@ -85,7 +87,7 @@ describe('Database Connection', () => {
8587
const result = await queryByGeometry('collection', point, 'intersects');
8688

8789
if (result.rowCount > 0) {
88-
expect(result.rows[0]).toHaveProperty('spatial_extend');
90+
expect(result.rows[0]).toHaveProperty('spatial_extent');
8991
}
9092
});
9193

@@ -131,7 +133,7 @@ describe('Database Connection', () => {
131133

132134
if (result.rowCount > 0) {
133135
expect(result.rows[0]).toHaveProperty('distance');
134-
expect(result.rows[0]).toHaveProperty('spatial_extend');
136+
expect(result.rows[0]).toHaveProperty('spatial_extent');
135137
}
136138
});
137139

@@ -163,4 +165,4 @@ describe('Database Connection', () => {
163165
expect(result2).toBeDefined();
164166
});
165167
});
166-
});
168+

api/__tests__/api.test.js

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -74,33 +74,33 @@ describe('STAC API Core Endpoints', () => {
7474
it('should return a STAC Collections response', async () => {
7575
const response = await request(app).get('/collections').expect(200);
7676

77-
7877
expect(response.body).toHaveProperty('collections');
7978
expect(response.body).toHaveProperty('links');
8079
expect(Array.isArray(response.body.collections)).toBe(true);
8180
expect(Array.isArray(response.body.links)).toBe(true);
8281
});
8382

8483
it('should include required link relations', async () => {
85-
const response = await request(app).get('/collections').expect(200);
86-
const rels = response.body.links.map(l => l.rel);
87-
expect(rels).toContain('self');
88-
expect(rels).toContain('root');
89-
expect(rels).toContain('parent');
90-
});
91-
});
84+
const response = await request(app).get('/collections').expect(200);
85+
const rels = response.body.links.map(l => l.rel);
86+
87+
expect(rels).toContain('self');
88+
expect(rels).toContain('root');
89+
expect(rels).toContain('parent');
90+
});
91+
});
9292

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

9797
expect(response.body).toHaveProperty('$schema');
9898
expect(response.body).toHaveProperty('type', 'object');
9999
expect(response.body).toHaveProperty('properties');
100100
});
101101

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

105105
const properties = response.body.properties;
106106
expect(properties).toHaveProperty('id');

api/__tests__/buildCollectionSearchQuery.aggregates.test.js

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ describe('buildCollectionSearchQuery - aggregated fields', () => {
1111
expect(sql).toMatch(/c\.title/);
1212
expect(sql).toMatch(/c\.description/);
1313
expect(sql).toMatch(/c\.license/);
14-
expect(sql).toMatch(/c\.spatial_extend/);
15-
expect(sql).toMatch(/c\.temporal_extend_start/);
16-
expect(sql).toMatch(/c\.temporal_extend_end/);
14+
expect(sql).toMatch(/c\.spatial_extent/);
15+
expect(sql).toMatch(/c\.temporal_extent_start/);
16+
expect(sql).toMatch(/c\.temporal_extent_end/);
1717
expect(sql).toMatch(/c\.created_at/);
1818
expect(sql).toMatch(/c\.updated_at/);
1919
expect(sql).toMatch(/c\.is_api/);
@@ -104,37 +104,36 @@ describe('buildCollectionSearchQuery - aggregated fields', () => {
104104
});
105105

106106
describe('WHERE clauses use collection alias c', () => {
107-
test('bbox filter uses c.spatial_extend', () => {
107+
test('bbox filter uses c.spatial_extent', () => {
108108
const bbox = [-10, 40, 10, 50];
109109
const { sql } = buildCollectionSearchQuery({ bbox, limit: 10, token: 0 });
110110

111-
expect(sql).toMatch(/c\.spatial_extend/);
112-
expect(sql).toMatch(/ST_Intersects\(\s*c\.spatial_extend/);
111+
expect(sql).toMatch(/c\.spatial_extent/);
112+
expect(sql).toMatch(/ST_Intersects\(\s*c\.spatial_extent/);
113113
});
114114

115-
test('datetime filter uses c.temporal_extend_start and c.temporal_extend_end', () => {
115+
test('datetime filter uses c.temporal_extent_start and c.temporal_extent_end', () => {
116116
const datetime = '2020-01-01/2021-12-31';
117117
const { sql } = buildCollectionSearchQuery({ datetime, limit: 10, token: 0 });
118118

119-
expect(sql).toMatch(/c\.temporal_extend_end >= \$/);
120-
expect(sql).toMatch(/c\.temporal_extend_start <= \$/);
119+
expect(sql).toMatch(/c\.temporal_extent_end >= \$/);
120+
expect(sql).toMatch(/c\.temporal_extent_start <= \$/);
121121
});
122122

123123
test('fulltext search uses c.title and c.description', () => {
124124
const q = 'satellite';
125125
const { sql } = buildCollectionSearchQuery({ q, limit: 10, token: 0 });
126126

127-
expect(sql).toMatch(/coalesce\(c\.title,''\)/);
128-
expect(sql).toMatch(/coalesce\(c\.description,''\)/);
129-
expect(sql).toMatch(/to_tsvector\('simple', coalesce\(c\.title,''\) \|\| ' ' \|\| coalesce\(c\.description,''\)\)/);
127+
expect(sql).toMatch(/c\.search_vector\s*@@\s*plainto_tsquery\('simple', \$1\)/);
128+
expect(sql).toMatch(/ts_rank_cd\(c\.search_vector,\s*plainto_tsquery\('simple', \$1\)\)\s+AS\s+rank/);
130129
});
131130
});
132131

133132
describe('ORDER BY uses collection alias c', () => {
134-
test('default ORDER BY uses c.id', () => {
133+
test('default ORDER BY uses c.stac_id', () => {
135134
const { sql } = buildCollectionSearchQuery({ limit: 10, token: 0 });
136135

137-
expect(sql).toMatch(/ORDER BY c\.id ASC/);
136+
expect(sql).toMatch(/ORDER BY c\.stac_id ASC/);
138137
});
139138

140139
test('sortby parameter uses c. prefix', () => {
@@ -144,11 +143,11 @@ describe('buildCollectionSearchQuery - aggregated fields', () => {
144143
expect(sql).toMatch(/ORDER BY c\.title DESC/);
145144
});
146145

147-
test('fulltext search with rank orders by rank DESC, c.id ASC', () => {
146+
test('fulltext search with rank orders by rank DESC, c.stac_id ASC', () => {
148147
const q = 'satellite';
149148
const { sql } = buildCollectionSearchQuery({ q, limit: 10, token: 0 });
150149

151-
expect(sql).toMatch(/ORDER BY rank DESC, c\.id ASC/);
150+
expect(sql).toMatch(/ORDER BY rank DESC, c\.stac_id ASC/);
152151
});
153152
});
154153

api/__tests__/buildCollectionSearchQuery.fulltext.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('buildCollectionSearchQuery - full-text search and ranking', () => {
1313
expect(sql).toMatch(/AS rank/);
1414

1515
// Ordering defaults to rank DESC when q present and no sortby
16-
expect(sql).toMatch(/ORDER BY rank DESC, c\.id ASC/);
16+
expect(sql).toMatch(/ORDER BY rank DESC, c\.stac_id ASC/);
1717

1818
// values: [q, limit, token]
1919
expect(values[0]).toBe('forest');

api/__tests__/buildCollectionSearchQuery.integration.test.js

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('Integration: Collection Search with Aggregated Fields', () => {
3636
const firstRow = result.rows[0];
3737

3838
// Core collection fields
39-
expect(firstRow).toHaveProperty('id');
39+
expect(firstRow).toHaveProperty('stac_id');
4040
expect(firstRow).toHaveProperty('title');
4141
expect(firstRow).toHaveProperty('description');
4242
expect(firstRow).toHaveProperty('license');
@@ -218,14 +218,17 @@ describe('Integration: Collection Search with Aggregated Fields', () => {
218218
});
219219

220220
describe('Sorting with Aggregated Fields', () => {
221-
test('default sort by c.id works with aggregated fields', async () => {
221+
test('default sort by c.stac_id works with aggregated fields', async () => {
222222
const { sql, values } = buildCollectionSearchQuery({ limit: 10, token: 0 });
223223
const result = await query(sql, values);
224224

225+
// Verify SQL contains ORDER BY c.stac_id ASC
226+
expect(sql).toMatch(/ORDER BY c\.stac_id ASC/);
227+
225228
if (result.rows.length > 1) {
226-
// IDs should be in ascending order
229+
// STAC IDs should be in ascending lexicographic order (string comparison)
227230
for (let i = 1; i < result.rows.length; i++) {
228-
expect(result.rows[i].id).toBeGreaterThanOrEqual(result.rows[i - 1].id);
231+
expect(result.rows[i].stac_id.localeCompare(result.rows[i - 1].stac_id)).toBeGreaterThanOrEqual(0);
229232
}
230233
}
231234
});
@@ -268,7 +271,7 @@ describe('Integration: Collection Search with Aggregated Fields', () => {
268271

269272
expect(result.rows.length).toBeLessThanOrEqual(3);
270273
result.rows.forEach(row => {
271-
expect(row).toHaveProperty('id');
274+
expect(row).toHaveProperty('stac_id');
272275
expect(row).toHaveProperty('keywords');
273276
expect(row).toHaveProperty('providers');
274277
});
@@ -280,8 +283,8 @@ describe('Integration: Collection Search with Aggregated Fields', () => {
280283

281284
if (page1.rows.length > 0 && page2.rows.length > 0) {
282285
// IDs should be different
283-
const page1Ids = page1.rows.map(r => r.id);
284-
const page2Ids = page2.rows.map(r => r.id);
286+
const page1Ids = page1.rows.map(r => r.stac_id);
287+
const page2Ids = page2.rows.map(r => r.stac_id);
285288

286289
const overlap = page1Ids.filter(id => page2Ids.includes(id));
287290
expect(overlap.length).toBe(0);
@@ -302,7 +305,7 @@ describe('Integration: Collection Search with Aggregated Fields', () => {
302305
const result = await query(sql, values);
303306

304307
// Collect all IDs
305-
const ids = result.rows.map(r => r.id);
308+
const ids = result.rows.map(r => r.stac_id);
306309
const uniqueIds = [...new Set(ids)];
307310

308311
// No duplicates: each collection should appear exactly once

api/__tests__/buildCollectionsSearchQuery.basic.test.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ describe('buildCollectionSearchQuery - basic cases', () => {
55
const { sql, values } = buildCollectionSearchQuery({ limit: 10, token: 0 });
66

77
expect(sql).toMatch(/FROM collection c/);
8-
expect(sql).toMatch(/ORDER BY c\.id ASC/);
8+
expect(sql).toMatch(/ORDER BY c\.stac_id ASC/);
99
// there should be LIMIT and OFFSET placeholders
1010
expect(sql).toMatch(/LIMIT \$1 OFFSET \$2/);
1111
expect(Array.isArray(values)).toBe(true);
@@ -30,8 +30,8 @@ describe('buildCollectionSearchQuery - basic cases', () => {
3030
const datetime = '2020-01-01/2021-12-31';
3131
const { sql, values } = buildCollectionSearchQuery({ datetime, limit: 10, token: 0 });
3232

33-
expect(sql).toMatch(/c\.temporal_extend_end >= \$1/); // TODO: Adjust naming to temporal_extent_end, when DB names are updated
34-
expect(sql).toMatch(/c\.temporal_extend_start <= \$2/); // TODO: Adjust naming to temporal_extent_start, when DB names are updated
33+
expect(sql).toMatch(/c\.temporal_extent_end >= \$1/);
34+
expect(sql).toMatch(/c\.temporal_extent_start <= \$2/);
3535
// values order: start, end, limit, token
3636
expect(values[0]).toBe('2020-01-01');
3737
expect(values[1]).toBe('2021-12-31');
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const request = require('supertest');
2+
const app = require('../app');
3+
4+
describe('GET /collections-queryables', () => {
5+
it('returns queryables as JSON Schema', async () => {
6+
const res = await request(app).get('/collections-queryables');
7+
8+
expect(res.status).toBe(200);
9+
10+
// content type should be schema+json (may include charset)
11+
expect(res.headers['content-type']).toMatch(/application\/schema\+json/);
12+
13+
// basic JSON Schema structure
14+
expect(res.body).toHaveProperty('$schema');
15+
expect(res.body).toHaveProperty('$id');
16+
expect(res.body).toHaveProperty('type', 'object');
17+
expect(res.body).toHaveProperty('properties');
18+
19+
// required properties from bid/schema
20+
expect(res.body.properties).toHaveProperty('id');
21+
expect(res.body.properties).toHaveProperty('title');
22+
expect(res.body.properties).toHaveProperty('description');
23+
expect(res.body.properties).toHaveProperty('license');
24+
expect(res.body.properties).toHaveProperty('keywords');
25+
expect(res.body.properties).toHaveProperty('providers');
26+
expect(res.body.properties).toHaveProperty('stac_extensions');
27+
28+
// spatial/temporal queryables
29+
expect(res.body.properties).toHaveProperty('spatial_extent');
30+
31+
// operators documented (vendor extension)
32+
expect(res.body.properties.id).toHaveProperty('x-ogc-operators');
33+
expect(Array.isArray(res.body.properties.id['x-ogc-operators'])).toBe(true);
34+
});
35+
});

0 commit comments

Comments
 (0)