Replace PATCH /subject endpoint with PUT#819
Merged
Merged
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- CreateSubjectApi: catch InvalidArgumentException → 400. SubjectLabel hardening in commit d0c4f7b would otherwise let an empty-label POST escape as 500. Add testEmptyLabelReturns400 regression test. - TS spec: add explicit assertion that updateSubject calls httpClient.put (not patch). InMemoryHttpClient routes by URL only, so the prior tests passed regardless of method. - SubjectFormat.md: restore POST in the doc intro's write-methods list (PUT alone misrepresents the API; the Create endpoint is still POST). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Split testOmittedStatementIsDeleted into two tests, one per behavior (omitted-key deletion and empty-map deletion). Per AGENTS.md "test one thing per test function". - Add testNonExistentSubjectReturns404. Spec required this case but it was missed in T4. Action-level coverage existed; this exercises the handler's RuntimeException → 404 dispatch end-to-end. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The select-property write description still said 'create/patch statement'. Replace endpoint is now PUT, so 'create/replace' matches the surface. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ReplaceSubjectApi was using str_contains($e->getMessage(), 'not found') to dispatch RuntimeException to either 404 or 403. A future refactor that touched the message would have silently broken the 404 behavior. Introduce SubjectNotFoundException and SubjectEditNotAuthorizedException in src/Application/. ReplaceSubjectAction throws the specific types; ReplaceSubjectApi catches each and maps to its status. The bare RuntimeException catch is removed — anything unexpected from deeper layers now surfaces as 500, which is more accurate than silent 403. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ct exceptions - Rename SelectPatchResolver → SelectStatementResolver. The "Patch" name was tied to the old PATCH endpoint; the resolver itself just walks an incoming statements array and resolves select-typed values to canonical option IDs, regardless of which write endpoint called it. - Move Subject exceptions to src/Application/Subject/Exception/, mirroring the src/Application/Query/Exception/ layout introduced by the public query API. Keeps domain-grouped exceptions together as both areas grow. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After PATCH→PUT, the patcher's merge-into-existing-list code path was unreachable: every caller (CreateSubject, ReplaceSubject) builds a StatementList from an empty starting point. That left two classes for one job — Builder wrapping Patcher with an empty seed. Inline the deserialization logic into StatementListBuilder and delete StatementListPatcher. Subject::patchStatements (last user of the patcher via Subject) is dead and removed. CreateSubjectAction now takes a StatementListBuilder directly. No behavior change. Pre-existing baselined phpstan errors moved with the code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Went though the manual browser check steps and did a few curls to validate.
Fixes #280
Summary
Replaces
PATCH /neowiki/v0/subject/{subjectId}withPUT /neowiki/v0/subject/{subjectId}and makes the contract honest. The PATCH endpoint claimed RFC 7396 merge-patch semantics but did not deliver them:statementswas required (omission was not "leave unchanged"), andnull-deletion of statements was a long-standing TODO. The new PUT endpoint replaces the writable view of a Subject in one shot — omit a statement key to delete it,labelis required and non-empty.Wire-level changes
PATCH /neowiki/v0/subject/{subjectId}→PUT /neowiki/v0/subject/{subjectId}{label, statements, comment}shape, butlabelis now required (non-empty aftertrim) andstatementskeys not in the map are deleted from the Subject. Pass{}to clear all statements. Immutable fields (id,schema,pageId,pageTitle) are not part of the body and are ignored if sent.200with{status: \"updated\", subjectId: ...}on success.400for missing/emptylabelor missingstatements.403for unauthorized.404for non-existent Subject.Pre-1.0 — third-party consumers were warned at
docs/RestApi.mdthat/neowiki/v0/*is not stable. No back-compat shims.Code structure
The handler/action pair is named after the use case rather than the HTTP method, matching existing
Application/Actions/conventions (CreateSubject,DeleteSubject,SetMainSubject):src/EntryPoints/REST/ReplaceSubjectApi.php— REST adapter. CatchesInvalidArgumentException→ 400,SubjectNotFoundException→ 404,SubjectEditNotAuthorizedException→ 403. Anything else propagates as 500.src/Application/Actions/ReplaceSubject/ReplaceSubjectAction.php— application use case: authorize → load (404 if absent) → setLabel → setStatements viaStatementListBuilder(withSelectStatementResolver) → save.src/Application/StatementListBuilder.php— deserializes a request statements array into aStatementList. Absorbed theStatementListPatcher(its merge code path was unreachable after this refactor).src/Application/Subject/Exception/SubjectNotFoundException.php,src/Application/Subject/Exception/SubjectEditNotAuthorizedException.php— typed exceptions, mirroring theApplication/Query/Exception/layout from Add public Cypher query REST API + close #549 #817.src/Application/SelectStatementResolver.php— renamed fromSelectPatchResolver(the "Patch" name was tied to the old endpoint).src/Domain/Subject/Subject::setStatements()— symmetric counterpart tosetLabel().Subject::patchStatements()removed (dead).src/Domain/Subject/SubjectLabel— constructor now rejects empty/whitespace-only strings.The
SubjectLabelhardening also affectsCreateSubjectApi: that handler only caughtRuntimeException, so an empty-label POST would have escaped as an unhandled exception. Added anInvalidArgumentExceptioncatch returning 400 plus a regression test.The TS frontend (
RestSubjectRepository.updateSubject) now callshttpClient.put. TheInMemoryHttpClienttest mock routes by URL only; added an explicit spy assertion to verify the method change.Test plan
make phpunit filter=ReplaceSubjectApiTest— 10 tests including 200 happy path, omit-to-delete, empty-map clear-all, missing/empty label → 400, missing statements → 400, non-existent subject → 404, 403 unauthorizedmake phpunit filter=ReplaceSubjectActionTest— 8 tests covering the action's full matrix (label replace, statements replace, omit deletion, comment forwarding, typed-exception throws, select resolution)make phpunit filter=StatementListBuilderTest— 4 testsmake phpunit filter=SelectStatementResolverTest— preserved from renamemake phpunit filter=SubjectLabelTest— 4 tests for the new validationmake phpunit filter=CreateSubjectApiTest— 5 tests including the newtestEmptyLabelReturns400regressionmake phpunit filter=ModuleSpecHandlerNeoWikiTest— OpenAPI spec drift, confirms PUT is registered and PATCH is gonemake phpunit filter=Applicationandfilter=EntryPoints— no regressionsmake cs(phpcs + phpstan) cleanmake ts-test— 782/782 pass; new spy test assertshttpClient.putis calledmake ts-lintcleanNote: full unfiltered
make phpunitwas not run; it hangs in the local dev env on something pre-existing (a Persistence/Neo4j integration test that doesn't terminate). All filtered scopes touching this change pass.Manual API Check
The PUT endpoint can be exercised via curl. Replace
localhost:8484with your dev wiki URL.Manual Browser Check
The SubjectsManager UI is the only consumer of the endpoint today. Run
make ts-buildfirst so the live wiki uses the updated bundle.PUT(not PATCH)./rest.php/neowiki/v0/subject/s1demo7aaaaaaa4.{\"status\":\"updated\",\"subjectId\":\"s1demo7aaaaaaa4\"}.Addresskey (it's deleted, not sent as null/empty).Known limitations / follow-ups
These are intentionally out of scope here:
type(read) vspropertyType(write) asymmetry on Statement objects.