Skip to content

Update: [AEA-6376] - cardinality within structured definitions#4570

Open
bencegadanyi1-nhs wants to merge 37 commits intomasterfrom
AEA-6376-cardinality-within-structured-definitions
Open

Update: [AEA-6376] - cardinality within structured definitions#4570
bencegadanyi1-nhs wants to merge 37 commits intomasterfrom
AEA-6376-cardinality-within-structured-definitions

Conversation

@bencegadanyi1-nhs
Copy link
Copy Markdown
Contributor

@bencegadanyi1-nhs bencegadanyi1-nhs commented Apr 16, 2026

Summary

  • Routine Change

Details

Formatted MedicationRequest (cardinality of definitions doesn't show here because all of them have a default min value of 0, so "minItem": 0 is omitted from body)

{
  "description": "see http://hl7.org/fhir/json.html#schema for information about the FHIR Json Schemas",
  "$ref": "#/definitions/MedicationRequest",
  "$id": "http://hl7.org/fhir/json-schema/MedicationRequest",
  "$schema": "http://json-schema.org/draft-04/schema#",
  "definitions": {
    "MedicationRequest": {
      "allOf": [
        {
          "$ref": "DomainResource#/definitions/DomainResource"
        },
        {
          "description": "The date (and perhaps time) when the prescription was initially written or authored on.",
          "properties": {
            "authoredOn": {
              "description": "The date (and perhaps time) when the prescription was initially written or authored on.",
              "type": "string",
              "pattern": "([0-9]([0-9]([0-9][1-9]|[1-9]0)|[1-9]00)|[1-9]000)(-(0[1-9]|1[0-2])(-(0[1-9]|[1-2][0-9]|3[0-1])(T([01][0-9]|2[0-3]):[0-5][0-9]:([0-5][0-9]|60)(\\.[0-9]+)?(Z|(\\+|-)((0[0-9]|1[0-3]):[0-5][0-9]|14:00)))?)?)?"
            },
            "basedOn": {
              "type": "array",
              "items": {
                "$ref": "Reference.schema.json#/$definitions/Reference",
                "description": "A plan or request that is fulfilled in whole or in part by this medication request."
              }
            },
            "category": {
              "type": "array",
              "items": {
                "$ref": "CodeableConcept.schema.json#/$definitions/CodeableConcept",
                "description": "Indicates the type of medication request (for example, where the medication is expected to be consumed or administered (i.e. inpatient or outpatient))."
              }
            },
            "courseOfTherapyType": {
              "$ref": "CodeableConcept.schema.json#/$definitions/CodeableConcept",
              "description": "The description of the overall patte3rn of the administration of the medication to the patient."
            },
            "detectedIssue": {
              "type": "array",
              "items": {
                "$ref": "Reference.schema.json#/$definitions/Reference",
                "description": "Indicates an actual or potential clinical issue with or between one or more active or proposed clinical actions for a patient; e.g. Drug-drug interaction, duplicate therapy, dosage alert etc."
              }
            },
            "dispenseRequest": {
              "$ref": "BackboneElement.schema.json#/$definitions/BackboneElement",
              "description": "Indicates the specific details for the dispense or medication supply part of a medication request (also known as a Medication Prescription or Medication Order).  Note that this information is not always sent with the order.  There may be in some settings (e.g. hospitals) institutional or system support for completing the dispense details in the pharmacy department."
            },
            "doNotPerform": {
              "description": "If true indicates that the provider is asking for the medication request not to occur.",
              "type": "boolean"
            },
            "dosageInstruction": {
              "type": "array",
              "items": {
                "$ref": "Dosage.schema.json#/$definitions/Dosage",
                "description": "Indicates how the medication is to be used by the patient."
              }
            },
            "encounter": {
              "$ref": "Reference.schema.json#/$definitions/Reference",
              "description": "The Encounter during which this [x] was created or to which the creation of this record is tightly associated."
            },
            "eventHistory": {
              "type": "array",
              "items": {
                "$ref": "Reference.schema.json#/$definitions/Reference",
                "description": "Links to Provenance records for past versions of this resource or fulfilling request or event resources that identify key state transitions or updates that are likely to be relevant to a user looking at the current version of the resource."
              }
            },
            "groupIdentifier": {
              "$ref": "Identifier.schema.json#/$definitions/Identifier",
              "description": "A shared identifier common to all requests that were authorized more or less simultaneously by a single author, representing the identifier of the requisition or prescription."
            },
            "identifier": {
              "type": "array",
              "items": {
                "$ref": "Identifier.schema.json#/$definitions/Identifier",
                "description": "Identifiers associated with this medication request that are defined by business processes and/or used to refer to it when a direct URL reference to the resource itself is not appropriate. They are business identifiers assigned to this resource by the performer or other systems and remain constant as the resource is updated and propagates from server to server."
              }
            },
            "instantiatesCanonical": {
              "type": "array",
              "items": {
                "description": "The URL pointing to a protocol, guideline, orderset, or other definition that is adhered to in whole or in part by this MedicationRequest.",
                "type": "string",
                "pattern": "\\S*"
              }
            },
            "instantiatesUri": {
              "type": "array",
              "items": {
                "description": "The URL pointing to an externally maintained protocol, guideline, orderset or other definition that is adhered to in whole or in part by this MedicationRequest.",
                "type": "string",
                "pattern": "\\S*"
              }
            },
            "insurance": {
              "type": "array",
              "items": {
                "$ref": "Reference.schema.json#/$definitions/Reference",
                "description": "Insurance plans, coverage extensions, pre-authorizations and/or pre-determinations that may be required for delivering the requested service."
              }
            },
            "intent": {
              "description": "Whether the request is a proposal, plan, or an original order.",
              "enum": [
                "proposal",
                "plan",
                "order",
                "original-order",
                "reflex-order",
                "filler-order",
                "instance-order",
                "option"
              ],
              "type": "string"
            },
            "medication[x]": {
              "$ref": "CodeableConcept.schema.json#/$definitions/CodeableConcept",
              "description": "Identifies the medication being requested. This is a link to a resource that represents the medication which may be the details of the medication or simply an attribute carrying a code that identifies the medication from a known list of medications."
            },
            "note": {
              "type": "array",
              "items": {
                "$ref": "Annotation.schema.json#/$definitions/Annotation",
                "description": "Extra information about the prescription that could not be conveyed by the other attributes."
              }
            },
            "performer": {
              "$ref": "Reference.schema.json#/$definitions/Reference",
              "description": "The specified desired performer of the medication treatment (e.g. the performer of the medication administration)."
            },
            "performerType": {
              "$ref": "CodeableConcept.schema.json#/$definitions/CodeableConcept",
              "description": "Indicates the type of performer of the administration of the medication."
            },
            "priority": {
              "description": "Indicates how quickly the Medication Request should be addressed with respect to other requests.",
              "enum": [
                "routine",
                "urgent",
                "asap",
                "stat"
              ],
              "type": "string"
            },
            "priorPrescription": {
              "$ref": "Reference.schema.json#/$definitions/Reference",
              "description": "A link to a resource representing an earlier order related order or prescription."
            },
            "reasonCode": {
              "type": "array",
              "items": {
                "$ref": "CodeableConcept.schema.json#/$definitions/CodeableConcept",
                "description": "The reason or the indication for ordering or not ordering the medication."
              }
            },
            "reasonReference": {
              "type": "array",
              "items": {
                "$ref": "Reference.schema.json#/$definitions/Reference",
                "description": "Condition or observation that supports why the medication was ordered."
              }
            },
            "recorder": {
              "$ref": "Reference.schema.json#/$definitions/Reference",
              "description": "The person who entered the order on behalf of another individual for example in the case of a verbal or a telephone order."
            },
            "reported[x]": {
              "description": "Indicates if this record was captured as a secondary 'reported' record rather than as an original primary source-of-truth record.  It may also indicate the source of the report.",
              "type": "boolean"
            },
            "requester": {
              "$ref": "Reference.schema.json#/$definitions/Reference",
              "description": "The individual, organization, or device that initiated the request and has responsibility for its activation."
            },
            "status": {
              "description": "A code specifying the current state of the order.  Generally, this will be active or completed state.",
              "enum": [
                "active",
                "on-hold",
                "cancelled",
                "completed",
                "entered-in-error",
                "stopped",
                "draft",
                "unknown"
              ],
              "type": "string"
            },
            "statusReason": {
              "$ref": "CodeableConcept.schema.json#/$definitions/CodeableConcept",
              "description": "Captures the reason for the current state of the MedicationRequest."
            },
            "subject": {
              "$ref": "Reference.schema.json#/$definitions/Reference",
              "description": "A link to a resource representing the person or set of individuals to whom the medication will be given."
            },
            "substitution": {
              "$ref": "BackboneElement.schema.json#/$definitions/BackboneElement",
              "description": "Indicates whether or not substitution can or should be part of the dispense. In some cases, substitution must happen, in other cases substitution must not happen. This block explains the prescriber's intent. If nothing is specified substitution may be done."
            },
            "supportingInformation": {
              "type": "array",
              "items": {
                "$ref": "Reference.schema.json#/$definitions/Reference",
                "description": "Include additional information (for example, patient height and weight) that supports the ordering of the medication."
              }
            }
          },
          "required": [
            "intent",
            "medication[x]",
            "status",
            "subject"
          ]
        }
      ]
    },
    "MedicationRequest_DispenseRequest": {
      "allOf": [
        {
          "$ref": "Duration#/definitions/Duration"
        },
        {
          "description": "The minimum period of time that must occur between dispenses of the medication.",
          "properties": {
            "dispenseInterval": {
              "$ref": "Duration.schema.json#/$definitions/Duration",
              "description": "The minimum period of time that must occur between dispenses of the medication."
            },
            "expectedSupplyDuration": {
              "$ref": "Duration.schema.json#/$definitions/Duration",
              "description": "Identifies the period time over which the supplied product is expected to be used, or the length of time the dispense is expected to last."
            },
            "initialFill": {
              "$ref": "BackboneElement.schema.json#/$definitions/BackboneElement",
              "description": "Indicates the quantity or duration for the first dispense of the medication.",
              "duration": {
                "$ref": "Duration.schema.json#/$definitions/Duration",
                "description": "The length of time that the first dispense is expected to last."
              },
              "quantity": {
                "$ref": "Quantity.schema.json#/$definitions/Quantity",
                "description": "The amount or quantity to provide as part of the first dispense."
              }
            },
            "numberOfRepeatsAllowed": {
              "description": "An integer indicating the number of times, in addition to the original dispense, (aka refills or repeats) that the patient can receive the prescribed medication. Usage Notes: This integer does not include the original order dispense. This means that if an order indicates dispense 30 tablets plus \"3 repeats\", then the order can be dispensed a total of 4 times and the patient can receive a total of 120 tablets.  A prescriber may explicitly say that zero refills are permitted after the initial dispense.",
              "type": "string",
              "pattern": "[0]|([1-9][0-9]*)"
            },
            "performer": {
              "$ref": "Reference.schema.json#/$definitions/Reference",
              "description": "Indicates the intended dispensing Organization specified by the prescriber."
            },
            "quantity": {
              "$ref": "Quantity.schema.json#/$definitions/Quantity",
              "description": "The amount that is to be dispensed for one fill."
            },
            "validityPeriod": {
              "$ref": "Period.schema.json#/$definitions/Period",
              "description": "This indicates the validity period of a prescription (stale dating the Prescription)."
            }
          }
        }
      ]
    },
    "MedicationRequest_Substitution": {
      "allOf": [
        {
          "$ref": "boolean#/definitions/boolean"
        },
        {
          "description": "True if the prescriber allows a different drug to be dispensed from what was prescribed.",
          "properties": {
            "allowed[x]": {
              "description": "True if the prescriber allows a different drug to be dispensed from what was prescribed.",
              "type": "boolean"
            },
            "reason": {
              "$ref": "CodeableConcept.schema.json#/$definitions/CodeableConcept",
              "description": "Indicates the reason for the substitution, or why substitution must or must not be performed."
            }
          },
          "required": [
            "allowed[x]"
          ]
        }
      ]
    }
  }
}

Copilot AI review requested due to automatic review settings April 16, 2026 13:13
@bencegadanyi1-nhs bencegadanyi1-nhs force-pushed the AEA-6376-cardinality-within-structured-definitions branch from 4c20d15 to 883c38d Compare April 16, 2026 13:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates the fhir-schema-generation package to generate JSON Schemas from Simplifier FHIR StructureDefinitions (including cardinality handling), migrates its test suite from Jest to Vitest, and refreshes frontend Jest snapshots impacted by styling/classname changes.

Changes:

  • Added a schema generation pipeline (download → parse → generate → write schemas) for FHIR StructureDefinitions.
  • Migrated packages/fhir-schema-generation tests/config from Jest to Vitest and added extensive unit tests.
  • Updated multiple client snapshot files (styled-components class hash changes) and added a local Grype scanning Makefile target.

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/tool/site/client/tests/pages/snapshots/validatePage.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/pages/snapshots/signPage.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/pages/snapshots/returnPage.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/pages/snapshots/loginPage.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/pages/snapshots/doseToTextPage.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/pages/snapshots/configPage.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/components/release/snapshots/releaseForm.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/components/prescription-summary/snapshots/prescriptionSummaryViewEditable.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/components/claim/snapshots/claimForm.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/components/snapshots/sorter.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/components/snapshots/pageHeader.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/tool/site/client/tests/components/snapshots/longRunningTask.spec.tsx.snap Snapshot update (styled-components class hash changes).
packages/fhir-schema-generation/vitest.config.ts Adds Vitest configuration for the package.
packages/fhir-schema-generation/tsconfig.json Updates TS config for Vitest (types/include/exclude).
packages/fhir-schema-generation/tests/parse-simplifier-package.test.ts Adds tests for parsing + definition file discovery utilities.
packages/fhir-schema-generation/tests/index.test.ts Adds tests around the new schema-generation pipeline entrypoint.
packages/fhir-schema-generation/tests/generate-openapi-schema.test.ts Adds comprehensive tests for schema generation/cardinality/binding enum logic.
packages/fhir-schema-generation/tests/fetch-fhir.test.ts Removes legacy Jest tests for the old downloader implementation.
packages/fhir-schema-generation/tests/download-simplifier-package.test.ts Adds Vitest tests for the new downloader/extractor implementation.
packages/fhir-schema-generation/tests/common.test.ts Adds tests for common helpers (filename normalization).
packages/fhir-schema-generation/src/utils/parse-simplifier-package.ts Adds parsing utilities and definition-file discovery helper.
packages/fhir-schema-generation/src/utils/generate-openapi-schema.ts Introduces schema generation logic (including cardinality + binding-enum handling).
packages/fhir-schema-generation/src/utils/download-simplifier-package.ts Reworks Simplifier package download/extraction and caching behavior.
packages/fhir-schema-generation/src/utils/common.ts Adds filename normalization helper.
packages/fhir-schema-generation/src/types/fhir-to-json-schema-type.ts Adds FHIR primitive → JSON Schema type mapping.
packages/fhir-schema-generation/src/types/editable-json-schema.type.ts Adds mutable JSON Schema type helper.
packages/fhir-schema-generation/src/types/deep-mutable.type.ts Adds generic deep-mutable utility type.
packages/fhir-schema-generation/src/models/structure-definition/type.interface.ts Adds StructureDefinition type model.
packages/fhir-schema-generation/src/models/structure-definition/snapshot.interface.ts Adds snapshot model interface.
packages/fhir-schema-generation/src/models/structure-definition/kind.type.ts Adds StructureDefinition kind union type.
packages/fhir-schema-generation/src/models/structure-definition/identity-map.interface.ts Adds mapping model interface.
packages/fhir-schema-generation/src/models/structure-definition/extension.interface.ts Adds extension model interface.
packages/fhir-schema-generation/src/models/structure-definition/differential-element.interface.ts Adds differential element model interface.
packages/fhir-schema-generation/src/models/structure-definition/constraint.interface.ts Adds constraint model interface.
packages/fhir-schema-generation/src/models/structure-definition/base-element.interface.ts Adds base element model interface.
packages/fhir-schema-generation/src/models/fhir/fhir-package-metadata.ts Removes legacy FHIR package metadata model.
packages/fhir-schema-generation/src/models/fhir-package/structure-definition.interface.ts Adds StructureDefinition model used by the new generator.
packages/fhir-schema-generation/src/models/fhir-package/package-version.interface.ts Adds package-version interface used by the downloader.
packages/fhir-schema-generation/src/models/fhir-package/fhir-package-metadata.interface.ts Adds package metadata interface used by the downloader.
packages/fhir-schema-generation/src/models/fhir-package/dist-tags.interface.ts Adds dist-tags interface used by package metadata.
packages/fhir-schema-generation/src/index.ts Implements the new end-to-end schema generation pipeline entrypoint.
packages/fhir-schema-generation/package.json Switches tests to Vitest and updates devDependencies.
packages/fhir-schema-generation/jest.config.ts Removes Jest config (migrated to Vitest).
Makefile Adds grype-scan-local target and adjusts workspace install ordering.
Comments suppressed due to low confidence (2)

packages/fhir-schema-generation/src/utils/download-simplifier-package.ts:18

  • The leading JSDoc delimiter is malformed (/** * ...), which breaks standard doc tooling/formatting. Also the doc still describes version as optional, but queryPackageVersion now throws when it’s undefined/empty—update the doc to match current behavior.
    packages/fhir-schema-generation/src/utils/download-simplifier-package.ts:140
  • downloadSimplifierPackage currently logs outputDir/rawOutputFile unconditionally, and the cache branch returns early when the tarball exists without ensuring the extracted package/ directory is present. This can leave the pipeline in a broken state if the extract folder was cleaned but the tarball remains. Consider removing the debug logs and, when the tarball is cached, either verify outputDir/package/package.json exists or re-run extraction from the cached tarball.

Comment on lines 24 to 36
"@eslint/js": "^10.0.1",
"@types/node": "^25.5.2",
"@types/tar": "^7.0.87",
"eslint": "^10.2.0",
"json-schema-to-ts": "^3.1.1",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"typescript-eslint": "^8.58.0",
"vitest": "^3.0.0",
"@vitest/coverage-v8": "^3.0.0"
},
"dependencies": {
"tar": "^7.5.13"
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tar is imported/used at runtime (e.g. in download-simplifier-package.ts), but package.json no longer has a dependencies section and lists tar under devDependencies. If this package is installed with --omit=dev (common in CI/production), node dist/index.js will fail at runtime. Move tar (and any other runtime deps) into dependencies, or ensure runtime always installs devDependencies.

Copilot uses AI. Check for mistakes.
Comment on lines +100 to +115
// sets a deeply nested property on an object, creating intermediate objects as needed.
function setNestedProp(
obj: Record<string, EditableJSONSchema>,
propertyPath: Array<string>,
value: EditableJSONSchema
): void {
const pathTraverse = propertyPath.slice(0, -1)
const finalKey = propertyPath[propertyPath.length - 1]

const targetObj = pathTraverse.reduce<Record<string, EditableJSONSchema>>((prev, curr) => {
if (!prev[curr]) {
const empty: EditableJSONSchema = {}
prev[curr] = empty
}
return prev[curr] as Record<string, EditableJSONSchema>
}, obj)
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setNestedProp builds nested structures by assigning plain objects (and even mutating existing schemas like $ref objects) instead of creating proper JSON Schema objects with type: "object" + properties (or allOf to extend a $ref). In draft-04, siblings of $ref are ignored, so nested fields like initialFill.duration will be dropped/ineffective. Consider generating a sub-definition (e.g. Resource_DispenseRequest_InitialFill) and referencing it, or using allOf: [{$ref: ...}, {type:"object", properties:{...}}] for nested additions.

Copilot uses AI. Check for mistakes.
Comment on lines +212 to +219
if (!definitions[definitionKey]) {
let baseRef: string
if (definitionKey === schema.name) {
baseRef = deriveBaseRef(schema)
} else {
const baseClass = element.type[0].code || "BackboneElement"
baseRef = `${baseClass}#/definitions/${baseClass}`
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sub-definitions (e.g. Resource_DispenseRequest), baseRef is derived from element.type[0].code, but for nested elements that type is the leaf field type (e.g. Quantity for ...dispenseRequest.quantity) rather than the container/backbone element type. This makes the generated schema inheritance incorrect. Prefer deriving the base from the structure/backbone element itself (typically BackboneElement), or look up the type from the corresponding container element in the differential (e.g. Resource.dispenseRequest).

Copilot uses AI. Check for mistakes.
Comment on lines +7 to +10
* Reads a JSON Schema file produced by the Simplifier package and returns
* its definitions types
*
* @param directory Root directory of the extracted Simplifier package.
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc for parseSimplifierPackage lists parameters (directory, filePath) that don’t match the actual function signature (only filePath). This makes the API misleading for callers; please update the comment to reflect the real parameters/behavior.

Suggested change
* Reads a JSON Schema file produced by the Simplifier package and returns
* its definitions types
*
* @param directory Root directory of the extracted Simplifier package.
* its definition types.
*
* @param filePath Path to the Simplifier JSON schema file to read
* (for example "openapi/MedicationRequest.schema.json").

Copilot uses AI. Check for mistakes.
*/
export async function getSimplifierDefinitionFiles(folderPath: string, prefix: string): Promise<Array<string>> {
try {
// Read the directory contents, returning fs.Dirent objects
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getSimplifierDefinitionFiles is declared async but uses fs.readdirSync (and even awaits it). This is misleading and blocks the event loop. Either make it fully synchronous (remove async/Promise) or switch to await fs.promises.readdir(...) and keep it async.

Suggested change
// Read the directory contents, returning fs.Dirent objects
const items = await fs.promises.readdir(folderPath, {withFileTypes: true})

Copilot uses AI. Check for mistakes.
Comment on lines +14 to +21
function buildOutputPath(): string {
return path.join(
process.cwd(),
".output",
"parsed",
normalizeFileName(`${PACKAGE_NAME}-${PACKAGE_VERSION}`)
)
}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildOutputPath() uses a constant PACKAGE_VERSION = "latest", so successive runs will extract different resolved package versions into the same directory. Because tar extraction won’t remove files that disappeared between versions, stale files can persist and affect schema generation. Consider incorporating the resolved version into the output directory name (after queryPackageVersion), or clearing the existing package/ directory before extracting a new version.

Copilot uses AI. Check for mistakes.
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
3 Security Hotspots

See analysis details on SonarQube Cloud

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.

3 participants