From 4169599177ee874eeec22a30b5a5e7b4a87147c4 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Thu, 12 Mar 2026 18:59:31 -0300 Subject: [PATCH 01/11] Update project files with recent changes and improvements --- .github/workflows/ci.yml | 44 -------- .github/workflows/docker-image.yml | 64 +++++++++++ .github/workflows/docker.yml | 34 ------ .github/workflows/pr-pipeline.yml | 89 ---------------- .github/workflows/release.yml | 166 ----------------------------- Dockerfile | 5 + 6 files changed, 69 insertions(+), 333 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/docker-image.yml delete mode 100644 .github/workflows/docker.yml delete mode 100644 .github/workflows/pr-pipeline.yml delete mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 0d71154d..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: CI Build - -on: - push: - branches: [ main ] - pull_request: - branches: [ main ] - -jobs: - test: - name: Test - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm run test:ci - timeout-minutes: 20 - env: - SUPPRESS_LOGS: 'true' - NODE_OPTIONS: "--max-old-space-size=14000" - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-results - path: ./test-results/junit.xml - - - name: Upload coverage reports - uses: codecov/codecov-action@v4 - with: - directory: ./coverage/ - token: ${{ secrets.CODECOV_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 00000000..4939165b --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,64 @@ +name: Docker Image CI + +on: + push: + branches: + - '**' + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - name: Checks out the push + uses: actions/checkout@v4 + with: + submodules: true + token: ${{ secrets.PAT_TOKEN }} + + - name: Set up QEMU (for multi-architecture support) + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver: docker-container + + - name: Log in to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Sanitize branch name + id: branch + run: | + echo "BRANCH_TAG=${GITHUB_REF_NAME//\//-}" >> $GITHUB_OUTPUT + + - name: Build and Push Docker images (latest + version) + run: | + docker buildx build \ + --provenance=true \ + --sbom=true \ + -t gointerop/fhirsmith:${{ steps.branch.outputs.BRANCH_TAG }} \ + --push . + + - name: Docker Scout CVE scan (remote image) + uses: docker/scout-action@v1 + continue-on-error: true # não bloqueia urgência + with: + command: cves + image: docker.io/gointerop/fhirsmith:${{ steps.branch.outputs.BRANCH_TAG }} + only-severities: critical + summary: true + + - name: Docker Scout Quickview (inclui policy quando disponível) + uses: docker/scout-action@v1 + continue-on-error: true + with: + organization: gointerop + command: quickview + image: registry://docker.io/gointerop/fhirsmith:${{ steps.branch.outputs.BRANCH_TAG }} + summary: true \ No newline at end of file diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml deleted file mode 100644 index c0be1b0c..00000000 --- a/.github/workflows/docker.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Docker Build - -on: - push: - branches: [ main ] - -jobs: - docker: - name: Build Docker Image - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Set lowercase repository name - run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ghcr.io/${{ env.REPO_LC }}:cibuild - ghcr.io/${{ env.REPO_LC }}:cibuild-${{ github.sha }} \ No newline at end of file diff --git a/.github/workflows/pr-pipeline.yml b/.github/workflows/pr-pipeline.yml deleted file mode 100644 index 77252f52..00000000 --- a/.github/workflows/pr-pipeline.yml +++ /dev/null @@ -1,89 +0,0 @@ -name: PR Pipeline - -on: - pull_request: - branches: [ main ] - -jobs: - lint: - name: Code Quality - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Needed for changed files detection - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Lint changed files only - run: | - # Get changed JS files, excluding vendor directories - CHANGED_FILES=$(git diff --name-only --diff-filter=ACMRT origin/main...HEAD | grep -E '\.(js|mjs)$' | grep -v 'static/' | grep -v 'node_modules/' | tr '\n' ' ') - if [ -n "$CHANGED_FILES" ]; then - echo "Linting changed files: $CHANGED_FILES" - npx eslint $CHANGED_FILES - else - echo "No relevant JavaScript files changed" - fi - - - name: Check code formatting - run: | - # Optional: Check if code is properly formatted - # npx prettier --check "**/*.{js,json,md}" --ignore-path .gitignore - - security: - name: Security Scan - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run npm audit - run: npm audit --audit-level=moderate - continue-on-error: true # Don't fail on low-severity issues - - - name: Check for known vulnerabilities - run: | - # Check for high/critical vulnerabilities only - # npm audit returns non-zero exit code if vulnerabilities are found at the specified level - if npm audit --audit-level=high; then - echo "No high or critical vulnerabilities found" - else - echo "High or critical vulnerabilities found!" - exit 1 - fi - - dependency-check: - name: Dependency Analysis - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Check for outdated dependencies - run: | - npm outdated || true # Don't fail, just report - - - name: Check package.json changes - run: | - if git diff --name-only origin/main...HEAD | grep -q "package.json\|package-lock.json"; then - echo "Dependencies changed - review required" - git diff origin/main...HEAD -- package.json package-lock.json - fi \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a34f6f6a..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: Release - -on: - push: - tags: - - 'v*.*.*' - -jobs: - test: - name: Test - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm run test:ci - env: - SUPPRESS_LOGS: 'true' - - - name: Upload test results - uses: actions/upload-artifact@v4 - if: always() - with: - name: test-results - path: ./test-results/junit.xml - - - name: Upload coverage reports - uses: codecov/codecov-action@v4 - with: - directory: ./coverage/ - token: ${{ secrets.CODECOV_TOKEN }} - - release: - name: Create Release - runs-on: ubuntu-latest - needs: test - - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version from tag - id: get_version - run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT - - - name: Extract release notes from CHANGELOG.md - id: extract_release_notes - run: | - VERSION=$(echo ${{ steps.get_version.outputs.VERSION }} | sed 's/^v//') - # Extract section for current version from CHANGELOG - SECTION=$(sed -n "/## \\[v${VERSION}\\]/,/## \\[v[0-9]\\+\\.[0-9]\\+\\.[0-9]\\+\\]/p" CHANGELOG.md | sed '$d') - - # If no section found, use a default message - if [ -z "$SECTION" ]; then - SECTION="## [v${VERSION}] - $(date +%Y-%m-%d)\nNo detailed release notes available." - fi - - # Escape newlines and special characters for GitHub Actions - RELEASE_NOTES="${SECTION//'%'/'%25'}" - RELEASE_NOTES="${RELEASE_NOTES//$'\n'/'%0A'}" - RELEASE_NOTES="${RELEASE_NOTES//$'\r'/'%0D'}" - - echo "RELEASE_NOTES<> $GITHUB_OUTPUT - echo "$RELEASE_NOTES" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: ${{ steps.get_version.outputs.VERSION }} - name: Release ${{ steps.get_version.outputs.VERSION }} - body: ${{ steps.extract_release_notes.outputs.RELEASE_NOTES }} - draft: false - prerelease: false - token: ${{ secrets.GITHUB_TOKEN }} - - npm-publish: - name: Publish to NPM - runs-on: ubuntu-latest - needs: test - permissions: - contents: read - id-token: write - - steps: - - uses: actions/checkout@v4 - - - name: Use Node.js - uses: actions/setup-node@v4 - with: - node-version: '24' - cache: 'npm' - registry-url: 'https://registry.npmjs.org' - - - name: Install dependencies - run: npm ci - - - name: Get version from tag - id: get_version - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - - - name: Update package.json version - run: npm pkg set version=${{ steps.get_version.outputs.VERSION }} - - - name: Publish to npm - run: npm publish --access public --provenance - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - docker: - name: Build and Push Docker Image - runs-on: ubuntu-latest - needs: release - permissions: - contents: read - packages: write - - steps: - - uses: actions/checkout@v4 - - - name: Get version from tag - id: get_version - run: | - VERSION=${GITHUB_REF#refs/tags/} - echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - echo "VERSION_NO_V=${VERSION#v}" >> $GITHUB_OUTPUT - - - name: Set lowercase repository name - run: echo "REPO_LC=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.repository_owner }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ghcr.io/${{ env.REPO_LC }}:latest - ghcr.io/${{ env.REPO_LC }}:${{ steps.get_version.outputs.VERSION }} - ghcr.io/${{ env.REPO_LC }}:${{ steps.get_version.outputs.VERSION_NO_V }} - cache-from: type=gha - cache-to: type=gha,mode=max - build-args: | - VERSION=${{ steps.get_version.outputs.VERSION_NO_V }} diff --git a/Dockerfile b/Dockerfile index 0f6a6cd8..ecb1b898 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,10 +22,15 @@ COPY --from=builder /app/node_modules ./node_modules COPY package*.json ./ COPY . . +# Bootstrap runtime data folder and config file +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Define build argument for version ARG VERSION=development ENV APP_VERSION=$VERSION # Expose port and define command EXPOSE 3000 +ENTRYPOINT ["docker-entrypoint.sh"] CMD ["node", "server.js"] \ No newline at end of file From 6e272d64d3c9088c8ea092d407b374f083bf5663 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Thu, 12 Mar 2026 19:03:57 -0300 Subject: [PATCH 02/11] Fixing data folder --- .gitignore | 18 ++++-------------- data/config.json | 25 +++++++++++++++++++++++++ data/library.yml | 23 +++++++++++++++++++++++ 3 files changed, 52 insertions(+), 14 deletions(-) create mode 100644 data/config.json create mode 100644 data/library.yml diff --git a/.gitignore b/.gitignore index 07bb985a..d890eda1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,39 +1,29 @@ .DS_Store -/data -/xig/data +# Ignore everything in data except config.json and library.yml +/data/* +!/data/config.json +!/data/library.yml node_modules/ -data/config.json - validator_cli.jar -xig/xig-download.log - .idea/codeStyles/ -data/logs/ - registry/registry-data.json -xig-download.log - locator/ tests/library/.last-integration-test-run package-cache/ -data/passwords.ini - test-cache/ tests/vs/.vsac-last-run -data/terminology-cache/ - data/shl/private-key.pem data/shl/public-key.pem diff --git a/data/config.json b/data/config.json new file mode 100644 index 00000000..99948bce --- /dev/null +++ b/data/config.json @@ -0,0 +1,25 @@ +{ + "server": { + "port": 80, + "cors": { + "origin": true, + "credentials": true + } + }, + "modules": { + "tx": { + "enabled": true, + "librarySource": "./data/library.yml", + "cacheTimeout": 30, + "expansionCacheSize": 1000, + "expansionCacheMemoryThreshold": 0, + "endpoints": [ + { + "path": "/r4", + "fhirVersion": "4.0", + "context": null + } + ] + } + } +} diff --git a/data/library.yml b/data/library.yml new file mode 100644 index 00000000..dbe6093c --- /dev/null +++ b/data/library.yml @@ -0,0 +1,23 @@ +base: + # Base used for large terminology artifacts (cache files, db snapshots, etc.) + url: https://storage.googleapis.com/tx-fhir-org + +sources: + # Built-in providers + - internal:lang + - internal:country + - internal:currency + - internal:mimetypes + + # Units + - ucum:tx/data/ucum-essence.xml + + # Native LOINC provider (complete base) + - loinc:loinc-2.77-a.db + + # HL7/NPM support used as fallback when OCL does not contain a resource + - npm:hl7.terminology + - npm:fhir.tx.support.r4 + + # Primary local source (custom content) + - ocl:https://oclapi2.ips.hsl.org.br From 93a3476d01e126b0c796e24fb011ef3ae9a5dc60 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Thu, 12 Mar 2026 19:05:14 -0300 Subject: [PATCH 03/11] Fixing Dockerfile --- Dockerfile | 5 ----- 1 file changed, 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index ecb1b898..0f6a6cd8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,15 +22,10 @@ COPY --from=builder /app/node_modules ./node_modules COPY package*.json ./ COPY . . -# Bootstrap runtime data folder and config file -COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh -RUN chmod +x /usr/local/bin/docker-entrypoint.sh - # Define build argument for version ARG VERSION=development ENV APP_VERSION=$VERSION # Expose port and define command EXPOSE 3000 -ENTRYPOINT ["docker-entrypoint.sh"] CMD ["node", "server.js"] \ No newline at end of file From cd1678c86226468754a70c89282a647e968f6b93 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Thu, 12 Mar 2026 19:10:22 -0300 Subject: [PATCH 04/11] Fixing Dockerignore --- .dockerignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.dockerignore b/.dockerignore index 3f3adc84..35e13ad4 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,7 +3,9 @@ npm-debug.log .git .gitignore logs -data +data/* +!data/config.json +!data/library.yml package-cache *.md .env From 4fef91c5a5262328638541cee4c66ea6390d9972 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Thu, 12 Mar 2026 19:29:43 -0300 Subject: [PATCH 05/11] Entrypoint configuration --- Dockerfile | 3 +-- config-defaults/config.json | 23 +++++++++++++++++++++++ config-defaults/library.yml | 23 +++++++++++++++++++++++ docker-compose.yml | 11 +++++++++++ entrypoint.sh | 22 ++++++++++++++++++++++ 5 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 config-defaults/config.json create mode 100644 config-defaults/library.yml create mode 100644 docker-compose.yml create mode 100644 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 0f6a6cd8..b5fb530f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -27,5 +27,4 @@ ARG VERSION=development ENV APP_VERSION=$VERSION # Expose port and define command -EXPOSE 3000 -CMD ["node", "server.js"] \ No newline at end of file +EXPOSE 3000 \ No newline at end of file diff --git a/config-defaults/config.json b/config-defaults/config.json new file mode 100644 index 00000000..dbe6093c --- /dev/null +++ b/config-defaults/config.json @@ -0,0 +1,23 @@ +base: + # Base used for large terminology artifacts (cache files, db snapshots, etc.) + url: https://storage.googleapis.com/tx-fhir-org + +sources: + # Built-in providers + - internal:lang + - internal:country + - internal:currency + - internal:mimetypes + + # Units + - ucum:tx/data/ucum-essence.xml + + # Native LOINC provider (complete base) + - loinc:loinc-2.77-a.db + + # HL7/NPM support used as fallback when OCL does not contain a resource + - npm:hl7.terminology + - npm:fhir.tx.support.r4 + + # Primary local source (custom content) + - ocl:https://oclapi2.ips.hsl.org.br diff --git a/config-defaults/library.yml b/config-defaults/library.yml new file mode 100644 index 00000000..dbe6093c --- /dev/null +++ b/config-defaults/library.yml @@ -0,0 +1,23 @@ +base: + # Base used for large terminology artifacts (cache files, db snapshots, etc.) + url: https://storage.googleapis.com/tx-fhir-org + +sources: + # Built-in providers + - internal:lang + - internal:country + - internal:currency + - internal:mimetypes + + # Units + - ucum:tx/data/ucum-essence.xml + + # Native LOINC provider (complete base) + - loinc:loinc-2.77-a.db + + # HL7/NPM support used as fallback when OCL does not contain a resource + - npm:hl7.terminology + - npm:fhir.tx.support.r4 + + # Primary local source (custom content) + - ocl:https://oclapi2.ips.hsl.org.br diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..95505d67 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' +services: + fhir-server: + image: gointerop/fhirsmith:main + container_name: tx-fhir-ocl + ports: + - "8084:80" + volumes: + - ./data:/app/data + entrypoint: ["/app/entrypoint.sh"] + command: ["node", "server.js"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100644 index 00000000..0f0f40d9 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,22 @@ +#!/bin/sh +set -e + +# Caminho dos arquivos de origem (ajuste se necessário) +DEFAULTS_DIR="/app/config-defaults" +DATA_DIR="/app/data" + +# Cria a pasta data se não existir +mkdir -p "$DATA_DIR" + +# Copia config.json se não existir +if [ ! -f "$DATA_DIR/config.json" ]; then + cp "$DEFAULTS_DIR/config.json" "$DATA_DIR/config.json" +fi + +# Copia library.yml se não existir +if [ ! -f "$DATA_DIR/library.yml" ]; then + cp "$DEFAULTS_DIR/library.yml" "$DATA_DIR/library.yml" +fi + +# Executa o comando padrão do container +exec "$@" From 722180d6ef7dcbbd90ef9ca1f33861f9fbd01336 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Thu, 12 Mar 2026 19:36:25 -0300 Subject: [PATCH 06/11] Fix entrypoint --- Dockerfile | 4 ++++ docker-compose.yml | 1 - entrypoint.sh | 6 +++++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index b5fb530f..40a853b7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,10 +18,14 @@ WORKDIR /app # Copy installed node_modules from builder COPY --from=builder /app/node_modules ./node_modules + # Bundle app source COPY package*.json ./ COPY . . +# Garante permissão de execução para o entrypoint +RUN chmod +x /app/entrypoint.sh + # Define build argument for version ARG VERSION=development ENV APP_VERSION=$VERSION diff --git a/docker-compose.yml b/docker-compose.yml index 95505d67..75a6f66f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,4 +8,3 @@ services: volumes: - ./data:/app/data entrypoint: ["/app/entrypoint.sh"] - command: ["node", "server.js"] diff --git a/entrypoint.sh b/entrypoint.sh index 0f0f40d9..b2c36708 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -19,4 +19,8 @@ if [ ! -f "$DATA_DIR/library.yml" ]; then fi # Executa o comando padrão do container -exec "$@" +if [ "$#" -eq 0 ]; then + exec node server.js +else + exec "$@" +fi From d2f4d1d60972d7896b26a018a65d524702511fd0 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Thu, 12 Mar 2026 19:39:49 -0300 Subject: [PATCH 07/11] Fixing config.json template --- config-defaults/config.json | 48 +++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/config-defaults/config.json b/config-defaults/config.json index dbe6093c..99948bce 100644 --- a/config-defaults/config.json +++ b/config-defaults/config.json @@ -1,23 +1,25 @@ -base: - # Base used for large terminology artifacts (cache files, db snapshots, etc.) - url: https://storage.googleapis.com/tx-fhir-org - -sources: - # Built-in providers - - internal:lang - - internal:country - - internal:currency - - internal:mimetypes - - # Units - - ucum:tx/data/ucum-essence.xml - - # Native LOINC provider (complete base) - - loinc:loinc-2.77-a.db - - # HL7/NPM support used as fallback when OCL does not contain a resource - - npm:hl7.terminology - - npm:fhir.tx.support.r4 - - # Primary local source (custom content) - - ocl:https://oclapi2.ips.hsl.org.br +{ + "server": { + "port": 80, + "cors": { + "origin": true, + "credentials": true + } + }, + "modules": { + "tx": { + "enabled": true, + "librarySource": "./data/library.yml", + "cacheTimeout": 30, + "expansionCacheSize": 1000, + "expansionCacheMemoryThreshold": 0, + "endpoints": [ + { + "path": "/r4", + "fhirVersion": "4.0", + "context": null + } + ] + } + } +} From 1947a3f70a476c988c00610d55ea85247f3a8782 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Tue, 24 Mar 2026 09:52:41 -0300 Subject: [PATCH 08/11] fix(ocl): prevent stale cache from hiding updated ValueSet content Two issues fixed in the OCL ValueSet provider: 1. #indexValueSet rejected fresh discovery metadata when a cold-cached entry already existed (different object identity check). This meant updated `lastUpdated` timestamps from OCL were never propagated, so #isCachedExpansionValid always passed and stale compose data was never invalidated. 2. #ensureComposeIncludes called #normalizeComposeIncludes which stripped `concept` arrays from compose entries built by background expansion. This caused the expand engine to fall back to "include whole CodeSystem", returning concepts that were deliberately excluded from the collection (e.g. LCVMA1DE001 appearing despite not being referenced in the ValueSet collection on OCL). Co-Authored-By: Claude Opus 4.6 (1M context) --- tx/ocl/vs-ocl.cjs | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/tx/ocl/vs-ocl.cjs b/tx/ocl/vs-ocl.cjs index d056a6b1..87602fe8 100644 --- a/tx/ocl/vs-ocl.cjs +++ b/tx/ocl/vs-ocl.cjs @@ -389,20 +389,12 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } #indexValueSet(vs) { - const existing = this.valueSetMap.get(vs.url) - || (vs.version ? this.valueSetMap.get(`${vs.url}|${vs.version}`) : null) - || this._idMap.get(vs.id) - || null; - - // Só indexa se não existe ou se for o mesmo objeto - if (!existing || existing === vs) { - this.valueSetMap.set(vs.url, vs); - if (vs.version) { - this.valueSetMap.set(`${vs.url}|${vs.version}`, vs); - } - this.valueSetMap.set(vs.id, vs); - this._idMap.set(vs.id, vs); + this.valueSetMap.set(vs.url, vs); + if (vs.version) { + this.valueSetMap.set(`${vs.url}|${vs.version}`, vs); } + this.valueSetMap.set(vs.id, vs); + this._idMap.set(vs.id, vs); } #toValueSet(collection) { @@ -568,6 +560,17 @@ class OCLValueSetProvider extends AbstractValueSetProvider { ? vs.jsonObj.compose.include : []; + // If the compose already has enumerated concepts (from background expansion), + // it is the authoritative representation of the collection contents — don't + // overwrite it with system-only entries that would cause the expand engine + // to include ALL concepts from the CodeSystem. + const hasEnumeratedConcepts = existingInclude.some( + inc => Array.isArray(inc.concept) && inc.concept.length > 0 + ); + if (hasEnumeratedConcepts) { + return; + } + // Always normalize existing compose entries first because discovery metadata // can carry non-canonical preferred_source values. const include = this.#normalizeComposeIncludes(existingInclude); From cc80cdd3dae29ec54a6339cae63be718d39d67f3 Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Tue, 24 Mar 2026 10:24:48 -0300 Subject: [PATCH 09/11] Fix expansion --- tx/ocl/vs-ocl.cjs | 52 +++++++++++++++++++++++++++++++---------------- 1 file changed, 34 insertions(+), 18 deletions(-) diff --git a/tx/ocl/vs-ocl.cjs b/tx/ocl/vs-ocl.cjs index 87602fe8..759da0bb 100644 --- a/tx/ocl/vs-ocl.cjs +++ b/tx/ocl/vs-ocl.cjs @@ -389,6 +389,20 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } #indexValueSet(vs) { + const existing = this.valueSetMap.get(vs.url) || null; + + // When fresh discovery metadata replaces a cold-cached entry, carry over + // the enumerated compose so the expand engine doesn't fall back to + // "include whole CodeSystem". The background expansion will eventually + // refresh it with up-to-date collection contents. + if (existing && existing !== vs + && Array.isArray(existing.jsonObj?.compose?.include) + && existing.jsonObj.compose.include.some(inc => Array.isArray(inc.concept) && inc.concept.length > 0) + && (!vs.jsonObj.compose || !Array.isArray(vs.jsonObj.compose.include) || vs.jsonObj.compose.include.length === 0) + ) { + vs.jsonObj.compose = existing.jsonObj.compose; + } + this.valueSetMap.set(vs.url, vs); if (vs.version) { this.valueSetMap.set(`${vs.url}|${vs.version}`, vs); @@ -975,9 +989,12 @@ class OCLValueSetProvider extends AbstractValueSetProvider { return; } - const cached = this.backgroundExpansionCache.get(cacheKey); + let cached = this.backgroundExpansionCache.get(cacheKey); + let invalidated = false; if (cached && !this.#isCachedExpansionValid(vs, cached)) { this.backgroundExpansionCache.delete(cacheKey); + cached = null; + invalidated = true; } // Already have a cached compose ready @@ -985,23 +1002,22 @@ class OCLValueSetProvider extends AbstractValueSetProvider { return; } - const cacheFilePath = getCacheFilePath(CACHE_VS_DIR, vs.url, vs.version || null, paramsKey); - const cacheAgeFromFileMs = getColdCacheAgeMs(cacheFilePath); - const persistedCache = this.backgroundExpansionCache.get(cacheKey); - const cacheAgeFromMetadataMs = Number.isFinite(persistedCache?.createdAt) - ? Math.max(0, Date.now() - persistedCache.createdAt) - : null; - - // Treat cache as fresh when either file mtime or persisted timestamp is recent. - const freshnessCandidates = [cacheAgeFromFileMs, cacheAgeFromMetadataMs].filter(age => age != null); - const freshestCacheAgeMs = freshnessCandidates.length > 0 ? Math.min(...freshnessCandidates) : null; - if (freshestCacheAgeMs != null && freshestCacheAgeMs <= COLD_CACHE_FRESHNESS_MS) { - const freshnessSource = cacheAgeFromFileMs != null && cacheAgeFromMetadataMs != null - ? 'file+metadata' - : cacheAgeFromFileMs != null - ? 'file' - : 'metadata'; - return; + // Skip freshness check when cache was just invalidated (VS metadata changed + // on the server) — the cold cache file is stale even if recently written. + if (!invalidated) { + const cacheFilePath = getCacheFilePath(CACHE_VS_DIR, vs.url, vs.version || null, paramsKey); + const cacheAgeFromFileMs = getColdCacheAgeMs(cacheFilePath); + const persistedCache = this.backgroundExpansionCache.get(cacheKey); + const cacheAgeFromMetadataMs = Number.isFinite(persistedCache?.createdAt) + ? Math.max(0, Date.now() - persistedCache.createdAt) + : null; + + // Treat cache as fresh when either file mtime or persisted timestamp is recent. + const freshnessCandidates = [cacheAgeFromFileMs, cacheAgeFromMetadataMs].filter(age => age != null); + const freshestCacheAgeMs = freshnessCandidates.length > 0 ? Math.min(...freshnessCandidates) : null; + if (freshestCacheAgeMs != null && freshestCacheAgeMs <= COLD_CACHE_FRESHNESS_MS) { + return; + } } const jobKey = `vs:${cacheKey}`; From 3c995832a9312ca4efad2f21f87ac885363eb43d Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Tue, 24 Mar 2026 15:33:56 -0300 Subject: [PATCH 10/11] FIx ocl cache i18n --- tx/ocl/cs-ocl.cjs | 60 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/tx/ocl/cs-ocl.cjs b/tx/ocl/cs-ocl.cjs index 2d3e9750..d71b984f 100644 --- a/tx/ocl/cs-ocl.cjs +++ b/tx/ocl/cs-ocl.cjs @@ -735,6 +735,17 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider { return { context: this.conceptCache.get(code), message: null }; } + // OCL concept IDs may differ in case from the FHIR code (e.g. "y" vs "Y"). + // Try a case-insensitive cache lookup before hitting the network. + const codeLower = code.toLowerCase(); + for (const [key, value] of this.conceptCache.entries()) { + if (key.toLowerCase() === codeLower) { + // Cache under the requested case as well so future lookups are O(1). + this.conceptCache.set(code, value); + return { context: value, message: null }; + } + } + if (this.scheduleBackgroundLoad) { this.scheduleBackgroundLoad('lookup-miss'); } @@ -1032,23 +1043,26 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider { this.scheduleBackgroundLoad('concept-miss'); } - const url = this.#buildConceptUrl(code); const pending = (async () => { - let response; - try { - response = await this.httpClient.get(url, { params: { verbose: true } }); - } catch (error) { - // Missing concept should be treated as not-found, not as an internal server failure. - if (error && error.response && error.response.status === 404) { - return null; - } - throw error; + const concept = await this.#fetchConceptByCode(code); + if (concept) { + return concept; } - const concept = this.#toConceptContext(response.data); - if (concept && concept.code) { - this.conceptCache.set(concept.code, concept); + // OCL concept IDs may differ in case from the FHIR code (e.g. "y" vs "Y"). + // Try common case alternatives before giving up. + const lower = code.toLowerCase(); + const upper = code.toUpperCase(); + for (const alt of [lower, upper]) { + if (alt !== code) { + const altConcept = await this.#fetchConceptByCode(alt); + if (altConcept) { + // Cache under the originally requested code so future lookups hit directly. + this.conceptCache.set(code, altConcept); + return altConcept; + } + } } - return concept; + return null; })(); this.pendingConceptRequests.set(pendingKey, pending); @@ -1059,6 +1073,24 @@ class OCLSourceCodeSystemProvider extends CodeSystemProvider { } } + async #fetchConceptByCode(code) { + const url = this.#buildConceptUrl(code); + let response; + try { + response = await this.httpClient.get(url, { params: { verbose: true } }); + } catch (error) { + if (error && error.response && error.response.status === 404) { + return null; + } + throw error; + } + const concept = this.#toConceptContext(response.data); + if (concept && concept.code) { + this.conceptCache.set(concept.code, concept); + } + return concept; + } + async #allConceptContexts() { const concepts = new Map(); From 8661f9793979e434c51fd6c49f13fa62f701676e Mon Sep 17 00:00:00 2001 From: Italo Macedo Date: Tue, 24 Mar 2026 15:58:01 -0300 Subject: [PATCH 11/11] Fix system code language designation missing --- tx/ocl/vs-ocl.cjs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tx/ocl/vs-ocl.cjs b/tx/ocl/vs-ocl.cjs index 759da0bb..72057332 100644 --- a/tx/ocl/vs-ocl.cjs +++ b/tx/ocl/vs-ocl.cjs @@ -1140,7 +1140,11 @@ class OCLValueSetProvider extends AbstractValueSetProvider { if (!systemConcepts.has(entry.system)) { systemConcepts.set(entry.system, []); } - systemConcepts.get(entry.system).push(entry.code); + const concept = { code: entry.code }; + if (Array.isArray(entry.designation) && entry.designation.length > 0) { + concept.designation = entry.designation; + } + systemConcepts.get(entry.system).push(concept); totalCount++; } if (progressState) { @@ -1157,9 +1161,9 @@ class OCLValueSetProvider extends AbstractValueSetProvider { } return { - include: Array.from(systemConcepts.entries()).map(([system, codes]) => ({ + include: Array.from(systemConcepts.entries()).map(([system, concepts]) => ({ system, - concept: codes.map(code => ({ code })) + concept: concepts })) }; }