Skip to content

feat(client): conditional create via If-None-Exist - #151

Merged
quotentiroler merged 1 commit into
developfrom
feat/conditional-create-if-none-exist
Aug 9, 2026
Merged

feat(client): conditional create via If-None-Exist#151
quotentiroler merged 1 commit into
developfrom
feat/conditional-create-if-none-exist

Conversation

@quotentiroler

Copy link
Copy Markdown
Collaborator

What

Adds conditional create to FhirResourceWriter:

await writer.create(person, { ifNoneExist: `identifier=${system}|${value}` });

The server creates the resource only when that search matches nothing, and returns the existing one otherwise. create(resource) is unchanged, so this is purely additive.

Why

Without it, any caller that needs an idempotent write has to leave the client and hand-roll a fetch just to set one header. That is how a repo ends up with its own FHIR writer sitting next to this one.

The concrete case: maxhealth.tech provisions a member into FHIR as a resumable job (Person → Patient → Consent → Coverage). Every write has to be safe to retry, because the job runs again after any failure — and "one human, one Person" is enforced by nothing except the conditional create. Running that job twice must produce one Person, not two.

Two details worth reviewing

The header takes the raw search, not url-encoded. If-None-Exist is a header, not a query string, and HAPI parses it raw. Encoding the | makes the search match nothing, which silently turns a conditional create into an unconditional one — every retry adds another copy. Documented on the option and asserted in a test.

A match may answer 200 with no body. The server has nothing new to report, and the id then exists only in Location. Reading the body alone would throw exactly on the idempotent path, so the response reader falls back to Location and errors only when neither carries an id.

Changes

  • scripts/client-codegen/template/writer.tsCreateOptions.ifNoneExist, plus the Location fallback
  • scripts/client-codegen/template/writer.test.ts — 5 new cases; mockFetch now serves text and headers as well as json
  • packages/client-{r4,r4b,r5}/src/writer{,.test}.ts — regenerated, not edited

Verification

  • 13 tests pass in each of the three client packages (39 total)
  • npm run generate:clients:check is clean, so the generated files match the template

The four TS2307: Cannot find module '@babelfhir-ts/smart-auth' errors from tsc --noEmit in client-r4 are present identically on a clean develop — the repo declares no npm workspaces, so a root install never links the sibling packages. Unrelated to this change.

…cond resource

`create()` could only POST unconditionally, so every caller that needs an
idempotent write had to drop out of the client and hand-roll a fetch to set
`If-None-Exist` — which is how a third copy of a FHIR writer gets written.

It is now an option on `create()`: pass the search string and the server creates
only when nothing matches, returning the existing resource otherwise. The header
takes the raw search, deliberately not url-encoded, because encoding the `|`
makes the search match nothing and silently turns every retry into another copy.

A match is allowed to answer 200 with no body, where the id exists only in
`Location`. Parsing the body alone would throw exactly on the idempotent path,
so the response reader falls back to `Location` and errors only when neither
carries an id.

Additive: `create(resource)` is unchanged, and the three clients are regenerated
from the template rather than edited.
@quotentiroler
quotentiroler merged commit 545fa28 into develop Aug 9, 2026
8 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.

1 participant