Skip to content

Commit 591fe02

Browse files
dmealingclaude
andcommitted
fix: PUT on a writable entity 404s on two TS mounts and is undocumented in three ports
The cross-port REST contract (FR-008) makes the update verb reachable via BOTH PATCH and PUT, routed to one handler. Every port's generated controller serves both, and the Drizzle Fastify mount has registered both by default for some time. Two `@metaobjectsdev/runtime-ts` mounts had not caught up: - The Hono mount registered PATCH alone unless `updateMethod` said otherwise, so a PUT the contract promises fell through to Hono's plain-text 404. - The ObjectManager Fastify mount did the same. Neither api-contract lane runs those mounts, which is how it survived; it is the blind spot the Hono error-code defect sat in. Both now mount both verbs by default, with `updateMethod` kept as the explicit single-verb restriction. Its doc comment in all three flavors claimed a "patch" default that the Drizzle mount had stopped honouring, and the agent-context reference taught the same thing; both now say what the code does. API docs moved with it. TypeScript (Fastify and Hono), Java and Kotlin documented PATCH only for a writable entity while C# and Python documented both, so an adopter or an agent reading `meta docs --agent` was told an address the server answers did not exist. Each builder now emits a PUT row beside PATCH. The Java and Kotlin accuracy tests, which check every documented verb against the emitted Spring mapping, count it; the TS accuracy gate already checked the verb against the full-CRUD mount. Found while working the handoff's note that Java api-docs lacked PUT. That note named one port; the gap was three ports' docs and two runtime mounts. Tests: new Hono and ObjectManager-Fastify PUT tests went red first (Hono answered its plain-text 404) and pass now. codegen-ts 1702, runtime-ts 473, sdk 324, codegen-spring 307, codegen-kotlin 394, all green. `scripts/ci-local.sh --quick` 36/36. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 0f9ba74 commit 591fe02

17 files changed

Lines changed: 225 additions & 24 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,29 @@ edit (two registered `description` strings) and was ruled a hold, as 1.0.4's was
223223

224224
### Fixed
225225

226+
- **TypeScript: two `@metaobjectsdev/runtime-ts` writable mounts answered `PUT` with a 404.**
227+
The cross-port REST contract (FR-008) makes the update verb reachable via BOTH `PATCH`
228+
and `PUT`, routed to one handler, and every other port's controller maps both. The
229+
Drizzle Fastify mount already registered both by default; the **Hono** mount and the
230+
ObjectManager **Fastify** mount registered `PATCH` alone unless `updateMethod` said
231+
otherwise, so a `PUT` the contract promises fell through to the framework's own 404.
232+
Neither api-contract lane runs those mounts, which is how it survived — the same blind
233+
spot the Hono error-code defect sat in. Both now mount both verbs by default. **Additive,
234+
nothing to do on upgrade:** `updateMethod: "patch"` or `"put"` still restricts the update
235+
verb to exactly the one it names, and now says so in its doc comment rather than claiming
236+
a `"patch"` default that the Drizzle mount had not honoured for some time. One case is
237+
not silent: a consumer that registered its OWN `PUT` handler at `<path>/:id` beside the
238+
ObjectManager Fastify mount now gets Fastify's duplicate-route error at startup — pass
239+
`updateMethod: "patch"` to keep that handler.
240+
241+
- **API docs: TypeScript, Java and Kotlin did not document `PUT` on a writable entity.** The
242+
generated update route serves `PATCH` and `PUT` from one handler in all five ports; C#
243+
and Python documented both, the other three documented `PATCH` only, so an adopter (or
244+
an agent reading `meta docs --agent`) was told an address the server answers did not
245+
exist. Each builder now emits a `PUT` row beside `PATCH`, and the accuracy tests that
246+
check every documented verb against the emitted routes count it. **Generated docs gain
247+
one REST row per writable entity**; nothing else in the output moves.
248+
226249
- **Java: a database constraint violation answered a bare 500 instead of the cross-port
227250
`409 {"error":"constraint_violation","constraint":"unique"|"foreign_key"}`.** The generated
228251
CRUD routes had no try/catch around their writes at all, so a driver failure reached Spring's

‎agent-context/skills/metaobjects-runtime-ui/references/typescript.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,9 @@ mountGetRoute({ ...opts }); // a single verb
205205

206206
`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }`
207207
plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g.
208-
`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default
209-
/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the
208+
`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` or
209+
`"put"` restricts update to that one verb; absent mounts both, as the cross-port
210+
contract requires). So **mount the standard verbs with these helpers and hand-write only the
210211
custom routes** (HTML pages, nested resources, computed fields) — calling the
211212
generated query helpers, and a projection's generated query for derived/aggregate
212213
data. Generate the data layer; hand-write only what's genuinely custom.

‎fixtures/agent-context-conformance/ts-react-tanstack/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,9 @@ mountGetRoute({ ...opts }); // a single verb
205205

206206
`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }`
207207
plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g.
208-
`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default
209-
/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the
208+
`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` or
209+
`"put"` restricts update to that one verb; absent mounts both, as the cross-port
210+
contract requires). So **mount the standard verbs with these helpers and hand-write only the
210211
custom routes** (HTML pages, nested resources, computed fields) — calling the
211212
generated query helpers, and a projection's generated query for derived/aggregate
212213
data. Generate the data layer; hand-write only what's genuinely custom.

‎fixtures/agent-context-conformance/ts-requirements/expected/.claude/skills/metaobjects-runtime-ui/references/typescript.md‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,8 +205,9 @@ mountGetRoute({ ...opts }); // a single verb
205205

206206
`CrudRoutesOptions` = `{ fastify, path, db, table, insertSchema, updateSchema }`
207207
plus `expose?` (limit verbs), `routeOptions?` (Fastify hooks — e.g.
208-
`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` default
209-
/ `"put"`). So **mount the standard verbs with these helpers and hand-write only the
208+
`{ preHandler: requireAuthHook }` for auth), and `updateMethod?` (`"patch"` or
209+
`"put"` restricts update to that one verb; absent mounts both, as the cross-port
210+
contract requires). So **mount the standard verbs with these helpers and hand-write only the
210211
custom routes** (HTML pages, nested resources, computed fields) — calling the
211212
generated query helpers, and a projection's generated query for derived/aggregate
212213
data. Generate the data layer; hand-write only what's genuinely custom.

‎server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/apidocs/KotlinApiModelBuilder.kt‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,8 @@ class KotlinApiModelBuilder {
313313
rest("GET $base/{id}", "fetch one by id")
314314
rest("POST $base", "create")
315315
rest("PATCH $base/{id}", "update")
316+
// One handler serves both verbs (`method = [PATCH, PUT]` in the emitted controller).
317+
rest("PUT $base/{id}", "update (PUT alias)")
316318
rest("DELETE $base/{id}", "delete")
317319
// FR-018 M:N traversal — GET /<source-plural>/{id}/<relation>.
318320
for (nav in KotlinM2mSupport.resolve(obj, loader)) {

‎server/java/codegen-kotlin/src/test/kotlin/com/metaobjects/codegen/kotlin/apidocs/KotlinApiDocsAccuracyKtTest.kt‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -244,15 +244,16 @@ class KotlinApiDocsAccuracyKtTest {
244244
restChecked++
245245
}
246246
// 5 CRUD verbs + 1 M:N traversal (tags).
247-
assertEquals(6, restChecked, "expected 6 REST symbols on Author (5 CRUD + 1 M:N)")
247+
// Update is documented under BOTH verbs it answers, PATCH and PUT.
248+
assertEquals(7, restChecked, "expected 7 REST symbols on Author (5 CRUD, update as PATCH + PUT, + 1 M:N)")
248249
}
249250

250251
/** The exact Spring mapping the Kotlin controller emits for a verb + sub-path. */
251252
private fun mappingFor(verb: String, sub: String): String = when (verb) {
252253
"GET" -> if (sub.isEmpty()) "@GetMapping\n" else "@GetMapping(\"$sub\")"
253254
"POST" -> "@PostMapping\n"
254255
"DELETE" -> "@DeleteMapping(\"$sub\")"
255-
"PATCH" -> "@RequestMapping(value = [\"$sub\"], method = [RequestMethod.PATCH, RequestMethod.PUT])"
256+
"PATCH", "PUT" -> "@RequestMapping(value = [\"$sub\"], method = [RequestMethod.PATCH, RequestMethod.PUT])"
256257
else -> throw AssertionError("unexpected REST verb: $verb")
257258
}
258259

‎server/java/codegen-spring/src/main/java/com/metaobjects/generator/apidocs/JavaApiModelBuilder.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,8 @@ private void addRestSymbols(List<ApiSymbol> symbols, MetaObject obj,
227227
addRest(symbols, controllerFqn, "GET " + base + "/{id}", "fetch one by id");
228228
addRest(symbols, controllerFqn, "POST " + base, "create");
229229
addRest(symbols, controllerFqn, "PATCH " + base + "/{id}", "update");
230+
// One handler serves both verbs (`method = { PATCH, PUT }` in the emitted controller).
231+
addRest(symbols, controllerFqn, "PUT " + base + "/{id}", "update (PUT alias)");
230232
addRest(symbols, controllerFqn, "DELETE " + base + "/{id}", "delete");
231233
for (SpringM2mSupport.M2mNav nav : SpringM2mSupport.resolve(obj, loader)) {
232234
addRest(symbols, controllerFqn,

‎server/java/codegen-spring/src/test/java/com/metaobjects/generator/apidocs/JavaApiDocsAccuracyTest.java‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,10 @@ public void everyRestSymbolMapsToARealControllerMapping() {
204204
controller.contains(expectedMapping));
205205
restChecked++;
206206
}
207-
// The fixture's Author yields the 5 CRUD verbs + 1 M:N traversal.
208-
assertEquals("expected 6 REST symbols on Author (5 CRUD + 1 M:N)", 6, restChecked);
207+
// The fixture's Author yields the 5 CRUD verbs — update documented under BOTH verbs it
208+
// answers, PATCH and PUT — + 1 M:N traversal.
209+
assertEquals("expected 7 REST symbols on Author (5 CRUD, update as PATCH + PUT, + 1 M:N)",
210+
7, restChecked);
209211
}
210212

211213
/** The exact Spring mapping annotation the controller emits for a verb + sub-path. */
@@ -219,6 +221,7 @@ private static String mappingFor(String verb, String sub) {
219221
case "DELETE":
220222
return "@DeleteMapping(\"" + sub + "\")";
221223
case "PATCH":
224+
case "PUT":
222225
// PATCH + PUT share one composed @RequestMapping with method={PATCH, PUT}.
223226
return "@RequestMapping(value = \"" + sub
224227
+ "\", method = { RequestMethod.PATCH, RequestMethod.PUT })";

‎server/java/codegen-spring/src/test/java/com/metaobjects/generator/apidocs/JavaApiModelBuilderTest.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ public void enumeratesTheJavaSdkSurface() throws Exception {
9191
assertTrue("REST POST", hasName(author, ApiSymbolKind.REST, "POST /api/authors"));
9292
assertTrue("REST GET by id", hasName(author, ApiSymbolKind.REST, "GET /api/authors/{id}"));
9393
assertTrue("REST PATCH", hasName(author, ApiSymbolKind.REST, "PATCH /api/authors/{id}"));
94+
assertTrue("REST PUT", hasName(author, ApiSymbolKind.REST, "PUT /api/authors/{id}"));
9495
assertTrue("REST DELETE", hasName(author, ApiSymbolKind.REST, "DELETE /api/authors/{id}"));
9596
assertTrue("REST M:N traversal", hasName(author, ApiSymbolKind.REST, "GET /api/authors/{id}/tags"));
9697
assertTrue("FILTER AuthorFilterAllowlist", has(author, ApiSymbolKind.FILTER, "AuthorFilterAllowlist"));

‎server/typescript/packages/codegen-ts/src/generators/api-model.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ function restSymbols(
635635
symbols.push(
636636
ep("POST", path, `Create a ${name} (body validated by ${name}InsertSchema).`, createShape),
637637
ep("PATCH", `${path}/:id`, `Partially update a ${name} by id (body validated by ${name}UpdateSchema).`, updateShape),
638+
ep("PUT", `${path}/:id`, `PUT alias of PATCH — the same handler and partial body (validated by ${name}UpdateSchema).`, updateShape),
638639
ep("DELETE", `${path}/:id`, `Delete a ${name} by id.`),
639640
);
640641
}
@@ -840,6 +841,7 @@ function restHonoSymbols(
840841
symbols.push(
841842
ep("POST", path, `[Hono] Create a ${name} (body validated by ${name}InsertSchema).`, createShape),
842843
ep("PATCH", `${path}/:id`, `[Hono] Partially update a ${name} by id (body validated by ${name}UpdateSchema).`, updateShape),
844+
ep("PUT", `${path}/:id`, `[Hono] PUT alias of PATCH — the same handler and partial body (validated by ${name}UpdateSchema).`, updateShape),
843845
ep("DELETE", `${path}/:id`, `[Hono] Delete a ${name} by id.`),
844846
);
845847
}

0 commit comments

Comments
 (0)