Skip to content

Commit

Permalink
feat: support more geometry types (#36)
Browse files Browse the repository at this point in the history
* feat: support more geometry types

In addition to `Point`, we now support additional geometry objects.

Closes [#35].

[#35]: #35

* chore: Ensure all alert geometry types are tested

* chore: remove console.log

* chore: fix tsconfig for VSCode inline checking

* fix: fix types in tests

---------

Co-authored-by: Gregor MacLennan <[email protected]>
  • Loading branch information
EvanHahn and gmaclennan authored Jan 23, 2025
1 parent 3499a9f commit f065886
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 62 deletions.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
"format": "prettier --write .",
"test:prettier": "prettier --check .",
"test:eslint": "eslint .",
"test:typescript": "tsc --project ./tsconfig.dev.json",
"test:typescript": "tsc --project ./tsconfig.json",
"test:node": "node --test",
"test": "npm-run-all --aggregate-output --print-label --parallel test:*",
"watch:test:typescript": "tsc --watch --project ./tsconfig.dev.json",
"watch:test:typescript": "tsc --watch --project ./tsconfig.json",
"watch:test:node": "npm run test:node -- --watch",
"prepare": "husky || true",
"prepack": "npm run build"
Expand Down
15 changes: 2 additions & 13 deletions src/routes.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,14 +344,7 @@ export default async function routes(
*/
async function (req, reply) {
const { projectPublicId } = req.params
console.log('projectPublicId', projectPublicId)
let project
try {
project = await this.comapeo.getProject(projectPublicId)
} catch (e) {
console.error(e)
throw e
}
const project = await this.comapeo.getProject(projectPublicId)

await project.remoteDetectionAlert.create({
schemaName: 'remoteDetectionAlert',
Expand Down Expand Up @@ -471,11 +464,7 @@ async function ensureProjectExists(fastify, req) {
try {
await fastify.comapeo.getProject(req.params.projectPublicId)
} catch (e) {
if (
e instanceof Error &&
// TODO: Add a better way to check for this error in @comapeo/core
(e.message.startsWith('NotFound') || e.message.match(/not found/iu))
) {
if (e instanceof Error && e.constructor.name === 'NotFoundError') {
throw errors.projectNotFoundError()
}
throw e
Expand Down
34 changes: 30 additions & 4 deletions src/schemas.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export const observationResult = Type.Object({
),
})

const position = Type.Tuple([longitude, latitude])

export const remoteDetectionAlertToAdd = Type.Object({
detectionDateStart: dateTimeString,
detectionDateEnd: dateTimeString,
Expand All @@ -68,8 +70,32 @@ export const remoteDetectionAlertToAdd = Type.Object({
),
]),
),
geometry: Type.Object({
type: Type.Literal('Point'),
coordinates: Type.Tuple([longitude, latitude]),
}),
geometry: Type.Union([
Type.Object({
type: Type.Literal('Point'),
coordinates: position,
}),
Type.Object({
type: Type.Literal('LineString'),
coordinates: Type.Array(position, { minItems: 2 }),
}),
Type.Object({
type: Type.Literal('MultiLineString'),
coordinates: Type.Array(Type.Array(position, { minItems: 2 })),
}),
Type.Object({
type: Type.Literal('Polygon'),
coordinates: Type.Array(Type.Array(position, { minItems: 4 })),
}),
Type.Object({
type: Type.Literal('MultiPoint'),
coordinates: Type.Array(position),
}),
Type.Object({
type: Type.Literal('MultiPolygon'),
coordinates: Type.Array(
Type.Array(Type.Array(position, { minItems: 4 })),
),
}),
]),
})
107 changes: 64 additions & 43 deletions test/add-alerts-endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
runWithRetries,
} from './test-helpers.js'

/** @import { RemoteDetectionAlertValue } from '@comapeo/schema'*/
/** @import { FastifyInstance } from 'fastify' */

test('returns a 401 if no auth is provided', async (t) => {
Expand Down Expand Up @@ -119,16 +118,6 @@ test('returns a 400 if trying to add invalid alerts', async (t) => {
coordinates: [0, 90.01],
},
},
{
...generateAlert(),
geometry: {
type: 'MultiPoint',
coordinates: [
[1, 2],
[3, 4],
],
},
},
...alertKeys.flatMap((keyToMessUp) => [
omit(generateAlert(), keyToMessUp),
{ ...generateAlert(), [keyToMessUp]: null },
Expand Down Expand Up @@ -177,32 +166,42 @@ test('adding alerts', async (t) => {
dangerouslyAllowInsecureConnections: true,
})

const alert = generateAlert()
const alerts = generateAlerts(100)

const response = await server.inject({
authority: serverUrl.host,
method: 'POST',
url: `/projects/${projectId}/remoteDetectionAlerts`,
headers: {
Authorization: 'Bearer ' + BEARER_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify(alert),
})
assert.equal(response.statusCode, 201)
assert.equal(response.body, '')
await Promise.all(
alerts.map(async (alert) => {
const response = await server.inject({
authority: serverUrl.host,
method: 'POST',
url: `/projects/${projectId}/remoteDetectionAlerts`,
headers: {
Authorization: 'Bearer ' + BEARER_TOKEN,
'Content-Type': 'application/json',
},
body: JSON.stringify(alert),
})
assert.equal(response.statusCode, 201)
assert.equal(response.body, '')
}),
)

project.$sync.start()
project.$sync.connectServers()

await project.$sync.waitForSync('full')

const expectedSourceIds = new Set(alerts.map((a) => a.sourceId))

// It's possible that the client thinks it's synced but doesn't know about
// the server's alert yet, so we try a few times.
await runWithRetries(3, async () => {
const alerts = await project.remoteDetectionAlert.getMany()
const hasOurAlert = alerts.some((a) => a.sourceId === alert.sourceId)
assert(hasOurAlert, 'alert was added and synced')
const actualSourceIds = new Set(alerts.map((a) => a.sourceId))
assert.deepEqual(
actualSourceIds,
expectedSourceIds,
'alerts were added and synced',
)
})
})

Expand All @@ -223,23 +222,45 @@ async function addProject(server) {
return projectKeyToPublicId(Buffer.from(projectKey, 'hex'))
}

function generateAlert() {
const [result] = generateAlerts(1, ['Point'])
assert(result)
return result
}

const SUPPORTED_GEOMETRY_TYPES = /** @type {const} */ ([
'Point',
'MultiPoint',
'LineString',
'MultiLineString',
'Polygon',
'MultiPolygon',
])

/**
* @param {number} min
* @param {number} max
* @returns {number}
* @param {number} count
* @param {ReadonlyArray<typeof SUPPORTED_GEOMETRY_TYPES[number]>} [geometryTypes]
*/
const randomNumber = (min, max) => min + Math.random() * (max - min)
const randomLatitude = randomNumber.bind(null, -90, 90)
const randomLongitude = randomNumber.bind(null, -180, 180)

function generateAlert() {
const remoteDetectionAlertDoc = generate('remoteDetectionAlert')[0]
assert(remoteDetectionAlertDoc)
return valueOf({
...remoteDetectionAlertDoc,
geometry: {
type: 'Point',
coordinates: [randomLongitude(), randomLatitude()],
},
})
function generateAlerts(count, geometryTypes = SUPPORTED_GEOMETRY_TYPES) {
if (count < geometryTypes.length) {
throw new Error(
'test setup: count must be at least as large as geometryTypes',
)
}
// Hacky, but should get the job done ensuring we have all geometry types in the test
const alerts = []
for (const geometryType of geometryTypes) {
/** @type {import('@comapeo/schema').RemoteDetectionAlert | undefined} */
let alert
while (!alert || alert.geometry.type !== geometryType) {
;[alert] = generate('remoteDetectionAlert', { count: 1 })
}
alerts.push(alert)
}
// eslint-disable-next-line prefer-spread
alerts.push.apply(
alerts,
generate('remoteDetectionAlert', { count: count - alerts.length }),
)
return alerts.map((alert) => valueOf(alert))
}
File renamed without changes.

0 comments on commit f065886

Please sign in to comment.