diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
new file mode 100644
index 00000000..02eb140d
--- /dev/null
+++ b/.github/copilot-instructions.md
@@ -0,0 +1,10 @@
+# Code style
+
+- Do not add jsdoc comments (/** ... */); TypeScript typing is sufficient.
+- Use C block syntax (/* ... */) for method-level comments.
+- Use C++ style comments (// ...) for inline comments (within method body).
+- Nullable or boolean arguments are code smells; prefer configuration objects as arguments.
+- Avoid casts and check if a cast is really necessary; do not use unnecessary casts.
+- Always remove legacy and unreachable code.
+- Avoid creating overly complex or large methods/modules; split into smaller, manageable functions with clear naming.
+- Comment unclear code sections with C block comments explaining the reason for the code and, when applicable, the input and output produced.
diff --git a/.github/workflows/code_review_infra_exp.yaml b/.github/workflows/code_review_infra_exp.yaml
deleted file mode 100644
index 49a9aa04..00000000
--- a/.github/workflows/code_review_infra_exp.yaml
+++ /dev/null
@@ -1,264 +0,0 @@
-name: Infra Code Review
-
-on:
- workflow_dispatch:
- pull_request:
- types:
- - opened
- - synchronize
- - reopened
- - ready_for_review
- paths:
- # Trigger the workflow when source files that generate terraform are modified
- - "**/opex.ts"
- - "**/openapi*.yaml"
- - "**/openapi/**/*.yaml"
- # Trigger the workflow when the involved workflows are modified
- - ".github/workflows/code_review_infra_exp.yaml"
-
-permissions:
- contents: read
- pull-requests: write
- id-token: write
-
-jobs:
- setup_and_detect:
- runs-on: ubuntu-latest
- outputs:
- environments: ${{ steps.detect.outputs.environments }}
- env:
- TURBO_CACHE_DIR: .turbo-cache
- steps:
- - name: Checkout
- uses: actions/checkout@v4
-
- - name: Setup yarn
- shell: bash
- run: corepack enable
-
- - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
- with:
- node-version-file: ".node-version"
- cache: yarn
- cache-dependency-path: "yarn.lock"
-
- - name: Setup turbo cache
- if: ${{ env.TURBO_CACHE_DIR != '' }}
- uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
- with:
- path: ./${{ env.TURBO_CACHE_DIR }}
- key: ${{ runner.os }}-turbo-${{ github.job }}-${{ github.sha }}
- restore-keys: |
- ${{ runner.os }}-turbo-${{ github.job }}-
-
- - name: Install dependencies
- run: yarn install --immutable
-
- - name: Generate Terraform code
- run: |
- yarn turbo run infra:generate
-
- - name: Verify generated files
- run: |
- echo "Generated Terraform files:"
- git ls-files --others '*.tf.json' || echo "No .tf.json files found"
-
- - name: Upload generated terraform files
- uses: actions/upload-artifact@v4
- with:
- name: terraform-files
- path: |
- **/*.tf.json
- retention-days: 1
-
- - name: Detect environments
- id: detect
- run: |
- # Fetch main branch for comparison
- git fetch origin main:main 2>/dev/null || git fetch origin main 2>/dev/null || true
-
- # Get changed files compared to main branch (same logic for both PR and manual dispatch)
- if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
- changed_files=$(git diff --name-only main...HEAD 2>/dev/null || git diff --name-only HEAD^ 2>/dev/null || echo "")
- else
- changed_files=$(git diff --name-only main...HEAD 2>/dev/null || gh pr diff ${{ github.event.number }} --name-only 2>/dev/null || echo "")
- fi
-
- if [ -z "$changed_files" ]; then
- environments="[]"
- else
- # Find directories that meet the criteria
- environments=$(echo "$changed_files" | while read file; do
- if [ -n "$file" ]; then
- dir=$(dirname "$file")
-
- # Check if the file is a .tf file that was modified
- if [[ "$file" =~ \.tf$ ]]; then
- echo "$dir"
- # Check if the directory contains cdk.tf.json and has any modified files
- elif [ -f "$dir/cdk.tf.json" ]; then
- echo "$dir"
- fi
- fi
- done | \
- sort -u | \
- while read dir; do
- # Filter out directories that start with underscore or are contained in underscore directories
- if [[ ! "$(basename "$dir")" =~ ^_ ]] && [[ ! "$dir" =~ /_[^/]*/ ]] && [[ ! "$dir" =~ /\._/ ]]; then
- echo "$dir"
- fi
- done | \
- sort -u | \
- jq -R -s -c 'split("\n")[:-1] | map(select(length > 0))')
- fi
-
- echo "environments=$environments" >> $GITHUB_OUTPUT
- echo "Found environments with changes: $environments"
- env:
- GH_TOKEN: ${{ github.token }}
-
- code_review:
- needs: [setup_and_detect]
- if: needs.setup_and_detect.outputs.environments != '[]'
- strategy:
- matrix:
- environment: ${{ fromJson(needs.setup_and_detect.outputs.environments) }}
- fail-fast: false
- name: Code Review Infra Plan (${{ matrix.environment }})
- runs-on: ubuntu-latest
- environment: infra-dev-ci
- concurrency:
- group: ${{ github.workflow }}-${{ matrix.environment }}-infra/resources-ci
- cancel-in-progress: false
- permissions:
- id-token: write
- contents: read
- pull-requests: write
- env:
- ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
- ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
- ARM_USE_OIDC: true
- ARM_USE_AZUREAD: true
- ARM_STORAGE_USE_AZUREAD: true
- ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- steps:
- - name: Set directory
- id: directory
- env:
- ENVIRONMENT: ${{ matrix.environment }}
- run: |
- set -euo pipefail
- if [ -z "$ENVIRONMENT" ]; then
- echo "Environment must be provided."
- exit 1
- else
- printf "dir=%q" "$ENVIRONMENT" >> "$GITHUB_OUTPUT"
- fi
-
- - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- name: Checkout
-
- - name: Download generated terraform files
- uses: actions/download-artifact@v4
- with:
- name: terraform-files
- path: infra/
-
- - name: Azure Login
- uses: pagopa/dx/.github/actions/azure-login@main
-
- - name: Terraform Setup
- id: set-terraform-version
- uses: pagopa/dx/.github/actions/terraform-setup@main
-
- - name: Terraform Init
- working-directory: ${{ steps.directory.outputs.dir }}
- run: |
- terraform init
-
- - name: Terraform Plan
- id: plan
- working-directory: ${{ steps.directory.outputs.dir }}
- run: |
- terraform plan -lock-timeout=3000s -no-color -out=plan.out 2>&1 | grep -v "hidden-link:" | tee tf_plan_stdout.txt
- terraform show -no-color plan.out > full_plan.txt
-
- # Extracts only the diff section from the Plan by skipping everything before the resource changes,
- # and filters out non-essential log lines like state refreshes and reads.
- if [ -s full_plan.txt ]; then
- sed -n '/^ #/,$p' full_plan.txt | grep -Ev "Refreshing state|state lock|Reading|Read" > filtered_plan.txt || echo "No changes detected." > filtered_plan.txt
- else
- echo "No plan output available." > filtered_plan.txt
- fi
-
- # The summary with number of resources to be added, changed, or destroyed (will be used in case the plan output is too long)
- SUMMARY_LINE=$(grep -E "^Plan: [0-9]+ to add" tf_plan_stdout.txt || echo "No changes.")
-
- echo "$SUMMARY_LINE" > plan_summary.txt
-
- # If the filtered plan is too long use the summary line, otherwise use the full filtered plan
- if [ "$(wc -c < filtered_plan.txt)" -gt 60000 ]; then
- echo "${SUMMARY_LINE}" > plan_output_multiline.txt
- echo "" >> plan_output_multiline.txt
- echo "Full plan output was too long and was omitted. Check the workflow logs for full details." >> plan_output_multiline.txt
- else
- cat filtered_plan.txt > plan_output_multiline.txt
- fi
-
- # Error detection based on tf_plan_stdout.txt content
- if grep -q "::error::Terraform exited with code" tf_plan_stdout.txt; then
- echo "failed"
- exit 1
- fi
-
- - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
- name: Post Plan on PR
- id: comment
- if: always() && github.event_name == 'pull_request'
- env:
- OUTPUT_DIR: ${{ steps.directory.outputs.dir }}
- PLAN_STATUS: ${{ steps.plan.outcome }}
- with:
- script: |
- const fs = require('fs');
- const outputDir = process.env.OUTPUT_DIR;
- const output = fs.readFileSync(`${outputDir}/plan_output_multiline.txt`, 'utf8');
- const status = process.env.PLAN_STATUS;
- const { data: comments } = await github.rest.issues.listComments({
- owner: context.repo.owner,
- repo: context.repo.repo,
- issue_number: context.issue.number
- })
- const botComment = comments.find(comment => {
- return comment.user.type === 'Bot' && comment.body.includes(`Terraform Plan (${outputDir})`)
- })
- const commentBody = `#### 📖 Terraform Plan (${outputDir}) - ${status}
-
- Terraform Plan
-
- \`\`\`hcl
- ${output}
- \`\`\`
-
-
- `;
- if (botComment) {
- await github.rest.issues.deleteComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- comment_id: botComment.id
- })
- }
-
- await github.rest.issues.createComment({
- owner: context.repo.owner,
- repo: context.repo.repo,
- body: commentBody,
- issue_number: context.issue.number
- })
-
- - name: Check Terraform Plan Result
- if: always() && steps.plan.outcome != 'success'
- run: |
- exit 1
diff --git a/.github/workflows/code_review_infra_opex.yaml b/.github/workflows/code_review_infra_opex.yaml
new file mode 100644
index 00000000..69b5572a
--- /dev/null
+++ b/.github/workflows/code_review_infra_opex.yaml
@@ -0,0 +1,67 @@
+name: Infra OPEX Code Review
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+ - reopened
+ - ready_for_review
+ paths:
+ # Trigger the workflow when source files that generate terraform are modified
+ - "test-opex-api/**/opex.ts"
+ - "test-opex-api/**/openapi/*.yaml"
+ # - "**/openapi*.yaml"
+ # - "**/openapi/**/*.yaml"
+ # Trigger the workflow when the involved workflows are modified
+ - ".github/workflows/code_review_infra_opex.yaml"
+
+permissions:
+ contents: read
+ pull-requests: write
+ id-token: write
+
+jobs:
+ generate_infra:
+ runs-on: ubuntu-latest
+ name: Generate Infra OPEX
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+
+ - name: Corepack Enable
+ run: corepack enable
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v3
+ with:
+ node-version: "20"
+ cache: "yarn"
+
+ - name: Install dependencies
+ run: yarn install
+
+ - name: Build dependencies
+ run: yarn build
+
+ - name: Generate infra for test-opex-api
+ run: yarn run generate:infra --workspace=test-opex-api
+
+ - name: Upload generated infra artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: generated-infra-opex
+ path: apps/test-opex-api/stacks/
+
+ code_review_dev:
+ needs: generate_infra
+ uses: pagopa/dx-playground/.github/workflows/infra_plan_opex.yaml@chores/opex-refactor-hex
+ name: Code Review Infra OPEX Plan
+ secrets: inherit
+ with:
+ environment: opex-dashboard
+ override_github_environment: "infra-dev"
+ base_path: test-opex-api/stacks
+ artifact_name: generated-infra-opex
diff --git a/.github/workflows/infra_plan_opex.yaml b/.github/workflows/infra_plan_opex.yaml
new file mode 100644
index 00000000..1657bbc2
--- /dev/null
+++ b/.github/workflows/infra_plan_opex.yaml
@@ -0,0 +1,273 @@
+on:
+ workflow_call:
+ inputs:
+ environment:
+ description: Environment where the resources will be deployed.
+ type: string
+ required: true
+ base_path:
+ description: The base path on which the script will look for Terraform projects
+ type: string
+ required: true
+ artifact_name:
+ description: Name of the artifact containing generated infra files to download
+ type: string
+ required: false
+ env_vars:
+ description: List of environment variables to set up, given in env=value format.
+ type: string
+ required: false
+ use_private_agent:
+ description: Use a private agent to run the Terraform plan.
+ type: boolean
+ required: false
+ default: false
+ override_github_environment:
+ description: Set a value if GitHub Environment name is different than the TF environment folder
+ type: string
+ required: false
+ default: ""
+ use_labels:
+ description: Use labels to start the right environment's GitHub runner. If use_labels is true, also use_private_agent must be set to true
+ type: boolean
+ required: false
+ default: false
+ override_labels:
+ description: Needed for special cases where the environment alone is not sufficient as a distinguishing label
+ type: string
+ required: false
+ default: ""
+
+env:
+ ARM_SUBSCRIPTION_ID: ${{ secrets.ARM_SUBSCRIPTION_ID }}
+ ARM_TENANT_ID: ${{ secrets.ARM_TENANT_ID }}
+ ARM_USE_OIDC: true
+ ARM_USE_AZUREAD: true
+ ARM_STORAGE_USE_AZUREAD: true
+ TF_IN_AUTOMATION: true
+ REFRESH_STATE_EVERY_SECONDS: 600 # 10 minutes
+
+jobs:
+ get-terraform-version:
+ name: Get Terraform Version
+ runs-on: ubuntu-latest
+ outputs:
+ terraform_version: ${{ steps.get-version.outputs.terraform_version }}
+ steps:
+ - name: Checkout
+ uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.7
+
+ - name: Get terraform version from .terraform-version file
+ id: get-version
+ uses: pagopa/dx/.github/actions/get-terraform-version@main
+ with:
+ default_version: "1.10.4"
+
+ tf-modules-check:
+ uses: pagopa/dx/.github/workflows/static_analysis.yaml@main
+ name: Check terraform registry modules hashes
+ needs: [get-terraform-version]
+ secrets: inherit
+ with:
+ terraform_version: ${{ needs.get-terraform-version.outputs.terraform_version }}
+ check_to_run: lock_modules
+ folder: ${{ inputs.base_path }}/${{ inputs.environment }}/
+ verbose: true
+
+ tf_plan:
+ name: "Terraform Plan"
+ needs: [get-terraform-version, tf-modules-check]
+ # Use inputs.override_labels if set; otherwise, fall back to inputs.environment.
+ # When inputs.use_labels and inputs.use_private_agent are true, apply the selected labels.
+ # Default to 'self-hosted' if inputs.use_private_agent is true, or 'ubuntu-latest' otherwise.
+ runs-on: ${{ inputs.use_labels && inputs.use_private_agent && (inputs.override_labels != '' && inputs.override_labels || inputs.environment) || inputs.use_private_agent && 'self-hosted' || 'ubuntu-latest' }}
+ environment: ${{ inputs.override_github_environment == '' && inputs.environment || inputs.override_github_environment}}-ci
+ concurrency:
+ group: ${{ github.workflow }}-${{ inputs.environment }}-${{ inputs.base_path }}-ci
+ cancel-in-progress: false
+ permissions:
+ id-token: write
+ contents: read
+ pull-requests: write
+ env:
+ ARM_CLIENT_ID: ${{ secrets.ARM_CLIENT_ID }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ steps:
+ # Set the directory where the Terraform files are located
+ # The directory the value is then available in ${{ steps.directory.outputs.dir }}
+ - name: Set directory
+ id: directory
+ env:
+ ENVIRONMENT: ${{ inputs.environment }}
+ BASE_PATH: ${{ inputs.base_path }}
+ run: |
+ set -euo pipefail
+
+ if [ -z "$ENVIRONMENT" ]; then
+ echo "Environment must be provided."
+ exit 1
+ else
+ # The directory is expected to be in the format
+ # infra/resources/$ENVIRONMENT
+ # Example: infra/resources/prod
+ printf "dir=%q/%q" "$BASE_PATH" "$ENVIRONMENT" >> "$GITHUB_OUTPUT"
+ fi
+
+ - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
+ name: Checkout
+
+ - name: Download generated infra artifacts
+ if: ${{ inputs.artifact_name }}
+ uses: actions/download-artifact@v4
+ with:
+ name: ${{ inputs.artifact_name }}
+ path: ${{ inputs.base_path }}
+
+ - name: Set Environment Variables
+ if: ${{ inputs.env_vars }}
+ env:
+ ENV_VARS: ${{ inputs.env_vars }}
+ run: |
+ set -euo pipefail
+
+ for i in "$ENV_VARS[@]"
+ do
+ printf "%q\n" "$i" >> "$GITHUB_ENV"
+ done
+
+ - name: Azure Login
+ uses: pagopa/dx/.github/actions/azure-login@main
+
+ - name: Terraform Setup
+ id: set-terraform-version
+ uses: pagopa/dx/.github/actions/terraform-setup@main
+ with:
+ terraform_version: ${{ needs.get-terraform-version.outputs.terraform_version }}
+
+ - name: Terraform Init
+ working-directory: ${{ steps.directory.outputs.dir }}
+ run: |
+ terraform init -input=false
+
+ - name: Generate Cache Key
+ if: github.event_name == 'pull_request'
+ id: cache-key
+ env:
+ PR_NUMBER: ${{ github.event.pull_request.number }}
+ run: |
+ # Get current epoch time rounded to chosen window
+ TIME_WINDOW=$(( $(date +%s) / $REFRESH_STATE_EVERY_SECONDS ))
+ echo "cache-key=tfplan-pr${PR_NUMBER}-${TIME_WINDOW}s" >> $GITHUB_OUTPUT
+
+ # Restore PR TFPlan Cache (miss = first run or >10 minutes, hit = within 10 minutes)
+ - name: Check for Cache
+ if: github.event_name == 'pull_request'
+ id: tfplan-pr-cache
+ uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
+ with:
+ path: ${{ steps.directory.outputs.dir }}/.pr_plan_marker
+ key: ${{ steps.cache-key.outputs.cache-key }}
+
+ - name: Set Terraform Refresh Flag
+ if: github.event_name == 'pull_request'
+ working-directory: ${{ steps.directory.outputs.dir }}
+ env:
+ CACHE_HIT: ${{ steps.tfplan-pr-cache.outputs.cache-hit }}
+ run: |
+ if [ "$CACHE_HIT" == "true" ]; then
+ echo "TF_PLAN_REFRESH_FLAG=-refresh=false -lock=false" >> $GITHUB_ENV
+ echo "::notice::State will NOT be refreshed"
+ else
+ echo "TF_PLAN_REFRESH_FLAG=" >> $GITHUB_ENV
+ echo "::notice::State will be refreshed"
+
+ echo "$(date)" > ./.pr_plan_marker
+ fi
+
+ # Run Terraform Plan
+ # The plan output is saved in a file and then processed to remove unnecessary lines
+ # The step never fails but the result is checked in the next step
+ # This is because we want to post the plan output in the PR even if the plan fails
+ - name: Terraform Plan
+ id: plan
+ working-directory: ${{ steps.directory.outputs.dir }}
+ run: |
+ terraform plan \
+ -lock-timeout=120s \
+ -input=false \
+ -no-color \
+ $TF_PLAN_REFRESH_FLAG \
+ -out=plan.out 2>&1 | \
+ grep -v "hidden-link:" | \
+ tee tf_plan_stdout.txt
+
+ terraform show -no-color plan.out > full_plan.txt
+
+ # Extracts only the diff section from the Plan by skipping everything before the resource changes,
+ # and filters out non-essential log lines like state refreshes and reads.
+ if [ -s full_plan.txt ]; then
+ sed -n '/^ #/,$p' full_plan.txt | grep -Ev "hidden-link:|Refreshing state|state lock|Reading|Read" > filtered_plan.txt || echo "No changes detected." > filtered_plan.txt
+ else
+ echo "No plan output available." > filtered_plan.txt
+ fi
+
+ # The summary with number of resources to be added, changed, or destroyed (will be used in case the plan output is too long)
+ SUMMARY_LINE=$(grep -E "^Plan: [0-9]+ to (add|change|destroy|import)" tf_plan_stdout.txt || echo "No changes.")
+
+ # If the filtered plan is too long use the summary line, otherwise use the full filtered plan
+ if [ "$(wc -c < filtered_plan.txt)" -gt 60000 ]; then
+ echo "${SUMMARY_LINE}" > plan_output.txt
+ echo "COMPLETE_PLAN=false" >> $GITHUB_OUTPUT
+ else
+ cat filtered_plan.txt > plan_output.txt
+ echo "COMPLETE_PLAN=true" >> $GITHUB_OUTPUT
+ fi
+
+ # Error detection based on tf_plan_stdout.txt content
+ if grep -q "::error::Terraform exited with code" tf_plan_stdout.txt; then
+ echo "failed"
+ exit 1
+ fi
+
+ - name: Set Plan Output
+ id: set-plan-output
+ env:
+ WORKFLOW_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
+ OUTPUT_DIR: ${{ steps.directory.outputs.dir }}
+ PLAN_OUTCOME: ${{ steps.plan.outcome }}
+ COMPLETE_PLAN: ${{ steps.plan.outputs.COMPLETE_PLAN }}
+ working-directory: ${{ steps.directory.outputs.dir }}
+ run: |
+ echo "### 📖 Terraform Plan ($OUTPUT_DIR) - $PLAN_OUTCOME" > message_body.txt
+ echo "" >> message_body.txt
+ echo "Show Plan
" >> message_body.txt
+ echo "" >> message_body.txt
+
+ echo "\`\`\`hcl" >> message_body.txt
+ cat plan_output.txt >> message_body.txt
+ echo "\`\`\`" >> message_body.txt
+
+ if [ $COMPLETE_PLAN == 'false' ]; then
+ echo "Full plan output was too long and was omitted. Check the [workflow logs]($WORKFLOW_URL) for full details." >> message_body.txt
+ fi
+
+ echo "" >> message_body.txt
+ echo " " >> message_body.txt
+
+ # Post the plan output in the PR
+ - name: Post Plan on PR
+ id: comment
+ if: always() && github.event_name == 'pull_request'
+ uses: pagopa/dx/actions/pr-comment@main
+ env:
+ COMMENT_BODY_FILE: "${{ steps.directory.outputs.dir }}/message_body.txt"
+ with:
+ comment-body-file: ${{ env.COMMENT_BODY_FILE }}
+ search-pattern: "Terraform Plan (${{ steps.directory.outputs.dir }})"
+
+ # Fail the workflow if the Terraform plan failed
+ - name: Check Terraform Plan Result
+ if: always() && steps.plan.outcome != 'success'
+ run: |
+ exit 1
diff --git a/.gitignore b/.gitignore
index 6eca8e55..07cc7ad2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -294,3 +294,5 @@ changeset-status.json
cdktf.out/
cdk.tf.json
manifest.json
+
+stacks
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 99282936..fc16e396 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,5 +6,8 @@
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "file",
"files.autoSave": "onFocusChange",
- "vs-code-prettier-eslint.prettierLast": false
+ "vs-code-prettier-eslint.prettierLast": false,
+ "[typescript]": {
+ "editor.defaultFormatter": "esbenp.prettier-vscode"
+ }
}
diff --git a/apps/test-opex-api/package.json b/apps/test-opex-api/package.json
index adf55354..0d0c9a17 100644
--- a/apps/test-opex-api/package.json
+++ b/apps/test-opex-api/package.json
@@ -11,13 +11,12 @@
"typecheck": "tsc --noemit",
"lint": "eslint src --fix",
"lint:check": "eslint src",
- "infra:generate": "yarn -q dlx tsx src/opex.ts"
+ "generate:infra": "yarn -q dlx tsx src/opex.ts"
},
"dependencies": {
+ "@pagopa/opex-dashboard-ts": "workspace:^",
"cdktf": "^0.21.0",
- "cdktf-monitoring-stack": "workspace:*",
- "constructs": "^10.4.2",
- "opex-common": "workspace:*"
+ "constructs": "^10.4.2"
},
"devDependencies": {
"@pagopa/eslint-config": "^5.0.0",
diff --git a/apps/test-opex-api/src/opex.ts b/apps/test-opex-api/src/opex.ts
index 6a0e4220..b8cc4799 100644
--- a/apps/test-opex-api/src/opex.ts
+++ b/apps/test-opex-api/src/opex.ts
@@ -1,31 +1,81 @@
-import { App, AzurermBackend } from "cdktf";
-import { MonitoringStack } from "cdktf-monitoring-stack";
-import { backendConfig, opexConfig } from "opex-common";
+/* eslint-disable no-console */
+import { addOpexStack, DashboardConfig } from "@pagopa/opex-dashboard-ts";
+import { App, AzurermBackend, AzurermBackendConfig } from "cdktf";
-const app = new App();
+const opexConfig: DashboardConfig = {
+ action_groups: [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email",
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack",
+ ],
+ data_source:
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw",
+ name: "My spec",
+ oa3_spec:
+ "https://raw.githubusercontent.com/pagopa/opex-dashboard/refs/heads/main/test/data/io_backend_light.yaml",
+ overrides: {
+ endpoints: {
+ "/api/v1/services/{service_id}": {
+ availability_evaluation_frequency: 30, // Default: 10
+ availability_evaluation_time_window: 50, // Default: 20
+ availability_event_occurrences: 3, // Default: 1
+ availability_threshold: 0.95, // Default: 99%
+ response_time_evaluation_frequency: 35, // Default: 10
+ response_time_evaluation_time_window: 55, // Default: 20
+ response_time_event_occurrences: 5, // Default: 1
+ response_time_threshold: 2, // Default: 1
+ },
+ },
+ hosts: [
+ // Use these hosts instead of those inside the OpenApi spec
+ "https://example.com",
+ "https://example2.com",
+ ],
+ },
+ resource_group_name: "dashboards",
+ resource_type: "app-gateway",
+ tags: {
+ Environment: "TEST",
+ Owner: "team-opex",
+ },
+ timespan: "6m", // Default, a number or a timespan https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan
+};
-const stack1 = new MonitoringStack(app, "test-opex-api-v1", {
- config: opexConfig,
- openApiFilePaths: ["docs/openapi-v2.yaml"],
-});
-
-new AzurermBackend(stack1, {
- ...backendConfig,
+const backendConfig: AzurermBackendConfig = {
+ containerName: "tfstate",
key: "dx.test-opex-api1.tfstate",
-});
+ resourceGroupName: "my-rg",
+ storageAccountName: "mystorageaccount",
+};
+
+const app = new App({ hclOutput: true, outdir: "." });
-///////
+(async function () {
+ try {
+ const { opexStack: s1 } = await addOpexStack({
+ app,
+ config: opexConfig,
+ });
-const stack2 = new MonitoringStack(app, "test-opex-api-v2", {
- config: opexConfig,
- openApiFilePaths: ["docs/openapi-v3.yaml"],
-});
+ new AzurermBackend(s1, {
+ ...backendConfig,
+ key: "dx.test-opex-api1.tfstate",
+ });
-new AzurermBackend(stack2, {
- ...backendConfig,
- key: "dx.test-opex-api2.tfstate",
-});
+ // Add more stacks if needed
+ // const { opexStack: s2 } = await addAzureDashboard({
+ // app,
+ // config: opexConfig,
+ // });
+ // new AzurermBackend(s2, {
+ // ...backendConfig,
+ // key: "dx.test-opex-api2.tfstate",
+ // });
-///////
+ app.synth();
-app.synth();
+ console.log("Terraform CDKTF code generated successfully");
+ } catch (error) {
+ console.error("Error generating Terraform CDKTF code:", error);
+ process.exit(1);
+ }
+})();
diff --git a/apps/to-do-api/package.json b/apps/to-do-api/package.json
index ca242f0d..fa771936 100644
--- a/apps/to-do-api/package.json
+++ b/apps/to-do-api/package.json
@@ -30,11 +30,9 @@
"@to-do/azure-adapters": "workspace:^",
"@to-do/domain": "workspace:^",
"cdktf": "^0.21.0",
- "cdktf-monitoring-stack": "workspace:*",
"constructs": "^10.4.2",
"fp-ts": "^2.16.10",
"io-ts": "^2.2.22",
- "opex-common": "workspace:*",
"ulid": "^3.0.0"
},
"devDependencies": {
diff --git a/apps/to-do-api/src/opex.ts b/apps/to-do-api/src/opex.ts
index d2438d4e..328d94fe 100644
--- a/apps/to-do-api/src/opex.ts
+++ b/apps/to-do-api/src/opex.ts
@@ -1,24 +1,24 @@
-import { App, AzurermBackend } from "cdktf";
-import { MonitoringStack } from "cdktf-monitoring-stack";
-import { backendConfig, opexConfig } from "opex-common";
+// import { App, AzurermBackend } from "cdktf";
+// import { MonitoringStack } from "cdktf-monitoring-stack";
+// import { backendConfig, opexConfig } from "opex-common";
-const app = new App();
+// const app = new App();
-// Use the following lines if you need to resolve paths dynamically
-//
-// import * as path from "path";
-// import { fileURLToPath } from "url";
-// const __filename = fileURLToPath(import.meta.url);
-// const __dirname = path.dirname(__filename);
-//
-const stack = new MonitoringStack(app, "to-do-api", {
- config: opexConfig,
- openApiFilePaths: ["docs/openapi.yaml"],
-});
+// // Use the following lines if you need to resolve paths dynamically
+// //
+// // import * as path from "path";
+// // import { fileURLToPath } from "url";
+// // const __filename = fileURLToPath(import.meta.url);
+// // const __dirname = path.dirname(__filename);
+// //
+// const stack = new MonitoringStack(app, "to-do-api", {
+// config: opexConfig,
+// openApiFilePaths: ["docs/openapi.yaml"],
+// });
-new AzurermBackend(stack, {
- ...backendConfig,
- key: "dx.opex.to-do-api.tfstate",
-});
+// new AzurermBackend(stack, {
+// ...backendConfig,
+// key: "dx.opex.to-do-api.tfstate",
+// });
-app.synth();
+// app.synth();
diff --git a/apps/to-do-webapp/package.json b/apps/to-do-webapp/package.json
index 334dc582..3dbfaa4d 100644
--- a/apps/to-do-webapp/package.json
+++ b/apps/to-do-webapp/package.json
@@ -6,10 +6,10 @@
"scripts": {
"clean": "shx rm -rf out/ dist/ tmp_dir/",
"dev": "next dev --turbopack",
- "build": "yarn generate && next build && yarn standalone:copy && yarn standalone:clean && yarn standalone:rename",
+ "build": "next build && yarn standalone:copy && yarn standalone:clean && yarn standalone:rename",
"start": "next start",
"lint": "eslint src --fix",
- "standalone:copy": "shx mkdir tmp_dir && shx cp -r .next/standalone/apps/to-do-webapp/.next/* tmp_dir && shx cp -r .next/static tmp_dir && shx mkdir dist && shx cp .next/standalone/apps/to-do-webapp/server.js dist",
+ "standalone:copy": "shx mkdir -p tmp_dir && shx cp -r .next/standalone/apps/to-do-webapp/.next/* tmp_dir && shx cp -r .next/static tmp_dir && shx mkdir dist && shx cp .next/standalone/apps/to-do-webapp/server.js dist",
"standalone:clean": "shx rm -rf .next",
"standalone:rename": "shx mv tmp_dir .next",
"generate": "gen-api-models --api-spec https://raw.githubusercontent.com/pagopa/dx-playground/4cb60e3994eb14a76e6b3d1c49075c76f2f3cfbc/apps/to-do-api/docs/openapi.yaml --out-dir ./src/lib/client --no-strict --request-types --response-decoders --client"
diff --git a/infra/bootstrapper/dev/.terraform.lock.hcl b/infra/bootstrapper/dev/.terraform.lock.hcl
index 8dce70bf..71f2b405 100644
--- a/infra/bootstrapper/dev/.terraform.lock.hcl
+++ b/infra/bootstrapper/dev/.terraform.lock.hcl
@@ -9,6 +9,7 @@ provider "registry.terraform.io/hashicorp/azuread" {
"h1:2rk36pu4YyhBVz/Mf4swYCQxaB31iPaXOiWNlqZMXbM=",
"h1:7ZNdNGnUB6N6Z6St3COzRXFaghMEyYkZt7WyOCRKOqo=",
"h1:EZNO8sEtUABuRxujQrDrW1z1QsG0dq6iLbzWtnG7Om4=",
+ "h1:GS/WN8VS6Wp9hvs46lgDsR4ERV8o3Sr+zatF/z2XohU=",
"zh:162916b037e5133f49298b0ffa3e7dcef7d76530a8ca738e7293373980f73c68",
"zh:1c3e89cf19118fc07d7b04257251fc9897e722c16e0a0df7b07fcd261f8c12e7",
"zh:492931cea4f30887ab5bca36a8556dfcb897288eddd44619c0217fc5da2d57e7",
@@ -28,6 +29,7 @@ provider "registry.terraform.io/hashicorp/azurerm" {
version = "4.35.0"
constraints = ">= 3.0.0, >= 3.110.0, ~> 4.0, < 5.0.0"
hashes = [
+ "h1:/4tb6lwsWlSu3XCqFS7XPItU3dkvMSCOIW9/nhF2+IA=",
"h1:3zJsyLWItriZDhtx6kBkoy9UPcA9l5G4PKi4ZYxhsnA=",
"h1:WnSak7fbscDv2LIBl9o/2zoQj4fnEq1JmUelFw2EMrw=",
"h1:k2PriDCNaoY/osyZzfje7oyZipzbCHcjtQcEmNtKYHU=",
@@ -55,6 +57,7 @@ provider "registry.terraform.io/integrations/github" {
"h1:GV6m1zdKNV0ECNhvzj7UoBXQz1Cv2UQa0UZ7Wt4RgTw=",
"h1:P4SRG4605PvPKASeDu1lW49TTz1cCGsjQ7qbOBgNd6I=",
"h1:Yq0DZYKVFwPdc+v5NnXYcgTYWKInSkjv5WjyWMODj9U=",
+ "h1:jPtUxZC/fwDeA+CPJoJHAhoDy/KhZdE7viycsWuXvgM=",
"zh:0b1b5342db6a17de7c71386704e101be7d6761569e03fb3ff1f3d4c02c32d998",
"zh:2fb663467fff76852126b58315d9a1a457e3b04bec51f04bf1c0ddc9dfbb3517",
"zh:4183e557a1dfd413dae90ca4bac37dbbe499eae5e923567371f768053f977800",
@@ -78,6 +81,7 @@ provider "registry.terraform.io/pagopa-dx/azure" {
constraints = ">= 0.0.6, >= 0.0.7, < 1.0.0"
hashes = [
"h1:+1dGui62P1/cvAnQYiQLwW50cTrHg3hsPo4GblmIYaE=",
+ "h1:fCV5KUBAZPEG/KHos32DeYkipT4adxzDFQVdNsHBr4U=",
"h1:h7qyvjqn7mGB56DvVhCb2laGWNRxSD+mnj0VC0HhGE4=",
"h1:i5I234l00EsX7wTJfU2t521pRXlr0T/tH729aF7Fa8w=",
"h1:w8bOpDG6/f+Wgdgnlc66gFXRbEL+6fNt8z/ht3R0ufs=",
diff --git a/infra/resources/_modules/api/.terraform.lock.hcl b/infra/resources/_modules/api/.terraform.lock.hcl
index 119f8bd1..50584c88 100644
--- a/infra/resources/_modules/api/.terraform.lock.hcl
+++ b/infra/resources/_modules/api/.terraform.lock.hcl
@@ -4,6 +4,7 @@
provider "registry.terraform.io/hashicorp/azurerm" {
version = "4.14.0"
hashes = [
+ "h1:FYWhn/x1jSjnxUsKkV4+sXIfOc+H3Sq8ja/ZBB2IAaU=",
"h1:cGkb9Ps5A1FwXO7BaZ9T7Ufe79gsNzk6lfaNfcWn0+s=",
"zh:05aaea16fc5f27b14d9fbad81654edf0638949ed3585576b2219c76a2bee095a",
"zh:065ce6ed16ba3fa7efcf77888ea582aead54e6a28f184c6701b73d71edd64bb0",
diff --git a/infra/resources/_modules/application_insights/.terraform.lock.hcl b/infra/resources/_modules/application_insights/.terraform.lock.hcl
index cf9cb179..3292709c 100644
--- a/infra/resources/_modules/application_insights/.terraform.lock.hcl
+++ b/infra/resources/_modules/application_insights/.terraform.lock.hcl
@@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/azurerm" {
version = "4.17.0"
hashes = [
"h1:VgnUh7PiRa/76P+0NFk8vmrmfLnPT6+tOZ/AP6h4TeQ=",
+ "h1:aTtjvuCBO0X0VbcorVVdBmxJBcTiLx43om+UpmNBxHc=",
"zh:163b81a3bf29c8f161a1c100a48164b1bd1af434cd564b44596cb71a6c33f03d",
"zh:2996b107d3c05a9db14458b32b6f22f8cde0adb96263196d82d3dc302907a257",
"zh:361abd84b6e73016ebebb9ef9cd14c237d8b1e4500ea75f73243ff0534e5e4fb",
@@ -25,6 +26,7 @@ provider "registry.terraform.io/pagopa-dx/azure" {
constraints = "~> 0.0"
hashes = [
"h1:I/tTgCuaapwwzUuoaxIg+x8/HNr8pYdYSGeiNpobKOw=",
+ "h1:OahH6LwxRUBmh0GqOlX6gM68dpb24wm6svQl/hCrZeA=",
"zh:2301715691aabde18a23834654b236f972aecf4448df62750c5235e4c219b9ff",
"zh:42c235c4bd422fdf97c84dd91cb5b8db00293b9f8785c86377358f9d72d66f92",
"zh:49ece07513d55aa3c1fffd374ab51c3097aa7518196e76eb5d8dff066d5c1a53",
diff --git a/infra/resources/_modules/virtual_machine/.terraform.lock.hcl b/infra/resources/_modules/virtual_machine/.terraform.lock.hcl
index 28d7f2ee..4708f07a 100644
--- a/infra/resources/_modules/virtual_machine/.terraform.lock.hcl
+++ b/infra/resources/_modules/virtual_machine/.terraform.lock.hcl
@@ -5,6 +5,7 @@ provider "registry.terraform.io/hashicorp/azurerm" {
version = "4.13.0"
hashes = [
"h1:IAy+6S1EY78ZyipSZDqjMLFLMMn9UBaz9tZE2i4aKEI=",
+ "h1:IzEHsQIYeSaD1gwSyP474QYELBAezcvhPWCQIkjfbgs=",
"zh:23e04573f50cec091cb32113e3e78033b1ba00ddbc9b7aece0d6397ae60b9b5e",
"zh:53d07a697e5aa36561a4b11e22a68c7cb582d46ed42cd4a61c08796d38f18bc9",
"zh:56064e9fbd5330ba734af24aca23ed0c93b12117474ae08d8180bca0dbf3ac06",
diff --git a/infra/resources/dev/tfmodules.lock.json b/infra/resources/dev/tfmodules.lock.json
index 949dbfdb..ec3313b3 100644
--- a/infra/resources/dev/tfmodules.lock.json
+++ b/infra/resources/dev/tfmodules.lock.json
@@ -1,15 +1,15 @@
{
"apim": {
- "hash": "7c4ede49c7e6ebd45b656abca088b846d0e417d3e8e9b700464c7acaefbe5d04",
- "version": "1.2.2",
+ "hash": "853ca35ce07874a2bf1891ef9e9fc8f687c330a8cfa79c31ff1c76ddfa375b06",
+ "version": "2.0.1",
"name": "azure_api_management",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-api-management/azurerm/1.2.2"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-api-management/azurerm/2.0.1"
},
"apim_roles": {
- "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1",
- "version": "1.1.2",
+ "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae",
+ "version": "1.2.1",
"name": "azure_role_assignments",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1"
},
"app_service": {
"hash": "1578c3d974a6e23784264e334d4a5fc51fc11bb42e73d650d7f6ff335346184c",
@@ -18,34 +18,34 @@
"source": "https://registry.terraform.io/modules/pagopa-dx/azure-app-service/azurerm/1.0.1"
},
"app_service_roles": {
- "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1",
- "version": "1.1.2",
+ "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae",
+ "version": "1.2.1",
"name": "azure_role_assignments",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1"
},
"azure_function_v3_function_app": {
- "hash": "20d1bf568efdf78dc97cf5dac105c7f30caf710e7a49a3edf7acab1ea78a4699",
- "version": "1.0.1",
+ "hash": "a38c42efbd1577c3971123cc4f335a6827e0ead44695b9d014789e0598c2672d",
+ "version": "3.0.0",
"name": "azure_function_app",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/1.0.1"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/3.0.0"
},
"cosmos": {
- "hash": "398ffd2c1fb90f2228a5d42e7f90e6601162c2e90e23daeee726e1f1f02aa322",
- "version": "0.1.0",
+ "hash": "1e6e66245084310310d36106509c99ee0eed25e98640680c4acc8cff426ff321",
+ "version": "0.2.0",
"name": "azure_cosmos_account",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-cosmos-account/azurerm/0.1.0"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-cosmos-account/azurerm/0.2.0"
},
"func_api_role": {
- "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1",
- "version": "1.1.2",
+ "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae",
+ "version": "1.2.1",
"name": "azure_role_assignments",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1"
},
"function_app": {
- "hash": "20d1bf568efdf78dc97cf5dac105c7f30caf710e7a49a3edf7acab1ea78a4699",
- "version": "1.0.1",
+ "hash": "a38c42efbd1577c3971123cc4f335a6827e0ead44695b9d014789e0598c2672d",
+ "version": "3.0.0",
"name": "azure_function_app",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/1.0.1"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/3.0.0"
},
"function_test_durable": {
"hash": "367548c5df2a42f5273583c0de4181c865bd419a4d0aca3cffa2ba19a90c210a",
@@ -54,10 +54,10 @@
"source": "https://registry.terraform.io/modules/pagopa-dx/azure-function-app/azurerm/0.3.1"
},
"function_v3_api_role": {
- "hash": "433fe44b2863b8af8d4f4206ee2590599b0cb67d78b372e65d68317d3199aaa1",
- "version": "1.1.2",
+ "hash": "9d429fd0aba8a29c1a0dbb6005e7dd72a79d9d8dda5076220eb9f0c8bef438ae",
+ "version": "1.2.1",
"name": "azure_role_assignments",
- "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.1.2"
+ "source": "https://registry.terraform.io/modules/pagopa-dx/azure-role-assignments/azurerm/1.2.1"
},
"naming_convention": {
"hash": "d7973237e601af346ca3cc8797623f4edff8bdb94b661453d4242609f8d49118",
diff --git a/packages/cdktf-monitoring-stack/package.json b/packages/cdktf-monitoring-stack/package.json
deleted file mode 100644
index e69aba2f..00000000
--- a/packages/cdktf-monitoring-stack/package.json
+++ /dev/null
@@ -1,28 +0,0 @@
-{
- "name": "cdktf-monitoring-stack",
- "version": "1.0.0",
- "private": true,
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
- "type": "module",
- "scripts": {
- "build": "tsc",
- "typecheck": "tsc --noemit",
- "lint": "eslint src --fix",
- "lint:check": "eslint src"
- },
- "dependencies": {
- "@cdktf/provider-azurerm": "^14.2.0",
- "cdktf": "^0.21.0",
- "constructs": "^10.4.2",
- "js-yaml": "^4.1.0"
- },
- "devDependencies": {
- "@pagopa/eslint-config": "^5.0.0",
- "@pagopa/typescript-config-node": "workspace:^",
- "@types/js-yaml": "^4.0.9",
- "eslint": "^9.30.1",
- "prettier": "^3.6.2",
- "typescript": "^5.8.3"
- }
-}
diff --git a/packages/cdktf-monitoring-stack/src/dashboard-generator.ts b/packages/cdktf-monitoring-stack/src/dashboard-generator.ts
deleted file mode 100644
index 5b013506..00000000
--- a/packages/cdktf-monitoring-stack/src/dashboard-generator.ts
+++ /dev/null
@@ -1,156 +0,0 @@
-import type { EndpointWithProperties } from "./openapi-processor.js";
-
-import { MonitoringConfig } from "./monitoring-stack.js";
-import {
- getApimAvailabilityQuery,
- getApimResponseCodesQuery,
- getApimResponseTimeQuery,
-} from "./query-builder.js";
-import { uriToRegex } from "./uri-utils.js";
-
-/**
- * Dashboard chart configuration types
- */
-export interface ChartOptions {
- logAnalyticsWorkspaceId: string;
- position: { colSpan: number; rowSpan: number; x: number; y: number };
- query: string;
- specificChart: "Line" | "Pie" | "StackedArea";
- splitBy?: { name: string; type: string }[];
- subtitle: string;
- title: string;
- yAxis: { name: string; type: string }[];
-}
-
-/**
- * Creates a chart part configuration for the dashboard
- */
-export function createChartPart(
- options: ChartOptions,
-): Record {
- return {
- metadata: {
- inputs: [
- {
- name: "Scope",
- value: { resourceIds: [options.logAnalyticsWorkspaceId] },
- },
- { name: "Query", value: options.query },
- { name: "Version", value: "2.0" },
- { name: "TimeRange", value: "PT4H" },
- { name: "PartTitle", value: options.title },
- { name: "PartSubTitle", value: options.subtitle },
- ],
- settings: {
- content: {
- Dimensions: {
- aggregation: "Sum",
- splitBy: options.splitBy || [],
- xAxis: { name: "TimeGenerated", type: "datetime" },
- yAxis: options.yAxis,
- },
- LegendOptions: { isEnabled: true, position: "Bottom" },
- PartSubTitle: options.subtitle,
- PartTitle: options.title,
- Query: options.query,
- SpecificChart: options.specificChart,
- },
- },
- type: "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
- },
- position: options.position,
- };
-}
-
-/**
- * Generates dashboard properties JSON string for the Azure Portal Dashboard
- */
-export function generateDashboardProperties(
- endpoints: EndpointWithProperties[],
- config: MonitoringConfig,
-): string {
- const parts: Record> = {};
-
- endpoints.forEach((endpoint, index) => {
- const yPos = index * 4;
- const hostPath = endpoint.host
- ? `${endpoint.host}${endpoint.path}`
- : endpoint.path;
- const subtitle = `${endpoint.method} ${hostPath}`;
- const endpointPath = uriToRegex(endpoint.path);
-
- // Create three parts for each endpoint: Availability, Response Codes,
- // and Response Time
-
- // Availability Chart
- parts[index * 3 + 0] = createChartPart({
- logAnalyticsWorkspaceId: config.logAnalyticsWorkspaceId,
- position: { colSpan: 6, rowSpan: 4, x: 0, y: yPos },
- query: getApimAvailabilityQuery({
- endpointPath,
- isAlarm: false,
- threshold: endpoint.availabilityThreshold,
- timeSpan: endpoint.availabilityTimeSpan,
- }),
- specificChart: "Line",
- subtitle,
- title: "Availability (PT5M)",
- yAxis: [
- { name: "availability", type: "real" },
- { name: "watermark", type: "real" },
- ],
- });
-
- // Response Codes Chart
- parts[index * 3 + 1] = createChartPart({
- logAnalyticsWorkspaceId: config.logAnalyticsWorkspaceId,
- position: { colSpan: 6, rowSpan: 4, x: 6, y: yPos },
- query: getApimResponseCodesQuery({
- endpointPath,
- isAlarm: false,
- threshold: endpoint.responseCodeThreshold,
- timeSpan: endpoint.responseCodeTimeSpan,
- }),
- specificChart: "StackedArea",
- splitBy: [{ name: "HTTPStatus", type: "string" }],
- subtitle,
- title: "Response Codes (PT5M)",
- yAxis: [{ name: "count_", type: "long" }],
- });
-
- // Response Time Chart
- parts[index * 3 + 2] = createChartPart({
- logAnalyticsWorkspaceId: config.logAnalyticsWorkspaceId,
- position: { colSpan: 6, rowSpan: 4, x: 12, y: yPos },
- query: getApimResponseTimeQuery({
- endpointPath,
- isAlarm: false,
- threshold: endpoint.responseTimeThreshold,
- timeSpan: endpoint.responseTimeTimeSpan,
- }),
- specificChart: "Line",
- subtitle,
- title: "95th Percentile Response Time (ms)",
- yAxis: [
- { name: "duration_percentile_95", type: "real" },
- { name: "watermark", type: "long" },
- ],
- });
- });
-
- const dashboardStructure = {
- properties: {
- lenses: { "0": { order: 0, parts: parts } },
- metadata: {
- model: {
- timeRange: {
- type: "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange",
- value: { relative: { duration: 4, timeUnit: 2 } },
- },
- },
- },
- },
- };
-
- return JSON.stringify(dashboardStructure.properties);
-}
diff --git a/packages/cdktf-monitoring-stack/src/index.ts b/packages/cdktf-monitoring-stack/src/index.ts
deleted file mode 100644
index bc08b07e..00000000
--- a/packages/cdktf-monitoring-stack/src/index.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export {
- MonitoringConfig,
- MonitoringStack,
- MonitoringStackProps,
- Tags,
-} from "./monitoring-stack.js";
diff --git a/packages/cdktf-monitoring-stack/src/monitoring-stack.ts b/packages/cdktf-monitoring-stack/src/monitoring-stack.ts
deleted file mode 100644
index 030bf28e..00000000
--- a/packages/cdktf-monitoring-stack/src/monitoring-stack.ts
+++ /dev/null
@@ -1,190 +0,0 @@
-import { MonitorScheduledQueryRulesAlert } from "@cdktf/provider-azurerm/lib/monitor-scheduled-query-rules-alert";
-import { PortalDashboard } from "@cdktf/provider-azurerm/lib/portal-dashboard";
-import { AzurermProvider } from "@cdktf/provider-azurerm/lib/provider";
-import { ResourceGroup } from "@cdktf/provider-azurerm/lib/resource-group";
-import { TerraformStack } from "cdktf";
-import { Construct } from "constructs";
-
-import { generateDashboardProperties } from "./dashboard-generator.js";
-import {
- endpointsWithDefaultProperties,
- processOpenApiFiles,
-} from "./openapi-processor.js";
-import {
- getApimAvailabilityQuery,
- getApimResponseTimeQuery,
-} from "./query-builder.js";
-import { uriToRegex } from "./uri-utils.js";
-
-/**
- * Core configuration types for the monitoring stack
- */
-
-export interface MonitoringConfig {
- actionGroupId: string;
- apimServiceName: string;
- location: string;
- logAnalyticsWorkspaceId: string;
- resourceGroupName: string;
- tags?: Tags;
-}
-
-export interface MonitoringStackProps {
- config: MonitoringConfig;
- openApiFilePaths: string[];
-}
-
-export interface Tags {
- // Allow additional unknown properties
- [key: string]: string;
- BusinessUnit: string;
- CostCenter: string;
- CreatedBy: string;
- Environment: string;
- ManagementTeam: string;
- Scope: string;
- Source: string;
-}
-
-/**
- * Azure monitoring stack for API Management services
- * Creates alerts and dashboards based on OpenAPI specifications
- */
-export class MonitoringStack extends TerraformStack {
- constructor(scope: Construct, id: string, props: MonitoringStackProps) {
- super(scope, id);
-
- const { config, openApiFilePaths } = props;
-
- // Configure Azure provider
- new AzurermProvider(this, "azurerm", {
- features: [{}],
- storageUseAzuread: true,
- });
-
- // Create resource group
- const resourceGroup = new ResourceGroup(this, "rg", {
- location: config.location,
- name: config.resourceGroupName,
- });
-
- // Process OpenAPI specs to extract endpoints
- const uniqueEndpoints = processOpenApiFiles(openApiFilePaths);
- const endpointsWithProps = endpointsWithDefaultProperties(uniqueEndpoints);
-
- // Create monitoring alerts for each endpoint
- this.createMonitoringAlerts(id, endpointsWithProps, config, resourceGroup);
-
- // Create monitoring dashboard
- this.createMonitoringDashboard(
- id,
- endpointsWithProps,
- config,
- resourceGroup,
- );
- }
-
- /**
- * Creates availability and response time alerts for all endpoints
- */
- private createMonitoringAlerts(
- id: string,
- endpoints: ReturnType,
- config: MonitoringStackProps["config"],
- resourceGroup: ResourceGroup,
- ): void {
- endpoints.forEach((endpoint) => {
- const sanitizedName = this.createSanitizedEndpointName(endpoint);
- const baseAlertName = `${id}-${sanitizedName}`;
- const endpointPath = uriToRegex(endpoint.path);
-
- // Create availability alert
- new MonitorScheduledQueryRulesAlert(
- this,
- `avail-alert-${sanitizedName}`,
- {
- action: { actionGroup: [config.actionGroupId] },
- dataSourceId: config.logAnalyticsWorkspaceId,
- frequency: 5,
- location: config.location,
- name: `Alert-Avail-${baseAlertName}`,
- query: getApimAvailabilityQuery({
- endpointPath,
- isAlarm: true,
- threshold: endpoint.availabilityThreshold,
- timeSpan: endpoint.availabilityTimeSpan,
- }),
- resourceGroupName: resourceGroup.name,
- severity: 2,
- tags: config.tags,
- timeWindow: 10,
- trigger: { operator: "GreaterThan", threshold: 0 },
- },
- );
-
- // Create response time alert
- new MonitorScheduledQueryRulesAlert(
- this,
- `resptime-alert-${sanitizedName}`,
- {
- action: { actionGroup: [config.actionGroupId] },
- dataSourceId: config.logAnalyticsWorkspaceId,
- frequency: 5,
- location: config.location,
- name: `Alert-RespTime-${baseAlertName}`,
- query: getApimResponseTimeQuery({
- endpointPath,
- isAlarm: true,
- threshold: endpoint.responseTimeThreshold,
- timeSpan: endpoint.responseTimeTimeSpan,
- }),
- resourceGroupName: resourceGroup.name,
- severity: 2,
- tags: config.tags,
- timeWindow: 10,
- trigger: { operator: "GreaterThan", threshold: 0 },
- },
- );
- });
- }
-
- /**
- * Creates a monitoring dashboard with charts for all endpoints
- */
- private createMonitoringDashboard(
- id: string,
- endpoints: ReturnType,
- config: MonitoringStackProps["config"],
- resourceGroup: ResourceGroup,
- ): void {
- const dashboardName = `Dashboard-${id}`;
-
- new PortalDashboard(this, "api-dashboard", {
- dashboardProperties: generateDashboardProperties(endpoints, config),
- location: config.location,
- name: dashboardName,
- resourceGroupName: resourceGroup.name,
- tags: { "hidden-title": dashboardName, ...config.tags },
- });
- }
-
- /**
- * Creates a sanitized name for an endpoint that can be used in Azure resource names
- */
- private createSanitizedEndpointName(endpoint: {
- host?: string;
- method: string;
- path: string;
- }): string {
- const hostPrefix = endpoint.host
- ? endpoint.host.replace(/[./]/g, "-") + "-"
- : "";
- const methodPrefix = endpoint.method.toLowerCase() + "-";
-
- return (
- hostPrefix +
- methodPrefix +
- endpoint.path.replace(/[/{}]/g, "-").replace(/^-|-$/g, "")
- );
- }
-}
diff --git a/packages/cdktf-monitoring-stack/src/openapi-processor.ts b/packages/cdktf-monitoring-stack/src/openapi-processor.ts
deleted file mode 100644
index 4ec5f493..00000000
--- a/packages/cdktf-monitoring-stack/src/openapi-processor.ts
+++ /dev/null
@@ -1,215 +0,0 @@
-import * as fs from "fs";
-import * as yaml from "js-yaml";
-
-export interface EndpointDetails {
- host?: string;
- method: string;
- path: string;
-}
-
-export interface EndpointWithProperties {
- availabilityThreshold: number;
- availabilityTimeSpan: string;
- host?: string;
- method: string;
- path: string;
- responseCodeThreshold: number;
- responseCodeTimeSpan: string;
- responseTimeThreshold: number;
- responseTimeTimeSpan: string;
-}
-
-/**
- * OpenAPI specification types and related data structures
- */
-export interface OpenApiSpec {
- basePath?: string; // OpenAPI 2.0 base path field
- host?: string; // OpenAPI 2.0 legacy field
- paths: Record>;
- servers?: { description?: string; url: string }[]; // OpenAPI 3.x field
-}
-
-/**
- * Extract base paths from OpenAPI spec.
- * For OpenAPI 3.x: extracts path portion from server URLs
- * For OpenAPI 2.0: extracts the basePath field
- * Returns an array of base paths.
- */
-export function extractServerBasePaths(openApiSpec: OpenApiSpec): string[] {
- const basePaths: string[] = [];
-
- // Handle OpenAPI 3.x servers field
- if (openApiSpec.servers && openApiSpec.servers.length > 0) {
- openApiSpec.servers.forEach((server) => {
- try {
- const url = new URL(server.url);
- const pathname = url.pathname.endsWith("/")
- ? url.pathname.slice(0, -1)
- : url.pathname;
- basePaths.push(pathname || "");
- } catch {
- // If URL is relative or invalid, treat as path-only
- const pathname = server.url.startsWith("/")
- ? server.url
- : `/${server.url}`;
- basePaths.push(
- pathname.endsWith("/") ? pathname.slice(0, -1) : pathname,
- );
- }
- });
- } else {
- // Handle OpenAPI 2.0 basePath field
- const basePath = openApiSpec.basePath || "";
- basePaths.push(basePath);
- }
-
- return basePaths;
-}
-
-/**
- * Extract server URLs from OpenAPI spec.
- * Handles both OpenAPI 3.x servers field and OpenAPI 2.0 host + basePath fields.
- * Returns an array of host URLs (without path components for consistency).
- */
-export function extractServerUrls(openApiSpec: OpenApiSpec): string[] {
- const serverUrls: string[] = [];
-
- // Handle OpenAPI 3.x servers field
- if (openApiSpec.servers && openApiSpec.servers.length > 0) {
- openApiSpec.servers.forEach((server) => {
- try {
- const url = new URL(server.url);
- serverUrls.push(url.host);
- } catch {
- // If URL is relative or invalid, skip it silently
- }
- });
- }
-
- // Fallback to OpenAPI 2.0 host field if no servers found
- if (serverUrls.length === 0 && openApiSpec.host) {
- serverUrls.push(openApiSpec.host);
- }
-
- return serverUrls;
-}
-
-const HTTP_METHODS = [
- "delete",
- "get",
- "head",
- "options",
- "patch",
- "post",
- "put",
- "trace",
-];
-
-/**
- * Convert endpoint details to endpoints with monitoring properties
- */
-export function endpointsWithDefaultProperties(
- endpoints: EndpointDetails[],
-): EndpointWithProperties[] {
- return endpoints.map((endpoint) => ({
- availabilityThreshold: 99.0,
- availabilityTimeSpan: "5m",
- host: endpoint.host,
- method: endpoint.method,
- path: endpoint.path,
- responseCodeThreshold: 1000,
- responseCodeTimeSpan: "5m",
- responseTimeThreshold: 1000,
- responseTimeTimeSpan: "5m",
- }));
-}
-
-/**
- * Process all OpenAPI specs and collect all endpoints with their hosts and methods
- */
-export function processOpenApiFiles(
- openApiFilePaths: string[],
-): EndpointDetails[] {
- const allEndpointsWithDetails: EndpointDetails[] = [];
-
- openApiFilePaths.forEach((openApiFilePath) => {
- const openApiSpec = yaml.load(
- fs.readFileSync(openApiFilePath, "utf8"),
- ) as OpenApiSpec;
-
- const endpoints = extractEndpointsFromSpec(openApiSpec);
- allEndpointsWithDetails.push(...endpoints);
- });
-
- return removeDuplicateEndpoints(allEndpointsWithDetails);
-}
-
-/**
- * Extract endpoints from a single OpenAPI specification
- */
-function extractEndpointsFromSpec(openApiSpec: OpenApiSpec): EndpointDetails[] {
- const endpoints: EndpointDetails[] = [];
-
- // Extract server URLs and base paths from the spec
- const serverUrls = extractServerUrls(openApiSpec);
- const basePaths = extractServerBasePaths(openApiSpec);
-
- const paths = Object.keys(openApiSpec.paths);
-
- paths.forEach((path) => {
- const pathItem = openApiSpec.paths[path];
- if (pathItem) {
- const methods = Object.keys(pathItem).filter((key) =>
- HTTP_METHODS.includes(key.toLowerCase()),
- );
-
- methods.forEach((method) => {
- // If we have server URLs, create an endpoint for each server
- if (serverUrls.length > 0 && basePaths.length > 0) {
- // For OpenAPI 3.x with servers, or OpenAPI 2.0 with host + basePath
- serverUrls.forEach((serverUrl, index) => {
- const basePath = basePaths[index] || basePaths[0] || "";
- const fullPath = basePath ? `${basePath}${path}` : path;
- endpoints.push({
- host: serverUrl,
- method: method.toUpperCase(),
- path: fullPath,
- });
- });
- } else {
- // Fallback to legacy host field or no host
- // For OpenAPI 2.0, we need to combine basePath with the path
- const fullPath = openApiSpec.basePath
- ? `${openApiSpec.basePath}${path}`
- : path;
-
- endpoints.push({
- host: openApiSpec.host,
- method: method.toUpperCase(),
- path: fullPath,
- });
- }
- });
- }
- });
-
- return endpoints;
-}
-
-/**
- * Remove duplicates based on path, method, and host combination
- */
-function removeDuplicateEndpoints(
- endpoints: EndpointDetails[],
-): EndpointDetails[] {
- return endpoints.filter(
- (endpoint, index, self) =>
- index ===
- self.findIndex(
- (e) =>
- e.path === endpoint.path &&
- e.method === endpoint.method &&
- e.host === endpoint.host,
- ),
- );
-}
diff --git a/packages/cdktf-monitoring-stack/src/query-builder.ts b/packages/cdktf-monitoring-stack/src/query-builder.ts
deleted file mode 100644
index d98e15ac..00000000
--- a/packages/cdktf-monitoring-stack/src/query-builder.ts
+++ /dev/null
@@ -1,72 +0,0 @@
-/**
- * Query parameter types and KQL query generation functions
- */
-
-export interface QueryParams {
- endpointPath: string;
- isAlarm: boolean;
- threshold: number;
- timeSpan: string;
-}
-
-/**
- * Generate KQL query for APIM availability monitoring
- */
-export function getApimAvailabilityQuery(params: QueryParams): string {
- const { endpointPath, isAlarm, threshold, timeSpan } = params;
- return `
- let threshold = ${threshold / 100};
- AzureDiagnostics
- | where ResourceProvider == "MICROSOFT.APIMANAGEMENT"
- and url_s matches regex "${endpointPath}"
- | summarize
- Total=count(),
- Success=count(responseCode_d < 500) by bin(TimeGenerated, ${timeSpan})
- | extend availability=toreal(Success) / Total
- ${
- isAlarm
- ? "| where availability < threshold"
- : `| project TimeGenerated, availability, watermark=threshold
- | render timechart with (xtitle = "time", ytitle= "availability(%)")`
- }
-`;
-}
-
-/**
- * Generate KQL query for APIM response codes monitoring
- */
-export function getApimResponseCodesQuery(params: QueryParams): string {
- const { endpointPath, timeSpan } = params;
- return `
- AzureDiagnostics
- | where url_s matches regex "${endpointPath}"
- | extend HTTPStatus = case(
- responseCode_d between (100 .. 199), "1XX",
- responseCode_d between (200 .. 299), "2XX",
- responseCode_d between (300 .. 399), "3XX",
- responseCode_d between (400 .. 499), "4XX",
- "5XX")
- | summarize count() by HTTPStatus, bin(TimeGenerated, ${timeSpan})
- | render areachart with (xtitle = "time", ytitle= "count")
- `;
-}
-
-/**
- * Generate KQL query for APIM response time monitoring
- */
-export function getApimResponseTimeQuery(params: QueryParams): string {
- const { endpointPath, isAlarm, threshold, timeSpan } = params;
- return `
- let threshold = ${threshold};
- AzureDiagnostics
- | where url_s matches regex "${endpointPath}"
- | summarize
- watermark=threshold,
- duration_percentile_95=percentiles(todouble(DurationMs)/1000, 95) by bin(TimeGenerated, ${timeSpan})
- ${
- isAlarm
- ? `| where duration_percentile_95 > threshold`
- : `| render timechart with (xtitle = "time", ytitle= "response time(s)")`
- }
- `;
-}
diff --git a/packages/cdktf-monitoring-stack/src/uri-utils.ts b/packages/cdktf-monitoring-stack/src/uri-utils.ts
deleted file mode 100644
index 10f6353f..00000000
--- a/packages/cdktf-monitoring-stack/src/uri-utils.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-/**
- * URI transformation utilities
- */
-
-/**
- * Translate path parameters of a URI to a generic version thanks to regex
- */
-export function uriToRegex(value: string): string {
- return String(value).replace(/{[^}]+}/g, "[^/]+") + "$";
-}
diff --git a/packages/cdktf-monitoring-stack/tsconfig.json b/packages/cdktf-monitoring-stack/tsconfig.json
deleted file mode 100644
index 4c35d6d7..00000000
--- a/packages/cdktf-monitoring-stack/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "extends": "@pagopa/typescript-config-node/tsconfig.json",
- "compilerOptions": {
- "outDir": "./dist",
- "rootDir": "./src",
- "sourceMap": true,
- "declaration": true,
- "moduleResolution": "bundler",
- "module": "ESNext"
- }
-}
\ No newline at end of file
diff --git a/packages/opex-common/eslint.config.js b/packages/opex-common/eslint.config.js
deleted file mode 100644
index 9011b4d7..00000000
--- a/packages/opex-common/eslint.config.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import lintRules from "@pagopa/eslint-config";
-
-export default [
- ...lintRules,
- {
- ignores: ["dist/*"],
- },
-];
diff --git a/packages/opex-common/package.json b/packages/opex-common/package.json
deleted file mode 100644
index 4084c88a..00000000
--- a/packages/opex-common/package.json
+++ /dev/null
@@ -1,25 +0,0 @@
-{
- "name": "opex-common",
- "version": "1.0.0",
- "private": true,
- "main": "dist/index.js",
- "types": "dist/index.d.ts",
- "type": "module",
- "scripts": {
- "build": "tsc",
- "typecheck": "tsc --noemit",
- "lint": "eslint src --fix",
- "lint:check": "eslint src"
- },
- "dependencies": {
- "cdktf": "^0.21.0",
- "cdktf-monitoring-stack": "workspace:*"
- },
- "devDependencies": {
- "@pagopa/eslint-config": "^5.0.0",
- "@pagopa/typescript-config-node": "workspace:^",
- "eslint": "^9.30.1",
- "prettier": "^3.6.2",
- "typescript": "^5.8.3"
- }
-}
diff --git a/packages/opex-common/src/index.ts b/packages/opex-common/src/index.ts
deleted file mode 100644
index 6337d265..00000000
--- a/packages/opex-common/src/index.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { AzurermBackendConfig } from "cdktf";
-import { MonitoringConfig } from "cdktf-monitoring-stack";
-
-export const opexConfig: MonitoringConfig = {
- actionGroupId:
- "/subscriptions/d7de83e0-0571-40ad-b63a-64c942385eae/resourceGroups/dev/providers/microsoft.insights/actionGroups/dx-d-itn-opex-rg-01",
- apimServiceName: "dx-d-itn-playground-pg-apim-01",
- location: "Italy North",
- logAnalyticsWorkspaceId:
- "/subscriptions/d7de83e0-0571-40ad-b63a-64c942385eae/resourceGroups/dx-d-itn-test-rg-01/providers/Microsoft.OperationalInsights/workspaces/dx-d-itn-playground-azure-function-v3-log-01",
- resourceGroupName: "dx-d-itn-opex-rg-01",
- tags: {
- BusinessUnit: "DevEx",
- CostCenter: "TS000 - Tecnologia e Servizi",
- CreatedBy: "Terraform",
- Environment: "Dev",
- ManagementTeam: "Developer Experience",
- Scope: "Opex PoC",
- Source:
- "https://github.com/pagopa/dx-playground/tree/main/infra/repository",
- },
-} as const;
-
-export const backendConfig: AzurermBackendConfig = {
- containerName: "terraform-state",
- key: "dx.opex.tfstate",
- resourceGroupName: "terraform-state-rg",
- storageAccountName: "tfdevdx",
-} as const;
diff --git a/packages/opex-common/tsconfig.json b/packages/opex-common/tsconfig.json
deleted file mode 100644
index 4c35d6d7..00000000
--- a/packages/opex-common/tsconfig.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "extends": "@pagopa/typescript-config-node/tsconfig.json",
- "compilerOptions": {
- "outDir": "./dist",
- "rootDir": "./src",
- "sourceMap": true,
- "declaration": true,
- "moduleResolution": "bundler",
- "module": "ESNext"
- }
-}
\ No newline at end of file
diff --git a/packages/opex-dashboard-ts/CONTRIBUTING.md b/packages/opex-dashboard-ts/CONTRIBUTING.md
new file mode 100644
index 00000000..37462fd1
--- /dev/null
+++ b/packages/opex-dashboard-ts/CONTRIBUTING.md
@@ -0,0 +1,310 @@
+# Contributing to Opex Dashboard TS
+
+Thank you for your interest in contributing to the Opex Dashboard TypeScript package!
+This document outlines the guidelines and best practices for contributing to this project.
+
+## Installation
+
+```bash
+# Clone the repository
+git clone
+cd packages/opex-dashboard-ts
+
+# Install dependencies
+yarn install
+
+# Build the project
+yarn build
+```
+
+## Architecture
+
+This project follows **Hexagonal Architecture** (Ports & Adapters) principles,
+separating business logic from infrastructure concerns for better testability,
+maintainability, and extensibility.
+
+### Core Principles
+
+- **Domain Layer**: Pure business logic, independent of frameworks
+- **Application Layer**: Use cases orchestrating domain services
+- **Infrastructure Layer**: Adapters for external systems (CLI, files, OpenAPI,
+ Terraform)
+
+### Layer Structure
+
+1. **Domain** (`src/domain/`)
+ - **Entities**: Core business objects (`Endpoint`, `DashboardConfig`)
+ - **Services**: Business logic (`EndpointParserService`, `KustoQueryService`)
+ - **Ports**: Interfaces for external dependencies
+
+2. **Application** (`src/application/`)
+ - **Use Cases**: Orchestrate domain services (`GenerateDashboardUseCase`)
+
+3. **Infrastructure** (`src/infrastructure/`)
+ - **Adapters**: Concrete implementations of ports
+ - CLI Adapter: Commander.js integration
+ - OpenAPI Adapter: SwaggerParser integration
+ - Terraform Adapter: CDKTF integration
+ - File Adapter: File system operations
+ - Config Adapter: Zod validation
+
+### Key Components
+
+#### Domain Services
+
+- **EndpointParserService**: Parses OpenAPI specs into endpoint configurations
+- **KustoQueryService**: Generates Azure Log Analytics queries
+
+#### Application Use Cases
+
+- **GenerateDashboardUseCase**: Main workflow for dashboard generation
+
+#### Infrastructure Adapters
+
+- **OpenAPISpecResolverAdapter**: Resolves OpenAPI specifications
+- **TerraformGeneratorAdapter**: Generates CDKTF code
+- **ConfigValidatorAdapter**: Validates configuration with Zod
+- **FileReaderAdapter**: Reads YAML configuration files
+
+### Benefits
+
+- **Testability**: Domain logic tested in isolation with mock adapters
+- **Extensibility**: Easy to add new adapters (e.g., AWS, different CLI
+ frameworks)
+- **Maintainability**: Clear separation of concerns
+- **Framework Independence**: Domain layer free from CDKTF/Commander
+ dependencies
+
+## API Documentation
+
+### Domain Layer
+
+#### Entities
+
+```typescript
+interface Endpoint {
+ path: string;
+ availabilityThreshold?: number;
+ responseTimeThreshold?: number;
+ // ... other properties
+}
+
+interface DashboardConfig {
+ oa3_spec: string;
+ name: string;
+ location: string;
+ data_source: string;
+ // ... other properties
+}
+```
+
+#### Domain Services
+
+```typescript
+class EndpointParserService {
+ parseEndpoints(spec: OpenAPISpec, config: DashboardConfig): Endpoint[];
+}
+
+class KustoQueryService {
+ buildAvailabilityQuery(endpoint: Endpoint, config: DashboardConfig): string;
+ buildResponseTimeQuery(endpoint: Endpoint, config: DashboardConfig): string;
+ buildResponseCodesQuery(endpoint: Endpoint, config: DashboardConfig): string;
+}
+```
+
+### Application Layer
+
+#### Use Cases
+
+```typescript
+class GenerateDashboardUseCase {
+ constructor(
+ fileReader: IFileReader,
+ configValidator: IConfigValidator,
+ openAPISpecResolver: IOpenAPISpecResolver,
+ endpointParser: IEndpointParser,
+ kustoQueryGenerator: IKustoQueryGenerator,
+ terraformGenerator: ITerraformGenerator,
+ ) {}
+
+ async execute(configFilePath: string): Promise;
+}
+```
+
+### Infrastructure Layer
+
+#### Adapters
+
+```typescript
+class OpenAPISpecResolverAdapter implements IOpenAPISpecResolver {
+ async resolve(specPath: string): Promise;
+}
+
+class TerraformGeneratorAdapter implements ITerraformGenerator {
+ async generate(config: DashboardConfig): Promise;
+}
+
+class ConfigValidatorAdapter implements IConfigValidator {
+ validateConfig(rawConfig: unknown): DashboardConfig;
+}
+
+class FileReaderAdapter implements IFileReader {
+ async readYamlFile(filePath: string): Promise;
+}
+```
+
+## Example: Programmatic Usage
+
+```typescript
+import { GenerateDashboardUseCase } from "./src/application/index.js";
+import { FileReaderAdapter } from "./src/infrastructure/file/file-reader-adapter.js";
+import { ConfigValidatorAdapter } from "./src/infrastructure/config/config-validator-adapter.js";
+import { OpenAPISpecResolverAdapter } from "./src/infrastructure/openapi/openapi-spec-resolver-adapter.js";
+import { EndpointParserService } from "./src/domain/services/endpoint-parser-service.js";
+import { KustoQueryService } from "./src/domain/services/kusto-query-service.js";
+import { TerraformGeneratorAdapter } from "./src/infrastructure/terraform/terraform-generator-adapter.js";
+
+async function generateDashboard() {
+ // Create adapters
+ const fileReader = new FileReaderAdapter();
+ const configValidator = new ConfigValidatorAdapter();
+ const openAPISpecResolver = new OpenAPISpecResolverAdapter();
+ const endpointParser = new EndpointParserService();
+ const kustoQueryGenerator = new KustoQueryService();
+ const terraformGenerator = new TerraformGeneratorAdapter();
+
+ // Create use case
+ const useCase = new GenerateDashboardUseCase(
+ fileReader,
+ configValidator,
+ openAPISpecResolver,
+ endpointParser,
+ kustoQueryGenerator,
+ terraformGenerator,
+ );
+
+ // Execute
+ await useCase.execute("./config.yaml");
+}
+```
+
+## Testing
+
+### Running Tests
+
+```bash
+# Run all tests
+yarn test
+
+# Run with coverage
+yarn test --coverage
+
+# Run specific test file
+yarn test resolver.test.ts
+
+# Watch mode
+yarn test --watch
+```
+
+### Test Structure
+
+```
+test/
+├── unit/
+│ ├── resolver.test.ts # OpenAPI resolver tests
+│ ├── endpoint-parser.test.ts # Endpoint parsing tests
+│ ├── kusto-queries.test.ts # Query generation tests
+│ └── cli.test.ts # CLI tests
+└── integration/ # Integration tests (future)
+```
+
+### Test Coverage
+
+Current test coverage includes:
+
+- ✅ OpenAPI specification parsing
+- ✅ Endpoint extraction and configuration
+- ✅ Kusto query generation for both resource types
+- ✅ CLI command structure and options
+- ✅ Error handling and edge cases
+
+## Development
+
+### Prerequisites
+
+- Node.js 18+
+- Yarn 1.22+
+- Azure CLI (for deployment)
+
+### Development Workflow
+
+```bash
+# Install dependencies
+yarn install
+
+# Build in watch mode
+yarn watch
+
+# Run tests in watch mode
+yarn test --watch
+
+# Lint code
+yarn lint
+
+# Format code
+yarn format
+```
+
+### Project Structure
+
+```
+packages/opex-dashboard-ts/
+├── src/
+│ ├── domain/ # Business logic layer
+│ │ ├── entities/ # Core business objects
+│ │ │ ├── endpoint.ts # Endpoint entity
+│ │ │ └── dashboard-config.ts # Configuration entity
+│ │ ├── services/ # Domain services
+│ │ │ ├── endpoint-parser-service.ts
+│ │ │ └── kusto-query-service.ts
+│ │ ├── ports/ # Interface definitions
+│ │ │ └── index.ts
+│ │ └── index.ts # Domain exports
+│ ├── application/ # Application layer
+│ │ ├── use-cases/ # Use case implementations
+│ │ │ └── generate-dashboard-use-case.ts
+│ │ └── index.ts # Application exports
+│ ├── infrastructure/ # Infrastructure layer
+│ │ ├── cli/ # CLI adapter
+│ │ │ ├── index.ts
+│ │ │ └── generate.ts
+│ │ ├── openapi/ # OpenAPI adapter
+│ │ │ └── openapi-spec-resolver-adapter.ts
+│ │ ├── terraform/ # Terraform adapter
+│ │ │ ├── azure-dashboard.ts
+│ │ │ ├── azure-alerts.ts
+│ │ │ ├── dashboard-properties.ts
+│ │ │ └── terraform-generator-adapter.ts
+│ │ ├── file/ # File adapter
+│ │ │ └── file-reader-adapter.ts
+│ │ ├── config/ # Config adapter
+│ │ │ └── config-validator-adapter.ts
+│ │ └── index.ts # Infrastructure exports
+│ ├── shared/ # Shared utilities
+│ │ └── openapi.ts # OpenAPI type guards
+│ └── index.ts # Main exports
+├── test/ # Test files
+│ └── unit/ # Unit tests
+├── examples/ # Example configurations
+├── package.json
+├── tsconfig.json
+└── README.md
+```
+
+## Debug Mode
+
+Enable debug logging:
+
+```bash
+DEBUG=cdktf:* yarn tsx src/cli/index.ts generate --config-file config.yaml
+```
diff --git a/packages/opex-dashboard-ts/README.md b/packages/opex-dashboard-ts/README.md
new file mode 100644
index 00000000..79955706
--- /dev/null
+++ b/packages/opex-dashboard-ts/README.md
@@ -0,0 +1,181 @@
+# OpEx Dashboard TypeScript
+
+**Generate standardized PagoPA's Operational Excellence dashboards from OpenAPI
+specs using TypeScript and CDKTF.**
+
+## Overview
+
+This is a TypeScript port of the Python
+[opex-dashboard](https://github.com/pagopa/opex-dashboard) project. It generates
+Azure dashboards and alerts from OpenAPI specifications using CDK for Terraform
+(CDKTF), maintaining exact compatibility with the Python version.
+
+## Features
+
+- ** Scheduled Alerts**: Generates Azure Monitor alerts for availability and
+ response time
+- **📋 OpenAPI Support**: Parses Swagger and OpenAPI 3.x specifications
+- **🏗️ CDKTF Integration**: Uses CDK for Terraform for infrastructure as code
+- **🔒 Type Safety**: Full TypeScript support with comprehensive type checking
+- **⚡ Exact Replication**: Maintains 100% compatibility with Python version
+ output
+
+## Quick Start
+
+1. **Create a configuration file** (`config.yaml`):
+
+```yaml
+oa3_spec: ./examples/petstore.yaml
+name: PetStore Dashboard
+resource_group_name: dashboards # the dashboard and alerts will be created in this RG
+# Location is inherited from the Resource Group (no need to specify it)
+data_source: /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/applicationGateways/xxx
+resource_type: app-gateway
+action_groups:
+ - /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Insights/actionGroups/xxx
+tags:
+ BusinessUnit: PAGOPA
+ Environment: DEV
+```
+
+2. **Generate Terraform (CDKTF) code**:
+
+```bash
+yarn tsx src/cli/index.ts generate --config-file config.yaml
+```
+
+## Configuration
+
+The configuration format is identical to the Python version:
+
+### Basic Configuration
+
+```yaml
+# Required fields
+oa3_spec: ./path/to/openapi.yaml # Path to OpenAPI spec file
+name: My API Dashboard # Dashboard name
+resource_group_name: dashboards # Resource Group to host the dashboard and alerts
+data_source: /subscriptions/.../applicationGateways/my-gtw # Resource ID used in queries
+
+# Optional fields
+resource_type: app-gateway # 'app-gateway' or 'api-management' (default: app-gateway)
+timespan: 5m # Dashboard timespan (default: 5m)
+action_groups: # Action groups for alerts
+ - /subscriptions/.../actionGroups/my-action-group
+tags: # Tags applied to dashboard and alerts
+ CostCenter: CC123
+ Environment: DEV
+```
+
+### Advanced Configuration
+
+For each host and endpoint, you can override default settings:
+
+```yaml
+# Override defaults
+overrides:
+ hosts:
+ - api.example.com
+ - staging.api.example.com
+ endpoints:
+ /api/users:
+ availability_threshold: 0.95
+ response_time_threshold: 2.0
+ /api/orders:
+ enabled: false # Disable monitoring for this endpoint
+```
+
+### Configuration Reference
+
+| Field | Type | Required | Default | Description |
+| --------------------- | --------------------- | -------- | ------------- | -------------------------------------------------------------------------- |
+| `oa3_spec` | string | ✅ | - | Path/URL to OpenAPI specification |
+| `name` | string | ✅ | - | Dashboard name |
+| `resource_group_name` | string | ✅ | `dashboards` | Resource Group where dashboard and alerts will be created |
+| `data_source` | string | ✅ | - | Azure resource ID used by KQL queries (e.g., Application Gateway resource) |
+| `resource_type` | string | ❌ | `app-gateway` | Resource type (`app-gateway` or `api-management`) |
+| `timespan` | string | ❌ | `5m` | Dashboard timespan |
+| `action_groups` | string[] | ❌ | - | Action groups for alerts |
+| `tags` | Record | ❌ | - | Tags applied to dashboard and alerts |
+| `overrides` | object | ❌ | - | Override default settings for hosts/endpoints |
+
+Notes:
+
+- The Azure location is inferred from the specified `resource_group_name` via data source lookup; do not set `location` in config.
+
+## Migration
+
+This project now inherits the Azure location from the specified Resource Group and supports applying tags to both the dashboard and alerts. If you're upgrading from an earlier version where `location` was set explicitly in the configuration, follow the steps below.
+
+### What changed
+
+- Location is no longer read from the configuration file. It is resolved at synth time from the Resource Group via a data source. The `location` key, if present, is ignored.
+- A new optional `tags` block can be specified and will be applied to the `azurerm_portal_dashboard` and all `azurerm_monitor_scheduled_query_rules_alert` resources.
+
+### How to update your configuration
+
+1. Ensure `resource_group_name` points to the target Azure Resource Group. The resources will inherit the location of this group.
+2. Remove the `location` field from your YAML (it will be ignored if left in place).
+3. Optionally add a `tags` map to propagate tags to the dashboard and alerts.
+
+Before (old config):
+
+```yaml
+oa3_spec: ./examples/petstore.yaml
+name: PetStore Dashboard
+location: westeurope
+data_source: /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/applicationGateways/xxx
+resource_type: app-gateway
+action_groups:
+ - /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Insights/actionGroups/xxx
+```
+
+After (new config):
+
+```yaml
+oa3_spec: ./examples/petstore.yaml
+name: PetStore Dashboard
+resource_group_name: dashboards # location is inherited from this RG
+data_source: /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Network/applicationGateways/xxx
+resource_type: app-gateway
+action_groups:
+ - /subscriptions/xxx/resourceGroups/xxx/providers/Microsoft.Insights/actionGroups/xxx
+tags:
+ BusinessUnit: PAGOPA
+ Environment: DEV
+```
+
+You can omit `resource_group_name` which defaults to `dashboards`.
+
+### Impact on Terraform plans
+
+- If your previous `location` matched the Resource Group's location, you should see no resource recreation due to location. The dashboard and alerts will continue to be deployed in the same region.
+- If your previous `location` differed from the Resource Group's location, Terraform may propose to replace resources to align with the RG location. Align the Resource Group or accept the plan as appropriate.
+- Adding `tags` will result in in-place updates where supported.
+
+### Programmatic usage (TypeScript)
+
+- The `DashboardConfig` type now treats `location` as optional and it is not used. Set `resource_group_name` to control the deployment location. You may also set the new optional `tags: Record` property.
+
+### FAQ
+
+- Can I still specify `location` in my config? Yes, but it is ignored. The only source of truth for location is the Resource Group.
+- Do I need to migrate immediately? No. Leaving `location` in your YAML won't break generation, but removing it is recommended to avoid confusion.
+
+## Examples
+
+### Example 1: Basic Dashboard Generation
+
+```bash
+# Generate Terraform CDK code
+yarn tsx src/cli/index.ts generate \
+ --config-file examples/basic-config.yaml
+```
+
+## Related Projects
+
+- [opex-dashboard (Python)](https://github.com/pagopa/opex-dashboard) - Original
+ Python implementation
+- [CDK for Terraform](https://www.terraform.io/cdktf) - CDKTF documentation
+- [Azure Monitor](https://docs.microsoft.com/en-us/azure/azure-monitor/) - Azure
+ monitoring documentation
diff --git a/packages/cdktf-monitoring-stack/eslint.config.js b/packages/opex-dashboard-ts/eslint.config.js
similarity index 63%
rename from packages/cdktf-monitoring-stack/eslint.config.js
rename to packages/opex-dashboard-ts/eslint.config.js
index 9011b4d7..ebf6e6e3 100644
--- a/packages/cdktf-monitoring-stack/eslint.config.js
+++ b/packages/opex-dashboard-ts/eslint.config.js
@@ -1,8 +1,8 @@
import lintRules from "@pagopa/eslint-config";
export default [
- ...lintRules,
{
- ignores: ["dist/*"],
+ ignores: ["test/**/*", "**/*local*", "examples"],
},
+ ...lintRules,
];
diff --git a/packages/opex-dashboard-ts/package.json b/packages/opex-dashboard-ts/package.json
new file mode 100644
index 00000000..9b0a8533
--- /dev/null
+++ b/packages/opex-dashboard-ts/package.json
@@ -0,0 +1,67 @@
+{
+ "name": "@pagopa/opex-dashboard-ts",
+ "version": "0.0.1",
+ "description": "Generate standardized Operational Excellence dashboards from OpenAPI specs using TypeScript and CDKTF",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "bin": "dist/infrastructure/cli/index.js",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js",
+ "require": "./dist/index.cjs"
+ },
+ "./cli": {
+ "types": "./dist/infrastructure/cli/index.d.ts",
+ "import": "./dist/infrastructure/cli/index.js",
+ "require": "./dist/infrastructure/cli/index.cjs"
+ }
+ },
+ "scripts": {
+ "build": "tsup",
+ "lint": "eslint --fix src",
+ "lint:check": "eslint src",
+ "format": "prettier --write .",
+ "format:check": "prettier --check .",
+ "typecheck": "tsc --noEmit",
+ "test": "vitest run",
+ "test:coverage": "vitest --coverage",
+ "watch": "tsc --watch",
+ "cdktf:deploy": "cdktf deploy",
+ "clean": "rm -rf dist cdktf.out"
+ },
+ "keywords": [
+ "opex",
+ "dashboard",
+ "azure",
+ "terraform",
+ "cdkt",
+ "openapi"
+ ],
+ "author": "PagoPA",
+ "license": "MIT",
+ "dependencies": {
+ "@apidevtools/swagger-parser": "^10.1.0",
+ "@cdktf/provider-azurerm": "^14.12.0",
+ "cdktf": "^0.21.0",
+ "commander": "^12.0.0",
+ "constructs": "^10.3.0",
+ "js-yaml": "^4.1.0",
+ "openapi-types": "^12.1.3",
+ "zod": "^4.1.5"
+ },
+ "devDependencies": {
+ "@pagopa/eslint-config": "^5.0.0",
+ "@tsconfig/node22": "^22.0.2",
+ "@types/js-yaml": "^4.0.5",
+ "@types/node": "^20.0.0",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
+ "eslint": "^8.0.0",
+ "prettier": "^3.6.2",
+ "tsup": "^8.5.0",
+ "typescript": "^5.0.0",
+ "vitest": "^3.2.4"
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/application/index.ts b/packages/opex-dashboard-ts/src/application/index.ts
new file mode 100644
index 00000000..9b67ff34
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/application/index.ts
@@ -0,0 +1,2 @@
+export * from "./use-cases/create-dashboard-stack-use-case.js";
+export * from "./use-cases/generate-dashboard-use-case.js";
diff --git a/packages/opex-dashboard-ts/src/application/use-cases/create-dashboard-stack-use-case.ts b/packages/opex-dashboard-ts/src/application/use-cases/create-dashboard-stack-use-case.ts
new file mode 100644
index 00000000..46b53e2b
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/application/use-cases/create-dashboard-stack-use-case.ts
@@ -0,0 +1,42 @@
+import { App } from "cdktf";
+
+import {
+ Endpoint,
+ IConfigValidator,
+ IEndpointParser,
+ IOpenAPISpecResolver,
+ ITerraformStackGenerator,
+} from "../../domain/index.js";
+import { AzureOpexStack } from "../../infrastructure/terraform/azure-dashboard.js";
+
+export class CreateDashboardStackUseCase {
+ constructor(
+ private readonly configValidator: IConfigValidator,
+ private readonly openAPISpecResolver: IOpenAPISpecResolver,
+ private readonly endpointParser: IEndpointParser,
+ private readonly terraformGenerator: ITerraformStackGenerator,
+ ) {}
+
+ async execute(config: unknown, app: App): Promise {
+ // Validate configuration
+ const validatedConfig = this.configValidator.validateConfig(config);
+
+ // Resolve OpenAPI spec and extract hosts
+ const { hosts: extractedHosts, spec } =
+ await this.openAPISpecResolver.resolveWithHosts(validatedConfig.oa3_spec);
+
+ // Parse endpoints
+ const endpoints: Endpoint[] = this.endpointParser.parseEndpoints(
+ spec,
+ validatedConfig,
+ );
+
+ // Update config with parsed data
+ validatedConfig.endpoints = endpoints;
+ validatedConfig.hosts = validatedConfig.overrides?.hosts || extractedHosts;
+ validatedConfig.resourceIds = [validatedConfig.data_source];
+
+ // Generate and return the stack
+ return await this.terraformGenerator.generate(validatedConfig, app);
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/application/use-cases/generate-dashboard-use-case.ts b/packages/opex-dashboard-ts/src/application/use-cases/generate-dashboard-use-case.ts
new file mode 100644
index 00000000..02a26e38
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/application/use-cases/generate-dashboard-use-case.ts
@@ -0,0 +1,44 @@
+import {
+ Endpoint,
+ IConfigValidator,
+ IEndpointParser,
+ IFileReader,
+ IOpenAPISpecResolver,
+ ITerraformFileGenerator,
+} from "../../domain/index.js";
+
+export class GenerateDashboardUseCase {
+ constructor(
+ private readonly fileReader: IFileReader,
+ private readonly configValidator: IConfigValidator,
+ private readonly openAPISpecResolver: IOpenAPISpecResolver,
+ private readonly endpointParser: IEndpointParser,
+ private readonly terraformGenerator: ITerraformFileGenerator,
+ ) {}
+
+ async execute(configFilePath: string): Promise {
+ // Load and parse configuration
+ const rawConfig = await this.fileReader.readYamlFile(configFilePath);
+
+ // Validate configuration
+ const validatedConfig = this.configValidator.validateConfig(rawConfig);
+
+ // Resolve OpenAPI spec and extract hosts
+ const { hosts: extractedHosts, spec } =
+ await this.openAPISpecResolver.resolveWithHosts(validatedConfig.oa3_spec);
+
+ // Parse endpoints
+ const endpoints: Endpoint[] = this.endpointParser.parseEndpoints(
+ spec,
+ validatedConfig,
+ );
+
+ // Update config with parsed data
+ validatedConfig.endpoints = endpoints;
+ validatedConfig.hosts = validatedConfig.overrides?.hosts || extractedHosts;
+ validatedConfig.resourceIds = [validatedConfig.data_source];
+
+ // Generate Terraform code
+ await this.terraformGenerator.generate(validatedConfig);
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/core/resolver.ts b/packages/opex-dashboard-ts/src/core/resolver.ts
new file mode 100644
index 00000000..9a620dce
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/core/resolver.ts
@@ -0,0 +1,24 @@
+import SwaggerParser from "@apidevtools/swagger-parser";
+
+import { OpenAPISpec } from "../shared/openapi.js";
+
+export class OA3Resolver {
+ async resolve(specPath: string): Promise {
+ try {
+ const spec = await SwaggerParser.parse(specPath);
+ return spec as OpenAPISpec;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new ParseError(`OA3 parsing error: ${error.message}`);
+ }
+ throw new ParseError(`OA3 parsing error: ${String(error)}`);
+ }
+ }
+}
+
+export class ParseError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "ParseError";
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/domain/entities/dashboard-config.ts b/packages/opex-dashboard-ts/src/domain/entities/dashboard-config.ts
new file mode 100644
index 00000000..d6a5f835
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/entities/dashboard-config.ts
@@ -0,0 +1,37 @@
+import { z } from "zod";
+
+import { EndpointSchema } from "./endpoint.js";
+
+// Zod schema for DashboardConfig
+export const DashboardConfigSchema = z.object({
+ action_groups: z.array(z.string()).optional(),
+ data_source: z.string(),
+ endpoints: z.array(EndpointSchema).optional(),
+ evaluation_frequency: z.number().default(10),
+ evaluation_time_window: z.number().default(20),
+ event_occurrences: z.number().default(1),
+ // Computed properties (optional in input)
+ hosts: z.array(z.string()).optional(),
+ location: z.string().optional(),
+ name: z.string(),
+ oa3_spec: z.string(),
+ overrides: z
+ .object({
+ endpoints: z.record(z.string(), EndpointSchema.partial()).optional(),
+ hosts: z.array(z.string()).optional(),
+ })
+ .optional(),
+ resource_group_name: z.string().default("dashboards"),
+ resource_type: z
+ .enum(["app-gateway", "api-management"])
+ .default("app-gateway"),
+ resourceIds: z.array(z.string()).optional(),
+ tags: z.record(z.string(), z.string()).optional(),
+ timespan: z.string().default("5m"),
+});
+
+// Fields with defaults applied can be omitted in input
+export type DashboardConfig = z.input;
+
+// Inferred types from Zod schemas
+export type ValidDashboardConfig = z.infer;
diff --git a/packages/opex-dashboard-ts/src/domain/entities/endpoint.ts b/packages/opex-dashboard-ts/src/domain/entities/endpoint.ts
new file mode 100644
index 00000000..b7e622ff
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/entities/endpoint.ts
@@ -0,0 +1,17 @@
+import { z } from "zod";
+
+// Zod schema for Endpoint
+export const EndpointSchema = z.object({
+ availability_evaluation_frequency: z.number().default(10),
+ availability_evaluation_time_window: z.number().default(20),
+ availability_event_occurrences: z.number().default(1),
+ availability_threshold: z.number().default(0.99),
+ path: z.string(),
+ response_time_evaluation_frequency: z.number().default(10),
+ response_time_evaluation_time_window: z.number().default(20),
+ response_time_event_occurrences: z.number().default(1),
+ response_time_threshold: z.number().default(1),
+});
+
+// Inferred types from Zod schemas
+export type Endpoint = z.infer;
diff --git a/packages/opex-dashboard-ts/src/domain/entities/panel.ts b/packages/opex-dashboard-ts/src/domain/entities/panel.ts
new file mode 100644
index 00000000..ae2f41d8
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/entities/panel.ts
@@ -0,0 +1,21 @@
+export interface Panel {
+ chart: {
+ inputSpecificChart: string;
+ settingsSpecificChart: string;
+ };
+ dimensions: {
+ aggregation?: string;
+ splitBy?: { name: string; type: string }[];
+ xAxis: { name: string; type: string };
+ yAxis: { name: string; type: string }[];
+ };
+ id: number;
+ kind: PanelKind;
+ path: string;
+ position: { colSpan: number; rowSpan: number; x: number; y: number };
+ query: string;
+ subtitle: string;
+ title: string;
+}
+
+export type PanelKind = "availability" | "response-codes" | "response-time";
diff --git a/packages/opex-dashboard-ts/src/domain/index.ts b/packages/opex-dashboard-ts/src/domain/index.ts
new file mode 100644
index 00000000..ca3bb7a7
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/index.ts
@@ -0,0 +1,8 @@
+export * from "../shared/openapi.js";
+export * from "./entities/dashboard-config.js";
+export * from "./entities/endpoint.js";
+export * from "./entities/panel.js";
+export * from "./ports/index.js";
+export * from "./services/endpoint-parser-service.js";
+export * from "./services/kusto-query-service.js";
+export * from "./services/panel-factory.js";
diff --git a/packages/opex-dashboard-ts/src/domain/ports/index.ts b/packages/opex-dashboard-ts/src/domain/ports/index.ts
new file mode 100644
index 00000000..6a593b7d
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/ports/index.ts
@@ -0,0 +1,49 @@
+export interface IConfigValidator {
+ validateConfig(rawConfig: unknown): ValidDashboardConfig;
+}
+
+export interface IEndpointParser {
+ parseEndpoints(spec: OpenAPISpec, config: ValidDashboardConfig): Endpoint[];
+}
+
+export interface IFileReader {
+ readYamlFile(filePath: string): Promise;
+}
+
+export interface IKustoQueryGenerator {
+ buildAvailabilityQuery(
+ endpoint: Endpoint,
+ config: ValidDashboardConfig,
+ ): string;
+ buildResponseCodesQuery(
+ endpoint: Endpoint,
+ config: ValidDashboardConfig,
+ ): string;
+ buildResponseTimeQuery(
+ endpoint: Endpoint,
+ config: ValidDashboardConfig,
+ ): string;
+}
+
+export interface IOpenAPISpecResolver {
+ resolve(specPath: string): Promise;
+ resolveWithHosts(
+ specPath: string,
+ ): Promise<{ hosts: string[]; spec: OpenAPISpec }>;
+}
+
+export interface ITerraformFileGenerator {
+ generate(config: ValidDashboardConfig): Promise;
+}
+
+export interface ITerraformStackGenerator {
+ generate(config: ValidDashboardConfig, app: App): Promise;
+}
+
+import type { App } from "cdktf";
+
+import type { AzureOpexStack } from "../../infrastructure/terraform/azure-dashboard.js";
+// Import types
+import type { OpenAPISpec } from "../../shared/openapi.js";
+import type { ValidDashboardConfig } from "../entities/dashboard-config.js";
+import type { Endpoint } from "../entities/endpoint.js";
diff --git a/packages/opex-dashboard-ts/src/domain/services/endpoint-parser-service.ts b/packages/opex-dashboard-ts/src/domain/services/endpoint-parser-service.ts
new file mode 100644
index 00000000..eef65a74
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/services/endpoint-parser-service.ts
@@ -0,0 +1,66 @@
+import { isOpenAPIV2, isOpenAPIV3, OpenAPISpec } from "../../shared/openapi.js";
+import { ValidDashboardConfig } from "../entities/dashboard-config.js";
+import { Endpoint, EndpointSchema } from "../entities/endpoint.js";
+
+export class EndpointParserService {
+ parseEndpoints(spec: OpenAPISpec, config: ValidDashboardConfig): Endpoint[] {
+ const endpoints: Endpoint[] = [];
+ const hosts = this.extractHosts(spec);
+ const paths = Object.keys(spec.paths);
+
+ for (const host of hosts) {
+ for (const path of paths) {
+ const endpointPath = this.buildEndpointPath(host, path, spec);
+ const endpoint = EndpointSchema.parse({
+ path: endpointPath,
+ ...this.getEndpointOverrides(endpointPath, config.overrides),
+ });
+ endpoints.push(endpoint);
+ }
+ }
+
+ return endpoints;
+ }
+
+ private buildEndpointPath(
+ host: string,
+ path: string,
+ spec: OpenAPISpec,
+ ): string {
+ if (host.startsWith("http")) {
+ const url = new URL(host);
+ return `${url.pathname}${path}`.replace(/\/+/g, "/");
+ } else {
+ // For OpenAPI 2.x, use basePath if available
+ const basePath = isOpenAPIV2(spec) ? spec.basePath || "" : "";
+ return `${basePath}${path}`.replace(/\/+/g, "/");
+ }
+ }
+
+ private extractHosts(spec: OpenAPISpec): string[] {
+ if (isOpenAPIV3(spec)) {
+ // OpenAPI 3.x uses servers array
+ if (spec.servers && spec.servers.length > 0) {
+ return spec.servers.map((server) => server.url);
+ }
+ } else if (isOpenAPIV2(spec)) {
+ // OpenAPI 2.x uses host and basePath
+ if (spec.host && spec.basePath) {
+ return [`${spec.host}${spec.basePath}`];
+ } else if (spec.host) {
+ return [spec.host];
+ }
+ }
+ return [];
+ }
+
+ private getEndpointOverrides(
+ endpointPath: string,
+ overrides?: ValidDashboardConfig["overrides"],
+ ): Partial {
+ if (!overrides?.endpoints) {
+ return {};
+ }
+ return overrides.endpoints[endpointPath] || {};
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/domain/services/kusto-query-service.ts b/packages/opex-dashboard-ts/src/domain/services/kusto-query-service.ts
new file mode 100644
index 00000000..39f0d33c
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/services/kusto-query-service.ts
@@ -0,0 +1,137 @@
+import { ValidDashboardConfig } from "../entities/dashboard-config.js";
+import { Endpoint } from "../entities/endpoint.js";
+
+/*
+ KustoQueryService now supports two contexts:
+ - "dashboard": return full time-series with watermark and render clause (no threshold filtering)
+ - "alert": return filtered series (apply threshold predicate) suitable for scheduled query alerts
+ Default remains "alert" to preserve existing alert generation behaviour.
+*/
+
+export type QueryContext = "alert" | "dashboard";
+
+export class KustoQueryService {
+ buildAvailabilityQuery(
+ endpoint: Endpoint,
+ config: ValidDashboardConfig,
+ context: QueryContext = "alert",
+ ): string {
+ const threshold = endpoint.availability_threshold ?? 0.99;
+ const regex = this.createGenericRegex(endpoint.path);
+
+ const base =
+ config.resource_type === "api-management"
+ ? `AzureDiagnostics
+| where url_s matches regex "${regex}"
+| summarize Total=count(), Success=count(responseCode_d < 500) by bin(TimeGenerated, ${config.timespan})
+| extend availability=toreal(Success) / Total`
+ : `${this.createHostsDataTable(config.hosts || [])}
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "${regex}"
+| summarize
+ Total=count(),
+ Success=count(httpStatus_d < 500) by bin(TimeGenerated, ${config.timespan})
+| extend availability=toreal(Success) / Total`;
+
+ if (context === "dashboard") {
+ // Keep full series with watermark line for visualization
+ return `let threshold = ${threshold};
+${base}
+| extend watermark=threshold
+| project TimeGenerated, availability, watermark
+| render timechart with (xtitle = "time", ytitle= "availability(%)")`;
+ }
+ // alert context: filtered series to fire on breaches
+ return `let threshold = ${threshold};
+${base}
+| where availability < threshold`;
+ }
+
+ buildResponseCodesQuery(
+ endpoint: Endpoint,
+ config: ValidDashboardConfig,
+ ): string {
+ /*
+ Aggregate HTTP status into families (1XX .. 5XX) to reduce noise and align with reference dashboard.
+ Anything outside the standard ranges will be bucketed as "Other".
+ */
+ const regex = this.createGenericRegex(endpoint.path);
+ const timeBin = config.timespan;
+ const codeColumn =
+ config.resource_type === "api-management"
+ ? "responseCode_d"
+ : "httpStatus_d";
+ const sourceFilter =
+ config.resource_type === "api-management"
+ ? `AzureDiagnostics\n| where url_s matches regex "${regex}"`
+ : `${this.createHostsDataTable(config.hosts || [])}\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex "${regex}"`;
+
+ return `${sourceFilter}
+| extend HTTPStatus = case(${codeColumn} between (100 .. 199), "1XX", ${codeColumn} between (200 .. 299), "2XX", ${codeColumn} between (300 .. 399), "3XX", ${codeColumn} between (400 .. 499), "4XX", ${codeColumn} between (500 .. 599), "5XX", "Other")
+| summarize count_ = count() by bin(TimeGenerated, ${timeBin}), HTTPStatus
+| render timechart with (xtitle = "time", ytitle = "count")`;
+ }
+
+ buildResponseTimeQuery(
+ endpoint: Endpoint,
+ config: ValidDashboardConfig,
+ context: QueryContext = "alert",
+ ): string {
+ const threshold = endpoint.response_time_threshold ?? 1;
+ const regex = this.createGenericRegex(endpoint.path);
+
+ if (config.resource_type === "api-management") {
+ if (context === "dashboard") {
+ return `let threshold = ${threshold};
+AzureDiagnostics
+| where url_s matches regex "${regex}"
+| summarize duration_percentile_95_ms = percentile(DurationMs, 95) by bin(TimeGenerated, ${config.timespan})
+| extend duration_percentile_95 = duration_percentile_95_ms / 1000.0, watermark = threshold
+| render timechart with (xtitle = "time", ytitle = "response time (s)")`;
+ }
+ return `let threshold = ${threshold};
+AzureDiagnostics
+| where url_s matches regex "${regex}"
+| summarize duration_percentile_95_ms = percentile(DurationMs, 95) by bin(TimeGenerated, ${config.timespan})
+| extend duration_percentile_95 = duration_percentile_95_ms / 1000.0
+| where duration_percentile_95 > threshold`;
+ }
+
+ const hosts = config.hosts || [];
+ const hostsDataTable = this.createHostsDataTable(hosts);
+ if (context === "dashboard") {
+ return `let threshold = ${threshold};
+${hostsDataTable}
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "${regex}"
+| summarize duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, ${config.timespan})
+| extend watermark=threshold
+| render timechart with (xtitle = "time", ytitle = "response time (s)")`;
+ }
+ return `let threshold = ${threshold};
+${hostsDataTable}
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "${regex}"
+| summarize duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, ${config.timespan})
+| extend watermark=threshold
+| where duration_percentile_95 > threshold`;
+ }
+
+ private createGenericRegex(path: string): string {
+ // Convert path like "/api/v1/services/{service_id}" to "/api/v1/services/[^/]+$"
+ return (
+ path
+ .replace(/\{[^}]+\}/g, "[^/]+") // Replace {param} with [^/]+
+ .replace(/[.*?${}()|\\]/g, "\\$&") + // Escape regex special chars (but not [ ] ^ +)
+ "$"
+ ); // Add end anchor
+ }
+
+ private createHostsDataTable(hosts: string[]): string {
+ const hostsArray = hosts.map((host) => `"${host}"`).join(", ");
+ return `let api_hosts = datatable (name: string) [${hostsArray}];`;
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/domain/services/panel-factory.ts b/packages/opex-dashboard-ts/src/domain/services/panel-factory.ts
new file mode 100644
index 00000000..cd1c0b97
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/services/panel-factory.ts
@@ -0,0 +1,90 @@
+import { ValidDashboardConfig } from "../entities/dashboard-config.js";
+import { Panel } from "../entities/panel.js";
+import { KustoQueryService } from "./kusto-query-service.js";
+import { normalizePathPlaceholders } from "./path-utils.js";
+
+/*
+ PanelFactory converts Endpoints into a list of logical Panel models (domain) decoupled
+ from Terraform/JSON serialization. This makes layout rules and future panel kinds testable
+ without touching infrastructure adapters.
+*/
+export class PanelFactory {
+ private readonly queryService = new KustoQueryService();
+
+ buildPanels(config: ValidDashboardConfig): Panel[] {
+ if (!config.endpoints) return [];
+
+ const panels: Panel[] = [];
+
+ config.endpoints.forEach((endpoint, idx) => {
+ const rowY = idx * 4;
+ // Availability
+ panels.push({
+ chart: { inputSpecificChart: "Line", settingsSpecificChart: "Line" },
+ dimensions: {
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [
+ { name: "availability", type: "real" },
+ { name: "watermark", type: "real" },
+ ],
+ },
+ id: idx * 3,
+ kind: "availability",
+ path: endpoint.path,
+ position: { colSpan: 6, rowSpan: 4, x: 0, y: rowY },
+ query: this.queryService.buildAvailabilityQuery(
+ endpoint,
+ config,
+ "dashboard",
+ ),
+ subtitle: normalizePathPlaceholders(endpoint.path),
+ title: `Availability (${config.timespan})`,
+ });
+ // Response Codes
+ panels.push({
+ chart: {
+ inputSpecificChart: "Pie",
+ settingsSpecificChart: "StackedArea",
+ },
+ dimensions: {
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [{ name: "count_", type: "long" }],
+ },
+ id: idx * 3 + 1,
+ kind: "response-codes",
+ path: endpoint.path,
+ position: { colSpan: 6, rowSpan: 4, x: 6, y: rowY },
+ query: this.queryService.buildResponseCodesQuery(endpoint, config),
+ subtitle: normalizePathPlaceholders(endpoint.path),
+ title: `Response Codes (${config.timespan})`,
+ });
+ // Response Time
+ panels.push({
+ chart: {
+ inputSpecificChart: "StackedColumn",
+ settingsSpecificChart: "Line",
+ },
+ dimensions: {
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [
+ { name: "duration_percentile_95", type: "real" },
+ { name: "watermark", type: "long" },
+ ],
+ },
+ id: idx * 3 + 2,
+ kind: "response-time",
+ path: endpoint.path,
+ position: { colSpan: 6, rowSpan: 4, x: 12, y: rowY },
+ query: this.queryService.buildResponseTimeQuery(
+ endpoint,
+ config,
+ "dashboard",
+ ),
+ subtitle: normalizePathPlaceholders(endpoint.path),
+ title: `Percentile Response Time (${config.timespan})`,
+ });
+ });
+
+ return panels;
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/domain/services/path-utils.ts b/packages/opex-dashboard-ts/src/domain/services/path-utils.ts
new file mode 100644
index 00000000..7b3e2b8e
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/domain/services/path-utils.ts
@@ -0,0 +1,13 @@
+/* Utility functions for path placeholder normalization */
+
+/* Convert placeholders like {serviceId} or {ServiceID} to {service_id} for display */
+export function normalizePathPlaceholders(path: string): string {
+ return path.replace(/\{([^}]+)\}/g, (_, name) => `{${toSnakeCase(name)}}`);
+}
+
+function toSnakeCase(input: string): string {
+ return input
+ .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
+ .replace(/[-\s]+/g, "_")
+ .toLowerCase();
+}
diff --git a/packages/opex-dashboard-ts/src/index.ts b/packages/opex-dashboard-ts/src/index.ts
new file mode 100644
index 00000000..4ddc260d
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/index.ts
@@ -0,0 +1,3 @@
+export { GenerateDashboardUseCase } from "./application/index.js";
+export * from "./domain/index.js";
+export * from "./infrastructure/index.js";
diff --git a/packages/opex-dashboard-ts/src/infrastructure/cli/generate.ts b/packages/opex-dashboard-ts/src/infrastructure/cli/generate.ts
new file mode 100644
index 00000000..22b20d53
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/cli/generate.ts
@@ -0,0 +1,50 @@
+/* eslint-disable no-console */
+import { Command } from "commander";
+
+import { GenerateDashboardUseCase } from "../../application/index.js";
+import { EndpointParserService } from "../../domain/services/endpoint-parser-service.js";
+import { ConfigValidatorAdapter } from "../config/config-validator-adapter.js";
+import { FileReaderAdapter } from "../file/file-reader-adapter.js";
+import { OpenAPISpecResolverAdapter } from "../openapi/openapi-spec-resolver-adapter.js";
+import { TerraformFileGeneratorAdapter } from "../terraform/terraform-generator-adapter.js";
+
+export const generateCommand = new Command()
+ .name("generate")
+ .description("Generate dashboard definition")
+ .requiredOption("-c, --config-file ", "YAML config file")
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ .action(async (options: any) => {
+ try {
+ // See https://github.com/hashicorp/terraform-cdk/pull/3876
+ process.env.SYNTH_HCL_OUTPUT = "true";
+
+ // Create adapters
+ const fileReader = new FileReaderAdapter();
+ const configValidator = new ConfigValidatorAdapter();
+ const openAPISpecResolver = new OpenAPISpecResolverAdapter();
+ const endpointParser = new EndpointParserService();
+ const terraformGenerator = new TerraformFileGeneratorAdapter();
+
+ // Create use case
+ const generateDashboardUseCase = new GenerateDashboardUseCase(
+ fileReader,
+ configValidator,
+ openAPISpecResolver,
+ endpointParser,
+ terraformGenerator,
+ );
+
+ // Execute use case
+ await generateDashboardUseCase.execute(options.configFile);
+
+ // Output result
+ console.log("Terraform CDKTF code generated successfully");
+ } catch (error: unknown) {
+ console.error(
+ "Error:",
+ error instanceof Error ? error.message : "Unknown error",
+ { cause: error },
+ );
+ process.exit(1);
+ }
+ });
diff --git a/packages/opex-dashboard-ts/src/infrastructure/cli/index.ts b/packages/opex-dashboard-ts/src/infrastructure/cli/index.ts
new file mode 100644
index 00000000..8fe22308
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/cli/index.ts
@@ -0,0 +1,18 @@
+#!/usr/bin/env node
+
+import { Command } from "commander";
+
+import { generateCommand } from "./generate.js";
+
+const program = new Command();
+
+program
+ .name("opex-dashboard-ts")
+ .description(
+ "Generate standardized PagoPA Operational Excellence dashboards from OpenAPI specs",
+ )
+ .version("1.0.0");
+
+program.addCommand(generateCommand);
+
+program.parse();
diff --git a/packages/opex-dashboard-ts/src/infrastructure/config/config-validator-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/config/config-validator-adapter.ts
new file mode 100644
index 00000000..711e0fc5
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/config/config-validator-adapter.ts
@@ -0,0 +1,23 @@
+import {
+ DashboardConfigSchema,
+ IConfigValidator,
+ ValidDashboardConfig,
+} from "../../domain/index.js";
+
+export class ConfigValidatorAdapter implements IConfigValidator {
+ validateConfig(rawConfig: unknown): ValidDashboardConfig {
+ // Parse and validate with zod using safeParse
+ const result = DashboardConfigSchema.safeParse(rawConfig);
+
+ if (!result.success) {
+ // Format validation errors
+ const errorMessage = result.error.issues
+ .map((err) => `• ${err.path.join(".")}: ${err.message}`)
+ .join("\n");
+ throw new Error(`Configuration validation failed:\n${errorMessage}`);
+ }
+
+ // Apply defaults
+ return result.data;
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/exports/add-opex-stack.ts b/packages/opex-dashboard-ts/src/infrastructure/exports/add-opex-stack.ts
new file mode 100644
index 00000000..50c397cc
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/exports/add-opex-stack.ts
@@ -0,0 +1,49 @@
+import { App } from "cdktf";
+
+import { CreateDashboardStackUseCase } from "../../application/use-cases/create-dashboard-stack-use-case.js";
+import { DashboardConfig } from "../../domain/index.js";
+import { EndpointParserService } from "../../domain/services/endpoint-parser-service.js";
+import { ConfigValidatorAdapter } from "../config/config-validator-adapter.js";
+import { OpenAPISpecResolverAdapter } from "../openapi/openapi-spec-resolver-adapter.js";
+import { AzureOpexStack } from "../terraform/azure-dashboard.js";
+import { TerraformStackGeneratorAdapter } from "../terraform/terraform-stack-generator-adapter.js";
+
+/**
+ * Generates Azure dashboard and alerts from configuration object.
+ */
+export async function addOpexStack({
+ app,
+ config,
+}: {
+ app: App;
+ config: DashboardConfig;
+}): Promise<{ opexStack: AzureOpexStack }> {
+ try {
+ // See https://github.com/hashicorp/terraform-cdk/pull/3876
+ process.env.SYNTH_HCL_OUTPUT = "true";
+
+ // Create adapters
+ const configValidator = new ConfigValidatorAdapter();
+ const openAPISpecResolver = new OpenAPISpecResolverAdapter();
+ const endpointParser = new EndpointParserService();
+ const terraformGenerator = new TerraformStackGeneratorAdapter();
+
+ // Create use case
+ const createDashboardStackUseCase = new CreateDashboardStackUseCase(
+ configValidator,
+ openAPISpecResolver,
+ endpointParser,
+ terraformGenerator,
+ );
+
+ // Execute use case with config object and app
+ const opexStack = await createDashboardStackUseCase.execute(config, app);
+
+ return { opexStack };
+ } catch (error: unknown) {
+ throw new Error(
+ `Error generating dashboard: ${error instanceof Error ? error.message : "Unknown error"}`,
+ { cause: error },
+ );
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/file/file-reader-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/file/file-reader-adapter.ts
new file mode 100644
index 00000000..25b55036
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/file/file-reader-adapter.ts
@@ -0,0 +1,11 @@
+import * as fs from "fs";
+import * as yaml from "js-yaml";
+
+import { IFileReader } from "../../domain/index.js";
+
+export class FileReaderAdapter implements IFileReader {
+ async readYamlFile(filePath: string): Promise {
+ const fileContent = fs.readFileSync(filePath, "utf8");
+ return yaml.load(fileContent);
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/index.ts b/packages/opex-dashboard-ts/src/infrastructure/index.ts
new file mode 100644
index 00000000..baa49709
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/index.ts
@@ -0,0 +1,7 @@
+export * from "./cli/generate.js";
+export * from "./config/config-validator-adapter.js";
+export * from "./exports/add-opex-stack.js";
+export * from "./file/file-reader-adapter.js";
+export * from "./openapi/openapi-spec-resolver-adapter.js";
+export * from "./terraform/terraform-generator-adapter.js";
+export * from "./terraform/terraform-stack-generator-adapter.js";
diff --git a/packages/opex-dashboard-ts/src/infrastructure/openapi/openapi-spec-resolver-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/openapi/openapi-spec-resolver-adapter.ts
new file mode 100644
index 00000000..36e1934f
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/openapi/openapi-spec-resolver-adapter.ts
@@ -0,0 +1,55 @@
+import SwaggerParser from "@apidevtools/swagger-parser";
+
+import { IOpenAPISpecResolver, OpenAPISpec } from "../../domain/index.js";
+import { isOpenAPIV2, isOpenAPIV3 } from "../../shared/openapi.js";
+
+export class OpenAPISpecResolverAdapter implements IOpenAPISpecResolver {
+ async resolve(specPath: string): Promise {
+ try {
+ const spec = await SwaggerParser.parse(specPath);
+ return spec as OpenAPISpec;
+ } catch (error: unknown) {
+ if (error instanceof Error) {
+ throw new ParseError(`OA3 parsing error: ${error.message}`);
+ }
+ throw new ParseError(`OA3 parsing error: ${String(error)}`);
+ }
+ }
+
+ async resolveWithHosts(
+ specPath: string,
+ ): Promise<{ hosts: string[]; spec: OpenAPISpec }> {
+ const spec = await this.resolve(specPath);
+ const hosts = this.extractHostsFromSpec(spec);
+ return { hosts, spec };
+ }
+
+ private extractHostsFromSpec(spec: OpenAPISpec): string[] {
+ if (isOpenAPIV3(spec)) {
+ // OpenAPI 3.x uses servers array
+ if (spec.servers && spec.servers.length > 0) {
+ return spec.servers.map((server) => {
+ try {
+ const url = new URL(server.url);
+ return url.hostname;
+ } catch {
+ return server.url.replace(/^https?:\/\//, "").split("/")[0];
+ }
+ });
+ }
+ } else if (isOpenAPIV2(spec)) {
+ // OpenAPI 2.x uses host field
+ if (spec.host) {
+ return [spec.host];
+ }
+ }
+ return [];
+ }
+}
+
+export class ParseError extends Error {
+ constructor(message: string) {
+ super(message);
+ this.name = "ParseError";
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-alerts.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-alerts.ts
new file mode 100644
index 00000000..e5848809
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-alerts.ts
@@ -0,0 +1,196 @@
+import {
+ dataAzurermClientConfig,
+ monitorScheduledQueryRulesAlert,
+ portalDashboard,
+} from "@cdktf/provider-azurerm";
+import { Construct } from "constructs";
+
+import { Endpoint, ValidDashboardConfig } from "../../domain/index.js";
+import { KustoQueryService } from "../../domain/services/kusto-query-service.js";
+
+export class AzureAlertsConstruct {
+ private readonly kustoQueryService = new KustoQueryService();
+
+ constructor(
+ scope: Construct,
+ config: ValidDashboardConfig,
+ dashboard: portalDashboard.PortalDashboard,
+ clientConfig: dataAzurermClientConfig.DataAzurermClientConfig,
+ resolvedLocation: string,
+ ) {
+ if (!config.endpoints) return;
+
+ config.endpoints.forEach((endpoint, index) => {
+ this.createAvailabilityAlert(
+ scope,
+ config,
+ endpoint,
+ index,
+ dashboard,
+ clientConfig,
+ resolvedLocation,
+ );
+ this.createResponseTimeAlert(
+ scope,
+ config,
+ endpoint,
+ index,
+ dashboard,
+ clientConfig,
+ resolvedLocation,
+ );
+ });
+ }
+
+ private buildAlertDescription(
+ endpointPath: string,
+ alertType: "availability" | "responsetime",
+ thresholdValue: number,
+ dashboardId: string,
+ tenantId: string,
+ ): string {
+ const url = `https://portal.azure.com/#@${tenantId}/dashboard/arm${dashboardId}`;
+ if (alertType === "availability") {
+ const pct = (thresholdValue * 100).toFixed(0) + "%";
+ return `Availability for ${endpointPath} is below ${pct} - ${url}`;
+ }
+ return `Response time (p95) for ${endpointPath} is above ${thresholdValue}s - ${url}`;
+ }
+
+ private buildAlertName(
+ dashboardName: string,
+ alertType: "availability" | "responsetime",
+ endpointPath: string,
+ ): string {
+ return `${this.slug(dashboardName)}-${alertType}_${this.slug(endpointPath)}`;
+ }
+
+ private createAvailabilityAlert(
+ scope: Construct,
+ config: ValidDashboardConfig,
+ endpoint: Endpoint,
+ index: number,
+ dashboard: portalDashboard.PortalDashboard,
+ clientConfig: dataAzurermClientConfig.DataAzurermClientConfig,
+ resolvedLocation: string,
+ ) {
+ const alertName = this.buildAlertName(
+ config.name,
+ "availability",
+ endpoint.path,
+ );
+ const availabilityThreshold = endpoint.availability_threshold ?? 0.99;
+ new monitorScheduledQueryRulesAlert.MonitorScheduledQueryRulesAlert(
+ scope,
+ `alarm_availability_${index}`, // Changed from availability-alert-{index}
+ {
+ action: {
+ actionGroup: config.action_groups || [],
+ },
+ autoMitigationEnabled: false,
+ dataSourceId: config.data_source,
+ description: this.buildAlertDescription(
+ endpoint.path,
+ "availability",
+ availabilityThreshold,
+ dashboard.id,
+ clientConfig.tenantId,
+ ),
+ enabled: true,
+ frequency: endpoint.availability_evaluation_frequency ?? 10,
+ location: resolvedLocation,
+ name: alertName,
+ query: this.kustoQueryService.buildAvailabilityQuery(
+ endpoint,
+ config,
+ "alert",
+ ),
+ resourceGroupName: config.resource_group_name,
+ severity: 1,
+ tags: config.tags,
+ timeWindow: endpoint.availability_evaluation_time_window ?? 20,
+ trigger: {
+ operator: "GreaterThanOrEqual",
+ threshold: endpoint.availability_event_occurrences ?? 1,
+ },
+ },
+ );
+ }
+
+ private createResponseTimeAlert(
+ scope: Construct,
+ config: ValidDashboardConfig,
+ endpoint: Endpoint,
+ index: number,
+ dashboard: portalDashboard.PortalDashboard,
+ clientConfig: dataAzurermClientConfig.DataAzurermClientConfig,
+ resolvedLocation: string,
+ ) {
+ const alertName = this.buildAlertName(
+ config.name,
+ "responsetime",
+ endpoint.path,
+ );
+ const responseThreshold = endpoint.response_time_threshold ?? 1;
+
+ new monitorScheduledQueryRulesAlert.MonitorScheduledQueryRulesAlert(
+ scope,
+ `alarm_time_${index}`, // Changed from response-time-alert-{index}
+ {
+ action: {
+ actionGroup: config.action_groups || [],
+ },
+ autoMitigationEnabled: false,
+ dataSourceId: config.data_source,
+ description: this.buildAlertDescription(
+ endpoint.path,
+ "responsetime",
+ responseThreshold,
+ dashboard.id,
+ clientConfig.tenantId,
+ ),
+ enabled: true,
+ frequency: endpoint.response_time_evaluation_frequency ?? 10,
+ location: resolvedLocation,
+ name: alertName,
+ query: this.kustoQueryService.buildResponseTimeQuery(
+ endpoint,
+ config,
+ "alert",
+ ),
+ resourceGroupName: config.resource_group_name,
+ severity: 1,
+ tags: config.tags,
+ timeWindow: endpoint.response_time_evaluation_time_window ?? 20,
+ trigger: {
+ operator: "GreaterThanOrEqual",
+ threshold: endpoint.response_time_event_occurrences ?? 1,
+ },
+ },
+ );
+ }
+
+ /* Build a safe slug for embedding in Azure resource names */
+ private slug(input: string): string {
+ return (
+ input
+ .trim()
+ // remove leading slashes to avoid leading underscores later
+ .replace(/^\/+/, "")
+ // normalize spaces and unsupported chars
+ .replace(/\s+/g, "-")
+ .replace(/[{}]/g, "")
+ .replace(/[^a-zA-Z0-9-_/]/g, "-")
+ // drop trailing slashes first
+ .replace(/\/+$/g, "")
+ // turn path separators into underscores
+ .replace(/\//g, "_")
+ // collapse duplicates
+ .replace(/-+/g, "-")
+ .replace(/_+/g, "_")
+ // finally, trim leading/trailing separators
+ .replace(/^[-_]+/, "")
+ .replace(/[-_]+$/, "")
+ );
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-dashboard.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-dashboard.ts
new file mode 100644
index 00000000..460a0fd5
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/azure-dashboard.ts
@@ -0,0 +1,57 @@
+import {
+ dataAzurermClientConfig,
+ dataAzurermResourceGroup,
+ portalDashboard,
+ provider,
+} from "@cdktf/provider-azurerm";
+import { TerraformStack } from "cdktf";
+import { Construct } from "constructs";
+
+import { ValidDashboardConfig } from "../../domain/index.js";
+import { AzureAlertsConstruct } from "./azure-alerts.js";
+import { buildDashboardPropertiesTemplate } from "./dashboard-properties.js";
+
+export class AzureOpexStack extends TerraformStack {
+ constructor(scope: Construct, id: string, config: ValidDashboardConfig) {
+ super(scope, id);
+
+ // Configure Azure provider
+ new provider.AzurermProvider(this, "azure", {
+ features: [{}],
+ storageUseAzuread: true,
+ });
+
+ // Get current Azure client configuration for tenant info
+ const clientConfig = new dataAzurermClientConfig.DataAzurermClientConfig(
+ this,
+ "current",
+ {},
+ );
+
+ // Lookup Resource Group to inherit location
+ const rg = new dataAzurermResourceGroup.DataAzurermResourceGroup(
+ this,
+ "rg",
+ { name: config.resource_group_name },
+ );
+ const resolvedLocation = rg.location;
+
+ // Create the dashboard using CDKTF PortalDashboard
+ const dashboard = new portalDashboard.PortalDashboard(this, "dashboard", {
+ dashboardProperties: buildDashboardPropertiesTemplate(config),
+ location: resolvedLocation,
+ name: config.name.replace(/\s+/g, "_"),
+ resourceGroupName: config.resource_group_name,
+ tags: config.tags,
+ });
+
+ // Create alerts within the same stack, passing dashboard reference and tenant
+ new AzureAlertsConstruct(
+ this,
+ config,
+ dashboard,
+ clientConfig,
+ resolvedLocation,
+ );
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/dashboard-properties.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/dashboard-properties.ts
new file mode 100644
index 00000000..028c6228
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/dashboard-properties.ts
@@ -0,0 +1,155 @@
+import { Panel } from "../../domain/entities/panel.js";
+import { ValidDashboardConfig } from "../../domain/index.js";
+import { PanelFactory } from "../../domain/services/panel-factory.js";
+
+export function buildDashboardPropertiesTemplate(
+ config: ValidDashboardConfig,
+): string {
+ const factory = new PanelFactory();
+ const panels = factory.buildPanels(config);
+ const parts = panels
+ .map((panel) => `"${panel.id}": ${serializePanel(panel, config)}`)
+ .join(",");
+
+ return `{
+ "lenses": {
+ "0": {
+ "order": 0,
+ "parts": {
+ ${parts}
+ }
+ }
+ },
+ "metadata": {
+ "model": {
+ "timeRange": {
+ "value": {
+ "relative": {
+ "duration": 24,
+ "timeUnit": 1
+ }
+ },
+ "type": "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange"
+ },
+ "filterLocale": {
+ "value": "en-us"
+ },
+ "filters": {
+ "value": {
+ "MsPortalFx_TimeRange": {
+ "model": {
+ "format": "local",
+ "granularity": "auto",
+ "relative": "48h"
+ },
+ "displayCache": {
+ "name": "Local Time",
+ "value": "Past 48 hours"
+ },
+ "filteredPartIds": [
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ed",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ef",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f1",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f3",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f5",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f7",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f9",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fb",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fd"
+ ]
+ }
+ }
+ }
+ }
+ }
+ }`;
+}
+
+function buildInputDimensions(panel: Panel): string {
+ if (panel.kind === "availability") {
+ return JSON.stringify({
+ aggregation: "Sum",
+ splitBy: [],
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [
+ { name: "availability", type: "real" },
+ { name: "watermark", type: "real" },
+ ],
+ });
+ }
+ if (panel.kind === "response-codes") {
+ return JSON.stringify({
+ aggregation: "Sum",
+ splitBy: [{ name: "HTTPStatus", type: "string" }],
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [{ name: "count_", type: "long" }],
+ });
+ }
+ if (panel.kind === "response-time") {
+ return JSON.stringify({
+ aggregation: "Sum",
+ splitBy: [],
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [
+ { name: "duration_percentile_95", type: "real" },
+ { name: "watermark", type: "long" },
+ ],
+ });
+ }
+ return "{}";
+}
+
+function buildSettingsDimensions(panel: Panel): string | undefined {
+ if (panel.kind === "response-codes") {
+ return JSON.stringify({
+ aggregation: "Sum",
+ splitBy: [{ name: "HTTPStatus", type: "string" }],
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [{ name: "count_", type: "long" }],
+ });
+ }
+ if (panel.kind === "response-time") {
+ return JSON.stringify({
+ aggregation: "Sum",
+ splitBy: [],
+ xAxis: { name: "TimeGenerated", type: "datetime" },
+ yAxis: [
+ { name: "duration_percentile_95", type: "real" },
+ { name: "watermark", type: "long" },
+ ],
+ });
+ }
+ return undefined;
+}
+
+function serializePanel(panel: Panel, config: ValidDashboardConfig): string {
+ const resourceIds = JSON.stringify(config.resourceIds || []);
+ const inputsChart = panel.chart.inputSpecificChart;
+ const dimensions = buildInputDimensions(panel);
+ const settingsDimensions = buildSettingsDimensions(panel);
+ return `{
+ "position": ${JSON.stringify(panel.position)},
+ "metadata": {
+ "inputs": [
+ { "name": "resourceTypeMode", "isOptional": true },
+ { "name": "ComponentId", "isOptional": true },
+ { "name": "Scope", "value": { "resourceIds": ${resourceIds} }, "isOptional": true },
+ { "name": "PartId", "isOptional": true },
+ { "name": "Version", "value": "2.0", "isOptional": true },
+ { "name": "TimeRange", "value": "PT4H", "isOptional": true },
+ { "name": "DashboardId", "isOptional": true },
+ { "name": "DraftRequestParameters", "value": { "scope": "hierarchy" }, "isOptional": true },
+ { "name": "Query", "value": ${JSON.stringify(panel.query)}, "isOptional": true },
+ { "name": "ControlType", "value": "FrameControlChart", "isOptional": true },
+ { "name": "SpecificChart", "value": "${inputsChart}", "isOptional": true },
+ { "name": "PartTitle", "value": "${panel.title}", "isOptional": true },
+ { "name": "PartSubTitle", "value": "${panel.subtitle}", "isOptional": true },
+ { "name": "Dimensions", "value": ${dimensions}, "isOptional": true },
+ { "name": "LegendOptions", "value": { "isEnabled": true, "position": "Bottom" }, "isOptional": true },
+ { "name": "IsQueryContainTimeRange", "value": false, "isOptional": true }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": { "content": { "Query": ${JSON.stringify(panel.query)}, ${panel.kind === "response-codes" ? '"SpecificChart": "StackedArea",' : panel.kind === "response-time" ? '"SpecificChart": "Line",' : ""} "PartTitle": "${panel.title}"${settingsDimensions ? `, "Dimensions": ${settingsDimensions}` : ""} } }
+ }
+ }`;
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-generator-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-generator-adapter.ts
new file mode 100644
index 00000000..f43de552
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-generator-adapter.ts
@@ -0,0 +1,20 @@
+import { App } from "cdktf";
+
+import {
+ ITerraformFileGenerator,
+ ValidDashboardConfig,
+} from "../../domain/index.js";
+import { AzureOpexStack } from "./azure-dashboard.js";
+
+export class TerraformFileGeneratorAdapter implements ITerraformFileGenerator {
+ async generate(config: ValidDashboardConfig): Promise {
+ // Create CDKTF app
+ const app = new App({ hclOutput: true, outdir: "opex" });
+
+ // Create the main stack
+ new AzureOpexStack(app, "opex-dashboard", config);
+
+ // Generate Terraform code
+ app.synth();
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-stack-generator-adapter.ts b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-stack-generator-adapter.ts
new file mode 100644
index 00000000..b2b964ff
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/infrastructure/terraform/terraform-stack-generator-adapter.ts
@@ -0,0 +1,20 @@
+import { App } from "cdktf";
+
+import {
+ ITerraformStackGenerator,
+ ValidDashboardConfig,
+} from "../../domain/index.js";
+import { AzureOpexStack } from "./azure-dashboard.js";
+
+export class TerraformStackGeneratorAdapter
+ implements ITerraformStackGenerator
+{
+ async generate(
+ config: ValidDashboardConfig,
+ app: App,
+ ): Promise {
+ // Create and return the stack using the provided app
+ const opexStack = new AzureOpexStack(app, "opex-dashboard", config);
+ return opexStack;
+ }
+}
diff --git a/packages/opex-dashboard-ts/src/shared/openapi.ts b/packages/opex-dashboard-ts/src/shared/openapi.ts
new file mode 100644
index 00000000..3e8f107a
--- /dev/null
+++ b/packages/opex-dashboard-ts/src/shared/openapi.ts
@@ -0,0 +1,12 @@
+import { OpenAPIV2, OpenAPIV3 } from "openapi-types";
+
+export type OpenAPISpec = OpenAPIV2.Document | OpenAPIV3.Document;
+
+// Type guards to check OpenAPI version
+export function isOpenAPIV2(spec: OpenAPISpec): spec is OpenAPIV2.Document {
+ return "swagger" in spec;
+}
+
+export function isOpenAPIV3(spec: OpenAPISpec): spec is OpenAPIV3.Document {
+ return "openapi" in spec;
+}
diff --git a/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_config.yaml b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_config.yaml
new file mode 100644
index 00000000..4331ec25
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_config.yaml
@@ -0,0 +1,12 @@
+oa3_spec: https://raw.githubusercontent.com/pagopa/opex-dashboard/refs/heads/main/test/data/io_backend_light.yaml
+name: My Dashboard
+timespan: 5m
+data_source: /subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw
+resource_type: app-gateway
+resource_group_name: dashboards
+action_groups:
+ - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email
+ - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack
+tags:
+ Environment: TEST
+ CostCenter: CC123
diff --git a/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_overrides_config.yaml b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_overrides_config.yaml
new file mode 100644
index 00000000..f6491cd8
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/fixtures/azure_dashboard_overrides_config.yaml
@@ -0,0 +1,26 @@
+oa3_spec: https://raw.githubusercontent.com/pagopa/opex-dashboard/refs/heads/main/test/data/io_backend_light.yaml
+name: My spec
+timespan: 6m # Default, a number or a timespan https://docs.microsoft.com/en-us/azure/data-explorer/kusto/query/scalar-data-types/timespan
+data_source: /subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw
+resource_type: app-gateway
+resource_group_name: dashboards
+action_groups:
+ - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email
+ - /subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack
+tags:
+ Environment: TEST
+ Owner: team-opex
+overrides:
+ hosts: # Use these hosts instead of those inside the OpenApi spec
+ - https://example.com
+ - https://example2.com
+ endpoints:
+ /api/v1/services/{service_id}:
+ availability_threshold: 0.95 # Default: 99%
+ availability_evaluation_frequency: 30 # Default: 10
+ availability_evaluation_time_window: 50 # Default: 20
+ availability_event_occurrences: 3 # Default: 1
+ response_time_threshold: 2 # Default: 1
+ response_time_evaluation_frequency: 35 # Default: 10
+ response_time_evaluation_time_window: 55 # Default: 20
+ response_time_event_occurrences: 5 # Default: 1
diff --git a/packages/opex-dashboard-ts/test/fixtures/io_backend_light.yaml b/packages/opex-dashboard-ts/test/fixtures/io_backend_light.yaml
new file mode 100644
index 00000000..3e0297ce
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/fixtures/io_backend_light.yaml
@@ -0,0 +1,124 @@
+swagger: "2.0"
+info:
+ version: 1.0.0
+ title: Proxy API
+ description: Mobile and web proxy API gateway.
+host: app-backend.io.italia.it
+basePath: /api/v1
+schemes:
+ - https
+security:
+ - Bearer: []
+paths:
+ "/services/{service_id}":
+ x-swagger-router-controller: ServicesController
+ parameters:
+ - name: service_id
+ in: path
+ type: string
+ required: true
+ description: The ID of an existing Service.
+ get:
+ operationId: getService
+ summary: Get Service
+ description: A previously created service with the provided service ID is returned.
+ responses:
+ "200":
+ description: Service found.
+ schema:
+ "$ref": "#/definitions/ServicePublic"
+ examples:
+ application/json:
+ department_name: "IO"
+ organization_fiscal_code: "00000000000"
+ organization_name: "IO"
+ service_id: "5a563817fcc896087002ea46c49a"
+ service_name: "App IO"
+ version: 1
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: No service found for the provided ID.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the service.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ parameters: []
+ "/services":
+ x-swagger-router-controller: ServicesController
+ get:
+ operationId: getVisibleServices
+ summary: Get all visible services
+ description: |-
+ Returns the description of all visible services.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/PaginatedServiceTupleCollection"
+ examples:
+ application/json:
+ items:
+ - service_id: "AzureDeployc49a"
+ version: 1
+ - service_id: "5a25abf4fcc89605c082f042c49a"
+ version: 0
+ page_size: 1
+ "401":
+ description: Bearer token null or expired.
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the services.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ parameters:
+ - $ref: "#/parameters/PaginationRequest"
+definitions:
+ ProblemJson:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ProblemJson"
+ ServiceId:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceId"
+ ServiceName:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceName"
+ ServicePublic:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePublic"
+ PaginatedServiceTupleCollection:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedServiceTupleCollection"
+responses: {}
+parameters:
+ PageSize:
+ name: page_size
+ type: integer
+ in: query
+ minimum: 1
+ maximum: 100
+ required: false
+ description: How many items a page should include.
+ PaginationRequest:
+ type: string
+ name: cursor
+ in: query
+ minimum: 1
+ description: An opaque identifier that points to the next item in the collection.
+consumes:
+ - application/json
+produces:
+ - application/json
+securityDefinitions:
+ Bearer:
+ type: apiKey
+ name: Authorization
+ in: header
diff --git a/packages/opex-dashboard-ts/test/fixtures/iobackend_light_no_overrides.txt b/packages/opex-dashboard-ts/test/fixtures/iobackend_light_no_overrides.txt
new file mode 100644
index 00000000..7c7ee448
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/fixtures/iobackend_light_no_overrides.txt
@@ -0,0 +1,1027 @@
+
+locals {
+ name = "${var.prefix}-${var.env_short}-PROD-IO/IO_App_Availability"
+ dashboard_base_addr = "https://portal.azure.com/#@pagopait.onmicrosoft.com/dashboard/arm"
+}
+
+data "azurerm_resource_group" "this" {
+ name = "dashboards"
+}
+
+resource "azurerm_portal_dashboard" "this" {
+ name = local.name
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ dashboard_properties = <<-PROPS
+ {
+ "lenses": {
+ "0": {
+ "order": 0,
+ "parts": {
+ "0": {
+ "position": {
+ "x": 0,
+ "y": 0,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Line",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Availability (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services/{service_id}",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "availability",
+ "type": "real"
+ },
+ {
+ "name": "watermark",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "PartTitle": "Availability (5m)"
+ }
+ }
+ }
+ },
+ "1": {
+ "position": {
+ "x": 6,
+ "y": 0,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Pie",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Response Codes (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services/{service_id}",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "httpStatus_d",
+ "type": "string"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "SpecificChart": "StackedArea",
+ "PartTitle": "Response Codes (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [
+ {
+ "name": "HTTPStatus",
+ "type": "string"
+ }
+ ],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ },
+ "2": {
+ "position": {
+ "x": 12,
+ "y": 0,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "StackedColumn",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Percentile Response Time (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services/{service_id}",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "SpecificChart": "Line",
+ "PartTitle": "Percentile Response Time (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "watermark",
+ "type": "long"
+ },
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ },
+ "3": {
+ "position": {
+ "x": 0,
+ "y": 4,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Line",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Availability (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "availability",
+ "type": "real"
+ },
+ {
+ "name": "watermark",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "PartTitle": "Availability (5m)"
+ }
+ }
+ }
+ },
+ "4": {
+ "position": {
+ "x": 6,
+ "y": 4,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Pie",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Response Codes (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "httpStatus_d",
+ "type": "string"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "SpecificChart": "StackedArea",
+ "PartTitle": "Response Codes (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [
+ {
+ "name": "HTTPStatus",
+ "type": "string"
+ }
+ ],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ },
+ "5": {
+ "position": {
+ "x": 12,
+ "y": 4,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/io-p-rg-external/providers/Microsoft.Network/applicationGateways/io-p-appgateway"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "StackedColumn",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Percentile Response Time (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "SpecificChart": "Line",
+ "PartTitle": "Percentile Response Time (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "watermark",
+ "type": "long"
+ },
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "metadata": {
+ "model": {
+ "timeRange": {
+ "value": {
+ "relative": {
+ "duration": 24,
+ "timeUnit": 1
+ }
+ },
+ "type": "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange"
+ },
+ "filterLocale": {
+ "value": "en-us"
+ },
+ "filters": {
+ "value": {
+ "MsPortalFx_TimeRange": {
+ "model": {
+ "format": "local",
+ "granularity": "auto",
+ "relative": "48h"
+ },
+ "displayCache": {
+ "name": "Local Time",
+ "value": "Past 48 hours"
+ },
+ "filteredPartIds": [
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ed",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ef",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f1",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f3",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f5",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f7",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f9",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fb",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fd"
+ ]
+ }
+ }
+ }
+ }
+ }
+}
+ PROPS
+
+ tags = var.tags
+}
+
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_0" {
+ name = replace(join("_",split("/", "${local.name}-availability @ /api/v1/services/{service_id}")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "data_source_id"
+ description = "Availability for /api/v1/services/{service_id} is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 0.99;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services/[^/]+$"
+| summarize
+ Total=count(),
+ Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)
+| extend availability=toreal(Success) / Total
+| where availability < threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 2
+ }
+
+ tags = var.tags
+}
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_0" {
+ name = replace(join("_",split("/", "${local.name}-responsetime @ /api/v1/services/{service_id}")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "data_source_id"
+ description = "Response time for /api/v1/services/{service_id} is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 1;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services/[^/]+$"
+| summarize
+ watermark=threshold,
+ duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)
+| where duration_percentile_95 > threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 2
+ }
+
+ tags = var.tags
+}
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_1" {
+ name = replace(join("_",split("/", "${local.name}-availability @ /api/v1/services")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "data_source_id"
+ description = "Availability for /api/v1/services is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 0.99;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services$"
+| summarize
+ Total=count(),
+ Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)
+| extend availability=toreal(Success) / Total
+| where availability < threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 2
+ }
+
+ tags = var.tags
+}
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_1" {
+ name = replace(join("_",split("/", "${local.name}-responsetime @ /api/v1/services")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "data_source_id"
+ description = "Response time for /api/v1/services is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 1;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services$"
+| summarize
+ watermark=threshold,
+ duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)
+| where duration_percentile_95 > threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 2
+ }
+
+ tags = var.tags
+}
+
diff --git a/packages/opex-dashboard-ts/test/fixtures/legacy.tf.txt b/packages/opex-dashboard-ts/test/fixtures/legacy.tf.txt
new file mode 100644
index 00000000..856358a1
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/fixtures/legacy.tf.txt
@@ -0,0 +1,1028 @@
+
+locals {
+ name = "${var.prefix}-${var.env_short}-My_Dashboard"
+ dashboard_base_addr = "https://portal.azure.com/#@pagopait.onmicrosoft.com/dashboard/arm"
+}
+
+data "azurerm_resource_group" "this" {
+ name = "dashboards"
+}
+
+resource "azurerm_portal_dashboard" "this" {
+ name = local.name
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ dashboard_properties = <<-PROPS
+ {
+ "lenses": {
+ "0": {
+ "order": 0,
+ "parts": {
+ "0": {
+ "position": {
+ "x": 0,
+ "y": 0,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Line",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Availability (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services/{service_id}",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "availability",
+ "type": "real"
+ },
+ {
+ "name": "watermark",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "PartTitle": "Availability (5m)"
+ }
+ }
+ }
+ },
+ "1": {
+ "position": {
+ "x": 6,
+ "y": 0,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Pie",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Response Codes (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services/{service_id}",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "httpStatus_d",
+ "type": "string"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_url = \"/api/v1/services/[^/]+$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "SpecificChart": "StackedArea",
+ "PartTitle": "Response Codes (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [
+ {
+ "name": "HTTPStatus",
+ "type": "string"
+ }
+ ],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ },
+ "2": {
+ "position": {
+ "x": 12,
+ "y": 0,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "StackedColumn",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Percentile Response Time (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services/{service_id}",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services/[^/]+$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "SpecificChart": "Line",
+ "PartTitle": "Percentile Response Time (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "watermark",
+ "type": "long"
+ },
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ },
+ "3": {
+ "position": {
+ "x": 0,
+ "y": 4,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Line",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Availability (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "availability",
+ "type": "real"
+ },
+ {
+ "name": "watermark",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 0.99;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n Total=count(),\n Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)\n| extend availability=toreal(Success) / Total\n| project TimeGenerated, availability, watermark=threshold\n| render timechart with (xtitle = \"time\", ytitle= \"availability(%)\")\n",
+ "PartTitle": "Availability (5m)"
+ }
+ }
+ }
+ },
+ "4": {
+ "position": {
+ "x": 6,
+ "y": 4,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "Pie",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Response Codes (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "httpStatus_d",
+ "type": "string"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_url = \"/api/v1/services$\";\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex api_url\n| extend HTTPStatus = case(\n httpStatus_d between (100 .. 199), \"1XX\",\n httpStatus_d between (200 .. 299), \"2XX\",\n httpStatus_d between (300 .. 399), \"3XX\",\n httpStatus_d between (400 .. 499), \"4XX\",\n \"5XX\")\n| summarize count() by HTTPStatus, bin(TimeGenerated, 5m)\n| render areachart with (xtitle = \"time\", ytitle= \"count\")\n",
+ "SpecificChart": "StackedArea",
+ "PartTitle": "Response Codes (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "count_",
+ "type": "long"
+ }
+ ],
+ "splitBy": [
+ {
+ "name": "HTTPStatus",
+ "type": "string"
+ }
+ ],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ },
+ "5": {
+ "position": {
+ "x": 12,
+ "y": 4,
+ "colSpan": 6,
+ "rowSpan": 4
+ },
+ "metadata": {
+ "inputs": [
+ {
+ "name": "resourceTypeMode",
+ "isOptional": true
+ },
+ {
+ "name": "ComponentId",
+ "isOptional": true
+ },
+ {
+ "name": "Scope",
+ "value": {
+ "resourceIds": [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ ]
+ },
+ "isOptional": true
+ },
+ {
+ "name": "PartId",
+ "isOptional": true
+ },
+ {
+ "name": "Version",
+ "value": "2.0",
+ "isOptional": true
+ },
+ {
+ "name": "TimeRange",
+ "value": "PT4H",
+ "isOptional": true
+ },
+ {
+ "name": "DashboardId",
+ "isOptional": true
+ },
+ {
+ "name": "DraftRequestParameters",
+ "value": {
+ "scope": "hierarchy"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "Query",
+ "value": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "isOptional": true
+ },
+ {
+ "name": "ControlType",
+ "value": "FrameControlChart",
+ "isOptional": true
+ },
+ {
+ "name": "SpecificChart",
+ "value": "StackedColumn",
+ "isOptional": true
+ },
+ {
+ "name": "PartTitle",
+ "value": "Percentile Response Time (5m)",
+ "isOptional": true
+ },
+ {
+ "name": "PartSubTitle",
+ "value": "/api/v1/services",
+ "isOptional": true
+ },
+ {
+ "name": "Dimensions",
+ "value": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "LegendOptions",
+ "value": {
+ "isEnabled": true,
+ "position": "Bottom"
+ },
+ "isOptional": true
+ },
+ {
+ "name": "IsQueryContainTimeRange",
+ "value": false,
+ "isOptional": true
+ }
+ ],
+ "type": "Extension/Microsoft_OperationsManagementSuite_Workspace/PartType/LogsDashboardPart",
+ "settings": {
+ "content": {
+ "Query": "\nlet api_hosts = datatable (name: string) [\"app-backend.io.italia.it\"];\nlet threshold = 1;\nAzureDiagnostics\n| where originalHost_s in (api_hosts)\n| where requestUri_s matches regex \"/api/v1/services$\"\n| summarize\n watermark=threshold,\n duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)\n| render timechart with (xtitle = \"time\", ytitle= \"response time(s)\")\n",
+ "SpecificChart": "Line",
+ "PartTitle": "Percentile Response Time (5m)",
+ "Dimensions": {
+ "xAxis": {
+ "name": "TimeGenerated",
+ "type": "datetime"
+ },
+ "yAxis": [
+ {
+ "name": "watermark",
+ "type": "long"
+ },
+ {
+ "name": "duration_percentile_95",
+ "type": "real"
+ }
+ ],
+ "splitBy": [],
+ "aggregation": "Sum"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "metadata": {
+ "model": {
+ "timeRange": {
+ "value": {
+ "relative": {
+ "duration": 24,
+ "timeUnit": 1
+ }
+ },
+ "type": "MsPortalFx.Composition.Configuration.ValueTypes.TimeRange"
+ },
+ "filterLocale": {
+ "value": "en-us"
+ },
+ "filters": {
+ "value": {
+ "MsPortalFx_TimeRange": {
+ "model": {
+ "format": "local",
+ "granularity": "auto",
+ "relative": "48h"
+ },
+ "displayCache": {
+ "name": "Local Time",
+ "value": "Past 48 hours"
+ },
+ "filteredPartIds": [
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ed",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432ef",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f1",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f3",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f5",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f7",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432f9",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fb",
+ "StartboardPart-LogsDashboardPart-9badbd78-7607-4131-8fa1-8b85191432fd"
+ ]
+ }
+ }
+ }
+ }
+ }
+}
+ PROPS
+
+ tags = var.tags
+}
+
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_0" {
+ name = replace(join("_", split("/", "${local.name}-availability @ /api/v1/services/{service_id}")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ description = "Availability for /api/v1/services/{service_id} is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 0.99;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services/[^/]+$"
+| summarize
+ Total=count(),
+ Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)
+| extend availability=toreal(Success) / Total
+| where availability < threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 1
+ }
+
+ tags = var.tags
+}
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_0" {
+ name = replace(join("_", split("/", "${local.name}-responsetime @ /api/v1/services/{service_id}")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ description = "Response time for /api/v1/services/{service_id} is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 1;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services/[^/]+$"
+| summarize
+ watermark=threshold,
+ duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)
+| where duration_percentile_95 > threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 1
+ }
+
+ tags = var.tags
+}
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_availability_1" {
+ name = replace(join("_", split("/", "${local.name}-availability @ /api/v1/services")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ description = "Availability for /api/v1/services is less than or equal to 99% - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 0.99;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services$"
+| summarize
+ Total=count(),
+ Success=count(httpStatus_d < 500) by bin(TimeGenerated, 5m)
+| extend availability=toreal(Success) / Total
+| where availability < threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 1
+ }
+
+ tags = var.tags
+}
+
+resource "azurerm_monitor_scheduled_query_rules_alert" "alarm_time_1" {
+ name = replace(join("_", split("/", "${local.name}-responsetime @ /api/v1/services")), "/\\{|\\}/", "")
+ resource_group_name = data.azurerm_resource_group.this.name
+ location = data.azurerm_resource_group.this.location
+
+ action {
+ action_group = ["/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-email", "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group-slack"]
+ }
+
+ data_source_id = "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw"
+ description = "Response time for /api/v1/services is less than or equal to 1s - ${local.dashboard_base_addr}${azurerm_portal_dashboard.this.id}"
+ enabled = true
+ auto_mitigation_enabled = false
+
+ query = <<-QUERY
+
+
+let api_hosts = datatable (name: string) ["app-backend.io.italia.it"];
+let threshold = 1;
+AzureDiagnostics
+| where originalHost_s in (api_hosts)
+| where requestUri_s matches regex "/api/v1/services$"
+| summarize
+ watermark=threshold,
+ duration_percentile_95=percentiles(timeTaken_d, 95) by bin(TimeGenerated, 5m)
+| where duration_percentile_95 > threshold
+
+
+ QUERY
+
+ severity = 1
+ frequency = 10
+ time_window = 20
+ trigger {
+ operator = "GreaterThanOrEqual"
+ threshold = 1
+ }
+
+ tags = var.tags
+}
+
+
diff --git a/packages/opex-dashboard-ts/test/fixtures/test_openapi.yaml b/packages/opex-dashboard-ts/test/fixtures/test_openapi.yaml
new file mode 100644
index 00000000..1ebae78f
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/fixtures/test_openapi.yaml
@@ -0,0 +1,1533 @@
+swagger: "2.0"
+info:
+ version: 1.0.0
+ title: Proxy API
+ description: Mobile and web proxy API gateway.
+host: app-backend.io.italia.it
+basePath: /api/v1
+schemes:
+ - https
+security:
+ - Bearer: []
+paths:
+ "/services/{service_id}":
+ x-swagger-router-controller: ServicesController
+ parameters:
+ - name: service_id
+ in: path
+ type: string
+ required: true
+ description: The ID of an existing Service.
+ get:
+ operationId: getService
+ summary: Get Service
+ description: A previously created service with the provided service ID is returned.
+ responses:
+ "200":
+ description: Service found.
+ schema:
+ "$ref": "#/definitions/ServicePublic"
+ examples:
+ application/json:
+ department_name: "IO"
+ organization_fiscal_code: "00000000000"
+ organization_name: "IO"
+ service_id: "5a563817fcc896087002ea46c49a"
+ service_name: "App IO"
+ version: 1
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: No service found for the provided ID.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the service.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ parameters: []
+ "/services/{service_id}/preferences":
+ post:
+ operationId: upsertServicePreferences
+ summary: UpsertServicePreferences
+ parameters:
+ - name: service_id
+ in: path
+ type: string
+ required: true
+ description: The ID of an existing Service.
+ - in: body
+ name: body
+ schema:
+ $ref: "#/definitions/UpsertServicePreference"
+ responses:
+ "200":
+ description: Service Preference found.
+ schema:
+ "$ref": "#/definitions/ServicePreference"
+ examples:
+ application/json:
+ can_access_message_read_status: true
+ is_inbox_enabled: true
+ is_email_enabled: false
+ is_webhook_enabled: true
+ settings_version: 1
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Unauthorized
+ "404":
+ description: No service found for the provided ID.
+ "409":
+ description: |-
+ Conflict. Either the provided preference setting version is not consistent with the current version stored in the Profile
+ or the Profile is not in the correct preference mode.
+ "429":
+ description: Too many requests
+ "500":
+ description: Internal Server Error
+ get:
+ operationId: getServicePreferences
+ summary: GetServicePreferences
+ parameters:
+ - name: service_id
+ in: path
+ type: string
+ required: true
+ description: The ID of an existing Service.
+ responses:
+ "200":
+ description: Service Preference found.
+ schema:
+ "$ref": "#/definitions/ServicePreference"
+ examples:
+ application/json:
+ can_access_message_read_status: true
+ is_inbox_enabled: true
+ is_email_enabled: false
+ is_webhook_enabled: true
+ settings_version: 1
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Unauthorized
+ "404":
+ description: No service found for the provided ID.
+ "409":
+ description: Conflict. The Profile is not in the correct preference mode.
+ "429":
+ description: Too many requests
+ "500":
+ description: Internal Server Error
+ "/services":
+ x-swagger-router-controller: ServicesController
+ get:
+ operationId: getVisibleServices
+ summary: Get all visible services
+ description: |-
+ Returns the description of all visible services.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/PaginatedServiceTupleCollection"
+ examples:
+ application/json:
+ items:
+ - service_id: "AzureDeployc49a"
+ version: 1
+ - service_id: "5a25abf4fcc89605c082f042c49a"
+ version: 0
+ page_size: 1
+ "401":
+ description: Bearer token null or expired.
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the services.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ parameters:
+ - $ref: "#/parameters/PaginationRequest"
+ "/messages":
+ x-swagger-router-controller: MessagesController
+ parameters:
+ - $ref: "#/parameters/PageSize"
+ - $ref: "#/parameters/EnrichResultData"
+ - $ref: "#/parameters/GetArchivedMessages"
+ - $ref: "#/parameters/MaximumId"
+ - $ref: "#/parameters/MinimumId"
+ get:
+ operationId: getUserMessages
+ summary: Get user's messages
+ description: |-
+ Returns the messages for the user identified by the provided fiscal code.
+ Messages will be returned in inverse acceptance order (from last to first).
+ The "next" field, when present, contains an URL pointing to the next page of results.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/PaginatedPublicMessagesCollection"
+ examples:
+ application/json:
+ items:
+ - created_at: "2018-05-21T07:36:41.209Z"
+ fiscal_code: "LSSLCU79B24L219P"
+ id: "01CE0T1Z18T3NT9ECK5NJ09YR3"
+ sender_service_id: "5a563817fcc896087002ea46c49a"
+ time_to_live: 3600
+ - created_at: "2018-05-21T07:41:01.361Z"
+ fiscal_code: "LSSLCU79B24L219P"
+ id: "01CE0T9X1HT595GEF8FH9NRSW7"
+ sender_service_id: "5a563817fcc896087002ea46c49a"
+ time_to_live: 3600
+ next: 01CE0T9X1HT595GEF8FH9NRSW7
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: No message found.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the messages.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/messages/{id}":
+ x-swagger-router-controller: MessagesController
+ parameters:
+ - name: id
+ in: path
+ type: string
+ required: true
+ description: The ID of the message.
+ - $ref: "#/parameters/PublicMessage"
+ get:
+ operationId: getUserMessage
+ summary: Get message
+ description: |-
+ Returns the message with the provided message ID.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/CreatedMessageWithContentAndAttachments"
+ examples:
+ application/json: |
+ content: {
+ markdown: "hey hey !! some content here ..... this is a link with a style applied, some other content",
+ subject: "my subject ............",
+ attachments: [{name:"attachment", content:"aBase64Encoding", mime_type: "image/png"}]
+ },
+ created_at: "2018-06-06T12:22:24.523Z",
+ fiscal_code: "LSSLCU79B24L219P",
+ id: "01CFAGRMGB9XCA8B2CQ4QA7K76",
+ sender_service_id: "5a25abf4fcc89605c082f042c49a",
+ time_to_live: 3600
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: No message found for the provided ID.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the message.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/messages/{id}/message-status":
+ x-swagger-router-controller: MessagesController
+ put:
+ operationId: upsertMessageStatusAttributes
+ summary: UpsertMessageStatusAttributes
+ description: Updates the status of a message with attributes
+ parameters:
+ - name: id
+ in: path
+ type: string
+ required: true
+ description: The ID of the message.
+ - name: body
+ in: body
+ schema:
+ $ref: "#/definitions/MessageStatusChange"
+ required: true
+ x-examples:
+ application/json: |
+ change_type: "bulk",
+ is_archived: true,
+ is_read: true
+ responses:
+ "200":
+ description: Success.
+ schema:
+ $ref: "#/definitions/MessageStatusAttributes"
+ examples:
+ application/json:
+ is_read: true,
+ is_archived: false
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "403":
+ description: Operation forbidden.
+ "404":
+ description: No message found for the provided ID.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in upserting message's status attributes.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/legal-messages/{id}":
+ x-swagger-router-controller: MessagesController
+ parameters:
+ - name: id
+ in: path
+ type: string
+ required: true
+ description: The ID of the message.
+ get:
+ operationId: getUserLegalMessage
+ summary: Get legal message
+ description: |-
+ Returns the legal message with the provided message ID.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/LegalMessageWithContent"
+ examples:
+ application/json: |
+ content: {
+ markdown: "hey hey !! some content here ..... this is a link with a style applied, some other content",
+ subject: "my subject ............",
+ attachments: [{name:"attachment", content:"aBase64Encoding", mime_type: "image/png"}]
+ },
+ created_at: "2018-06-06T12:22:24.523Z",
+ fiscal_code: "LSSLCU79B24L219P",
+ id: "01CFAGRMGB9XCA8B2CQ4QA7K76",
+ sender_service_id: "5a25abf4fcc89605c082f042c49a",
+ time_to_live: 3600
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: No message found for the provided ID.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the message.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/legal-messages/{id}/attachments/{attachment_id}":
+ x-swagger-router-controller: MessagesController
+ get:
+ operationId: getLegalMessageAttachment
+ summary: Retrieve an attachment of a legal message
+ produces:
+ - application/octet-stream
+ parameters:
+ - name: id
+ in: path
+ type: string
+ required: true
+ description: The ID of the message.
+ - in: path
+ name: attachment_id
+ required: true
+ type: string
+ responses:
+ "200":
+ description: Success
+ schema:
+ format: binary
+ type: string
+ "400":
+ description: Bad Request
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
+ "429":
+ description: Too Many Requests
+ "500":
+ description: Internal Server Error
+ "/third-party-messages/{id}":
+ x-swagger-router-controller: MessagesController
+ get:
+ operationId: getThirdPartyMessage
+ summary: Retrieve Third Party message
+ description: |-
+ Returns the Third Party message with the provided message ID.
+ parameters:
+ - name: id
+ in: path
+ type: string
+ minLength: 1
+ required: true
+ description: ID of the IO message.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/ThirdPartyMessageWithContent"
+ examples:
+ text/json: |
+ content: {
+ markdown: "hey hey !! some content here ..... this is a link with a style applied, some other content",
+ subject: "my subject ............",
+ third_party_data: [{id: "aThirdPartyMessageId"}]
+ },
+ third_party_message: {
+ attachments: [],
+ custom_property: "a custom property"
+ },
+ created_at: "2018-06-06T12:22:24.523Z",
+ fiscal_code: "LSSLCU79B24L219P",
+ id: "01CFAGRMGB9XCA8B2CQ4QA7K76",
+ sender_service_id: "5a25abf4fcc89605c082f042c49a",
+ time_to_live: 3600
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "403":
+ description: Forbidden
+ "404":
+ description: No message found for the provided ID.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "410":
+ description: Third Party Service no longer available
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the message.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "501":
+ description: Not Implemented
+ "504":
+ description: Gateway Timeout
+ "/third-party-messages/{id}/attachments/{attachment_url}":
+ x-swagger-router-controller: MessagesController
+ get:
+ operationId: getThirdPartyMessageAttachment
+ summary: Retrieve an attachment of a Thrid Party message
+ produces:
+ - application/octet-stream
+ parameters:
+ - name: id
+ in: path
+ type: string
+ minLength: 1
+ required: true
+ description: ID of the IO message.
+ - in: path
+ name: attachment_url
+ type: string
+ minLength: 1
+ required: true
+ responses:
+ "200":
+ description: Success
+ schema:
+ format: binary
+ type: string
+ "400":
+ description: Bad Request
+ "401":
+ description: Unauthorized
+ "403":
+ description: Forbidden
+ "404":
+ description: No message found for the provided ID.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "410":
+ description: Third Party Service no longer available
+ "429":
+ description: Too Many Requests
+ "500":
+ description: Internal Server Error
+ "501":
+ description: Not Implemented
+ "504":
+ description: Gateway Timeout
+ "/profile":
+ x-swagger-router-controller: ProfileController
+ get:
+ operationId: getUserProfile
+ summary: Get user's profile
+ description: Returns the profile for the user identified by the provided fiscal code.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/InitializedProfile"
+ examples:
+ application/json:
+ email: "email@example.com"
+ family_name: "Rossi"
+ fiscal_code: "TMMEXQ60A10Y526X"
+ has_profile: true
+ is_email_set: true
+ is_inbox_enabled: true
+ is_webhook_enabled: true
+ name: "Mario"
+ spid_email: "preferred@example.com"
+ service_preferences_settings:
+ - mode: LEGACY
+ version: 1
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the user profile.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ post:
+ operationId: updateProfile
+ summary: Update the User's profile
+ description: Update the profile for the user identified by the provided fiscal code.
+ parameters:
+ - in: body
+ name: body
+ schema:
+ $ref: "#/definitions/Profile"
+ required: true
+ x-examples:
+ application/json:
+ email: foobar@example.com
+ preferred_languages: [it_IT]
+ is_inbox_enabled: true
+ is_webhook_enabled: false
+ version: 1
+ responses:
+ "200":
+ description: Profile updated.
+ schema:
+ $ref: "#/definitions/InitializedProfile"
+ examples:
+ application/json:
+ email: "email@example.com"
+ family_name: "Rossi"
+ fiscal_code: "TMMEXQ60A10Y526X"
+ has_profile: true
+ is_email_set: true
+ is_inbox_enabled: true
+ is_webhook_enabled: true
+ name: "Mario"
+ spid_email: "preferred@example.com"
+ version: 0
+ "400":
+ description: Invalid payload.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: User not found
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "409":
+ description: Conflict.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: Profile cannot be updated.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/api-profile":
+ x-swagger-router-controller: ProfileController
+ get:
+ operationId: getApiUserProfile
+ summary: Get user's profile stored into the API
+ description: Returns the profile for the user identified by the provided fiscal code.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/ExtendedProfile"
+ examples:
+ application/json:
+ email: "email@example.com"
+ preferred_languages: ["it_IT"]
+ is_inbox_enabled: true
+ accepted_tos_version: 1
+ is_webhook_enabled: true
+ is_email_enabled: true
+ version: 1
+ sender_allowed: true
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: Profile not found
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error in retrieving the user profile.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/email-validation-process":
+ x-swagger-router-controller: ProfileController
+ post:
+ operationId: startEmailValidationProcess
+ summary: Start the Email Validation Process
+ description: |-
+ Start the email validation process that create the validation token
+ and send the validation email
+ responses:
+ "202":
+ description: Accepted
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: Profile not found
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: There was an error starting email validation process
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/user-metadata":
+ x-swagger-router-controller: userMetadataController
+ get:
+ operationId: getUserMetadata
+ summary: Get user's metadata
+ description: Returns metadata for the current authenticated user.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/UserMetadata"
+ "204":
+ description: No Content.
+ "401":
+ description: Bearer token null or expired.
+ "500":
+ description: There was an error in retrieving the user metadata.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ post:
+ operationId: upsertUserMetadata
+ summary: Set User's metadata
+ description: Create or update metadata for the current authenticated user.
+ parameters:
+ - in: body
+ name: body
+ schema:
+ $ref: "#/definitions/UserMetadata"
+ required: true
+ responses:
+ "200":
+ description: User Metadata updated.
+ schema:
+ $ref: "#/definitions/UserMetadata"
+ "400":
+ description: Invalid payload.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "409":
+ description: Conflict.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: Profile cannot be updated.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/installations/{installationID}":
+ x-swagger-router-controller: NotificationController
+ parameters:
+ - name: installationID
+ in: path
+ required: true
+ description: The ID of the message.
+ type: string
+ put:
+ operationId: createOrUpdateInstallation
+ summary: Create or update an Installation
+ description: Create or update an Installation to the Azure Notification hub.
+ parameters:
+ - name: body
+ in: body
+ schema:
+ $ref: "#/definitions/Installation"
+ required: true
+ x-examples:
+ application/json:
+ platform: "gcm"
+ pushChannel: "fLKP3EATnBI:APA91bEy4go681jeSEpLkNqhtIrdPnEKu6Dfi-STtUiEnQn8RwMfBiPGYaqdWrmzJyXIh5Yms4017MYRS9O1LGPZwA4sOLCNIoKl4Fwg7cSeOkliAAtlQ0rVg71Kr5QmQiLlDJyxcq3p"
+ responses:
+ "200":
+ description: Success.
+ schema:
+ $ref: "#/definitions/SuccessResponse"
+ examples:
+ application/json:
+ "message": "ok"
+ "401":
+ description: Bearer token null or expired.
+ "500":
+ description: There was an error in registering the device to the Notification Hub.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/session":
+ x-swagger-router-controller: AuthenticationController
+ get:
+ operationId: getSessionState
+ summary: Get the user current session
+ description: Return the session state for the current authenticated user.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/PublicSession"
+ examples:
+ application/json:
+ spidLevel: "https://www.spid.gov.it/SpidL2"
+ walletToken: "c77de47586c841adbd1a1caeb90dce25dcecebed620488a4f932a6280b10ee99a77b6c494a8a6e6884ccbeb6d3fe736b"
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "500":
+ description: Internal server error
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/sessions":
+ x-swagger-router-controller: AuthenticationController
+ get:
+ operationId: listUserSessions
+ summary: List sessions of a User
+ description: Return all the active sessions for an authenticated User.
+ responses:
+ "200":
+ description: Found.
+ schema:
+ $ref: "#/definitions/SessionsList"
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "500":
+ description: Unavailable service
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/token/support":
+ x-swagger-router-controller: SupportController
+ get:
+ operationId: getSupportToken
+ summary: Get a JWT Support Token
+ description: Return a JWT Support Token for the authenticated user.
+ responses:
+ "200":
+ description: Created.
+ schema:
+ $ref: "#/definitions/SupportToken"
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "500":
+ description: Unavailable service
+ schema:
+ $ref: "#/definitions/ProblemJson"
+
+ "/payment-requests/{rptId}":
+ x-swagger-router-controller: PagoPAProxyController
+ parameters:
+ - name: rptId
+ in: path
+ required: true
+ description: Unique identifier for payments.
+ type: string
+ - name: test
+ in: query
+ description: Use test environment of PagoPAClient
+ type: boolean
+ required: false
+ get:
+ operationId: getPaymentInfo
+ summary: Get Payment Info
+ description: Retrieve information about a payment
+ responses:
+ "200":
+ description: Payment information retrieved
+ schema:
+ "$ref": "#/definitions/PaymentRequestsGetResponse"
+ examples:
+ application/json:
+ importoSingoloVersamento: 200,
+ codiceContestoPagamento: "ABC123"
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "500":
+ description: PagoPA services are not available or request is rejected
+ schema:
+ $ref: "#/definitions/PaymentProblemJson"
+ "504":
+ description: gateway timeout.
+ "/payment-activations":
+ x-swagger-router-controller: PagoPAProxyController
+ parameters:
+ - name: test
+ in: query
+ description: Use test environment of PagoPAClient
+ type: boolean
+ required: false
+ post:
+ operationId: activatePayment
+ summary: Activate Payment
+ description: Require a lock (activation) for a payment
+ parameters:
+ - in: body
+ name: body
+ schema:
+ $ref: "#/definitions/PaymentActivationsPostRequest"
+ required: true
+ x-examples:
+ application/json:
+ rptId: "12345678901012123456789012345"
+ importoSingoloVersamento: 200
+ codiceContestoPagamento: "ABC123"
+ responses:
+ "200":
+ description: Payment activation process started
+ schema:
+ "$ref": "#/definitions/PaymentActivationsPostResponse"
+ examples:
+ application/json:
+ importoSingoloVersamento: 200
+ "400":
+ description: Bad request
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "500":
+ description: PagoPA services are not available or request is rejected
+ schema:
+ $ref: "#/definitions/PaymentProblemJson"
+ "504":
+ description: gateway timeout.
+ "/payment-activations/{codiceContestoPagamento}":
+ x-swagger-router-controller: PagoPAProxyController
+ parameters:
+ - name: codiceContestoPagamento
+ in: path
+ required: true
+ description: Transaction Id used to identify the communication flow.
+ type: string
+ - name: test
+ in: query
+ description: Use test environment of PagoPAClient
+ type: boolean
+ required: false
+ get:
+ operationId: getActivationStatus
+ summary: Get Activation status
+ description: Check the activation status to retrieve the paymentId
+ responses:
+ "200":
+ description: Payment information
+ schema:
+ $ref: "#/definitions/PaymentActivationsGetResponse"
+ examples:
+ application/json:
+ idPagamento: "123455"
+ "400":
+ description: Invalid input
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: Activation status not found
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "500":
+ description: Unavailable service
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "504":
+ description: gateway timeout.
+ "/user-data-processing":
+ x-swagger-router-controller: UserDataProcessingController
+ post:
+ operationId: upsertUserDataProcessing
+ summary: Set User's data processing choices
+ description: Let the authenticated user express his will to retrieve or delete his stored data.
+ parameters:
+ - in: body
+ name: body
+ schema:
+ $ref: "#/definitions/UserDataProcessingChoiceRequest"
+ required: true
+ responses:
+ "200":
+ description: User Data processing created / updated.
+ schema:
+ $ref: "#/definitions/UserDataProcessing"
+ "400":
+ description: Invalid payload.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Bearer token null or expired.
+ "409":
+ description: Conflict.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too may requests
+ "500":
+ description: User Data processing choice cannot be taken in charge.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "/user-data-processing/{choice}":
+ x-swagger-router-controller: UserDataProcessingController
+ get:
+ operationId: getUserDataProcessing
+ summary: Get User's data processing
+ description: Get the user's request to delete or download his stored data by providing a kind of choice.
+ parameters:
+ - $ref: "#/parameters/UserDataProcessingChoiceParam"
+ responses:
+ "200":
+ description: User data processing retrieved
+ schema:
+ $ref: "#/definitions/UserDataProcessing"
+ "401":
+ description: Bearer token null or expired.
+ "404":
+ description: Not found.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ delete:
+ operationId: abortUserDataProcessing
+ summary: Abort User's revious data processing request
+ description: |-
+ Ask for a request to abort, if present
+ tags:
+ - restricted
+ parameters:
+ - $ref: "#/parameters/UserDataProcessingChoiceParam"
+ responses:
+ "202":
+ description: The abort request has been recorded
+ "400":
+ description: Invalid request.
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "401":
+ description: Unauthorized
+ "404":
+ description: Not Found
+ "409":
+ description: Conflict
+ schema:
+ $ref: "#/definitions/ProblemJson"
+ "429":
+ description: Too many requests
+ "500":
+ description: Server Error
+ schema:
+ $ref: "#/definitions/ProblemJson"
+definitions:
+ # Definitions from the digital citizenship APIs
+ AcceptedTosVersion:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/AcceptedTosVersion"
+ AppVersion:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/AppVersion"
+ BlockedInboxOrChannels:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/BlockedInboxOrChannels"
+ DepartmentName:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/DepartmentName"
+ EmailAddress:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/EmailAddress"
+ PreferredLanguage:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PreferredLanguage"
+ PreferredLanguages:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PreferredLanguages"
+ Profile:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Profile"
+ ExtendedProfile:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ExtendedProfile"
+ FiscalCode:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/FiscalCode"
+ IsEmailEnabled:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsEmailEnabled"
+ IsInboxEnabled:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsInboxEnabled"
+ IsEmailValidated:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsEmailValidated"
+ IsTestProfile:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsTestProfile"
+ IsWebhookEnabled:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/IsWebhookEnabled"
+ LimitedProfile:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LimitedProfile"
+ MessageBodyMarkdown:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageBodyMarkdown"
+ MessageContent:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageContent"
+ MessageResponseNotificationStatus:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageResponseNotificationStatus"
+ NotificationChannelStatusValue:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/NotificationChannelStatusValue"
+ NotificationChannel:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/NotificationChannel"
+ MessageSubject:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageSubject"
+ MessageContentBase:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageContentBase"
+ EUCovidCert:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/EUCovidCert"
+ OrganizationFiscalCode:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/OrganizationFiscalCode"
+ NewMessageContent:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/NewMessageContent"
+ Payee:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Payee"
+ PaymentDataBase:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentDataBase"
+ PaymentDataWithRequiredPayee:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentDataWithRequiredPayee"
+ OrganizationName:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/OrganizationName"
+ PaginationResponse:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginationResponse"
+ PrescriptionData:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PrescriptionData"
+ ProblemJson:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ProblemJson"
+ ServiceId:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceId"
+ ServiceName:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceName"
+ ServicePublic:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePublic"
+ ServiceMetadata:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceMetadata"
+ CommonServiceMetadata:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CommonServiceMetadata"
+ StandardServiceMetadata:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/StandardServiceMetadata"
+ SpecialServiceMetadata:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/SpecialServiceMetadata"
+ ServiceTuple:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceTuple"
+ ServiceScope:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceScope"
+ ServiceCategory:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServiceCategory"
+ SpecialServiceCategory:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/SpecialServiceCategory"
+ StandardServiceCategory:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/StandardServiceCategory"
+ PaginatedServiceTupleCollection:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedServiceTupleCollection"
+ Timestamp:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Timestamp"
+ PaymentNoticeNumber:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentNoticeNumber"
+ PaymentAmount:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentAmount"
+ PaymentData:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaymentData"
+ TimeToLiveSeconds:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/TimeToLiveSeconds"
+ CreatedMessageWithContent:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithContent"
+ CreatedMessageWithoutContent:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithoutContent"
+ CreatedMessageWithoutContentCollection:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithoutContentCollection"
+ PaginatedCreatedMessageWithoutContentCollection:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedCreatedMessageWithoutContentCollection"
+ UserDataProcessingStatus:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessingStatus"
+ UserDataProcessingChoice:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessingChoice"
+ UserDataProcessingChoiceRequest:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessingChoiceRequest"
+ UserDataProcessing:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UserDataProcessing"
+ MessageResponseWithContent:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageResponseWithContent"
+ ServicePreferencesSettings:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePreferencesSettings"
+ ServicesPreferencesMode:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicesPreferencesMode"
+ BasicServicePreference:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/BasicServicePreference"
+ ServicePreference:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ServicePreference"
+ UpsertServicePreference:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/UpsertServicePreference"
+ EnrichedMessage:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/EnrichedMessage"
+ PublicMessage:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PublicMessage"
+ PublicMessagesCollection:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PublicMessagesCollection"
+ PaginatedPublicMessagesCollection:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/PaginatedPublicMessagesCollection"
+ MessageCategory:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategory"
+ MessageCategoryBase:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategoryBase"
+ MessageCategoryPayment:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategoryPayment"
+ LegalMessageWithContent:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessageWithContent"
+ MessageCategoryPN:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageCategoryPN"
+ LegalMessage:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessage"
+
+ ThirdPartyData:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyData"
+ ThirdPartyMessageWithContent:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyMessageWithContent"
+ ThirdPartyMessage:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyMessage"
+ ThirdPartyAttachment:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/ThirdPartyAttachment"
+
+ LegalMessageEml:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessageEml"
+ LegalMessageCertData:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalMessageCertData"
+ CertData:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CertData"
+ CertDataHeader:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CertDataHeader"
+ Attachment:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/Attachment"
+ LegalData:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/LegalData"
+ MessageStatusAttributes:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusAttributes"
+ MessageStatusReadingChange:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusReadingChange"
+ MessageStatusArchivingChange:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusArchivingChange"
+ MessageStatusBulkChange:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusBulkChange"
+ MessageStatusChange:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/MessageStatusChange"
+ CreatedMessageWithContentResponse:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithContentResponse"
+ CreatedMessageWithContentAndEnrichedData:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-functions-commons/v25.5.1/openapi/definitions.yaml#/CreatedMessageWithContentAndEnrichedData"
+ # Definitions from pagopa-proxy
+ PaymentProblemJson:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentProblemJson"
+ CodiceContestoPagamento:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/CodiceContestoPagamento"
+ EnteBeneficiario:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/EnteBeneficiario"
+ Iban:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/Iban"
+ ImportoEuroCents:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/ImportoEuroCents"
+ PaymentActivationsGetResponse:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentActivationsGetResponse"
+ PaymentActivationsPostRequest:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentActivationsPostRequest"
+ PaymentActivationsPostResponse:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentActivationsPostResponse"
+ PaymentRequestsGetResponse:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/PaymentRequestsGetResponse"
+ RptId:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/RptId"
+ SpezzoneStrutturatoCausaleVersamento:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/SpezzoneStrutturatoCausaleVersamento"
+ SpezzoniCausaleVersamento:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/SpezzoniCausaleVersamento"
+ SpezzoniCausaleVersamentoItem:
+ $ref: "https://raw.githubusercontent.com/pagopa/io-pagopa-proxy/v0.20.0/api_pagopa.yaml#/definitions/SpezzoniCausaleVersamentoItem"
+ MessageContentWithAttachments:
+ allOf:
+ - type: object
+ properties:
+ attachments:
+ type: array
+ items:
+ $ref: "#/definitions/MessageAttachment"
+ - $ref: "#/definitions/NewMessageContent"
+ MessageAttachment:
+ type: object
+ title: MessageAttachment
+ description: Describes a message's attachment
+ properties:
+ name:
+ type: string
+ content:
+ type: string
+ mime_type:
+ type: string
+ required:
+ - name
+ - content
+ - mime_type
+ CreatedMessageWithContentAndAttachments:
+ allOf:
+ - type: object
+ properties:
+ content:
+ $ref: "#/definitions/MessageContentWithAttachments"
+ required:
+ - content
+ - $ref: "#/definitions/CreatedMessageWithContentResponse"
+ Installation:
+ type: object
+ title: Installation
+ description: Describes an app installation.
+ properties:
+ platform:
+ $ref: "#/definitions/Platform"
+ pushChannel:
+ $ref: "#/definitions/PushChannel"
+ required:
+ - platform
+ - pushChannel
+ InitializedProfile:
+ type: object
+ title: Initialized profile
+ description: Describes the user's profile after it has been stored in the Profile API.
+ properties:
+ accepted_tos_version:
+ $ref: "#/definitions/AcceptedTosVersion"
+ email:
+ $ref: "#/definitions/EmailAddress"
+ blocked_inbox_or_channels:
+ $ref: "#/definitions/BlockedInboxOrChannels"
+ preferred_languages:
+ $ref: "#/definitions/PreferredLanguages"
+ is_inbox_enabled:
+ $ref: "#/definitions/IsInboxEnabled"
+ is_email_validated:
+ $ref: "#/definitions/IsEmailValidated"
+ is_email_enabled:
+ $ref: "#/definitions/IsEmailEnabled"
+ is_webhook_enabled:
+ $ref: "#/definitions/IsWebhookEnabled"
+ family_name:
+ type: string
+ fiscal_code:
+ $ref: "#/definitions/FiscalCode"
+ has_profile:
+ $ref: "#/definitions/HasProfile"
+ last_app_version:
+ $ref: "#/definitions/AppVersion"
+ name:
+ type: string
+ spid_email:
+ $ref: "#/definitions/EmailAddress"
+ date_of_birth:
+ type: string
+ format: date
+ service_preferences_settings:
+ $ref: "#/definitions/ServicePreferencesSettings"
+ version:
+ $ref: "#/definitions/Version"
+ required:
+ - family_name
+ - fiscal_code
+ - has_profile
+ - is_inbox_enabled
+ - is_email_enabled
+ - is_webhook_enabled
+ - name
+ - service_preferences_settings
+ - version
+ UserMetadata:
+ type: object
+ title: User Metadata information
+ properties:
+ version:
+ type: number
+ metadata:
+ type: string
+ required:
+ - version
+ - metadata
+ PublicSession:
+ type: object
+ title: User session data
+ description: Describe the current session of an authenticated user.
+ properties:
+ spidLevel:
+ $ref: "#/definitions/SpidLevel"
+ walletToken:
+ type: string
+ myPortalToken:
+ type: string
+ bpdToken:
+ type: string
+ zendeskToken:
+ type: string
+ fimsToken:
+ type: string
+ required:
+ - spidLevel
+ - walletToken
+ - myPortalToken
+ - bpdToken
+ - zendeskToken
+ - fimsToken
+ SessionInfo:
+ type: object
+ title: Session info of a user
+ description: Decribe a session of an authenticated user.
+ properties:
+ createdAt:
+ $ref: "#/definitions/Timestamp"
+ sessionToken:
+ type: string
+ required:
+ - createdAt
+ - sessionToken
+ SessionsList:
+ description: Contains all active sessions for an authenticated user.
+ type: object
+ properties:
+ sessions:
+ type: array
+ items:
+ $ref: "#/definitions/SessionInfo"
+ required:
+ - sessions
+ InstallationID:
+ type: string
+ description: |-
+ The sixteen octets of an Installation ID are represented as 32 hexadecimal (base 16) digits, displayed in five groups
+ separated by hyphens, in the form 8-4-4-4-12 for a total of 36 characters (32 alphanumeric characters and four
+ hyphens).
+ See https://en.wikipedia.org/wiki/Universally_unique_identifier
+ minLength: 1
+ HasProfile:
+ type: boolean
+ default: false
+ description: True if the user has a remote profile.
+ IsEmailSet:
+ type: boolean
+ default: false
+ description: True if the user has presonalized the email.
+ Version:
+ type: integer
+ description: The entity version.
+ Platform:
+ type: string
+ description: The platform type where the installation happened.
+ x-extensible-enum:
+ - apns
+ - gcm
+ PushChannel:
+ type: string
+ description: |-
+ The Push Notification Service handle for this Installation.
+ See https://docs.microsoft.com/en-us/azure/notification-hubs/notification-hubs-push-notification-registration-management
+ SpidLevel:
+ type: string
+ description: A SPID level.
+ x-extensible-enum:
+ - https://www.spid.gov.it/SpidL1
+ - https://www.spid.gov.it/SpidL2
+ - https://www.spid.gov.it/SpidL3
+ SuccessResponse:
+ type: object
+ properties:
+ message:
+ type: string
+ LimitedFederatedUser:
+ title: Federated user
+ description: User data needed by federated applications.
+ type: object
+ properties:
+ fiscal_code:
+ $ref: "#/definitions/FiscalCode"
+ required:
+ - fiscal_code
+ FederatedUser:
+ title: Federated user
+ description: User data needed by federated applications.
+ allOf:
+ - type: object
+ properties:
+ name:
+ type: string
+ family_name:
+ type: string
+ required:
+ - name
+ - family_name
+ - $ref: "#/definitions/LimitedFederatedUser"
+ SupportToken:
+ title: Support token
+ description: A Support Token response
+ type: object
+ properties:
+ access_token:
+ type: string
+ expires_in:
+ type: number
+ required:
+ - access_token
+ - expires_in
+responses: {}
+parameters:
+ PublicMessage:
+ name: public_message
+ in: query
+ type: boolean
+ description: Discriminate when to return public message shape. Default to false.
+ EnrichResultData:
+ name: enrich_result_data
+ type: boolean
+ in: query
+ required: false
+ description: Indicates whether result data should be enriched or not.
+ GetArchivedMessages:
+ name: archived
+ type: boolean
+ in: query
+ required: false
+ description: Indicates whether retrieve archived/not archived messages. Default is false
+ PageSize:
+ name: page_size
+ type: integer
+ in: query
+ minimum: 1
+ maximum: 100
+ required: false
+ description: How many items a page should include.
+ MaximumId:
+ name: maximum_id
+ type: string
+ in: query
+ required: false
+ description: >-
+ The maximum id to get messages until to.
+ MinimumId:
+ name: minimum_id
+ type: string
+ in: query
+ required: false
+ description: >-
+ The minimum id to get messages from.
+ PaginationRequest:
+ type: string
+ name: cursor
+ in: query
+ minimum: 1
+ description: An opaque identifier that points to the next item in the collection.
+ UserDataProcessingChoiceParam:
+ name: choice
+ in: path
+ type: string
+ enum: [DOWNLOAD, DELETE]
+ description: A representation of a user data processing choice
+ required: true
+ x-example: DOWNLOAD
+consumes:
+ - application/json
+produces:
+ - application/json
+securityDefinitions:
+ Bearer:
+ type: apiKey
+ name: Authorization
+ in: header
diff --git a/packages/opex-dashboard-ts/test/unit/cli.test.ts b/packages/opex-dashboard-ts/test/unit/cli.test.ts
new file mode 100644
index 00000000..94aa5eb2
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/unit/cli.test.ts
@@ -0,0 +1,125 @@
+import { generateCommand } from "../../src/infrastructure/cli/generate.js";
+import { ConfigValidatorAdapter } from "../../src/infrastructure/config/config-validator-adapter.js";
+import { describe, it, expect } from "vitest";
+
+describe("CLI Commands", () => {
+ describe("generateCommand", () => {
+ it("should be a commander command instance", () => {
+ expect(generateCommand).toBeDefined();
+ expect(generateCommand.name()).toBe("generate");
+ });
+
+ it("should have correct description", () => {
+ expect(generateCommand.description()).toContain(
+ "Generate dashboard definition",
+ );
+ });
+
+ it("should have required config-file option", () => {
+ const options = generateCommand.options;
+ const configOption = options.find((opt) =>
+ opt.flags.includes("--config-file"),
+ );
+
+ expect(configOption).toBeDefined();
+ expect(configOption?.flags).toContain("-c");
+ expect(configOption?.flags).toContain("--config-file");
+ expect(configOption?.required).toBe(true);
+ });
+
+ it("should not have template-name option", () => {
+ const options = generateCommand.options;
+ const templateOption = options.find((opt) =>
+ opt.flags.includes("--template-name"),
+ );
+
+ expect(templateOption).toBeUndefined();
+ });
+
+ it("should have an action configured", () => {
+ // Since we can't access private properties, we verify the command has the expected structure
+ expect(generateCommand).toHaveProperty("options");
+ expect(Array.isArray(generateCommand.options)).toBe(true);
+ });
+ });
+
+ describe("config validation", () => {
+ const configValidator = new ConfigValidatorAdapter();
+
+ it("should validate a valid config", () => {
+ const validConfig = {
+ oa3_spec: "https://example.com/spec.yaml",
+ name: "Test Dashboard",
+ location: "West Europe",
+ data_source:
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw",
+ resource_type: "app-gateway" as const,
+ timespan: "5m",
+ action_groups: [
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/microsoft.insights/actionGroups/my-action-group",
+ ],
+ };
+
+ expect(() => configValidator.validateConfig(validConfig)).not.toThrow();
+ const result = configValidator.validateConfig(validConfig);
+ expect(result.oa3_spec).toBe("https://example.com/spec.yaml");
+ expect(result.name).toBe("Test Dashboard");
+ });
+
+ it("should throw error for missing required fields", () => {
+ const invalidConfig = {
+ name: "Test Dashboard",
+ location: "West Europe",
+ // missing oa3_spec and data_source
+ };
+
+ expect(() => configValidator.validateConfig(invalidConfig)).toThrow(
+ "Configuration validation failed:",
+ );
+ expect(() => configValidator.validateConfig(invalidConfig)).toThrow(
+ "oa3_spec: Invalid input: expected string, received undefined",
+ );
+ expect(() => configValidator.validateConfig(invalidConfig)).toThrow(
+ "data_source: Invalid input: expected string, received undefined",
+ );
+ });
+
+ it("should apply defaults for optional fields", () => {
+ const configWithDefaults = {
+ oa3_spec: "https://example.com/spec.yaml",
+ name: "Test Dashboard",
+ location: "West Europe",
+ data_source:
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw",
+ };
+
+ const result = configValidator.validateConfig(configWithDefaults);
+ expect(result.resource_type).toBe("app-gateway"); // default value
+ expect(result.timespan).toBe("5m"); // default value
+ expect(result.resource_group_name).toBe("dashboards"); // default value
+ });
+ });
+
+ describe("command validation", () => {
+ it("should accept valid template names", () => {
+ const validTemplates = ["azure-dashboard"];
+
+ validTemplates.forEach((template) => {
+ expect(() => {
+ // This would normally validate the template name in the action handler
+ // For testing purposes, we just check the command structure
+ }).not.toThrow();
+ });
+ });
+
+ it("should require config file to exist", () => {
+ // This test validates the conceptual requirement
+ // The actual file existence check happens in the action handler
+ expect(
+ generateCommand.options.some((opt) =>
+ opt.flags.includes("--config-file"),
+ ),
+ ).toBe(true);
+ });
+ });
+});
diff --git a/packages/opex-dashboard-ts/test/unit/dashboard-properties.test.ts b/packages/opex-dashboard-ts/test/unit/dashboard-properties.test.ts
new file mode 100644
index 00000000..264576f9
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/unit/dashboard-properties.test.ts
@@ -0,0 +1,50 @@
+import { describe, it, expect } from "vitest";
+import { buildDashboardPropertiesTemplate } from "../../src/infrastructure/terraform/dashboard-properties.js";
+import { DashboardConfig, Endpoint } from "../../src/domain/index.js";
+
+/*
+ Snapshot-style test to ensure dashboard queries (availability & response time) contain
+ full time-series (render clause + watermark) and do NOT include alert-only filters.
+*/
+
+describe("dashboard properties template", () => {
+ const endpoint: Endpoint = {
+ path: "/api/v1/services/{serviceId}",
+ availability_threshold: 0.99,
+ response_time_threshold: 1,
+ } as Endpoint;
+
+ const config: DashboardConfig = {
+ oa3_spec: "spec.yaml",
+ name: "Demo Dashboard",
+ location: "westeurope",
+ data_source: "workspace-id",
+ resource_type: "app-gateway",
+ timespan: "5m",
+ resource_group_name: "rg-demo",
+ resourceIds: [
+ "/subscriptions/xxxx/resourceGroups/rg-demo/providers/Microsoft.Network/applicationGateways/gtw-demo",
+ ],
+ hosts: ["app-backend.io.italia.it"],
+ endpoints: [endpoint],
+ } as unknown as DashboardConfig;
+
+ it("generates dashboard JSON with full-series availability & response time queries", () => {
+ const json = buildDashboardPropertiesTemplate(config);
+
+ // Availability: should have render + watermark projection; no threshold filter line
+ expect(json).toContain("availability(%)");
+ expect(json).toContain("watermark");
+ expect(json).not.toContain("where availability < threshold");
+
+ // Response time: dashboard variant renders chart, not filtered by threshold
+ expect(json).toContain("response time (s)");
+ expect(json).not.toContain("where duration_percentile_95 > threshold");
+
+ // Regex expansion check
+ expect(json).toContain("/api/v1/services/[^/]+$");
+ // Subtitle normalization: camelCase placeholder should be snake_case
+ expect(json).toContain("{service_id}");
+ expect(json).not.toContain("{serviceId}");
+ });
+});
diff --git a/packages/opex-dashboard-ts/test/unit/endpoint-parser.test.ts b/packages/opex-dashboard-ts/test/unit/endpoint-parser.test.ts
new file mode 100644
index 00000000..d3088191
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/unit/endpoint-parser.test.ts
@@ -0,0 +1,82 @@
+import { EndpointParserService } from "../../src/domain/services/endpoint-parser-service.js";
+import { OpenAPISpec } from "../../src/shared/openapi.js";
+import { DashboardConfig } from "../../src/domain/entities/dashboard-config.js";
+import { describe, it, expect } from "vitest";
+
+describe("parseEndpoints", () => {
+ const endpointParser = new EndpointParserService();
+ const mockConfig: DashboardConfig = {
+ oa3_spec: "/path/to/spec.yaml",
+ name: "Test Dashboard",
+ location: "eastus",
+ data_source: "test-workspace",
+ endpoints: [],
+ };
+
+ describe("with simple OpenAPI 3.0 spec", () => {
+ const mockSpec: OpenAPISpec = {
+ openapi: "3.0.0",
+ info: { title: "Test API", version: "1.0.0" },
+ servers: [{ url: "https://api.example.com" }],
+ paths: {
+ "/users": { get: {} } as any,
+ "/users/{id}": { get: {}, put: {}, delete: {} } as any,
+ "/posts/{postId}/comments": { get: {}, post: {} } as any,
+ },
+ } as OpenAPISpec;
+
+ it("should parse endpoints with server URL", () => {
+ const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig);
+
+ expect(endpoints.length).toBe(3);
+ expect(endpoints.map((e) => e.path)).toEqual([
+ "/users",
+ "/users/{id}",
+ "/posts/{postId}/comments",
+ ]);
+ });
+
+ it("should apply default configuration to endpoints", () => {
+ const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig);
+
+ endpoints.forEach((endpoint) => {
+ expect(endpoint).toHaveProperty("path");
+ expect(endpoint).toHaveProperty("availability_threshold", 0.99); // from defaults
+ expect(endpoint).toHaveProperty("response_time_threshold", 1); // from defaults
+ });
+ });
+ });
+
+ describe("with OpenAPI 2.0 spec without servers", () => {
+ const mockSpec: OpenAPISpec = {
+ swagger: "2.0",
+ info: { title: "Test API", version: "1.0.0" },
+ host: "api.example.com",
+ basePath: "/v1",
+ paths: {
+ "/users": { get: {} } as any,
+ },
+ } as OpenAPISpec;
+
+ it("should use host and basePath", () => {
+ const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig);
+
+ expect(endpoints.length).toBe(1);
+ expect(endpoints[0].path).toBe("/v1/users");
+ });
+ });
+
+ describe("with empty paths", () => {
+ const mockSpec: OpenAPISpec = {
+ openapi: "3.0.0",
+ info: { title: "Test API", version: "1.0.0" },
+ servers: [{ url: "https://api.example.com" }],
+ paths: {},
+ } as OpenAPISpec;
+
+ it("should return empty array", () => {
+ const endpoints = endpointParser.parseEndpoints(mockSpec, mockConfig);
+ expect(endpoints).toEqual([]);
+ });
+ });
+});
diff --git a/packages/opex-dashboard-ts/test/unit/kusto-queries.test.ts b/packages/opex-dashboard-ts/test/unit/kusto-queries.test.ts
new file mode 100644
index 00000000..8fa3147d
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/unit/kusto-queries.test.ts
@@ -0,0 +1,207 @@
+import { describe, expect, it } from "vitest";
+
+import { DashboardConfig } from "../../src/domain/entities/dashboard-config.js";
+import { Endpoint } from "../../src/domain/entities/endpoint.js";
+import { KustoQueryService } from "../../src/domain/services/kusto-query-service.js";
+
+describe("Kusto Query Generation", () => {
+ const kustoQueryService = new KustoQueryService();
+ const mockEndpoint: Endpoint = {
+ path: "/api/users",
+ availability_threshold: 0.99,
+ availability_evaluation_frequency: 10,
+ availability_evaluation_time_window: 20,
+ availability_event_occurrences: 1,
+ response_time_threshold: 1,
+ response_time_evaluation_frequency: 10,
+ response_time_evaluation_time_window: 20,
+ response_time_event_occurrences: 1,
+ };
+
+ const mockConfig: DashboardConfig = {
+ oa3_spec: "/path/to/spec.yaml",
+ name: "Test Dashboard",
+ location: "eastus",
+ data_source: "test-workspace",
+ resource_type: "app-gateway",
+ timespan: "5m",
+ hosts: ["api.example.com"],
+ endpoints: [],
+ };
+
+ describe("buildAvailabilityQuery", () => {
+ it("should generate correct availability query for app-gateway (alert)", () => {
+ const query = kustoQueryService.buildAvailabilityQuery(
+ mockEndpoint,
+ mockConfig,
+ "alert",
+ );
+
+ expect(query).toContain("AzureDiagnostics");
+ expect(query).toContain("originalHost_s in (api_hosts)");
+ expect(query).toContain(
+ 'let api_hosts = datatable (name: string) ["api.example.com"]',
+ );
+ expect(query).toContain("requestUri_s matches regex");
+ expect(query).toContain("httpStatus_d < 500");
+ expect(query).toContain("availability=toreal(Success) / Total");
+ expect(query).toContain("where availability < threshold");
+ expect(query).toContain("let threshold = 0.99");
+ });
+
+ it("should generate correct availability query for api-management (alert)", () => {
+ const apiConfig = {
+ ...mockConfig,
+ resource_type: "api-management" as const,
+ };
+ const query = kustoQueryService.buildAvailabilityQuery(
+ mockEndpoint,
+ apiConfig,
+ "alert",
+ );
+
+ expect(query).toContain("url_s matches regex");
+ expect(query).toContain("responseCode_d < 500");
+ expect(query).not.toContain("originalHost_s");
+ expect(query).not.toContain("api_hosts"); // No datatable for api-management
+ });
+
+ it("should include time window in query (alert)", () => {
+ const query = kustoQueryService.buildAvailabilityQuery(
+ mockEndpoint,
+ mockConfig,
+ "alert",
+ );
+ expect(query).toContain("bin(TimeGenerated, 5m)");
+ });
+
+ it("should include api_hosts datatable for app-gateway (alert)", () => {
+ const query = kustoQueryService.buildAvailabilityQuery(
+ mockEndpoint,
+ mockConfig,
+ "alert",
+ );
+
+ expect(query).toContain("let api_hosts = datatable (name: string)");
+ expect(query).toContain("originalHost_s in (api_hosts)");
+ });
+ it("should NOT filter by threshold in dashboard availability query", () => {
+ const query = kustoQueryService.buildAvailabilityQuery(
+ mockEndpoint,
+ mockConfig,
+ "dashboard",
+ );
+ expect(query).toContain("render timechart");
+ expect(query).not.toContain("where availability < threshold");
+ expect(query).toContain("watermark=threshold");
+ });
+ });
+
+ describe("buildResponseTimeQuery", () => {
+ it("should generate correct response time query for app-gateway (alert)", () => {
+ const query = kustoQueryService.buildResponseTimeQuery(
+ mockEndpoint,
+ mockConfig,
+ "alert",
+ );
+
+ expect(query).toContain("AzureDiagnostics");
+ expect(query).toContain("originalHost_s in (api_hosts)");
+ expect(query).toContain("let api_hosts = datatable (name: string)");
+ expect(query).toContain("requestUri_s matches regex");
+ expect(query).toContain("timeTaken_d");
+ expect(query).toContain("percentiles(timeTaken_d, 95)");
+ expect(query).toContain("watermark=threshold");
+ expect(query).toContain("where duration_percentile_95 > threshold");
+ });
+
+ it("should generate correct response time query for api-management (alert)", () => {
+ const apiConfig = {
+ ...mockConfig,
+ resource_type: "api-management" as const,
+ };
+ const query = kustoQueryService.buildResponseTimeQuery(
+ mockEndpoint,
+ apiConfig,
+ "alert",
+ );
+
+ expect(query).toContain("url_s matches regex");
+ expect(query).toContain("DurationMs");
+ expect(query).toContain("percentile(DurationMs, 95)");
+ expect(query).not.toContain("originalHost_s");
+ expect(query).not.toContain("api_hosts"); // No datatable for api-management
+ });
+
+ it("should use correct response time threshold (alert)", () => {
+ const customEndpoint = { ...mockEndpoint, response_time_threshold: 2 };
+ const query = kustoQueryService.buildResponseTimeQuery(
+ customEndpoint,
+ mockConfig,
+ "alert",
+ );
+
+ expect(query).toContain("let threshold = 2");
+ });
+ it("should NOT filter by threshold in dashboard response time query", () => {
+ const query = kustoQueryService.buildResponseTimeQuery(
+ mockEndpoint,
+ mockConfig,
+ "dashboard",
+ );
+ expect(query).toContain("render timechart");
+ expect(query).not.toContain("where duration_percentile_95 > threshold");
+ });
+ });
+
+ describe("query validation", () => {
+ it("should generate valid Kusto syntax", () => {
+ const availabilityQuery = kustoQueryService.buildAvailabilityQuery(
+ mockEndpoint,
+ mockConfig,
+ "alert",
+ );
+ const responseTimeQuery = kustoQueryService.buildResponseTimeQuery(
+ mockEndpoint,
+ mockConfig,
+ "alert",
+ );
+
+ // Basic syntax checks
+ expect(availabilityQuery).toMatch(/^[A-Za-z]/); // Starts with letter
+ expect(responseTimeQuery).toMatch(/^[A-Za-z]/); // Starts with letter
+ });
+
+ it("should use generic regex pattern for parameterized paths", () => {
+ const endpointWithParam = {
+ ...mockEndpoint,
+ path: "/api/v1/services/{service_id}",
+ };
+ const query = kustoQueryService.buildAvailabilityQuery(
+ endpointWithParam,
+ mockConfig,
+ "alert",
+ );
+
+ expect(query).toContain("requestUri_s matches regex");
+ expect(query).toContain("/api/v1/services/[^/]+$");
+ expect(query).not.toContain("{service_id}");
+ });
+
+ it("should handle multiple parameters in path", () => {
+ const endpointWithMultipleParams = {
+ ...mockEndpoint,
+ path: "/api/v1/users/{user_id}/posts/{post_id}",
+ };
+ const query = kustoQueryService.buildAvailabilityQuery(
+ endpointWithMultipleParams,
+ mockConfig,
+ "alert",
+ );
+
+ expect(query).toContain("/api/v1/users/[^/]+/posts/[^/]+$");
+ expect(query).not.toContain("{user_id}");
+ expect(query).not.toContain("{post_id}");
+ });
+ });
+});
diff --git a/packages/opex-dashboard-ts/test/unit/resolver.test.ts b/packages/opex-dashboard-ts/test/unit/resolver.test.ts
new file mode 100644
index 00000000..afb72bf4
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/unit/resolver.test.ts
@@ -0,0 +1,43 @@
+import {
+ OpenAPISpecResolverAdapter,
+ ParseError,
+} from "../../src/infrastructure/openapi/openapi-spec-resolver-adapter.js";
+import { describe, it, expect, beforeEach } from "vitest";
+
+describe("OA3Resolver", () => {
+ let resolver: OpenAPISpecResolverAdapter;
+
+ beforeEach(() => {
+ resolver = new OpenAPISpecResolverAdapter();
+ });
+
+ describe("ParseError", () => {
+ it("should be a custom error class", () => {
+ const error = new ParseError("Test error");
+ expect(error).toBeInstanceOf(Error);
+ expect(error.name).toBe("ParseError");
+ expect(error.message).toBe("Test error");
+ });
+
+ it("should have proper inheritance", () => {
+ const error = new ParseError("Test error");
+ expect(error instanceof Error).toBe(true);
+ expect(error instanceof ParseError).toBe(true);
+ });
+ });
+
+ describe("OA3Resolver class", () => {
+ it("should be instantiable", () => {
+ expect(resolver).toBeInstanceOf(OpenAPISpecResolverAdapter);
+ });
+
+ it("should have a resolve method", () => {
+ expect(typeof resolver.resolve).toBe("function");
+ });
+
+ it("should have resolve method that returns a Promise", () => {
+ const result = resolver.resolve("./test/fixtures/test_openapi.yaml");
+ expect(result).toBeInstanceOf(Promise);
+ });
+ });
+});
diff --git a/packages/opex-dashboard-ts/test/unit/synth-tags.test.ts b/packages/opex-dashboard-ts/test/unit/synth-tags.test.ts
new file mode 100644
index 00000000..f376f90e
--- /dev/null
+++ b/packages/opex-dashboard-ts/test/unit/synth-tags.test.ts
@@ -0,0 +1,87 @@
+import { describe, it, expect } from "vitest";
+import { App } from "cdktf";
+import { promises as fs } from "fs";
+import * as os from "os";
+import * as path from "path";
+
+import { AzureOpexStack } from "../../src/infrastructure/terraform/azure-dashboard.js";
+import { DashboardConfig, Endpoint } from "../../src/domain/index.js";
+
+describe("synth: tags are applied to dashboard and alerts", () => {
+ it("emits tags in cdk.tf for portal dashboard and alerts", async () => {
+ const tmp = await fs.mkdtemp(path.join(os.tmpdir(), "opex-tags-"));
+
+ const endpoint: Endpoint = {
+ path: "/api/v1/services",
+ availability_threshold: 0.99,
+ response_time_threshold: 1,
+ } as Endpoint;
+
+ const dataSource =
+ "/subscriptions/uuid/resourceGroups/my-rg/providers/Microsoft.Network/applicationGateways/my-gtw";
+
+ const config: DashboardConfig = {
+ oa3_spec: "spec.yaml",
+ name: "Tags Dashboard",
+ data_source: dataSource,
+ resource_type: "app-gateway",
+ timespan: "5m",
+ resource_group_name: "dashboards",
+ resourceIds: [dataSource],
+ hosts: ["app-backend.io.italia.it"],
+ endpoints: [endpoint],
+ tags: { Environment: "TEST", CostCenter: "CC123" },
+ } as unknown as DashboardConfig;
+
+ const app = new App({ outdir: tmp, hclOutput: false });
+ // eslint-disable-next-line no-new
+ new AzureOpexStack(app, "opex-dashboard", config);
+ app.synth();
+
+ // Prefer JSON output from CDKTF; fall back to legacy paths if needed
+ const candidates = [
+ path.join(tmp, "stacks", "opex-dashboard", "cdk.tf.json"),
+ path.join(tmp, "stacks", "opex-dashboard", "cdk.tf"),
+ path.join(tmp, "cdk.tf.json"),
+ path.join(tmp, "cdk.tf"),
+ ];
+
+ let tf: string | undefined;
+ let tfPath: string | undefined;
+ for (const p of candidates) {
+ try {
+ tf = await fs.readFile(p, "utf8");
+ tfPath = p;
+ break;
+ } catch {
+ // try next candidate
+ }
+ }
+
+ if (!tf || !tfPath) {
+ throw new Error(
+ `Could not find synthesized Terraform output. Tried: ${candidates.join(", ")}`,
+ );
+ }
+ const json = JSON.parse(tf);
+
+ // Dashboard has tags
+ const dashboard = json.resource.azurerm_portal_dashboard.dashboard;
+ expect(dashboard).toBeDefined();
+ expect(dashboard.tags).toMatchObject({
+ Environment: "TEST",
+ CostCenter: "CC123",
+ });
+
+ // Alerts have tags
+ const alerts = json.resource.azurerm_monitor_scheduled_query_rules_alert;
+ expect(alerts.alarm_availability_0.tags).toMatchObject({
+ Environment: "TEST",
+ CostCenter: "CC123",
+ });
+ expect(alerts.alarm_time_0.tags).toMatchObject({
+ Environment: "TEST",
+ CostCenter: "CC123",
+ });
+ });
+});
diff --git a/packages/opex-dashboard-ts/tsconfig.json b/packages/opex-dashboard-ts/tsconfig.json
new file mode 100644
index 00000000..bb7596b2
--- /dev/null
+++ b/packages/opex-dashboard-ts/tsconfig.json
@@ -0,0 +1,11 @@
+{
+ "extends": "@tsconfig/node22/tsconfig.json",
+ "compilerOptions": {
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "moduleResolution": "NodeNext",
+ "module": "NodeNext",
+ "target": "ES2022"
+ },
+ "include": ["src"]
+}
diff --git a/packages/opex-dashboard-ts/tsup.config.ts b/packages/opex-dashboard-ts/tsup.config.ts
new file mode 100644
index 00000000..47747936
--- /dev/null
+++ b/packages/opex-dashboard-ts/tsup.config.ts
@@ -0,0 +1,14 @@
+import { defineConfig } from "tsup";
+
+export default defineConfig({
+ clean: true,
+ dts: true,
+ entry: {
+ index: "src/index.ts",
+ cli: "src/infrastructure/cli/index.ts",
+ },
+ format: ["esm", "cjs"],
+ outDir: "dist",
+ target: "esnext",
+ tsconfig: "./tsconfig.json",
+});
diff --git a/turbo.json b/turbo.json
index d2a57c9d..d7084d6a 100644
--- a/turbo.json
+++ b/turbo.json
@@ -18,6 +18,7 @@
"dependsOn": [
"^clean",
"^generate",
+ "generate",
"^build"
]
},
@@ -70,7 +71,7 @@
"infra:generate": {
"dependsOn": [
"^build"
- ],
+ ],
"inputs": [
"src/**/opex.ts",
"**/openapi*.yaml",
diff --git a/vitest.workspace.ts b/vitest.workspace.ts
index 66b7b777..4c5796ee 100644
--- a/vitest.workspace.ts
+++ b/vitest.workspace.ts
@@ -1,3 +1,4 @@
export default [
- "apps/*"
+ "apps/*",
+ "packages/*"
]
diff --git a/yarn.lock b/yarn.lock
index f8de8716..13f7ba1b 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -82,7 +82,7 @@ __metadata:
languageName: node
linkType: hard
-"@apidevtools/swagger-parser@npm:^10.0.1":
+"@apidevtools/swagger-parser@npm:^10.0.1, @apidevtools/swagger-parser@npm:^10.1.0":
version: 10.1.1
resolution: "@apidevtools/swagger-parser@npm:10.1.1"
dependencies:
@@ -551,13 +551,13 @@ __metadata:
languageName: node
linkType: hard
-"@cdktf/provider-azurerm@npm:^14.2.0":
- version: 14.2.0
- resolution: "@cdktf/provider-azurerm@npm:14.2.0"
+"@cdktf/provider-azurerm@npm:^14.12.0":
+ version: 14.12.0
+ resolution: "@cdktf/provider-azurerm@npm:14.12.0"
peerDependencies:
cdktf: ^0.21.0
constructs: ^10.4.2
- checksum: 10c0/17ec170de0c2b4e3b23110361cdcb32a6ad2602290c75e5eed94a004e6961acad892c1a70ef6f82876e41b5db4d1964c67a8e11e51a084bc366c0b5d83cfd753
+ checksum: 10c0/c1fe62fa6bc846b6e07bd5a5ed5f540b11ac276b2d0da59b36176bda8cc4c777074a4e9ab49df17992ab859e12bd653970efd9618c55135253859b40b2117601
languageName: node
linkType: hard
@@ -1337,7 +1337,18 @@ __metadata:
languageName: node
linkType: hard
-"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.6.1":
+"@eslint-community/eslint-utils@npm:^4.4.0":
+ version: 4.9.0
+ resolution: "@eslint-community/eslint-utils@npm:4.9.0"
+ dependencies:
+ eslint-visitor-keys: "npm:^3.4.3"
+ peerDependencies:
+ eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+ checksum: 10c0/8881e22d519326e7dba85ea915ac7a143367c805e6ba1374c987aa2fbdd09195cc51183d2da72c0e2ff388f84363e1b220fd0d19bef10c272c63455162176817
+ languageName: node
+ linkType: hard
+
+"@eslint-community/regexpp@npm:^4.10.0, @eslint-community/regexpp@npm:^4.12.1, @eslint-community/regexpp@npm:^4.5.1, @eslint-community/regexpp@npm:^4.6.1":
version: 4.12.1
resolution: "@eslint-community/regexpp@npm:4.12.1"
checksum: 10c0/a03d98c246bcb9109aec2c08e4d10c8d010256538dcb3f56610191607214523d4fb1b00aa81df830b6dffb74c5fa0be03642513a289c567949d3e550ca11cdf6
@@ -1723,24 +1734,6 @@ __metadata:
languageName: node
linkType: hard
-"@infra/github-runner@workspace:infra/github-runner":
- version: 0.0.0-use.local
- resolution: "@infra/github-runner@workspace:infra/github-runner"
- languageName: unknown
- linkType: soft
-
-"@infra/identity@workspace:infra/identity":
- version: 0.0.0-use.local
- resolution: "@infra/identity@workspace:infra/identity"
- languageName: unknown
- linkType: soft
-
-"@infra/repository@workspace:infra/repository":
- version: 0.0.0-use.local
- resolution: "@infra/repository@workspace:infra/repository"
- languageName: unknown
- linkType: soft
-
"@infra/resources@workspace:infra/resources":
version: 0.0.0-use.local
resolution: "@infra/resources@workspace:infra/resources"
@@ -3086,6 +3079,34 @@ __metadata:
languageName: node
linkType: hard
+"@pagopa/opex-dashboard-ts@workspace:^, @pagopa/opex-dashboard-ts@workspace:packages/opex-dashboard-ts":
+ version: 0.0.0-use.local
+ resolution: "@pagopa/opex-dashboard-ts@workspace:packages/opex-dashboard-ts"
+ dependencies:
+ "@apidevtools/swagger-parser": "npm:^10.1.0"
+ "@cdktf/provider-azurerm": "npm:^14.12.0"
+ "@pagopa/eslint-config": "npm:^5.0.0"
+ "@tsconfig/node22": "npm:^22.0.2"
+ "@types/js-yaml": "npm:^4.0.5"
+ "@types/node": "npm:^20.0.0"
+ "@typescript-eslint/eslint-plugin": "npm:^6.0.0"
+ "@typescript-eslint/parser": "npm:^6.0.0"
+ cdktf: "npm:^0.21.0"
+ commander: "npm:^12.0.0"
+ constructs: "npm:^10.3.0"
+ eslint: "npm:^8.0.0"
+ js-yaml: "npm:^4.1.0"
+ openapi-types: "npm:^12.1.3"
+ prettier: "npm:^3.6.2"
+ tsup: "npm:^8.5.0"
+ typescript: "npm:^5.0.0"
+ vitest: "npm:^3.2.4"
+ zod: "npm:^4.1.5"
+ bin:
+ opex-dashboard-ts: dist/infrastructure/cli/index.js
+ languageName: unknown
+ linkType: soft
+
"@pagopa/ts-commons@npm:^10.15.0":
version: 10.15.0
resolution: "@pagopa/ts-commons@npm:10.15.0"
@@ -3238,6 +3259,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-android-arm-eabi@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-android-arm-eabi@npm:4.50.1"
+ conditions: os=android & cpu=arm
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-android-arm64@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-android-arm64@npm:4.40.2"
@@ -3245,6 +3273,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-android-arm64@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-android-arm64@npm:4.50.1"
+ conditions: os=android & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-darwin-arm64@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-darwin-arm64@npm:4.40.2"
@@ -3252,6 +3287,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-darwin-arm64@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-darwin-arm64@npm:4.50.1"
+ conditions: os=darwin & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-darwin-x64@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-darwin-x64@npm:4.40.2"
@@ -3259,6 +3301,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-darwin-x64@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-darwin-x64@npm:4.50.1"
+ conditions: os=darwin & cpu=x64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-freebsd-arm64@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-freebsd-arm64@npm:4.40.2"
@@ -3266,6 +3315,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-freebsd-arm64@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-freebsd-arm64@npm:4.50.1"
+ conditions: os=freebsd & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-freebsd-x64@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-freebsd-x64@npm:4.40.2"
@@ -3273,6 +3329,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-freebsd-x64@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-freebsd-x64@npm:4.50.1"
+ conditions: os=freebsd & cpu=x64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm-gnueabihf@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.40.2"
@@ -3280,6 +3343,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.50.1"
+ conditions: os=linux & cpu=arm & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm-musleabihf@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.40.2"
@@ -3287,6 +3357,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm-musleabihf@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.50.1"
+ conditions: os=linux & cpu=arm & libc=musl
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm64-gnu@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.40.2"
@@ -3294,6 +3371,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm64-gnu@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.50.1"
+ conditions: os=linux & cpu=arm64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-arm64-musl@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-arm64-musl@npm:4.40.2"
@@ -3301,6 +3385,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-arm64-musl@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-arm64-musl@npm:4.50.1"
+ conditions: os=linux & cpu=arm64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-loongarch64-gnu@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.40.2"
@@ -3308,6 +3399,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.50.1"
+ conditions: os=linux & cpu=loong64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.40.2"
@@ -3315,6 +3413,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-ppc64-gnu@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-ppc64-gnu@npm:4.50.1"
+ conditions: os=linux & cpu=ppc64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-riscv64-gnu@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.40.2"
@@ -3322,6 +3427,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-riscv64-gnu@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.50.1"
+ conditions: os=linux & cpu=riscv64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-riscv64-musl@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.40.2"
@@ -3329,6 +3441,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-riscv64-musl@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.50.1"
+ conditions: os=linux & cpu=riscv64 & libc=musl
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-s390x-gnu@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.40.2"
@@ -3336,6 +3455,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-s390x-gnu@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.50.1"
+ conditions: os=linux & cpu=s390x & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-x64-gnu@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-x64-gnu@npm:4.40.2"
@@ -3343,6 +3469,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-x64-gnu@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-x64-gnu@npm:4.50.1"
+ conditions: os=linux & cpu=x64 & libc=glibc
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-linux-x64-musl@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-linux-x64-musl@npm:4.40.2"
@@ -3350,6 +3483,20 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-linux-x64-musl@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-linux-x64-musl@npm:4.50.1"
+ conditions: os=linux & cpu=x64 & libc=musl
+ languageName: node
+ linkType: hard
+
+"@rollup/rollup-openharmony-arm64@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-openharmony-arm64@npm:4.50.1"
+ conditions: os=openharmony & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-win32-arm64-msvc@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.40.2"
@@ -3357,6 +3504,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-win32-arm64-msvc@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.50.1"
+ conditions: os=win32 & cpu=arm64
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-win32-ia32-msvc@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.40.2"
@@ -3364,6 +3518,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-win32-ia32-msvc@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.50.1"
+ conditions: os=win32 & cpu=ia32
+ languageName: node
+ linkType: hard
+
"@rollup/rollup-win32-x64-msvc@npm:4.40.2":
version: 4.40.2
resolution: "@rollup/rollup-win32-x64-msvc@npm:4.40.2"
@@ -3371,6 +3532,13 @@ __metadata:
languageName: node
linkType: hard
+"@rollup/rollup-win32-x64-msvc@npm:4.50.1":
+ version: 4.50.1
+ resolution: "@rollup/rollup-win32-x64-msvc@npm:4.50.1"
+ conditions: os=win32 & cpu=x64
+ languageName: node
+ linkType: hard
+
"@rtsao/scc@npm:^1.1.0":
version: 1.1.0
resolution: "@rtsao/scc@npm:1.1.0"
@@ -3475,6 +3643,13 @@ __metadata:
languageName: node
linkType: hard
+"@tsconfig/node22@npm:^22.0.2":
+ version: 22.0.2
+ resolution: "@tsconfig/node22@npm:22.0.2"
+ checksum: 10c0/c75e6b9ea86ec2a384adefac0e2f16a6c08202ae5cf6c8c745944396a6931ffb38e742809491c1882d1868c2af1c33744193701779674bfb1e05f6a130045a18
+ languageName: node
+ linkType: hard
+
"@tybys/wasm-util@npm:^0.9.0":
version: 0.9.0
resolution: "@tybys/wasm-util@npm:0.9.0"
@@ -3503,6 +3678,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/chai@npm:^5.2.2":
+ version: 5.2.2
+ resolution: "@types/chai@npm:5.2.2"
+ dependencies:
+ "@types/deep-eql": "npm:*"
+ checksum: 10c0/49282bf0e8246800ebb36f17256f97bd3a8c4fb31f92ad3c0eaa7623518d7e87f1eaad4ad206960fcaf7175854bdff4cb167e4fe96811e0081b4ada83dd533ec
+ languageName: node
+ linkType: hard
+
"@types/connect@npm:*":
version: 3.4.38
resolution: "@types/connect@npm:3.4.38"
@@ -3512,6 +3696,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/deep-eql@npm:*":
+ version: 4.0.2
+ resolution: "@types/deep-eql@npm:4.0.2"
+ checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844
+ languageName: node
+ linkType: hard
+
"@types/estree@npm:1.0.7, @types/estree@npm:^1.0.0":
version: 1.0.7
resolution: "@types/estree@npm:1.0.7"
@@ -3519,7 +3710,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/estree@npm:^1.0.6":
+"@types/estree@npm:1.0.8, @types/estree@npm:^1.0.6":
version: 1.0.8
resolution: "@types/estree@npm:1.0.8"
checksum: 10c0/39d34d1afaa338ab9763f37ad6066e3f349444f9052b9676a7cc0252ef9485a41c6d81c9c4e0d26e9077993354edf25efc853f3224dd4b447175ef62bdcc86a5
@@ -3564,7 +3755,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/js-yaml@npm:^4.0.9":
+"@types/js-yaml@npm:^4.0.5":
version: 4.0.9
resolution: "@types/js-yaml@npm:4.0.9"
checksum: 10c0/24de857aa8d61526bbfbbaa383aa538283ad17363fcd5bb5148e2c7f604547db36646440e739d78241ed008702a8920665d1add5618687b6743858fae00da211
@@ -3578,7 +3769,7 @@ __metadata:
languageName: node
linkType: hard
-"@types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.6":
+"@types/json-schema@npm:^7.0.12, @types/json-schema@npm:^7.0.15, @types/json-schema@npm:^7.0.6":
version: 7.0.15
resolution: "@types/json-schema@npm:7.0.15"
checksum: 10c0/a996a745e6c5d60292f36731dd41341339d4eeed8180bb09226e5c8d23759067692b1d88e5d91d72ee83dfc00d3aca8e7bd43ea120516c17922cbcb7c3e252db
@@ -3624,6 +3815,15 @@ __metadata:
languageName: node
linkType: hard
+"@types/node@npm:^20.0.0":
+ version: 20.19.13
+ resolution: "@types/node@npm:20.19.13"
+ dependencies:
+ undici-types: "npm:~6.21.0"
+ checksum: 10c0/25fba13d50c4f18ac56a50d4491502e7d095f09e44c17c0c6887aa5e4e6122e961e92fc3b682ed8c053ab87a05ce10f501345d23d96fdfba8002ecce6c8ced51
+ languageName: node
+ linkType: hard
+
"@types/node@npm:^20.17.43":
version: 20.17.47
resolution: "@types/node@npm:20.17.47"
@@ -3728,6 +3928,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/semver@npm:^7.5.0":
+ version: 7.7.1
+ resolution: "@types/semver@npm:7.7.1"
+ checksum: 10c0/c938aef3bf79a73f0f3f6037c16e2e759ff40c54122ddf0b2583703393d8d3127130823facb880e694caa324eb6845628186aac1997ee8b31dc2d18fafe26268
+ languageName: node
+ linkType: hard
+
"@types/send@npm:*":
version: 0.17.4
resolution: "@types/send@npm:0.17.4"
@@ -3814,6 +4021,31 @@ __metadata:
languageName: node
linkType: hard
+"@typescript-eslint/eslint-plugin@npm:^6.0.0":
+ version: 6.21.0
+ resolution: "@typescript-eslint/eslint-plugin@npm:6.21.0"
+ dependencies:
+ "@eslint-community/regexpp": "npm:^4.5.1"
+ "@typescript-eslint/scope-manager": "npm:6.21.0"
+ "@typescript-eslint/type-utils": "npm:6.21.0"
+ "@typescript-eslint/utils": "npm:6.21.0"
+ "@typescript-eslint/visitor-keys": "npm:6.21.0"
+ debug: "npm:^4.3.4"
+ graphemer: "npm:^1.4.0"
+ ignore: "npm:^5.2.4"
+ natural-compare: "npm:^1.4.0"
+ semver: "npm:^7.5.4"
+ ts-api-utils: "npm:^1.0.1"
+ peerDependencies:
+ "@typescript-eslint/parser": ^6.0.0 || ^6.0.0-alpha
+ eslint: ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ checksum: 10c0/f911a79ee64d642f814a3b6cdb0d324b5f45d9ef955c5033e78903f626b7239b4aa773e464a38c3e667519066169d983538f2bf8e5d00228af587c9d438fb344
+ languageName: node
+ linkType: hard
+
"@typescript-eslint/parser@npm:8.35.1":
version: 8.35.1
resolution: "@typescript-eslint/parser@npm:8.35.1"
@@ -3863,7 +4095,7 @@ __metadata:
languageName: node
linkType: hard
-"@typescript-eslint/parser@npm:^6.21.0":
+"@typescript-eslint/parser@npm:^6.0.0, @typescript-eslint/parser@npm:^6.21.0":
version: 6.21.0
resolution: "@typescript-eslint/parser@npm:6.21.0"
dependencies:
@@ -3943,6 +4175,23 @@ __metadata:
languageName: node
linkType: hard
+"@typescript-eslint/type-utils@npm:6.21.0":
+ version: 6.21.0
+ resolution: "@typescript-eslint/type-utils@npm:6.21.0"
+ dependencies:
+ "@typescript-eslint/typescript-estree": "npm:6.21.0"
+ "@typescript-eslint/utils": "npm:6.21.0"
+ debug: "npm:^4.3.4"
+ ts-api-utils: "npm:^1.0.1"
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ peerDependenciesMeta:
+ typescript:
+ optional: true
+ checksum: 10c0/7409c97d1c4a4386b488962739c4f1b5b04dc60cf51f8cd88e6b12541f84d84c6b8b67e491a147a2c95f9ec486539bf4519fb9d418411aef6537b9c156468117
+ languageName: node
+ linkType: hard
+
"@typescript-eslint/type-utils@npm:8.32.1":
version: 8.32.1
resolution: "@typescript-eslint/type-utils@npm:8.32.1"
@@ -4076,6 +4325,23 @@ __metadata:
languageName: node
linkType: hard
+"@typescript-eslint/utils@npm:6.21.0":
+ version: 6.21.0
+ resolution: "@typescript-eslint/utils@npm:6.21.0"
+ dependencies:
+ "@eslint-community/eslint-utils": "npm:^4.4.0"
+ "@types/json-schema": "npm:^7.0.12"
+ "@types/semver": "npm:^7.5.0"
+ "@typescript-eslint/scope-manager": "npm:6.21.0"
+ "@typescript-eslint/types": "npm:6.21.0"
+ "@typescript-eslint/typescript-estree": "npm:6.21.0"
+ semver: "npm:^7.5.4"
+ peerDependencies:
+ eslint: ^7.0.0 || ^8.0.0
+ checksum: 10c0/ab2df3833b2582d4e5467a484d08942b4f2f7208f8e09d67de510008eb8001a9b7460f2f9ba11c12086fd3cdcac0c626761c7995c2c6b5657d5fa6b82030a32d
+ languageName: node
+ linkType: hard
+
"@typescript-eslint/utils@npm:8.32.1":
version: 8.32.1
resolution: "@typescript-eslint/utils@npm:8.32.1"
@@ -4341,6 +4607,19 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/expect@npm:3.2.4":
+ version: 3.2.4
+ resolution: "@vitest/expect@npm:3.2.4"
+ dependencies:
+ "@types/chai": "npm:^5.2.2"
+ "@vitest/spy": "npm:3.2.4"
+ "@vitest/utils": "npm:3.2.4"
+ chai: "npm:^5.2.0"
+ tinyrainbow: "npm:^2.0.0"
+ checksum: 10c0/7586104e3fd31dbe1e6ecaafb9a70131e4197dce2940f727b6a84131eee3decac7b10f9c7c72fa5edbdb68b6f854353bd4c0fa84779e274207fb7379563b10db
+ languageName: node
+ linkType: hard
+
"@vitest/mocker@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/mocker@npm:3.1.4"
@@ -4360,6 +4639,25 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/mocker@npm:3.2.4":
+ version: 3.2.4
+ resolution: "@vitest/mocker@npm:3.2.4"
+ dependencies:
+ "@vitest/spy": "npm:3.2.4"
+ estree-walker: "npm:^3.0.3"
+ magic-string: "npm:^0.30.17"
+ peerDependencies:
+ msw: ^2.4.9
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0
+ peerDependenciesMeta:
+ msw:
+ optional: true
+ vite:
+ optional: true
+ checksum: 10c0/f7a4aea19bbbf8f15905847ee9143b6298b2c110f8b64789224cb0ffdc2e96f9802876aa2ca83f1ec1b6e1ff45e822abb34f0054c24d57b29ab18add06536ccd
+ languageName: node
+ linkType: hard
+
"@vitest/pretty-format@npm:3.1.4, @vitest/pretty-format@npm:^3.1.4":
version: 3.1.4
resolution: "@vitest/pretty-format@npm:3.1.4"
@@ -4369,6 +4667,15 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/pretty-format@npm:3.2.4, @vitest/pretty-format@npm:^3.2.4":
+ version: 3.2.4
+ resolution: "@vitest/pretty-format@npm:3.2.4"
+ dependencies:
+ tinyrainbow: "npm:^2.0.0"
+ checksum: 10c0/5ad7d4278e067390d7d633e307fee8103958806a419ca380aec0e33fae71b44a64415f7a9b4bc11635d3c13d4a9186111c581d3cef9c65cc317e68f077456887
+ languageName: node
+ linkType: hard
+
"@vitest/runner@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/runner@npm:3.1.4"
@@ -4379,6 +4686,17 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/runner@npm:3.2.4":
+ version: 3.2.4
+ resolution: "@vitest/runner@npm:3.2.4"
+ dependencies:
+ "@vitest/utils": "npm:3.2.4"
+ pathe: "npm:^2.0.3"
+ strip-literal: "npm:^3.0.0"
+ checksum: 10c0/e8be51666c72b3668ae3ea348b0196656a4a5adb836cb5e270720885d9517421815b0d6c98bfdf1795ed02b994b7bfb2b21566ee356a40021f5bf4f6ed4e418a
+ languageName: node
+ linkType: hard
+
"@vitest/snapshot@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/snapshot@npm:3.1.4"
@@ -4390,6 +4708,17 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/snapshot@npm:3.2.4":
+ version: 3.2.4
+ resolution: "@vitest/snapshot@npm:3.2.4"
+ dependencies:
+ "@vitest/pretty-format": "npm:3.2.4"
+ magic-string: "npm:^0.30.17"
+ pathe: "npm:^2.0.3"
+ checksum: 10c0/f8301a3d7d1559fd3d59ed51176dd52e1ed5c2d23aa6d8d6aa18787ef46e295056bc726a021698d8454c16ed825ecba163362f42fa90258bb4a98cfd2c9424fc
+ languageName: node
+ linkType: hard
+
"@vitest/spy@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/spy@npm:3.1.4"
@@ -4399,6 +4728,15 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/spy@npm:3.2.4":
+ version: 3.2.4
+ resolution: "@vitest/spy@npm:3.2.4"
+ dependencies:
+ tinyspy: "npm:^4.0.3"
+ checksum: 10c0/6ebf0b4697dc238476d6b6a60c76ba9eb1dd8167a307e30f08f64149612fd50227682b876420e4c2e09a76334e73f72e3ebf0e350714dc22474258292e202024
+ languageName: node
+ linkType: hard
+
"@vitest/utils@npm:3.1.4":
version: 3.1.4
resolution: "@vitest/utils@npm:3.1.4"
@@ -4410,6 +4748,17 @@ __metadata:
languageName: node
linkType: hard
+"@vitest/utils@npm:3.2.4":
+ version: 3.2.4
+ resolution: "@vitest/utils@npm:3.2.4"
+ dependencies:
+ "@vitest/pretty-format": "npm:3.2.4"
+ loupe: "npm:^3.1.4"
+ tinyrainbow: "npm:^2.0.0"
+ checksum: 10c0/024a9b8c8bcc12cf40183c246c244b52ecff861c6deb3477cbf487ac8781ad44c68a9c5fd69f8c1361878e55b97c10d99d511f2597f1f7244b5e5101d028ba64
+ languageName: node
+ linkType: hard
+
"a-sync-waterfall@npm:^1.0.0":
version: 1.0.1
resolution: "a-sync-waterfall@npm:1.0.1"
@@ -5126,6 +5475,17 @@ __metadata:
languageName: node
linkType: hard
+"bundle-require@npm:^5.1.0":
+ version: 5.1.0
+ resolution: "bundle-require@npm:5.1.0"
+ dependencies:
+ load-tsconfig: "npm:^0.2.3"
+ peerDependencies:
+ esbuild: ">=0.18"
+ checksum: 10c0/8bff9df68eb686f05af952003c78e70ffed2817968f92aebb2af620cc0b7428c8154df761d28f1b38508532204278950624ef86ce63644013dc57660a9d1810f
+ languageName: node
+ linkType: hard
+
"busboy@npm:1.6.0":
version: 1.6.0
resolution: "busboy@npm:1.6.0"
@@ -5229,23 +5589,6 @@ __metadata:
languageName: node
linkType: hard
-"cdktf-monitoring-stack@workspace:*, cdktf-monitoring-stack@workspace:packages/cdktf-monitoring-stack":
- version: 0.0.0-use.local
- resolution: "cdktf-monitoring-stack@workspace:packages/cdktf-monitoring-stack"
- dependencies:
- "@cdktf/provider-azurerm": "npm:^14.2.0"
- "@pagopa/eslint-config": "npm:^5.0.0"
- "@pagopa/typescript-config-node": "workspace:^"
- "@types/js-yaml": "npm:^4.0.9"
- cdktf: "npm:^0.21.0"
- constructs: "npm:^10.4.2"
- eslint: "npm:^9.30.1"
- js-yaml: "npm:^4.1.0"
- prettier: "npm:^3.6.2"
- typescript: "npm:^5.8.3"
- languageName: unknown
- linkType: soft
-
"cdktf@npm:^0.21.0":
version: 0.21.0
resolution: "cdktf@npm:0.21.0"
@@ -5338,6 +5681,15 @@ __metadata:
languageName: node
linkType: hard
+"chokidar@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "chokidar@npm:4.0.3"
+ dependencies:
+ readdirp: "npm:^4.0.1"
+ checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad
+ languageName: node
+ linkType: hard
+
"chownr@npm:^3.0.0":
version: 3.0.0
resolution: "chownr@npm:3.0.0"
@@ -5472,6 +5824,13 @@ __metadata:
languageName: node
linkType: hard
+"commander@npm:^12.0.0":
+ version: 12.1.0
+ resolution: "commander@npm:12.1.0"
+ checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9
+ languageName: node
+ linkType: hard
+
"commander@npm:^4.0.0":
version: 4.1.1
resolution: "commander@npm:4.1.1"
@@ -5520,14 +5879,28 @@ __metadata:
languageName: node
linkType: hard
-"constructs@npm:^10.4.2":
- version: 10.4.2
- resolution: "constructs@npm:10.4.2"
- checksum: 10c0/dcd5edd631c7313964f89fffb7365e1eebaede16cbc9ae69eab5337710353913684b860ccc4b2a3dfaf147656f48f0ae7853ca94cb51833e152b46047ac7a4ff
+"confbox@npm:^0.1.8":
+ version: 0.1.8
+ resolution: "confbox@npm:0.1.8"
+ checksum: 10c0/fc2c68d97cb54d885b10b63e45bd8da83a8a71459d3ecf1825143dd4c7f9f1b696b3283e07d9d12a144c1301c2ebc7842380bdf0014e55acc4ae1c9550102418
languageName: node
linkType: hard
-"content-disposition@npm:0.5.4":
+"consola@npm:^3.4.0":
+ version: 3.4.2
+ resolution: "consola@npm:3.4.2"
+ checksum: 10c0/7cebe57ecf646ba74b300bcce23bff43034ed6fbec9f7e39c27cee1dc00df8a21cd336b466ad32e304ea70fba04ec9e890c200270de9a526ce021ba8a7e4c11a
+ languageName: node
+ linkType: hard
+
+"constructs@npm:^10.3.0, constructs@npm:^10.4.2":
+ version: 10.4.2
+ resolution: "constructs@npm:10.4.2"
+ checksum: 10c0/dcd5edd631c7313964f89fffb7365e1eebaede16cbc9ae69eab5337710353913684b860ccc4b2a3dfaf147656f48f0ae7853ca94cb51833e152b46047ac7a4ff
+ languageName: node
+ linkType: hard
+
+"content-disposition@npm:0.5.4":
version: 0.5.4
resolution: "content-disposition@npm:0.5.4"
dependencies:
@@ -5707,7 +6080,7 @@ __metadata:
languageName: node
linkType: hard
-"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0":
+"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.4.0, debug@npm:^4.4.1":
version: 4.4.1
resolution: "debug@npm:4.4.1"
dependencies:
@@ -6708,7 +7081,7 @@ __metadata:
languageName: node
linkType: hard
-"eslint@npm:^8.57.1":
+"eslint@npm:^8.0.0, eslint@npm:^8.57.1":
version: 8.57.1
resolution: "eslint@npm:8.57.1"
dependencies:
@@ -7079,6 +7452,18 @@ __metadata:
languageName: node
linkType: hard
+"fdir@npm:^6.5.0":
+ version: 6.5.0
+ resolution: "fdir@npm:6.5.0"
+ peerDependencies:
+ picomatch: ^3 || ^4
+ peerDependenciesMeta:
+ picomatch:
+ optional: true
+ checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f
+ languageName: node
+ linkType: hard
+
"fecha@npm:^4.2.0":
version: 4.2.3
resolution: "fecha@npm:4.2.3"
@@ -7155,6 +7540,17 @@ __metadata:
languageName: node
linkType: hard
+"fix-dts-default-cjs-exports@npm:^1.0.0":
+ version: 1.0.1
+ resolution: "fix-dts-default-cjs-exports@npm:1.0.1"
+ dependencies:
+ magic-string: "npm:^0.30.17"
+ mlly: "npm:^1.7.4"
+ rollup: "npm:^4.34.8"
+ checksum: 10c0/61a3cbe32b6c29df495ef3aded78199fe9dbb52e2801c899fe76d9ca413d3c8c51f79986bac83f8b4b2094ebde883ddcfe47b68ce469806ba13ca6ed4e7cd362
+ languageName: node
+ linkType: hard
+
"flat-cache@npm:^3.0.4":
version: 3.2.0
resolution: "flat-cache@npm:3.2.0"
@@ -7745,7 +8141,7 @@ __metadata:
languageName: node
linkType: hard
-"ignore@npm:^5.2.0":
+"ignore@npm:^5.2.0, ignore@npm:^5.2.4":
version: 5.3.2
resolution: "ignore@npm:5.3.2"
checksum: 10c0/f9f652c957983634ded1e7f02da3b559a0d4cc210fca3792cb67f1b153623c9c42efdc1c4121af171e295444459fc4a9201101fb041b1104a3c000bccb188337
@@ -8301,7 +8697,7 @@ __metadata:
languageName: node
linkType: hard
-"joycon@npm:^3.0.1":
+"joycon@npm:^3.0.1, joycon@npm:^3.1.1":
version: 3.1.1
resolution: "joycon@npm:3.1.1"
checksum: 10c0/131fb1e98c9065d067fd49b6e685487ac4ad4d254191d7aa2c9e3b90f4e9ca70430c43cad001602bdbdabcf58717d3b5c5b7461c1bd8e39478c8de706b3fe6ae
@@ -8315,6 +8711,13 @@ __metadata:
languageName: node
linkType: hard
+"js-tokens@npm:^9.0.1":
+ version: 9.0.1
+ resolution: "js-tokens@npm:9.0.1"
+ checksum: 10c0/68dcab8f233dde211a6b5fd98079783cbcd04b53617c1250e3553ee16ab3e6134f5e65478e41d82f6d351a052a63d71024553933808570f04dbf828d7921e80e
+ languageName: node
+ linkType: hard
+
"js-yaml@npm:^3.13.1, js-yaml@npm:^3.14.0, js-yaml@npm:^3.6.1":
version: 3.14.1
resolution: "js-yaml@npm:3.14.1"
@@ -8561,6 +8964,13 @@ __metadata:
languageName: node
linkType: hard
+"lilconfig@npm:^3.1.1":
+ version: 3.1.3
+ resolution: "lilconfig@npm:3.1.3"
+ checksum: 10c0/f5604e7240c5c275743561442fbc5abf2a84ad94da0f5adc71d25e31fa8483048de3dcedcb7a44112a942fed305fd75841cdf6c9681c7f640c63f1049e9a5dcc
+ languageName: node
+ linkType: hard
+
"lines-and-columns@npm:^1.1.6":
version: 1.2.4
resolution: "lines-and-columns@npm:1.2.4"
@@ -8754,6 +9164,13 @@ __metadata:
languageName: node
linkType: hard
+"loupe@npm:^3.1.4":
+ version: 3.2.1
+ resolution: "loupe@npm:3.2.1"
+ checksum: 10c0/910c872cba291309664c2d094368d31a68907b6f5913e989d301b5c25f30e97d76d77f23ab3bf3b46d0f601ff0b6af8810c10c31b91d2c6b2f132809ca2cc705
+ languageName: node
+ linkType: hard
+
"lru-cache@npm:^10.0.1, lru-cache@npm:^10.2.0":
version: 10.4.3
resolution: "lru-cache@npm:10.4.3"
@@ -9037,6 +9454,18 @@ __metadata:
languageName: node
linkType: hard
+"mlly@npm:^1.7.4":
+ version: 1.8.0
+ resolution: "mlly@npm:1.8.0"
+ dependencies:
+ acorn: "npm:^8.15.0"
+ pathe: "npm:^2.0.3"
+ pkg-types: "npm:^1.3.1"
+ ufo: "npm:^1.6.1"
+ checksum: 10c0/f174b844ae066c71e9b128046677868e2e28694f0bbeeffbe760b2a9d8ff24de0748d0fde6fabe706700c1d2e11d3c0d7a53071b5ea99671592fac03364604ab
+ languageName: node
+ linkType: hard
+
"module-details-from-path@npm:^1.0.3":
version: 1.0.4
resolution: "module-details-from-path@npm:1.0.4"
@@ -9090,7 +9519,7 @@ __metadata:
languageName: node
linkType: hard
-"nanoid@npm:^3.3.6, nanoid@npm:^3.3.8":
+"nanoid@npm:^3.3.11, nanoid@npm:^3.3.6, nanoid@npm:^3.3.8":
version: 3.3.11
resolution: "nanoid@npm:3.3.11"
bin:
@@ -9427,19 +9856,12 @@ __metadata:
languageName: node
linkType: hard
-"opex-common@workspace:*, opex-common@workspace:packages/opex-common":
- version: 0.0.0-use.local
- resolution: "opex-common@workspace:packages/opex-common"
- dependencies:
- "@pagopa/eslint-config": "npm:^5.0.0"
- "@pagopa/typescript-config-node": "workspace:^"
- cdktf: "npm:^0.21.0"
- cdktf-monitoring-stack: "workspace:*"
- eslint: "npm:^9.30.1"
- prettier: "npm:^3.6.2"
- typescript: "npm:^5.8.3"
- languageName: unknown
- linkType: soft
+"openapi-types@npm:^12.1.3":
+ version: 12.1.3
+ resolution: "openapi-types@npm:12.1.3"
+ checksum: 10c0/4ad4eb91ea834c237edfa6ab31394e87e00c888fc2918009763389c00d02342345195d6f302d61c3fd807f17723cd48df29b47b538b68375b3827b3758cd520f
+ languageName: node
+ linkType: hard
"optionator@npm:^0.9.3":
version: 0.9.4
@@ -9670,7 +10092,7 @@ __metadata:
languageName: node
linkType: hard
-"pathe@npm:^2.0.3":
+"pathe@npm:^2.0.1, pathe@npm:^2.0.3":
version: 2.0.3
resolution: "pathe@npm:2.0.3"
checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1
@@ -9761,6 +10183,13 @@ __metadata:
languageName: node
linkType: hard
+"picomatch@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "picomatch@npm:4.0.3"
+ checksum: 10c0/9582c951e95eebee5434f59e426cddd228a7b97a0161a375aed4be244bd3fe8e3a31b846808ea14ef2c8a2527a6eeab7b3946a67d5979e81694654f939473ae2
+ languageName: node
+ linkType: hard
+
"pify@npm:^4.0.1":
version: 4.0.1
resolution: "pify@npm:4.0.1"
@@ -9775,6 +10204,17 @@ __metadata:
languageName: node
linkType: hard
+"pkg-types@npm:^1.3.1":
+ version: 1.3.1
+ resolution: "pkg-types@npm:1.3.1"
+ dependencies:
+ confbox: "npm:^0.1.8"
+ mlly: "npm:^1.7.4"
+ pathe: "npm:^2.0.1"
+ checksum: 10c0/19e6cb8b66dcc66c89f2344aecfa47f2431c988cfa3366bdfdcfb1dd6695f87dcce37fbd90fe9d1605e2f4440b77f391e83c23255347c35cf84e7fd774d7fcea
+ languageName: node
+ linkType: hard
+
"possible-typed-array-names@npm:^1.0.0":
version: 1.1.0
resolution: "possible-typed-array-names@npm:1.1.0"
@@ -9800,6 +10240,29 @@ __metadata:
languageName: node
linkType: hard
+"postcss-load-config@npm:^6.0.1":
+ version: 6.0.1
+ resolution: "postcss-load-config@npm:6.0.1"
+ dependencies:
+ lilconfig: "npm:^3.1.1"
+ peerDependencies:
+ jiti: ">=1.21.0"
+ postcss: ">=8.0.9"
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ peerDependenciesMeta:
+ jiti:
+ optional: true
+ postcss:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+ checksum: 10c0/74173a58816dac84e44853f7afbd283f4ef13ca0b6baeba27701214beec33f9e309b128f8102e2b173e8d45ecba45d279a9be94b46bf48d219626aa9b5730848
+ languageName: node
+ linkType: hard
+
"postcss@npm:8.4.31":
version: 8.4.31
resolution: "postcss@npm:8.4.31"
@@ -9822,6 +10285,17 @@ __metadata:
languageName: node
linkType: hard
+"postcss@npm:^8.5.6":
+ version: 8.5.6
+ resolution: "postcss@npm:8.5.6"
+ dependencies:
+ nanoid: "npm:^3.3.11"
+ picocolors: "npm:^1.1.1"
+ source-map-js: "npm:^1.2.1"
+ checksum: 10c0/5127cc7c91ed7a133a1b7318012d8bfa112da9ef092dddf369ae699a1f10ebbd89b1b9f25f3228795b84585c72aabd5ced5fc11f2ba467eedf7b081a66fad024
+ languageName: node
+ linkType: hard
+
"postgres-array@npm:~2.0.0":
version: 2.0.0
resolution: "postgres-array@npm:2.0.0"
@@ -10246,6 +10720,13 @@ __metadata:
languageName: node
linkType: hard
+"readdirp@npm:^4.0.1":
+ version: 4.1.2
+ resolution: "readdirp@npm:4.1.2"
+ checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62
+ languageName: node
+ linkType: hard
+
"readdirp@npm:~3.6.0":
version: 3.6.0
resolution: "readdirp@npm:3.6.0"
@@ -10456,6 +10937,84 @@ __metadata:
languageName: node
linkType: hard
+"rollup@npm:^4.34.8, rollup@npm:^4.43.0":
+ version: 4.50.1
+ resolution: "rollup@npm:4.50.1"
+ dependencies:
+ "@rollup/rollup-android-arm-eabi": "npm:4.50.1"
+ "@rollup/rollup-android-arm64": "npm:4.50.1"
+ "@rollup/rollup-darwin-arm64": "npm:4.50.1"
+ "@rollup/rollup-darwin-x64": "npm:4.50.1"
+ "@rollup/rollup-freebsd-arm64": "npm:4.50.1"
+ "@rollup/rollup-freebsd-x64": "npm:4.50.1"
+ "@rollup/rollup-linux-arm-gnueabihf": "npm:4.50.1"
+ "@rollup/rollup-linux-arm-musleabihf": "npm:4.50.1"
+ "@rollup/rollup-linux-arm64-gnu": "npm:4.50.1"
+ "@rollup/rollup-linux-arm64-musl": "npm:4.50.1"
+ "@rollup/rollup-linux-loongarch64-gnu": "npm:4.50.1"
+ "@rollup/rollup-linux-ppc64-gnu": "npm:4.50.1"
+ "@rollup/rollup-linux-riscv64-gnu": "npm:4.50.1"
+ "@rollup/rollup-linux-riscv64-musl": "npm:4.50.1"
+ "@rollup/rollup-linux-s390x-gnu": "npm:4.50.1"
+ "@rollup/rollup-linux-x64-gnu": "npm:4.50.1"
+ "@rollup/rollup-linux-x64-musl": "npm:4.50.1"
+ "@rollup/rollup-openharmony-arm64": "npm:4.50.1"
+ "@rollup/rollup-win32-arm64-msvc": "npm:4.50.1"
+ "@rollup/rollup-win32-ia32-msvc": "npm:4.50.1"
+ "@rollup/rollup-win32-x64-msvc": "npm:4.50.1"
+ "@types/estree": "npm:1.0.8"
+ fsevents: "npm:~2.3.2"
+ dependenciesMeta:
+ "@rollup/rollup-android-arm-eabi":
+ optional: true
+ "@rollup/rollup-android-arm64":
+ optional: true
+ "@rollup/rollup-darwin-arm64":
+ optional: true
+ "@rollup/rollup-darwin-x64":
+ optional: true
+ "@rollup/rollup-freebsd-arm64":
+ optional: true
+ "@rollup/rollup-freebsd-x64":
+ optional: true
+ "@rollup/rollup-linux-arm-gnueabihf":
+ optional: true
+ "@rollup/rollup-linux-arm-musleabihf":
+ optional: true
+ "@rollup/rollup-linux-arm64-gnu":
+ optional: true
+ "@rollup/rollup-linux-arm64-musl":
+ optional: true
+ "@rollup/rollup-linux-loongarch64-gnu":
+ optional: true
+ "@rollup/rollup-linux-ppc64-gnu":
+ optional: true
+ "@rollup/rollup-linux-riscv64-gnu":
+ optional: true
+ "@rollup/rollup-linux-riscv64-musl":
+ optional: true
+ "@rollup/rollup-linux-s390x-gnu":
+ optional: true
+ "@rollup/rollup-linux-x64-gnu":
+ optional: true
+ "@rollup/rollup-linux-x64-musl":
+ optional: true
+ "@rollup/rollup-openharmony-arm64":
+ optional: true
+ "@rollup/rollup-win32-arm64-msvc":
+ optional: true
+ "@rollup/rollup-win32-ia32-msvc":
+ optional: true
+ "@rollup/rollup-win32-x64-msvc":
+ optional: true
+ fsevents:
+ optional: true
+ bin:
+ rollup: dist/bin/rollup
+ checksum: 10c0/2029282826d5fb4e308be261b2c28329a4d2bd34304cc3960da69fd21d5acccd0267d6770b1656ffc8f166203ef7e865b4583d5f842a519c8ef059ac71854205
+ languageName: node
+ linkType: hard
+
"rollup@npm:^4.34.9":
version: 4.40.2
resolution: "rollup@npm:4.40.2"
@@ -11278,6 +11837,15 @@ __metadata:
languageName: node
linkType: hard
+"strip-literal@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "strip-literal@npm:3.0.0"
+ dependencies:
+ js-tokens: "npm:^9.0.1"
+ checksum: 10c0/d81657f84aba42d4bbaf2a677f7e7f34c1f3de5a6726db8bc1797f9c0b303ba54d4660383a74bde43df401cf37cce1dff2c842c55b077a4ceee11f9e31fba828
+ languageName: node
+ linkType: hard
+
"styled-jsx@npm:5.1.6":
version: 5.1.6
resolution: "styled-jsx@npm:5.1.6"
@@ -11301,7 +11869,7 @@ __metadata:
languageName: node
linkType: hard
-"sucrase@npm:^3.20.3":
+"sucrase@npm:^3.20.3, sucrase@npm:^3.35.0":
version: 3.35.0
resolution: "sucrase@npm:3.35.0"
dependencies:
@@ -11444,13 +12012,12 @@ __metadata:
resolution: "test-opex-api@workspace:apps/test-opex-api"
dependencies:
"@pagopa/eslint-config": "npm:^5.0.0"
+ "@pagopa/opex-dashboard-ts": "workspace:^"
"@pagopa/typescript-config-node": "workspace:^"
"@types/node": "npm:^22.15.14"
cdktf: "npm:^0.21.0"
- cdktf-monitoring-stack: "workspace:*"
constructs: "npm:^10.4.2"
eslint: "npm:9.30.1"
- opex-common: "workspace:*"
prettier: "npm:^3.6.2"
typescript: "npm:^5.8.3"
languageName: unknown
@@ -11504,6 +12071,16 @@ __metadata:
languageName: node
linkType: hard
+"tinyglobby@npm:^0.2.11, tinyglobby@npm:^0.2.14, tinyglobby@npm:^0.2.15":
+ version: 0.2.15
+ resolution: "tinyglobby@npm:0.2.15"
+ dependencies:
+ fdir: "npm:^6.5.0"
+ picomatch: "npm:^4.0.3"
+ checksum: 10c0/869c31490d0d88eedb8305d178d4c75e7463e820df5a9b9d388291daf93e8b1eb5de1dad1c1e139767e4269fe75f3b10d5009b2cc14db96ff98986920a186844
+ languageName: node
+ linkType: hard
+
"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.13":
version: 0.2.13
resolution: "tinyglobby@npm:0.2.13"
@@ -11531,6 +12108,13 @@ __metadata:
languageName: node
linkType: hard
+"tinypool@npm:^1.1.1":
+ version: 1.1.1
+ resolution: "tinypool@npm:1.1.1"
+ checksum: 10c0/bf26727d01443061b04fa863f571016950888ea994ba0cd8cba3a1c51e2458d84574341ab8dbc3664f1c3ab20885c8cf9ff1cc4b18201f04c2cde7d317fff69b
+ languageName: node
+ linkType: hard
+
"tinyrainbow@npm:^2.0.0":
version: 2.0.0
resolution: "tinyrainbow@npm:2.0.0"
@@ -11545,6 +12129,13 @@ __metadata:
languageName: node
linkType: hard
+"tinyspy@npm:^4.0.3":
+ version: 4.0.3
+ resolution: "tinyspy@npm:4.0.3"
+ checksum: 10c0/0a92a18b5350945cc8a1da3a22c9ad9f4e2945df80aaa0c43e1b3a3cfb64d8501e607ebf0305e048e3c3d3e0e7f8eb10cea27dc17c21effb73e66c4a3be36373
+ languageName: node
+ linkType: hard
+
"tmp@npm:^0.0.33":
version: 0.0.33
resolution: "tmp@npm:0.0.33"
@@ -11572,13 +12163,11 @@ __metadata:
"@vitest/coverage-v8": "npm:^3.1.4"
azure-functions-core-tools: "npm:^4.0.7317"
cdktf: "npm:^0.21.0"
- cdktf-monitoring-stack: "workspace:*"
constructs: "npm:^10.4.2"
esbuild: "npm:^0.25.4"
eslint: "npm:9.30.1"
fp-ts: "npm:^2.16.10"
io-ts: "npm:^2.2.22"
- opex-common: "workspace:*"
prettier: "npm:^3.6.2"
shx: "npm:^0.4.0"
swagger-cli: "npm:^4.0.4"
@@ -11794,6 +12383,48 @@ __metadata:
languageName: node
linkType: hard
+"tsup@npm:^8.5.0":
+ version: 8.5.0
+ resolution: "tsup@npm:8.5.0"
+ dependencies:
+ bundle-require: "npm:^5.1.0"
+ cac: "npm:^6.7.14"
+ chokidar: "npm:^4.0.3"
+ consola: "npm:^3.4.0"
+ debug: "npm:^4.4.0"
+ esbuild: "npm:^0.25.0"
+ fix-dts-default-cjs-exports: "npm:^1.0.0"
+ joycon: "npm:^3.1.1"
+ picocolors: "npm:^1.1.1"
+ postcss-load-config: "npm:^6.0.1"
+ resolve-from: "npm:^5.0.0"
+ rollup: "npm:^4.34.8"
+ source-map: "npm:0.8.0-beta.0"
+ sucrase: "npm:^3.35.0"
+ tinyexec: "npm:^0.3.2"
+ tinyglobby: "npm:^0.2.11"
+ tree-kill: "npm:^1.2.2"
+ peerDependencies:
+ "@microsoft/api-extractor": ^7.36.0
+ "@swc/core": ^1
+ postcss: ^8.4.12
+ typescript: ">=4.5.0"
+ peerDependenciesMeta:
+ "@microsoft/api-extractor":
+ optional: true
+ "@swc/core":
+ optional: true
+ postcss:
+ optional: true
+ typescript:
+ optional: true
+ bin:
+ tsup: dist/cli-default.js
+ tsup-node: dist/cli-node.js
+ checksum: 10c0/2eddc1138ad992a2e67d826e92e0b0c4f650367355866c77df8368ade9489e0a8bf2b52b352e97fec83dc690af05881c29c489af27acb86ac2cef38b0d029087
+ languageName: node
+ linkType: hard
+
"tsutils@npm:^3.21.0":
version: 3.21.0
resolution: "tsutils@npm:3.21.0"
@@ -11995,6 +12626,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@npm:^5.0.0":
+ version: 5.9.2
+ resolution: "typescript@npm:5.9.2"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10c0/cd635d50f02d6cf98ed42de2f76289701c1ec587a363369255f01ed15aaf22be0813226bff3c53e99d971f9b540e0b3cc7583dbe05faded49b1b0bed2f638a18
+ languageName: node
+ linkType: hard
+
"typescript@npm:^5.8.3, typescript@npm:~5.8.3":
version: 5.8.3
resolution: "typescript@npm:5.8.3"
@@ -12015,6 +12656,16 @@ __metadata:
languageName: node
linkType: hard
+"typescript@patch:typescript@npm%3A^5.0.0#optional!builtin":
+ version: 5.9.2
+ resolution: "typescript@patch:typescript@npm%3A5.9.2#optional!builtin::version=5.9.2&hash=5786d5"
+ bin:
+ tsc: bin/tsc
+ tsserver: bin/tsserver
+ checksum: 10c0/34d2a8e23eb8e0d1875072064d5e1d9c102e0bdce56a10a25c0b917b8aa9001a9cf5c225df12497e99da107dc379360bc138163c66b55b95f5b105b50578067e
+ languageName: node
+ linkType: hard
+
"typescript@patch:typescript@npm%3A^5.8.3#optional!builtin, typescript@patch:typescript@npm%3A~5.8.3#optional!builtin":
version: 5.8.3
resolution: "typescript@patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"
@@ -12025,6 +12676,13 @@ __metadata:
languageName: node
linkType: hard
+"ufo@npm:^1.6.1":
+ version: 1.6.1
+ resolution: "ufo@npm:1.6.1"
+ checksum: 10c0/5a9f041e5945fba7c189d5410508cbcbefef80b253ed29aa2e1f8a2b86f4bd51af44ee18d4485e6d3468c92be9bf4a42e3a2b72dcaf27ce39ce947ec994f1e6b
+ languageName: node
+ linkType: hard
+
"ulid@npm:^2.3.0":
version: 2.4.0
resolution: "ulid@npm:2.4.0"
@@ -12248,6 +12906,21 @@ __metadata:
languageName: node
linkType: hard
+"vite-node@npm:3.2.4":
+ version: 3.2.4
+ resolution: "vite-node@npm:3.2.4"
+ dependencies:
+ cac: "npm:^6.7.14"
+ debug: "npm:^4.4.1"
+ es-module-lexer: "npm:^1.7.0"
+ pathe: "npm:^2.0.3"
+ vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ bin:
+ vite-node: vite-node.mjs
+ checksum: 10c0/6ceca67c002f8ef6397d58b9539f80f2b5d79e103a18367288b3f00a8ab55affa3d711d86d9112fce5a7fa658a212a087a005a045eb8f4758947dd99af2a6c6b
+ languageName: node
+ linkType: hard
+
"vite@npm:^5.0.0 || ^6.0.0":
version: 6.3.5
resolution: "vite@npm:6.3.5"
@@ -12303,6 +12976,61 @@ __metadata:
languageName: node
linkType: hard
+"vite@npm:^5.0.0 || ^6.0.0 || ^7.0.0-0":
+ version: 7.1.5
+ resolution: "vite@npm:7.1.5"
+ dependencies:
+ esbuild: "npm:^0.25.0"
+ fdir: "npm:^6.5.0"
+ fsevents: "npm:~2.3.3"
+ picomatch: "npm:^4.0.3"
+ postcss: "npm:^8.5.6"
+ rollup: "npm:^4.43.0"
+ tinyglobby: "npm:^0.2.15"
+ peerDependencies:
+ "@types/node": ^20.19.0 || >=22.12.0
+ jiti: ">=1.21.0"
+ less: ^4.0.0
+ lightningcss: ^1.21.0
+ sass: ^1.70.0
+ sass-embedded: ^1.70.0
+ stylus: ">=0.54.8"
+ sugarss: ^5.0.0
+ terser: ^5.16.0
+ tsx: ^4.8.1
+ yaml: ^2.4.2
+ dependenciesMeta:
+ fsevents:
+ optional: true
+ peerDependenciesMeta:
+ "@types/node":
+ optional: true
+ jiti:
+ optional: true
+ less:
+ optional: true
+ lightningcss:
+ optional: true
+ sass:
+ optional: true
+ sass-embedded:
+ optional: true
+ stylus:
+ optional: true
+ sugarss:
+ optional: true
+ terser:
+ optional: true
+ tsx:
+ optional: true
+ yaml:
+ optional: true
+ bin:
+ vite: bin/vite.js
+ checksum: 10c0/782d2f20c25541b26d1fb39bef5f194149caff39dc25b7836e25f049ca919f2e2ce186bddb21f3f20f6195354b3579ec637a8ca08d65b117f8b6f81e3e730a9c
+ languageName: node
+ linkType: hard
+
"vitest-mock-extended@npm:^3.1.0":
version: 3.1.0
resolution: "vitest-mock-extended@npm:3.1.0"
@@ -12369,6 +13097,62 @@ __metadata:
languageName: node
linkType: hard
+"vitest@npm:^3.2.4":
+ version: 3.2.4
+ resolution: "vitest@npm:3.2.4"
+ dependencies:
+ "@types/chai": "npm:^5.2.2"
+ "@vitest/expect": "npm:3.2.4"
+ "@vitest/mocker": "npm:3.2.4"
+ "@vitest/pretty-format": "npm:^3.2.4"
+ "@vitest/runner": "npm:3.2.4"
+ "@vitest/snapshot": "npm:3.2.4"
+ "@vitest/spy": "npm:3.2.4"
+ "@vitest/utils": "npm:3.2.4"
+ chai: "npm:^5.2.0"
+ debug: "npm:^4.4.1"
+ expect-type: "npm:^1.2.1"
+ magic-string: "npm:^0.30.17"
+ pathe: "npm:^2.0.3"
+ picomatch: "npm:^4.0.2"
+ std-env: "npm:^3.9.0"
+ tinybench: "npm:^2.9.0"
+ tinyexec: "npm:^0.3.2"
+ tinyglobby: "npm:^0.2.14"
+ tinypool: "npm:^1.1.1"
+ tinyrainbow: "npm:^2.0.0"
+ vite: "npm:^5.0.0 || ^6.0.0 || ^7.0.0-0"
+ vite-node: "npm:3.2.4"
+ why-is-node-running: "npm:^2.3.0"
+ peerDependencies:
+ "@edge-runtime/vm": "*"
+ "@types/debug": ^4.1.12
+ "@types/node": ^18.0.0 || ^20.0.0 || >=22.0.0
+ "@vitest/browser": 3.2.4
+ "@vitest/ui": 3.2.4
+ happy-dom: "*"
+ jsdom: "*"
+ peerDependenciesMeta:
+ "@edge-runtime/vm":
+ optional: true
+ "@types/debug":
+ optional: true
+ "@types/node":
+ optional: true
+ "@vitest/browser":
+ optional: true
+ "@vitest/ui":
+ optional: true
+ happy-dom:
+ optional: true
+ jsdom:
+ optional: true
+ bin:
+ vitest: vitest.mjs
+ checksum: 10c0/5bf53ede3ae6a0e08956d72dab279ae90503f6b5a05298a6a5e6ef47d2fd1ab386aaf48fafa61ed07a0ebfe9e371772f1ccbe5c258dd765206a8218bf2eb79eb
+ languageName: node
+ linkType: hard
+
"vue-eslint-parser@npm:^9.4.3":
version: 9.4.3
resolution: "vue-eslint-parser@npm:9.4.3"
@@ -12772,3 +13556,10 @@ __metadata:
checksum: 10c0/53cc090b949ed1776e241fb882d5de0f66096727aabbfb53ff9c64c875b575c9d3cbd75556bc816bcdd09aff0a26608e1c40d79dd863ab5c31a96331f1e7a4b9
languageName: node
linkType: hard
+
+"zod@npm:^4.1.5":
+ version: 4.1.5
+ resolution: "zod@npm:4.1.5"
+ checksum: 10c0/7826fb931bc71d4d0fff2fbb72f1a1cf30a6672cf9dbe6933a216bbb60242ef1c3bdfbcd3c5b27e806235a35efaad7a4a9897ff4d3621452f9ea278bce6fd42a
+ languageName: node
+ linkType: hard