Skip to content

Fix project API bugs carried over from the Java 8 code - #3

Merged
andreasarf merged 8 commits into
mainfrom
claude/fix-project-api-bugs
Oct 15, 2026
Merged

andreasarf merged 8 commits into
mainfrom
claude/fix-project-api-bugs

Conversation

@andreasarf

@andreasarf andreasarf commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

Summary

Fixes the bugs that came up while reviewing #1, all of them already present in the original Java 8 code, plus what six review rounds on this PR turned up.

Behaviour changes

Case Before After
POST body containing an id Silently overwrote that project, 201 id, createdDate and lastModifiedDate are read-only in JSON, so a new project is always created
PATCH re-sending the project's own externalId/sdlcSystem 409 against itself 200
PATCH {"sdlcSystem": null} / {"externalId": null} 500 NPE / silently ignored 400
PATCH blank externalId, or externalId/name over 255 chars (POST too) 500 / 409 400; PATCH validates the patched entity with the same constraints as POST
sdlcSystem given without an id 404 SdlcSystem 0 not found 400
Concurrent duplicate POST/PATCH (unique key) 500 409; only UNIQUE violations are mapped, anything else keeps default handling
Concurrent PATCHes of different fields Last full-row write won, so one change was lost Both changes kept (@DynamicUpdate, single transaction)
sdlcSystem in POST/PATCH responses Client's id-only stub The stored SDLC system
@CreatedDate/@LastModifiedDate Never populated (@EnableJpaAuditing was missing) Populated by auditing, truncated to seconds to match the datetime columns
/projects, /sdlcSystems (Spring Data REST) Exposed the repositories, bypassing every check above Removed (spring-boot-starter-data-rest dropped)
/h2-console Enabled in every deployment Only while devtools is active (documented in README)

Design

  • PATCH runs in one transaction. ProjectService.updateProject(id, update) loads the project, applies the change, validates it, checks for a duplicate and commits. Any rejection rolls back, including the checked NotFoundException.
  • Duplicate check. It runs only when externalId or the SDLC system changed. It uses a dedicated query that doesn't flush pending changes first.
  • No open-in-view. spring.jpa.open-in-view=false.
  • Error mapping. Exception handlers map NotFoundException to 404, ConflictException and unique violations to 409, and bean-validation failures to 400. All are logged at DEBUG.
  • Service API. SdlcSystemService.getSdlcSystem throws NotFoundException, like ProjectService.getProject.

Intentional:

  • A PATCH that changes nothing doesn't move lastModifiedDate.
  • When two PATCHes change the same field at the same time, the last write wins; there is no version column.

Test plan

  • ./mvnw clean test on JDK 25: 41 tests pass
  • Each bug fix has a test that fails on the old main code. The concurrency test was checked to fail without @DynamicUpdate, and the rejected-PATCH test to fail without rollbackFor.
  • Commit-time unique violation is recognised as a 409 case (real H2 transaction)
  • Packaged jar: /h2-console returns 404; a POST with "id": 1 creates a new project and leaves project 1 unchanged
  • CI green on GitHub

🤖 Generated with Claude Code

https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2

- POST ignores client-supplied id/createdDate/lastModifiedDate instead of
  silently overwriting an existing project
- PATCH no longer reports a conflict against the project being updated
  (e.g. re-sending its current externalId)
- PATCH with "sdlcSystem": null returns 400 instead of a 500 NPE
- A unique-constraint violation from a concurrent duplicate returns 409
  instead of 500
- POST/PATCH responses carry the stored SDLC system instead of the
  client's id-only stub
- Enable JPA auditing so @CreatedDate/@LastModifiedDate are populated
- Stop enabling the H2 console in every deployment; spring-boot-h2console
  still turns it on automatically when devtools is active
- Map NotFoundException to 404 in one handler and log it at INFO

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
- Reject externalId/name longer than the 255-char columns and blank
  externalId on PATCH with 400 instead of 409/500
- Map only unique-constraint violations to 409; other integrity errors
  keep the default handling
- Remove spring-boot-starter-data-rest, which exported the repositories at
  /projects and /sdlcSystems and bypassed every controller check
- Rely on JPA auditing for timestamps: drop the manual lastModifiedDate
  update and field initializers; updateProject now returns Project, which
  removes an unreachable 400 branch
- Log routine 404/409s at DEBUG
- Make the unchanged-externalId test independent of test order
- Document that the H2 console is a devtools-only feature

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
… audit dates

- PATCH externalId uses the same blank rule as @notblank on POST, and an
  explicit null is rejected with 400 instead of being silently ignored
- Audit timestamps are truncated to seconds to match the datetime columns,
  so responses show what is stored
- Unique-violation detection walks the whole cause chain
- SdlcSystemService.getSdlcSystem throws NotFoundException, like
  ProjectService.getProject, removing the controller-side lookup helper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
- PATCH applies the changes to the loaded project and validates it with the
  entity's own constraints (@notblank, @SiZe, @NotNull), replacing the
  hand-copied @AssertTrue/@SiZe rules that had drifted from POST
- Turn off open-in-view so the project PATCH loads is detached and a
  rejected change can never be flushed later in the request
- Comment why the duplicate pre-check stays alongside the unique-key handler
- Tests: POST and PATCH agree on unusual externalIds; a rejected PATCH leaves
  the project unchanged

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
- ProjectService.updateProject loads, applies, validates and conflict-checks
  the project in one transaction, rolling back on any rejection (including
  the checked NotFoundException); correctness no longer depends on the
  open-in-view setting, and the extra SELECT from merging a detached copy
  is gone
- @DynamicUpdate writes only changed columns, so concurrent PATCHes of
  different fields no longer overwrite each other
- The conflict query doesn't flush pending changes first, so the explicit
  409 check runs against the stored state
- An sdlcSystem reference without an id is a 400, not "SdlcSystem 0 not found"
- Drop the no-op @Valid on the PATCH body; clearer handler naming
- Tests: concurrent field updates are both kept, rejected PATCH rolls back
  (each verified to fail without its fix), service unit tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
…me 409

- Only query for a duplicate when the PATCH changes externalId or the SDLC
  system
- Test that a unique violation raised at commit is recognised as one (the
  path concurrent PATCHes take)
- Concurrency test removes the project it creates from the shared database

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
…ve ids

- The FlushMode COMMIT hint moves to a dedicated
  existsByExternalIdAndSdlcSystemIdAndIdNot query used only by
  updateProject, so other callers of the shared finder keep auto-flush
- Only an absent sdlcSystem id (0) is a 400; a negative id is looked up and
  reported as 404 like any other unknown system

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012MLyPXGHeCc1fvTeB8JnT2
@andreasarf
andreasarf merged commit ffb41c3 into main Oct 15, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants