OSNBT6AANZB3TF7HAJ35N3Z2EGDU5VQ4LGQORKMA25ACMNV35CQQC
* This file implements the server-side API for Skraak using Hono on Cloudflare Workers.
* It provides authentication middleware and several API endpoints for retrieving data
* from the Neon PostgreSQL database.
* This file implements the server-side API using Hono on Cloudflare Workers.
* It handles:
* - JWT-based authentication via Kinde identity provider
* - API endpoints for retrieving acoustic datasets, locations, clusters
* - File and selection management with filtering capabilities
* - Species and call type information
*
* The API connects to a Neon PostgreSQL database for data storage and retrieval.
* All endpoints except the health check (/api) require authentication.
* Structure of the JWT token payload
* Contains standard JWT claims plus any additional custom claims
* Structure of the JWT token payload from Kinde identity provider
*
* @interface JWTPayload
* @description Defines the structure of the JWT token payload used for authentication
* Contains standard JWT claims (sub, iat, exp, aud, iss) plus any additional custom claims
* Used for extracting user information after successful token verification
* No authentication required
*
* @route GET /api
* @authentication None - publicly accessible
* @returns {Object} Simple status object with "ok" status
* @description Simple endpoint to verify API is running and responding
* Can be used for health checks and monitoring
* Returns datasets owned by the authenticated user
*
* @route GET /api/datasets
* @authentication Required
* @returns {Object} Response containing:
* - data: Array of dataset objects with id, name, description, public status, createdAt, owner
* - userId: The authenticated user's ID (for debugging)
* @description Returns active datasets limited to 20 items
* @example Response:
* {
* "data": [
* { "id": "123", "name": "Bird Sounds 2024", "description": "Costa Rica recordings", "public": true, ... }
* ],
* "userId": "user_123abc"
* }
* Supports pagination with optional page and pageSize parameters
*
* @route GET /api/locations
* @authentication Required
* @param {string} datasetId - Required query parameter specifying the dataset to fetch locations from
* @param {number} [page=1] - Optional page number for pagination (starts at 1)
* @param {number} [pageSize=100] - Optional page size (1-100, defaults to 100)
* @returns {Object} Response containing:
* - data: Array of location objects with id, name, latitude, longitude, description
* - pagination: Object with pagination metadata (currentPage, pageSize, totalPages, totalItems, etc.)
* @error 400 - If datasetId is missing or page is invalid
* @description Returns active locations for the specified dataset with pagination support
* Includes associated recording patterns through a LEFT JOIN
*
* @route GET /api/clusters
* @authentication Required
* @param {string} locationId - Required query parameter specifying the location to fetch clusters from
* @returns {Object} Response containing:
* - data: Array of cluster objects with recording pattern information
* @error 400 - If locationId is missing
* @description Returns clusters for the specified location along with their recording patterns
* Performs a LEFT JOIN with the cyclicRecordingPattern table to include recording duration data
* Results are returned with recording pattern information embedded in each cluster object
* Supports pagination and filtering by:
* - Solar night (true/false)
* - Civil night (true/false)
* - Species ID (for files with selections labeled with a specific species)
*
* Returns file data with metadata, moth metadata, and associated species
* @route GET /api/files
* @authentication Required
* @param {string} clusterId - Required query parameter specifying the cluster to fetch files from
* @param {number} [page=1] - Optional page number for pagination (starts at 1)
* @param {number} [pageSize=100] - Optional page size (10-500, defaults to 100)
* @param {string} [solarNight] - Optional filter for files recorded during solar night ('true'/'false')
* @param {string} [civilNight] - Optional filter for files recorded during civil night ('true'/'false')
* @param {string} [speciesId] - Optional filter for files with selections labeled with specific species
* @returns {Object} Response containing:
* - data: Array of file objects with metadata, mothMetadata, and species information
* - pagination: Object with pagination metadata
* - filters: Object showing the filters that were applied
* @error 400 - If clusterId is missing or page is invalid
* @description Returns audio files for the specified cluster with comprehensive metadata:
* - Basic file information (name, path, timestamp, duration, etc.)
* - File metadata (JSON format)
* - Recording device metadata (gain, battery voltage, temperature)
* - Species found in each file
*
* When speciesId is provided, only returns files that have at least one
* selection labeled with the specified species.
* Returns a nested structure with species and their associated call types
* Uses a JOIN approach to efficiently fetch related data in a single query
* @route GET /api/species
* @authentication Required
* @param {string} datasetId - Required query parameter specifying the dataset to fetch species from
* @returns {Object} Response containing:
* - data: Array of species objects, each with an array of associated call types
* @error 400 - If datasetId is missing
* @description Returns species associated with the specified dataset along with their call types
* Each species object includes:
* - id, label, ebirdCode, description
* - callTypes: Array of call type objects with id and label
*
* Uses an efficient JOIN approach to fetch data in a single query to avoid N+1 query problems
* Results are transformed to provide a nested structure with call types inside species objects
* Supports pagination and filtering by:
* - Solar night (true/false)
* - Civil night (true/false)
*
* Returns file data with metadata, moth metadata, and associated species
* @route GET /api/selections
* @authentication Required
* @param {string} datasetId - Required query parameter specifying the dataset
* @param {string} speciesId - Required query parameter specifying the species to filter by
* @param {number} [page=1] - Optional page number for pagination (starts at 1)
* @param {number} [pageSize=100] - Optional page size (10-500, defaults to 100)
* @param {string} [solarNight] - Optional filter for files recorded during solar night ('true'/'false')
* @param {string} [civilNight] - Optional filter for files recorded during civil night ('true'/'false')
* @returns {Object} Response containing:
* - data: Array of file objects with metadata, mothMetadata, and species information
* - pagination: Object with pagination metadata
* - filters: Object showing the filters that were applied
* @error 400 - If datasetId or speciesId is missing or page is invalid
* @description Returns files that contain selections labeled with the specified species
* Designed for cross-cluster searches within a dataset
*
* Each file object includes:
* - Basic file information (name, path, timestamp, duration, etc.)
* - File metadata (JSON format)
* - Recording device metadata (gain, battery voltage, temperature)
* - Species information
*
* Uses efficient query optimization with:
* - Parallel Promise.all for metadata queries
* - Proper SQL JOINs to avoid N+1 query problems
* - Data transformation for optimal client-side consumption
// Skraak Component Flow Diagram at Friday 16th May 2025
// To render this diagram, copy the contents and paste into the Mermaid playground:
// https://www.mermaidchart.com/play
//
// This diagram visualizes the component hierarchy and API flow in the React application.
graph TD
App["App"] --> KindeProvider["KindeProvider (Auth Context)"]
KindeProvider --> PublicContent["PublicContent"]
KindeProvider --> AuthorisedContent["AuthorisedContent"]
%% Public content branch
PublicContent --> PC_Auth{"isAuthenticated?"}
PC_Auth -- "No" --> Authenticate["Authenticate"]
PC_Auth -- "Yes" --> NoPublicContent["(No Content)"]
%% Authorized content branch
AuthorisedContent --> AC_Auth{"isAuthenticated?"}
AC_Auth -- "No" --> NoAuthorisedContent["(No Content)"]
AC_Auth -- "Yes" --> AC_Content["Authorized Content"]
AC_Content --> UserProfile["UserProfile"]
AC_Content --> AC_View{"activeView?"}
%% Datasets view
AC_View -- "datasets" --> Datasets["Datasets
API: /api/datasets
(fetch datasets)"]
%% Dataset view
AC_View -- "dataset" --> Dataset["Dataset
(Tabs component)"]
Dataset --> DatasetTabs["Tabs"]
DatasetTabs --> Locations["Locations
API: /api/locations?datasetId={id}
(fetch locations)"]
DatasetTabs --> Species["Species
API: /api/species?datasetId={id}
(fetch species)"]
%% Clusters view
AC_View -- "clusters" --> Clusters["Clusters
API: /api/clusters?locationId={id}
(fetch clusters)"]
%% Cluster view
AC_View -- "cluster" --> ClusterComponent["Cluster
API: /api/files?clusterId={id}
(fetch files)
API: /api/species?datasetId={id}
(fetch species for filter)"]
%% Selections view
AC_View -- "selections" --> Selections["Selections
API: /api/selections?datasetId={id}&speciesId={id}
API: /api/species?datasetId={id}
(fetch species for filter)"]
%% User interactions
Datasets -- "onDatasetSelect(id, name)" --> AC_Content
Locations -- "onLocationSelect(id, name)" --> AC_Content
Clusters -- "onClusterSelect(id, name)" --> AC_Content
Species -- "onSpeciesSelect(id, name)" --> AC_Content
%% Authentication flow
UserProfile -. "useKindeAuth()" .-> KindeProvider
PublicContent -. "useKindeAuth()" .-> KindeProvider
AuthorisedContent -. "useKindeAuth()" .-> KindeProvider
Datasets -. "useKindeAuth()" .-> KindeProvider
Locations -. "useKindeAuth()" .-> KindeProvider
Clusters -. "useKindeAuth()" .-> KindeProvider
ClusterComponent -. "useKindeAuth()" .-> KindeProvider
Species -. "useKindeAuth()" .-> KindeProvider
Selections -. "useKindeAuth()" .-> KindeProvider