Skip to content

[v0.2] Add /api/v1/projects endpoint and FK-validate project_id in External Results sessions #315

Description

@bg-playground

Goal

Close two related v0.1 gaps that the External Results smoke workflow surfaced:

  1. There is no POST /api/v1/projects (or any projects API) on main — only the underlying Project model. The smoke workflow's bootstrap.py falls back to a synthetic UUID every run as a result.
  2. POST /api/v1/external-results/session accepts any project_id UUID without FK-validating against the projects table. The spec advertises a 400 session.project_not_found error code, but the path that should raise it does not exist yet (see docs/specs/external_results_v1.md § c.1, "Error codes" table).

Fixing #1 and #2 together turns the smoke workflow into a real end-to-end check: the smoke run will create a real project row, and the session-create call will validate against it.

Why this is v0.2, not v0.1

The External Results v0.1 contract chain (#291 and its subs) explicitly froze surface and merged a complete, deliverable v0.1. Adding a projects API and FK validation are surface widenings — appropriate for v0.2, not retrofits to v0.1.

Tracked here so the smoke workflow's TODO comments and the spec's aspirational-but-not-implemented 400 session.project_not_found row get a real home.

Discovered during

Scope

Backend

  • backend/app/api/projects.py — new router with POST /projects, GET /projects, GET /projects/{id}, PATCH /projects/{id}. Skip DELETE for v0.2 (separate concern: cascades).
  • backend/app/schemas/project.pyProjectCreate, ProjectResponse, ProjectUpdate. Match the existing Project model's columns.
  • backend/app/crud/project.py — standard CRUD helpers, async, mirroring crud/requirement.py.
  • backend/app/main.py — register the new router under API_V1_PREFIX.

Auth

External Results FK validation

  • app.crud.external_results.create_session — before insert, verify project_id resolves to an existing row. If not, raise ValueError with {"code": "session.project_not_found", "message": "Project <uuid> does not exist.", "details": null}.
  • The router maps that to a 400 Bad Request per the existing spec table.
  • Test: create a session with a random UUID → 400; with a real project → 201.

Smoke workflow updates (after this lands)

  • Remove the synthetic-UUID fallback in scripts/smoke/bootstrap.py.
  • Update the inline comment to point at this issue's resolution.
  • Bump the smoke fixture pin if any frameworks-side change is needed (likely none — the reporter doesn't care whether the project is real, only that the API returns a project_id).

Tests

  • Standard CRUD tests for projects (mirror test_requirements.py).
  • test_external_results_session_project_fk.py — explicit test for the new 400 code path.
  • Smoke-bootstrap regression test — assert bootstrap.py no longer takes the fallback path.

Spec doc

  • Already declares 400 session.project_not_found in section c.1 — update the surrounding prose to indicate it's now enforced (was aspirational).
  • Add a one-paragraph "Project lifecycle" section (or similar) under endpoints documentation.

Out of scope

  • Project membership / per-project authorization (separate v0.2 epic if we want it).
  • Soft-delete on projects.
  • Cross-project resource visibility — projects table only, no project_id columns added to other tables.
  • Changes to the External Results contract surface beyond the FK enforcement — external_results_v1 stays frozen.

Acceptance criteria

  • POST /api/v1/projects returns 201 with project response.
  • POST /api/v1/external-results/session returns 400 session.project_not_found for unknown project_id.
  • Existing session creation still works for valid project_id.
  • Audit-log entries on project create/update.
  • Smoke workflow's bootstrap.py creates a real project (no fallback path remaining).
  • Spec doc updated to reflect that the 400 code is enforced.
  • CI green (existing tests + new tests).

References

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions