Files
website/src/routes/projects/animaldex/api/map/+server.ts
Stan Runge 4cfe6bad6c app: lists on meta tables, schema-qualified SQL, typed columns
- lists read/write real lists.* tables via meta.tables/meta.columns and the
  SECURITY DEFINER meta functions; typed extra columns in the item forms
- old numeric list URLs 308-redirect to the slug
- include_recurring is boolean, osu accuracy/pp/rate numeric
- raw SQL qualified with the new schemas
2026-10-23 22:03:42 +00:00

123 lines
3.4 KiB
TypeScript

import { error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { db } from '#lib/server/db/index.js';
import { animals, sightings } from '#lib/server/db/schema/index.js';
import { mapQuerySchema } from '#lib/validation/sighting.js';
import { and, eq, gte, ilike, isNotNull, isNull, lte, sql } from 'drizzle-orm';
export const GET: RequestHandler = async ({ url }) => {
const params = Object.fromEntries(url.searchParams);
const parsed = mapQuerySchema.safeParse(params);
if (!parsed.success) {
throw error(400, 'Invalid query parameters');
}
const q = parsed.data;
const conditions = [
isNotNull(animals.acceptedAt),
isNull(animals.deniedAt),
isNotNull(sightings.acceptedAt),
isNull(sightings.deniedAt),
gte(sightings.lat, q.minLat),
lte(sightings.lat, q.maxLat),
gte(sightings.lng, q.minLng),
lte(sightings.lng, q.maxLng)
];
if (q.species) conditions.push(ilike(animals.species, `%${q.species}%`));
if (q.breed) conditions.push(ilike(animals.breed, `%${q.breed}%`));
if (q.name) conditions.push(ilike(animals.animalName, `%${q.name}%`));
if (q.reporter) conditions.push(ilike(sightings.reporterName, `%${q.reporter}%`));
if (q.fromDate) conditions.push(gte(sightings.seenAt, new Date(q.fromDate)));
if (q.toDate) conditions.push(lte(sightings.seenAt, new Date(q.toDate)));
const rows = await db
.select({
animalId: animals.id,
species: animals.species,
breed: animals.breed,
animalName: animals.animalName,
sightingId: sightings.id,
seenAt: sightings.seenAt,
reporterName: sightings.reporterName,
lat: sightings.lat,
lng: sightings.lng,
photoUrl: sql<string>`(
SELECT url FROM animaldex.sighting_photos
WHERE sighting_id = ${sightings.id}
ORDER BY sort_order ASC
LIMIT 1
)`
})
.from(sightings)
.innerJoin(animals, eq(sightings.animalId, animals.id))
.where(and(...conditions))
.orderBy(sightings.seenAt);
const animalMap = new Map<
number,
{
animalId: number;
species: string;
breed: string | null;
animalName: string | null;
sightings: Array<{
id: number;
lat: number;
lng: number;
seenAt: Date;
reporterName: string | null;
photoUrl: string | null;
}>;
}
>();
for (const row of rows) {
if (!animalMap.has(row.animalId)) {
animalMap.set(row.animalId, {
animalId: row.animalId,
species: row.species,
breed: row.breed,
animalName: row.animalName,
sightings: []
});
}
animalMap.get(row.animalId)!.sightings.push({
id: row.sightingId,
lat: row.lat,
lng: row.lng,
seenAt: row.seenAt,
reporterName: row.reporterName,
photoUrl: row.photoUrl
});
}
const features = Array.from(animalMap.values()).map((animal) => {
const sorted = [...animal.sightings].sort((a, b) => b.seenAt.getTime() - a.seenAt.getTime());
const latest = sorted[0];
return {
type: 'Feature' as const,
geometry: { type: 'Point' as const, coordinates: [latest.lng, latest.lat] },
properties: {
animalId: animal.animalId,
species: animal.species,
breed: animal.breed,
animalName: animal.animalName,
sightingCount: animal.sightings.length,
trail: animal.sightings.map((s) => ({
id: s.id,
lng: s.lng,
lat: s.lat,
seenAt: s.seenAt.toISOString(),
reporterName: s.reporterName,
photoUrl: s.photoUrl
}))
}
};
});
return Response.json({ type: 'FeatureCollection', features });
};