Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support more geometry types #36

Merged
merged 6 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
Loading