Skip to content

Correctness fixes from full-repo review: query semantics, element projection, deletion, status codes - #28

Open
blake-regalia wants to merge 11 commits into
developfrom
fix/review-findings
Open

Correctness fixes from full-repo review: query semantics, element projection, deletion, status codes#28
blake-regalia wants to merge 11 commits into
developfrom
fix/review-findings

Conversation

@blake-regalia

Copy link
Copy Markdown
Contributor

Summary

Eleven incremental fixes from a full review of the implementation, each committed with its own regression tests. The suite grows from 29 to 55 tests, all green against the layer1 + Fuseki test stack.

Issue fixes

  • Fixes Error for Query - PrimitiveConstraint value cannot be array #13PrimitiveConstraint semantics completed per the OMG schema: = (and the schema's in, now accepted) is set membership over the whole value array instead of silently using only value[0]; "value": null and null array entries match elements lacking the property (the old null check compared a List against JsonNull — dead code); value is nullable as the schema requires; or composites over different properties no longer exclude elements missing either property (constraint patterns are OPTIONAL with bound() guards); property names are sanitized before becoming SPARQL variables; queries missing select/where return 400 instead of an NPE 500. Adds the first execution coverage of POST /query-results and saved-query results.
  • Fixes /elements response for nested behavioral flows appears to omit featureChain information #19 — multi-valued sysml: properties without a json: array annotation (e.g. models loaded as raw RDF rather than through this API's commit path) were silently dropped from every /elements response — exactly the reported disappearance of featureChain. They now render as a best-effort array; when the annotation exists it remains authoritative and preserves order. Also removes a second silent-loss path: a commit change whose identity.@id disagrees with its payload.@id was discarded while the commit still returned 200 — now a 400.
  • References GET /projects/{projectId}/branches returns branch names mismatched with @id values #20 (not closed here) — the name/id shuffling reproduces upstream: layer1's list-branches wildcard CONSTRUCT is mis-evaluated by GraphDB when joined with the access-control BGP, and this service faithfully translates the corrupted graph. This PR hardens the local side — branch/tag/project builders no longer fabricate created/name defaults that masked the upstream corruption, and a new test pins the name@id pairing in the list response — but the issue should stay open until a layer1 release with the explicit per-branch property binding lands and the compose files pin it.

Additional correctness fixes found during review

  • Element deletion left dangling references: the collected incoming-reference set was never used in the SPARQL update. Deletion now removes incoming link triples and the referencing property's array annotation.
  • forward() never actually propagated upstream response headers (the built header map was discarded inside the respondText lambda) — ETag, Location, and WWW-Authenticate from layer1 now reach clients; forwarded 401s carry their challenge.
  • Status codes: GET of a nonexistent element returned 200 with a fabricated {"@type": null} body (now 404); commits/queries against a nonexistent project crashed on next()!! (now forwarded 404); malformed payloads threw a bare java.lang.Error (now 400); blank-node subjects NPE-crashed whole list responses (now skipped).
  • Soft-deleted projects/branches/tags remained retrievable by id (now 404); POST /projects with an existing @id silently overwrote the project via the underlying PUT (now 409).
  • RDF escaping missed carriage returns (a \r in any string failed the whole commit with an opaque upstream parse error); element @ids (subject and references) are validated before interpolation into SPARQL IRI positions.
  • CORS was anyHost() with allowCredentials = true: any website could make credentialed cross-origin requests replaying a logged-in user's Authorization header. Origins are now configurable via FLEXO_CORS_ALLOWED_ORIGINS; without configuration the API still answers any origin but credentialed cross-origin requests are disabled.

Housekeeping

  • Minor/patch dependency bumps (Ktor 3.5.1, Jena 6.1.0, Kotest 6.2.2, logback 1.5.38, coroutines-test 1.11.0, java-jwt 4.6.0, JaCoCo 0.8.15); major migrations (Gradle 9, Kotlin 2.4, Sonar plugin 7) deliberately deferred.
  • README cp command fix.

Test plan

  • docker compose -f src/test/resources/docker-compose.yml up -d
  • ./gradlew test — 55 tests, 0 failures (new suites: QueryResultsTest, ElementArrayTest, DeleteElementTest, ForwardHeadersTest, NegativePathTest, SoftDeleteTest, EscapingTest, BranchListPairingTest)

🤖 Generated with Claude Code

blake-regalia and others added 11 commits July 17, 2026 01:28
The schema defines 'value' as an array (or null), and commit 9e93353 made
deserialization accept that shape — but execution still only honored the
first array element, the null-value branch compared a List against
JsonNull (dead code), a JSON null value was rejected outright, and every
constraint added a required triple pattern so 'or' composites over
different properties silently excluded elements missing either property.

- '=' (and the schema's 'in', now accepted) is set membership over all
  values in the array
- 'value': null and null array entries match elements lacking the
  property, via OPTIONAL + !bound()
- every constraint pattern is OPTIONAL with a bound() guard, so
  or-composites over different properties work
- property names are sanitized before becoming SPARQL variable names
- POST/PUT queries missing select/where return 400 instead of a 500 NPE

Adds QueryResultsTest — the first execution coverage of
POST /query-results, GET /queries/{id}/results, and the constraint
translation (equality, membership, @type, null, and/or composites).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two silent data-loss paths around array-valued properties:

- extractModelElementToJson skipped any multi-valued sysml: property that
  had no json: annotation triple, so models ingested as raw RDF (rather
  than through this API's commit path, which writes the annotation) lost
  properties like featureChain from every /elements response with no
  error. Emit a best-effort array instead; when an annotation exists it
  remains authoritative, and the skip no longer depends on predicate
  iteration order.
- a commit change whose identity @id disagreed with its payload @id was
  silently discarded while the commit still returned 200; now a 400.

Adds ElementArrayTest: array round-trip through the API (order
preserved via annotation), the annotation-less raw-RDF load path, and
the identity/payload mismatch rejection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The commit handler collected deleted-element nodes into deleteIncoming
but never used it: only the deleted element's own (outgoing) triples
were removed, so every element that referenced it kept a dangling
{"@id": <deleted>} forever.

The update now also deletes incoming link triples and the referencing
property's json: array annotation (which cannot be rewritten in SPARQL;
the surviving link triples render as a best-effort array via the
annotation-less fallback path).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The header-forwarding block inside the respondText configure lambda read
from the outgoing (empty) content headers and discarded the headersOf()
value it built — dead code, so ETag, Location, WWW-Authenticate, etc.
from layer1 never reached clients (401s arrived without a challenge).

Append upstream headers to the response before responding, excluding
content headers (managed by respondText) and this server's Date/Server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- GET of a nonexistent element returned 200 with a fabricated
  {"@type": null} body; now 404
- POST /commits and query-results against a nonexistent project crashed
  on next()!! over an empty model (500); layer1 failures are now
  forwarded (404) and an empty repo result falls back safely
- extra keys next to an @id reference threw a bare java.lang.Error
  (500); now InvalidSysmlSerializationError (400)
- blank-node subjects in model graphs NPE-crashed every list endpoint
  (elements/roots/relationships/query results); they are now skipped

Adds NegativePathTest covering the 404/400 surfaces.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…is 409

Deletion only set a urn:sysmlv2:deleted flag that list endpoints
filtered — GET-by-id still returned deleted projects/branches/tags with
200. Single-resource GETs now apply the same filter.

POST /projects with an existing @id silently overwrote the project via
the underlying PUT (replacing title/description/defaultBranch); it now
returns 409 Conflict.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A payload string containing \r produced an illegal SPARQL string
literal and failed the whole commit with an opaque upstream parse
error. Element @ids (subject and references) were interpolated into
IRI positions unvalidated — hostile or merely malformed ids now get a
400 up front, and an array member without @id no longer NPEs into a
500.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Branch/tag/project builders defaulted a missing dct:title to "" and a
missing mms:created to OffsetDateTime.now(), so broken upstream data
(e.g. the GraphDB wildcard-CONSTRUCT bug behind issue #20) surfaced as
plausible-looking invented values instead of an observable failure.
Malformed resources are now omitted (404 on single GET) rather than
silently patched up.

Adds BranchListPairingTest asserting each branch's name stays paired
with its own @id in the list response — the exact regression surface
reported in #20.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Ktor 3.4.3 -> 3.5.1, Jena 6.0.0 -> 6.1.0, Kotest 6.1.11 -> 6.2.2,
logback 1.5.18 -> 1.5.38, kotlinx-coroutines-test 1.10.2 -> 1.11.0,
java-jwt 4.5.0 -> 4.6.0, JaCoCo 0.8.12 -> 0.8.15. All API-compatible;
full suite green. Major migrations (Gradle 9, Kotlin 2.4, Sonar plugin
7, JUnit 6) deliberately deferred to a dedicated change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CORS was anyHost() with allowCredentials = true (flagged @todo), letting
any website make credentialed cross-origin requests that replay a
logged-in user's Authorization header. Origins are now configurable via
flexo.corsAllowedOrigins / FLEXO_CORS_ALLOWED_ORIGINS (space-separated);
when unset the API still answers any origin, but credentialed
cross-origin requests are disabled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

/elements response for nested behavioral flows appears to omit featureChain information Error for Query - PrimitiveConstraint value cannot be array

1 participant