Skip to content

Commit de87c2b

Browse files
authored
Merge pull request #509 from stac-utils/jcw/extened-sns-attributes
2 parents 2802f06 + 11a067e commit de87c2b

File tree

5 files changed

+290
-3
lines changed

5 files changed

+290
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1010
### Added
1111

1212
- Publish ingest results to a post-ingest SNS topic
13+
- Add datetime and bbox attributes to post-ingest SNS messages
1314

1415
### Changed
1516

src/lib/sns.js

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { sns } from './aws-clients.js'
22
import logger from './logger.js'
3-
import { isCollection, isItem } from './stac-utils.js'
3+
import { getBBox, getStartAndEndDates, isCollection, isItem } from './stac-utils.js'
44

55
const attrsFromPayload = function (payload) {
66
let type = 'unknown'
@@ -13,7 +13,7 @@ const attrsFromPayload = function (payload) {
1313
collection = payload.record.collection || ''
1414
}
1515

16-
return {
16+
const attributes = {
1717
recordType: {
1818
DataType: 'String',
1919
StringValue: type
@@ -25,8 +25,61 @@ const attrsFromPayload = function (payload) {
2525
collection: {
2626
DataType: 'String',
2727
StringValue: collection
28+
},
29+
}
30+
31+
const bbox = getBBox(payload.record)
32+
if (bbox) {
33+
attributes['bbox.sw_lon'] = {
34+
DataType: 'Number',
35+
StringValue: bbox[0].toString(),
36+
}
37+
attributes['bbox.sw_lat'] = {
38+
DataType: 'Number',
39+
StringValue: bbox[1].toString(),
40+
}
41+
attributes['bbox.ne_lon'] = {
42+
DataType: 'Number',
43+
StringValue: bbox[2].toString(),
44+
}
45+
attributes['bbox.ne_lat'] = {
46+
DataType: 'Number',
47+
StringValue: bbox[3].toString(),
48+
}
49+
}
50+
51+
if (payload.record?.properties?.datetime) {
52+
attributes.datetime = {
53+
DataType: 'String',
54+
StringValue: payload.record.properties.datetime
55+
}
56+
}
57+
58+
const { startDate, endDate } = getStartAndEndDates(payload.record)
59+
60+
if (startDate) {
61+
attributes.start_datetime = {
62+
DataType: 'String',
63+
StringValue: startDate.toISOString()
64+
}
65+
attributes.start_unix_epoch_ms_offset = {
66+
DataType: 'Number',
67+
StringValue: startDate.getTime().toString()
2868
}
2969
}
70+
71+
if (endDate) {
72+
attributes.end_datetime = {
73+
DataType: 'String',
74+
StringValue: endDate.toISOString()
75+
}
76+
attributes.end_unix_epoch_ms_offset = {
77+
DataType: 'Number',
78+
StringValue: endDate.getTime().toString()
79+
}
80+
}
81+
82+
return attributes
3083
}
3184

3285
/* eslint-disable-next-line import/prefer-default-export */

src/lib/stac-utils.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,62 @@
1+
import logger from './logger.js'
2+
13
export function isCollection(record) {
24
return record && record.type === 'Collection'
35
}
46

57
export function isItem(record) {
68
return record && record.type === 'Feature'
79
}
10+
11+
export function getStartAndEndDates(record) {
12+
let startDate
13+
let endDate
14+
15+
if (isCollection(record)) {
16+
const interval = record.extent?.temporal?.interval || [[null, null]]
17+
if (!interval) {
18+
logger.info(
19+
`Missing extent.temporal.interval in record ${record.id}`
20+
)
21+
}
22+
// STAC spec 1.0.0 says
23+
// "The first time interval always describes the overall temporal extent of the data"
24+
// Nulls are allows for open-ended intervals and nulls for both ends is not forbidden
25+
const [intervalStart, intervalEnd] = interval.length > 0 ? interval[0] : [null, null]
26+
if (intervalStart) {
27+
startDate = new Date(intervalStart)
28+
}
29+
if (intervalEnd) {
30+
endDate = new Date(intervalEnd)
31+
}
32+
} else if (isItem(record)) {
33+
const properties = record.properties || {}
34+
if (properties.start_datetime && properties.end_datetime) {
35+
startDate = new Date(properties.start_datetime)
36+
endDate = new Date(properties.end_datetime)
37+
} else if (properties.datetime) {
38+
startDate = new Date(properties.datetime)
39+
endDate = startDate
40+
} else {
41+
const propertiesString = JSON.stringify(properties)
42+
logger.info(
43+
`Missing properties in record ${record.id}:
44+
Expected datetime or both start_datetime and end_datetime in ${propertiesString}`
45+
)
46+
}
47+
}
48+
49+
return { startDate, endDate }
50+
}
51+
52+
export function getBBox(record) {
53+
if (isCollection(record)) {
54+
return record.extent?.spatial?.bbox?.length > 0
55+
? record.extent.spatial.bbox[0]
56+
: undefined
57+
}
58+
if (isItem(record)) {
59+
return record.bbox || undefined
60+
}
61+
return undefined
62+
}

tests/system/test-ingest.js

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,22 @@ test('Ingested collection is published to post-ingest SNS topic', async (t) => {
348348
t.is(attrs.collection.Value, collection.id)
349349
t.is(attrs.ingestStatus.Value, 'successful')
350350
t.is(attrs.recordType.Value, 'Collection')
351+
352+
const bbox = collection.extent.spatial.bbox[0]
353+
t.is(bbox[0].toString(), attrs['bbox.sw_lon'].Value)
354+
t.is(bbox[1].toString(), attrs['bbox.sw_lat'].Value)
355+
t.is(bbox[2].toString(), attrs['bbox.ne_lon'].Value)
356+
t.is(bbox[3].toString(), attrs['bbox.ne_lat'].Value)
357+
358+
const expectedStartOffsetValue = (new Date(collection.extent.temporal.interval[0][0]))
359+
.getTime().toString()
360+
t.is(expectedStartOffsetValue, attrs.start_unix_epoch_ms_offset.Value)
361+
t.is(
362+
(new Date(collection.extent.temporal.interval[0][0])).toISOString(),
363+
attrs.start_datetime.Value
364+
)
365+
t.is(undefined, attrs.end_unix_epoch_ms_offset)
366+
t.is(undefined, attrs.end_datetime)
351367
})
352368

353369
test('Ingested collection is published to post-ingest SNS topic with updated links', async (t) => {
@@ -382,6 +398,10 @@ test('Ingest collection failure is published to post-ingest SNS topic', async (t
382398
t.is(attrs.collection.Value, 'badCollection')
383399
t.is(attrs.ingestStatus.Value, 'failed')
384400
t.is(attrs.recordType.Value, 'Collection')
401+
t.is(undefined, attrs.start_unix_epoch_ms_offset)
402+
t.is(undefined, attrs.start_datetime)
403+
t.is(undefined, attrs.end_unix_epoch_ms_offset)
404+
t.is(undefined, attrs.end_datetime)
385405
})
386406

387407
async function emptyPostIngestQueue(t) {
@@ -422,16 +442,37 @@ test('Ingested item is published to post-ingest SNS topic', async (t) => {
422442

423443
const item = await loadFixture(
424444
'stac/ingest-item.json',
425-
{ id: randomId('item'), collection: collection.id }
445+
{
446+
id: randomId('item'),
447+
collection: collection.id
448+
}
426449
)
427450

451+
item.properties.start_datetime = '1955-11-05T13:00:00.000Z'
452+
item.properties.end_datetime = '1985-11-05T13:00:00.000Z'
453+
428454
const { message, attrs } = await testPostIngestSNS(t, item)
429455

430456
t.is(message.record.id, item.id)
431457
t.deepEqual(message.record.links, item.links)
432458
t.is(attrs.collection.Value, item.collection)
433459
t.is(attrs.ingestStatus.Value, 'successful')
434460
t.is(attrs.recordType.Value, 'Item')
461+
462+
t.is(item.bbox[0].toString(), attrs['bbox.sw_lon'].Value)
463+
t.is(item.bbox[1].toString(), attrs['bbox.sw_lat'].Value)
464+
t.is(item.bbox[2].toString(), attrs['bbox.ne_lon'].Value)
465+
t.is(item.bbox[3].toString(), attrs['bbox.ne_lat'].Value)
466+
467+
t.is(message.record.properties.datetime, attrs.datetime.Value)
468+
469+
const expectedStartOffsetValue = (new Date(item.properties.start_datetime)).getTime().toString()
470+
t.is(expectedStartOffsetValue, attrs.start_unix_epoch_ms_offset.Value)
471+
t.is(item.properties.start_datetime, attrs.start_datetime.Value)
472+
473+
const expectedEndOffsetValue = (new Date(item.properties.end_datetime)).getTime().toString()
474+
t.is(expectedEndOffsetValue, attrs.end_unix_epoch_ms_offset.Value)
475+
t.is(item.properties.end_datetime, attrs.end_datetime.Value)
435476
})
436477

437478
test('Ingest item failure is published to post-ingest SNS topic', async (t) => {

tests/unit/test-stac-utils.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// @ts-nocheck
2+
3+
import test from 'ava'
4+
import { getStartAndEndDates } from '../../src/lib/stac-utils.js'
5+
6+
test('getStartandEndDates uses item datetime', (t) => {
7+
const stringDate = '1955-11-05T13:00:00Z'
8+
const { startDate, endDate } = getStartAndEndDates({
9+
type: 'Feature',
10+
id: 'test',
11+
properties: {
12+
datetime: stringDate
13+
}
14+
})
15+
const date = new Date(stringDate)
16+
t.deepEqual(date, startDate, 'startDate did not match datetime')
17+
t.deepEqual(date, endDate, 'endDate did not match datetime')
18+
})
19+
20+
test('getStartandEndDates uses item start_datetime and end_datetime', (t) => {
21+
const datetime = '1955-11-05T13:00:00Z'
22+
const startDatetime = '1985-11-05T13:00:00Z'
23+
const endDatetime = '2015-11-05T13:00:00Z'
24+
const { startDate, endDate } = getStartAndEndDates({
25+
type: 'Feature',
26+
id: 'test',
27+
properties: {
28+
datetime,
29+
start_datetime: startDatetime,
30+
end_datetime: endDatetime,
31+
}
32+
})
33+
t.deepEqual(new Date(startDatetime), startDate, 'startDate did not match start_datetime')
34+
t.deepEqual(new Date(endDatetime), endDate, 'endDate did not match end_datetime')
35+
})
36+
37+
test('getStartandEndDates uses item start_datetime and end_datetime with null datetime', (t) => {
38+
const startDatetime = '1985-11-05T13:00:00Z'
39+
const endDatetime = '2015-11-05T13:00:00Z'
40+
const { startDate, endDate } = getStartAndEndDates({
41+
type: 'Feature',
42+
id: 'test',
43+
properties: {
44+
datetime: null,
45+
start_datetime: startDatetime,
46+
end_datetime: endDatetime,
47+
}
48+
})
49+
t.deepEqual(new Date(startDatetime), startDate, 'startDate did not match start_datetime')
50+
t.deepEqual(new Date(endDatetime), endDate, 'endDate did not match end_datetime')
51+
})
52+
53+
test('getStartandEndDates returns undefineds if item datetime is null', (t) => {
54+
const { startDate, endDate } = getStartAndEndDates({
55+
type: 'Feature',
56+
id: 'test',
57+
properties: {
58+
datetime: null
59+
}
60+
})
61+
t.deepEqual(undefined, startDate, 'startDate is not undefined')
62+
t.deepEqual(undefined, endDate, 'endDate is not undefined')
63+
})
64+
65+
test('getStartandEndDates returns undefineds if collection interval is missing', (t) => {
66+
const { startDate, endDate } = getStartAndEndDates({
67+
type: 'Collection',
68+
id: 'test',
69+
extent: {
70+
temporal: {
71+
interval: []
72+
}
73+
}
74+
})
75+
t.deepEqual(undefined, startDate, 'startDate is not undefined')
76+
t.deepEqual(undefined, endDate, 'endDate is not undefined')
77+
})
78+
79+
test('getStartandEndDates returns startDate if collection interval has start', (t) => {
80+
const startDatetime = '1985-11-05T13:00:00Z'
81+
const { startDate, endDate } = getStartAndEndDates({
82+
type: 'Collection',
83+
id: 'test',
84+
extent: {
85+
temporal: {
86+
interval: [[startDatetime, null]]
87+
}
88+
}
89+
})
90+
t.deepEqual(new Date(startDatetime), startDate, 'startDate was not returned')
91+
t.deepEqual(undefined, endDate, 'endDate is not undefined')
92+
})
93+
94+
test('getStartandEndDates returns endDate if collection interval has end', (t) => {
95+
const endDatetime = '1985-11-05T13:00:00Z'
96+
const { startDate, endDate } = getStartAndEndDates({
97+
type: 'Collection',
98+
id: 'test',
99+
extent: {
100+
temporal: {
101+
interval: [[null, endDatetime]]
102+
}
103+
}
104+
})
105+
t.deepEqual(undefined, startDate, 'startDate is not undefined')
106+
t.deepEqual(new Date(endDatetime), endDate, 'endDate was not returned')
107+
})
108+
109+
test('getStartandEndDates returns collection interval', (t) => {
110+
const startDatetime = '1955-11-05T13:00:00Z'
111+
const endDatetime = '1985-11-05T13:00:00Z'
112+
const { startDate, endDate } = getStartAndEndDates({
113+
type: 'Collection',
114+
id: 'test',
115+
extent: {
116+
temporal: {
117+
interval: [[startDatetime, endDatetime]]
118+
}
119+
}
120+
})
121+
t.deepEqual(new Date(startDatetime), startDate, 'startDate was not returned')
122+
t.deepEqual(new Date(endDatetime), endDate, 'endDate was not returned')
123+
})
124+
125+
test('getStartandEndDates only looks at first collection interval', (t) => {
126+
const { startDate, endDate } = getStartAndEndDates({
127+
type: 'Collection',
128+
id: 'test',
129+
extent: {
130+
temporal: {
131+
interval: [[null, null], ['1955-11-05T13:00:00Z', '1985-11-05T13:00:00Z']]
132+
}
133+
}
134+
})
135+
t.deepEqual(undefined, startDate, 'startDate is not undefined')
136+
t.deepEqual(undefined, endDate, 'endDate is not undefined')
137+
})

0 commit comments

Comments
 (0)