mirror of
https://github.com/azaion/missions.git
synced 2026-06-21 19:31:06 +00:00
7025f4d075
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.
103 lines
5.2 KiB
Markdown
103 lines
5.2 KiB
Markdown
# Module: `Azaion.Missions.Services.VehicleService`
|
|
|
|
**File**: `Services/VehicleService.cs`
|
|
|
|
> **NOTE (forward-looking)**: post-rename. Today's source is `Services/AircraftService.cs` operating over `Aircraft` entities. Renames tracked under Jira AZ-EPIC child B6.
|
|
|
|
## Purpose
|
|
|
|
Encapsulates vehicle-related domain operations: CRUD plus the "is_default" exclusivity rule. This is the only place the "exactly one vehicle is default" invariant is enforced.
|
|
|
|
## Public Interface
|
|
|
|
```csharp
|
|
public class VehicleService(AppDataConnection db) {
|
|
Task<Vehicle> CreateVehicle(CreateVehicleRequest request);
|
|
Task<Vehicle> UpdateVehicle(Guid id, UpdateVehicleRequest request);
|
|
Task<Vehicle> GetVehicle(Guid id);
|
|
Task<List<Vehicle>> GetVehicles(GetVehiclesQuery query);
|
|
Task DeleteVehicle(Guid id);
|
|
Task SetDefault(Guid id, SetDefaultRequest request);
|
|
}
|
|
```
|
|
|
|
## Internal Logic
|
|
|
|
### `CreateVehicle`
|
|
1. If `request.IsDefault` is `true`, **clear `is_default` on every other vehicle first** (`UPDATE vehicles SET is_default = FALSE WHERE is_default = TRUE`).
|
|
2. Build a fresh `Vehicle` with `Id = Guid.NewGuid()` and copy every request field 1:1.
|
|
3. `db.InsertAsync(vehicle)` and return the persisted entity.
|
|
|
|
### `UpdateVehicle`
|
|
1. Load the row by id; throw `KeyNotFoundException` if not found (mapped to 404 by middleware).
|
|
2. For each property on `UpdateVehicleRequest`, apply only the non-null fields (partial update).
|
|
3. If `IsDefault.Value == true`, run the same exclusivity clear as `CreateVehicle` BEFORE setting the new default.
|
|
4. `db.UpdateAsync(vehicle)`.
|
|
|
|
### `GetVehicle`
|
|
- `FirstOrDefaultAsync(v => v.Id == id)` -> 404 on miss.
|
|
|
|
### `GetVehicles`
|
|
- Builds a LINQ query on `db.Vehicles`.
|
|
- Filters: `query.Name` -> case-insensitive `Contains` (uses `string.ToLower()` on both sides -- runs as `LOWER(name) LIKE %lower(input)%` server-side via LinqToDB translation); `query.IsDefault` -> exact equality.
|
|
- Orders by `Name` ascending.
|
|
- Returns the full list -- **no pagination** despite `GetVehiclesQuery` being a query object. Spec accepts this (vehicles are a small dataset).
|
|
|
|
### `DeleteVehicle`
|
|
1. Guard: `db.Missions.AnyAsync(m => m.VehicleId == id)` -> if any mission references the vehicle, throw `InvalidOperationException` ("Vehicle is referenced by missions") -> 409 Conflict.
|
|
2. Load by id (404 on miss).
|
|
3. `db.Vehicles.DeleteAsync(v => v.Id == id)`.
|
|
|
|
### `SetDefault`
|
|
- Loads the row (404 on miss).
|
|
- If `request.IsDefault == true`, clears the flag on every other vehicle, then sets it on the target.
|
|
- If `request.IsDefault == false`, simply clears the flag on the target -- **no guarantee remains that exactly one vehicle is default**. Behavior is "the user told me to clear the flag, so I clear it".
|
|
|
|
## "Exactly one default" rule -- spec vs code
|
|
|
|
This is the canonical surviving spec-vs-code divergence in the rename Epic. See Jira **B12** (decision-only ticket).
|
|
|
|
- **Spec** (`../../suite/_docs/02_missions.md`): `PATCH /vehicles/{id}/default` "toggles the default flag on a vehicle." Only the target row changes.
|
|
- **Code**: when setting `IsDefault = true`, the service first clears the flag on **every other vehicle** in the same connection. Two operations, no transaction.
|
|
|
|
Two real consequences:
|
|
1. **Code is stricter than spec** -- in code there can be 0 or 1 default vehicle, never 2+. Spec would allow N defaults if the UI sets multiple.
|
|
2. **Race window**: two concurrent "set default = true" calls on different rows could both clear-then-set, leaving two defaults. linq2db default behavior is autocommit per statement; no `BeginTransactionAsync` is called.
|
|
|
|
B12 either lifts the rule into spec + wraps in a transaction (preferred), or drops the side-effect from code and lets UI handle exclusivity. The race fix is part of either resolution.
|
|
|
|
## Dependencies
|
|
|
|
- `Azaion.Missions.Database.AppDataConnection`
|
|
- `Azaion.Missions.Database.Entities.Vehicle`
|
|
- `Azaion.Missions.Database.Entities.Mission` (queried via `db.Missions` in `DeleteVehicle`)
|
|
- `Azaion.Missions.DTOs` (request/query DTOs)
|
|
|
|
## Consumers
|
|
|
|
- `Controllers.VehiclesController` -- wraps every method 1:1 with an HTTP route.
|
|
|
|
## Data Models
|
|
|
|
Reads/writes only `vehicles` table; reads (existence check) `missions` table.
|
|
|
|
## Configuration / External Integrations
|
|
|
|
None.
|
|
|
|
## Security
|
|
|
|
- All endpoints in the controller carry `[Authorize(Policy = "FL")]`, so this service is unreachable without a JWT bearing `permissions=FL`.
|
|
- Exception messages include the supplied id (`"Vehicle {id} not found"`) -- no PII risk for GUIDs.
|
|
|
|
## Tests
|
|
|
|
None present.
|
|
|
|
## Notes / Smells
|
|
|
|
1. **`GetVehicles` ignores pagination** by design -- the dataset is small (a fleet, not a catalog of millions). Inconsistent listing contract with `MissionService.GetMissions` but intentional.
|
|
2. **"Exactly one default" race** -- see B12 above.
|
|
3. **Delete fails fast on referenced vehicle** but does NOT cascade-delete or null-out the `vehicle_id` -- strictly a 409 to the caller. Consistent with the schema: `missions.vehicle_id` is `NOT NULL REFERENCES vehicles(id)` with no `ON DELETE` clause (PostgreSQL defaults to `NO ACTION`).
|
|
4. **Case-insensitive search** uses `string.ToLower()` on both sides, which LinqToDB renders as `LOWER(...)` -- non-indexed; full-table scan on large datasets. Fine at fleet size.
|