/** * - WHICH QUESTIONNAIRE DOES THIS RESPONSE ANSWER? * * The defect * `QuestionnaireResponse.questionnaire` is handed straight to `fetchQuestionnaire(id)`, which builds * `${API_URL}/Questionnaire/${id}`. When the stored value is already `Questionnaire/{id}` the request * goes to `/R4/Questionnaire/Questionnaire/{id}`, which matches no route - so the preflight gets no * CORS headers back and the browser reports it as a CORS failure. The patient presses **View Answers** * and nothing at all happens. * * THE TWO WRITERS DISAGREE, AND THAT IS THE WHOLE POINT * There are exactly two ways a QuestionnaireResponse comes into being, and they store DIFFERENT shapes: * * | how the response was created | writer | stored value | * |---|---|---| * | a patient completes an ASSIGNED questionnaire | `QuestionnaireService.transformToFhirQuestionnaireResponse` | `questionnaire.id` - a **bare id** | * | a public-form submission is attached to a patient | api `FormSubmissions/convert.ts` | `` `Questionnaire/${id}` `` - a **relative reference** | * * **View Answers was never broken for every patient.** It works on the assigned path and fails on the * attached one, which is why the report came from a converted submission. Worth stating plainly, * because the card raised the opposite possibility as the thing that would decide severity. * * Both shapes exist on real records right now, so this is resolved on READ. Converging the two * writers would not remove the need for this - the already-written rows would still carry both shapes - * and rewriting stored clinical data to fix a URL is a far larger act than the defect warrants. * * WHY `FhirService.extractIdFromReference` IS NOT THE ANSWER HERE * It returns `undefined` for anything without a `/`: * * const parts = reference.split('/'); * return parts.length > 1 ? parts[parts.length - 1]: undefined; * * Reaching for it would repair the reported path and **BREAK the one that works today**, turning a * bare id into `undefined`. That failure would look exactly like this one: a dead button. The mixed * input is the requirement, so the resolver has to be told about it rather than inherited. * * What it refuses, and why refusing beats guessing * A true FHIR R4 `canonical` is the questionnaire's own `url` - `http://acme.org/forms/intake` - whose * last segment is a name, NOT a resource id. Nothing in this product writes one, but if one ever * arrives, taking its tail would request a plausible-looking id that does not exist. So a URL is only * resolved when it actually contains a `Questionnaire/{id}` segment pair; anything else returns `null`, * and the caller says it could not load the questionnaire. **A wrong id fetched confidently is worse * than an honest failure** - it would produce someone else's form, or a 404 blamed on the record. */ /** * The FHIR R4 `id` datatype, `[A-Za-z0-9\-\.]{1,64}`, **plus a requirement the spec does not make**: at * least one alphanumeric character. * * THE LOOKAHEAD IS NOT TIDINESS. `.` is in the FHIR id charset, so the datatype's own regex admits * `..` - and `..` is a path traversal the moment it is pasted into `${API_URL}/Questionnaire/${id}`, * where it addresses the collection root rather than a resource. A dots-only segment is never a real id, * so refusing it costs nothing and closes the only way a value that passes this check can still change * which URL is requested. * * Everything this admits is already URL-safe, so the id needs no percent-encoding downstream. */ const FHIR_ID = /^(?=.*[A-Za-z0-9])[A-Za-z0-9\-.]{1,64}$/; /** * Resolve the bare resource id from any shape `QuestionnaireResponse.questionnaire` is known to hold. * * Accepts a bare id (`abc-123`), a relative reference (`Questionnaire/abc-123`), a versioned reference * (`Questionnaire/abc-123/_history/2`), an absolute URL containing the type segment, and any of those * carrying a canonical version suffix (`...|1.0.0`). * * @returns the id, or `null` when the value is absent, malformed, or a canonical this client cannot * resolve to a resource id. Never a best guess. */ export const questionnaireIdFromReference = (value: string | undefined | null): string | null => { if (typeof value !== 'string') { return null; } // A canonical may carry `|version`. The version is not part of the address we fetch by id. const withoutVersion = value.split('|')[0].trim(); if (!withoutVersion) { return null; } // `Questionnaire/{id}/_history/{n}` addresses one version of a resource; the id is still the id. const withoutHistory = withoutVersion.replace(/\/_history\/[^/]*\/?$/, ''); const segments = withoutHistory.split('/').filter((segment) => segment.length > 0); if (segments.length === 0) { return null; } // A BARE ID the assigned-questionnaire path. One segment and no scheme, so there is nothing to // strip. Guarded against `://` so a scheme-only string cannot be mistaken for an id. if (segments.length === 1 && !withoutHistory.includes('://')) { // `Questionnaire` and `Questionnaire/` both reduce to a single segment that is the TYPE, and // the type passes the id charset - so without this they would resolve to the id "Questionnaire" // and request `/R4/Questionnaire/Questionnaire`. That is a reference with its id missing, and it // is certainly not a resource; the truncation is the finding, so say so instead of fetching. if (segments[0] === 'Questionnaire') { return null; } return FHIR_ID.test(segments[0]) ? segments[0] : null; } // A REFERENCE OR A RESOLVABLE URL the id is whatever follows the LAST `Questionnaire` segment. // Last rather than first: a base path may legitimately contain the word earlier // (`/fhir/Questionnaire/Questionnaire/x` is not something we write, but the tail is the answer either // way, and taking the first would return the literal string `Questionnaire`). const typeIndex = segments.lastIndexOf('Questionnaire'); if (typeIndex === -1 || typeIndex === segments.length - 1) { return null; } const id = segments[typeIndex + 1]; return FHIR_ID.test(id) ? id : null; }; export default questionnaireIdFromReference;