Skip to content

Commit fa690e3

Browse files
committed
feat(api): add cell geolocation cache
1 parent db63a41 commit fa690e3

File tree

6 files changed

+319
-0
lines changed

6 files changed

+319
-0
lines changed

package-lock.json

Lines changed: 116 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@
8787
],
8888
"prettier": "@bifravst/prettier-config",
8989
"peerDependencies": {
90+
"@aws-sdk/client-dynamodb": "^3.552.0",
9091
"@aws-sdk/client-ssm": "^3.552.0",
92+
"@aws-sdk/util-dynamodb": "^3.552.0",
9193
"@bifravst/aws-ssm-settings-helpers": "^1.0.3",
9294
"@hello.nrfcloud.com/proto": "^6.5.0",
9395
"@sinclair/typebox": "^0.32.20"

src/cellGeoLocation/cache.spec.ts

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, it, mock } from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import { ResourceNotFoundException } from '@aws-sdk/client-dynamodb'
4+
import { get, store } from './cache.js'
5+
import { arrayContaining, check, objectMatching } from 'tsmatchers'
6+
7+
export const assertCall = (
8+
mockFn: ReturnType<(typeof mock)['fn']>,
9+
args: Record<string, unknown>,
10+
callNumber: number = 0,
11+
): void => {
12+
check(mockFn.mock.calls[callNumber]?.arguments).is(
13+
arrayContaining(objectMatching(args)),
14+
)
15+
}
16+
17+
void describe('cellGeoLocation/cache', () => {
18+
void describe('get()', () => {
19+
void it('should return null if cell is not cached', async () => {
20+
const send = mock.fn(async () =>
21+
Promise.reject(
22+
new ResourceNotFoundException({
23+
message: ` Requested resource not found`,
24+
$metadata: {},
25+
}),
26+
),
27+
)
28+
assert.equal(
29+
await get({
30+
db: { send } as any,
31+
TableName: 'cacheTable',
32+
})({
33+
area: 42,
34+
mccmnc: 53005,
35+
cell: 666,
36+
}),
37+
null,
38+
)
39+
assertCall(send, {
40+
input: {
41+
Key: {
42+
cellId: {
43+
S: '53005-42-666',
44+
},
45+
},
46+
TableName: 'cacheTable',
47+
},
48+
})
49+
})
50+
51+
void it('should return the location if the cell is cached', async () => {
52+
const send = mock.fn(async () =>
53+
Promise.resolve({
54+
$metadata: {
55+
httpStatusCode: 200,
56+
requestId: 'SHTTK1LO0ELJ5LI2U0LUJOUBOJVV4KQNSO5AEMVJF66Q9ASUAAJG',
57+
extendedRequestId: undefined,
58+
cfId: undefined,
59+
attempts: 1,
60+
totalRetryDelay: 0,
61+
},
62+
Item: {
63+
cellId: { S: '53005-42-666' },
64+
lat: { N: '-36.87313199' },
65+
lng: { N: '174.7577405' },
66+
accuracy: { N: '510' },
67+
},
68+
}),
69+
)
70+
assert.deepEqual(
71+
await get({
72+
db: { send } as any,
73+
TableName: 'cacheTable',
74+
})({
75+
area: 42,
76+
mccmnc: 53005,
77+
cell: 666,
78+
}),
79+
{ accuracy: 510, lat: -36.87313199, lng: 174.7577405 },
80+
)
81+
assertCall(send, {
82+
input: {
83+
Key: {
84+
cellId: {
85+
S: '53005-42-666',
86+
},
87+
},
88+
TableName: 'cacheTable',
89+
},
90+
})
91+
})
92+
})
93+
void describe('store()', () => {
94+
void it('should store the cell', async () => {
95+
const send = mock.fn(async () => Promise.resolve({}))
96+
await store({
97+
db: { send } as any,
98+
TableName: 'cacheTable',
99+
})(
100+
{
101+
area: 42,
102+
mccmnc: 53005,
103+
cell: 666,
104+
},
105+
{ accuracy: 510, lat: -36.87313199, lng: 174.7577405 },
106+
)
107+
assertCall(send, {
108+
input: {
109+
Item: {
110+
accuracy: { N: '510' },
111+
cellId: { S: '53005-42-666' },
112+
lat: { N: '-36.87313199' },
113+
lng: { N: '174.7577405' },
114+
ttl: {
115+
N: `${Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60}`,
116+
},
117+
},
118+
TableName: 'cacheTable',
119+
},
120+
})
121+
})
122+
})
123+
})

src/cellGeoLocation/cache.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
DynamoDBClient,
3+
GetItemCommand,
4+
PutItemCommand,
5+
} from '@aws-sdk/client-dynamodb'
6+
import { cellId } from './cellId.js'
7+
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb'
8+
9+
export const get =
10+
({ db, TableName }: { db: DynamoDBClient; TableName: string }) =>
11+
async (
12+
cell: Parameters<typeof cellId>[0],
13+
): Promise<{ lat: number; lng: number; accuracy: number } | null> => {
14+
try {
15+
const { Item } = await db.send(
16+
new GetItemCommand({
17+
TableName,
18+
Key: {
19+
cellId: {
20+
S: cellId(cell),
21+
},
22+
},
23+
}),
24+
)
25+
const { lat, lng, accuracy } = unmarshall(
26+
Item as Record<string, never>,
27+
) as {
28+
lat: number
29+
lng: number
30+
accuracy: number
31+
}
32+
return { lat, lng, accuracy }
33+
} catch {
34+
return null
35+
}
36+
}
37+
38+
export const store =
39+
({ db, TableName }: { db: DynamoDBClient; TableName: string }) =>
40+
async (
41+
cell: Parameters<typeof cellId>[0],
42+
location: { lat: number; lng: number; accuracy: number },
43+
): Promise<void> => {
44+
await db.send(
45+
new PutItemCommand({
46+
TableName,
47+
Item: marshall({
48+
cellId: cellId(cell),
49+
...location,
50+
ttl: Math.floor(Date.now() / 1000) + 30 * 24 * 60 * 60,
51+
}),
52+
}),
53+
)
54+
}

src/cellGeoLocation/cellId.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe, it } from 'node:test'
2+
import assert from 'node:assert/strict'
3+
import { cellId } from './cellId.js'
4+
5+
void describe('cellId', () => {
6+
void it('should generate a cellId', () =>
7+
assert.equal(
8+
cellId({
9+
area: 42,
10+
mccmnc: 53005,
11+
cell: 666,
12+
}),
13+
'53005-42-666',
14+
))
15+
})

src/cellGeoLocation/cellId.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export const cellId = ({
2+
area,
3+
mccmnc,
4+
cell,
5+
}: {
6+
area: number
7+
mccmnc: number
8+
cell: number
9+
}): string => `${mccmnc}-${area}-${cell}`

0 commit comments

Comments
 (0)