Files
missions/_docs/02_document/components/02_mission_planning/description.md
T
Oleksandr Bezdieniezhnykh 7025f4d075 refactor: enhance JWT authentication and CORS configuration
Updated JWT authentication to use configuration values instead of hardcoded secrets, improving security and flexibility. Enhanced CORS policy to conditionally allow origins based on configuration settings, with logging for permissive defaults. Updated README to reflect project renaming and clarify service context.
2026-05-14 19:48:25 +03:00

9.7 KiB

02 — Mission Planning (Missions + Waypoints + Cross-Service Cascade)

Spec source: ../../../suite/_docs/02_missions.md § "Missions" (items 1-9), ../../../suite/_docs/00_database_schema.md § Missions + Waypoints.

Required permission: FL.

Implementation status: implemented (with two divergences -- see Caveats).

NOTE (forward-looking): file paths, route prefixes, and identifiers below reflect the post-rename state. Today's source still uses Flight* filenames + [Route("flights")] and the cascade still touches orthophotos + gps_corrections. Renames + cascade shrink tracked under Jira AZ-EPIC children B6 (rename), B7 (GPS-Denied removal), B8 (HTTP routes).

Files (post-rename):

  • HTTP: Controllers/MissionsController.cs (parent + nested waypoint routes)
  • Services: Services/MissionService.cs, Services/WaypointService.cs
  • DTOs: DTOs/CreateMissionRequest.cs, DTOs/UpdateMissionRequest.cs, DTOs/GetMissionsQuery.cs, DTOs/CreateWaypointRequest.cs, DTOs/UpdateWaypointRequest.cs, DTOs/GeoPoint.cs
  • Resource enums: Enums/WaypointSource.cs, Enums/WaypointObjective.cs

(Entity row maps live in 04_persistence.)

1. High-Level Overview

Purpose: Own the mission lifecycle for an edge deployment. A "mission" is a planned record with a name, creation timestamp, and assigned vehicle (Plane / Copter / UGV / GuidedMissile); "waypoints" are the ordered geo-points (with altitude, source, and objective) that define the mission's route. This component is consumed by autopilot (reads the mission and waypoints to drive the vehicle) and by the ui (map view + planning UI).

Architectural pattern: Aggregate root (Mission) with a sub-aggregate (Waypoint). Manual cascade-delete -- schema declares plain REFERENCES (no ON DELETE CASCADE); this service walks the dependency graph by hand.

Cross-service contract -- the cascade: when a mission or waypoint is deleted, this service also tears down rows in tables it does NOT own the schema for: media + annotations (owned by the annotations service) and detection (owned by the detection pipeline), plus its own map_objects. Per ../../../suite/_docs/02_missions.md §5 + §9 this is the canonical, spec-defined behavior -- this service is the only place that knows the full mission ownership graph and is contractually responsible for this cleanup. The shared local PostgreSQL on the edge device makes the multi-table cascade physically possible in one connection.

Removed from cascade in B7: orthophotos and gps_corrections. Those tables now live in the separate gps-denied service per ../../../suite/_docs/11_gps_denied.md. MissionService.DeleteMission and WaypointService.DeleteWaypoint no longer reference them.

Upstream dependencies: 05_identity ([Authorize FL]), 04_persistence, 06_http_conventions, 01_vehicle_catalog (existence check on vehicle_id).

Downstream consumers (live runtime): autopilot (reads missions + waypoints), ui (planning + map). The new gps-denied service references mission_id and waypoint_id from its own tables but does NOT depend on this service at runtime; cleanup of its rows is its own concern.

2. Internal Interface

public class MissionService(AppDataConnection db) {
    Task<Mission>                    CreateMission(CreateMissionRequest);
    Task<Mission>                    UpdateMission(Guid id, UpdateMissionRequest);
    Task<Mission>                    GetMission(Guid id);
    Task<PaginatedResponse<Mission>> GetMissions(GetMissionsQuery);
    Task                             DeleteMission(Guid id);   // cross-service cascade
}

public class WaypointService(AppDataConnection db) {
    Task<Waypoint>        CreateWaypoint(Guid missionId, CreateWaypointRequest);
    Task<Waypoint>        UpdateWaypoint(Guid missionId, Guid waypointId, UpdateWaypointRequest);
    Task<List<Waypoint>>  GetWaypoints(Guid missionId);          // unpaginated by spec
    Task                  DeleteWaypoint(Guid missionId, Guid waypointId);  // cross-service cascade
}

Throws KeyNotFoundException (-> 404), ArgumentException (-> 400, when referenced vehicle_id doesn't exist).

3. External API

Missions

Spec # Endpoint Method Auth Description
1 /missions POST FL Create. Body CreateMissionRequest. Code throws ArgumentException -> 400 if VehicleId doesn't exist; spec says 404 -- minor divergence.
2 /missions/{id:guid} PUT FL Partial update (Name and/or VehicleId).
7 /missions/{id:guid} GET FL Single by id.
8 /missions GET FL Paginated list. Query: Name?, FromDate?, ToDate?, Page=1, PageSize=20. Returns PaginatedResponse<Mission> (envelope from 06_http_conventions).
9 /missions/{id:guid} DELETE FL Cascade-deletes waypoints, media, annotations, detection, map_objects (in dependency order).

Waypoints (nested under mission)

Spec # Endpoint Method Auth Description
3 /missions/{id:guid}/waypoints POST FL Create. Body CreateWaypointRequest. 404 if mission missing.
4 /missions/{id:guid}/waypoints/{wpId:guid} PUT FL Full overwrite of all waypoint fields (Caveats #2 -- diverges from partial-update intent).
5 /missions/{id:guid}/waypoints/{wpId:guid} DELETE FL Cascade-deletes related media, annotations, detection.
6 /missions/{id:guid}/waypoints GET FL Ordered by OrderNum. Unpaginated (matches spec).

Wire shape: PascalCase (suite-wide divergence -- see 06_http_conventions).

4. Data Access Patterns

Read queries

Query Frequency Hot Path Index
missions WHERE id = ? Every read/update/delete Yes PK ✓
missions WHERE ... ORDER BY created_date DESC LIMIT N OFFSET M (+ count) Listing Yes None on created_date -- could be added
vehicles WHERE id = ? (existence) Every mission create / update with vehicle change Yes PK ✓ (cross-component)
waypoints WHERE mission_id = ? AND id = ? Per-waypoint read/update/delete Yes PK + ix_waypoints_mission_id
waypoints WHERE mission_id = ? ORDER BY order_num Nested list Medium ix_waypoints_mission_id (sort still in-memory)

Cascade-delete writes (MissionService.DeleteMission)

In strict dependency order:

  1. DELETE FROM map_objects WHERE mission_id = ? (autopilot-written, owned-here schema)
  2. Resolve waypointIds = SELECT id FROM waypoints WHERE mission_id = ?
  3. If any: resolve mediaIds, annotationIds, then DELETE FROM detection, DELETE FROM annotations, DELETE FROM media (cross-service tables -- schema owned by annotations + detection pipeline)
  4. DELETE FROM waypoints WHERE mission_id = ?
  5. DELETE FROM missions WHERE id = ?

WaypointService.DeleteWaypoint does the equivalent steps 2-4 scoped to one waypoint.

No transaction wraps either cascade -- partial failure leaves orphan rows. Tracked as Caveat #1; would be a one-line fix (db.BeginTransactionAsync()).

Caching

None.

Storage Estimates

Not specified.

5. Implementation Details

State Management: Stateless services.

Key business rules:

  • mission.vehicle_id must reference an existing vehicle (validated on create + on update if changed).
  • waypoint.mission_id must reference an existing mission (validated on create).
  • Cascade tables must be deleted in child-before-parent order due to FK constraints.

Error Handling: Services throw; 06_http_conventions middleware maps.

6. Extensions and Helpers

PaginatedResponse<T> (defined in 06_http_conventions) is consumed only by this component.

7. Caveats & Edge Cases

  1. No transaction around cascade delete -- partial failure orphans rows in media, annotations, detection, map_objects, or waypoints. Wrapping in db.BeginTransactionAsync() is one extra line and would make the cascade atomic.
  2. UpdateWaypoint overwrites all fields even though the request looks "partial-shaped" -- sending {} zeroes out coordinates and resets enums. Spec §4 also overwrites all fields, but spec uses the auto-converting Geopoint type so a missing Geopoint would be null not zero. With code's 3-flat-fields shape, this is more error-prone.
  3. Geopoint shape divergence from spec: spec defines a single string GPS with auto-conversion (Lat <-> MGRS). Code uses 3 separate columns with no conversion. Carries through Waypoint, MapObject, and the request DTOs.
  4. Vehicle existence check + mission insert is non-transactional -- TOCTOU window for vehicle delete is mitigated by the FK (which would reject the insert), but the error UX would surface as a 500 instead of a 400 in that race.
  5. No reorder endpoint -- N waypoints reordered = N PUTs, racy.
  6. Cascade depends on cross-service tables existing in the same DB. In standard edge deployment this is guaranteed (annotations/detection migrate them in the same compose stack, same postgres-local). In any deployment where those services are absent, the cascade will throw relation does not exist.
  7. Entity returned on the wire with [Association] properties (Mission.Vehicle, Mission.Waypoints, Waypoint.Mission); LinqToDB does NOT eager-load by default on FirstOrDefaultAsync(predicate), so they serialize as null / []. Verify in Step 4 against actual responses.
  8. Spec §1 says 404 on missing VehicleId; code throws ArgumentException which maps to 400. Minor divergence.

8. Dependency Graph

Must be implemented after: 05_identity, 06_http_conventions, 04_persistence, 01_vehicle_catalog.

Blocks: 07_host.

9. Logging Strategy

No app-level logs.