Skip to content

Commit 604700f

Browse files
committed
feat: add getLocationHistory
1 parent 5b82791 commit 604700f

File tree

5 files changed

+221
-4
lines changed

5 files changed

+221
-4
lines changed

src/api/getLocationHistory.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { describe, it } from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import { getLocationHistory } from './getLocationHistory.js'
4+
import APIresponse from './test-data/location.json'
5+
import { aString, arrayMatching, check, objectMatching } from 'tsmatchers'
6+
7+
await describe('getLocationHistory()', async () => {
8+
await it('return the location history', async () => {
9+
const res = await getLocationHistory(
10+
{
11+
endpoint: new URL('https://example.com/'),
12+
apiKey: 'some-key',
13+
},
14+
() =>
15+
Promise.resolve({
16+
ok: true,
17+
json: async () => Promise.resolve(APIresponse),
18+
}) as any,
19+
)({
20+
deviceId: 'oob-355025930003742',
21+
})
22+
assert.equal('error' in res, false)
23+
check('result' in res && res.result).is(
24+
objectMatching({
25+
items: arrayMatching([
26+
objectMatching({
27+
id: '3b45f2db-3b0c-4be8-be9a-273f12697fc4',
28+
deviceId: 'oob-355025930003742',
29+
serviceType: 'MCELL',
30+
insertedAt: '2024-06-04T09:54:52.651Z',
31+
uncertainty: '301',
32+
lat: '59.92335269',
33+
lon: '10.68829941',
34+
meta: {},
35+
}),
36+
]),
37+
total: 100,
38+
pageNextToken: aString,
39+
}) as any,
40+
)
41+
})
42+
})

src/api/getLocationHistory.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { Type, type Static } from '@sinclair/typebox'
2+
import { validatedFetch } from './validatedFetch.js'
3+
import type { ValidationError } from 'ajv'
4+
5+
export enum LocationHistoryServiceType {
6+
ANCHOR = 'ANCHOR',
7+
GNSS = 'GNSS',
8+
GPS = 'GPS',
9+
MCELL = 'MCELL',
10+
MCELL_EVAL = 'MCELL_EVAL',
11+
SCELL = 'SCELL',
12+
SCELL_EVAL = 'SCELL_EVAL',
13+
WIFI = 'WIFI',
14+
WIFI_EVAL = 'WIFI_EVAL',
15+
}
16+
17+
const LocationHistoryType = Type.Object(
18+
{
19+
items: Type.Array(
20+
Type.Object({
21+
id: Type.String({
22+
minLength: 1,
23+
title: 'ID',
24+
description: 'Universally unique identifier',
25+
examples: ['bc631093-7f7c-4c1b-aa63-a68c759bcd5c'],
26+
}),
27+
deviceId: Type.String({
28+
minLength: 1,
29+
title:
30+
'This is the canonical device id used in the device certificate, and as the MQTT client id.',
31+
examples: ['nrf-1234567890123456789000'],
32+
}),
33+
serviceType: Type.Enum(LocationHistoryServiceType, {
34+
title: 'Tracker Service Type',
35+
description:
36+
'This is the service used to obtain the location of a device. The "_EVAL" suffix means the location was obtained using an evaluation token. GNSS location is derived on the device and reported back to the cloud. "GPS" type has been deprecated, but will still return for older records.',
37+
examples: ['location'],
38+
}),
39+
insertedAt: Type.String({
40+
title: 'Insertion Time',
41+
description:
42+
'HTML-encoded ISO-8601 date-time string denoting the start or end of a date range. If the string includes only a date, the time is the beginning of the day (00:00:00).',
43+
examples: ['2021-08-31T20:00:00Z'],
44+
}),
45+
lat: Type.String({
46+
minLength: 1,
47+
examples: ['63.41999531'],
48+
description: 'Latitude in degrees',
49+
}),
50+
lon: Type.String({
51+
minLength: 1,
52+
examples: ['-122.688408'],
53+
description: 'Longitude in degrees',
54+
}),
55+
meta: Type.Record(Type.String({ minLength: 1 }), Type.Any(), {
56+
title: 'GNSS metatdata',
57+
description:
58+
'Metadata sent from device when reporting GNSS location in PVT format. Can include other non-gnss related key/value pairs for easy retrieval later. Only populated for GNSS PVT formatted fixes, empty object otherwise.',
59+
}),
60+
uncertainty: Type.RegExp(/^[0-9.]+$/, {
61+
title: 'Uncertainty',
62+
description:
63+
'Radius of the uncertainty circle around the location in meters. Also known as Horizontal Positioning Error (HPE).',
64+
examples: ['2420', '13.012'],
65+
}),
66+
anchors: Type.Optional(
67+
Type.Array(
68+
Type.Object({
69+
macAddress: Type.RegExp(/^([0-9a-f]{2}:){5}[0-9a-f]{2}$/i, {
70+
title: 'Mac Address',
71+
description:
72+
'String comprised of 6 hexadecimal pairs, separated by colons or dashes. When used in a positioning request, it must be universally assigned. See this help page for details.',
73+
examples: ['FE:1E:41:2D:9E:53'],
74+
}),
75+
name: Type.Optional(
76+
Type.RegExp(/^[a-z0-9_ -]{1,32}$/i, {
77+
title: 'Name',
78+
description:
79+
'Limit 32 characters. Only numbers, letters, underscores, dashes, and spaces allowed. All other characters will be removed.',
80+
examples: ['anchor-1'],
81+
}),
82+
),
83+
}),
84+
),
85+
),
86+
}),
87+
),
88+
total: Type.Number({ minimum: 0 }),
89+
pageNextToken: Type.Optional(Type.String({ minLength: 1 })),
90+
},
91+
{
92+
title: 'Location History',
93+
description:
94+
'See https://api.nrfcloud.com/v1#tag/Location-History/operation/GetLocationHistory',
95+
},
96+
)
97+
export type LocationHistory = Static<typeof LocationHistoryType>
98+
99+
export const getLocationHistory =
100+
(
101+
{
102+
apiKey,
103+
endpoint,
104+
}: {
105+
apiKey: string
106+
endpoint: URL
107+
},
108+
fetchImplementation?: typeof fetch,
109+
) =>
110+
async ({
111+
deviceId,
112+
pageNextToken,
113+
start,
114+
end,
115+
}: {
116+
deviceId: string
117+
pageNextToken?: string
118+
start?: Date
119+
end?: Date
120+
}): Promise<
121+
| { error: Error | ValidationError }
122+
| { result: Static<typeof LocationHistoryType> }
123+
> => {
124+
const query = new URLSearchParams({
125+
pageLimit: '100',
126+
deviceId,
127+
})
128+
if (start !== undefined) query.set('start', start.toISOString())
129+
if (end !== undefined) query.set('end', end.toISOString())
130+
if (pageNextToken !== undefined) query.set('pageNextToken', pageNextToken)
131+
const maybeHistory = await validatedFetch(
132+
{
133+
endpoint,
134+
apiKey,
135+
},
136+
fetchImplementation,
137+
)(
138+
{
139+
resource: 'location/history',
140+
query,
141+
},
142+
LocationHistoryType,
143+
)
144+
145+
if ('error' in maybeHistory) return maybeHistory
146+
return maybeHistory
147+
}

src/api/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './getCurrentMonthlyCosts.js'
44
export * from './createAccountDevice.js'
55
export * from './slashless.js'
66
export * from './getAccountInfo.js'
7+
export * from './getLocationHistory.js'
78
export * from './deleteAccountDevice.js'
89
export * from './getDeviceShadow.js'
910
export * from './DeviceShadow.js'

src/api/test-data/location.json

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"items": [
3+
{
4+
"id": "3b45f2db-3b0c-4be8-be9a-273f12697fc4",
5+
"deviceId": "oob-355025930003742",
6+
"serviceType": "MCELL",
7+
"insertedAt": "2024-06-04T09:54:52.651Z",
8+
"uncertainty": "301",
9+
"lat": "59.92335269",
10+
"lon": "10.68829941",
11+
"meta": {}
12+
},
13+
{
14+
"id": "1d2d7188-ef48-427f-9e7d-55caf47958ac",
15+
"deviceId": "oob-355025930003742",
16+
"serviceType": "WIFI",
17+
"insertedAt": "2024-06-04T09:54:47.014Z",
18+
"uncertainty": "15.066",
19+
"lat": "59.9212502",
20+
"lon": "10.6885059",
21+
"meta": {}
22+
}
23+
],
24+
"total": 100,
25+
"pageNextToken": "G6QAUI3UVncWwk%2Bps9f3dKLy3I6kBRhUXs5fmFKo3JJn4JADhwMGFhAH3BIINuA4xsDGDT8Ev7rn%2FV45Dd2vBBCx6wxjnHJOFJ6JQyLRS0Kmudiqz%2FMEsmojHdxaYxOCA06orvL7gm7dxH2o06ADouim%2BstZ2lqDqFDECoX6Rh%2FJGB5dsxTPDQM%3D"
26+
}

src/api/validatedFetch.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export const validatedFetch =
4343
fetchImplementation?: typeof fetch,
4444
) =>
4545
async <Schema extends TObject>(
46-
params:
46+
params: (
4747
| {
4848
resource: string
4949
}
@@ -54,12 +54,13 @@ export const validatedFetch =
5454
| {
5555
resource: string
5656
method: string
57-
},
57+
}
58+
) & { query?: URLSearchParams },
5859
schema: Schema,
5960
): Promise<
6061
{ error: Error | ValidationError } | { result: Static<Schema> }
6162
> => {
62-
const { resource } = params
63+
const { resource, query } = params
6364
const args: Parameters<typeof fetch>[1] = {
6465
headers: headers(apiKey),
6566
}
@@ -72,7 +73,7 @@ export const validatedFetch =
7273
args.method = params.method
7374
}
7475
return fetchData(fetchImplementation)(
75-
`${slashless(endpoint)}/v1/${resource}`,
76+
`${slashless(endpoint)}/v1/${resource}${query !== undefined ? `?${query.toString()}` : ''}`,
7677
args,
7778
)
7879
.then((res) => ({ result: validate(schema, res) }))

0 commit comments

Comments
 (0)