diff --git a/.github/workflows/buildReleaseContainers.yaml b/.github/workflows/buildReleaseContainers.yaml index dfa40d3b10..c809d5c37e 100644 --- a/.github/workflows/buildReleaseContainers.yaml +++ b/.github/workflows/buildReleaseContainers.yaml @@ -58,22 +58,6 @@ jobs: labels: | org.opencontainers.image.version=${{ inputs.container-tag }} - - name: Extract metadata (tags, labels) for Flyway Docker - id: flyway-meta - uses: docker/metadata-action@v4 - if: ${{ contains(fromJSON('["tefca-viewer"]'), matrix.container-to-build) }} - with: - ref: ${{ inputs.container-tag }} - images: ghcr.io/${{ github.repository }}/tefca-viewer-flyway - # this sets the version for tags and labels for each of the containers to be - # be the same as the version/tag where the code was pulled from - tags: | - type=semver,pattern={{raw}},value=${{ inputs.container-tag }} - type=ref,event=branch - type=ref,event=tag,pattern={{raw}},value=${{ inputs.container-tag }} - labels: | - org.opencontainers.image.version=${{ inputs.container-tag }} - - name: Build and push uses: docker/build-push-action@v3 if: ${{ !contains(fromJSON('["ecr-viewer", "tefca-viewer"]'), matrix.container-to-build) }} @@ -85,18 +69,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - - name: Build Flyway Container - uses: docker/build-push-action@v3 - if: ${{ contains(fromJSON('["tefca-viewer"]'), matrix.container-to-build) }} - with: - context: . - file: ./containers/${{ matrix.container-to-build }}/Dockerfile.flyway - push: true - tags: ${{ steps.flyway-meta.outputs.tags }} - labels: ${{ steps.flyway-meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - name: Build and push with shared-resources uses: docker/build-push-action@v3 if: ${{ contains(fromJSON('["ecr-viewer", "tefca-viewer"]'), matrix.container-to-build) }} diff --git a/.github/workflows/container-tefca-viewer.yaml b/.github/workflows/container-tefca-viewer.yaml index 29624cae74..ef734f4e43 100644 --- a/.github/workflows/container-tefca-viewer.yaml +++ b/.github/workflows/container-tefca-viewer.yaml @@ -75,14 +75,6 @@ jobs: push: false cache-from: type=gha cache-to: type=gha,mode=max - - name: Build ${{ env.CONTAINER }} Flyway Container - uses: docker/build-push-action@v3 - with: - context: . - file: ./containers/${{ env.CONTAINER }}/Dockerfile.flyway - push: false - cache-from: type=gha - cache-to: type=gha,mode=max playwright-tests: timeout-minutes: 10 @@ -96,10 +88,16 @@ jobs: node-version: ${{env.NODE_VERSION}} - name: Install dependencies working-directory: ./containers/${{env.CONTAINER}} - run: npm install + run: npm ci - name: Install Playwright Browsers working-directory: ./containers/${{env.CONTAINER}} run: npx playwright install --with-deps - - name: Run Playwright tests + - name: Build Query Connector + working-directory: ./containers/${{env.CONTAINER}} + run: docker compose build --no-cache + - name: Run Query Connector + working-directory: ./containers/${{env.CONTAINER}} + run: docker compose up -d + - name: Playwright Tests working-directory: ./containers/${{env.CONTAINER}} - run: npx playwright test + run: npx playwright test e2e --reporter=list --config playwright.config.ts diff --git a/containers/tefca-viewer/Dockerfile b/containers/tefca-viewer/Dockerfile index fc3d642b93..c42f778c8a 100644 --- a/containers/tefca-viewer/Dockerfile +++ b/containers/tefca-viewer/Dockerfile @@ -1,11 +1,11 @@ FROM node:18-alpine AS base FROM base AS builder -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. +# Install necessary packages for building RUN apk update RUN apk add --no-cache libc6-compat -# Set working directory +# Set working directory WORKDIR /app RUN npm i -g turbo COPY . . @@ -18,6 +18,20 @@ RUN apk update RUN apk add --no-cache libc6-compat WORKDIR /app +ENV FLYWAY_VERSION=10.19.0 + +# Add bash to enable running Flyway +RUN apk add --no-cache bash curl + +# Install Flyway and remove JRE directory to force flyway to use the openjdk11 version in the runner +RUN curl -L https://repo1.maven.org/maven2/org/flywaydb/flyway-commandline/${FLYWAY_VERSION}/flyway-commandline-${FLYWAY_VERSION}-linux-x64.tar.gz -o flyway.tar.gz \ + && tar -zxvf flyway.tar.gz \ + && mv flyway-${FLYWAY_VERSION} /flyway \ + && ln -s /flyway/flyway /usr/local/bin/flyway \ + && rm flyway.tar.gz \ + && rm -rf /flyway/jre \ + && chmod +x /flyway/flyway + # First install the dependencies COPY .gitignore .gitignore COPY --from=builder /app/out/json/ . @@ -32,9 +46,18 @@ ENV TURBO_TELEMETRY_DISABLED=1 RUN npx turbo build --filter=tefca-viewer... +# Final stage for running the app FROM base AS runner WORKDIR /app +RUN apk add --no-cache bash openjdk17-jre + +# Copy Flyway from the installer stage +COPY --from=installer /flyway /flyway +RUN chmod +x /flyway/flyway +# Create symlink to run Flyway from anywhere in the container +RUN ln -s /flyway/flyway /usr/local/bin/flyway + # Don't run production as root RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs @@ -43,17 +66,24 @@ USER nextjs # Set hostname to localhost ENV HOSTNAME "0.0.0.0" +# Copy necessary app files COPY --from=installer /app/containers/tefca-viewer/next.config.js . COPY --from=installer /app/containers/tefca-viewer/package.json . +COPY --from=installer /app/containers/tefca-viewer/flyway/conf/flyway.conf /flyway/conf/flyway.conf +COPY --from=installer /app/containers/tefca-viewer/flyway/sql /flyway/sql # Automatically leverage output traces to reduce image size -# https://nextjs.org/docs/advanced-features/output-file-tracing COPY --from=installer --chown=nextjs:nodejs /app/containers/tefca-viewer/.next/standalone ./ COPY --from=installer --chown=nextjs:nodejs /app/containers/tefca-viewer/.next/static ./containers/tefca-viewer/.next/static COPY --from=installer --chown=nextjs:nodejs /app/containers/tefca-viewer/public ./containers/tefca-viewer/public +# Set environment variables for Flyway and Node.js telemetry ENV TURBO_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1 +# Set JAVA_HOME to the OpenJDK version installed by apk for Flyway +ENV JAVA_HOME=/usr/lib/jvm/default-jvm +# Add the OpenJDK to the PATH so the java command is available for Flways +ENV PATH=$JAVA_HOME/bin:$PATH -CMD ["node", "containers/tefca-viewer/server.js"] +CMD ["sh", "-c","flyway -configFiles=/flyway/conf/flyway.conf -schemas=public -connectRetries=60 migrate && echo done with flyway && node containers/tefca-viewer/server.js"] \ No newline at end of file diff --git a/containers/tefca-viewer/Dockerfile.flyway b/containers/tefca-viewer/Dockerfile.flyway deleted file mode 100644 index e88c03eb3a..0000000000 --- a/containers/tefca-viewer/Dockerfile.flyway +++ /dev/null @@ -1,5 +0,0 @@ -FROM flyway/flyway:10.16-alpine - -COPY ./containers/tefca-viewer/flyway /flyway - - diff --git a/containers/tefca-viewer/docker-compose-dev.yaml b/containers/tefca-viewer/docker-compose-dev.yaml index e1eefd1ae2..edefbaf7ce 100644 --- a/containers/tefca-viewer/docker-compose-dev.yaml +++ b/containers/tefca-viewer/docker-compose-dev.yaml @@ -8,6 +8,8 @@ services: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=pw - POSTGRES_DB=tefca_db + # Note: you must have a local .env file with the DATABASE_URL set to the following: + # DATABASE_URL=postgresql://postgres:pw@localhost:5432/tefca_db healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 2s diff --git a/containers/tefca-viewer/docker-compose.yaml b/containers/tefca-viewer/docker-compose.yaml index a134338a77..2651ae7021 100644 --- a/containers/tefca-viewer/docker-compose.yaml +++ b/containers/tefca-viewer/docker-compose.yaml @@ -14,23 +14,13 @@ services: timeout: 5s retries: 20 - # Flyway migrations and DB version control - flyway: - image: flyway/flyway:10.16-alpine - command: -configFiles=/flyway/conf/flyway.conf -schemas=public -connectRetries=60 migrate - volumes: - - ./flyway/sql:/flyway/sql - - ./flyway/conf/flyway.conf:/flyway/conf/flyway.conf - depends_on: - db: - condition: service_started - - # Next.js app + # Next.js app with Flyway tefca-viewer: platform: linux/amd64 build: context: ../../ dockerfile: ./containers/tefca-viewer/Dockerfile + tty: true ports: - "3000:3000" environment: @@ -39,5 +29,3 @@ services: depends_on: db: condition: service_healthy - flyway: - condition: service_completed_successfully diff --git a/containers/tefca-viewer/e2e/query_workflow.spec.ts b/containers/tefca-viewer/e2e/query_workflow.spec.ts index eafafe14df..21035004df 100644 --- a/containers/tefca-viewer/e2e/query_workflow.spec.ts +++ b/containers/tefca-viewer/e2e/query_workflow.spec.ts @@ -1,6 +1,5 @@ // @ts-check -import { RETURN_TO_STEP_ONE_LABEL } from "@/app/query/components/PatientSearchResults"; import { test, expect } from "@playwright/test"; test.describe("querying with the TryTEFCA viewer", () => { @@ -30,6 +29,31 @@ test.describe("querying with the TryTEFCA viewer", () => { ).toBeVisible(); }); + //TODO: Add this test back in once you no longer have to click to select the query from the dropdown + // test("unsuccessful user query: no patients", async ({ page }) => { + // await page.getByRole("button", { name: "Go to the demo" }).click(); + // await page + // .getByLabel("Query", { exact: true }) + // .selectOption("social-determinants"); + // await page.getByRole("button", { name: "Fill fields" }).click(); + + // await page.getByLabel("First Name").fill("Ellie"); + // await page.getByLabel("Last Name").fill("Williams"); + // await page.getByLabel("Phone Number").fill("5555555555"); + // await page.getByLabel("Medical Record Number").fill("TLOU1TLOU2"); + // await page.getByRole("button", { name: "Search for patient" }).click(); + + // // Better luck next time, user! + // await expect( + // page.getByRole("heading", { name: "No Patients Found" }) + // ).toBeVisible(); + // await expect(page.getByText("There are no patient records")).toBeVisible(); + // await page.getByRole("link", { name: "Search for a new patient" }).click(); + // await expect( + // page.getByRole("heading", { name: "Search for a Patient", exact: true }) + // ).toBeVisible(); + // }); + test("successful demo user query: the quest for watermelon mcgee", async ({ page, }) => { @@ -92,35 +116,6 @@ test.describe("querying with the TryTEFCA viewer", () => { ).toBeVisible(); }); - test("unsuccessful user query: no patients", async ({ page }) => { - await page.getByRole("button", { name: "Go to the demo" }).click(); - await page - .getByLabel("Query", { exact: true }) - .selectOption("social-determinants"); - await page.getByRole("button", { name: "Advanced" }).click(); - await page - .getByLabel("FHIR Server (QHIN)", { exact: true }) - .selectOption("HELIOS Meld: Direct"); - - await page.getByLabel("First Name").fill("Ellie"); - await page.getByLabel("Last Name").fill("Williams"); - await page.getByLabel("Date of Birth").fill("2019-07-07"); - await page.getByLabel("Medical Record Number").fill("TLOU1TLOU2"); - await page.getByRole("button", { name: "Search for patient" }).click(); - - // Better luck next time, user! - await expect( - page.getByRole("heading", { name: "No Records Found" }), - ).toBeVisible(); - await expect( - page.getByText("No records were found for your search"), - ).toBeVisible(); - await page.getByText(RETURN_TO_STEP_ONE_LABEL).click(); - await expect( - page.getByRole("heading", { name: "Search for a Patient", exact: true }), - ).toBeVisible(); - }); - test("query using form-fillable demo patient by phone number", async ({ page, }) => { diff --git a/containers/tefca-viewer/flyway/sql/V01_01__tcr_custom_query_schema.sql b/containers/tefca-viewer/flyway/sql/V01_01__tcr_custom_query_schema.sql index 64628f5370..90274e55a2 100644 --- a/containers/tefca-viewer/flyway/sql/V01_01__tcr_custom_query_schema.sql +++ b/containers/tefca-viewer/flyway/sql/V01_01__tcr_custom_query_schema.sql @@ -1,4 +1,3 @@ -BEGIN; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE TABLE IF NOT EXISTS conditions ( @@ -81,6 +80,7 @@ CREATE TABLE IF NOT EXISTS query_included_concepts ( -- Create indexes for all primary and foreign keys CREATE INDEX IF NOT EXISTS conditions_id_index ON conditions (id); +CREATE INDEX IF NOT EXISTS conditions_name_index ON conditions (name); CREATE INDEX IF NOT EXISTS valuesets_id_index ON valuesets (id); @@ -97,6 +97,7 @@ CREATE INDEX IF NOT EXISTS valueset_to_concept_concept_id_index ON valueset_to_c CREATE INDEX IF NOT EXISTS icd_crosswalk_id_index ON icd_crosswalk (id); CREATE INDEX IF NOT EXISTS query_id_index ON query (id); +CREATE INDEX IF NOT EXISTS query_name_index ON query (query_name); CREATE INDEX IF NOT EXISTS query_to_valueset_id_index ON query_to_valueset (id); CREATE INDEX IF NOT EXISTS query_to_valueset_query_id_index ON query_to_valueset (query_id); @@ -105,5 +106,3 @@ CREATE INDEX IF NOT EXISTS query_to_valueset_valueset_id_index ON query_to_value CREATE INDEX IF NOT EXISTS query_included_concepts_id_index ON query_included_concepts (id); CREATE INDEX IF NOT EXISTS query_included_concepts_query_by_valueset_id_index ON query_included_concepts (query_by_valueset_id); CREATE INDEX IF NOT EXISTS query_included_concepts_concept_id_index ON query_included_concepts (concept_id); - -COMMIT; \ No newline at end of file diff --git a/containers/tefca-viewer/flyway/sql/V01_02__load_tcr_data.sql b/containers/tefca-viewer/flyway/sql/V01_02__load_tcr_data.sql index 8dc672197d..760a1c22e5 100644 --- a/containers/tefca-viewer/flyway/sql/V01_02__load_tcr_data.sql +++ b/containers/tefca-viewer/flyway/sql/V01_02__load_tcr_data.sql @@ -1,4 +1,3 @@ -BEGIN; INSERT INTO valuesets VALUES('2.16.840.1.113762.1.4.1146.6_20230602','2.16.840.1.113762.1.4.1146.6','20230602','Diphtheria (Disorders) (SNOMED)','CSTE Steward','dxtc'); INSERT INTO valuesets VALUES('2.16.840.1.113762.1.4.1146.2030_20230125','2.16.840.1.113762.1.4.1146.2030','20230125','Bartonella henselae infection (Disorders) (SNOMED)','CSTE Steward','dxtc'); @@ -175313,8 +175312,3 @@ INSERT INTO icd_crosswalk VALUES('81590','Z993','V463','00000'); INSERT INTO icd_crosswalk VALUES('81591','Z9981','V462','00000'); INSERT INTO icd_crosswalk VALUES('81592','Z9989','V468','10000'); INSERT INTO icd_crosswalk VALUES('81593','Z9989','V469','10000'); -COMMIT; - - - - diff --git a/containers/tefca-viewer/flyway/sql/V01_03__insert_hardcoded_values.sql b/containers/tefca-viewer/flyway/sql/V01_03__insert_hardcoded_values.sql index 70d1fcd29a..4a7ad06696 100644 --- a/containers/tefca-viewer/flyway/sql/V01_03__insert_hardcoded_values.sql +++ b/containers/tefca-viewer/flyway/sql/V01_03__insert_hardcoded_values.sql @@ -1,5 +1,3 @@ -BEGIN; - INSERT INTO conditions VALUES('1','DIBBs Local Code System','Newborn Screening','20240909'); INSERT INTO conditions VALUES('2','DIBBs Local Code System','Cancer (Leukemia)','20240909'); INSERT INTO conditions VALUES('3','DIBBs Local Code System','Social Determinants of Health','20240909'); -- has no valuesets @@ -157,7 +155,3 @@ INSERT INTO valueset_to_concept VALUES('45314','14_20240923','1_1255068005'); INSERT INTO valueset_to_concept VALUES('45315','14_20240923','1_9484.100004300'); INSERT INTO valueset_to_concept VALUES('45316','15_20240923','1_363346000'); INSERT INTO valueset_to_concept VALUES('45317','2_20240909','1_418689008'); - - - -COMMIT; diff --git a/containers/tefca-viewer/flyway/sql/V01_04__insert_tcr_default_queries.sql b/containers/tefca-viewer/flyway/sql/V01_04__insert_tcr_default_queries.sql index 15286a9336..91576ec2cb 100644 --- a/containers/tefca-viewer/flyway/sql/V01_04__insert_tcr_default_queries.sql +++ b/containers/tefca-viewer/flyway/sql/V01_04__insert_tcr_default_queries.sql @@ -1,4 +1,3 @@ -BEGIN; -- Select data from the TCR that will be used to create default queries WITH tcr_data AS ( SELECT @@ -57,6 +56,3 @@ select from qic_data join valueset_to_concept vtc on qic_data.valueset_id = vtc.valueset_id; - -COMMIT; - diff --git a/containers/tefca-viewer/package.json b/containers/tefca-viewer/package.json index 6b27a54e95..6e797e9917 100644 --- a/containers/tefca-viewer/package.json +++ b/containers/tefca-viewer/package.json @@ -4,9 +4,9 @@ "version": "1.0.1", "private": true, "scripts": { - "dev": "docker-compose -f docker-compose-dev.yaml up -d && next dev", - "dev-win": "start docker-compose -f docker-compose-dev.yaml up && next dev", - "dev:db": "docker-compose -f docker-compose-dev.yaml up", + "dev": "docker compose -f docker-compose-dev.yaml up -d && next dev", + "dev-win": "start docker compose -f docker-compose-dev.yaml up && next dev", + "dev:db": "docker compose -f docker-compose-dev.yaml up", "dev:next": "dotenv -e ./tefca.env.local -e ./tefca.env -- next dev", "setup-local-env": "./setup-env.sh", "build": "next build && cp -r .next/static .next/standalone/.next && cp -r public .next/standalone/", @@ -16,6 +16,7 @@ "test:unit": "jest --testPathPattern=tests/unit", "test:unit:watch": "jest --watch", "test:integration": "jest --testPathPattern=tests/integration", + "test:playwright": "docker compose build --no-cache && docker compose up -d && npx playwright test --reporter=list", "cypress:open": "cypress open", "cypress:run": "cypress run" }, diff --git a/containers/tefca-viewer/playwright-setup.ts b/containers/tefca-viewer/playwright-setup.ts index 10ff884a2b..a41940cf7c 100644 --- a/containers/tefca-viewer/playwright-setup.ts +++ b/containers/tefca-viewer/playwright-setup.ts @@ -3,12 +3,33 @@ */ async function globalSetup() { const url = "http://localhost:3000/tefca-viewer"; + const maxRetries = 300; // Maximum number of retries + const delay = 1000; // Delay between retries in milliseconds - const response = await fetch(url); - while (response.status !== 200) { - console.log(`Failed to connect to ${url}`); - const response = await fetch(url); + for (let attempts = 0; attempts < maxRetries; attempts++) { + try { + const response = await fetch(url); // Fetch the URL + if (response.status === 200) { + console.log(`Connected to ${url} successfully.`); + return; // Exit the function if the webpage loads successfully + } else { + console.log( + `Failed to connect to ${url}, status: ${response.status}. Retrying...`, + ); + // Wait before the next attempt + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } catch (error) { + console.log( + `Fetch failed for ${url}: ${(error as Error).message}. Retrying...`, + ); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + // Wait before the next attempt + await new Promise((resolve) => setTimeout(resolve, delay)); } + + throw new Error(`Unable to connect to ${url} after ${maxRetries} attempts.`); } export default globalSetup; diff --git a/containers/tefca-viewer/playwright.config.ts b/containers/tefca-viewer/playwright.config.ts index db7aab65dd..a33bd85dd5 100644 --- a/containers/tefca-viewer/playwright.config.ts +++ b/containers/tefca-viewer/playwright.config.ts @@ -18,7 +18,7 @@ export default defineConfig({ forbidOnly: !!process.env.CI, /* Retry on CI only */ retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 4 : undefined, + workers: process.env.CI ? 1 : 1, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "html", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ @@ -68,14 +68,6 @@ export default defineConfig({ // }, ], - /* Run your local dev server before starting the tests */ - webServer: { - command: "docker compose build --no-cache && docker compose up", - port: 3000, - timeout: 300 * 1000, - reuseExistingServer: !process.env.CI, - }, - /* Hook to ensure Docker is shut down after tests or on error */ globalTeardown: "./playwright-teardown", /* Hook to ensure DB is started & migrations have run before tests start*/ diff --git a/containers/tefca-viewer/setup-env.sh b/containers/tefca-viewer/setup-env.sh index dba23e6ed4..c505ad99a0 100755 --- a/containers/tefca-viewer/setup-env.sh +++ b/containers/tefca-viewer/setup-env.sh @@ -7,4 +7,5 @@ if [ ! -f .env.local ]; then echo ".env.local was created from .env" else echo ".env.local already exists" -fi \ No newline at end of file +fi + diff --git a/containers/tefca-viewer/src/app/database-service.ts b/containers/tefca-viewer/src/app/database-service.ts index 2a1414e6bb..af6832976d 100644 --- a/containers/tefca-viewer/src/app/database-service.ts +++ b/containers/tefca-viewer/src/app/database-service.ts @@ -13,8 +13,7 @@ select q.query_name, q.id, qtv.valueset_id, vs.name as valueset_name, vs.author where q.query_name = $1; `; -// Load environment variables from tefca.env and establish a Pool configuration -// dotenv.config({ path: "tefca.env" }); +// Load environment variables from .env and establish a Pool configuration const dbConfig: PoolConfig = { connectionString: process.env.DATABASE_URL, max: 10, // Maximum # of connections in the pool