feat: update board API to query board notes internally #209
No reviewers
Labels
No labels
domain:backend
domain:devops
domain:frontend
status:approved
status:in-progress
status:needs-fix
status:qa
type:bug
type:devops
type:feature
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
forgejo_admin/pal-e-api!209
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "197-board-api-use-notes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Updates all 14 board API endpoints to query board notes (note_type='board') internally, while preserving the boards table during transition. Board CRUD now creates/updates/deletes a corresponding Note alongside the Board row. Response contract is additive: BoardOut gains
note_id, BoardItemOut gainsboard_note_id.Changes
src/pal_e_docs/routes/boards.py— Added_resolve_board()helper that checks board notes first, falls back to boards table. Added_get_board_note_id()for note_id lookup with NULL-safe logging.create_boardnow creates a board note alongside the Board row.update_boardsyncs note title.delete_boardremoves both board and note. All item-creating endpoints (create_board_item, sync_board, sync_issues) populateboard_note_id.update_board_itembackfillsboard_note_idif missing.src/pal_e_docs/schemas.py— Addednote_id: int | None = NonetoBoardOut. Addedboard_note_id: int | None = NonetoBoardItemOut.tests/test_boards.py— Added 9 integration tests verifying: note_id in BoardOut responses, board_note_id in BoardItemOut responses, board note creation with correct note_type, title sync on update, cascading delete, and field compatibility.Test Plan
Prerequisites
scripts/migrate_boards_to_notes.pybefore deploying to create board notes for existing boards and backfillboard_note_idon existing board itemsRelated
QA Review
Findings (fixed in
413b71c)Redundant query in
_resolve_board-- The original implementation queriedNotefirst to check for a board note, then queriedBoardby slug, and if neither matched, queriedBoardby slug again. Since board slugs and board note slugs are always identical, the note lookup was redundant. Simplified to a singleBoard.slugquery with fallback to 404.Missing note slug collision guard in
create_board-- If a non-board note already existed with slugboard-{project_slug}, the endpoint would hit a DB unique constraint error (500) instead of returning a clean 409. Added an explicit check that returns a descriptive 409 error.Verified
/boards/{slug}routes use_resolve_boardinternallyboard_note_idhandled gracefully with logging and fallbackBoardOut.note_idandBoardItemOut.board_note_idadded, all existing fields preservedVERDICT: APPROVE
PR #209 Review
DOMAIN REVIEW
Tech stack: Python / FastAPI / SQLAlchemy / Pydantic (pal-e-docs API).
This PR updates all 14 board API endpoints to resolve boards via board notes (
note_type="board") while preserving theboardstable during the transition period. The response contract is additive:BoardOutgainsnote_id,BoardItemOutgainsboard_note_id. Board CRUD now creates/updates/deletes a correspondingNotealongside theBoardrow. Nine new integration tests cover the key paths.Endpoint audit -- all 14 endpoints now use
_resolve_board():GET /boards/backlog/items-- no slug resolution needed (cross-board query, correct)GET /boards/activity-- no slug resolution needed (cross-board query, correct)GET /boards-- uses_board_to_out(b, db)which calls_get_board_note_idPOST /boards-- creates board note alongside Board rowGET /boards/{slug}-- uses_resolve_boardPATCH /boards/{slug}-- uses_resolve_board, syncs note titleDELETE /boards/{slug}-- uses_resolve_board, cascading note deletePOST /boards/{slug}/sync-- uses_resolve_board, populatesboard_note_idon new itemsPOST /boards/{slug}/sync-issues-- uses_resolve_board, populates + backfillsboard_note_idPATCH /boards/{slug}/items/bulk-- uses_resolve_boardGET /boards/{slug}/items-- uses_resolve_boardPOST /boards/{slug}/items-- uses_resolve_board, populatesboard_note_idPATCH /boards/{slug}/items/{item_id}-- uses_resolve_board, backfillsboard_note_idDELETE /boards/{slug}/items/{item_id}-- uses_resolve_boardAll raw
db.query(Board).filter(Board.slug == slug)calls have been consolidated into_resolve_board(). Only one instance remains, inside_resolve_boarditself. This is clean DRY refactoring.Schema changes are additive and backward-compatible:
BoardOut.note_id: int | None = None-- defaults to None, safe for existing consumersBoardItemOut.board_note_id: int | None = None-- defaults to None, safe for existing consumersModel confirms
BoardItem.board_note_idisMapped[int | None]with FK tonotes.id, nullable, indexed. This was added in PR #207 (prerequisite, already merged).Migration script (
scripts/migrate_boards_to_notes.py) is present, idempotent, handles slug collisions, and includes clear run instructions. Correctly referenced in the PR body as a prerequisite step.NULL handling:
_get_board_note_idgracefully returnsNonewith a warning log for pre-migration boards. All callers handle the None case. Backfill logic inupdate_board_item(line 690) andsync_issues(line 482) correctly checksis Nonebefore setting.BLOCKERS
None.
NITS
Misleading
_resolve_boarddocstring (boards.py:34-36): The docstring says "Checks both the board note (note_type='board') and the boards table" but the implementation only queries theBoardtable. The note lookup described never happens. The docstring should be updated to match what the code actually does -- it resolves via the boards table and raises 404 if not found.sync_boardmissingboard_note_idbackfill on existing items (boards.py:370-375): Whensync_boardfinds an existing phase item and updates its column, it does NOT backfillboard_note_idlikesync_issues(line 482-484) andupdate_board_item(line 690-693) do. This means pre-migration phase items updated only viasync_boardwill never get theirboard_note_idpopulated through normal API usage. The migration script handles the initial backfill, but ongoing sync calls miss this path. Consider adding the same backfill pattern for consistency.N+1 query in
list_boards(boards.py:198):_board_to_outcalls_get_board_note_idwhich issues a separatedb.query(Note)for each board. With a small number of boards this is fine, but as the platform grows this becomes O(N) extra queries. A future optimization could use a subquery or join to fetch all board note IDs in one query. Non-blocking -- the board count is small today.create_boardslug collision edge case (boards.py:218-223): The guard at line 219 only blocks non-board note types. If a board note with the same slug already exists (e.g., migration was run but the Board row was somehow deleted and re-created), the code passes the guard and attempts to create a DUPLICATE Note with the same slug, which would throw anIntegrityErrorfrom the unique constraint. In practice this is unlikely because the duplicate-Board-per-project guard at line 208 fires first. But for defense-in-depth, the guard could also skip note creation whenexisting_note.note_type == "board"and reuse the existing note instead.No test for slug collision guard: The
409path for note slug collision increate_board(line 219-223) has no test coverage. Consider adding a test that pre-creates a non-board note with the expected slug and verifies the 409 response.SOP COMPLIANCE
197-board-api-use-notesreferences issue #197Forgejo issue: #197and prerequisitePR #207PROCESS OBSERVATIONS
sync_boardbackfill inconsistency (nit #2) could be tracked as a follow-up item on issue #208 (which is already open for sync_board refactoring).VERDICT: APPROVED